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

Dockerコンテナをホスト側のcronで実行する

やりたいこと

  • とあるプログラムを実行するDockerコンテナを毎日定刻に起動したい。
  • 処理が完了したらコンテナは消去したい。

つまり、次のコマンドをcronで実行したい。

$ docker run -it --rm my_image my_command

やったこと

crontabにそのまま書けばいいじゃん!
と思ったが、そうは東京医科歯科大学

$ crontab -e
0 7 * * * docker run -it --rm my_image my_command

定刻になっても、うんともすんともしない。

まずは、crontabの実行環境で、dockerコマンドにパスが通ってんのかが気になった。

$ which docker
/usr/bin/docker
$ crontab -e 
* * * * * echo $PATH > /tmp/env.txt

結果は、

$ cat /tmp/env.txt
PATH=/usr/bin:/bin

.bash_profileに書いてあるようなユーザ定義の環境変数は受け継がれていないが、 少なくともdockerコマンドへのパスは通っているようだ。

デフォルトの/var/log/cronログファイルには、実行結果までは出力されない。
そこで、エラーチェックのために、次のように設定した。

* * * * * docker run -it --rm my_image my_command > /tmp/cron.log 2>&1

結果は、

$ cat /tmp/cron.log
the input device is not a TTY

となっていた。 実端末(TTY)からの入力でないことで怒られている。

原因はイカの通り。

cronで指定されたコマンドは実端末から実行される訳ではない。
実際、

$ crontab -e
* * * * * tty > /tmp/tty.txt

とすると、

$ cat /tmp/tty.txt
not a tty

となる。
docker runコマンドの-itオプションは、 実行時の端末をコンテナ内のプロセスに割り当てるものであるため、 割り当てるべき端末がない、と怒っていたのだ。

結論

crontabには、次のように書けば良いです。(毎朝7時に実行したい場合)

0 7 * * * docker run --rm my_image my_command

こうしてできたのが、↓のbotです。

twitter.com

以上です。