ActiveRecord: запрос не использует правильное условие типа для подкласса STI

У меня есть набор подклассов STI, наследуемых от базового класса User. Я обнаружил, что при определенных условиях внутри определения подкласса запросы к подклассам неправильно используют условие type.

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

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

При загрузке консоли Rails в процессе разработки она делает то, что я ожидал:

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

Но при нажатии на приложение (localhost/pow) отсутствует условие type, и я получаю следующее:

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

Но не из приложения при развертывании на промежуточном сервере:

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

Это, конечно, приводит к тому, что любые запросы, выполняемые здесь, в приложении разработчика (но не из консоли), будут неверными. В частности, я пытаюсь предварительно загрузить (небольшой) кеш существующих значений базы данных, чтобы создать несколько полезных методов на основе этих данных. Без области типа кеш явно неверный!

Из того же места (Admin) мы получаем следующее сбивающее с толку противоречие:

[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`"

Кроме того, я определил одноразовый подкласс Q < User внутри файла user.rb. Я зарегистрировал Q.all.to_sql из его определения, из определения Admin и из представления. В таком порядке получаем:

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')

Что может привести к тому, что в первой строке определения подкласса Admin в admin.rb любой подкласс User не сможет использовать свой type_condition?

Это приводит к сбою тестов разработки, что имеет некоторые последствия для моего приложения. Что, черт возьми, может быть причиной такой разницы в поведении? Может ли кто-нибудь придумать более общий способ решения проблемы отсутствия условий STI, определенных для подкласса во время его определения только в среде разработки приложения?


person Andrew Schwartz    schedule 02.05.2016    source источник


Ответы (2)


Одно различие между производством и разработкой заключается в следующей строке внутри конфигурации приложения:

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

vs.

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

Таким образом, в вашей производственной среде все ваши классы загружаются при запуске приложения. Когда для eager_load установлено значение false, Rails попытается автоматически загрузить ваш класс User при первой загрузке класса Admin.

Учитывая это, я бы предположил, что у вас есть другой класс или модуль с именем User.

К вашему сведению: в ActiveRecord есть метод finder_needs_type_condition?. Он должен возвращать true для класса, использующего STI:

User.finder_needs_type_condition? # should be false
Admin.finder_needs_type_condition? # should be true
person Alex    schedule 05.05.2016
comment
Спасибо за советы. Я получаю нетерпеливую разницу в нагрузке, но все еще загадочно, почему проблема нет в консоли разработчика, но присутствует в приложении разработчика (когда я использую localhost/pow) . О методе finder_needs_type_condition? полезно знать: тем не менее, он возвращает true, когда я выхожу из приложения, в то же время выдавая мне "SELECT users.* FROM users", как в вопросе. В связи с этим я нашел приватный метод type_condition, который при вызове to_sql дает "`users`.`type` IN ('Admin')". Значит что-то явно сломалось. - person Andrew Schwartz; 06.05.2016
comment
В связи с этим я не смог воспроизвести это поведение в другом подклассе STI совершенно другого базового класса, поэтому, похоже, это как-то связано с классом User. Прогресс... - person Andrew Schwartz; 06.05.2016
comment
Еще одно обновление: я прокомментировал все в своем базовом классе User, кроме самого объявления класса, и проблема не устранена. Поэтому я не вижу, что может быть особенного в этом классе сейчас, или как то, что вызывает эту проблему, исчезает после завершения определения класса. - person Andrew Schwartz; 06.05.2016
comment
Итак, благодаря вашим указателям, я думаю, что теперь у меня есть обходной путь: при загрузке данных используйте self.finder_needs_type_condition? ? self.where(type_condition) : self.all. Уродливо, не уверен, зачем это нужно, и приводит к лишнему WHERE для других классов STI, у которых нет этой проблемы. Поэтому довольно нежелательно использовать этот код в геме. Но я не уверен, какое лучшее решение может быть на данный момент. - person Andrew Schwartz; 06.05.2016
comment
@andrew, только что нашел страницу, объясняющую автозагрузку с помощью STI: guides.rubyonrails.org / . Он предлагает явно require подклассы... - person andiba; 12.05.2016
comment
Спасибо @andiba! Мне на 100% непонятно, почему, но это решает проблему. Это рекомендуется для того, чтобы базовый класс (User) знал о своих подклассах (Admin), но как требование файла admin.rb из user.rb исправить ошибку, которая возникает внутри самого admin.rb? Очень загадочно. Поскольку я делаю это также из драгоценного камня, лучшее, что я могу сделать, это указать в документах, что вы должны сделать это, если используете драгоценный камень из подкласса STI. В любом случае, это эффективное решение, хотя мне бы хотелось знать, почему это эффективное решение!! - person Andrew Schwartz; 12.05.2016

В настоящее время любой столбец User будет иметь пустой столбец :type. Может ли это быть проблемой?

Вы пытались сделать User суперклассом? Например.

class User < ActiveRecord::Base
end

class Admin < User; end
class NormalUser < User; end
person andiba    schedule 09.05.2016
comment
Поправьте меня, если я что-то неправильно понимаю, я никогда не использовал abstract_class в Ruby/Rails. Читая документы, я вижу, что abstract_class должно быть установлено на true, если я не хочу, чтобы подклассы использовали одно и то же имя таблицы. Но я знаю, поэтому я не уверен, что это правильно для моего варианта использования. Когда я пробую это, я получаю то, что вы могли ожидать: ActiveRecord::StatementInvalid: Mysql2::Error: Table 'threeplay_development.admins' doesn't exist. Что вы имели в виду, говоря, что у любого User будет пустой столбец type? Таблица users заполнена правильно, мне просто нужно загрузить эти данные при запуске приложения. - person Andrew Schwartz; 10.05.2016
comment
Уппс, ты прав насчет abstract_class: это чепуха. Однако проблема с пустым столбцом :type. Когда вы выполняете User.create( {attributes....}) без указания :type, оно получает значение nil. То же самое с Admin.create(...) устанавливает автоматически type = 'Admin'. - person andiba; 10.05.2016
comment
Ага, спасибо за ответ. Данные уже существуют и имеют заполненный столбец type (текущее 13k всего Users, среди них 23 Admins). Здесь я просто читаю из таблицы, используя Admin.all (или self.all внутри определения класса Admin), и он не генерирует соответствующее предложение WHERE при конкретном условии, описанном выше. - person Andrew Schwartz; 10.05.2016