読者です 読者をやめる 読者になる 読者になる

blog @arfyasu

プログラミングとか趣味のこととか

Rails のモデルを関連付けるタイミングの検証

はじめに

今日、Rails の関連モデルの保存について「お〜、すごい!」と思ったことがあったので備忘として残しておきます。
といっても、ほんと大したことない内容だと思います。はい。

環境

Mac OS XYosemite
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

クラス図はこんな感じです。 f:id:kanz-labs:20160112232729p:plain

その時、以下のコードは親子関係を保ったまま保存されるでしょうか。

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 メソッドでモデルを関連付けて親モデルを永続化した際、

  • 親子関係が保持され、関連モデルも同時に保存される
  • 関連モデルの保存に失敗した場合、親モデルもロールバックされている

ことが分かりました。

今後も、自分の知識の間違い・勘違いがあったらこんな感じで紹介したいと思います。