[英]Rails 6 Joins Tables using has_many relationships and accepts_nested_attributes_for
I've been around a few circles with this one.我已经在这个圈子里转了几个圈。
I was having an issue adding more Regions
to the Listing
on update.我在更新时在
Listing
添加更多Regions
时遇到问题。
Now I cannot even get multiple Regions
added to the Listing
on create.现在我什至无法在创建时
Listing
多个Regions
添加到Listing
中。 If anyone can help me with the solution that would be great.如果有人可以帮助我解决这个问题,那就太好了。 A look over my code from fresh (experienced) eyes might notice what I'm doing that is stupid.
从新的(有经验的)眼睛看我的代码可能会注意到我在做什么是愚蠢的。
Listing
and Region
Listing
和Region
Regionalization
Regionalization
Models:楷模:
# app/models/listing.rb
class Listing < ApplicationRecord
has_many :regionalizations
has_many :regions, through: :regionalizations
accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :all_blank
end
# app/models/region.rb
class Region < ApplicationRecord
has_many :regionalizations
has_many :listings, through: :regionalizations
end
# app/models/regionalization.rb
class Regionalization < ApplicationRecord
belongs_to :listing
belongs_to :region
end
Models and associations seem sound to me.模型和关联对我来说似乎很合理。 I think the problem lies in the controller and or the nested form.
我认为问题在于控制器和/或嵌套表单。
Controller Actions [note I'm using dashboard namespace for this controller]控制器操作 [注意我正在为此控制器使用仪表板命名空间]
class Dashboard::ListingsController < Dashboard::BaseController
def new
@listing = Listing.new
end
def create
@listing = Listing.new(listing_params)
@listing.user_id = current_user.id
@listing.regionalizations.build
if @listing.save
redirect_to dashboard_path, notice: "Your Listing was created successfuly"
else
render :new
end
end
def update
respond_to do |format|
if @listing.update(listing_params)
format.html { redirect_to edit_dashboard_listing_path(@listing), notice: 'Your Listing was successfully updated.' }
format.json { render :show, status: :ok, location: @listing }
else
format.html { render :edit }
format.json { render json: @listing.errors, status: :unprocessable_entity }
end
end
end
private
def listing_params
params.require(:listing).permit(:id, :name, :excerpt, :description, :email, :website, :phone_number, :user_id, :featured_image, :business_logo, :address, :category_id, :facebook, :instagram, :twitter, :status, :regionalization_id, gallery_images: [], regionalizations_attributes: [:id, :region_id, :listing_id, :_destroy])
end
end
dashboard/listings/_form:仪表板/列表/_form:
<%= form_with(model: [:dashboard, listing], local: true) do |f| %>
<article class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-4">Delivery Regions</h5>
<%= f.fields_for :regionalizations do |regionalizations_form| %>
<%= render 'regionalization_fields', f: regionalizations_form %>
<% end %>
<%= link_to_add_fields "Add Region", f, :regionalizations %>
</div>
</article>
<%= f.submit data: { turbolinks: false }, class: "btn btn-outline-primary" %>
<% end %>
_regionalization_fields.html.erb: _regionalization_fields.html.erb:
<p class="nested-fields">
<%= f.collection_select(:region_id, Region.all, :id, :name, {multiple: true}, {class: 'form-control'}) %>
<%= f.hidden_field :_destroy %>
<%= link_to "Remove", '#', class: "remove_fields" %>
</p>
error on validation when creating a new Listing
:创建新
Listing
时验证错误:
Regionalizations region must exist
If I add this to the Regionalization table I can get the regionaliztion to work.如果我将其添加到区域化表中,我可以使区域化工作。
belongs_to :region, optional: true
Now my parameters only ever show one regionalization atribute unless I tell it to build 3 or 4.现在我的参数只显示一个区域化属性,除非我告诉它构建 3 或 4。
Like so:像这样:
4.times do @listing.regionalizations.build end
I have used Steve Polito's guide to try get this working.我已经使用Steve Polito 的指南来尝试使其正常工作。 I've not changed any of the javascript stuff or application_helper stuff.
我没有更改任何 javascript 内容或 application_helper 内容。
The add and delete fields work fine on front end.添加和删除字段在前端工作正常。 The remove nested field works fine in the dB.
删除嵌套字段在 dB 中工作正常。
Am I missing something totally stupid here, please?我在这里错过了一些完全愚蠢的东西吗?
The only thing I can notice any different to a new nested field and one pulled in from the build method is the "Selected" tag is not on the new nested field added to the form.我唯一能注意到的与新的嵌套字段和从构建方法中提取的字段不同的是“Selected”标签不在添加到表单的新嵌套字段上。
Params on submit:提交参数:
Started POST "/dashboard/listings" for ::1 at 2020-10-30 20:22:15 +0000
Processing by Dashboard::ListingsController#create as HTML
Parameters: {"authenticity_token"=>"shtfCS/cSj/w/I6S1tNey99L8TKf48Xj0GAOMsODU3l44o0pJdjucCteQXca496aosNCEp7sPD85UM4QO4jEnw==", "listing"=>{"name"=>"", "excerpt"=>"", "description"=>"", "category_id"=>"1", "email"=>"", "phone_number"=>"", "website"=>"", "address"=>"", "facebook"=>"#", "instagram"=>"#", "twitter"=>"#", "regionalizations_attributes"=>{"0"=>{"region_id"=>"14", "_destroy"=>"false"}}}, "commit"=>"Create Listing"}
I'm going to add in the applicaton_helper file taken from Steve's tutorial on nested forms.我将添加取自史蒂夫关于嵌套表单的教程的 appplicaton_helper 文件。 One of the comments makes mention about the dynamic ability of the code.
其中一条评论提到了代码的动态能力。 It works (just not for me).
它有效(只是不适合我)。 I can achieve what I need on the create method by forcing a numbered loop.
我可以通过强制编号循环来实现我需要的 create 方法。 Just can't get the fields to add dynamically into the db.
只是无法将字段动态添加到数据库中。
# This method creates a link with `data-id` `data-fields` attributes. These attributes are used to create new instances of the nested fields through Javascript.
def link_to_add_fields(name, f, association)
# Takes an object (@person) and creates a new instance of its associated model (:addresses)
# To better understand, run the following in your terminal:
# rails c --sandbox
# @person = Person.new
# new_object = @person.send(:addresses).klass.new
new_object = f.object.send(association).klass.new
# Saves the unique ID of the object into a variable.
# This is needed to ensure the key of the associated array is unique. This is makes parsing the content in the `data-fields` attribute easier through Javascript.
# We could use another method to achive this.
id = new_object.object_id
# https://api.rubyonrails.org/ fields_for(record_name, record_object = nil, fields_options = {}, &block)
# record_name = :addresses
# record_object = new_object
# fields_options = { child_index: id }
# child_index` is used to ensure the key of the associated array is unique, and that it matched the value in the `data-id` attribute.
# `person[addresses_attributes][child_index_value][_destroy]`
fields = f.fields_for(association, new_object, child_index: id) do |builder|
# `association.to_s.singularize + "_fields"` ends up evaluating to `address_fields`
# The render function will then look for `views/people/_address_fields.html.erb`
# The render function also needs to be passed the value of 'builder', because `views/people/_address_fields.html.erb` needs this to render the form tags.
render(association.to_s.singularize + "_fields", f: builder)
end
# This renders a simple link, but passes information into `data` attributes.
# This info can be named anything we want, but in this case we chose `data-id:` and `data-fields:`.
# The `id:` is from `new_object.object_id`.
# The `fields:` are rendered from the `fields` blocks.
# We use `gsub("\n", "")` to remove anywhite space from the rendered partial.
# The `id:` value needs to match the value used in `child_index: id`.
link_to(name, '#', class: "add_fields", data: {id: id, fields: fields.gsub("\n", "")})
end
Your error is telling you that at least one Regionalization
record can't be saved because it doesn't have a region_id
.您的错误告诉您至少有一个
Regionalization
记录无法保存,因为它没有region_id
。
And adding belongs_to :region, optional: true
is actually undermining your data integrity.并添加
belongs_to :region, optional: true
实际上会破坏您的数据完整性。 You now have Regionalization
records where the Listing
is present, but not the Region
, which defeats the purpose of the Regionalization
join table.您现在拥有
Listing
存在的Regionalization
记录,但没有Region
,这违背了Regionalization
连接表的目的。 Plus, you could totally bloat your database with one-sided records in your join table.另外,您可以使用连接表中的单边记录使数据库完全膨胀。
You need to reject Regionalization
s that don't have BOTH a :listing_id
and a :region_id
你需要拒绝
Regionalization
s表示不同时具有:listing_id
和:region_id
This:这个:
accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :all_blank
is not doing the job for you.不是为你做这项工作。 The record isn't "all blank" because it has a
:listing_id
该记录不是“全空白”,因为它有一个
:listing_id
Handling this in the model is doable:在模型中处理这个是可行的:
# app/models/listing.rb
class Listing < ApplicationRecord
has_many :regionalizations
has_many :regions, through: :regionalizations
accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :missing_keys
private
def missing_keys(attributes)
attributes['region_id'].blank? ||
attributes['listing_id'].blank?
end
end
So for whatever reason the regionalizations.build will only send the first one through the params.
因此,无论出于何种原因,regionizations.build 都只会通过参数发送第一个。 If I make it a loop like in the question it will pass them all through.
如果我使它像问题中那样循环,它将全部通过。
Yes, @listing.regionalizations.build
only builds 1 new record.是的,
@listing.regionalizations.build
只建立 1 个新记录。
If you know exactly how many drop-downs you want to appear on the page, you can use your 4.times do @listing.regionalizations.build end
code.如果您确切知道要在页面上显示多少个下拉列表,则可以使用
4.times do @listing.regionalizations.build end
代码。 This loads 4 new records into memory and Rails will find them as a collection when it runs this:这将 4 个新记录加载到内存中,Rails 会在运行时将它们作为集合查找:
<%= render 'regionalization_fields', f: regionalizations_form %>
BUT f.fields_for
is NOT a loop so the fact that this works is a bit mysterious to me.但是
f.fields_for
不是一个循环,所以这个工作的事实对我来说有点神秘。
If you are creating 4 child records, you should use f.fields_for
4 times.如果要创建 4 个子记录,则应使用
f.fields_for
4 次。
You can do this by changing around your partials a bit:你可以通过稍微改变你的部分来做到这一点:
class Dashboard::ListingsController < Dashboard::BaseController
def new
@listing = Listing.new
4.times { @listing.regionalization.build }
@regionalizations = @listing.regionalizations
end
...
def edit
@listing = Listing.find(params[:id])
4.times { @listing.regionalization.build }
# will include ALL existing regionalizations, and 4 new ones
@regionalizations = @listing.regionalizations
end
dashboard/listings/_form:仪表板/列表/_form:
<%= form_with(model: [:dashboard, listing], local: true) do |f| %>
<article class="card mb-3">
<div class="card-body">
<h5 class="card-title mb-4">Delivery Regions</h5>
<%= render 'regionalization_fields', collection: @regionalizations %>
<%= link_to_add_fields "Add Region", f, :regionalizations %>
</div>
</article>
<%= f.submit data: { turbolinks: false }, class: "btn btn-outline-primary" %>
<% end %>
RENAME: _regionalization_fields.html.erb
to _regionalizations_form.html.erb
for clarity:重命名:
_regionalization_fields.html.erb
为_regionalizations_form.html.erb
为清晰起见:
<!-- change the form handler's name so it doesn't conflict with the local variable 'regionalizations_form` -->
<%= f.fields_for regionalizations_form do |f_reg| %>
<p class="nested-fields">
<%= f_reg.collection_select(:region_id, Region.all, :id, :name, {multiple: true}, {class: 'form-control'}) %>
<%= f_reg.hidden_field :_destroy %>
<%= link_to "Remove", '#', class: "remove_fields" %>
</p>
<% end %>
If you need a variable number of Regionalizations
, the best way to handle that is with some Rails AJAX and UJS.如果您需要可变数量的
Regionalizations
,最好的处理方法是使用一些 Rails AJAX 和 UJS。 This would mean creating the form with only 1 new Regionalization
record and having a button that says "Add another".这意味着创建只有 1 个新
Regionalization
记录的表单,并有一个显示“添加另一个”的按钮。 The user clicks it and you add in another select field with everything you need, including the correct conventions to have the results posted to the params.用户单击它,然后您在另一个选择字段中添加您需要的所有内容,包括将结果发布到参数的正确约定。 This is a whole other can of worms, but I still recommend (and so does Steve Polito) Ryan Bate's Railscast on nested forms
这是一个完全不同的蠕虫罐头,但我仍然推荐(史蒂夫波利托也是如此) Ryan Bate 的嵌套形式的 Railscast
You've opted for the "has_many / belongs_to :through" option, but if Regionalization
is truly just a join table, you could simplify this with a has_and_belongs_to_many
.您选择了“has_many/belongs_to :through”选项,但如果
Regionalization
真的只是一个连接表,您可以使用has_and_belongs_to_many
来简化它。
If you're unsure if you can go this route, consider this: if Regionalization
needs no methods or other dB fields beyond the foreign key ID's, then you've added complexity with a model you don't need.如果您不确定是否可以走这条路,请考虑:如果
Regionalization
不需要方法或外键 ID 之外的其他 dB 字段,那么您就增加了不需要的模型的复杂性。
If Regionalization
can be a more standard JoinTable, you can avoid some nesting complexity and allow Rails to do it for you.如果
Regionalization
可以是一个更标准的 JoinTable,你就可以避免一些嵌套的复杂性,让 Rails 为你做。
If you can go this way, see this doc .如果您可以这样做, 请参阅此文档。 You'll need to change your migration, but you can use the
create_join_table
to let Rails handle the naming and indexes for you.您需要更改迁移,但可以使用
create_join_table
让 Rails 为您处理命名和索引。 Rails will call this join table listings_regions
instead of regionalizations
, but you won't ever really need to reference it. Rails 会调用这个连接表
listings_regions
而不是regionalizations
,但你真的不需要引用它。
Here's how your models could look with a simple join table:以下是使用简单连接表的模型的外观:
# app/models/listing.rb
class Listing < ApplicationRecord
has_and_belongs_to_many :regions
accepts_nested_attributes_for :regions, :dependent_destroy
end
# app/models/region.rb
class Region < ApplicationRecord
has_and_belongs_to_many :listings
end
# app/models/regionalization.rb can be deleted
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.