search
LoginSignup
0
Help us understand the problem. What are the problem?

posted at

Goのdatabase/sqlパッケージとGORMのコードリーディング

今日やること

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のような便利ライブラリがどう標準パッケージを利用しているかをコードを読んで理解しました。(ざっくり)
まだまだ知らないことがいっぱいあって、記事のネタに困らなくて良いですね。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0
Help us understand the problem. What are the problem?