简体   繁体   English

Rails 6 使用 has_many 关系和 accepts_nested_attributes_for 连接表

[英]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.从新的(有经验的)眼睛看我的代码可能会注意到我在做什么是愚蠢的。

  • Two models: Listing and Region两种模型: ListingRegion
  • Third model for joining: 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 issue你的问题

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.另外,您可以使用连接表中的单边记录使数据库完全膨胀。

Easy route简单路线

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

v2 update v2 更新

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

Better route (still)更好的路线(仍然)

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.

相关问题 has_many和accepts_nested_attributes_for具有嵌套表单和jquery / ajax - has_many and accepts_nested_attributes_for with nested forms and jquery/ajax 使用accepts_nested_attributes_for在rails中的三层嵌套表单 - Three level nested forms in rails using accepts_nested_attributes_for Rails 问题 accept_nested_attributes_for - Rails problem accepts_nested_attributes_for Rails嵌套属性form_for for has_many,使用javascript无限制嵌套模型 - Rails Nested Attributes form_for for has_many with unlimited nested models using javascript Rails和JQuery:无效的关联。 确保将accepts_nested_attributes_for用于 - Rails & JQuery: Invalid association. Make sure that accepts_nested_attributes_for is used for Ember和Rails has_many关系 - Ember and Rails has_many relationship Rails通过连接记录通过JavaScript将新的has_many附加到嵌套表单 - Rails append new has_many through join record to nested form via javascript Rails:使用has_many关系时,link_to(...,remote:true)的行为不一致 - Rails: Inconsistent behavior with link_to(…, remote: true) when using has_many relationship Rails form_for has_many通过使用AJAX的关联不起作用 - Rails form_for has_many through association using AJAX not working Nested_form_fields在验证失败时显示现有的has_many嵌套字段 - Nested_form_fields show existing has_many nested fields upon validation failure
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM