How to build ActiveRecord associations with STI
Asked Answered
I

2

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 ?

Ichthyosaur answered 7/3, 2013 at 6:36 Comment(0)
I
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

Ichthyosaur answered 7/3, 2013 at 10:45 Comment(1)
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!Phago
T
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')
Tyrannosaur answered 8/4, 2014 at 6:59 Comment(4)
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 ?Ichthyosaur
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.Tyrannosaur
Hey, Lucy, sorry for taking so long. I`ve tested your example by inputing these lines in an unix console 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 ?Ichthyosaur
@Ichthyosaur - Hmmm, I hadn't considered that. I use Mongoid, so it's possible that acts differently.Tyrannosaur

© 2022 - 2024 — McMap. All rights reserved.