はじめに
Go言語にはデータベース操作のための標準ライブラリdatabase/sqlが用意されています。
ORMフレームワーク(GORMなど)を使わずに生SQLを書くことで、以下のメリットがあります。
- SQLの理解が深まる
- 処理速度が速い
- 外部依存を最小限に抑えられる
この記事では、database/sql + SQLiteを使ったCRUD操作を一から解説します。
環境
| 項目 | バージョン |
|---|---|
| Go | 1.21以上 |
| SQLite ドライバー | github.com/mattn/go-sqlite3 |
1. プロジェクトのセットアップ
mkdir go-sqlite-sample
cd go-sqlite-sample
go mod init go-sqlite-sample
go get github.com/mattn/go-sqlite3
go-sqlite3はSQLiteのCバインディングです。インストール時にCコンパイラ(GCC)が必要です。Windowsの場合はTDM-GCC等を事前にインストールしてください。
2. database/sqlの構成を理解する
Go標準ライブラリ
└── database/sql ← DB操作の窓口(インターフェース)
↓
ドライバー(DB種別ごとに1つだけ外部パッケージが必要)
└── github.com/mattn/go-sqlite3 ← SQLite用ドライバー
database/sqlはGoの標準ライブラリです。DB種別ごとのドライバーを差し替えるだけで、MySQLやPostgreSQLにも同じ書き方で対応できます。
3. DB接続
package main
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3" // 副作用インポート
)
func main() {
db, err := sql.Open("sqlite3", "./sample.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
_ "github.com/mattn/go-sqlite3"の_は副作用インポートと呼ばれます。直接使用しないが、ドライバーをGo内部に登録するために必要な記述です。省略するとコンパイルエラーになります。
4. テーブル作成
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
log.Fatal(err)
}
IF NOT EXISTSをつけることで、テーブルが既に存在してもエラーにならずに済みます。
5. CRUD操作
5-1. Create(レコード追加)
_, err = db.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"田中太郎", "taro@example.com", 27,
)
if err != nil {
log.Fatal(err)
}
プレースホルダー(?)を必ず使う
値を文字列で直接埋め込む書き方("... VALUES ('" + name + "', ...")はSQLインジェクションの危険があります。?を使うことで安全にクエリを実行できます。
5-2. Read(レコード取得)
複数件取得
rows, err := db.Query("SELECT id, name, email, age FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close() // 必ずCloseする
for rows.Next() {
var id, age int
var name, email string
err := rows.Scan(&id, &name, &email, &age)
if err != nil {
log.Fatal(err)
}
fmt.Printf("ID:%d Name:%s Email:%s Age:%d\n", id, name, email, age)
}
// rows.Next()後のエラーチェックも忘れずに
if err = rows.Err(); err != nil {
log.Fatal(err)
}
1件取得
var name string
var age int
err = db.QueryRow(
"SELECT name, age FROM users WHERE id = ?", 1,
).Scan(&name, &age)
if err == sql.ErrNoRows {
fmt.Println("レコードが見つかりませんでした")
} else if err != nil {
log.Fatal(err)
}
fmt.Printf("Name:%s Age:%d\n", name, age)
QueryRowは該当レコードがない場合にsql.ErrNoRowsを返します。err != nilだけでなく、sql.ErrNoRowsのチェックを必ず行いましょう。
5-3. Update(レコード更新)
result, err := db.Exec(
"UPDATE users SET age = ? WHERE id = ?",
28, 1,
)
if err != nil {
log.Fatal(err)
}
// 更新件数の確認
rowsAffected, _ := result.RowsAffected()
fmt.Printf("更新件数: %d件\n", rowsAffected)
5-4. Delete(レコード削除)
result, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
if err != nil {
log.Fatal(err)
}
rowsAffected, _ := result.RowsAffected()
fmt.Printf("削除件数: %d件\n", rowsAffected)
6. トランザクション
複数の操作をまとめて行い、すべて成功したときだけコミットする仕組みです。
// トランザクション開始
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
// エラー時は自動ロールバック
defer func() {
if err != nil {
tx.Rollback()
}
}()
// トランザクション内での操作
_, err = tx.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"テストユーザー", "test@example.com", 30,
)
if err != nil {
return // deferでRollbackが実行される
}
_, err = tx.Exec(
"UPDATE users SET age = ? WHERE name = ?",
31, "テストユーザー",
)
if err != nil {
return // deferでRollbackが実行される
}
// すべて成功したらコミット
err = tx.Commit()
if err != nil {
log.Fatal(err)
}
fmt.Println("トランザクション完了")
7. よく使うメソッドの使い分け
| メソッド | 用途 | 戻り値 |
|---|---|---|
db.Exec() |
INSERT / UPDATE / DELETE |
Result, error
|
db.Query() |
複数行のSELECT |
*Rows, error
|
db.QueryRow() |
1行のSELECT | *Row |
db.Begin() |
トランザクション開始 |
*Tx, error
|
8. 全体コード
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// DB接続
db, err := sql.Open("sqlite3", "./sample.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// テーブル作成
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
log.Fatal(err)
}
// INSERT
_, err = db.Exec(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
"田中太郎", "taro@example.com", 27,
)
if err != nil {
log.Fatal(err)
}
// SELECT(全件)
rows, err := db.Query("SELECT id, name, email, age FROM users")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id, age int
var name, email string
rows.Scan(&id, &name, &email, &age)
fmt.Printf("ID:%d Name:%s Email:%s Age:%d\n", id, name, email, age)
}
// UPDATE
db.Exec("UPDATE users SET age = ? WHERE id = ?", 28, 1)
// DELETE
db.Exec("DELETE FROM users WHERE id = ?", 1)
fmt.Println("完了")
}
まとめ
| 操作 | 使うメソッド | ポイント |
|---|---|---|
| INSERT / UPDATE / DELETE | db.Exec() |
プレースホルダー?を使う |
| 複数件SELECT |
db.Query() + rows.Next()
|
defer rows.Close()を忘れずに |
| 1件SELECT | db.QueryRow() |
sql.ErrNoRowsのチェックを行う |
| まとめて操作 |
db.Begin() → tx.Commit()
|
エラー時はtx.Rollback()
|
さいごに
database/sqlは生SQLを書くため、最初は少し冗長に感じるかもしれません。しかしSQLの理解が直接コードに反映されるため、データベースの学習と実装を同時に進めることができます。
GORMなどのORMに移行する場合も、database/sqlの知識が土台になります。まずは生SQLで基礎を固めることをおすすめします。