Rails nested form on many-to-many: how to prevent

2019-05-21 11:52发布

I've setup a nested form in my rails 3.2.3 app, it's working fine, my models are:

class Recipe < ActiveRecord::Base
  attr_accessible :title, :description, :excerpt, :date, :ingredient_lines_attributes

  has_and_belongs_to_many :ingredient_lines
  accepts_nested_attributes_for :ingredient_lines
end

and:

class IngredientLine < ActiveRecord::Base
  attr_accessible :ingredient_id, :measurement_unit_id, :quantity

  has_and_belongs_to_many :recipes
  belongs_to :measurement_unit
  belongs_to :ingredient
end

As above, a Recipe can have multiple IngredientLines and vice versa.

What I'm trying to avoid is record duplication on IngredienLine table.

For example imagine that for recipe_1 an IngredientLine with {"measurement_unit_id" => 1, "ingredient_id" => 1, "quantity" => 3.5} is associated, if for recipe_5 the IngredientLine child form is compiled by the user with the same values, I don't want a new record on IngredientLine table, but only a new association record in the join table ingredient_lines_recipes.

Note that currently I dont't have any IngredientLine controller as saving and updating IngredientLines is handled by nested form routines. Even my Recipe controller is plain and standard:

class RecipesController < ApplicationController
  respond_to :html

  def new
    @recipe = Recipe.new
  end

  def create
    @recipe = Recipe.new(params[:recipe])
    flash[:notice] = 'Recipe saved.' if @recipe.save  
    respond_with(@recipe)
  end

  def destroy
    @recipe = Recipe.find(params[:id])
    @recipe.destroy
    respond_with(:recipes)
  end

  def edit
    respond_with(@recipe = Recipe.find(params[:id]))
  end

  def update
    @recipe = Recipe.find(params[:id])
    flash[:notice] = 'Recipe updated.' if @recipe.update_attributes(params[:recipe])
    respond_with(@recipe)
  end

end

My guess is that should be enough to override the standard create behavior for IngredientLine with find_or_create, but I don't know how to achieve it.

But there's another important point to take care, imagine the edit of a child form where some IngredientLines are present, if I add another IngredientLine, which is already stored in IngredientLine table, rails of course should not write anything on IngredientLine table, but should also distinguish between child records already associated to the parent, and the new child record for which needs to create the relation, writing a new record on the join table.

Thanks!

3条回答
在下西门庆
2楼-- · 2019-05-21 12:37

Old question but I had the same problem. Forgot to add :id to white list with rails 4 strong_parameters.

For example:

widgets_controller.rb

def widget_params
  params.require(:widget).permit(:name, :foos_attributes => [:id, :name, :_destroy],)
end

widget.rb

class Widget < ActiveRecord::Base
  has_many :foos, dependent: :destroy
  accepts_nested_attributes_for :foos, allow_destroy: true
end

foo.rb

class Foo < ActiveRecord::Base
  belongs_to :widget
end
查看更多
淡お忘
3楼-- · 2019-05-21 12:40

in Recipe model redefine method

def ingredient_lines_attributes=(attributes)
   self.ingredient_lines << IngredientLine.where(attributes).first_or_initialize
end
查看更多
我命由我不由天
4楼-- · 2019-05-21 12:42

I have run into a similar situation and found inspiration in this answer. In short, I don't worry about the duplication of nested models until save time.

Translated to your example, I added autosave_associated_records_for_ingredient_lines to Recipe. It iterates through ingredient_lines and performs a find_or_create as your intuition said. If ingredient_lines are complex, Yuri's first_or_initialize approach may be cleaner.

I believe this has the behavior you're looking for: nested models are never duplicated, but editing one causes a new record rather than updating a shared one. There is the strong possibility of orphaned ingredient_lines but if that's a serious concern you could choose to update if that model has only one recipe with an id that matches the current one.

查看更多
登录 后发表回答