これは、なにをしたくて書いたもの?
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パッケージまわりの使い方と、ドライバーの存在などがわかった感じです。
(途中でだんだん面倒になって雑になってますが)とりあえず雰囲気はわかったので良しとしましょう。