我在一个项目上工作,其中有 STI
Item
和 5 个子类(Item1
,Item2
... Item5
)。此 STI(items
表)映射到连接表 item_parents
到 Parent
记录(parents
表)记录。映射是通过has_many trough:
完成的。
每一项都有两个字段:
name
,code
都是字符串。 Parent
有很多字段,但为了举例,我们假设它有 name
, created_at
。
在前端,它们显示在一个表中,如下所示:
Parent.name | Parent.created_at | Item1.name | Item1.code | Item2.name | Item2.code | ...
用户可以为每一列配置过滤。它可以是任何组合或根本没有过滤器。例如,他们可以选择以下组合:
Parent.created_at before 2020.02.22
Item1.name containing 'abc'
Item2.name containing 'xyz'
Item3.code equals 'Z12'
过滤代码是这样实现的:
def search(filters)
filters.reduce(Parent.all) { |query, (key, value)| apply_filter(query, key, value) }
end
def apply_filter(query, key, value)
case filter_key
when :parent_name_contains
query.where(Parent.arel_table[:name].matches("%#{value}%"))
when :parent_created_at_before
query.where(Parent.arel_table[:created_at].lt(value))
when :item1_name_contains
query.joins(:item1s).where(Item1.arel_table[:name].matches("%#{value}%"))
when :item2_name_contains
query.joins(:item2s).where(Item2.arel_table[:name].matches("%#{value}%"))
when :item1_code_equals
query.joins(:item1s).where(Item1.arel_table[:name].eq(value))
when :item2_code_equals
query.joins(:item2s).where(Item2.arel_table[:name].eq(value))
# ... and so on for all the filters
else
query
end
end
当我通过
Item
的两个或多个不同子类的字段进行查询时,ActiveRecord 无法生成正确的 WHERE
子句。它不使用它在 JOIN
子句中为关联分配的别名。
假设我想按
Item1.name = 'i1'
和Item2.name = 'i2'
过滤,那么rails生成的是这样的:
SELECT "parents".*
FROM "parents"
INNER JOIN "item_parents"
ON "item_parents"."parent_id" = "parents"."id"
INNER JOIN "items"
ON "items"."id" = "item_parents"."item_id"
AND "items"."item_type" = 'Item::Item1'
INNER JOIN "item_parents" "item_parents_parents_join"
ON "item_parents_parents_join"."parent_id" = "parents"."id"
INNER JOIN "items" "item2s_parents" -- OK. join has an alias
ON "item2s_parents"."id" = "item_parents_parents_join"."item_id"
AND "item2s_parents"."item_type" = 'Item::Item2'
WHERE "items"."name" = 'i1'
AND "items"."name" = 'i2' -- Wrong! Must be "item2s_parents"."name" = 'i2'
结果,我返回了零行,因为不可能同时有一个名称等于
'i1'
和 'i2'
的项目。
编写自定义
joins_item
方法似乎是个好主意,它可以挖掘 query
并检查它之前是否有其他 joins
调用它(AR 将此类信息存储在 query.values[:joins]
和 query.values[:left_outer_joins]
) 如果有,那么它将返回另一个具有正确别名的 Arel::Table
实例。如果之前没有加入任何东西,那么我不需要别名并返回默认Arel::Table
.
但是后来我发现AR在构建SQL的那一刻就解析了别名。因此,即使我在加入时可以猜到正确的别名(或没有别名),它最终也会改变。这实际上是当你先做
left_outer_joins
然后做joins
时发生的事情。在生成的 SQL 中,AR 总是将 INNER JOIN
s 放在 LEFT OUTER JOIN
s 之前。
当我使用 Arel 或任何其他或多或少可维护的解决方法/修复/猴子补丁时,有没有办法强制 AR 对所有内容进行别名处理?
我没有答案,我也没有足够的声誉来发表评论,但我目前正在努力解决这个确切的问题并且尚未找到有用或可维护的解决方案。经过一番挖掘,我确实确认添加引用关联的
joins
left_outer_joins
以关联名称作为别名,而不是无别名或基于表名计算的别名。我遇到的问题是我不想打一个不必要的
where
电话只是为了确保我知道从协会建立的 JOIN
的别名。连接的范围已经在模型上定义,明确写出一个完整的where
语句作为一个字符串,这样我就可以确保它有一个一致的别名似乎不是正确的解决方案,但我不确定是什么其他事情要做。