こんにちは、エンジニアの @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)
選択肢:
["Task 1"]
["Task 2"]
["Task 1", "Task 2"]
[]
解説
正解は 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"]
になります。
バージョンを上げたときのブログ記事: 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 は companies
を JOIN
をしているため、where
メソッドにて companies
の絞り込みを行うことができます。
preload
は JOIN
しないので、companies を参照しようとしてエラーになります。
includes
は内部で preload
と eager_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_action
は before_action
の配列の先頭にメソッドを追加するので(Array#prepend
と同じ)、今回のような場合は親が先に before_action
の前に追加し、子がさらにその前に追加します。
参考:
- actionpack/lib/abstract_controller/callbacks.rb#L171-L175
- activesupport/lib/active_support/callbacks.rb#L744
問題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 以降では関連先に変更がなければロードしないようになり、クエリの発行回数が減っています。
まとめ
Kaigi on Railsのコアコンセプトは 「初学者から上級者までが楽しめるWeb系の技術カンファレンス」ということで、馴染みのあるメソッドを中心に選んでみました。
ブースではこのクイズを起点にして、Rails を始めたばかりの方やコミッターなど幅広い方と盛り上がることができ、とても楽しかったです。
最後に、スタメンではエンジニアを募集しています。Rails 好きな方はもちろん、フロントエンドやモバイルアプリなど各技術領域で募集中なので、ご応募お待ちしております!