Rails has_many 根据最新的 created_at 记录对列进行排序

Rails has_many sorting of a column based on latest created_at record

我有两个模型,分别命名为 'customer' 和 'membership'。客户可以有很多会员资格。我想根据成员资格的 created_at 和 cancelled_at 日期列对记录进行排序。会员记录应该是最近创建和取消的记录,不应包含 nil 值。例如,如果 ID 为 1 的客户有两条 created_at 分别为 2020 年 12 月 25 日和 2020 年 12 月 27 日的会员记录,取消日期为 2021 年 1 月 1 日和 nil,那么该记录应该放在底部,因为最新的会员资格仍然有效它的 cancelled_at 日期为 nil,不应出现在 asc 或 desc 排序列表中。但是,如果客户有 2 个 created_at 的会员资格,分别为 2020 年 12 月 25 日和 2020 年 12 月 27 日,取消日期为 2000 年 1 月 1 日和 2000 年 1 月 5 日,那么应该弹出一条记录,取消日期为 2000 年 1 月 5 日。

Database Table - Customer
Id      Name
1        A
2        B
3        C
4        D
5        E

Database Table - Membership
id   customer_id   created_at    cancelled_at
1       1           1 jan 2000     5 jan 2000
2       1           2 jan 2000     nil
3       2           1 Dec 1999     2 Dec 1999
4       5           15 Jan 2000    16 Jan 2000
5       5           17 Jan 2000    20 Jan 2000

Then result should be (for asc order of cancelled_at)
customer_id name cancelled at    
    2       B      2 Dec 1999
    5       E      20 Jan 2000
    1       A      nil
    3       C      nil
    4       D      nil

我的查询没有产生所需的输出:

Customer.joins('LEFT JOIN memberships')
      .where("memberships.cancel_at = (SELECT MAX(memberships.cancel_at) FROM memberships WHERE customers.id = memberships.customer_id)")
      .group('customers.id')
      .order('MAX(memberships.created_at) ASC')

解决这个问题的一个非常有效的方法是使用 lateral join:

SELECT c.*, latest_membership.cancelled_at AS cancelled_at 
FROM customers c
LEFT JOIN LATERAL (
  SELECT cancelled_at
  FROM memberships m
  WHERE m.customer_id = c.id -- lateral reference
  ORDER BY m.created_at
  LIMIT 1
) AS latest_membership ON TRUE
ORDER BY latest_membership.cancelled_at ASC NULLS LAST

这个奇特的新玩具可能有点难以理解,但它基本上就像一个 foreach 循环 - 除了 SQL。 PostgreSQL 将遍历结果集中的每一行并使用该行作为参数评估子查询。疯狂的东西!

ActiveRecord 不支持“开箱即用”的横向连接,因为它是一个相当 Postgres 特定的功能,它旨在成为多语言。但是您可以使用 SQL 字符串或 Arel.

创建它们
Customer
  .select(
     'customers.*', 
     'latest_membership.cancelled_at AS cancelled_at'
  )
  .joins(<<~SQL)
    LEFT JOIN LATERAL (
      SELECT cancelled_at
      FROM memberships m
      WHERE m.customer_id = c.id -- lateral reference
      ORDER created_at
      LIMIT 1
    ) AS latest_membership ON TRUE
  SQL
  .order('latest_membership.cancelled_at ASC NULLS LAST')