views:

255

answers:

3

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?

+1  A: 

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.

wombleton
Well, no... I suppose it doesn't HAVE to be. I've certainly considered your workaround... ultimately, I'd rather move around actual objects than keep counters. Love your pic.
Bill D
You could also do `StonePlacement.find_by_bowl_and_stone(@bowl, @stone).first.destroy` or something similar if you wanted to do it that way.
wombleton
Sure sure... having an intermediate object to manipulate opens up all kinds of possibilities. Is the answer to my question, then, a "no?"
Bill D
I don't think that working hard to have multiple identical rows (i.e. impossible to know which one you're going to operate on) in the join table is a good use of your time. If you want to add and remove stones from bowls, there's nothing stopping you from adding an add_stone or remove_stone method to your bowl object.
wombleton
Updated answer to fake out what I think you're after.
wombleton
It's clear that you're right - having an intermediate table is clearly the way to go.It really seems like there must be a way to do what I was originally trying to do, however.
Bill D
A: 

How about

bowl.stones.slice!(0)
Michael Sofaer
>> @bowl.stones.count=> 5>> @bowl.stones.slice!(0)=> #<Stone id: 2, created_at: "2009-07-08 13:41:06", updated_at: "2009-07-08 13:41:06">>> @bowl.stones.count=> 5# slice isn't a habtm method, but I did get my hopes up a bit.
Bill D
Ahh, sorry. Worth a shot, though.
Michael Sofaer
A: 

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.)

Dave Pirotte
I'm convinced! I did not know that LIMIT was not supported by DELETE (and hadn't gotten desperately curious enough to dig in the SQL lorebooks yet). Thanks!
Bill D