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

Rails API: ActiveRecord::NestedAttributes(翻訳)

概要

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

Rails API: ActiveRecord::NestedAttributes(翻訳)

Active RecordAttributesのネステッド版

ネステッド属性を使うと、関連付けられているレコードに親を介して属性を保存できます。ネステッド属性の更新はデフォルトでは無効になっており、accepts_nested_attributes_forクラスメソッドで有効にできます。ネステッド属性が有効になると、モデル上に属性ライターメソッドが定義されます。

この属性ライターメソッドは、関連付けに基づいて命名されます。つまり、以下の例ではモデルにauthor_attributes=(attributes)pages_attributes=(attributes)という新しいメソッドが2つ追加されます。

class Book < ActiveRecord::Base
  has_one :author
  has_many :pages

  accepts_nested_attributes_for :author, :pages
end

: このとき、accepts_nested_attributes_forの対象となるすべての関連付けで:autosaveオプションが自動的に有効になります。

🔗 関連付けが1対1の場合

以下のMemberモデルがAvatarを1つ持っている場合を考えます。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar
end

1対1関連付けでネステッド属性を有効にすると、以下のようにmemberとavatarを同時に作成できるようになります。

params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
member = Member.create(params[:member])
member.avatar.id # => 2
member.avatar.icon # => 'smiling'

以下のように、member経由でavatarを更新することも可能になります。

params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
member.update params[:member]
member.avatar.icon # => 'sad'

idを提供せずに現在のavatarを更新したい場合は、以下のように:update_onlyオプションを追加する必要があります。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar, update_only: true
end
params = { member: { avatar_attributes: { icon: 'sad' } } }
member.update params[:member]
member.avatar.id # => 2
member.avatar.icon # => 'sad'

デフォルトでできるのは、関連付けられるモデル上の属性を設定・更新することだけです。関連するモデルを属性ハッシュを経由して破棄したい場合は、最初に:allow_destroy オプションで有効にしておく必要があります。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar, allow_destroy: true
end

これで、_destroyを属性ハッシュに追加すると、その値がtrueと評価される場合は、関連付けられるモデルが削除されるようになります。

member.avatar_attributes = { id: '2', _destroy: '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil

ただし、このモデルは親がsaveされるまでは削除されない点にご注意ください。

また、更新されたハッシュ内でそのモデルのidも指定しておかないと、モデルが削除されない点にもご注意ください。

🔗 1対多

以下のMemberモデルを考えてみましょう。このモデルには多数の投稿(posts)があります。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

上のようにすることで、関連付けられているposts上の属性を、そのメンバーの属性ハッシュ経由で設定・更新できるようになります。値として、post属性のハッシュの配列を持つ:posts_attributesキーを含めてください。

id持たない個別のハッシュについては、以下のようにそれぞれ新しいレコードがインスタンス化されます(ただし、trueと評価される_destroyもハッシュに含まれている場合を除きます)。

params = { member: {
  name: 'joe', posts_attributes: [
    { title: 'Kari, the awesome Ruby documentation browser!' },
    { title: 'The egalitarian assumption of the modern citizen' },
    { title: '', _destroy: '1' } # これは無視される
  ]
}}

member = Member.create(params[:member])
member.posts.length # => 2
member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
member.posts.second.title # => 'The egalitarian assumption of the modern citizen'

以下のように:reject_ifにprocを設定することで、条件を満たさない場合に新規レコードハッシュを無視することも可能です。たとえば、上述の例は以下のように書き換えられます。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
end

params = { member: {
  name: 'joe', posts_attributes: [
    { title: 'Kari, the awesome Ruby documentation browser!' },
    { title: 'The egalitarian assumption of the modern citizen' },
    { title: '' } # :reject_ifのprocによって無視される
  ]
}}

member = Member.create(params[:member])
member.posts.length # => 2
member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
member.posts.second.title # => 'The egalitarian assumption of the modern citizen'

以下のように、利用するメソッドのシンボルも:reject_ifに渡せます。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: :new_record?
end
class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, reject_if: :reject_posts

  def reject_posts(attributes)
    attributes['title'].blank?
  end
end

既に関連付けられているレコードと一致するidキーがハッシュに含まれている場合は、一致するレコードが変更されます。

member.attributes = {
  name: 'Joe',
  posts_attributes: [
    { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
    { id: 2, title: '[UPDATED] other post' }
  ]
}

member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
member.posts.second.title # => '[UPDATED] other post'

ただし上が適用されるのは、親モデルも更新される場合です。たとえば、「joe」という名前のmemberを作成すると同時にpostsも更新すると、ActiveRecord::RecordNotFoundエラーが発生します。

デフォルトでは、関連付けられるレコードは削除から保護されています。関連付けられるレコードを属性ハッシュ経由で削除したい場合は、最初に:allow_destroyオプションで削除を有効にしておく必要があります。これによって、以下のように既存のレコードを_destroyキーでも削除できるようになります。

class Member < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts, allow_destroy: true
end

params = { member: {
  posts_attributes: [{ id: '2', _destroy: '1' }]
}}

member.attributes = params[:member]
member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
member.posts.length # => 2
member.save
member.reload.posts.length # => 1

関連付けられるコレクションのネステッド属性は、(ハッシュの配列の代わりに)ハッシュのハッシュという形で渡すことも可能です。

Member.create(
  name: 'joe',
  posts_attributes: {
    first:  { title: 'Foo' },
    second: { title: 'Bar' }
  }
)

上は以下と同等です。

Member.create(
  name: 'joe',
  posts_attributes: [
    { title: 'Foo' },
    { title: 'Bar' }
  ]
)

この場合、:posts_attributesの値となるハッシュのキーは無視されます。ただし、そうしたキーとして'id':idを使うことは許されません。もしそうすると、ハッシュが配列にラップされて、単一postの属性ハッシュとして解釈されてしまいます。

関連付けられるコレクションの属性を「ハッシュのハッシュ」形式で渡す方法は、HTTP/HTMLパラメータから生成されるハッシュ(ハッシュの配列を自然な形で送信する方法がない)で利用できます。

🔗 保存

モデルのあらゆる変更(削除マークが付けられたレコードの削除も含む)は、親モデルが保存されると自動的かつアトミックに保存および削除されます。これは、親のsaveメソッドで開始されたトランザクションの内部で発生します。
ActiveRecord::AutosaveAssociationを参照。

🔗 親モデルの存在をバリデーションする

belongs_to関連付けは、デフォルトで親モデルの存在をバリデーションします。この振る舞いは、optional: trueを指定することで無効にできます。これは、たとえば親モデルが存在するかどうかのバリデーションを条件付きで行う場合に利用できます。

class Veterinarian < ActiveRecord::Base
  has_many :patients, inverse_of: :veterinarian
  accepts_nested_attributes_for :patients
end

class Patient < ActiveRecord::Base
  belongs_to :veterinarian, inverse_of: :patients, optional: true
  validates :veterinarian, presence: true, unless: -> { awaiting_intake }
end

なお、:inverse_ofオプションを指定しない場合は、Active Recordがヒューリスティックに基づいて逆関連付けの自動推測を試みます。

1対1のネステッド関連付けでは、新たな子オブジェクトを自分で(メモリ上に)ビルドしてから代入する場合、以下のように、このモジュールはそれを上書きしません。

class Member < ActiveRecord::Base
  has_one :avatar
  accepts_nested_attributes_for :avatar

  def avatar
    super || build_avatar(width: 200)
  end
end

member = Member.new
member.avatar_attributes = {icon: 'sad'}
member.avatar.width # => 200

🔗 定数

🔗 REJECT_ALL_BLANK_PROC

# REJECT_ALL_BLANK_PROC
proc { |attributes| attributes.all? { |key, value| key == "_destroy" || value.blank? } }

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

🔗 accepts_nested_attributes_for(*attr_names)

指定された1つ以上の関連付けで属性ライターを定義します。

サポートされているオプション:

:allow_destroy
trueを指定すると、_destroyキーを持ち、かつその値がtrueに評価される(例: 1、'1'、true、または'true')属性ハッシュのメンバーをすべて削除します。このオプションはデフォルトではオフになっています。
:reject_if
Proc、または特定の属性ハッシュでレコードをビルドすべきかどうかをチェックするメソッドを指すSymbolを指定できます。ハッシュは指定のProcまたはメソッドに渡され、Procまたはメソッドはtruefalseを返す必要があります。 :reject_ifが指定されていない場合は、trueと評価される_destroy値を持たないすべての属性ハッシュについてレコードがビルドされます。Procの代わりに:all_blankを渡すと、_destroyの値以外の属性がすべて空白であるレコードを却下するprocを作成します。
:limit
ネステッド属性で処理可能な、関連付けられるレコードの最大個数を指定できます。この個数は、Procまたはメソッドを指すSymbolでも指定可能で、Procまたはメソッドは数値を返す必要があります。ネステッド属性の配列サイズが指定の最大個数を超えると、NestedAttributes::TooManyRecords例外が発生します。:limitオプションを省略した場合、任意の個数の関連付けを処理できます。:limitオプションを適用可能なのは「1対多」関連付けのみである点にご注意ください。
:update_only
このプションを1対1関連付けで使うと、関連付けられるレコードが既に存在する場合にネステッド属性がどう使われるかを指定できます。一般に、既存のレコードは「新しい属性値セットで更新される」か「それらの値を含むまったく新しいレコードに置き換えられる」かのどちらかになる可能性があります。:update_onlyオプションはデフォルトではfalseであり、既存のレコードの更新にネステッド属性が使われるのは、ネステッド属性にレコードのidが含まれている場合だけです。さもなければ、新しいレコードがインスタンス化されて既存のレコードの置き換えに使われます。ただし、:update_onlyオプションをtrueに設定すると、idが存在するかどうかにかかわらず、レコードの属性更新で常にネステッド属性が使われるようになります。このオプションは、コレクションの関連付けでは無視されます。

例:

# avatar_attributes=を作成する
accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
# avatar_attributes=を作成する
accepts_nested_attributes_for :avatar, reject_if: :all_blank
# avatar_attributes=とposts_attributes=を作成する
accepts_nested_attributes_for :avatar, :posts, allow_destroy: true

GitHubコード

関連記事

Rails API: ActiveRecord::AutosaveAssociation(翻訳)

Rails: ActiveRecord標準のattributes APIドキュメント(翻訳)

Rails: ActiveRecord::DelegatedType APIドキュメント(翻訳)


CONTACT

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