繁体   English   中英

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

[英]Rails 6 Joins Tables using has_many relationships and accepts_nested_attributes_for

我已经在这个圈子里转了几个圈。

我在更新时在Listing添加更多Regions时遇到问题。

现在我什至无法在创建时Listing多个Regions添加到Listing中。 如果有人可以帮助我解决这个问题,那就太好了。 从新的(有经验的)眼睛看我的代码可能会注意到我在做什么是愚蠢的。

  • 两种模型: ListingRegion
  • 加盟模式三: Regionalization

楷模:

# 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

模型和关联对我来说似乎很合理。 我认为问题在于控制器和/或嵌套表单。

控制器操作 [注意我正在为此控制器使用仪表板命名空间]

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

仪表板/列表/_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:

<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>

创建新Listing时验证错误:

Regionalizations region must exist

如果我将其添加到区域化表中,我可以使区域化工作。

belongs_to :region, optional: true

现在我的参数只显示一个区域化属性,除非我告诉它构建 3 或 4。

像这样:

4.times do @listing.regionalizations.build end

我已经使用Steve Polito 的指南来尝试使其正常工作。 我没有更改任何 javascript 内容或 application_helper 内容。

添加和删​​除字段在前端工作正常。 删除嵌套字段在 dB 中工作正常。

我在这里错过了一些完全愚蠢的东西吗?

我唯一能注意到的与新的嵌套字段和从构建方法中提取的字段不同的是“Selected”标签不在添加到表单的新嵌套字段上。

图像

提交参数:

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"}

我将添加取自史蒂夫关于嵌套表单的教程的 appplicaton_helper 文件。 其中一条评论提到了代码的动态能力。 它有效(只是不适合我)。 我可以通过强制编号循环来实现我需要的 create 方法。 只是无法将字段动态添加到数据库中。

# 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

你的问题

您的错误告诉您至少有一个Regionalization记录无法保存,因为它没有region_id

并添加belongs_to :region, optional: true实际上会破坏您的数据完整性。 您现在拥有Listing存在的Regionalization记录,但没有Region ,这违背了Regionalization连接表的目的。 另外,您可以使用连接表中的单边记录使数据库完全膨胀。

简单路线

你需要拒绝Regionalization s表示不同时具有:listing_id:region_id

这个:

accepts_nested_attributes_for :regionalizations, allow_destroy: true, reject_if: :all_blank

不是为你做这项工作。 该记录不是“全空白”,因为它有一个:listing_id

在模型中处理这个是可行的:

# 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 更新

因此,无论出于何种原因,regionizations.build 都只会通过参数发送第一个。 如果我使它像问题中那样循环,它将全部通过。

是的, @listing.regionalizations.build只建立 1 个新记录。

如果您确切知道要在页面上显示多少个下拉列表,则可以使用4.times do @listing.regionalizations.build end代码。 这将 4 个新记录加载到内存中,Rails 会在运行时将它们作为集合查找:

<%= render 'regionalization_fields', f: regionalizations_form %>

但是f.fields_for不是一个循环,所以这个工作的事实对我来说有点神秘。

如果要创建 4 个子记录,则应使用f.fields_for 4 次。

你可以通过稍微改变你的部分来做到这一点:

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

仪表板/列表/_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 %>

重命名: _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 %>

如果您需要可变数量的Regionalizations ,最好的处理方法是使用一些 Rails AJAX 和 UJS。 这意味着创建只有 1 个新Regionalization记录的表单,并有一个显示“添加另一个”的按钮。 用户单击它,然后您在另一个选择字段中添加您需要的所有内容,包括将结果发布到参数的正确约定。 这是一个完全不同的蠕虫罐头,但我仍然推荐(史蒂夫波利托也是如此) Ryan Bate 的嵌套形式的 Railscast

更好的路线(仍然)

您选择了“has_many/belongs_to :through”选项,但如果Regionalization真的只是一个连接表,您可以使用has_and_belongs_to_many简化它

如果您不确定是否可以走这条路,请考虑:如果Regionalization不需要方法或外键 ID 之外的其他 dB 字段,那么您就增加了不需要的模型的复杂性。

如果Regionalization可以是一个更标准的 JoinTable,你就可以避免一些嵌套的复杂性,让 Rails 为你做。

如果您可以这样做, 请参阅此文档 您需要更改迁移,但可以使用create_join_table让 Rails 为您处理命名和索引。 Rails 会调用这个连接表listings_regions而不是regionalizations ,但你真的不需要引用它。

以下是使用简单连接表的模型的外观:

# 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM