Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails API: ActiveRecord::AutosaveAssociation(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

訳文には適宜強調を加えています。

Rails API: ActiveRecord::AutosaveAssociation(翻訳)

AutosaveAssociationは、親がsaveされるときに、関連付けられているレコードも自動的にsaveされるようにするモジュールです。saveに加えて、mark_for_destruction済みの関連付けレコードのdestroyも行います(mark_for_destructionおよびmarked_for_destruction?を参照)。

親とその関連付けのsave、およびmark_for_destruction済みの関連付けレコードのdestroyは、すべて1個のトランザクション内で行われます。これにより、データベースの状態が不整合にならないようにしています。

関連付けでバリデーションが1個以上失敗すると、そのエラーメッセージは親に適用されます。

これは、「mark_for_destruction済みの関連付けが直接destroyされることはない」という意味でもある点にご注意ください。ただし、バリデーションが失敗したときの関連付けは引き続きmark_for_destruction済みのままになります。

また、「autosave: falseを宣言する」ことと「:autosaveを宣言しない」ことは同じでない点にもご注意ください。:autosaveオプションが存在しない後者の場合、新規の関連付けはsaveされますが、更新された関連付けはsaveされません。

🔗 バリデーション

子レコードは、validate: falseを指定しない限りバリデーションされます。

🔗 コールバック

自動保存オプションを指定した関連付けでは、さまざまなコールバック(around_savebefore_saveafter_createafter_update)をモデルに定義します。コールバックは「モデルでの定義順」で実行されることにご注意ください。また、自動保存コールバックが実行される前に関連付けの内容を変更することは避けてください。通常であれば、関連付けより後にコールバックを配置することをおすすめします。

🔗 1対1の例

class Post < ActiveRecord::Base
  has_one :author, autosave: true
end

上のように書くことで、親の変更とそれに関連付けられたモデルの変更が、自動的かつアトミックにsaveされるようになります。

post = Post.find(1)
post.title       # => "The current global position of migrating ducks"
post.author.name # => "alloy"

post.title = "On the migration of ducks"
post.author.name = "Eloy Duran"

post.save
post.reload
post.title       # => "On the migration of ducks"
post.author.name # => "Eloy Duran"

関連付けられたモデルを親のsave操作の一環としてdestroyするときは、以下のようにmark_for_destructionでマーキングするだけで簡単に行なえます。

post.author.mark_for_destruction
post.author.marked_for_destruction? # => true

ただし、上の時点ではデータベースからまだ削除されていません

id = post.author.id
Author.find_by(id: id).nil? # => false

post.save
post.reload.author # => nil

saveすると実際にデータベースから削除されます。

Author.find_by(id: id).nil? # => true

🔗 1対多の例

:autosaveオプションが宣言されていない場合、親がsaveされたときに子が新規レコードの場合にのみsaveされます。

class Post < ActiveRecord::Base
  has_many :comments # :autosaveオプションを宣言していない
end

post = Post.new(title: 'ruby rocks')
post.comments.build(body: 'hello world')
post.save # => postもcommentもsaveされる

post = Post.create(title: 'ruby rocks')
post.comments.build(body: 'hello world')
post.save # => postもcommentもsaveされる

post = Post.create(title: 'ruby rocks')
comment = post.comments.create(body: 'hello world')
comment.body = 'hi everyone'
post.save # => postはsaveされるがcommentはsaveされない

:autosave: trueを指定すると、子は新規レコードかどうかにかかわらず、すべてsaveされます。

class Post < ActiveRecord::Base
  has_many :comments, autosave: true
end

post = Post.create(title: 'ruby rocks')
comment = post.comments.create(body: 'hello world')
comment.body = 'hi everyone'
post.comments.build(body: "good morning.")
post.save # => postも2つのcommentもsaveされる

関連付けられたモデルの1つを親のsave操作の一環としてdestroyするときは、以下のようにmark_for_destructionでマーキングするだけで簡単に行なえます。

post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
post.comments[1].mark_for_destruction
post.comments[1].marked_for_destruction? # => true
post.comments.length # => 2

ただし、上の時点ではデータベースからまだ削除されていません

id = post.comments.last.id
Comment.find_by(id: id).nil? # => false

post.save
post.reload.comments.length # => 1

saveすると実際にデータベースから削除されます。

Comment.find_by(id: id).nil? # => true

🔗 注意

レコード自体が変更されたときに自動保存がトリガーされるのは、関連付けレコードが既に永続化済みの場合だけであることにご注意ください。その理由は、関連付けのバリデーションが循環することで引き起こされるSystemStackErrorから保護するためです。これには1つ例外があり、カスタムのバリデーションコンテキストが利用されている場合は、関連付けされたレコードに対して常にバリデーションが実行されます。

🔗 メソッド

🔗 インスタンスpublicメソッド

🔗 changed_for_autosave?()

このレコードが何らかの方法で変更されたかどうかを返します(ネストした自動保存関連付けが同様に変更されたかどうかについても返します)。

🔗 destroyed_by_association()

destroyされる親の関連付けを返します1

カウンタキャッシュが不必要に更新されるのを避けたいときに利用します。

🔗 destroyed_by_association=(reflection)

このレコードのdestroyをトリガーするのに使う特定の関連付けを指定します。指定した関連付けは、そのエンティティがdestroyされたときに、このレコードをmark_for_destructionでマーキングするのに使われます。

🔗 mark_for_destruction()

このレコードが、親のsaveトランザクションの一環としてdestroyされるようマーキングします。実際には、これによってレコードが即座にdestroyされるのではなく、親.saveが呼び出されたときに子レコードがdestroyされます。

このメソッドは、この関連付けられているモデルで親の:autosaveオプションが有効になっている場合にのみ有用です。

🔗 marked_for_destruction?()

このレコードが親のsaveトランザクションの一環としてdestroyされるかどうかを返します。

このメソッドは、この関連付けられているモデルで親の:autosaveオプションが有効になっている場合にのみ有用です。

🔗 reload(options = nil)

オブジェクトの属性を通常どおり再読み込みし、marked_for_destructionフラグをクリアします。

関連記事

Rails APIドキュメント: Active Recordのトランザクション(翻訳)

Rails API: ActiveRecord::NestedAttributes(翻訳)

Rails API: ActiveSupport::ConcernとModule::Concerning(翻訳)


  1. 訳注: 具体的には、destroyされる親が存在する場合は、その親との関連付けを返します。destroyされる親がない場合はnilを返します。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。