如何通过 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上的关联通常是单数形式:billBill对应的数据库表通常是复数形式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、连接外键的索引和实际执行计划。
备注:内容仅供参考。
