5

I'm having problems with AR trying to build associations of models that inherit from others. The problem is the associated models are being saved to the database before the call do the save method.

I found more information in this page http://techspry.com/ruby_and_rails/active-records-or-push-or-concat-method/

That's really weird, why would AR automatically save models appended to the association (with << method) ? One would obviously expect that the save method must called, even if the parent already exists. We can prevent this calling

@user.reviews.build(good_params)

but this would be a problem in a context where the association have an hierarchy, for example: if a Hunter has_many :animals, and Dog and Cat inherit from Animal, we can't do

@hunter.dogs.build
@hunter.cats.build 

instead we are stuck with

@hunter.animals << Cat.new
@hunter.animals << Dog.new 

and if the Cat/Dog class has no validations, the object will be saved automatically to the database. How can I prevent this behaviour ?

Gus
  • 942
  • 9
  • 32

2 Answers2

9

I found out that Rails 3 doesn't fully support associations with STI, and usually hacks are needed. Read more on this post http://simple10.com/rails-3-sti/. As mentioned in one of the comments, this issue is referred in rails 4 https://github.com/rails/rails/commit/89b5b31cc4f8407f648a2447665ef23f9024e8a5 Rails sux so bad handling inheritance = (( Hope Rails 4 fixes this.

Meanwhile I'm using this ugly workaround:

animal = @hunter.animals.build type: 'Dog' 

then replace the built object, this step may be necessary for reflection to workout (check Lucy's answer and comments)

hunter.animals[@hunter.animals.index(animal)] = animal.becomes(Dog)

this will behave correctly in this context, since

hunter.animals[@hunter.animals.index(animal)].is_a? Dog

will return true and no database calls will be made with the assignment

Gus
  • 942
  • 9
  • 32
  • hey Gus, can you mark your answer as the answer so I can delete mine? I believe I didn't have much experience with STI at the time I answered the question. Cheers! – jvnill Sep 15 '13 at 23:41
5

Based on Gus's answer I implemented a similar solution:

# instantiate a dog object
dog = Dog.new(name: 'fido')

# get the attributes from the dog, add the class (per Gus's answer)
dog_attributes = dog.attributes.merge(type: 'Dog')

# build a new dog using the correct attributes, including the type
hunter.animals.build(dog_attributes)

Note that the original dog object is just thrown away. Depending on how many attributes you need to set it might be easier to do:

hunter.animals.build(type: 'Dog', name: 'Fido')
Lucy Bain
  • 2,496
  • 7
  • 30
  • 45
  • Hi, Lucy, nice solution, the code is cleaner. I have a doubt though: will hunter.animals[@hunter.animals.index(animal)].is_a?(Dog) return true ? – Gus May 08 '14 at 16:54
  • I just ran a test with my code (obviously not using the exact hunter/dog scenario, but the same idea) and the `is_a?` returned true. If you test it and that's not the case please let me know, I'd be interested to see the code differences. – Lucy Bain May 09 '14 at 11:32
  • Hey, Lucy, sorry for taking so long. I`ve tested your example by inputing these lines in an unix console http://pastebin.com/L6GjB0B2 The last two lines return, respectively 'false' and 'true'. So I guess you have to do the animal.becomes(Dog) part in order to this work properly. What do you think ? Am I missing something ? – Gus Jul 03 '14 at 12:13
  • @Gus - Hmmm, I hadn't considered that. I use Mongoid, so it's possible that acts differently. – Lucy Bain Jul 26 '14 at 03:25