今日やること
database/sqlパッケージを素朴に使う。
その上でGORMのコードを読む。
Version
- Mysql 8.X
- Go 1.18
database/sqlパッケージを使う。
データベースとテーブル作成
CREATE DATABASE example_go_sql;
CREATE TABLE users (
id SERIAL,
name VARCHAR(20)
);
main.go
- 複数行取得
func queryRows
- 一行取得
func queryRow
- インサート
func create
- トランザクションの使用(Update)
func txUpdate
を行います。
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db := open()
defer db.Close()
configDB(db)
ctx := context.TODO()
conn := connection(db, ctx)
defer conn.Close()
queryRows(conn)
queryRow(conn)
create(conn, "hey")
txUpdate(conn)
}
func open() *sql.DB {
db, err := sql.Open("mysql", "root:sample@tcp(127.0.0.1:3306)/example_go_sql")
if err != nil {
log.Fatalln(err)
}
return db
}
func configDB(db *sql.DB) {
db.SetConnMaxLifetime(0)
db.SetMaxIdleConns(50)
db.SetMaxOpenConns(50)
}
func connection(db *sql.DB, ctx context.Context) *sql.Conn {
conn, err := db.Conn(ctx)
if err != nil {
log.Fatalln(err)
}
return conn
}
func queryRows(conn *sql.Conn) {
ctx := context.TODO()
rows, err := conn.QueryContext(ctx, "SELECT * FROM users WHERE id > ?", 0)
if err != nil {
log.Fatalln(err)
}
defer rows.Close()
for rows.Next() {
var id, name string
err := rows.Scan(&id, &name)
if err != nil {
log.Fatalln(err)
}
fmt.Println(id, name)
}
if err := rows.Err(); err != nil {
log.Fatalln(err)
}
}
func queryRow(conn *sql.Conn) {
ctx := context.TODO()
row := conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id > ?", 0)
var id, name string
err := row.Scan(&id, &name)
if err != nil {
log.Fatalln(err)
}
fmt.Println(id, name)
}
func create(conn *sql.Conn, name string) {
ctx := context.TODO()
res, err := conn.ExecContext(ctx, "INSERT INTO users (name) VALUES(?)", name)
if err != nil {
log.Fatalln(err)
}
updated, err := res.RowsAffected()
if err != nil {
log.Fatalln(err)
}
inserted, err := res.LastInsertId()
if err != nil {
log.Fatalln(err)
}
fmt.Println(inserted, updated)
}
func txUpdate(conn *sql.Conn) {
ctx := context.TODO()
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
log.Fatalln(err)
}
_, execErr := tx.ExecContext(ctx, "UPDATE users SET name = ? WHERE id = ?", "updated", 1)
if execErr != nil {
if rollbackErr := tx.Rollback(); rollbackErr != nil {
log.Fatalf("update failed: %v, unable to rollback: %v\n", execErr, rollbackErr)
}
log.Fatalf("update failed: %v", execErr)
}
if err := tx.Commit(); err != nil {
log.Fatal(err)
}
}
database/sqlを使用した感想
row := conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", 1)
var id, name string
row.Scan(&id, &name)
変数へのアサインがめんどくさいです。
上のコードスニペットの様に、
(愚直にやると)カラムの分だけ人間が変数を定義する羽目になります。
一方で普段使用しているGORMは次のコードスニペットの様に実行できて、構造体を指定するだけです。
var user User
db.WithContext(ctx).First(&user)
GORM便利!!
GORMのコードを読む
GORMが便利ということはわかりました。
あとはGORMがこの便利なインタフェースを、どう実現しているか知れれば完璧ですね。
今回はタグv1.23.8のコードを読みます
Scanで検索
database/sqlではScanを使用して変数にアサインをしました。
このScanメソッドは定義を見ると
任意の数のパラメーターを渡すことができます。
GORMでは当然 Scan(slice...)
のように呼び出されていると推測し
Scan(.+...)
で検索します
$ ag "Scan\(.+\.\.\."
interfaces.go
81: Scan(dest ...interface{}) error
scan.go
67: db.AddError(rows.Scan(values...))
125: db.AddError(rows.Scan(values...))
145: db.AddError(rows.Scan(values...))
結果2つのファイルで合計4行ヒットしました。
このうちinterface.goのものは明らかにinterface定義なのでscan.goを見ていきます。
scan.goでは次のメソッドがScanを呼び出していました。
func scanIntoStruct
func Scan
メソッド名的に scanIntoStruct
今回探していた関数です。
func (db *DB) scanIntoStruct(rows Rows, reflectValue reflect.Value, values []interface{}, fields []*schema.Field, joinFields [][2]*schema.Field) {
for idx, field := range fields {
if field != nil {
values[idx] = field.NewValuePool.Get()
} else if len(fields) == 1 {
if reflectValue.CanAddr() {
values[idx] = reflectValue.Addr().Interface()
} else {
values[idx] = reflectValue.Interface()
}
}
}
db.RowsAffected++
db.AddError(rows.Scan(values...))
joinedSchemaMap := make(map[*schema.Field]interface{}, 0)
for idx, field := range fields {
if field != nil {
if len(joinFields) == 0 || joinFields[idx][0] == nil {
db.AddError(field.Set(db.Statement.Context, reflectValue, values[idx]))
} else {
joinSchema := joinFields[idx][0]
relValue := joinSchema.ReflectValueOf(db.Statement.Context, reflectValue)
if relValue.Kind() == reflect.Ptr {
if _, ok := joinedSchemaMap[joinSchema]; !ok {
if value := reflect.ValueOf(values[idx]).Elem(); value.Kind() == reflect.Ptr && value.IsNil() {
continue
}
relValue.Set(reflect.New(relValue.Type().Elem()))
joinedSchemaMap[joinSchema] = nil
}
}
db.AddError(joinFields[idx][1].Set(db.Statement.Context, relValue, values[idx]))
}
// release data to pool
field.NewValuePool.Put(values[idx])
}
}
}
scanIntoStructメソッド内では values
に読み込んだ行データをセットしています。
このvaluesに入った値は field.Set
の引数に渡されています。
このvaluesが何かは一旦宿題としてfield.Set
を見ていきます。
Set func(context.Context, reflect.Value, interface{}) error
schema.Field 構造体にSetという名前で関数型のフィールドがありました。
このSetフィールドはsetupValuerAndSetter
メソッドでセットされています。
func (field *Field) setupValuerAndSetter() {
fieldに応じた型の種類によってswitch文でSetフィールドをセットしています。
Setフィールドにセットされる関数は SetBool
などの型に応じたメソッドを呼び
構造体に値をセットしています。
switch field.FieldType.Kind() {
case reflect.Bool:
field.Set = func(ctx context.Context, value reflect.Value, v interface{}) error {
switch data := v.(type) {
case **bool:
if data != nil && *data != nil {
field.ReflectValueOf(ctx, value).SetBool(**data)
} else {
field.ReflectValueOf(ctx, value).SetBool(false)
}
case bool:
field.ReflectValueOf(ctx, value).SetBool(data)
case int64:
field.ReflectValueOf(ctx, value).SetBool(data > 0)
case string:
b, _ := strconv.ParseBool(data)
field.ReflectValueOf(ctx, value).SetBool(b)
default:
return fallbackSetter(ctx, value, v, field.Set)
}
return nil
}
あとは scanIntoStruct
メソッドに渡ってくる fields
がどう初期化されているかですが
これも一旦宿題とします。
reflectパッケージについてちょっとだけ
reflectパッケージのValue構造体にある SetXXX
メソッドについてコードで補足します。
type (
User struct {
Name string
}
)
func main() {
var u User
v := reflect.ValueOf(&u)
f := reflect.Indirect(v).Field(0)
f.SetString("nishi")
fmt.Println(u.Name) // nishi
}
このようにreflectパッケージを使えば、構造体のフィールド名を使用せずに
構造体のフィールドを変更できます。
私もreflectパッケージについてはあまり詳しくないので、いつか調べてまた記事にします。
まとめ
今回は database/sql パッケージを触りました。
普段使う便利ライブラリもGoの標準パッケージを内部的に使用していることがほとんどです。
GORMのような便利ライブラリがどう標準パッケージを利用しているかをコードを読んで理解しました。(ざっくり)
まだまだ知らないことがいっぱいあって、記事のネタに困らなくて良いですね。