Rails のモデルを関連付けるタイミングの検証
はじめに
今日、Rails の関連モデルの保存について「お〜、すごい!」と思ったことがあったので備忘として残しておきます。
といっても、ほんと大したことない内容だと思います。はい。
環境
Mac OS X(Yosemite)
ruby 2.2.3
Ruby on Rails 4.2.4
関連付けのタイミングは永続化前でも大丈夫?
以下の親子関係をもつモデルがあります。
class Parent < ActiveRecord::Base has_many :children validates :name, presence: true, length: { maximum: 32 } end class Child < ActiveRecord::Base belongs_to :parent validates :name, presence: true, length: { maximum: 32 } end
クラス図はこんな感じです。
その時、以下のコードは親子関係を保ったまま保存されるでしょうか。
prent = Parent.new(name: 'Bob') child = parent.children.build(name: 'Tom') parent.save
正解は、親子関係が保持されたまま保存されます。
parent.save で child も同時に保存してくれます。
build 時に親モデルが保存されていなくても関連付けされる
上記のコードの save メソッド前後の child を表示してみましょう。
parent = Parent.new(name: 'Tom') child = parent.children.build(name: 'Bob') p child #<Child id: nil, parent_id: nil, name: "Bob", created_at: nil, updated_at: nil> parent.save p child #<Child id: 8, parent_id: 8, name: "Bob", created_at: "2016-01-12 13:41:13", updated_at: "2016-01-12 13:41:13">
っと、parent.save の実行前は parent が保存されていないので child.prent_id は nil ですが、saveメソッド実行後は child.parent_id に parent.id がセットされています。
素晴らしい!
これまで、子モデルを保存前の親モデルに関連付けても、親子関係は保持されたまま保存することはできないと思っていました。
下記のコードのような感じです。
parent = Parent.create(name: 'Bob') child = parent.children.build(name: 'Tom') p child #<Child id: nil, parent_id: 9, name: "Bob", created_at: nil, updated_at: nil> child.save
こちらは、child.save 前に parent_id がセットされています。
分かりやすいですね!
でも、このコード1つ問題があります。
トランザクション処理
このコードで問題になるケースは、child の保存に失敗した場合、parent のみ保存されてしまうことです。
これを回避するために、トランザクション処理を行うのが一般的です。
しかし、最初のコードならトランザクション処理は必要なく Rails がいい具合にやってくれます。
この辺りの内部処理がどのようになっているかはわからないので、また別の機会に処理の流れを確認してみたいです。
最後に、今回の処理を確認するための RSpec のコードを載せておきます。
require 'rails_helper' RSpec.describe Parent, type: :model do let(:parent) { Parent.new(name: 'Tom') } context '親子同時に保存する場合' do let(:child) { parent.children.build(name: name) } context '値にエラーがない場合' do let(:name) { 'Bob' } it '親子関係が維持されていること' do parent.save expect(child.persisted?).to be_truthy expect(child.parent_id).not_to be_nil expect(parent.reload.children).to eq [child] end end context '子にバリデーションエラーがある場合' do let(:name) { 'a' * 33 } it '親子ともに保存されないこと' do parent.save expect(parent.persisted?).to be_falsey expect(child.persisted?).to be_falsey end end end context '親子を別々に保存する場合' do it '親子関係が維持されていること' do parent.save child = parent.children.build(name: 'Bob') child.save expect(child.persisted?).to be_truthy expect(parent.reload.children).to eq [child] end end end
まとめ
ということで、build メソッドでモデルを関連付けて親モデルを永続化した際、
- 親子関係が保持され、関連モデルも同時に保存される
- 関連モデルの保存に失敗した場合、親モデルもロールバックされている
ことが分かりました。
今後も、自分の知識の間違い・勘違いがあったらこんな感じで紹介したいと思います。