Rails
ActiveRecord

accepts_nested_attributes_forについて学ぶ_100DaysOfCodeチャレンジ6日目(Day_6:#100DaysOfCode)

はじめに

この記事はTwitterで人気のハッシュタグ#100DaysOfCodeをつけて、
100日間プログラミング学習を続けるチャレンジに挑戦した6日目の記録です。

動作環境

  • ruby 2.4.1
  • Rails 5.0.1

現在学習している内容のリポジトリ

https://github.com/yuta-ushijima/notebook-api-on-rails

本日学んだこと

  • accepts_nested_attributes_forの使い方

accepts_nested_attributes_forとは?

Railsが標準で提供している、ActiveRecordのメソッドの一つ。
モデル同士が関連付けられている時に、ネストさせることで一度にまとめてレコードの更新ができるようになります。

今回は次のような関連づけられたモデルがあったと仮定します。

accepts_nested_attributes_for.png

Contactモデルを基準として、

  • Phoneモデルとはhas_many
  • Kindモデルとはbelongs_to

の関係にある状態です。

Contactモデル

# app/models/contact.rb
class Contact < ApplicationRecord
  # モデル同士の関連付け
  belongs_to :kind
  has_many :phones

  accepts_nested_attributes_for :phones
end

Phoneモデル

# app/models/phone.rb
class Phone < ApplicationRecord
  belongs_to :contact, optional: true
end

Kindモデル

# app/models/kind.rb
class Kind < ApplicationRecord
end

例えばパラメータとして次のような値が渡されたとしましょう。

 params = { contact: {
       name: 'jack',
       email: 'hak@hal.com',
       birthdate: '2018/11/02',
       kind_id: 2,
       phones_attributes: [
         {number: '1234' },
         {number: '5678'},
         {number: '9012'},
       ]
 }}

# ハッシュによる戻り値
{:contact=>
  {:name=>"jack",
   :email=>"hak@hal.com",
   :birthdate=>"2018/11/02",
   :kind_id=>2,
   :phones_attributes=>[{:number=>"1234"}, {:number=>"5678"}, {:number=>"9012"}]}}

Contactモデルにaccepts_nested_attributes_for :phonesと定義されているので、createメソッドでContactモデルがパラメータを引数としてレコードを作成すると、ネストされているphoneモデルも同時に一緒に作成することができることになります。

pry(main)> Contact.create(params[:contact])

# 発行されるSQL
(0.1ms)  begin transaction
  Kind Load (1.4ms)  SELECT  "kinds".* FROM "kinds" WHERE "kinds"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  SQL (3.8ms)  INSERT INTO "contacts" ("name", "email", "birthdate", "created_at", "updated_at", "kind_id") VALUES (?, ?, ?, ?, ?, ?)  [["name", "jack"], ["email", "hak@hal.com"], ["birthdate", "2018-11-02"], ["created_at", "2018-06-21 13:48:05.178496"], ["updated_at", "2018-06-21 13:48:05.178496"], ["kind_id", 2]]
  SQL (2.6ms)  INSERT INTO "phones" ("number", "contact_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["number", "1234"], ["contact_id", 104], ["created_at", "2018-06-21 13:48:05.186941"], ["updated_at", "2018-06-21 13:48:05.186941"]]
  SQL (0.1ms)  INSERT INTO "phones" ("number", "contact_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["number", "5678"], ["contact_id", 104], ["created_at", "2018-06-21 13:48:05.197365"], ["updated_at", "2018-06-21 13:48:05.197365"]]
  SQL (0.1ms)  INSERT INTO "phones" ("number", "contact_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["number", "9012"], ["contact_id", 104], ["created_at", "2018-06-21 13:48:05.200102"], ["updated_at", "2018-06-21 13:48:05.200102"]]
   (5.9ms)  commit transaction

# 作成されるContactオブジェクト
=> #<Contact:0x007fa63fec7678
 id: 104,
 name: "jack",
 email: "hak@hal.com",
 birthdate: Fri, 02 Nov 2018,
 created_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
 updated_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
 kind_id: 2>

最後に作成されたContactオブジェクトをlastメソッドで取得してみましょう。

 pry(main)> Contact.last

# 発行されるSQL
Contact Load (0.3ms)  SELECT  "contacts".* FROM "contacts" ORDER BY "contacts"."id" DESC LIMIT ?  [["LIMIT", 1]]

# 取得したContactオブジェクト情報
=> #<Contact:0x007fa642962270
 id: 104,
 name: "jack",
 email: "hak@hal.com",
 birthdate: Fri, 02 Nov 2018,
 created_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
 updated_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
 kind_id: 2>

PhoneモデルはContactオブジェクトに関連付けられているので、Contact.last.phonesとすればPhoneモデルの情報が取得できます。

pry(main)> Contact.last.phones

# 発行されたSQL
Contact Load (0.7ms)  SELECT  "contacts".* FROM "contacts" ORDER BY "contacts"."id" DESC LIMIT ?  [["LIMIT", 1]]
  Phone Load (0.5ms)  SELECT "phones".* FROM "phones" WHERE "phones"."contact_id" = ?  [["contact_id", 104]]

# id:104をもつContactオブジェクトに紐づいたPhoneオブジェクトの情報を取得

=> [#<Phone:0x007fa642b0fca8
  id: 412,
  number: "1234",
  contact_id: 104,
  created_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
  updated_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00>,
 #<Phone:0x007fa642b0fb40
  id: 413,
  number: "5678",
  contact_id: 104,
  created_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
  updated_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00>,
 #<Phone:0x007fa642b0f938
  id: 414,
  number: "9012",
  contact_id: 104,
  created_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00,
  updated_at: Thu, 21 Jun 2018 13:48:05 UTC +00:00>]

パラメータでPhoneモデルの情報が次のようにネストされていたのを思い出してください。

phones_attributes: [
         {number: '1234' },
         {number: '5678'},
         {number: '9012'},
       ]

ここで渡されたPhoneモデルのnumber属性がContactモデルのレコード作成時に一緒に保存されたというところがポイントです。

ただ、Railsの生みの親であるDHHがこのメソッド自体をdeplicateしようと考えているらしいので、いずれなくなるのかもしれませんね。

I'd actually like to kill accepts_nested_attributes_for in due time. Don't think we should promote it for this new API. Rather, let's just show how to do it by hand in the controller.
https://github.com/rails/rails/pull/26976#discussion_r87855694

DHH的には、複雑なことをモデルでやらせるよりも、controller側でシンプルに書くべきだと考えているようですね。

参考リンク

Active Record Nested Attributes

Railsでaccepts_nested_attributes_forとfields_forを使ってhas_many関連の子レコードを作成/更新するフォームを作成