この記事について
本記事は、2023年5月に社内で実施した勉強会の資料を基に、社外向けに再編集したものです。
記載の情報は執筆当時のものであり、最新の仕様やベストプラクティスとは異なる場合があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認ください。
はじめに
普段の業務でRailsのActiveRecordを利用していると、便利なメソッドに助けられる一方で、思わぬ落とし穴に出会うこともあります。
本記事では、業務を通じて得た知見を整理し、共有いたします。
基本動作の確認
基本的に、ActiveRecordのSQL発行はインスタンス変数の中身を参照したタイミングで行われます。
それまではオブジェクトとして保持されているため、定義後に.where
を追加したり、ransack
といった検索用gemで.result
を呼び出すことも可能です。
詳細は公式サイトをご参照ください。
参考:Railsガイド「Active Record の基礎」
https://railsguides.jp/active_record_basics.html
便利なインスタンスメソッド
ActiveRecord::Base.instance_methods
をコンソール上で実行すると多数表示されますが、その中から利用頻度の高いものを抜粋します。
changed?
オブジェクトが作成されてから、いずれかの属性が更新されているかどうかxxx_changed?
オブジェクトが作成されてから、xxxという属性が更新されているかどうかxxx_was
オブジェクト作成時のxxxという属性に格納されていた値。
なお、これらは現在非推奨となっており、現在は以下のメソッドの利用を推奨されてます。
出典:Qiita – @htz – Rails 5.1 以降で attribute_was, attribute_change, attribute_changed?, changed?, changed 等が DEPRECATION WARNING #Rails
https://qiita.com/htz/items/56798d53ec5988733fc6
代替メソッドは以下の通りです。
changed?
→has_changes_to_save?
xxx_changed?
→will_save_change_to_xxx?
xxx_was
→xxx_in_database
これらを整理した情報は以下のサイトにまとめられています。
出典:Zenn – megeton – ActiveRecord の change 系メソッドまとめ
https://zenn.dev/megeton/articles/425184479e6364
上記で触れた非推奨のxxx_was
系メソッドがすべて置き換え後の形で記載されているため、初めからこちらを参照するのが望ましいでしょう。
また、こちらのサイトには、create(x: "a")
, update(x: "b")
, update({})
実行時、before_save
やafter_save
などのコールバック内で各種インスタンスメソッドを呼び出した際の戻り値についても整理されています。
実際の業務開発で利用されることが多いメソッドを以下に示します。
will_save_change_to_attribute?(:x)
登録や更新がされるかどうかattribute_before_last_save
処理実行前のDB登録値attributes_in_database
DBに登録されている値
例えば、関連先IDを保持するカラムxxx_id
が UPDATE された際に関連先レコードへの処理が必要な場合、after_save
内でattribute_before_last_save(:xxx_id)
を利用することで、更新前のレコードIDを取得できます。
便利なクラスメソッド
.includes
関連オブジェクトをあらかじめ取得し、マッピングを行います。
一覧表示の際に1行ごとにSQLが発行される問題(N+1問題)を防ぎ、処理の遅延を回避するため、業務システムでは頻繁に利用されています。
.where
引数にはさまざまなバリエーションがあります。
文字列を第一引数に渡す場合、同じ数値を繰り返し利用する際には?
ではなく:name
を使用し、引数にHashを渡す方法が推奨されます。
また、Rangeを渡すことで 'xxx ≥ :start_val and xxx < end_val'
と毎回記述する必要がなくなります。
さらに、FromやToのいずれかが空である場合でも、自動的に適切なSQLに変換されます。
参考:TechRacho – hachi8833 – ActiveRecordで日付・時刻の範囲検索をシンプルに書く方法
https://techracho.bpsinc.jp/hachi8833/2023_04_12/24876
日付の範囲指定を簡潔に記述できるメソッド群
参考:Qiita – @terufumi1122 -【知らずに損した】ActiveSupportの期間指定メソッドall_day, all_week, all_month, all_year
https://qiita.com/terufumi1122/items/3aa21c20eeacbce33b93
注意すべきポイント
.pluck()
関連先レコードのカラム値は、データベースに登録されていない場合、pluck
では取得できません。.map
を用いて取得してください。
irb(main):010:0> shop = Shop.new
=> #<Shop id: nil, name: nil, memo: nil, rent: nil, created_at: nil, updated_at: nil>
irb(main):011:0> shop.cats = [Cat.new(name: 'new_cat')]
=> [#<Cat id: nil, name: "new_cat", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>]
irb(main):012:0> shop.cats << Cat.new(name: 'new_cat2')
=> #<ActiveRecord::Associations::CollectionProxy [#<Cat id: nil, name: "new_cat", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>, #<Cat id: nil, name: "new_cat2", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>]>
irb(main):013:0> shop.cats << Cat.new(name: 'new_cat3')
=> #<ActiveRecord::Associations::CollectionProxy [#<Cat id: nil, name: "new_cat", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>, #<Cat id: nil, name: "new_cat2", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>, #<Cat id: nil, name: "new_cat3", color: nil, memo: nil, fee: nil, created_at: nil, updated_at: nil, shop_id: nil>]>
irb(main):014:0> shop.cats.pluck(&:name)
=> []
irb(main):015:0> shop.cats.map(&:name)
=> ["new_cat", "new_cat2", "new_cat3"]
参考:ActiveRecord::Associations::CollectionProxy
https://edgeapi.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html
(データベースに登録されているレコードを取得する場合は、pluck
を使用できます。)
.or()
複数の.or()
を繋げる際に、想定とは異なる動作が確認されました。
irb(main):022:0> puts Shop.where('rent < 500000').to_sql
SELECT "shops".* FROM "shops" WHERE (rent < 500000)
=> nil
irb(main):023:0> puts Shop.where('rent < 50000').or(Shop.where('rent > 80000').where('exists (select * from cats where cats.shop_id = shop.id and cats.color="white")')).to_sql
SELECT "shops".* FROM "shops" WHERE ((rent < 50000) OR (rent > 80000) AND (exists (select * from cats where cats.shop_id = shop.id and cats.color="white")))
=> nil
# 全部orで繋げた例
irb(main):001:0> puts Shop.where('rent < 50000').or(Shop.where('rent > 80000')).or(Shop.where('exists (select * from cats where cats.shop_id = shop.id and cats.color="white")')).to_sql
SELECT "shops".* FROM "shops" WHERE (((rent < 50000) OR (rent > 80000)) OR (exists (select * from cats where cats.shop_id = shop.id and cats.color="white")))
=> nil
2番目の例では、家賃が5万未満 または (家賃8万より上 かつ 白い猫がいるお店)
を検索した際に、(家賃が5万未満 または 家賃が8万より上) または 白い猫がいるお店
になってしまうのです。
Rails 7.0 でも同様の挙動が確認されましたので、注意が必要です。
回避方法は以下の通りです。
# 1個のwhereに詰め込めば回避できます
irb(main):006:0> puts Shop.where('rent < 50000').or(Shop.where('rent > 80000 and exists (select * from cats where cats.shop_id = shop.id and cats.color="white")')).to_sql
SELECT "shops".* FROM "shops" WHERE ((rent < 50000) OR (rent > 80000 and exists (select * from cats where cats.shop_id = shop.id and cats.color="white")))
=> nil
.where.not(複数)
Railsのバージョンによって挙動が異なる場合があります。
[3] pry(main)> puts Member.where.not(status: 2, deleted_at: nil).to_sql
SELECT "users".* FROM "users" WHERE "users"."type" = 'Member' AND "users"."status" != 2 AND "users"."deleted_at" IS NOT NULL
=> nil
[4] pry(main)> Rails.version
=> "6.0.3.3"
status ≠ 2 AND deleted_at IS NOT NULL
この結果は、AでもBでもない、つまりNORの集合を示しています。
ベン図で表すと次の通りです。

[1] pry(main)> puts Member.where.not(status: 2, deleted_at: nil).to_sql
SELECT "users".* FROM "users" WHERE "users"."type" = 'Member' AND NOT ("users"."status" = 2 AND "users"."deleted_at" IS NULL)
=> nil
[2] pry(main)> Rails.version
=> "7.0.4.3"
NOT( status = 2 AND deleted_at is null )
となりました。AかつB ではない、といったNANDの集合を指します。ベン図は以下。
結果が異なっている気がしますね。
同じ動作にしたい場合は、where('status != 2 AND deleted_at IS NOT NULL')
と記述する必要がありそうです。

引用:Wikipedia – ベン図
https://ja.wikipedia.org/wiki/ベン図#論理演算とベン図
その他、ActiveRecordはRailsのアップデートにより挙動が変わる場合があります。
詳細は以下のリンクをご参照ください。
出典:Rails ガイド – Ruby on Rails 6.1 リリースノート
https://railsguides.jp/6_1_release_notes.html#active-record
特に、
「名前付きスコープをチェーンしたときのスコープが、クラスレベルのクエリメソッドにリークしないようになった」
の部分については注意が必要そうです。
includes().where()
関連先を一緒に保持してくれる上に検索までしてくれる夢のような組み合わせだと考えていましたが、where
に文字列を渡す場合、includes
ではJOINされません。where
に引数をハッシュで渡すか、where
をどうしても文字列で書かざるを得ない場合はjoins
を利用しましょう。
# できてない(エラーになる)
irb(main):001:0> puts Shop.includes(:cats).where('cats.color = "white"').to_sql
SELECT "shops".* FROM "shops" WHERE (cats.color = "white")
=> nil
# whereにHashを渡す
irb(main):002:0> puts Shop.includes(:cats).where(cats: {color: 'white'}).to_sql
SELECT "shops"."id" AS t0_r0, "shops"."name" AS t0_r1, "shops"."memo" AS t0_r2, "shops"."rent" AS t0_r3, "shops"."created_at" AS t0_r4, "shops"."updated_at" AS t0_r5, "cats"."id" AS t1_r0, "cats"."name" AS t1_r1, "cats"."color" AS t1_r2, "cats"."memo" AS t1_r3, "cats"."fee" AS t1_r4, "cats"."created_at" AS t1_r5, "cats"."updated_at" AS t1_r6, "cats"."shop_id" AS t1_r7 FROM "shops" LEFT OUTER JOIN "cats" ON "cats"."shop_id" = "shops"."id" WHERE "cats"."color" = 'white'
=> nil
# includesをjoinsに変更
irb(main):004:0> puts Shop.joins(:cats).where('cats.color = "white"').to_sql
SELECT "shops".* FROM "shops" INNER JOIN "cats" ON "cats"."shop_id" = "shops"."id" WHERE (cats.color = "white")
=> nil
# ちなみに両方かくとこうなる
irb(main):003:0> puts Shop.includes(:cats).joins(:cats).where('cats.color = "white"').to_sql
SELECT "shops"."id" AS t0_r0, "shops"."name" AS t0_r1, "shops"."memo" AS t0_r2, "shops"."rent" AS t0_r3, "shops"."created_at" AS t0_r4, "shops"."updated_at" AS t0_r5, "cats"."id" AS t1_r0, "cats"."name" AS t1_r1, "cats"."color" AS t1_r2, "cats"."memo" AS t1_r3, "cats"."fee" AS t1_r4, "cats"."created_at" AS t1_r5, "cats"."updated_at" AS t1_r6, "cats"."shop_id" AS t1_r7 FROM "shops" INNER JOIN "cats" ON "cats"."shop_id" = "shops"."id" WHERE (cats.color = "white")
=> nil
ActiveRecord::Base.transaction do
中でエラーが発生すると、ロールバックしてくれるので一般的によく使用されているかと思います。
ただし、ネストして使いたい場合は、内部でraiseされるエラーの種類によって挙動が変わってくるので注意が必要です。
具体的には、親子関係にあるトランザクションがあるとして、子トランザクションの中で発生したエラーによって、親もロールバックする場合と、子だけロールバックする場合とに別れます。
- 子トランザクションの中で
ActiveRecord::Rollback
がraiseされた場合- 子トランザクションのみがロールバックする
- 親トランザクションは処理が進む
- 子トランザクションの中でそれ以外のエラー(StandardErrorとか)がraiseされた場合
- 子トランザクションがロールバック
- 親トランザクションもロールバック
この違いを利用して、「この処理はロールバックしても良いが、本処理はロールバックしてはいけない」といった業務上のケースに対応することが可能な場合もあります。
ただし、特殊なケースを除き、トランザクションが二重にならないように設計するのが無難です。
.concat()
関連レコードに対して実行すると、has_one
ではなくhas_many
の関係を持つ場合にレコードが増加します。push
と同じ挙動を示します。
過去の事例として、外部キーがUPDATEされるという現象が発生しました。
ドキュメントを確認しても原因は明確ではありませんが、そのような現象が発生する場合があるようです。
aaa.concat(key: xxx)
#=> UPDATE table SET key=XXX where (id=aaaのもの)
単に文字列を連結して表示したいだけであっても、表示のたびにレコードが増加したり更新されたりすると、大きな問題となります。
実際にそのような事象が発生し、対応が困難でした。
参考までに、公式のRDocを示します(concat
は<<
のエイリアスのため、<<
のページへのリンクです)。
参考:ActiveRecord::Associations::CollectionProxy
https://edgeapi.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-3C-3C
所感
本記事で紹介した各メソッドは、実際の業務でActiveRecordを扱う際に頻繁に利用されるものです。
これらの特性や注意点を理解して適切に活用することで、より安全で効率的な開発が実現できると考えます。