ActiveRecord: query not using correct type condition for STI subclass
Asked Answered
H

2

11

I have a set of STI subclasses inheriting from a User base class. I am finding that under certain conditions inside a subclass' definition, queries on the subclasses do not correctly use the type condition.

class User < ActiveRecord::Base
  # ...
end

class Admin < User
  Rails.logger.info "#{name}: #{all.to_sql}"
  # ...
end

When loading the Rails console in development, it does what I would expect:

Admin: SELECT `users`.* FROM `users` WHERE `users`.`type` IN ('Admin')

But when hitting the app (localhost / pow), it is missing the type condition and I get this:

Admin: SELECT `users`.* FROM `users`

But not from the app when when deployed to a staging server:

Admin: SELECT `users`.* FROM `users` WHERE `users`.`type` IN ('Admin')

This, of course, causes any queries executed here in the dev app (but not from the console) to be incorrect. Specifically, I am trying to preload a (small) cache of existing db values in order to create a few helpful methods based on those data. Without the type scope, the cache is obviously incorrect!

From the same location (Admin), we get the following confusing contradiction:

[11] pry(Admin)> Admin.finder_needs_type_condition?
=> true
[12] pry(Admin)> Admin.send(:type_condition).to_sql
=> "`users`.`type` IN ('Admin')"
[13] pry(Admin)> Admin.all.to_sql
=> "SELECT `users`.* FROM `users`"

Further, I defined a throwaway subclass Q < User inside the user.rb file. I logged Q.all.to_sql from its definition, from the definition of Admin, and from a view. In that order, we get:

From Q: Q: SELECT `users`.* FROM `users` WHERE `users`.`type` IN ('Q')
From Admin: Q: SELECT `users`.* FROM `users`
From View: Q: SELECT `users`.* FROM `users` WHERE `users`.`type` IN ('Q')

What could cause, in the first line of the Admin subclass definition in admin.rb, any subclass of User to fail to use its type_condition?

This is causing development tests to fail, and so is of some consequence to my app. What on earth could be causing this difference in behavior? Can anyone think of a more general way around the problem of not having the STI conditions defined on a subclass during its definition only in the development app environment?

Help answered 2/5, 2016 at 22:14 Comment(0)
A
4

One difference between production and development is the following line inside of the application configuration:

# config/environments/development.rb
config.eager_load = false

vs.

# config/environments/production.rb
config.eager_load = true

So on your production environment, all your clases are loaded when the app is started. When eager_load is set to false, Rails will try to autoload your User class when you first load the Admin class.

Given that, I'd assume that you have another class or module named User.

FYI: ActiveRecord has a method called finder_needs_type_condition?. It should return true for a class that uses STI:

User.finder_needs_type_condition? # should be false
Admin.finder_needs_type_condition? # should be true
Aminopyrine answered 5/5, 2016 at 18:58 Comment(6)
Thanks for the tips. I get the eager load difference, but it's still mysterious why the issue is not present in the dev console but is present in the dev app (when I use localhost / pow). The finder_needs_type_condition? method is good to know about: yet, it returns true when I pry from the app while still giving me "SELECT users.* FROM users" as in the question. Relatedly, I found the private method type_condition, which, when calling to_sql on, yields "`users`.`type` IN ('Admin')". So something is clearly broke.Help
Related, I was not able to replicate this behavior on another STI subclass of a completely different base class, so this seems to have something to do with the User class specifically. Progress...Help
One more update: I've commented everything in my User base class out other than the class declaration itself, and the issue persists. So I can't see what could be special about this class now, or how whatever is causing this issue goes away after the class definition is finished.Help
Ok so thanks to your pointers I think I at least have a workaround now: when loading the data, use self.finder_needs_type_condition? ? self.where(type_condition) : self.all. Ugly, not sure why it's necessary, and leads to a superfluous WHERE for other STI classes that don't have this issue. So pretty undesirable to actually use this code in a gem. But I'm not sure what a better solution may be at this point.Help
@andrew, just found a page that explains autoloading with STI: guides.rubyonrails.org/… . It suggests to explicitely require subclasses...Irrelevant
Thanks @andiba! It's 100% not clear to me why, but that does fix the issue. That is recommended to allow the base class (User) to be aware of its subclasses (Admin), but how does requiring the admin.rb file from user.rb fix an error that is occurring inside admin.rb itself? Very puzzling. Since I'm doing this from a gem, too, the best I can do is put it in the docs that you should do this if you're using the gem from a STI subclass. In any case, this is an effective solution, it would seem -- though I would love to know why it's an effective solution!!Help
I
1

Currently, any User will have an empty :type column. Could this is be a problem?

Have you tried to make User a super class? E.g.

class User < ActiveRecord::Base
end

class Admin < User; end
class NormalUser < User; end
Irrelevant answered 9/5, 2016 at 15:17 Comment(3)
Correct me if Im misunderstanding something, I've never used abstract_class in Ruby / Rails. Reading the docs, I see that abstract_class should be set to true if I do not want the subclasses to use the same table name. But I do, so I'm not sure this is correct for my use case. When I try it, I get what you might then expect: ActiveRecord::StatementInvalid: Mysql2::Error: Table 'threeplay_development.admins' doesn't exist. What did you mean by any User will have an empty type column? The users table is populated correctly, I just need to load that data when the app starts.Help
Upps, you're right with the abstract_class: That was nonsense. However, the empty :type column problem. When you do a User.create( {attributes....}) without specifying :type it gets a nil value. The same with Admin.create(...) sets automatically type = 'Admin'.Irrelevant
Yep, thanks for the response. The data already exists and has the type column populated (current 13k total Users, among them 23 Admins). Here I'm just reading from the table using Admin.all (or self.all inside the Admin class' definition), and it is not generating an appropriate WHERE clause under the specific condition outlined above.Help

© 2022 - 2024 — McMap. All rights reserved.