如何仅查找所有孙子代在Rails中符合某些条件的祖父母记录?

时间:2018-12-13 10:27:33

标签: ruby-on-rails activerecord

我有以下具有这些关系的模型

项目has_many任务

任务has_many TodoItems

我想执行一个搜索,该搜索仅返回所有Tasks都将其TodoItems标记为已完成的Projects

我尝试通过:tasks添加到项目has_many:todo_items

然后执行

projects = Projects.joins(:todo_items).where(todo_items: {done: true})

但是这将返回Projects,其中某些待办事项已完成,而我只希望项目中所有待办事项都标记为已完成

3 个答案:

答案 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))

grouphaving的参数来自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')

更新

您还可以使用NULLIFdone列(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))