昔々あるところに、おじいさんとおばあさんが Web アプリケーション開発をしていました。
おじいさんはバックエンドを、おばあさんはフロントエンドを担当していました。
ある日突然、おばあさんはおじいさんに次のような仕様を言い渡しました。
「おじいさんや、User
の email
が更新された際に some_function
が実行されるようにしておくれ。」
急な仕様変更に戸惑いながらも不承不承ながら了承したおじいさんは、とりあえず現在の実装を確認しました。
user.rb
を開くと、以下のように実装されていました。
after_update :some_function, if: :saved_change_to_name?
def some_function
p "some_function called!"
end
上記では name
が更新された後に some_function
が実行されるようになっています。
おじいさんは「:saved_change_to_name?
を :saved_change_to_email?
に変えた行を追加すればよいのでは?」と考え、以下のように変更しました。
after_update :some_function, if: :saved_change_to_name?
+ after_update :some_function, if: :saved_change_to_email?
def some_function
p "some_function called!"
end
念のため rails console
で email
を変更して動作確認をしてみました。
irb(main):005:0> u.update(email: "test2@example.com")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test2@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.7ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "test2@example.com"], ["updated_at", "2022-06-20 13:10:03.984894"], ["id", 1]]
"some_function called!"
TRANSACTION (5.5ms) commit transaction
=> true
コンソールに "some_function called!"
が表示されていることを確認したおじいさんは「ヨシ!」と言いながらテストを追加して Pull Request を出そうとしたところ、CIで回していたテストが落ちていることに気づきました。
原因を調べたところ、name
を更新したときに some_function
が呼ばれなくなってしまったようです。
irb(main):002:0> u.update(name: "test")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test2@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (1.4ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "test"], ["updated_at", "2022-06-20 12:55:50.461921"], ["id", 1]]
TRANSACTION (5.7ms) commit transaction
=> true
さすがに雑に書きすぎたな、と反省したおじいさんは Rails ガイドを読んで、複数の条件を指定する方法を見つけました。
:if
と:unless
オプションは、procやメソッド名のシンボルの配列を受け取ることも可能です。
シンボルの配列を渡せることが分かったので、以下のように変更しました。
after_update :some_function, if: [:saved_change_to_name?, :saved_change_to_email?]
すると、name
と email
のどちらを変更しても、some_function
が呼ばれなくなってしまいました。
irb(main):004:0> u.update(name: "test1")
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test2@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.5ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "test1"], ["updated_at", "2022-06-20 13:12:59.072391"], ["id", 1]]
TRANSACTION (6.2ms) commit transaction
=> true
irb(main):005:0> u.update(email: "test@example.com")
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.4ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "test@example.com"], ["updated_at", "2022-06-20 13:13:44.376203"], ["id", 1]]
TRANSACTION (15.5ms) commit transaction
=> true
もしや、と思い試しに一度に両方変更したところ、"some_function called!"
が表示されました。
irb(main):008:0> u.update(name: "test3", email: "test3@example.com")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test3@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.4ms) UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "test3"], ["email", "test3@example.com"], ["updated_at", "2022-06-20 13:14:12.554928"], ["id", 1]]
"some_function called!"
TRANSACTION (6.7ms) commit transaction
=> true
if: [:saved_change_to_name?, :saved_change_to_email?]
のように書くと、両方の条件が満たされる場合にのみ実行されるようになることがわかりました。
もう一度 Rails ガイドを読み直したおじいさんは、 コールバックの条件に Proc
が指定できることに気づき、最終的に以下のようにすることでどちらかの条件が満たされる場合に some_function
が実行されるコードを実装することができました。
after_update :some_function, if: -> { saved_change_to_name? || saved_change_to_email? }
動作確認も問題なく、それぞれを変更した際に、some_function
が実行されました。
irb(main):002:0> u.update(name: "test")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.3ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test3@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (0.6ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "test"], ["updated_at", "2022-06-20 13:17:40.394976"], ["id", 1]]
"some_function called!"
TRANSACTION (8.9ms) commit transaction
=> true
irb(main):003:0> u.update(email: "test@example.com")
TRANSACTION (0.1ms) begin transaction
User Exists? (0.4ms) SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND "users"."id" != ? LIMIT ? [["email", "test@example.com"], ["id", 1], ["LIMIT", 1]]
User Update (1.4ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "test@example.com"], ["updated_at", "2022-06-20 13:17:45.752444"], ["id", 1]]
"some_function called!"
TRANSACTION (7.2ms) commit transaction
=> true
これでおじいさんは改めて Pull Request を提出し、おばあさんの機嫌を損ねることなく急な仕様変更に対応できました。
めでたしめでたし。
まとめ
- あるコールバック関数に対し、複数の条件がある場合、コールバックを複数に分けるのではなく一つにまとめる必要がある。
- 上書きされてしまうため、最後に指定したコールバックしか実行されない。
- Symbol を指定した場合
- 例:
after_update :some_function, if: :saved_change_to_name?
-
saved_change_to_name?
がtrue
のときに実行される
-
- 例:
- Array を指定した場合
- 例:
after_update :some_function, if: [:saved_change_to_name?, :saved_change_to_email?]
-
saved_change_to_name?
とsaved_change_to_email?
の両方がtrue
のときに実行される
-
- 例:
- Proc を指定した場合
- 例:
after_update :some_function, if: -> { saved_change_to_name? || saved_change_to_email? }
- Proc の結果が偽ではないときに実行される。
- 例: