gormのUpdateColumnsでモデル内のboolゼロ値(false)を持ったカラムが更新されなかった の続き
前回の記事(http://tapira.hatenablog.com/entry/2017/08/09/173718)で以下の通りよくわからんくて飛ばしてたところを、gormのコードを追ってみた。
func (s DB) UpdateColumns(values interface{}) DB { ここで呼ばれたタイミングではvaluesに値が渡ってきてるけど、その後instanceにセットしてcallback呼んだタイミングで消えてるので、 Setするどっかのロジックでゼロ値だとスキップするような動きがあると思うけどどこかわからんかった・・・
UpdateColumns
の呼び出し
db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
- gorm/callback_update.goのinit関数にて、
assignUpdatingAttributesCallback
がコールバック関数に登録されます
// Define callbacks for updating func init() { DefaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback)
- その後callCallbacksの呼び出しによって、登録されたコールバック関数が実行されます
func (s *DB) UpdateColumns(values interface{}) *DB { return s.clone().NewScope(s.Value). Set("gorm:update_column", true). Set("gorm:save_associations", false). InstanceSet("gorm:update_interface", values). callCallbacks(s.parent.callbacks.updates).db }
- assignUpdatingAttributesCallbackの中身
// assignUpdatingAttributesCallback assign updating attributes to model func assignUpdatingAttributesCallback(scope *Scope) { if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok { if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate { scope.InstanceSet("gorm:update_attrs", updateMaps) } else { scope.SkipLeft() } } }
以下の処理で UpdateColumns(&eventExAfter)
で渡された値をupdate_attrsにセットしている
if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate { scope.InstanceSet("gorm:update_attrs", updateMaps)
- updatedAttrsWithValuesの中を追いかけてみると、以下
func (scope *Scope) updatedAttrsWithValues(value interface{}) (results map[string]interface{}, hasUpdate bool) { if scope.IndirectValue().Kind() != reflect.Struct { return convertInterfaceToMap(value, false), true } results = map[string]interface{}{} for key, value := range convertInterfaceToMap(value, true) { if field, ok := scope.FieldByName(key); ok && scope.changeableField(field) {
ゼロ値の場合convertInterfaceToMapにて該当カラムが更新対象列から消える - convertInterfaceToMapは以下
func convertInterfaceToMap(values interface{}, withIgnoredField bool) map[string]interface{} { var attrs = map[string]interface{}{} switch value := values.(type) { case map[string]interface{}: return value case []interface{}: for _, v := range value { for key, value := range convertInterfaceToMap(v, withIgnoredField) { attrs[key] = value } } case interface{}: reflectValue := reflect.ValueOf(values) switch reflectValue.Kind() { case reflect.Map: for _, key := range reflectValue.MapKeys() { attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface() } default: for _, field := range (&Scope{Value: values}).Fields() { if !field.IsBlank && (withIgnoredField || !field.IsIgnored) { attrs[field.DBName] = field.Field.Interface() } } } } return attrs }
- 今回はinterface{}で値を受けており、ゼロ値が渡されたカラムは以下の処理でfalseと判定され、更新対象リストにアペンドされていないっぽい
if !field.IsBlank && (withIgnoredField || !field.IsIgnored) {
attrs[field.DBName] = field.Field.Interface()
}
- 確認してみるとゼロ値のカラムについてはIsBlank=trueとなっていた。IsBlankのセットは以下のコードでやっているぽい
func (field *Field) Set(value interface{}) (err error) {
・・・
field.IsBlank = isBlank(field.Field)
- isBlankの実装を見てみると
func isBlank(value reflect.Value) bool { switch value.Kind() { case reflect.String: return value.Len() == 0 case reflect.Bool: return !value.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return value.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return value.Uint() == 0 case reflect.Float32, reflect.Float64: return value.Float() == 0 case reflect.Interface, reflect.Ptr: return value.IsNil() } return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) }
!value.Bool()
false渡したらisBlank=true
…
bool型がtrueかfalseしかもてない以上、こうなるのはやむないのか。 ここにたどり着くのにめちゃくちゃ時間かかってしまった。 もっと素早くコードリーディングできるようになりたい
今回お世話になったツール
Visual Studio Code(Mac版)のデバッグ機能
gormのUpdateColumnsでモデル内のboolゼロ値(false)を持ったカラムが更新されなかった
schema
mysql> desc events; +-------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+--------------+------+-----+---------+-------+ | id | int(10) | YES | | NULL | | | name | varchar(255) | YES | | NULL | | | flag | tinyint(1) | YES | | NULL | | +-------+--------------+------+-----+---------+-------+ 3 rows in set (0.00 sec)
問題
type Event struct { Id uint `json:"id"` Name string `json:"name"` Flag bool `json:"flag"` } func gormConnect() *gorm.DB { db, err := gorm.Open("mysql", "root@tcp(localhost:3306)/sample") if err != nil { panic(err.Error()) } return db } func hoge(c echo.Context) { db := gormConnect() // 初期データ作成 eventEx := Event{Id: 1, Name: "taro", Flag: true} result := db.Create(eventEx) fmt.Println(result) // 初期データのID=1を更新する eventExBefore := Event{Id: 1} db.First(&eventExBefore) eventExAfter := new(Event) // {Name: "jiro", Flag: false} という値がPUTされてきたとする c.Bind(eventExAfter) db.Model(&eventExBefore).UpdateColumns(&eventExAfter) }
Createのタイミングで以下のINSERT文が発行される
INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',1);
Updateで以下のSQL発行を期待するが、、、
UPDATE `events` SET `name` = 'jiro', `flag` = 0 WHERE `events`.`id` = 1;
実際には以下のSQLが発行される
UPDATE `events` SET `name` = 'jiro' WHERE `events`.`id` = 1;
以下のとおり、bool型ゼロ値ではないtrueを指定すると
func main() { db := gormConnect() // 初期データ作成 eventEx := Event{Id: 1, Name: "taro", Flag: true} result := db.Create(eventEx) fmt.Println(result) // 初期データのID=1を更新する eventExBefore := Event{Id: 1} db.First(&eventExBefore) eventExAfter := new(Event) // {Name: "jiro", Flag: true} という値がPUTされてきたとする c.Bind(eventExAfter) db.Model(&eventExBefore).UpdateColumns(&eventExAfter) }
期待どおり動作する
INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',0); UPDATE `events` SET `name` = 'jiro', `flag` = 1 WHERE `events`.`id` = 1;
なんでこうなるのか
func (s *DB) UpdateColumns(values interface{}) *DB {
ここで呼ばれたタイミングではvaluesに値が渡ってきてるけど、その後instanceにセットしてcallback呼んだタイミングで消えてるので、
Setするどっかのロジックでゼロ値だとスキップするような動きがあると思うけどどこかわからんかった・・・
追記 以下でした
以下のように map[string]interface{}{}
に更新パラメータをバインドさせてUpdateすれば意図した動きになるが、Structに定義されたカラム全てが載ってしまい、思った使い方ができなかった。
event := map[string]interface{}{} if err := c.Bind(¶ms); err != nil { return err }
期待どおりに動かすには
boolポインタ型を使用する。
--- a/gorm-sample.go +++ b/gorm-sample.go @@ -10,7 +10,7 @@ import ( type Event struct { Id uint `json:"id"` Name string `json:"name"` - Flag bool `json:"flag"` + Flag *bool `json:"flag"` } func gormConnect() *gorm.DB { @@ -24,15 +24,17 @@ func gormConnect() *gorm.DB { func main() { db := gormConnect() + ptrue := &[]bool{true}[0] + pfalse := &[]bool{false}[0] // 初期データ作成 - eventEx := Event{Id: 1, Name: "taro", Flag: false} + eventEx := Event{Id: 1, Name: "taro", Flag: ptrue} result := db.Create(eventEx) fmt.Println(result) // 初期データのID=1を更新する eventExBefore := Event{Id: 1} db.First(&eventExBefore) eventExAfter := new(Event) // {Name: "jiro", Flag: false} という値がPUTされてきたとする c.Bind(eventExAfter) db.Model(&eventExBefore).UpdateColumns(&eventExAfter) }
こうするとboolポインタ型のゼロ値はnilとなり、falseと値を指定した場合も意図した動きになる。
eventExAfter := map[string]interface{}{} // {Name: "jiro", Flag: false} という値がPUTされてきたとする c.Bind(eventExAfter) db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
ちなみに、この挙動は github.com/go-playground/validator
を使用した際にも同じだった。
flag: falseと指定していた場合もrequire指定でエラーになってしまう。
uintなどを使用した場合にもゼロ値が 0
であるため、0で更新したい場合などは↑のようにuintポインタ型にしないとだめそう。
他にうまい回避方法がわからなくて、こんなことしたけど実はイケてるやり方があったら知りたい。。。
PHPカンファレンス福岡2017のスポンサーセッションで「mrubyで作る海外IPフィルター」という話をした
スライド
発表の振り返り
- 思ったよりも落ち着いて喋ることができた
- 正面を向いて話すよう意識できたのはよかった
- 本番よりも練習の方が話しづらい。本番はむしろやりやすい(重要)
- 練習大事…本当に練習大事
スライド作成の振り返り・今後意識したいこと
- プロジェクターの画面比率は早めに確認しよう
- 16:9で作ってたのを大至急4:3にして辛い思い
- カンファレンスの雰囲気・規模・どういった立場で喋るのか意識しよう
- スポンサーセッションは固めの文章、明るくやるならトークで
- 小規模なLTなどであればフレンドリーな文面も可 など
- どういった層を相手に話すか意識しよう
- 技術レベル
- 使う言葉が一般的なのか、内輪向けの話なのか、単語の定義を意識
どういうタイトル
で何が言いたいか
をまず決めよう- スライドの流れ・ストーリーを意識しよう
- 守らないと結局何が言いたいの?なスライドになる
- スライド作成前にアウトラインを固めよう
- ストーリー作りがスライドだとやり辛い、めちゃくちゃなものができやすい
- スライドへでのレビューは面倒(レビュワーへの負担)
- 箇条書きは一行で。改行避ける
- 文章で伝えづらいところだけ図にしよう
- わかりやすくするための図がわかりにくい問題
- テーマを統一しよう
- いろんな画像混ぜない
- 文字はでかいほどよい
- 自分自身で読み直す時必ず声に出したほうが問題に気づきやすい
- プロジェクタにうつしてレビューするとただ眺めるだけよりも多くの問題見えてくる
(※あくまでも僕個人の思い)
PHPカンファレンス運営スタッフの皆様、スライドづくりのご協力いただいたペパボの皆様、当日セッションを見に来てくれた方、ありがとうございました!!!
.
.
.
.
.
.
.
.
.
.
.
.
.
(おまけ)keynoteの 発表者ディスプレイをカスタマイズ
の残り時間設定が罠だった…w
15:00
で15分じゃなくて15時間… どんなプレゼンだw
MacBook Pro 2016 late 13inch Tach Bar搭載モデルの社外充電器
スペック
以下のAnker製品で問題なく充電できました ※充電できることを保証するわけではない
Anker PowerLine+ USB-C & USB-C ケーブル (0.9m) 新しいMacBook / Nexus 5X / Nexus 6P他対応 https://www.amazon.co.jp/gp/product/B01GNQXIMG/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1
Anker PowerPort+ 5 USB-C Power Delivery (60W 5ポート Power Delivery搭載 USB&USB-C 急速充電器) 新しいMacBook / iPhone / iPad / Android 各種他対応 A2053511 https://www.amazon.co.jp/gp/product/B01C8LM7Y0/ref=oh_aui_detailpage_o03_s00?ie=UTF8&psc=1
正規品の半額くらいか。
mgemテンプレート作成からDocker上でビルドまで
こんばんは。最近mgemを作成する機会があり、何から始めたらいいのかわからなくて困ったので、一連の流れをまとめて備忘録にしたいと思います。 本作業はMacOSX Siera上で実施しています。 誤った記述があれば突っ込んでいただけると・・・
mgemとは?
非常にざっくりいうとCRubyの gem
のmruby版が mgem
CRubyのように gem install hoge
するわけではなく、mgemでは依存ライブラリとして指定したうえで
mruby自身のコンパイル時に組み込み、 hoge mgemを組み込んだmrubyバイナリ
を作ることになる。
さっそく作る
1. matsumotory/mruby-mrbgem-template
を使用してmgemの雛形を作成する
詳細は http://blog.matsumoto-r.jp/?p=3923 など参照 基本的にリポジトリのREADME通りにやればOK
# リポジトリもってくる tahira at mac in ~/work ❯ git clone git@github.com:matsumotory/mruby-mrbgem-template.git 23:57 Cloning into 'mruby-mrbgem-template'... remote: Counting objects: 126, done. remote: Total 126 (delta 0), reused 0 (delta 0), pack-reused 126 Receiving objects: 100% (126/126), 20.05 KiB | 0 bytes/s, done. Resolving deltas: 100% (59/59), done. tahira at mac in ~/work ❯ cd mruby-mrbgem-template/ 3s 106ms # template_config.rbを編集する tahira at mac in ~/work/mruby-mrbgem-template on master ❯ vi template_config.rb tahira at mac in ~/work/mruby-mrbgem-template on master ❯ cat -n template_config.rb 29s 69ms 1 params = { 2 :mrbgem_name => 'mruby-example', 3 :license => 'MIT', 4 :github_user => 'tap1ra', 5 :mrbgem_prefix => File.expand_path('.'), 6 :class_name => 'Example', 7 :author => 'tap1ra', 8 } 9 10 c = MrbgemTemplate.new params 11 c.create # rake実行 いろいろ走って mruby-example が作成される # 完了するとその後叩くコマンドも全部出力される tahira at mac in ~/work/mruby-mrbgem-template on master [?] ❯ rake cd /Users/tahira/work/mruby-mrbgem-template/mruby-example # ★これがmgemを作成していくリポジトリ git init git add . git commit -m "first commit" git remote add origin git@github.com:tap1ra/mruby-example.git git push -u origin master > finally, pull-request mruby-example.gem to mgem-list https://github.com/bovi/mgem-list # あらかじめ自身のGithubアカウントに mruby-example リポジトリを作成し、上記コマンドを実行していく tahira at mac in ~/work/mruby-mrbgem-template on master [?] ❯ cd /Users/tahira/work/mruby-mrbgem-template/mruby-example 33s 125ms tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ git init 00:03 Initialized empty Git repository in /Users/tahira/work/mruby-mrbgem-template/mruby-example/.git/ tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ git add . 00:03 tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [+] ❯ git commit -m "first commit" 00:03 [master (root-commit) 451b629] first commit 10 files changed, 188 insertions(+) create mode 100644 .travis.yml create mode 100644 .travis_build_config.rb create mode 100644 LICENSE create mode 100644 README.md create mode 100644 mrbgem.rake create mode 100644 mrblib/mrb_example.rb create mode 100644 mruby-example.gem create mode 100644 src/mrb_example.c create mode 100644 src/mrb_example.h create mode 100644 test/mrb_example.rb tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master ❯ git remote add origin git@github.com:tap1ra/mruby-example.git 00:03 tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master ❯ git push -u origin master # これでmgemの雛形がリポジトリにpushされた
2. 雛形のなかみ
# 何も手を加えていない状態が以下 tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master ❯ ls -la 00:24 total 48 drwxr-xr-x 12 tahira 2033490572 408 5 19 00:03 . drwxr-xr-x 18 tahira 2033490572 612 5 19 00:00 .. drwxr-xr-x 12 tahira 2033490572 408 5 19 00:24 .git -rw-r--r-- 1 tahira 2033490572 329 5 19 00:00 .travis.yml -rw-r--r-- 1 tahira 2033490572 121 5 19 00:00 .travis_build_config.rb -rw-r--r-- 1 tahira 2033490572 1135 5 19 00:00 LICENSE -rw-r--r-- 1 tahira 2033490572 504 5 19 00:00 README.md -rw-r--r-- 1 tahira 2033490572 110 5 19 00:00 mrbgem.rake # 作成するmgemが依存する他のmgemの情報を記載するファイル drwxr-xr-x 3 tahira 2033490572 102 5 19 00:00 mrblib # この下に作成するmgemのmrubyソースコードを配置 -rw-r--r-- 1 tahira 2033490572 181 5 19 00:00 mruby-example.gem drwxr-xr-x 4 tahira 2033490572 136 5 19 00:00 src # この下に作成するmgemのCソースコードを配置 drwxr-xr-x 3 tahira 2033490572 102 5 19 00:00 test
3. 手元でビルドできるようRakefileを作成する
# .travis_build_config.rb と [https://github.com/pyama86/mruby-acme-client/blob/master/build_config.rb] などを真似して自身を組み込んだmrubyをビルドできるようbuild_config.rbを作成する # 多少違うかもしれないけど以下のようになっているはず ❯ cat .travis_build_config.rb 00:10 MRuby::Build.new do |conf| toolchain :gcc conf.gembox 'default' conf.gem '../mruby-example' conf.enable_test end # 「conf.gem '../mruby-mrbgem-template'」この行で依存gemとして自分自身を読ませている(認識違ったら突っ込んで…) # よって以下のようなbuild_config.rbを書く tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat -n build_config.rb 9s 854ms 1 MRuby::Build.new do |conf| 2 toolchain :gcc 3 conf.gembox 'default' 4 conf.gem File.expand_path(File.dirname(__FILE__)) 5 conf.enable_test 6 end # その上で「build_config.rbに↑で作成したものを利用してmrubyをビルドする」タスクの定義をおこなう tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat -n Rakefile 00:33 1 MRUBY_CONFIG=File.expand_path("build_config.rb") 2 MRUBY_VERSION="master" 3 desc "mrubyをビルドするタスク" 4 file :mruby do 5 sh "git clone --depth=1 git://github.com/mruby/mruby.git" 6 Dir.chdir("./mruby") do 7 sh "git checkout #{MRUBY_VERSION} || true" 8 end 9 end 10 11 desc "build_config.rbを指定して(指定しないとmruby標準のものが利用されるので)ビルドするタスク" 12 task :compile => :mruby do 13 sh "cd mruby && MRUBY_CONFIG=#{MRUBY_CONFIG} rake all" 14 end
4. ここまでやるとmruby-exampleを組み込んだmrubyのビルドができるのでタスクを実行する
tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ rake compile 209ms git clone --depth=1 git://github.com/mruby/mruby.git Cloning into 'mruby'... remote: Counting objects: 468, done. remote: Compressing objects: 100% (377/377), done. remote: Total 468 (delta 35), reused 216 (delta 13), pack-reused 0 Receiving objects: 100% (468/468), 474.67 KiB | 68.00 KiB/s, done. Resolving deltas: 100% (35/35), done. git checkout master|| true Already on 'master' Your branch is up-to-date with 'origin/master'. cd mruby && MRUBY_CONFIG=build_config.rb rake all ・・・(中略) ビルド完了 tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ ll mruby/bin/mruby 00:36 -rwxr-xr-x 1 tahira 2033490572 920760 5 19 00:35 mruby/bin/mruby # mruby-exampleで定義されたクラスは以下のようになっている tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat mrblib/mrb_example.rb 00:36 class Example def bye self.hello + " bye" end end # 以下のようなテストファイルを用意して作成したmrubyバイナリで実行すると、mrubyバイナリにmruby-exampleが組み込まれていれば結果が出力される tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat -n example.rb 10:30 1 example = Example.new('hoge') 2 p example.bye tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ mruby/bin/mruby example.rb 10:30 "hoge bye" # mruby-exampleが組み込まれていなければ以下のようにclass定義がみつからずエラーになる tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ mruby/bin/mruby example.rb 1m 7s 814ms trace: [0] example.rb:1 example.rb:1:uninitialized constant Example (NameError)
5. Linux上で実行できるバイナリを作るため、Dockerでビルドさせる
今回Mac上で作業したので、アーキテクチャが異なるため(?)Linux上では実行することができません (同様にLinux上でビルドしたものはMac上で実行しようとすると「cannot execute binary file」のように怒られます)
# 以下のようなDockerfileを作成します # centos latestを使用し、ビルドに必要なライブラリをインストールしているだけのきれいな環境 tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat -n Dockerfile 13s 155ms 1 FROM centos:latest 2 3 RUN yum install -y \ 4 epel-release \ 5 gcc \ 6 git \ 7 openssl-devel \ 8 ca-certificates \ 9 rubygems \ 10 curl \ 11 bison 12 13 RUN gem install \ 14 mgem \ 15 rake # Rakefileに以下のようにdockerでビルドするためのタスクの定義をおこないます # 1. ↑で作成したDockerfileを使用してexample:mrubyコンテナを作成 # 2. mruby-example作業ディレクトリをdockerコンテナ上にバインドマウントさせ、あとはMac上で行うときと同様にrake compileタスクを実行しています tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ cat -n Rakefile | tail -4 20s 100ms 16 task :build do 17 sh "docker build -t example:mruby ." 18 sh "docker run -v `pwd`:/tmp -w /tmp -t example:mruby rake compile" 19 end # rake buildタスクを実行すると無事mrubyバイナリが作成されました tahira at mac in ~/work/mruby-mrbgem-template/mruby-example on master [?] ❯ rake build 10:39 docker build -t example:mruby . Sending build context to Docker daemon 23.23 MB Step 1/3 : FROM centos:latest # 今ビルドしたものをMac上で実行すると、おこられます ❯ mruby/bin/mruby example.rb 54s 699ms Failed to execute process 'mruby/bin/mruby'. Reason: exec: Exec format error The file 'mruby/bin/mruby' is marked as an executable but could not be run by the operating system.
今回はmgem自身の実装は全く触れていませんが、とりあえず開発ができる準備ができました。 おしまい
(便利情報)
local_buildみたいなオプションでRakefileも作ってくれる。あとmrubyはクロスコンパイル出来る。RT:mgemテンプレート作成からDocker上でビルドまで - カメニッキ https://t.co/JzrO708vo6
— P山 (@pyama86) 2017年5月19日
(2017/05/19 16:09追記) github.com
テンプレート作成時にRakefileも標準で作成されるようにPRして、マージされました!!