Запрос 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

Выводы и предположения

  1. Работает в 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, поэтому его можно поддерживать при обновлении версий. Как вы думаете?

Если вам интересно, вот несколько ссылок, которые я использовал в будущем:

У меня слегка измененный подход от @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))
Другие вопросы по тегам