Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to build ActiveRecord associations with STI

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 ?

like image 691
Gus Avatar asked Dec 08 '22 17:12

Gus


2 Answers

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

like image 73
Gus Avatar answered Dec 11 '22 08:12

Gus


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')
like image 39
Lucy Bain Avatar answered Dec 11 '22 07:12

Lucy Bain