ActiveRecord builds instance of wrong class through a scope targeting an STI class
Asked Answered
H

2

2

I would like to be able to call the build method on a scope that targets a certain class of model via its STI type, and have ActiveRecord build an instance of the correct class.

class LineItem < ActiveRecord::Base
  scope :discount, where(type: 'DiscountLineItem')
end

class DiscountLineItem < LineItem; end

> LineItem.discount.build # Expect an instance of DiscountLineItem here
=> #<LineItem ...>

Here, I expected an instance of DiscountLineItem, not an instance of LineItem.

Huei answered 9/7, 2012 at 0:29 Comment(0)
K
4

Even though ActiveRecord doesn't instantiate the object as the right class, it does set the type correctly. You basically have two ways around this:

1) Create the object and then reload it from the database:

item = LineItem.discount.create(attrs...)
item = LineItem.find(item.id)

2) Use the STI class and build the object directly from it:

DiscountLineItem.build

With all that ActiveRecord can do, this does seem like kind of a senseless limitation and might not be too hard to change. Now you've piqued my interested :)

Update:

This was recently added to Rails 4.0 with the following commit message:

Allows you to do BaseClass.new(:type => "SubClass") as well as parent.children.build(:type => "SubClass") or parent.build_child to initialize an STI subclass. Ensures that the class name is a valid class and that it is in the ancestors of the super class that the association is expecting.

Kaufmann answered 9/7, 2012 at 0:43 Comment(6)
Perhaps it's time, then, to move this discussion into the Rails tracker. I'll write up a failing test case, and post it today.Huei
I coded up a fix last night and was going to post it to the rails core google group for feedback. I'll post it here as a update later this evening when I get a chance.Kaufmann
@Huei Here's a patch for ActiveRecord that you might want to try: gist.github.com/5cad22a11f011052d8f6Kaufmann
Would you like to submit that to github.com/rails/rails/issues/7021 @beerlington? I've written up that failing test and referenced it there.Huei
@Huei Looks like the issue got closed :/. If that's the expected behavior, it'll be hard to convince people to change it. You can use one of the options I suggested or feel free to take the patch I wrote and use it in your app or create a gem with it. Also, it was based on Rails 3.2.6 so I don't know if it will work in 4.0.Kaufmann
@Huei Good news, looks like someone's pull request got accepted for this. I updated my answer for anyone who stumbles on this in the future.Kaufmann
M
1

Forget about build for a moment. If you have some LineItem l and you do l.discount you're going to get LineItem instances, not DiscountLineItem instances. If you want to get DiscountLineItem instances, I suggest converting the scope to a method

def self.discount
  where(type: 'DiscountLineItem').map { |l| l.becomes(l.type.constantize) }
end

Now you will get back a collection of DiscountLineItem instances.

Milkwhite answered 9/7, 2012 at 0:44 Comment(5)
That should be def self.discountKaufmann
I like the concept, but using map will kill your ability to chain any other scopes or methods since you're converting the ActiveRecord::Relation object to an array. There has to be a way to do this cleanly :/Kaufmann
Yup, I just need to read docs on converting it back to a relation.Milkwhite
Ooh. Though this technique end-of-lifes the ActiveRecord::Relation, stopping my ability to chain, thank you for pointing out the becomes method.Huei
Yeah, its one of those methods you knew you were looking many times with your polymorphic work for but didn't know what to search for to find it. I'm looking more into an alternative solution to keep the relation, but have a feeling (with my current rails knowledge) its going to end up pretty hack-ish.Milkwhite

© 2022 - 2024 — McMap. All rights reserved.