Topic: has_and_belongs_to_many and dependence

Hi! How can I use the behaviour that :dependence => :destroy does when using has_and_belongs_to_many??

My particular problem is: I have items, each one tagged with some tags:

class Item < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
  validates_uniqueness_of :title
  has_and_belongs_to_many :tags
end

class Tag < ActiveRecord::Base
  has_and_belongs_to_many :items
  validates_uniqueness_of :name, :case_sensitive => false
end


...so that an item has some tags, but tags that are not bound to any item should not exist. I would like that tags don't exist if they are not bound to any item, which is a similar behaviour to that in has_many when using :dependence => :destroy.

How can I do it?

Re: has_and_belongs_to_many and dependence

You can try something like this (untested):

class Item < ActiveRecord::Base
  # the other stuff as before . . .
  # then:
  after_destroy :destroy_unused_tags

  private
  def destroy_unused_tags
    tags.each do |tag|
      tag.destroy if tag.items.empty?
    end
  end
end


This basically says that after an Item object is destroyed, it should go throgh each of the tags that was associated to it and destroy it if it isn't associated to any other Item. I'm not sure if this will work because I'm not sure what happens to the records in the items_tags table once an Item is deleted. If the items_tags records belonging to an Item are deleted, then this will work; if they are just left behin, then it wont.  Try it out, if it doesn't work, I have another solution.

Re: has_and_belongs_to_many and dependence

Thanks! I will try it, but, anyway, it only considers the destroy case. It could be that some tags are unbinded from the item, and they should be destroyed too. I wonder if there is a more DRY way of doing that without covering all those cases...

Re: has_and_belongs_to_many and dependence

I disagree. If a tag is created when you add it to an item, and every time you destroy an item it destroys its tags that aren't associated to any other items, there aren't any other "cases" to cover. You will never have any orphaned tags. The only other way that could happen would be if you create a tag without associating it to an item. The solution is trivial: add a custom validation to Tag that checks to see that it is associated to at least on Item before allowing it to save, adding a validation error if it doesn't.

If you have orphaned tags already, you can create a custom rake task that iterates over each tag and destroys any that aren't associated to items. If you aren't convinced that there won't be any future orphaned tags, create a cron job that runs once a week or once a day that runs the rake task you create.

Re: has_and_belongs_to_many and dependence

This is what I tried to say:

item.new(...)
item.tags << Tag.find_by_name("sports") || Tag.new("sports")

And if you change your mind and change that item's tags:

item.tags = [Tag.find_by_name("football") || Tag.new("football")]

Then you might have a "sports" tag which is unbinded... or maybe I'm missing something smile

Re: has_and_belongs_to_many and dependence

You are right, sorry. How about this:

class Item < ActiveRecord::Base
  before_destroy :destroy_unused_tags
  before_save :destroy_unused_tags

  private
  def destroy_unused_tags
    tags.each do |tag|
      # EDIT:
      # tag.destroy unless tag.items.size
      # that won't work, this will:
      tag.destroy unless tag.items.size > 1
    end
  end
end


As an aside, you can clean up the code you just posted by using the dynamic finders rails automatically adds:

@item = Item.new(...)
@item.tags << Tag.find_or_create_by_name("sports")

Last edited by fabio (2007-06-12 13:22:03)

Re: has_and_belongs_to_many and dependence

Thanks man!! I like that solution so will try it. Anyway, I found acts_as_taggable gem (and plugin), so I (or we) have been reinventing the wheel smile But it was fun.

Re: has_and_belongs_to_many and dependence

Well, I'm taking a glance at acts_as_taggable, and I think it doesn't destroy unused tags, so it could be a good idea to include your code someway... The problem is that it's difficult to track all models that use tags to ensure that you can consider a tag as unused and destroy it...

Re: has_and_belongs_to_many and dependence

With acts_as_taggable the tags aren't related directly to a model, they are related to a tagging which is in turn related to a model. Just put the destroy_unused_tags method and callbacks in the Tagging model and make it so that a model's taggings are destroyed when the model object is destroyed.

Re: has_and_belongs_to_many and dependence

Do you mean in the joint model (items_tags)?

Re: has_and_belongs_to_many and dependence

No.  You can ignore my last post. I thought acts_as_taggable worked differently. After looking through the documentation, I see it doesn't work as I thought. Sorry.