これは、なにをしたくて書いたもの?
Goを使って、データベースにアクセスするコードを書いてみたいなぁと思いまして。
sqlパッケージ
Goでデータベースにアクセスするには、sqlパッケージを使うようです。
sql - The Go Programming Language
sqlパッケージは、SQL(ライクな)データベースにアクセスするための、汎用インターフェースを提供するパッケージだそうです。
Package sql provides a generic interface around SQL (or SQL-like) databases.
基本的な使い方は、こちらを参照。
SQLInterface · golang/go Wiki · GitHub
そして、sqlパッケージのインターフェースを実装したドライバーは、こちらの一覧で確認できます。
SQLDrivers · golang/go Wiki · GitHub
今回は、MySQLのドライバーを使いたいと思います。
環境
今回の環境は、こちらです。
$ go version go version go1.16.2 linux/amd64
MySQLは8.0.23を使い、172.17.0.2で動作しているものとします。
確認用のプロジェクト。
$ go mod init mysql-example go: creating new go.mod: module mysql-example
動作確認は、テストコードで行うことにします。
$ go get github.com/stretchr/testify
ここに、MySQLのドライバーを加えたgo.modはこちらです。
go.mod
module mysql-example go 1.16 require ( github.com/go-sql-driver/mysql v1.5.0 // indirect github.com/stretchr/testify v1.7.0 // indirect )
GoのMySQLドライバーをインストールする
インストールは、go getすればOKです。
Go-MySQL-Driver / Installation
$ go get github.com/go-sql-driver/mysql
先述したgo.modに記載の通り、今回はv1.5.0を使います。
テストコードの雛形
まずは、テストコードの雛形を載せておきます。
main_test.go
package main import ( "context" "database/sql" "testing" _ "github.com/go-sql-driver/mysql" "github.com/stretchr/testify/assert" ) // ここに、テストコードを書く
MySQLドライバーを使う
sqlのドライバーを使う時のimportの書き方は、こちらみたいですね。
import ( _ "github.com/go-sql-driver/mysql" )
この書き方は、副作用を目的としてimportする場合に使うようです。
This table illustrates how Sin is accessed in files that import the package after the various types of import declaration.
The Go Programming Language Specification / Import declarations
importしたパッケージ自体は、ソースコード内では使いません。使うのはsqlパッケージ側です。
では、使っていってみましょう。
ドライバーが認識されているかを確認する
importしただけで、sql#Driversが認識しているドライバーの名前を返すようになります。
func TestGetRegisteredDriver(t *testing.T) { assert.Equal(t, []string{"mysql"}, sql.Drivers()) }
sqlパッケージがドライバーを認識しているかどうか、確認するのに良さそうですね。
データベースに接続する
データベースに接続するには、sql#Openを使います。
func TestPingMySql(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) assert.NotNil(t, db) defer db.Close() err = db.Ping() assert.Nil(t, err) }
sql#Openの第1引数はドライバー名、第2引数はdataSourceName、DSNとも略されるみたいですね(?)を指定します。
db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice")
Go-MySQL-Driver / DSN (Data Source Name)
アドレスの部分とか、最初ちょっとピンとこなかったです…。
アドレスは()で囲むんですねぇ。
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
sql#Openの結果としてDBが返ってくるのですが、今回は終了時にDB#Closeするようにしています。
ですが、ドキュメントを見ていると、通常は自分でクローズすることはなさそうですね。
The returned DB is safe for concurrent use by multiple goroutines and maintains its own pool of idle connections. Thus, the Open function should be called just once. It is rarely necessary to close a DB.
最後にpingして、接続確認。
err = db.Ping()
接続時にパラメーターを指定する
MySQLドライバーのDSNには、パラメーターを付与できます。
※他のドライバーは見ていないので、わかりません
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
QueryStringな形式ですね。
こんな感じで。
func TestConnectMysqlParameter(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice?charset=utf8mb4&interpolateParams=true") assert.Nil(t, err) defer db.Close() err = db.Ping() assert.Nil(t, err) }
今回の例ではcharsetとinterpolateParamsを指定しています。
ところで、charsetは非推奨で、collationを使った方がよいという話のようです。
なのですが、utf8mb4_ja_0900_as_cs_ksみたいなCollationを指定すると、以下のようにエラーになったりします。
unknown collation
ここに定義されているCollationでないと、ダメそうですねぇ…。
https://github.com/go-sql-driver/mysql/blob/v1.5.0/collations.go
https://github.com/go-sql-driver/mysql/blob/v1.5.0/packets.go#L342-L349
余談でした。
DDLを実行してみる
テーブルのcreate & dropをしてみましょう。DB#Execを使うようです。
func TestExecuteDDL(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
これ以降は、このcreate & dropで挟み込む感じで書いていきます。
select文やinsert文を実行してみる
次は、select文やinsert文を実行してみましょう。
func TestExecuteQueryUsingInterpolateParams(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice?interpolateParams=true") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) // insert文やselect文を書く _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
まずはinsert文から。クエリではない場合は、DB#Execを使います。パラメーターは?でバインドさせるようです。
// insert result, err := db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) assert.Nil(t, err) rowsAffected, err := result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) result, err = db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) result, err = db.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected)
Resultからは、影響のあった行数を取得できます。また、auto incrementなどを使っている場合は、LastInsertIdでデータベースが
生成したIDを取得できるようです。
rowsAffected, err := result.RowsAffected()
assert.Nil(t, err)
assert.Equal(t, int64(1), rowsAffected)
続いて、クエリです。1行取得すればいいものは、DB#QueryRowを使います。結果は、Row型で返ります。
// query row row := db.QueryRow(`select count(*) from book`) assert.Nil(t, row.Err()) var count int row.Scan(&count) assert.Equal(t, 3, count) row = db.QueryRow(`select * from book where isbn = ?`, "978-4798161488") assert.Nil(t, row.Err()) var isbn, name string var price int row.Scan(&isbn, &name, &price) assert.Equal(t, "978-4798161488", isbn) assert.Equal(t, "MySQL徹底入門 第4版", name) assert.Equal(t, 4180, price)
値の取得は、Row#Scanで行います。
var isbn, name string var price int row.Scan(&isbn, &name, &price)
複数行が返る可能性がある場合は、DB#Queryですね。この場合は、Rowsが返ってきます。
// query rows rows, err := db.Query(`select title from book where price > ? order by price desc`, 4000) assert.Nil(t, err) names := []string{} for rows.Next() { var name string err := rows.Scan(&name) assert.Nil(t, err) names = append(names, name) } assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版", "MySQL徹底入門 第4版"}, names)
結果セットから取得する行を進めるにはRows#Nextを
for rows.Next() {
値の取得は、Rowと同様にRows#Scanを使います。
var name string err := rows.Scan(&name)
RowsはCloseメソッドを備えているのですが、取得する行がなくなると自動的にクローズされるとは書かれています。
if Next is called and returns false and there are no further result sets, the Rows are closed automatically and it will suffice to check the result of Err.
Package sql / func (*Rows) Close
ところで、パラメーターをバインドする際に?を使っているのですが、これにはinterpolateParams=trueと指定する必要が
あるようです。
Go-MySQL-Driver / / interpolateParams
なのですが、このパラメーターを指定しなくても動作するような…?
あと、Named Parameterはサポートしていませんでした。
https://github.com/go-sql-driver/mysql/blob/v1.5.0/utils.go#L676-L686
DB#Prepareを使う場合
パラメーターをバインドする際に、ちゃんと手続きを踏む場合はDB#PrepareでStmtを取得し、Stmt#ExecやStmt#Queryを
使用します。
// insert stmt, err := db.Prepare(`insert into book(isbn, title, price) values(?, ?, ?)`) assert.Nil(t, err) result, err := stmt.Exec("978-4798161488", "MySQL徹底入門 第4版", 4180)
使い終わらったらStmt#Close。
stmt.Close()
先ほどの例を、DB#ExecやDB#Queryに一気にパラメーターをバインドさせずに、DB#PrepareとStmtを使って書き直したのが
こちらです。
func TestExecutePrepared(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) // insert stmt, err := db.Prepare(`insert into book(isbn, title, price) values(?, ?, ?)`) assert.Nil(t, err) result, err := stmt.Exec("978-4798161488", "MySQL徹底入門 第4版", 4180) assert.Nil(t, err) rowsAffected, err := result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) result, err = stmt.Exec("978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) result, err = stmt.Exec("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) stmt.Close() // query row row := db.QueryRow(`select count(*) from book`) assert.Nil(t, row.Err()) var count int row.Scan(&count) assert.Equal(t, 3, count) stmt, err = db.Prepare(`select * from book where isbn = ?`) assert.Nil(t, err) row = stmt.QueryRow("978-4798161488") assert.Nil(t, row.Err()) var isbn, name string var price int row.Scan(&isbn, &name, &price) assert.Equal(t, "978-4798161488", isbn) assert.Equal(t, "MySQL徹底入門 第4版", name) assert.Equal(t, 4180, price) stmt.Close() // query rows stmt, err = db.Prepare(`select title from book where price > ? order by price desc`) assert.Nil(t, err) rows, err := stmt.Query(4000) assert.Nil(t, err) names := []string{} for rows.Next() { var name string err := rows.Scan(&name) assert.Nil(t, err) names = append(names, name) } assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版", "MySQL徹底入門 第4版"}, names) stmt.Close() _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
interpolateParams=trueとした時の違いは?というと、ラウンドトリップの回数が減るようです。
his reduces the number of roundtrips, since the driver has to prepare a statement, execute it with given parameters and close the statement again with interpolateParams=false.
Go-MySQL-Driver / / interpolateParams
クライアントでエスケープをしているみたいですからね。
https://github.com/go-sql-driver/mysql/blob/v1.5.0/connection.go#L183-L306
トランザクションを使ってみる(簡易)
次は、トランザクションを使ってみましょう。
func TestTransactionSimply(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) // ここに、トランザクションを使ったコードを書く _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
簡単に使うには、DB#BeginでTxを取得して、Tx経由でExecやQueryを実行し、最後にTx#Commit、Tx#Rollbackします。
insertしてロールバック。
tx, err := db.Begin()
assert.Nil(t, err)
// insert
result, err := tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180)
assert.Nil(t, err)
rowsAffected, err := result.RowsAffected()
assert.Nil(t, err)
assert.Equal(t, int64(1), rowsAffected)
err = tx.Rollback()
assert.Nil(t, err)
Tx#CommitまたはTx#Rollbackした後には、そのTxはもう使えないようです。
// transaction has already been committed or rolled back row := tx.QueryRow(`select count(*) from book`) assert.EqualError(t, row.Err(), "sql: transaction has already been committed or rolled back") assert.ErrorIs(t, sql.ErrTxDone, row.Err())
Txを使ってクエリーの実行したり、データをinsertしてコミット。
tx, err = db.Begin()
assert.Nil(t, err)
// query row
row = tx.QueryRow(`select count(*) from book`)
assert.Nil(t, row.Err())
var count int
row.Scan(&count)
assert.Equal(t, 0, count)
result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280)
assert.Nil(t, err)
rowsAffected, err = result.RowsAffected()
assert.Nil(t, err)
assert.Equal(t, int64(1), rowsAffected)
result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960)
assert.Nil(t, err)
rowsAffected, err = result.RowsAffected()
assert.Nil(t, err)
assert.Equal(t, int64(1), rowsAffected)
err = tx.Commit()
assert.Nil(t, err)
tx, err = db.Begin()
assert.Nil(t, err)
// query row
row = tx.QueryRow(`select count(*) from book`)
assert.Nil(t, row.Err())
row.Scan(&count)
assert.Equal(t, 2, count)
row = tx.QueryRow(`select * from book where isbn = ?`, "978-4873116389")
assert.Nil(t, row.Err())
var isbn, name string
var price int
row.Scan(&isbn, &name, &price)
assert.Equal(t, "978-4873116389", isbn)
assert.Equal(t, "実践ハイパフォーマンスMySQL 第3版", name)
assert.Equal(t, 5280, price)
// query rows
rows, err := tx.Query(`select title from book where price > ? order by price desc`, 4000)
assert.Nil(t, err)
names := []string{}
for rows.Next() {
var name string
err := rows.Scan(&name)
assert.Nil(t, err)
names = append(names, name)
}
assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版"}, names)
err = tx.Commit()
assert.Nil(t, err)
トランザクションを使う
先ほどは簡易版的な感じで紹介しましたが、もっとちゃんと使うにはDB#BeginTxを使います。こちらを使うと、
Contextやトランザクションのオプションを指定できるようです。
ctx := context.Background()
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
assert.Nil(t, err)
DB#BeginTxの戻り値は、TxなのはDB#Beginと同じなのであとの流れは変わりません。
DB#Beginで書いていたコードを、DB#BeginTxで書き直したのがこちら。
func TestTransaction(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) ctx := context.Background() tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) assert.Nil(t, err) // insert result, err := tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) assert.Nil(t, err) rowsAffected, err := result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) err = tx.Rollback() assert.Nil(t, err) // transaction has already been committed or rolled back row := tx.QueryRow(`select count(*) from book`) assert.EqualError(t, row.Err(), "sql: transaction has already been committed or rolled back") assert.ErrorIs(t, sql.ErrTxDone, row.Err()) ctx = context.Background() tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) assert.Nil(t, err) // query row row = tx.QueryRow(`select count(*) from book`) assert.Nil(t, row.Err()) var count int row.Scan(&count) assert.Equal(t, 0, count) result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4873116389", "実践ハイパフォーマンスMySQL 第3版", 5280) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) result, err = tx.Exec(`insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960) assert.Nil(t, err) rowsAffected, err = result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) err = tx.Commit() assert.Nil(t, err) ctx = context.Background() tx, err = db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) assert.Nil(t, err) // query row row = tx.QueryRow(`select count(*) from book`) assert.Nil(t, row.Err()) row.Scan(&count) assert.Equal(t, 2, count) row = tx.QueryRow(`select * from book where isbn = ?`, "978-4873116389") assert.Nil(t, row.Err()) var isbn, name string var price int row.Scan(&isbn, &name, &price) assert.Equal(t, "978-4873116389", isbn) assert.Equal(t, "実践ハイパフォーマンスMySQL 第3版", name) assert.Equal(t, 5280, price) // query rows rows, err := tx.Query(`select title from book where price > ? order by price desc`, 4000) assert.Nil(t, err) names := []string{} for rows.Next() { var name string err := rows.Scan(&name) assert.Nil(t, err) names = append(names, name) } assert.Equal(t, []string{"実践ハイパフォーマンスMySQL 第3版"}, names) err = tx.Commit() assert.Nil(t, err) _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
コネクションプールの設定をする
ところで、これまでずっとDBを使っていたのですが、説明を読むとどうやらコネクションプールが裏にあるようです。
The sql package creates and frees connections automatically; it also maintains a free pool of idle connections. If the database has a concept of per-connection state, such state can be reliably observed within a transaction (Tx) or connection (Conn). Once DB.Begin is called, the returned Tx is bound to a single connection. Once Commit or Rollback is called on the transaction, that transaction's connection is returned to DB's idle connection pool.
トランザクションを使った場合は、コネクションはそのTxに紐付けられます、と。だから、Tx経由でクエリーを実行したり
するんでしょうね。
コネクションプールの設定は、DBに対して行うようです。
func TestConfigurationPool(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() assert.Zero(t, db.Stats().MaxOpenConnections) db.SetMaxOpenConns(10) assert.Equal(t, 10, db.Stats().MaxOpenConnections) }
コネクションを使う
最後に、明示的にコネクションを使ってみましょう。
コネクション(Conn)を取得するには、DB#Connを使います。この時、Contextが必要になります。
// handle connection
ctx := context.Background()
conn, err := db.Conn(ctx)
使い終わったConnはクローズしましょう。プールに返却することを意味します。
defer conn.Close()
クエリーの使い方などはConn経由となるくらいで大きくは変わりませんが、メソッド名にContextが入り、引数にもContextが
必要になります。
// insert result, err := conn.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) // query row row := conn.QueryRowContext(ctx, `select count(*) from book`) // query rows rows, err := conn.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)
トランザクションを使う場合。
// handle connection & tx
ctx := context.Background()
conn, err := db.Conn(ctx)
tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
Contextを使うのは変わりません。
// insert result, err := tx.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) // query row row := tx.QueryRowContext(ctx, `select count(*) from book`) // query rows rows, err := tx.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000)
Connを使ったコード例は、こちら。
func TestConnectionPool(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) // handle connection ctx := context.Background() conn, err := db.Conn(ctx) assert.Nil(t, err) defer conn.Close() // insert result, err := conn.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) assert.Nil(t, err) rowsAffected, err := result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) // query row row := conn.QueryRowContext(ctx, `select count(*) from book`) assert.Nil(t, row.Err()) var count int row.Scan(&count) assert.Equal(t, 1, count) row = conn.QueryRowContext(ctx, `select * from book where isbn = ?`, "978-4798161488") assert.Nil(t, row.Err()) var isbn, name string var price int row.Scan(&isbn, &name, &price) assert.Equal(t, "978-4798161488", isbn) assert.Equal(t, "MySQL徹底入門 第4版", name) assert.Equal(t, 4180, price) // query rows rows, err := conn.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000) assert.Nil(t, err) names := []string{} for rows.Next() { var name string err := rows.Scan(&name) assert.Nil(t, err) names = append(names, name) } assert.Equal(t, []string{"MySQL徹底入門 第4版"}, names) _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
トランザクションを使った場合。
func TestConnectionPoolTx(t *testing.T) { db, err := sql.Open("mysql", "kazuhira:password@(172.17.0.2:3306)/practice") assert.Nil(t, err) defer db.Close() _, err = db.Exec(`create table if not exists book(isbn varchar(14), title varchar(200), price int, primary key(isbn))`) assert.Nil(t, err) // handle connection & tx ctx := context.Background() conn, err := db.Conn(ctx) tx, err := conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) assert.Nil(t, err) defer conn.Close() // insert result, err := tx.ExecContext(ctx, `insert into book(isbn, title, price) values(?, ?, ?)`, "978-4798161488", "MySQL徹底入門 第4版", 4180) assert.Nil(t, err) rowsAffected, err := result.RowsAffected() assert.Nil(t, err) assert.Equal(t, int64(1), rowsAffected) err = tx.Commit() assert.Nil(t, err) ctx = context.Background() tx, err = conn.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) // query row row := tx.QueryRowContext(ctx, `select count(*) from book`) assert.Nil(t, row.Err()) var count int row.Scan(&count) assert.Equal(t, 1, count) row = tx.QueryRowContext(ctx, `select * from book where isbn = ?`, "978-4798161488") assert.Nil(t, row.Err()) var isbn, name string var price int row.Scan(&isbn, &name, &price) assert.Equal(t, "978-4798161488", isbn) assert.Equal(t, "MySQL徹底入門 第4版", name) assert.Equal(t, 4180, price) // query rows rows, err := tx.QueryContext(ctx, `select title from book where price > ? order by price desc`, 4000) assert.Nil(t, err) names := []string{} for rows.Next() { var name string err := rows.Scan(&name) assert.Nil(t, err) names = append(names, name) } assert.Equal(t, []string{"MySQL徹底入門 第4版"}, names) err = tx.Commit() assert.Nil(t, err) _, err = db.Exec(`drop table if exists book`) assert.Nil(t, err) }
ところで、DB#Queryなどを使った場合はどうなっているんでしょう?
ソースコードを見ると、裏でConnを取得しているようです。
https://github.com/golang/go/blob/go1.16.2/src/database/sql/sql.go#L1622-L1629
さらに言うと、Contextもその場で取得しているようです。
https://github.com/golang/go/blob/go1.16.2/src/database/sql/sql.go#L1619
このContext、なにに使うんでしょうね?と少し見てみたのですが、キャンセルまわりみたいですね。
まとめ
ざっくりと、Goを使ってMySQLにアクセスしてみました。
sqlパッケージまわりの使い方と、ドライバーの存在などがわかった感じです。
(途中でだんだん面倒になって雑になってますが)とりあえず雰囲気はわかったので良しとしましょう。