具有别名表名的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查询,该查询引用包含关注点的少数模型,则会导致“模糊列名称”exception,这是有意义的。

为了帮助解决这个问题,我将列名称辅助方法更改为现在

 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中强制执行查询顺序?

好吧,好吧。 经过相当大的时间查看ArelActiveRecordRails问题的来源(似乎这不是新的),我能够找到访问当前arel_table对象的方法,如果它们被使用,则使用其table_aliasescurrent范围。

这使得可以知道作用域是否将在具有表名别名的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 ,而Arel::SelectManager将为您提供#left上的当前表。 此时有两个选项:你有一个Arel::Table (没有别名,表名在#name )或者你有一个Arel::Nodes::TableAliasArel::Nodes::TableAlias上的别名。

使用该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方法对AR / Arel公共API有用,因此可以跨版本升级进行维护。 你怎么看?

如果你有兴趣,这里有一些我在路上使用的参考:

  • 关于SO的一个类似的问题 ,回答了大量的代码,你可以使用而不是你美丽而简洁的能力。
  • 这个Rails问题和另一个 问题 。
  • 并在github 上的测试应用程序上提交 ,使测试变为绿色!