在 rails 6 中使用 order_by 表达式和不同的 true 与 postgresql

using order_by expression and distinct true in rails 6 with postgresql

我正在尝试实现一个非常具体的查询,但似乎无法找到方法。

我正在使用 ransack gem 进行过滤,并且我设置了 (distinct: true),因为我想同时进行过滤和排序,这给我带来了我正在尝试解决的问题。

我有一个名为 A 的模型,A 有一个名为 'status' 的列。 'status' 可以是 'approved'、'approved by manager'、'pending' 或 'rejected'.

我想保留数据库默认顺序,但想return它遵循自定义顺序

STATUS_ORDER = ['approved_by_leader', 'pending', 'approved', 'rejected']

为此我使用了这段代码:

  def self.order_by_status
    ret = "CASE"
    STATUS_ORDER.each_with_index do |s, i|
      ret << " WHEN state LIKE '#{s}' THEN #{i}"
    end
    ret << " END ASC"
  end

然后,我刚刚定义了范围:

scope :by_status_priority, -> { order(order_by_status) }

这是我在控制器中使用的范围。这会起作用,除了我想在 ransack 中保留 (distinct: true),以避免在使用查询时出现重复结果。

我得到的错误是:

"PG::InvalidColumnReference: ERROR:  for SELECT DISTINCT, ORDER BY expressions must appear in select list\nLINE 1:

我可以使用一些建议来处理这个问题。我需要保留 (distinct: true) 或有避免重复的替代方法。

稍后我也会使用嵌套排序,这是 (distinct: true) 无法使用的,但是可以通过在我的控制器中使用 include 和 join 来解决,所以这不会是一个问题。更多信息请点击此处:Ransack, Postgres - sort on column from associated table with distinct: true

我最初的建议是使用 statuspriority 列创建一个 Status 模型和 table,然后与现有模型建立正式关系,因为这将允许在未来进行简单的添加和重新排序,而无需更改任何代码。

我代替这个,错误很明显:

for SELECT DISTINCT, ORDER BY expressions must appear in select list

这意味着当使用 SELECT DISTINCT 时,您只能按出现在 SELECT 子句

中的列进行排序

为了解决这个问题,我建议如下:

  • self.order_by_status 方法中删除“ASC”(以便我们可以重用 CASE 语句
  • 然后更新范围如下
scope :by_status_priority, -> { 
  select(arel_table[Arel.star],Arel.sql(order_by_status).as('status_order') )
    .order(Arel.sql(order_by_status).asc) 
}

TL;DRActiveRecord和Arel的解释

你会在上面看到几次ArelArel 是 rails 的底层查询汇编器,提供了极大的灵活性,允许更多可组合的查询。

每个 ActiveRecord 对象通过名为 arel_table 的方法公开其 Arel::Table。这部分 arel_table[Arel.star] 将组装为“table_name”。“*”(select 所有内容)

这部分 Arel.sql(order_by_status) 将 return 一个 Arel::Nodes::SqlLiteral 允许我们链接别名(如 as('status_order'))以及排序(如 .asc).

我们甚至可以使用 Arel 通过

构建您的整个 CASE 语句
def self.order_by_status
  STATUS_ORDER.each_with_index.inject(Arel::Nodes::Case.new) do |cassette, (s, i)|
    cassette.when(arel_table[:status].eq(s)).then(i)
  end
end

这将允许您摆脱 by_status_priority 中的 Arel.sql 包装器(用于原始 sql 字符串),因此范围现在可以变为

scope :by_status_priority, -> { 
  select(arel_table[Arel.star],order_by_status.as('status_order'))
    .order(order_by_status.asc) 
}

ActiveRecord 提供了很多方便的方法来公开 Arel 查询接口,例如selectwhereorderjoins 等,但不可能以提供简单的顶级 DSL 的简单方式公开所有内容;然而,几乎所有这些“顶级”方法都会毫无问题地接受 Arel 参数,从而允许您构建您可能想要的任何查询。如果它是有效的 SQL 那么 Arel 可以构建它。

试试这个:

  STATUS_ORDER = ['approved_by_leader', 'pending', 'approved', 'rejected'].freeze

  def self.order_by_status
    STATUS_ORDER.map.with_index do |status, ordering|
      "('#{status}', #{ordering})"
    end.join(",\n")
  end

  scope :by_status_priority, -> do
    select("#{self.table_name}.*, status_ordering.*").
      joins("JOIN (values #{order_by_status}) AS status_ordering (status, ordering) ON #{self.table_name}.status = status_ordering.status").
      order_by("status_ordering.ordering")
  end

这个想法是建立一个名为 status_ordering

的虚拟关系
status ordering
'approved_by_leader' 0
... ...

order_by_status方法产生 并加入原来的 table.

查询应如下所示:

SELECT TABLE_NAME.*, status_ordering.* FROM TABLE_NAME
  JOIN (
    values ('approved_by_leader', 0), ('pending', 1), ('approved', 2), ('rejected', 3)
  ) AS status_ordering (status, ordering) ON TABLE_NAME.status = status_ordering.status
  ORDER BY status_ordering.ordering
;

如果您添加 DISTINCT 它应该仍然有效,因为 status_ordering.ordering 仍在 SELECT 表达式中。

P.S。我还没有测试上面的代码。