Как найти только записи дедов, где все внуки соответствуют некоторым критериям в Rails? - PullRequest
0 голосов
/ 13 декабря 2018

У меня есть следующие модели с этими отношениями

Project has_many Задачи

Task has_many TodoItems

Я хочу выполнить поиск, который возвращает только проекты, в которых есть все его задачивсе их TodoItems, помеченные как выполненные

Я пытался добавить в Project has_many: todo_items через:: tasks

и затем сделать это

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

но это вернет проекты, в которых выполнены некоторые элементы задачи, тогда как я хочу только проекты, в которых все элементы задачи были отмечены как выполненные

Ответы [ 3 ]

0 голосов
/ 13 декабря 2018

Прежде всего, вам нужно объединить обе задачи и 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)
0 голосов
/ 13 декабря 2018

Вы можете легко сделать это с помощью левых соединений.

Я покажу вам, как получить наиболее активный код, похожий на запись, начиная с необработанного 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 справа.

Забывая о конкретных столбцах, вы можете представить, что этот запрос будетreturn:

(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.Когда у задачи есть какое-то неполное 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, вам повезло, потому что левые объединения поддерживаются напрямую, если вы определитеассоциация для 'pending_todo_items' в Task:

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 column (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))
0 голосов
/ 13 декабря 2018

Найти список задач, содержащий по крайней мере 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 })
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...