Rails中joins两种查询写法差异及merge与includes兼容问题
一、两种joins写法的差异:看似一致,实则场景不同
首先,你观察到的“返回结果一致”在简单场景下是对的,但这两种写法在复用性、灵活性上有明显区别:
1. 直接where指定关联条件
Table1.joins(:table2).where(:table2 => {field_of_table2: "value"}) 是最直白的写法——直接在where子句中硬编码关联表的过滤条件。Rails会生成对应的SQL,把关联表通过INNER JOIN连接,然后把条件加到WHERE子句里。
这种写法适合简单的一次性查询,但缺点是无法复用查询逻辑。如果之后field_of_table2的过滤条件需要修改(比如改成"new_value"),你得找到所有写了这个条件的地方逐一修改,维护成本高。
2. 使用merge复用关联查询逻辑
Table1.joins(:table2).merge(Table2.where(field_of_table2: "value")) 的核心优势是复用关联模型的查询逻辑。比如如果Table2有一个预先定义的scope:
class Table2 < ApplicationRecord scope :with_target_value, -> { where(field_of_table2: "value") } end
你可以直接写成 Table1.joins(:table2).merge(Table2.with_target_value),不用重复写where条件。后续如果scope的逻辑调整(比如加个时间范围),所有复用这个scope的地方都会自动生效,维护性拉满。
另外,merge还能自动处理表别名问题。比如你写了嵌套关联:Table1.joins(table2: :table3),这时候Table2可能被Rails分配了别名,直接写where条件需要手动指定别名,而merge会自动适配,不用你操心表名的问题。
在SQL层面,简单场景下两者生成的SQL可能完全相同,但merge的扩展性和复用性是直接where写法比不了的。
二、为什么merge无法与includes配合使用?
要搞懂这个问题,得先回忆下includes和joins的本质区别:
joins是内连接+单次查询:直接在数据库层面过滤掉不满足关联条件的主表记录,所有逻辑在一次SQL中完成。includes是预加载:默认分两次查询——先查主表所有记录,再根据主表记录的关联ID批量查询关联表,核心目的是避免N+1查询问题。
merge的作用是把另一个查询的条件合并到当前查询的WHERE子句中,但includes的预加载逻辑是“先查主表,再查关联表”,这就导致了冲突:
- 如果你写
Table1.includes(:table2).merge(Table2.where(field_of_table2: "value")),Rails会先执行SELECT * FROM table1(没有任何过滤),然后再执行SELECT * FROM table2 WHERE id IN (...) AND field_of_table2 = 'value'。这时候主表的记录是全量的,只是关联表只加载了满足条件的记录,完全达不到“过滤主表”的效果。
那如果想要同时实现预加载关联+过滤关联条件怎么办?可以直接用includes配合where条件:
Table1.includes(:table2).where(table2: {field_of_table2: "value"})
这时候Rails会自动把includes转换成LEFT JOIN(或INNER JOIN,取决于你的条件),生成单次SQL查询,既过滤了主表记录,又预加载了关联表,完美解决N+1问题。
内容的提问来源于stack exchange,提问作者Qile

