【问题标题】:How do I reference an existing instance of a model in a nested Rails form?如何在嵌套 Rails 表单中引用模型的现有实例?
【发布时间】:2013-11-03 15:27:51
【问题描述】:

我正在尝试使用三个主要模型构建一个配方管理器应用程序:

食谱 - 特定菜肴的食谱
成分 - 成分列表,经过独特性验证
数量 -成分和配方之间的连接表,还反映了特定配方所需的特定成分的量。

我正在使用嵌套表单(见下文),我使用嵌套表单(Part 1Part 2)上的出色 Railscast 构建以获取灵感。 (由于这个特定模式的需要,我的表单在某些方面比教程更复杂,但我能够使其以类似的方式工作。)

但是,当我提交表单时,列出的所有成分都会重新创建 - 如果该成分已存在于数据库中,则它无法通过唯一性验证并阻止创建配方。总阻力。

所以我的问题是: 有没有办法提交此表单,以便如果存在名称与我的某个成分名称字段匹配的成分,它会引用现有成分,而不是尝试创建新的同名?

下面的代码细节...


Recipe.rb:

class Recipe < ActiveRecord::Base
  attr_accessible :name, :description, :directions, :quantities_attributes,
                  :ingredient_attributes

  has_many :quantities, dependent: :destroy
  has_many :ingredients, through: :quantities
  accepts_nested_attributes_for :quantities, allow_destroy: true

Quantity.rb:

class Quantity < ActiveRecord::Base
  attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes

  belongs_to :recipe
  belongs_to :ingredient
  accepts_nested_attributes_for :ingredient

Ingredient.rb:

class Ingredient < ActiveRecord::Base
  attr_accessible :name
  validates :name, :uniqueness => { :case_sensitive => false }

  has_many :quantities
  has_many :recipes, through: :quantities

这是我的嵌套表单,显示在 Recipe#new

<%= form_for @recipe do |f| %>
  <%= render 'recipe_form_errors' %>

  <%= f.label :name %><br>
  <%= f.text_field :name %><br>
  <h3>Ingredients</h3>

  <div id='ingredients'>
    <%= f.fields_for :quantities do |ff| %>
      <div class='ingredient_fields'>
        <%= ff.fields_for :ingredient_attributes do |fff| %>
          <%= fff.label :name %>
          <%= fff.text_field :name %> 
        <% end %>
        <%= ff.label :amount %>
        <%= ff.text_field :amount, size: "10" %>
        <%= ff.hidden_field :_destroy %>
        <%= link_to_function "remove", "remove_fields(this)" %><br>
      </div>
    <% end %>
    <%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %> 
  </div><br>

  <%= f.label :description %><br>
  <%= f.text_area :description, rows: 4, columns: 100 %><br>
  <%= f.label :directions %><br>
  <%= f.text_area :directions, rows: 4, columns: 100 %><br>
  <%= f.submit %>
<% end %>

link_tolink_to_function 允许动态添加和删除数量/成分对,并且改编自前面提到的 Railscast。他们可以使用一些重构,但或多或​​少地工作。


更新:根据 Leger 的要求,这里是来自 recipes_controller.rb 的相关代码。在Recipes#new 路由中,3.times { @recipe.quantities.build } 为任何给定配方设置三个空白数量/配料对;这些可以使用上面提到的“添加成分”和“删除”链接即时删除或添加。

class RecipesController < ApplicationController

  def new
    @recipe = Recipe.new
    3.times { @recipe.quantities.build }
    @quantity = Quantity.new
  end

  def create
    @recipe = Recipe.new(params[:recipe])

    if @recipe.save
      redirect_to @recipe
    else
      render :action => 'new'
    end
  end

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-3 forms


    【解决方案1】:

    你不应该把成分匹配的逻辑放在眼里——Recipe#create 有责任在将它们传递给模型之前创建适当的对象。请分享控制器的相关代码

    写代码前的几点说明:

    1. 我使用 Rails4@ruby2.0,但尝试编写兼容 Rails3 的代码。
    2. attr_acessible 在 Rails 4 中被弃用,因此使用强参数。如果您想升级您的应用,请从一开始就使用强大的参数。
    3. 建议使Ingredient 小写以在不区分大小写的基础上提供统一的外观

    好的,我们开始:

    删除Recipe.rbQuantity.rbIngredient.rb中的attr_accessible字符串。

    不区分大小写,小写Ingredient.rb

    class Ingredient < ActiveRecord::Base
      before_save { self.name.downcase! } # to simplify search and unified view
      validates :name, :uniqueness => { :case_sensitive => false }
    
      has_many :quantities
      has_many :recipes, through: :quantities
    end
    


    &lt;div id='ingredients'&gt; 调整表格的一部分以创建/更新配方:

    <%= f.fields_for :quantities do |ff| %>
      <div class='ingredient_fields'>
        <%= ff.fields_for :ingredient do |fff| %>
          <%= fff.label :name %>
          <%= fff.text_field :name, size: "10" %>
        <% end %>
        ...
      </div>
    <% end %>
    <%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %> 
    

    我们应该使用来自Quantitynested_attributes 的:ingredient,Rails 将在创建params-hash 时添加_attributes-part 以进行进一步的质量分配。它允许在新操作和更新操作中使用相同的表单。为了使这部分正常工作,应提前定义关联。请参阅以下调整后的Recipe#new

    最后是recipes_controller.rb:

    def new
      @recipe = Recipe.new
      3.times do
        @recipe.quantities.build #initialize recipe -> quantities association
        @recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
      end
    end
    
    def create
      @recipe = Recipe.new(recipe_params)    
      prepare_recipe
    
      if @recipe.save ... #now all saved in proper way
    end
    
    def update
      @recipe = Recipe.find(params[:id])
      @recipe.attributes = recipe_params
      prepare_recipe    
    
      if @recipe.save ... #now all saved in proper way
    end
    
    private 
    def prepare_recipe
      @recipe.quantities.each do |quantity|
        # do case-insensitive search via 'where' and building SQL-request
        if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
          quantity.ingredient_id = quantity.ingredient.id = ingredient.id
        end
      end
    end
    
    def recipe_params
      params.require(:recipe).permit(
        :name,
        :description,
        :directions,
        :quantities_attributes => [
          :id,
          :amount,
          :_destroy,
          :ingredient_attributes => [
            #:id commented bc we pick 'id' for existing ingredients manually and for new we create it
            :name
      ]])
    end
    

    prepare_recipe 中,我们做了以下事情:

    1. 查找具有给定名称的成分 ID
    2. 将 foreign_key quantity.ingredient_id 设置为 ID
    3. quantity.ingredient.id 设置为 ID(想想如果您不这样做并更改配方中的成分名称会发生​​什么情况)

    享受吧!

    【讨论】:

    • 感谢您的回复——我已将相关控制器代码添加到原始帖子的底部。适当地指出,关于视图中的逻辑,但我不确定哪些行是有问题的。我想确保用户在创建食谱时可以添加成分及其数量,并且我尝试将代码限制为表单所需的内容。
    • 更新食谱时如何填充成分字段?上面的 sn-p 似乎不起作用。 (导轨 4.1.4)
    • 另外,如何查询数量?有没有办法将其映射到成分?即@recipe.ingredient[1].quantity ?
    • 非常感谢您的回答。我能够使其适应我的应用程序。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    • 1970-01-01
    • 1970-01-01
    • 2011-10-07
    • 1970-01-01
    • 2016-06-13
    • 1970-01-01
    相关资源
    最近更新 更多