コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

RailsでのActiveRecordへのfreeze, readonly, strict_loading の違いをざっくり整理する

「引数にActiveRecordを渡したいが、そこから操作されてほしくない」というようなときにどういうアプローチがあるかが気になったので、ざっくりまとめ。

検証した環境

$ bin/rails -v
Rails 7.1.2
$ ruby -v
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [aarch64-linux]

例示に使うモデル

今回は BookAuthor というモデルを例示に使う。定義は以下。

# == 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つを検証します。

  1. freeze - オブジェクトの変更を完全に禁止
  2. readonly! - データベースへの保存を禁止
  3. 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> 

なお、追加でのクエリ発行を抑制するだけなので、保存や代入のブロックは行わない

関連リンク