database/sql
TL;DR
- database/sqlを使用する際は同時に使いたいDBのSQL Driverもインポートする
- 疎通確認はPingを使う
- 発行したいクエリに合わせてメソッドやトランザクションをうまく利用する
1. 基本
database/sql
とは、Goが提供するDB操作を行うための標準パッケージ- 実際に利用する場合はdatabase/sqlと使用したいDBのSQL Driverをインポートしdatabase/sqlを介して利用する
- database/sqlはコネクションプール(=一度確立したコネクションを使い回す手法)の管理等を行い特定のDBへの実装は提供していない
- 提供しているのはinterface
- 具体的な実装はinterfaceを満たしたSQL Driverが行う
- ORMもdatabase/sqlをラップする形で実装されている
- GORM:database/sqlを満たすようにSQL Driverを提供している
- Ent:database/sqlの実装をラップして拡張している
1.1 blank import
インポート宣言は、インポートするパッケージとインポートされるパッケージの依存関係を宣言するものである。
パッケージがそれ自身を直接または間接的にインポートすることや、エクスポートされた識別子を一切参照せずにパッケージを直接インポートすることは間違った使い方です。
パッケージの副作用(初期化)のためだけにパッケージをインポートするには、明示的なパッケージ名として空白の識別子を使用します。
(出典:https://go.dev/ref/spec#Import_declarations)
import (
"context"
"database/sql"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/sample?charset=utf8&parseTime=true")
if err != nil {
log.Printf("failed to open a db err = %s", err.Error())
return
}
.
.
.
どういうことか:
database/sql
では、db, err :=sql.Open("mysql", ...)
のように第一引数にDBエンジン名を渡すだけでDBの指定ができるdatabase/sql
では、特定のDBエンジンとの接続を行う際に、そのDBのドライバを介して接続している- もし、blank importによる宣言が不要だった場合、
database/sql
は暗黙的にmysqlドライバをインポートしていることになる- mainのコードを見るだけではどのドライバを使っているのかわからない
- -> 明示的に宣言して、依存関係をわかりやすくした方がいいよね!ってこと
- この仕組みのため、依存関係を知りたいときはimport分の中を見るだけでいいので親切
- 各種DBのドライバをインポートするときはblank importでインポートする
- パッケージ名の前に
_
を加えることでblank importできる - ちなみに普通にimportすると
github.com/go-sql-driver/mysql" imported and not used
とエラーが出てしまう。 - ドライバはmain関数の中で使われないので当然と言えば当然
- パッケージ名の前に
じゃあどのタイミングで使われているの?
- 依存先のパッケージ(
"github.com/go-sql-driver/mysql"
)のinit関数- Goのinitは組み込みの初期化関数
- 依存先のパッケージにinitがある場合先に依存先が解決される
- initを同一パッケージで複数書いた場合ファイルの上から順番に解決される
database/sql
がinterfaceの具象実装をSQL Driverから呼び出せるようにinitしている
1.2 コネクション
- コネクションを張るタイミングは、
sql.Open
とdb.PingContext
の2つ- sql.OpenはSQL Driverによるが必ずコネクションを張るかはわからない
- 実際MySQL Driverはコネクションを張っていない
- 疎通確認は必ずPing/PingContextで確認する
- sql.OpenはSQL Driverによるが必ずコネクションを張るかはわからない
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/sample?charset=utf8&parseTime=true")
if err != nil {
log.Printf("failed to open a db err = %s", err.Error())
return
}
if err := db.PingContext(context.Background()); err != nil {
log.Printf("failed to ping err = %s", err.Error())
return
}
1.3 レコードの取得・操作
- 複数レコードを取得する場合は
Query/QueryContext
を利用する- 返り値としてRowsが取得できる
- Rows.NextでScanするレコードを確認する
- Rows.Scanでデータを特定の変数、structのフィールドにマッピングする
- 単一レコードを取得する場合は
QueryRow/QueryRowContext
を利用する- 返り値としてRowが取得できる
- Row.Scanでデータを特定の変数、structのフィールドにマッピングする
- INSERT/UPDATE/DELETE etc..は
Exec/ExecContext
を利用する
1.4 トランザクション
- トランザクションはBegin/BeginTxで取得する
- Query/QueryRow/Execなどを同じように使える
- Commitでトランザクションをコミットする
- Rollbackでトランザクションをロールバックする