【问题标题】:How to find only Grandparent records where all grandchildren match some criteria in Rails?如何在 Rails 中仅查找所有孙子都符合某些条件的祖父母记录?
【发布时间】:2018-12-13 10:27:32
【问题描述】:

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

项目有_许多任务

任务 has_many TodoItems

我想执行一个搜索,它只返回所有任务都将其所有 TodoItem 标记为已完成的项目

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

然后这样做

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

但这将返回一些待办事项已完成的项目,而我只想要所有待办事项都已标记为已完成的项目

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    首先,你需要同时加入tasks和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)
    

    【讨论】:

      【解决方案2】:

      您可以使用左连接轻松做到这一点。

      我将向您展示如何获得最活跃的类似记录的代码,从原始 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))
      

      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.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))
      

      【讨论】:

      • 这很好,感谢您提供详细解释的答案!
      【解决方案3】:

      查找任务列表,至少有 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 })
      

      【讨论】:

      • 我建议跳过ids 部分或至少使用select(:id)ids 使用 pluck,它返回一个 Array,而其他 2 个选项只会创建一个子查询
      猜你喜欢
      • 2021-05-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多