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: ActiveRecord::Base 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