Kaigi on Rails 2023 スポンサーブースクイズの解説

こんにちは、エンジニアの @natsuokawai です。
先日開催された Kaigi on Rails 2023 にて、スタメンとしてスポンサーブースを出展しておりました。
遊びに来てくれた皆さんありがとうございました!

スタメンのブース

その際 Ruby on Rails に関するクイズを出題していたので、本記事ではそれらの解説を簡単にしたいと思います。

問題1

以下のコードを Rails 6.1 以前で実行した時の結果は?(Task は ActiveRecord オブジェクト、id は integer 型のカラムとする)

task1 = Task.create(title: "Task 1")
task2 = Task.create(title: "Task 2")
tasks = Task.where(id: task1.id).merge(Task.where(id: task1.id..task2.id)).pluck(&:title)

選択肢:

  1. ["Task 1"]
  2. ["Task 2"]
  3. ["Task 1", "Task 2"]
  4. []

解説

正解は 1. ["Task 1"] です!

弊社のサービス TUNAG(ツナグ)の Rails バージョンを 6.1 → 7.0 に上げる際に引っかかったので、今回問題にしてみました。

Rails 6.1 以前では、relation1.merge(relation2) とした際、relation1 と 2 の条件が AND で結合されたり、relation2 の条件で上書きされたりと、一貫性のない仕様になっていました。
上記コードでは AND で結合されるため 、tasks を取得する際に発行される SQL は SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = 1 AND "tasks"."id" BETWEEN 1 AND 2 となり、Task 1 のみが取得されます。

ちなみに Rails 7.0 以降では常に relation2 の条件で上書きされるように変更されたので、["Task 1", "Task 2"] になります。

詳細: Deprecate inconsistent behavior that merging conditions on the same column by kamipo · Pull Request #39328 · rails/rails · GitHub

バージョンを上げたときのブログ記事: TUNAG の Rails バージョンが 7.0 になりました - stmn tech blog

問題2

以下のアソシエーションを持つモデルにおいて、正しく動作しないものはどれか?

class User < ApplicationRecord
  belongs_to :company
end

class Company < ApplicationRecord
  has_many :users
  validates :name, presence: true
end

選択肢:
1. User.joins(:company).where(company: {name: 'stmn'}).count
2. User.preload(:company).where(company: {name: 'stmn'}).count
3. User.eager_load(:company).where(company: {name: 'stmn'}).count
4. User.includes(:company).where(company: {name: 'stmn'}).count

解説

正解は(正しく動作しないのは) 2. User.preload(:company).where(company: {name: 'stmn'}).count です!

N+1 問題の解消でお世話になるメソッドたちについてのクイズでした。
こちらは基礎的な問題だったので、正解者も多かったですね。

実際にコードを実行してクエリを見てみるとわかりやすいですが、1、3、4 は companiesJOIN をしているため、where メソッドにて companies の絞り込みを行うことができます。

preloadJOIN しないので、companies を参照しようとしてエラーになります。

includes は内部で preloadeager_load を呼び分けるメソッドです。上記のような companies の絞り込みクエリなどがない場合は preload と同じ挙動になります。

参考:

問題3

下記のようなコントローラーが定義されているとき、コールバックに定義されたメソッドが実行される順番として正しいものはどれか?

class ParentController < ApplicationController
  before_action :parent_before
  prepend_before_action :parent_prepend_before
end

class ChildController < ParentController
  before_action :child_before
  prepend_before_action :child_prepend_before
end

選択肢:
1. parent_prepend_before → child_prepend_before → child_before → parent_before
2. parent_prepend_before → child_prepend_before → parent_before → child_before
3. child_prepend_before → parent_prepend_before → child_before → parent_before
4. child_prepend_before → parent_prepend_before → parent_before → child_before

解説

正解は 4. child_prepend_before → parent_prepend_before → parent_before → child_before です!

コールバックの実行順序に関する問題です。レビューでこんなコードが来たら通さないよねと言われた回数第一位です (それはそう)

継承関係にあるとき、親→子の順でコールバックが実行されます。
また、prepend_before_actionbefore_action の配列の先頭にメソッドを追加するので(Array#prepend と同じ)、今回のような場合は親が先に before_action の前に追加し、子がさらにその前に追加します。

参考:

問題4

update! を実行したときのクエリの発行回数は Rails 7.0 以前と 7.1 でそれぞれ何回か?(Post.last で発行されるクエリは考慮しない)

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

post = Post.last
post.update!(title: "Rails is the best")

解説

正解は 「7.0 以前は 2 回、7.1 以降は 1 回」です!

こちらもまた Rails バージョンによる差異の問題になります。

optional: true ではない belongs_to 関連が定義されているとき、Rails 7.0 以前は update! のタイミングで関連先のレコードをロードしていました。
7.1 以降では関連先に変更がなければロードしないようになり、クエリの発行回数が減っています。

詳細: Avoid validating `belongs_to` association if it has not changed by fatkodima · Pull Request #46522 · rails/rails · GitHub

まとめ

Kaigi on Railsのコアコンセプトは 「初学者から上級者までが楽しめるWeb系の技術カンファレンス」ということで、馴染みのあるメソッドを中心に選んでみました。

ブースではこのクイズを起点にして、Rails を始めたばかりの方やコミッターなど幅広い方と盛り上がることができ、とても楽しかったです。

最後に、スタメンではエンジニアを募集しています。Rails 好きな方はもちろん、フロントエンドやモバイルアプリなど各技術領域で募集中なので、ご応募お待ちしております!

herp.careers