0

Model are like:

class Forrest < ActiveRecord::Base
  has_many :trees, inverse_of: :forrest
  has_many :oak_trees, inverse_of: :forrest
  has_many :palm_trees, inverse_of: :forrest
end

class Tree < ActiveRecord::Base
  belongs_to :forrest, inverse_of: :trees
end

class OakTree < Tree
  def height=(value)
    # Stuff
  end
end

Then this:

forrest = Forrest.create
forrest.oak_trees.build(height: :tall)

In the height= method I need forrest, but it's nil. I assume there must be some way to correct this.

I tried moving the belongs_to from Tree to the subclasses and adding like inverse_of: oak_trees.

But I cannot figure out how to make it work.

Tim Scott
  • 15,106
  • 9
  • 65
  • 79
  • 1
    "Forrest" or "forest"? How did you create your `OakTree` record? Did you save it? You may only have the `id` populated during the `build` call, it's part of the create scope. – tadman May 20 '15 at 21:28
  • What data type is height expected to be? Perhaps you should be using a string "tall" instead of :tall. Also you haven't shown it here but the forest instance must exist before you build the oak_trees on it. – 6ft Dan May 21 '15 at 00:15
  • @tadman I'm not sure what you're asking. The ```build``` method instantiates an OakTree and adds it to the ```oak_trees``` collection of the forrest instance. It should also set the ```forrest``` of the new OakTree to that same instance of forrest, completing the bi-directional association in memory. – Tim Scott May 21 '15 at 01:22
  • @6ft Dan The type of height is not relevant here. The problem is that the forrest of the new oak tree is nil. Yes, in my code example, ```forrest``` is not nil. It is a persisted instance. I updated the question to make that clear. – Tim Scott May 21 '15 at 01:25

2 Answers2

1

Unsaved models are not accessible in this manner. The relationship between them isn't formalized, the associated model doesn't exist yet.

ActiveRecord is usually pretty good about filling in the gaps, but the way the information on creation scope is passed down is via ID, not object, and as your Forest record isn't saved, it doesn't exist yet.

The way ActiveRecord handles this is by keeping unsaved records in the oak_trees cache structure. On save they're baked out properly.

This will work:

forest = Forest.create
tree = forest.oak_trees.build(height: tall)

If you need access prior to saving you might have to hack around a lot, or you can create them this way:

forest = Forest.new
tree = OakTree.new(forest: forest, height: tall)
tadman
  • 208,517
  • 23
  • 234
  • 262
  • Sorry, my example was wrong. The ```forrest``` is indeed persisted, and yet it does not work. I have updated my question. – Tim Scott May 21 '15 at 21:15
  • The workaround you suggest does work. That is, ```forrest``` is populated in the ```height``` setter. However, if you move ```forrest``` after ```height``` in the parameter list, then ```forrest``` is nil. Devs don't (and shouldn't) expect order of parameters to matter, so I'm not sure I can leave behind such a nasty landmine. – Tim Scott May 21 '15 at 21:27
  • @TimScott The options are applied in the order they're given, so if you need to defer something, you'll need to have a hook like `before_validation` or `before_save` which triggers the desired behaviour. – tadman May 21 '15 at 21:34
0

It's a guess, but maybe rails sets the attributes you pass before setting the parent.

So, does this work?

oak = forrest.oak_trees.build
oak.height = :tall

Also try the version with a block

forrest.oak_trees.build { |o| o.height = :tall }
Robin
  • 21,667
  • 10
  • 62
  • 85
  • Although these might "work" the system would remain broken because ```#build``` and ```#create``` would not behave as expected. I prefer not to leave a huge landmine like this for future developers. On a more practical note, I would have to litter my tests with this special handing in dozens of places replacing simple factory calls. – Tim Scott May 20 '15 at 21:59
  • I believe this works because `height=` is not an attribute, it's a method. `#build` expects attributes. – Rots May 20 '15 at 22:34
  • When you create get and set methods named the same as an attribute, ActiveRecord uses those instead of ```write_attribute``` and ```read_attribute```. http://stackoverflow.com/a/10465235/29493 – Tim Scott May 21 '15 at 01:18
  • @TimScott Yes but you must define the attribute, which you haven't done. eg: `attr_accessible: :height `. That allows mass assignment of attributes: http://stackoverflow.com/questions/3136420/difference-between-attr-accessor-and-attr-accessible – Rots May 21 '15 at 08:31
  • @Rots Sorry, yes, it's accessible. I omitted that for simplicity since setting the the attribute is not the issue but access to forrest while setting it. – Tim Scott May 21 '15 at 14:18
  • @Rots but in rails 4, the concern of mass assignment has been moved to the controller away from the model, so attr_accessible isn't needed. – Robin May 21 '15 at 21:02