我有以下具有这些关系的模型
项目has_many任务
任务has_many TodoItems
我想执行一个搜索,该搜索仅返回所有Tasks都将其TodoItems标记为已完成的Projects
我尝试通过:tasks添加到项目has_many:todo_items
然后执行
projects = Projects.joins(:todo_items).where(todo_items: {done: true})
但是这将返回Projects,其中某些待办事项已完成,而我只希望项目中所有待办事项都标记为已完成
答案 0 :(得分:1)
首先,您需要同时加入任务和todo_items:
projects = Projects.joins(tasks: :todo_items)
然后让我们讨论一下条件: 我不知道,是否可以使用Activerecord语法,所以我会想到SQL。
如果是一次性操作,我将在ruby中进行如下迭代:
# not production code, very expensive
projects = Projects.joins(tasks: todo_items).all.select { |project| project.tasks.any? { |task| task.todo_items.all?(&:done) } }
如果您需要经常调用它,我将创建缓存:
rails g migration AddAllDoneToTasks all_done:boolean{null: false}
class Task
before_save :set_all_done
def set_all_done
self.all_done = todo_items.all?(&:done)
end
end
class TodoItem
belongs_to :task, touch: true
end
然后搜索非常简单:
Projects.joins(:tasks).where.not(all_done: false)
答案 1 :(得分:0)
查找任务列表,其中至少有1个尚未完成的todo_item,
tasks_not_done = Task.joins(:todo_items).where(todo_items: { done: false }).ids
获取未在tasks_not_done
中列出任务的项目
projects = Project.joins(:tasks).where.not(tasks: { id: tasks_not_done })
答案 2 :(得分:0)
您可以使用左联接轻松地做到这一点。
我将向您展示如何从原始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)
...
这是在AND NOT todo_items.done
子句中使用ON
的原因。每当任务仅完成TODO时,它将与NULL
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
...
现在,我们跳过那些未完成的待办事项
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)
因此,现在我们需要红宝石代码,如果您在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 .
完成, 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))