Golang の database/sql パッケージ を使用すると、Postgres、MariaDB (MySQL)、SQLite といった RDB 系のデータベースを共通のインタフェースで操作することができます。
database/sql
を使ってコーディングしておけば、将来的な RDBMS の乗り換えが容易になります。
ドライバーのインストール
database/sql
はデータベース操作用の抽象化レイヤーを提供するだけなので、実際にデータベースに接続するには、それぞれのデータベースごとのドライバーが必要です。
ドライバーは SQLDrivers の一覧ページ から好きなものを選択します。
例えば、mattn 氏の SQLite 用ドライバーを使う場合は、次のように go.mod
の依存関係を更新し、
$ go get github.com/mattn/go-sqlite3
Go プログラム内で次のようにインポートしておきます(先にインポート文を書いてから go get .
とする方法もあります)。
go mod tidy
時にこの行が削除されないようにしておく必要があります。
インポート行が削除されてしまうと、database/sql
パッケージがドライバーを見つけられれず、unknown driver "sqlite3" (forgotten import?)
といったエラーが発生します。データベースへの接続 (sql.Open, DB.Ping)
データベースに接続(ドライバーをオープン)するには、sql.Open 関数 を使用します。
func Open(driverName, dataSourceName string) (*sql.DB, error)
第 1 引数には使用するドライバーの名前(例: mysql
、sqlite3
)、第 2 引数にはドライバーごとの接続文字列 (DSN: data source name) を指定します。
SQLite3 の場合は、接続文字列はデータベースファイル名なので、とてもシンプルです。
db, err := sql.Open("sqlite3", "./books.db")
sql.Open()
が返す *sql.DB
インスタンスを使って、データベースの各種操作を行うことになります。
package main
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// データベースをオープン
db, err := sql.Open("sqlite3", "./books.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// ... データベースを操作 ...
}
SQLite の場合は接続文字列はシンプル(ファイル名のみ)ですが、MySQL や Postgres などでは複雑な接続文字列を指定する必要があります。 そのため、データベースドライバーによっては、接続文字列を構築するためのユーティリティ関数が提供されていることがあります。
データベースのオープンに成功したら、(*sql.DB) Ping
メソッドを実行することで、実際にデーターベースが操作可能な状態になっているかを確認できます(実際のアプリで Ping
メソッドを呼び出す必要はありません)。
func checkIfDatabaseIsReady(db *sql.DB) {
if err := db.Ping(); err != nil {
log.Fatal(err)
}
log.Println("Database is ready")
}
CRUD 操作 (DB.Query, DB.Exec)
データベースからレコードを取得するには (*sql.DB) Query
系メソッド、その他の更新操作には (*sql.DB) Exec
系メソッドを使用します。
- レコードの取得 (SELECT)
func (db *DB) Query(query string, args ...any) (*Rows, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (db *DB) QueryRow(query string, args ...any) *Row
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
- 更新操作 (CREATE TABLE, ALTER TABLE, DROP TABLE, INSERT, UDPATE, DELETE)
func (db *DB) Exec(query string, args ...any) (Result, error)
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
CREATE TABLE
次の例では、簡単な books
テーブルを作成しています。
更新操作なので、(*sql.DB) Exec
/ (*sql.DB) ExecContext
メソッドを使用します。
func createBooksTable(db *sql.DB) {
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS books(
id TEXT PRIMARY KEY NOT NULL,
title TEXT NOT NULL,
price INTEGER NOT NULL)`)
if err != nil {
log.Fatal(err)
}
}
INSERT
テーブルにレコードを追加するときも、(*sql.DB) Exec
/ (*sql.DB) ExecContext
メソッドを使用します。
func insertSampleRecord(db *sql.DB) {
// INSERT の実行
query := `INSERT INTO books (id, title, price) VALUES (?, ?, ?)`
result, err := db.Exec(query, "id-1", "Title 1", 1000)
if err != nil {
log.Fatal(err)
}
// 挿入されたレコード数を取得
count, err := result.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("%d rows inserted", count)
}
同様のクエリで複数のレコードを登録する場合は、(*sql.DB) Prepare
メソッドで、Prepared statement (sql.Stmt
) を生成すると便利です。
func insertSampleRecords(db *sql.DB) {
// Prepared statement を作成する
stmt, err := db.Prepare("INSERT INTO books (id, title, price) VALUES (?, ?, ?)")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
// 複数のレコードを追加する
for i := 1; i <= 3; i++ {
id := fmt.Sprintf("id-%d", i)
title := fmt.Sprintf("Title %d", i)
price := 1000 * i
_, err := stmt.Exec(id, title, price)
if err != nil {
log.Fatal(err)
}
}
}
SELECT
SELECT
でレコードを取得するときは、(*sql.DB) Query
/ (*sql.DB) QueryContext
メソッドを使用します。
戻り値の *sql.Rows
で取得したレコードを参照できます。
func queryBooks(db *sql.DB) {
// クエリ実行
rows, err := db.Query("SELECT id, title, price FROM books")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// レコードを 1 件ずつ取り出す
for rows.Next() {
var id string
var title string
var price int64
if err := rows.Scan(&id, &title, &price); err != nil {
log.Fatal(err)
}
log.Printf("%s, %s, %d\n", id, title, price)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
}
取得するレコードが 1 件だけだとわかっている場合は、効率のよい (*sql.DB) QueryRow
/ (*sql.DB) QueryRowContext
メソッドを使用します。
これらのメソッドは、1 件のレコードを参照するための *sql.Row
を返します。
このメソッドは必ず成功し、エラーが発生することはありません(Scan
時のエラーを確認するだけで十分だからです)。
func queryBook(db *sql.DB) {
bookId := "id-1"
row := db.QueryRow("SELECT title, price FROM books WHERE id = ?", bookId)
var title string
var price int64
if err := row.Scan(&title, &price); err != nil {
if err == sql.ErrNoRows {
log.Fatalf("Book not found (id=%s)\n", bookId)
}
log.Fatal(err)
}
log.Printf("%s, %d", title, price)
}
WHERE 条件に一致するレコードが複数ある場合は、(*sql.DB) QueryRow
メソッドは最初のレコードのみを返します。
条件に一致するレコードが見つからない場合は、(*sql.Row) Scan
を呼び出したときに sql.ErrNoRows
が返されます。
トランザクション処理 (DB.BeginTx)
データベースのトランザクションは、複数の更新要求をアトミックに処理するための仕組みです。
複数の更新処理を一括でコミットするか、すべてなかったことにすることができます(ロールバック)。
(*sql.DB) BeginTx
メソッドを使うとトランザクション処理を実行するための *sqlTx
インスタンスを取得できます。
func (*sql.DB).BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
レコードの更新を行うときに、(*sql.DB) Exec
の代わりに (*sql.Tx) Exec
を呼び出すことで、その更新処理は 1 つのトランザクション内での処理とみなされます。
func updateRecordsWithTransaction(db *sql.DB) error {
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
return err
}
defer tx.Rollback() // コミットしなかった場合は自動でロールバック
// 関連する更新処理をトランザクション内で実行する
if _, err := tx.Exec("...省略..."); err != nil {
return err
}
if _, err := tx.Exec("...省略..."); err != nil {
return err
}
// トランザクション処理をコミット
if err := tx.Commit(); err != nil {
return err
}
return nil
}
一連の処理が終わったあとに、(*sql.Tx) Commit
メソッドか、(*sql.Tx) Rollback
メソッドを呼び出す必要があります。
上記のようにトランザクション開始直後に Rollback
を defer
呼び出ししておけば、関数内で Commit
が呼ばれなかったときに自動でロールバックしてくれます(Commit
が呼ばれた場合は、Rollback
は実行されません)。
NULL 値を含むレコードを扱う
テーブルスキーマで NOT NULL
宣言されていないカラムには、NULL 値が格納されている可能性があります。
NULL 値を含むレコードを Scan
するときに、バッファーとして string
や int64
などのプリミティブな変数を使用するとエラーが発生します。
Scan error on column index 2, name “price”: converting NULL to int64 is unsupported
NULL 値を含む可能性があるレコードを Scan
する場合は、次のような NULL 値を扱える専用の型を使用します。
これらの型は、値が NULL でないことを調べるための Valid
プロパティを持っています。
Valid
が true
の場合は、各カラムの値を安全に参照できます。
func queryBook(db *sql.DB) error {
id := "id-1"
row := db.QueryRow("SELECT title, price FROM books WHERE id = ?", id)
// NULL 値を考慮した Scan
var title sql.NullString
var price sql.NullInt64
if err := row.Scan(&title, &price); err != nil {
return err
}
// Nullable な title カラムを参照する
if title.Valid {
log.Printf("title = %s\n", title.String)
} else {
log.Println("title is NULL")
}
// Nullable な price カラムを参照する
if price.Valid {
log.Printf("price = %d\n", price.Int64)
} else {
log.Println("price is NULL")
}
return nil
}