No.
2023-10-20
  • Jan
  • Feb
  • Mar
  • Apr
  • May
  • Jun
  • Jul
  • Aug
  • Sep
  • Oct
  • Nov
  • Dec
  • Sun
  • Mon
  • Tue
  • Wed
  • Thu
  • Fri
  • Sat
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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.Opendb.PingContextの2つ
    • sql.OpenはSQL Driverによるが必ずコネクションを張るかはわからない
      • 実際MySQL Driverはコネクションを張っていない
    • 疎通確認は必ずPing/PingContextで確認する
	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でトランザクションをロールバックする