您可以使用左连接轻松做到这一点。
我将向您展示如何获得最活跃的类似记录的代码,从原始 SQL 开始,即时修复它并越来越多地提高抽象级别。这可能会在将来帮助您完成其他类似的任务。
让我们从可以工作的最简单的 SQL 开始(实际上它没有)。
SELECT `projects`.`*`,
`tasks`.`*`,
`todo_items`.`*`
FROM `projects`
LEFT OUTER JOIN `tasks`
ON `tasks`.`project_id` = `projects`.`id
LEFT OUTER JOIN `todo_items`
ON `todo_items`.`task_id` = `tasks`.`id
AND NOT `todo_items`.`done`
WHERE `todo_items`.`id` IS NULL
;
这将在指定条件下连接三个表。如果您需要一种简单的方法来理解连接,您可以想象结果是三个表中的所有行的组合,前提是满足ON 部分中的条件。
LEFT 连接的基本事实是,每当左侧的某些行不匹配时,数据库会确保给出一个结果,右侧为 NULL。
忘记具体的列,你可以想象这个查询会返回:
(p1, NULL, NULL) -- project with no tasks
(p2, t2_1, NULL) -- project with three tasks, first is complete
(p2, t2_2, todo2_2_1) -- this task has one pending todo
(p2, t2_3, todo2_3_1) -- this task has two pending todos
(p2, t2_3, todo2_3_2)
(p3, t3_1, NULL) -- project with two complete tasks
(p3, t3_2, NULL)
...
这就是在ON 子句中使用AND NOT todo_items.done 的原因。每当一个任务只完成了 TODO 时,它就会以NULL todo 出现。当一个任务有一些不完整的 TODO 时,它会与它的数据一起出现。另一方面,如果任务t2_2 有任何已完成的 todo_item,则不会返回。
现在原始查询失败了,因为有一个完整任务的p2 将被返回,尽管有另一个未完成的任务。
但是有一个很好的 SQL 函数可以用来实际检查非空值:
SELECT `projects`.`*`,
COUNT(`todo_items`.`id`) AS `pending_todo_count`
FROM `projects`
LEFT OUTER JOIN `tasks`
ON `tasks`.`project_id` = `projects`.`id
LEFT OUTER JOIN `todo_items`
ON `todo_items`.`task_id` = `tasks`.`id
AND NOT `todo_items`.`done`
GROUP BY `projects`.`*`
;
使用上面的数据,这将返回类似
(p1, 0) -- project with no tasks
(p2, 3) -- project with three tasks, first is complete
(p3, 0) -- project with two complete tasks
...
现在我们跳过那些 TODO 不完整的那些
SELECT `projects`.`*`
FROM `projects`
LEFT OUTER JOIN `tasks`
ON `tasks`.`project_id` = `projects`.`id
LEFT OUTER JOIN `todo_items`
ON `todo_items`.`task_id` = `tasks`.`id
AND NOT `todo_items`.`done`
GROUP BY `projects`.`*`
HAVING COUNT(`todo_items`.`id`) = 0
;
您可以看到我们在 SELECT 中去掉了列,并在分组上添加了一个条件。这个查询终于对了,会返回
(p1)
(p3)
所以现在我们需要它的 ruby 代码,如果你在 Rails 5 中你很幸运,因为直接支持左连接,只要你在 Task 中为 'pending_todo_items' 定义一个关联:
class Task
has_many :pending_todo_items, -> { where(done: false) }, class_name: 'TodoItem'
end
Project.
left_joins(tasks: :pending_todo_items).
group(Project.arel_table[:id]).
having(TodoItem.arel_table[:id].count.eq(0))
group 和 having 的参数来自 Arel,它只是 ActiveRecord 之下的一层抽象,以避免使用硬编码字符串,如
Project.
left_joins(tasks: :pending_todo_items).
group('projects.id').
having('COUNT(todo_items.id) = 0')
一旦模型 表名映射发生变化就会中断。
如果您使用的是 Rails 4 或更低版本,则必须手动或通过 Arel 编写左连接:
Project.
joins('LEFT OUTER JOIN .........').
group('projects.id').
having('COUNT(todo_items.id) = 0')
更新
您还可以使用NULLIF 将done 列(1/0)转换为pending 列(NULL/notnull),这样您就可以避免特殊的作用域关联(尽管我肯定看到它们可以有用):
SELECT `projects`.`*`
FROM `projects`
LEFT OUTER JOIN `tasks`
ON `tasks`.`project_id` = `projects`.`id
LEFT OUTER JOIN `todo_items`
ON `todo_items`.`task_id` = `tasks`.`id
GROUP BY `projects`.`*`
HAVING COUNT(NULLIF(`todo_items`.`done`, 1)) = 0
;
NULLIF(todo_items.done, 1) 表达式为已完成的项目返回 NULL,为待处理的项目返回 0(原始值)。
在 ActiveRecord 中,您必须修补谓词以方便使用:
module Arel::Predications
def null_if(other)
Arel::Nodes::NamedFunction.new('NULLIF', [self, other])
end
end
Project.
left_joins(tasks: :todo_items).
group(Project.arel_table[:id]).
having(TodoItem.arel_table[:done].null_if(true).count.eq(0))