ActiveRecordが提供する has_many
、has_one
、belongs_to
といったアソシエーションの設定を行うメソッドには autosave
というオプションがある。
APIドキュメント を見るとオプションの効果がおおよそわかるのだけど、いまいち理解しきれた感は得られなかったので、手元で検証してみることにした。
特に、autosaveオプションの設定によって関連する子モデルごとまとめて新規作成・更新しようとしたときにどのように振る舞いが変わるかについて調べている。
検証時の環境は次の通り。
❯ ruby -v ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin18] ❯ bundle exec rails -v Rails 6.1.1
最初に結論
以下が検証結果の簡単なまとめになる。
未指定の場合、子モデル側にバリデーションエラーがあった場合の挙動が結構やばいと思ったので、通常 autosave: true
をつけるのが良さそうという見解を得られた。
autosave
未指定時- 新規作成は子モデル含めて操作することができる
- 更新時は子モデル側の変更は無視される
- 新規作成時、子モデルにバリデーションエラーがあった場合、子モデルだけエラーで保存されず親モデルは保存される
autosave: true
の時- 新規作成も更新も子モデル含めて操作する
- 子モデルにバリデーションエラーがあった場合、新規作成時も更新時も全体がロールバックされる。
autosave: false
の時- 新規作成も更新時も子モデルの変更は無視される
検証に利用するModelと関連設定
Company
モデルが Website
モデルを1つ持つ、というhas_oneの関係のモデルを検証に使う。
# app/models/company.rb class Company < ApplicationRecord has_one :website # ★ ここに autosave オプションを付けたりはずしたり未指定にしたりする validates :name, presence: true end
# app/models/website.rb class Website < ApplicationRecord belongs_to :company validates :url, presence: true end
親モデル側を主体として子モデルをまとめて操作する、という使い方をすることが多いと思うので、autosave
はCompany側で設定する。
autosave
オプション未指定時
まずは、autosaveオプションを明示的に指定しない場合。
子モデルごと新規作成
子モデルごとまとめて作成される。
company = Company.new(name: 'foo') company.build_website(url: 'https://example.com') company.save! # TRANSACTION (0.1ms) SAVEPOINT active_record_1 # Company Create (1.3ms) INSERT INTO "companies" ("name", "created_at", "updated_at") VALUES (?, ?, ?) ... # Website Create (0.4ms) INSERT INTO "websites" ("url", "company_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) ... # TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1 # => true
子モデルごと更新
子モデルの更新は無視され、親モデル側だけ更新される。
company = Company.last company.name = 'bar' company.website.url = 'https://www.mogulla3.tech' company.save! # TRANSACTION (0.1ms) SAVEPOINT active_record_1 # Company Update (0.5ms) UPDATE "companies" SET "name" = ?, "updated_at" = ? WHERE "companies"."id" = ? ... # TRANSACTION (0.2ms) RELEASE SAVEPOINT active_record_1 # => true p company.reload.website.url # => "https://example.com"
子モデルにバリデーションエラーが発生する場合
更新時は子モデルはそもそも無視されるので、新規作成時に子モデル側でバリデーションエラーが起こった場合にどうなるか?という話。
子モデル側でバリデーションエラーになる場合、親モデルの新規作成処理だけ行われる。 ここの振る舞いは、知らないと結構やばいなと思った。
company = Company.new(name: 'foo') company.build_website(url: nil) # urlは必須なのでバリデーションエラーに引っかかる company.save! # TRANSACTION (0.2ms) SAVEPOINT active_record_1 # Company Create (1.0ms) INSERT INTO "companies" ("name", "created_at", "updated_at") VALUES (?, ?, ?) ... # TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 # => true p company.reload.website # => nil p company.website.errors # => #<ActiveModel::Errors:0x00007fcf19d4be40 @base=#<Website:0x00007fcf19440c10 id: nil, url: nil, company_id: 1, created_at: nil, updated_at: nil>, @errors=[#<ActiveModel::Error attribute=url, type=blank, options={}>]>
エラーは子モデル側に格納されている。
autosave: true
の時
次は autosave: true
を指定した場合
class Company < ApplicationRecord - has_one :website + has_one :website, autosave: true validates :name, presence: true end
子モデルごと新規作成
子モデルごとまとめて作成される。autosave未指定時と同じ挙動。
company = Company.new(name: 'foo') company.build_website(url: 'https://example.com') company.save! # TRANSACTION (0.1ms) SAVEPOINT active_record_1 # Company Create (1.1ms) INSERT INTO "companies" ("name", "created_at", "updated_at") VALUES (?, ?, ?) ... # Website Create (0.9ms) INSERT INTO "websites" ("url", "company_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) ... # TRANSACTION (0.3ms) RELEASE SAVEPOINT active_record_1 # => true
子モデルごと更新
未指定時と違い、子モデルごとまとめて更新される。
company = Company.last company.name = 'bar' company.website.url = 'https://www.mogulla3.tech' company.save! # TRANSACTION (0.1ms) SAVEPOINT active_record_1 # Company Update (0.4ms) UPDATE "companies" SET "name" = ?, "updated_at" = ? WHERE "companies"."id" = ? ... # Website Update (0.1ms) UPDATE "websites" SET "url" = ?, "updated_at" = ? WHERE "websites"."id" = ? ... # TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 # => true p company.reload.website.url # => "https://www.mogulla3.tech"
子モデルにバリデーションエラーが発生する場合
未指定時と違い、子モデルにバリデーションエラーが発生するような場合、新規作成処理も更新処理もロールバックされるため、親モデル側のみ中途半端に作られるようなことは起きない。
新規作成時
company = Company.new(name: 'foo') company.build_website(url: nil) # urlは必須なのでバリデーションエラーに引っかかる company.save! ActiveRecord::RecordInvalid: Validation failed: Website url can't be blank
更新時
company = Company.last company.name = 'bar' company.website.url = nil company.save! ActiveRecord::RecordInvalid: Validation failed: Website url can't be blank
autosave: false
の時
最後に autosave: false
を指定した場合。検証せずとも子モデル側は無視されることが予想がつくが、念のためやっておく。
class Company < ApplicationRecord - has_one :website + has_one :website, autosave: false validates :name, presence: true end
子モデルごと新規作成
子モデルの作成処理は無視される。
company = Company.new(name: 'foo') company.build_website(url: 'https://example.com') company.save! # TRANSACTION (0.2ms) SAVEPOINT active_record_1 # Company Create (0.8ms) INSERT INTO "companies" ("name", "created_at", "updated_at") VALUES (?, ?, ?) ... # TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 # => true
子モデルごと更新
子モデルの更新処理は無視される。
company = Company.last company.name = 'bar' company.website.url = 'https://www.mogulla3.tech' company.save! # TRANSACTION (0.4ms) SAVEPOINT active_record_1 # Company Update (0.5ms) UPDATE "companies" SET "name" = ?, "updated_at" = ? WHERE "companies"."id" = ? ... # TRANSACTION (0.1ms) RELEASE SAVEPOINT active_record_1 # => true p company.reload.website.url # => "https://example.com"