Запрос ActiveRecord с псевдонимами имен таблиц
Используя проблемы модели, которые включают области, каков наилучший способ написать их, зная, что вероятны вложенные и / или ссылающиеся на себя запросы?
В одной из моих проблем у меня есть области, подобные этим:
scope :current, ->(as_at = Time.now) { current_and_expired(as_at).current_and_future(as_at) }
scope :current_and_future, ->(as_at = Time.now) { where("#{upper_bound_column} IS NULL OR #{upper_bound_column} >= ?", as_at) }
scope :current_and_expired, ->(as_at = Time.now) { where("#{lower_bound_column} IS NULL OR #{lower_bound_column} <= ?", as_at) }
def self.lower_bound_column
lower_bound_field
end
def self.upper_bound_column
upper_bound_field
end
И упоминается через has_many, например: has_many :company_users, -> { current }
Если делается запрос ActiveRecord, который ссылается на несколько моделей, которые включают проблему, это приводит к исключению "неоднозначное имя столбца", которое имеет смысл.
Чтобы помочь преодолеть это, я изменяю вспомогательные методы имени столбца, чтобы теперь
def self.lower_bound_column
"#{self.table_name}.#{lower_bound_field}"
end
def self.upper_bound_column
"#{self.table_name}.#{upper_bound_field}"
end
Что прекрасно работает, пока вам не потребуются запросы с самостоятельными ссылками. Arel помогает смягчить эти проблемы, используя псевдонимы таблицы в результирующем SQL, например:
LEFT OUTER JOIN "company_users" "company_users_companies" ON "company_users_companies"."company_id" = "companies"."id"
а также
INNER JOIN "company_users" ON "users"."id" = "company_users"."user_id" WHERE "company_users"."company_id" = $2
Проблема здесь в том, что self.table_name
больше не ссылается на имя таблицы в запросе. И это приводит к языку в щеке намек: HINT: Perhaps you meant to reference the table alias "company_users_companies"
В попытке перенести эти запросы в Arel я изменил вспомогательные методы имени столбца на:
def self.lower_bound_column
self.class.arel_table[lower_bound_field.to_sym]
end
def self.upper_bound_column
self.class.arel_table[upper_bound_field.to_sym]
end
и обновил области, чтобы отразить:
lower_bound_column.eq(nil).or(lower_bound_column.lteq(as_at))
но это просто перенесло проблему через self.class.arel_table
всегда будет одинаковым независимо от запроса.
Я предполагаю, что мой вопрос заключается в том, как мне создать области, которые можно использовать в запросах с самообращением, для которых требуются такие операторы, как <=
а также >=
?
Правки
Я создал основное приложение, чтобы помочь продемонстрировать эту проблему.
git clone git@github.com:fattymiller/expirable_test.git
cd expirable_test
createdb expirable_test-development
bundle install
rake db:migrate
rake db:seed
rails s
Выводы и предположения
- Работает в sqlite3, а не в Postgres. Скорее всего, потому что Postgres обеспечивает порядок запросов в SQL?
1 ответ
Так так так. После довольно большого времени, просматривая источники Arel
, ActiveRecord
а также Rails
проблемы (кажется, это не ново), я смог найти способ доступа к текущему arel_table
объект, с его table_aliases
если они используются, внутри current
сфера в момент ее исполнения.
Это позволило узнать, будет ли область использоваться в JOIN
с псевдонимом таблицы, или, с другой стороны, область действия может использоваться для реального имени таблицы.
Я только что добавил этот метод в ваш Expirable
беспокойство:
def self.current_table_name
current_table = current_scope.arel.source.left
case current_table
when Arel::Table
current_table.name
when Arel::Nodes::TableAlias
current_table.right
else
fail
end
end
Как видите, я использую current_scope
в качестве базового объекта для поиска таблицы arel, вместо предыдущих попыток использования self.class.arel_table
или даже relation.arel_table
, который, как вы сказали, остался прежним, независимо от того, где был использован объем. Я просто звоню source
на этом объекте, чтобы получить Arel::SelectManager
что в свою очередь даст вам текущую таблицу на #left
, На данный момент есть два варианта: что у вас там есть Arel::Table
(без псевдонима, имя таблицы включено #name
) или что у вас есть Arel::Nodes::TableAlias
с псевдонимом на его #right
,
С этим table_name вы можете вернуться к своей первой попытке #{current_table_name}.#{lower_bound_field}
а также #{current_table_name}.#{upper_bound_field}
в ваших областях:
def self.lower_bound_column
"#{current_table_name}.#{lower_bound_field}"
end
def self.upper_bound_column
"#{current_table_name}.#{upper_bound_field}"
end
scope :current_and_future, ->(as_at = Time.now) { where("#{upper_bound_column} IS NULL OR #{upper_bound_column} >= ?", as_at) }
scope :current_and_expired, ->(as_at = Time.now) { where("#{lower_bound_column} IS NULL OR #{lower_bound_column} <= ?", as_at) }
это current_table_name
Мне кажется, что этот метод был бы полезен для публичного API AR / Arel, поэтому его можно поддерживать при обновлении версий. Как вы думаете?
Если вам интересно, вот несколько ссылок, которые я использовал в будущем:
- На аналогичный вопрос по SO ответил тонна кода, которую вы могли бы использовать вместо своей красивой и лаконичной Способности.
- Этот вопрос Rails и этот другой.
- И сделайте коммит на вашем тестовом приложении на github, которое сделало тесты зелеными!
У меня слегка измененный подход от @dgilperez, который использует всю мощь Arel
def self.current_table_name
current_table = current_scope.arel.source.left
end
Теперь вы можете изменить ваши методы с синтаксисом arel_table
def self.lower_bound_column
current_table[:lower_bound_field]
end
def self.upper_bound_column
current_table[:upper_bound_field]
end
и использовать это запрос, как это
lower_bound_column.eq(nil).or(lower_bound_column.lteq(as_at))