[Rails] Nested Model
keywords: fields_for
, accepts_nested_attributes_for
Building Complex Form @ RailsGuides
許多時候我們都不是只要修改單一個 Model,例如,當我們建立 Person
時,你可能會想要讓使用者可以在同一個表單同時建立/編輯與該 Person
有關的許多 Address
的資料。
可參考專案
- 需要關聯到對應的屬性時
filmCloud > posts_controller
- 只需要取得對應的 ids 時
ux-website > articles_controller, article/_form
概念
透過在 Model 中使用 accepts_nested_attributes_for
可以自動幫我們建立相對應的方法可以使用,例如:
# app/models/person.rb
# 透過 accepts_nested_attributes_for :address
# 會產生相對應的 addresses_attributes= 方法讓我們可以對 address 進行 crud
class Person < ApplicationRecord
has_one :address
accepts_nested_attributes_for :address, allow_destroy: true, reject_if: :all_blank
end
一對一關係 One to One
假設 Person has_one
Address:
Model
keywords: accepts_nested_attributes_for
accepts_nested_attributes_for
allow_destroy
reject_if
當 Person 已經有定義 has_one :address
後,可以使用 accepts_nested_attributes_for :address
:
# app/models/person.rb
class Person < ApplicationRecord
has_one :address
accepts_nested_attributes_for :address, allow_destroy: true, reject_if: :all_blank
end
View
keywords: <instance>.fields_for
可以使用 <instance>.fields_for
:
<!-- app/views/people/_form.html.erb -->
<%= form_for @person do |person_form| %>
<!-- Person 的欄位 -->
<div class="field">
<%= person_form.label :name %>
<%= person_form.text_field :name %>
</div><!-- / Person 的欄位 -->
<!-- Person.address 的欄位 -->
<div class="field">
<%= person_form.fields_for :address do |address_fields| %>
Location : <%= address_fields.text_field :location %>
Delete: <%= address_fields.check_box :_destroy %>
<% end %>
</div><!-- Person.address 的欄位 -->
<div class="actions">
<%= person_form.submit %>
</div>
<% end %>
如此將會產生相對應的 HTML:
_id
:value 的值會對應到 DB 當中的 id,用來告訴 Rails 上面的內容是要修改 DB 中的哪筆 record_destroy
:當 value 設成 1 或 true 時將會刪除該筆資料
如果有使用
_destroy
的話,記得在 Model 的地方要設定allow_destroy: true
<div class="field">
Location :
<input
type="text"
value="123"
name="person[address_attributes][location]"
id="person_address_attributes_location"
/>
Delete:
<input name="person[address_attributes][_destroy]" type="hidden" value="0" />
<input
type="checkbox"
value="1"
name="person[address_attributes][_destroy]"
id="person_address_attributes__destroy"
/>
<input
type="hidden"
value="3"
name="person[address_attributes][id]"
id="person_address_attributes_id"
/>
</div>
Controller
keywords: <instance>_attributes
Person.address 的資料到 Controller 後會自動變成 address_attributes
。
在 Controller 的地方要記得把允許的欄位(address_attributes: [:location, :id, :_destroy]
)加到 permit
中 。
如果你想要允許 address_attributes 中的所有參數,可以透過物件{ }
的方式允許所有參數,使用 address_attributes: {}
:
class PeopleController < ApplicationController
before_action :set_person, only: [:show, :edit, :update, :destroy]
# ...
private
# Never trust parameters from the scary internet, only allow the white list through.
def person_params
# 允許所有參數:
# params.require(:person).permit(:name, address_attributes: {})
params.require(:person).permit(:name, address_attributes: [:location, :id, :_destroy])
end
end
一對多關係 One to Many
假設 Person has_many
Address:
Model
keywords: accepts_nested_attributes_for
accepts_nested_attributes_for
allow_destroy
reject_if
當 Person 已經有定義 has_one :address
後,可以使用 accepts_nested_attributes_for :address
:
# app/models/person.rb
class Person < ApplicationRecord
has_many :address
accepts_nested_attributes_for :address, allow_destroy: true
end
View
keywords: <instance>.fields_for
可以使用 <instance>.fields_for
:
<!-- app/views/people/_form.html.erb -->
<%= form_for @person do |person_form| %>
<!-- Person 的欄位 -->
<div class="field">
<%= person_form.label :name %>
<%= person_form.text_field :name %>
</div><!-- / Person 的欄位 -->
<!-- Person.address 的欄位 -->
<div class="field">
<%= person_form.fields_for :address do |address_fields| %>
Location : <%= address_fields.text_field :location %>
Delete: <%= address_fields.check_box :_destroy %>
<% end %>
</div><!-- Person.address 的欄位 -->
<div class="actions">
<%= person_form.submit %>
</div>
<% end %>
如此將會產生相對應的 HTML:
在 person[address_attributes]
後因為可以接多個 address,所以多了一個 []
,這是用來方便後端對應哪些資料是屬於同一群的
_id
:value 的值會對應到 DB 當中的 id,用來告訴 Rails 上面的內容是要修改 DB 中的哪筆 record_destroy
:當 value 設成 1 或 true 時將會刪除該筆資料[ ]
:用來讓後端對應哪些是同一群的資料,所以不同群的不能用同一個值。通常會用 0, 1, 2 區別;但當內容是uuid
或字串時,記得在 controller.permit()
的地方要用{ }
允許整個物件。
如果有使用
_destroy
的話,記得在 Model 的地方要設定allow_destroy: true
<div class="field">
<!-- 第一列 -->
Location :
<input
type="text"
value="123"
name="person[address_attributes][0][location]"
id="person_address_attributes_0_location"
/>
Delete:
<input name="person[address_attributes][0][_destroy]" type="hidden" value="0" />
<input
type="checkbox"
value="1"
name="person[address_attributes][0][_destroy]"
id="person_address_attributes_0__destroy"
/>
<input
type="hidden"
value="3"
name="person[address_attributes][0][id]"
id="person_address_attributes_0_id"
/>
<!-- 第二列 -->
Location :
<input
type="text"
value="10666"
name="person[address_attributes][1][location]"
id="person_address_attributes_1_location"
/>
Delete:
<input name="person[address_attributes][1][_destroy]" type="hidden" value="0" />
<input
type="checkbox"
value="1"
name="person[address_attributes][1][_destroy]"
id="person_address_attributes_1__destroy"
/>
<input
type="hidden"
value="4"
name="person[address_attributes][1][id]"
id="person_address_attributes_1_id"
/>
</div>
Controller
keywords: <instance>_attributes
Person.address 的資料到 Controller 後會自動變成 address_attributes
。
在 Controller 的地方要記得把允許的欄位(address_attributes: [:location, :id, :_destroy]
)加到 permit
中 。
如果你想要允許 address_attributes 中的所有參數,可以透過物件{ }
的方式允許所有參數,使用 address_attributes: {}
。
如果在 View 中用來辨認群組的
[ ]
中帶入的是 uuid,因為在後端會把 uuid 當成物件的 key,所以.permit()
中需要透過foobar_attributes: {}
當成物件來允許所有欄位內容。
class PeopleController < ApplicationController
before_action :set_person, only: [:show, :edit, :update, :destroy]
# ...
private
# Never trust parameters from the scary internet, only allow the white list through.
def person_params
# 允許所有參數(當 View 中使用 uuid 時):
# params.require(:person).permit(:name, address_attributes: {})
params.require(:person).permit(:name, address_attributes: [:location, :id, :_destroy])
end
end
針對 create 時的情況
你會發現,如果 Person.address
中沒有資料時,在表單中不會產生任何可以填寫 address 的欄位,這時候常用的方式是在 people_controller.rb
中加上:
# app/controllers/people_controller.rb
def new
@person = Person.new
2.times { @person.address.build } # @person 後的 Model (address) 可能需要用複數
end
避免空資料存到 Model 中
使用 reject_if
可以避面空的資料存到 Model 中:
reject_if: :all_blank
reject_if: lambda {|attributes| attributes['kind'].blank?}
class Person < ApplicationRecord
has_many :addresses
accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
end
只需要關聯到的 id 而不需要內部的 attributes 時
這種情況通常用在 JOIN TABLE,在 JOIN TABLE 中只會存放兩個不同表中 id 是如何關聯起來的,而不會有其他的 attributes,這時候的做法可以參考:
View
<%# app/views/articles/_form.html.erb %>
<%# 直接關聯到 article.users %>
<%= form_with model: article, local: true, html: { class: 'needs-validation', novalidate: true } do |form| %>
<div class="form-group">
<%= form.label :status, class: 'd-block' %>
<%= form.collection_check_boxes(:user_ids, User.all, :id, :name) %>
</div>
<% end %>
Controller
記得要設定 params permit 的部分:
def article_params
params.fetch(:article, {}).permit(:title, user_ids: [])
end
在 Controller 的部分,則可以透過:
article.user_ids # 取得和此 article 關聯的所有 user ids
article.users # 取得和此 article 關聯的所有 user (object)
article.user_ids = [1, 2] # 直接使用 user id 建立關聯
u = User.first
article.users = [u] # 透過 User Object 建立關聯
⚠️ 注意:
xxx_ids
這是 Rails 的慣例。
最後產生出來的 HTML 結構會像這樣:
<div class="form-group">
<label class="d-block" for="article_status">Status</label>
<input type="hidden" name="article[user_ids][]" value="" />
<input type="checkbox" value="2" name="article[user_ids][]" id="article_user_ids_2" />
<label for="article_user_ids_2">Wei</label>
<input type="checkbox" value="1" name="article[user_ids][]" id="article_user_ids_1" />
<label for="article_user_ids_1">PJCHENder</label>
</div>
Model 地方的設定類似這樣:
# app/models/article.rb
class Article < ApplicationRecord
has_and_belongs_to_many :users
end
# app/models/user.rb
class Article < ApplicationRecord
has_and_belongs_to_many :articles
end
- Rails 多對多關聯設定與存取
- collection_check_boxes @APIDock
參考
- Building complex form @ RailsGuides - Form Helpers
- Active Record: Nested Attributes @ Rails API
- method: fields_for @ Rails API
- RESTful 應用實作 @ Rails 實戰聖經 by ihower