0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GoのSQL周りをちょっとだけいい感じにしてみた

Last updated at Posted at 2019-10-07

最近サイバーエージェント主催のヒダッカソンというインターンで優勝をいただきました。インターンはGoで参加したのですが、コードを書く上で特にORM周りが実装の速度低下を引き起こしていたので自前で作ってみることにしました。

GoのSQLあたりって書きづらいんですよね.. Columnの分だけPointerを渡さないといけないし… そこでjson.Unmarshalみたいに、StructのTagでいい感じにしてくれたらいいんじゃねということでQueryRow, Query, Exec関数を非常に薄くラップしてみることにしました。

できたもの: https://github.com/K-jun1221/gosqlper

参考にしたもの

意気込んで作り始めたのはいいのですが、冒頭からつまづきました 笑 引数としてinterface{}で受け取らなければならず、Tagは愚か型情報が一切関数に渡って来ないんですねこれが… StackOverFlow上でも同じようなことで迷っている人がいました。 笑

じゃあ、JsonPackageのUnmarshalどうやって実装しとるんやということで、Unmarshalのソースコードを見にいきました。

Unmarshal.go

func Unmarshal(data []byte, v interface{}) error {
	// Check for well-formedness.
	// Avoids filling out half a data structure
	// before discovering a JSON syntax error.
	var d decodeState
	err := checkValid(data, &d.scan)
	if err != nil {
		return err
	}

	d.init(data)
	return d.unmarshal(v)
}

ほーん。と適当に眺めたところで、d.unmarshal(v)を見にいきます。

d_unmarshal.go

func (d *decodeState) unmarshal(v interface{}) error {
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.IsNil() {
		return &InvalidUnmarshalError{reflect.TypeOf(v)}
	}

	d.scan.reset()
	d.scanWhile(scanSkipSpace)
	// We decode rv not rv.Elem because the Unmarshaler interface
	// test must be applied at the top level of the value.
	err := d.value(rv)
	if err != nil {
		return d.addErrorContext(err)
	}
	return d.savedError
}

こいつも適当にほーんと眺めて最後にd.value(rv)を見にいきます。

d_value.go

func (d *decodeState) value(v reflect.Value) error {

	fmt.Println("[value]")
	switch d.opcode {
	default:
		panic(phasePanicMsg)

	case scanBeginArray:

		fmt.Println("scanBeginArray")
		if v.IsValid() {
			if err := d.array(v); err != nil {
				return err
			}
		} else {
			d.skip()
		}
		d.scanNext()

	case scanBeginObject:
		if v.IsValid() {
			if err := d.object(v); err != nil {
				return err
			}
		} else {
			d.skip()
		}
		d.scanNext()

	case scanBeginLiteral:
		// All bytes inside literal return scanContinue op code.
		start := d.readIndex()
		d.scanWhile(scanContinue)
		fmt.Println(d.data[start:d.readIndex()])

		if v.IsValid() {
			if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
				return err
			}
		}
	}

	return nil
}

なんか出てきた... 苦戦しながらも色々と読んでみると、思いの外単純であることがわかりました。JsonをDecode + 型判定しないといけないからこんなに複雑になっているんですね...

めちゃくちゃシンプルにしてみると以下のようなコードになります。


var obj interface{}
v := reflect.ValueOf(obj)

subv := v.Field("fieldName")
subv.SetString("fieldValue")

え、めっちゃ簡単やん。 ってことでこいつを参考にコードを書いていきます。タグはdbという名前でつけることにしました。型はめんどくさいのでStringで統一することにしました。自分用なので型の判定とかは甘々です... 許してくださいなんでもします...

自分で書いてみる


func QueryRow(db *sql.DB, sql string, obj interface{}) error {
	cns, err := columnGetter(sql)
	if err != nil {
		return err
	}

	// create reflect.Value
	v := reflect.Indirect(reflect.ValueOf(obj))

	// get tag mapping list
	tm, err := tagMappingGetter(cns, v)
	if err != nil {
		return err
	}

	// call scan
	columns := make([]interface{}, len(cns))
	for i := 0; i < len(cns); i++ {
		var str string
		columns[i] = &str
	}

	err = db.QueryRow(sql).Scan(columns...)
	if err != nil {
		return err
	}

	for i, column := range columns {
		subv := v.Field(tm[i])
		str, ok := column.(*string)
		if !ok {
			return errors.New("could not cast interface{} to *string type")
		}
		subv.SetString(*str)
	}

	return nil
}

ついでにExec()とかQuery()とかもUnmarshalを参考にして実装しておきます。

書き換えてみる

QueryRow

old.go
type User struct {
	UserID      string `json:"user_id"`
	UserName    string `json:"user_name"`
	Password    string `json:"password"`
	IsAdmin     string `json:"is_admin"`
	UserComment string `json:"user_comment"`
}

var row User
_ := db.QueryRow("SELECT id, name, pass, comment, is_admin FROM users WHERE user_id = ?", id).Scan(&row.UserID, &row.UserName, &row.Password, &row.IsAdmin, &row.UserComment)
new.go

type User struct {
	UserID      string `json:"user_id" db:"user_id"`
	UserName    string `json:"user_name" db:"user_name"`
	Password    string `json:"password" db:"password"`
	IsAdmin     string `json:"is_admin" db:"is_admin"`
	UserComment string `json:"user_comment" db:"user_comment"`
}

var row User

_ := gosqlper.QueryRow(db, "SELECT id, name, pass, comment, is_admin FROM users WHERE user_id = 1", &row)

Query

old.go

type User struct {
	UserID      string `json:"user_id"`
	UserName    string `json:"user_name"`
	Password    string `json:"password"`
	IsAdmin     string `json:"is_admin"`
	UserComment string `json:"user_comment"`
}

var users []User
rows, _ = db.Query("SELECT id, name, pass, comment, is_admin FROM users")

for rows.Next() {
    var row User
    _ := rows.Scan(&row.UserID, &row.UserName, &row.Password, &row.IsAdmin, &row.UserComment)
    users = append(users, row)
}
new.go

type User struct {
	UserID      string `json:"user_id" db:"user_id"`
	UserName    string `json:"user_name" db:"user_name"`
	Password    string `json:"password" db:"password"`
	IsAdmin     string `json:"is_admin" db:"is_admin"`
	UserComment string `json:"user_comment" db:"user_comment"`
}

var users []User

rows, _ = gosqlper.Query(db, "SELECT id, name, pass, comment, is_admin FROM users", &users)


QueryRowの箇所はあんまり改善しませんね.. ですが、複数行のQueryMethodは結構いい感じになったのではないでしょうか? インターンでも結構SQLを書く場所が多くて時間が取られたので、こういうものを作っておけばもう少し時間短縮ができて楽に優勝ができたのかなと思います。

感想

ライブラリとかを作るとか初めてだったのですごく楽しかったです。次はMigration機構や、Version管理ツールとかに挑戦してみたいですね。すでにいい感じのものが作られているのですが、やっぱり自分で作ってみたいじゃないですか。あ、もちろんインターンも楽しかったです。

0
1
0

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
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?