Bill D
Bill D

Reputation: 331

Removing one of the many duplicate entries in a habtm relationship?

For the purposes of the discussion I cooked up a test with two tables:

:stones and :bowls (both created with just timestamps - trivial)

create_table :bowls_stones, :id => false do |t|
  t.integer :bowl_id,  :null => false
  t.integer :stone_id, :null => false
end

The models are pretty self-explanatory, and basic, but here they are:

class Stone < ActiveRecord::Base

  has_and_belongs_to_many :bowls

end

class Bowl < ActiveRecord::Base

  has_and_belongs_to_many :stones

end

Now, the issue is: I want there to be many of the same stone in each bowl. And I want to be able to remove only one, leaving the other identical stones behind. This seems pretty basic, and I'm really hoping that I can both find a solution and not feel like too much of an idiot when I do.

Here's a test run:

@stone = Stone.new
@stone.save
@bowl = Bowl.new
@bowl.save

#test1 - .delete
5.times do
  @bowl.stones << @stone
end

@bowl.stones.count
=> 5
@bowl.stones.delete(@stone)
@bowl.stones.count
=> 0
#removed them all!

#test2 - .delete_at
5.times do
  @bowl.stones << @stone
end

@bowl.stones.count
=> 5
index = @bowl.stones.index(@stone)
@bowl.stones.delete_at(index)
@bowl.stones.count
=> 5
#not surprising, I guess... delete_at isn't part of habtm. Fails silently, though.
@bowl.stones.clear

#this is ridiculous, but... let's wipe it all out
5.times do
  @bowl.stones << @stone
end

@bowl.stones.count
=> 5
ids = @bowl.stone_ids
index = ids.index(@stone.id)
ids.delete_at(index)
@bowl.stones.clear
ids.each do |id|
  @bowl.stones << Stone.find(id)
end
@bowl.stones.count
=> 4
#Is this really the only way?

So... is blowing away the whole thing and reconstructing it from keys really the only way?

Upvotes: 1

Views: 1688

Answers (3)

Dave Pirotte
Dave Pirotte

Reputation: 3816

You should really be using a has_many :through relationship here. Otherwise, yes, the only way to accomplish your goal is to create a method to count the current number of a particular stone, delete them all, then add N - 1 stones back.

class Bowl << ActiveRecord::Base
  has_and_belongs_to_many :stones

  def remove_stone(stone, count = 1)
    current_stones = self.stones.find(:all, :conditions => {:stone_id => stone.id})
    self.stones.delete(stone)
    (current_stones.size - count).times { self.stones << stone }
  end
end

Remember that LIMIT clauses are not supported in DELETE statements so there really is no way to accomplish what you want in SQL without some sort of other identifier in your table.

(MySQL actually does support DELETE ... LIMIT 1 but AFAIK ActiveRecord won't do that for you. You'd need to execute raw SQL.)

Upvotes: 1

wombleton
wombleton

Reputation: 8376

Does the relationship have to be habtm?

You could have something like this ...

class Stone < ActiveRecord::Base
  has_many :stone_placements
end

class StonePlacement < ActiveRecord::Base
  belongs_to :bowl
  belongs_to :stone
end

class Bowl < ActiveRecord::Base
  has_many :stone_placements
  has_many :stones, :through => :stone_placements

  def contents
    self.stone_placements.collect{|p| [p.stone] * p.count }.flatten
  end

  def contents= contents
    contents.sort!{|a, b| a.id <=> b.id}
    contents.uniq.each{|stone|
      count = (contents.rindex(stone) - contents.index(stone)) + 1
      if self.stones.include?(stone)
        placement = self.stone_placements.find(:first, :conditions => ["stone_id = ?", stone])
        if contents.include?(stone)
          placement.count = count
          placement.save!
        else
          placement.destroy!
        end
      else
        self.stone_placements << StonePlacement.create(:stone => stone, :bowl => self, :count => count)
      end
    }
  end
end

... assuming you have a count field on StonePlacement to increment and decrement.

Upvotes: 1

Michael Sofaer
Michael Sofaer

Reputation: 2947

How about

bowl.stones.slice!(0)

Upvotes: 0

Related Questions