PAYATHON 2026

如何通过 has_one through 查询客户数组的所有 payments

支付阿杰

结论

如果 Payment 通过单数关联 bill 连接客户,应使用 joins(:bill),再用结构化的 where 条件筛选 bills.client_id:

client_ids = [1, 2, 3, 4]

@payments = Payment
  .joins(:bill)
  .where(bills: { client_id: client_ids })

Rails 会把数组自动转换成 SQL 的 IN (...) 条件。这样不必手动拼接 SQL,也能清楚地区分关联名和数据库表名。

原查询的问题

在 Rails 中,关联名和表名并不是一回事:

  • Payment 上的关联通常是单数形式 :bill
  • Bill 对应的数据库表通常是复数形式 bills

连接关联时应使用:

joins(:bill)

在查询条件中引用数据表时则使用:

where(bills: { client_id: client_ids })

原查询可能把这两个名称都写反了:

Payment.joins(:bills).where('bill.client_id IN (?)', [1, 2, 3, 4])

如果模型声明的是 has_one :bill 或 belongs_to :bill,那么 :bills 就不是正确的关联名。数据库表通常叫 bills,也不是 bill。

推荐写法

假设关联大致如下:

class Payment < ApplicationRecord
  has_one :bill
  has_one :client, through: :bill
end

class Bill < ApplicationRecord
  belongs_to :client
  belongs_to :payment
end

class Client < ApplicationRecord
  has_many :bills
  has_many :payments, through: :bills
end

可以这样查询指定客户的所有付款:

client_ids = [1, 2, 3, 4]

@payments = Payment
  .joins(:bill)
  .where(bills: { client_id: client_ids })

生成的 SQL 逻辑类似:

SELECT payments.*
FROM payments
INNER JOIN bills
  ON bills.payment_id = payments.id
WHERE bills.client_id IN (1, 2, 3, 4);

也可以从 Client 一侧查询:

@payments = Client
  .where(id: [1, 2, 3, 4])
  .joins(:payments)
  .select("payments.*")

不过,从 Payment 开始查询通常更清楚,之后要添加付款相关条件也更方便。

includes 不能直接解决超时

includes 主要用于预加载关联,避免逐条访问关联对象时产生 N+1 查询。筛选关联表时,它通常不是首选。

如果查询后还需要访问每笔付款对应的 bill,可以写成:

@payments = Payment
  .includes(:bill)
  .where(bills: { client_id: [1, 2, 3, 4] })
  .references(:bills)

如果只是按 client_id 筛选付款,使用 joins 更直接:

@payments = Payment
  .joins(:bill)
  .where(bills: { client_id: [1, 2, 3, 4] })

原来的哈希条件语法也有问题:

where(bills: { 'client_id IN (?)', [1, 2, 3, 4] })

哈希条件需要写成字段与值的映射:

where(bills: { client_id: [1, 2, 3, 4] })

如果确实要使用字符串条件,应该这样写:

Payment
  .joins(:bill)
  .where("bills.client_id IN (?)", [1, 2, 3, 4])

但在这个查询中,没有必要手写 IN。

正确查询仍然超时时

如果查询语法已经正确,但 PostgreSQL 仍然超时,就需要检查索引和数据量。至少要确认 bills.client_id 和连接使用的外键都有索引:

add_index :bills, :client_id
add_index :bills, :payment_id

如果每个 payment 最多只能对应一张 bill,可以根据实际业务约束给 payment_id 添加唯一索引:

add_index :bills, :payment_id, unique: true

是否添加唯一索引,要以数据库中的真实关系为准,不能只根据 Rails 的 has_one 声明判断。

还可以查看实际生成的 SQL:

query = Payment
  .joins(:bill)
  .where(bills: { client_id: [1, 2, 3, 4] })

puts query.to_sql

然后用 PostgreSQL 的 EXPLAIN 或 EXPLAIN ANALYZE 检查查询是否进行了大范围顺序扫描。EXPLAIN ANALYZE 会真正执行查询,在生产环境或大表上使用前需要评估影响。

重复结果

如果实际关系并非严格的一对一,或者连接可能返回重复的 payment,可以加上 distinct:

@payments = Payment
  .joins(:bill)
  .where(bills: { client_id: [1, 2, 3, 4] })
  .distinct

真正的 has_one :bill 关系通常不会产生重复记录。可以优先使用:

Payment.joins(:bill).where(bills: { client_id: [1, 2, 3, 4] })

如果仍然超时,再检查 bills.client_id、连接外键的索引和实际执行计划。

备注:内容仅供参考。