「引数にActiveRecordを渡したいが、そこから操作されてほしくない」というようなときにどういうアプローチがあるかが気になったので、ざっくりまとめ。
検証した環境
$ bin/rails -v
Rails 7.1.2
$ ruby -v
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [aarch64-linux]
例示に使うモデル
今回は Book と Author というモデルを例示に使う。定義は以下。
class Author < ApplicationRecord
end
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
=>
irb(main):002> book.title = "test"
=> "test"
irb(main):003> book.freeze
=>
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
=>
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
=>
irb(main):002> book.author
Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1 LIMIT 1
=>
irb(main):003> strict_book = Book.strict_loading.first
Book Load (1.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 1
=>
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
=>
irb(main):002> strict_book.author
=>
irb(main):003>
なお、追加でのクエリ発行を抑制するだけなので、保存や代入のブロックは行わない
関連リンク