最近サイバーエージェント主催のヒダッカソンというインターンで優勝をいただきました。インターンは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のソースコードを見にいきました。
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)
を見にいきます。
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)
を見にいきます。
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
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)
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
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)
}
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管理ツールとかに挑戦してみたいですね。すでにいい感じのものが作られているのですが、やっぱり自分で作ってみたいじゃないですか。あ、もちろんインターンも楽しかったです。