202号室の手記

頑張らないブログ

[Go言語] gormのクエリ条件をイテレーション内で指定する際は結果の格納先を初期化するべきという話

GoのORMライブラリgorm(http://doc.gorm.io/)でqueryをしたときの挙動でハマった.
コードは次の通り.

package main

import (
    "fmt"
    "log"

    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/postgres"
)

type Employee struct {
    gorm.Model
    Num string
    LN  string
    FN  string
}

func main() {
    connStr := "postgres://example:example@localhost:5432/example?sslmode=disable"
    db, err := gorm.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // db.LogMode(true)
    db.AutoMigrate(&Employee{})

    emps := []Employee{
        Employee{Num: "001", LN: "ichi", FN: "taro"},
        Employee{Num: "002", LN: "ninomiya", FN: "kinjiro"},
        Employee{Num: "003", LN: "minowa", FN: "akihiro"},
        Employee{Num: "004", LN: "yon", FN: "jun"},
        Employee{Num: "005", LN: "godaigo", FN: "tenno"},
    }

    for _, e := range emps {
        db.NewRecord(e)
        db.Create(&e)
    }

    nums := []string{"001", "004", "005"}
    var emp Employee
    for _, n := range nums {
        notFound := db.Where("num = ?", n).First(&emp).RecordNotFound()
        if notFound {
            continue
        }
        fmt.Printf("%s %s\n", emp.LN, emp.FN)
    }

    return
}

numsスライス内に記載した従業員番号に該当する従業員の名前を表示するというもの.
狙いとしては

ichi taro
yon jun
godaigo tenno

という結果が欲しかったのだが, 実際は次の通り, 初めの一人しかヒットしていない.

ichi taro

db.LogMode(true)のコメントを外してログを見てみると, numsイテレーションにより, 次のクエリが発行されていた.

SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND ((num = '001')) ORDER BY "employees"."id" ASC LIMIT 1
SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND "employees"."id" = '1' AND ((num = '004')) ORDER BY "employees"."id" ASC LIMIT 1
SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND "employees"."id" = '1' AND ((num = '005')) ORDER BY "employees"."id" ASC LIMIT 1

どうやら, 従業員番号"001"でヒットしたユーザ(id=1)データが &empポインタに残っていると, これを引数にとるFirstメソッド はid=1のユーザの中から探してしまうようだ.
id=1かつ従業員番号"004"の従業員はいないので, スルーされてしまう.

このFirstメソッドは, プライマリキー順にサーチし, 該当の条件に一致する列を返す.
実装は次の通り.

// First find first record that match given conditions, order by primary key
func (s *DB) First(out interface{}, where ...interface{}) *DB {
    newScope := s.NewScope(out)
    newScope.Search.Limit(1)
    return newScope.Set("gorm:order_by_primary_key", "ASC").
        inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db
}

s.parent.callbacks.queriesをコールしてWHERE句へ条件を追加していき, 結果DBへの参照を返す.

イテレーションごとに, クエリ結果を格納する変数(ポインタ)を初期化してやれば, クエリ条件も初期化され, 望みの結果が得られた.

次のように, empの宣言をforループの内部で行うよう修正.

nums := []string{"001", "004", "005"}
// var emp Employee
     for _, n := range nums {
        var emp Employee
        notFound := db.Where("num = ?", n).First(&emp).RecordNotFound()
        if notFound {
            continue
        }
...

実行されたクエリ

SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND ((num = '001')) ORDER BY "employees"."id" ASC LIMIT 1
SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND ((num = '004')) ORDER BY "employees"."id" ASC LIMIT 1
SELECT * FROM "employees"  WHERE "employees"."deleted_at" IS NULL AND ((num = '005')) ORDER BY "employees"."id" ASC LIMIT 1

結果

ichi taro
yon jun
godaigo tenno