--- tags: Go --- # GoでDatabaseを利用する ## インストール ### MySQL(Docker)のインストール * 指定したディレクトリにsqlを置いて、開始時にテーブルを初期化できる * docker-compose.yaml ```yaml version: '3' services: db: image: mysql:5.7 container_name: mysql_host environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: test MYSQL_USER: user MYSQL_PASSWORD: TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - ./docker/db/data:/var/lib/mysql - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf - ./docker/db/sql:/docker-entrypoint-initdb.d ports: - 3306:3306 ``` * ./docker/db/sql/init.sql ```sql CREATE DATABASE testdb; CREATE TABLE `test_table` ( id INT(20) AUTO_INCREMENT, name VARCHAR(20) NOT NULL, PRIMARY KEY (`id`) ) ``` ### 起動 * docker-compose起動 ```bash $ docker-compose up ``` ### 接続 * MySQLクライアントをインストールする ```bash $ brew intall mysql-client ``` * mysql-clientで接続する ```bash $ mysql -uroot -p'root' -h 127.0.0.1 ``` ## database/sqlパッケージ ### sql.Open() * sql.OpenにDB名と、接続先を指定して接続を行う * 初期化を行った後は、疎通チェックのため、`Ping()`を呼ぶ ```go package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" "log" ) func main() { db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test") if err != nil { log.Fatalln(err) } ping(db) } func ping(db *sql.DB) { if err := db.Ping(); err != nil { log.Fatalln(err) } } ``` #### importについて * mysqlのDBドライバは、直接指定しないがimportを行う必要がある * それぞれのパッケージ内で、init()関数でドライバが追加される * init()関数はmain()関数より前に呼び出される関数 * 例: https://github.com/go-sql-driver/mysql/blob/v1.4/driver.go#L171 ```go func init() { sql.Register("mysql", &MySQLDriver{}) } ``` * importの指定を忘れると以下のようなエラーが返る ``` sql: unknown driver "mysql" (forgotten import?) ``` ### db.Exec() * UPDATEやDELETEなど、更新のクエリを実行する際に使う ```go func (s *Stmt) Exec(args ...interface{}) (Result, error) ``` * Example: INSERTを行う場合 ```go result, err = db.Exec("INSERT INTO test_table (name) VALUES ('sato')") ``` ### db.Query() * SELECTなどの参照のクエリを実行する際に使う ```go func (db *DB) Query(query string, args ...interface{}) (*Rows, error) ``` * `Rows.Next()`で結果をイテレーション、`Rows.Close()`でRowsを閉じる ```go rows, err := db.Query("SELECT id, name FROM test_table") if err != nil { log.Fatalln(err) } for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { log.Fatalf("cannot scan: %s", err) } fmt.Printf("id: %d, name: %s", id, name) } if err := rows.Close(); err != nil { log.Fatalln(err) } ``` ### db.Stats() * DBの統計情報を取得する ```go fmt.Printf("%+v", db.Stats()) {MaxOpenConnections:0 OpenConnections:1 InUse:0 Idle:1 WaitCount:0 WaitDuration:0s MaxIdleClosed:0 MaxLifetimeClosed:0} ``` ### db.Prepare() * プリペアドステートメントのステートメント`Stmt`が生成される * プリペアドステートメントについて * 処理の流れ 1. プレースホルダ付きのSQLをサーバーに送る 2. サーバーからステートメントIDが返る 3. クライアントはIDとパラメータを送り実行 * SQLのパースは比較的時間がかかるため、プリペアドステートメントによりパフォーマンスが上がる * Example ```go stmt, err := db.Prepare("INSERT INTO test_table (id, name) VALUES(?, ?)") if err != nil { log.Fatal(err) } defer stmt.Close() for id, name := range names { if _, err := stmt.Exec(id+1, name); err != nil { log.Fatal(err) } } ``` ### Transaction * db.Begin() * トランザクションを開始することができる * 戻り値`Tx`はトランザクションを表す * tx.Rollback() * ロールバックを行う * tx.Commit() * トランザクションが完了する * 実行例 ```go tx, err := db.Begin() result, err := tx.Exec("INSERT INTO test_table (name) VALUES ('sato')") if err != nil { // error handling } if err := failedSomething(); err != nil { if err := tx.Rollback(); err != nil { // error handling } } tx.Commit() ``` ## [sqlx](https://github.com/jmoiron/sqlx) * database/sqlの拡張パッケージ * sqlの結果を構造体にMarshalできる * 基本的な使い方は、database/sqlとほぼ同じ ### [db.Get()](https://godoc.org/github.com/jmoiron/sqlx#DB.Get) * 引数 * 第1引数: 結果を格納する変数のポインタ * 第2引数: sql * 第3引数以降: プレースホルダへ埋め込む変数 * ゼロ件だった場合はエラーとなる * サンプル ```go p := Place{} err = db.Get(&p, "SELECT * FROM place LIMIT 1") ``` ### [db.Select()](https://godoc.org/github.com/jmoiron/sqlx#DB.Select) * db.Get()とほぼ同様だが、結果が複数件になる場合利用する ## Unit Test TODO ## 参考 - https://blog.withnic.net/2018/08/golang%E3%81%A7transaction%E5%87%A6%E7%90%86%E3%82%92%E8%80%83%E3%81%88%E3%82%8B/