问题描述
我正在尝试使用以下三种主要模型构建一个食谱管理器应用程序:
I'm attempting to build a recipe-keeper app with three primary models:
食谱-特定菜肴的食谱
成分-通过唯一性验证的成分列表
数量-成分和配方之间的联接表,还反映了特定配方所需的特定成分的数量.
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
我使用的是嵌套表格(请参见下文),该表格是通过嵌套表格上的棒Railscast构建的(第1部分,第2部分)以获取灵感.(由于这种特定模式的需要,我的表单在某种程度上比本教程要复杂,但是我能够使它以类似的方式工作.)
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
但是,提交我的表单时,列出的所有成分都会重新创建-如果该成分已经存在于数据库中,它将无法通过唯一性验证并阻止创建配方.总阻力.
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
所以我的问题是:有没有一种方法可以提交此表单,以便如果存在名称与我的成分名称字段之一匹配的成分,那么它将引用现有成分,而不是尝试创建该成分一个同名的新人?
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
下面的代码详细信息...
Code specifics below...
在 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
:
Here's my nested form that displays at 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_to
和 link_to_function
可以即时添加和删除数量/成分对,它们是从前面提到的Railscast改编而来的.他们可以使用一些重构,但是可以按需工作或多或少.
The link_to
and link_to_function
are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
更新:根据Leger的要求,以下是 recipes_controller.rb
中的相关代码.在 Recipes#new
路线中, 3.times {@ recipe.quantities.build}
为任何给定的配方设置三个空白数量/成分对.您可以使用上面提到的添加成分"和删除"链接将其删除或添加.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb
. In the Recipes#new
route, 3.times { @recipe.quantities.build }
sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
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
推荐答案
您不应该考虑配料匹配的逻辑-在传递'前, Recipe#create
的职责是创建适当的对象em到Model.请分享控制器的相关代码
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create
to create proper objects before passing 'em to Model. Pls share the relevant code for controller
在编写代码之前很少注意:
Few notes before coming to code:
- 我使用[email protected],但尝试编写与Rails3兼容的代码. 在Rails 4中不推荐使用
-
attr_acessible
,因此使用了更强的参数.如果您想升级您的应用,只需从一开始就使用强大的参数即可. - 建议使
Ingredient
小写,以在不区分大小写的基础上提供统一的外观
- I use [email protected], but tried to write Rails3-compatible code.
attr_acessible
was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.- Recommend to make
Ingredient
low-cased to provide uniform appearance on top of case-insensitivity
好的,我们开始:
删除 Recipe.rb
, Quantity.rb
和 Ingredient.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
< div id ='ingredients'>
部分,以创建/更新配方:
<div id='ingredients'>
part of adjusted form to create/update Recipe:
<%= 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' %>
我们应该使用 Quantity
nested_attributes中的:ingredient
,Rails在创建 params
时会添加 _attributes
-part-哈希以进行进一步的批量分配.它允许在新动作和更新动作中使用相同的形式.为使此部分正常工作,应提前定义关联.请参见下面的调整后的 Recipe#new
.
We should use :ingredient
from Quantity
nested_attributes and Rails will add up _attributes
-part while creating params
-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new
bellow.
,最后是 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
中,我们执行以下操作:
In prepare_recipe
we do the following things:
- 查找具有给定名称的配料的ID
- 将Foreign_key
quantity.ingredient_id
设置为ID - 将
quantity.ingredient.id
设置为ID(想想如果不这样做会发生什么情况,并在配方中更改成分名称会发生什么情况)
- Find ID of ingredient with given name
- Set foreign_key
quantity.ingredient_id
to ID - Set
quantity.ingredient.id
to ID (think what happens if you don't do that and change ingredient name in Recipe)
享受!
这篇关于如何以嵌套的Rails形式引用模型的现有实例?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!