Delete in has_many :through

Hi Rails-talkers,

I am having some trouble finding the right way to delete a relationship
in a
has_many :through. I'm running on edge rails here.

class Recipe < ActiveRecord::Base
  has_many :additions
  has_many :ingredients, :through => :additions
end

class Ingredient < ActiveRecord::Base
  has_many :additions
  has_many :recipes, :through => :additions
end

class Addition < ActiveRecord::Base
  belongs_to :recipe
  belongs_to :ingredient
end

In my controller:

def dropingredient
    ingredient = @recipe.additions.find( :first,
                :conditions => ['ingredient_id = ? AND amount = ?',
                                params[:iid].to_i,
params[:amount].to_f] )
    @recipe.additions.delete(ingredient)
    redirect_to :action => 'show', :id => @recipe.id
end

Produces this:

Mysql::Error: Unknown column 'id' in 'where clause': UPDATE additions
SET
recipe_id = NULL WHERE (recipe_id = 16 AND id IN (NULL))

What am I missing?

Thanks,
--Dean - Unscrambler of eggs

Haven't use has_many :through much, so I may have got this wrong, but
don't you want to be doing something like:

def dropingredient
  @recipe.additions.find( :first,
                    :conditions => ['ingredient_id = ? AND amount = ?',
                          params[:iid].to_i, params[:amount].to_f] ).destroy
  redirect_to :action => 'show', :id => @recipe.id
end

My suggestion would be to walk back the cat through the console,
checking the result of each step. It's saved me more times than I can
remember. So:
>> @recipe
>> @recipe.additions
>> @recipe.additions(:first, :conditions => ['ingredient_id = 16 AND
amount = 2'])
>> @recipe.additions(:first, :conditions => ['ingredient_id = 16 AND
amount = 2']).destroy

HTH

Dean wrote:

Chris T wrote:

Haven't use has_many :through much, so I may have got this wrong, but
don't you want to be doing something like:

def dropingredient
  @recipe.additions.find( :first,
                    :conditions => ['ingredient_id = ? AND amount = ?',
                          params[:iid].to_i, params[:amount].to_f] ).destroy
  redirect_to :action => 'show', :id => @recipe.id
end

My suggestion would be to walk back the cat through the console,
checking the result of each step. It's saved me more times than I can
remember. So:
>> @recipe
>> @recipe.additions
>> @recipe.additions(:first, :conditions => ['ingredient_id = 16 AND
amount = 2'])
>> @recipe.additions(:first, :conditions => ['ingredient_id = 16 AND
amount = 2']).destroy

HTH

The doc for has_many lists is a delete method:

http://api.rubyonrails.com/classes/ActiveRecord/Associations/ClassMethods.html#M000530

# collection.delete(object, ...) - removes one or more objects from the
collection by setting their foreign keys to NULL. This will also
destroy the objects if they're declared as belongs_to and dependent
on this model.

Where collection is @recipe.additions in my example. Even using
destroy rails is still looking for an addition.id, which isn't
necessary for a join table. I ended up adding an id column to my join
table and I'll figure it out later. ;-(

--Dean

The delete method is different from destroy in that it deletes the item
without instantiating it first (and so skips any callbacks, dependency
checking etc., which is which destroy is generally preferred in rails
unless there's a specific reason you want to skip them). Also the delete
method you're referring to is a class method, which is why it needs an
id (or an array of them). There's nothing to stop you from using this,
but having found the object through @recipe.additions.find(:first,...)
it makes sense to destroy (or delete if you prefer) the object, rather
than using the class method which essentially does the find again. IMHO
it's more elegant (and more idiomatic Rails). See:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M000907 for
details of this instance method:

    Deletes the record in the database and freezes this instance to
    reflect that no changes should be made (since they can’t be persisted).

Re the id column, yes that's right -- you must have an id column on the
association table (in fact all tables, except has_and_belongs_to_many
join tables) for has_many :through to work, as it's a model in it's own
right.
HTH
Chris

Dean wrote:

Dumb question here, and perhaps a little off topic, but in trying to
understand the "has_many :ingredients, :through => :additions" which
table does additions belong to? (My question is one of my own not
understanding 'has_many:through') .Is there a recipes.additions which
equals ingredients.id ? And if so, why would you use 'through' and not
'foreign_key'

-Ralph Vince

class Recipe < ActiveRecord::Base
  has_many :additions
  has_many :ingredients, :through => :additions
end

class Ingredient < ActiveRecord::Base
  has_many :additions
  has_many :recipes, :through => :additions
end

Ike wrote:

Dumb question here, and perhaps a little off topic, but in trying to
understand the "has_many :ingredients, :through => :additions" which
table does additions belong to? (My question is one of my own not
understanding 'has_many:through') .Is there a recipes.additions which
equals ingredients.id ? And if so, why would you use 'through' and not
'foreign_key'

-Ralph Vince

class Recipe < ActiveRecord::Base
  has_many :additions
  has_many :ingredients, :through => :additions
end

class Ingredient < ActiveRecord::Base
  has_many :additions
  has_many :recipes, :through => :additions
end

Don't forget the additions model

class Addition < ActiveRecrod::Base
  belongs_to :recipe
  belongs_to :ingredient
end

Additions is a model all by itself. has_many :through exists to
augment a has_and_belongs_to with extra data. The additions table
replaces the intermediary table ingredients_recipes that would be used
in a h&btm. It gets its own file in the models directory and you can
add any extra data to the table you want.

For example, when you add an ingredient to your recipe you usually add
it in an amount (eg, 2 Tbs). That data is stored in the additions
table now.

recipe.additions is an array of all the additions - the rows from the
additions table that belong to the recipe. You can then call
recipe.additions[0].ingredient to access the first ingredient.
recipe.additions[0].amount would get you the amount of the first
ingredient you added to the recipe.

HTH,
--Dean