Slide 1

Slide 1 text

本当は怖い Rails の build_xxx @ giftee tech bash #2 (2019‑07‑30) kazama (@megane42) Solution dev. GCP

Slide 2

Slide 2 text

概要 Rails の build_xxx 系のメソッドが怖かったという話をします

Slide 3

Slide 3 text

今日の登場人物(モデル) User Blog user has_one blog

Slide 4

Slide 4 text

お題 ある user の持ち物としての blog インスタンスを作りたい

Slide 5

Slide 5 text

方法 create_xxx インスタンスを作って DB に即保存 user.create_blog(title: "foo", ...) build_xxx インスタンスは作るけど DB に保存はしない 今日の主役 user.build_blog(title: "foo", ...)

Slide 6

Slide 6 text

疑問 has_one だけど何度も繰り返したらどうなるの?

Slide 7

Slide 7 text

create の場合 まず新しい blog レコードを保存 次に古い blog レコードの外部キー (user_id) を null にする dependent: :destroy のときはここが DELETE になる 結果的に子レコードは 1 つに保たれるので、まあわかる # 2 回目の user.create_blog を実行したときの SQL begin transaction INSERT INTO "blogs" ("user_id", "created_at", "updated_at" commit transaction begin transaction UPDATE "blogs" SET "user_id" = ?, "updated_at" = ? WHERE commit transaction

Slide 8

Slide 8 text

build の場合 # 1 回目 blog = user.build_blog blog.save # 2 回目 blog = user.build_blog さて何が起きるでしょう?

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

結果 まさかの 更新系 が走る dependent: :destroy の場合は DELETE が走る! このあと結局 blog.save をしなかった場合 or 失敗した場合、当 然旧レコードは更新されっぱなしのまま build_xxx は DB を更新しない、という直感に反している # 2 回目の blog = user.build_blog を実行したときの SQL begin transaction UPDATE "blogs" SET "user_id" = ?, "updated_at" = ? WHERE commit transaction

Slide 11

Slide 11 text

対策 new を使う build_xxx まで含めてトランザクションを張る build_xxx が更新系を実行しうる、という認識を持っておく transaction do blog = user.build_blog blog.some_operation blog.save end

Slide 12

Slide 12 text

余談 : ドキュメントにはどう書いてある? Account#build_beneficiary (similar to Beneficiary.new(account_id: id) ) シミラートゥー(イコールとは言ってない) https://api.rubyonrails.org/classes/ActiveRecord/Associations/Cla ssMethods.html#method‑i‑has_one

Slide 13

Slide 13 text

余談 : create_xxx も怖くね? なぜか トランザクションが分かれている INSERT 後の UPDATE or DELETE に失敗するとゴミレコードが残る 例えば belongs_to: に optional: true を付け忘れると UPDATE に失敗する これもトランザクション張った方がいいのかも? # 2 回目の user.create_blog を実行したときの SQL (再掲) begin transaction INSERT INTO "blogs" ("user_id", "created_at", "updated_at" commit transaction begin transaction UPDATE "blogs" SET "user_id" = ?, "updated_at" = ? WHERE commit transaction

Slide 14

Slide 14 text

まとめ build_xxx は 更新系 を発行しうる create_xxx も怖かった