「引数にActiveRecordを渡したいが、そこから操作されてほしくない」というようなときにどういうアプローチがあるかが気になったので、ざっくりまとめ。
検証した環境
$ bin/rails -v Rails 7.1.2 $ ruby -v ruby 3.2.1 (2023-02-08 revision 31819e82c8) [aarch64-linux]
例示に使うモデル
今回は Book と Author というモデルを例示に使う。定義は以下。
# == Schema Information # # Table name: authors # # id :bigint not null, primary key # name :string(255) # created_at :datetime not null # updated_at :datetime not null # class Author < ApplicationRecord end # == Schema Information # # Table name: books # # id :bigint not null, primary key # title :string(255) # author_id :bigint not null # created_at :datetime not null # updated_at :datetime not null # class Book < ApplicationRecord belongs_to :author end
本記事で扱う3つのアプローチ
ActiveRecordオブジェクトの変更を制限する方法として、以下の3つを検証します。
freeze- オブジェクトの変更を完全に禁止readonly!- データベースへの保存を禁止strict_loading- 関連レコードの遅延読み込みを禁止
各メソッドごとのざっくりまとめ
freezeとは
freezeはRubyのObjectクラスに定義されているものであり、オブジェクトの変更を禁止するもの。定数などではよく使われるが、ActiveRecordにも適用できる。
例えば以下のように、Bookが持つattributeは更新できなくなる。
irb(main):001> book = Book.last Book Load (0.3ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1 => #<Book:0x0000ffffb14fd808 id: 1, title: "hon", author_id: 1, created_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00> irb(main):002> book.title = "test" => "test" irb(main):003> book.freeze => #<Book:0x0000ffffb14fd808 id: 1, title: "test", author_id: 1, created_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00> irb(main):004> book.title = "test2" /usr/local/bundle/gems/activemodel-7.1.2/lib/active_model/attribute_set.rb:59:in `write_from_user': can't modify frozen attributes (FrozenError) irb(main):005> book.save /usr/local/bundle/gems/activemodel-7.1.2/lib/active_model/attribute_set.rb:59:in `write_from_user': can't modify frozen attributes (FrozenError)
なお、メソッド自体に関してはObject#freeze (Ruby 3.4 リファレンスマニュアル)を参考のこと。
なお、関連付けられたオブジェクト(例:book.author)まではfreezeされないので、個別にfreezeする必要がある。
readonly!とは
ActiveRecordのメソッドの一つであり、更新作業をできない状態にするもの。
例えば以下のように代入は可能だが保存はできない状態にできる。
irb(main):001> book = Book.last Book Load (0.3ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1 => #<Book:0x0000ffff8970d8a0 id: 1, title: "hon", author_id: 1, created_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2024 03:44:36.248348000 UTC +00:00> irb(main):002> book.title = "test" => "test" irb(main):003> book.readonly! => true irb(main):004> book.title = "test2" => "test2" irb(main):005> book.save /usr/local/bundle/gems/activerecord-7.1.2/lib/active_record/persistence.rb:1281:in `_raise_readonly_record_error': Book is marked as readonly (ActiveRecord::ReadOnlyRecord) irb(main):006>
こちらも関連オブジェクトはreadonlyにはならないので、気をつける。
strict_loadingとは
Rails6.1から導入された機能で、N+1を抑止するための機能。あらかじめオブジェクトがロードされていない状態で関連を取ろうとするとエラーになる。
例えば以下のように includes などでオブジェクトを取得していない状態だと、関連レコードの情報を取ろうとするとエラーになる。
irb(main):001> book = Book.first Book Load (0.3ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 => #<Book:0x0000ffff7d9ef7f0 id: 1, title: "hon", author_id: 1, created_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00> irb(main):002> book.author Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1 LIMIT 1 => #<Author:0x0000ffff7dd8bb70 id: 1, name: "suzuki", created_at: Sat, 25 Oct 2025 03:43:52.517291000 UTC +00:00, updated_at: Sat, 25 Oct 2025 03:43:52.517291000 UTC +00:00> irb(main):003> strict_book = Book.strict_loading.first Book Load (1.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 => #<Book:0x0000ffff7dca2d08 id: 1, title: "hon", author_id: 1, created_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00> irb(main):004> strict_book.author /usr/local/bundle/gems/activerecord-7.1.2/lib/active_record/core.rb:230:in `strict_loading_violation!': `Book` is marked for strict_loading. The Author association named `:author` cannot be lazily loaded. (ActiveRecord::StrictLoadingViolationError)
ちゃんと取得していればエラーにならない。
irb(main):001> strict_book = Book.strict_loading.includes(:author).first Book Load (0.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1 Author Load (0.2ms) SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1 => #<Book:0x0000ffffadc2da50 id: 1, title: "hon", author_id: 1, created_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00, updated_at: Sat, 25 Oct 2025 03:44:36.248348000 UTC +00:00> irb(main):002> strict_book.author => #<Author:0x0000ffffae99dbb8 id: 1, name: "suzuki", created_at: Sat, 25 Oct 2025 03:43:52.517291000 UTC +00:00, updated_at: Sat, 25 Oct 2025 03:43:52.517291000 UTC +00:00> irb(main):003>
なお、追加でのクエリ発行を抑制するだけなので、保存や代入のブロックは行わない