views:

228

answers:

3

I'm having a frustrating problem with a has_many through: namely the fact that the through models are not created until save. Unfortunately, I need to set data on these models prior to saving the parent.

Here's the loose setup:

class Wtf < ActiveRecord::Base
  belongs_to :foo
  belongs_to :bar
end

class Bar < ActiveRecord::Base
  has_many :wtfs
  has_many :foos, :through => :wtfs
end


class Foo < ActiveRecord::Base
  has_many :wtfs
  has_many :bars, :through => :wtfs

  def after_initialize
    Bar.all.each do |bar|
      bars << bar
    end
  end

end

Everything is fine except that I need to access the "wtf"'s prior to save:

f = Foo.new => #

f.bars => [list of bars]

empty list here

f.wtfs => []

f.save! => true

now I get stuff

f.wtfs => [list of stuff]

I even went so far as to explicitly create the wtfs doing this:

 def after_initialize
    Bar.all.each do |bar|
      wtfs << Wtf.new( :foo => self, :bar => bar, :data_i_need_to_set => 10)
    end
  end

This causes the f.wtfs to be populated, but not the bars. When I save and retrieve, I get double the expected wtfs.

Anyone have any ideas?

+1  A: 

Couldn't you write a before_save handler on Wtf that would set the data you need to set? It would have access to both the foo and bar, if needed.

nathanvda
Yeah this would work, but I need to display the Wtf data for the use to override.
Joe Cairns
+1  A: 

You could set the method that populates bar to an after_create, like this:

class Foo < ActiveRecord::Base
  has_many :wtfs
  has_many :bars, :through => :wtfs
  after_create :associate_bars

  def associate_bars
    Bar.all.each do |bar|
      bars << bar
    end
  end
end

This would make the wtfs be already be created when this method is called.

robertokl
I tried using after_create, this only seemed to fire after I did a foo.save. Right after Foo.new I want to display Wtf data and allow the user to mess with
Joe Cairns
Also, I really like your idea of creating an "associate_bars" method, I'm definitely implementing that. It's a lot cleaner than what I had.
Joe Cairns
+1  A: 

I think you have the right idea with creating the Wtfs directly. I think it will turn out OK if you just set the bars at the same time:

def after_initialize
  Bar.all.each do |bar|
    wtfs << Wtf.new(:bar => bar, :data_i_need_to_set => 10)  # Rails should auto-assign :foo => self
    bars << bar
  end
end

Rails should save the records correctly because they are the same collection of objects. The only drag might be that if rails doesn't have the smarts to check if a new Bar record in the bars collection already has a Wtf associated, it might create one anyway. Try it out.

Daniel Beardsley
Thanks, yeah it was right, and I'll add your bit about assigning the bars there tooHere was the problem:after_initialize will fire every time the Foo is instantiated, I only need it to fire when I create a new one. So Ill be adding this to the start of the function.return unless self.new_record?
Joe Cairns
Yeah, that might be your best bet for that style... though I might suggest creating a class method on the model called `new_with_bars` that creates a new record and assigns the bars and wtfs
Daniel Beardsley