Выберите записи, все записи которых существуют в другой соединительной таблице - PullRequest
0 голосов
/ 10 октября 2018

В следующем примере книжного клуба с ассоциациями:

class User
  has_and_belongs_to_many :clubs
  has_and_belongs_to_many :books
end

class Club
  has_and_belongs_to_many :users
  has_and_belongs_to_many :books
end

class Book
  has_and_belongs_to_many :users
  has_and_belongs_to_many :clubs
end

с учетом конкретной записи клуба:

club = Club.find(params[:id])

как мне найти все users в клубе, которые имеютвсе книги в массиве книг?

club.users.where_has_all_books(books)

Ответы [ 3 ]

0 голосов
/ 10 октября 2018

Это похоже на ситуацию, когда вы делаете два запроса: один для получения всех необходимых идентификаторов, другой - для выбора WHERE IN.

0 голосов
/ 10 октября 2018

В PostgreSQL это можно сделать одним запросом.(Может быть, и в MySQL, я просто не уверен.)

Итак, сначала несколько основных предположений.3 таблицы: clubs, users и books, каждая таблица имеет id в качестве первичного ключа.3 таблицы соединения, books_clubs, books_users, clubs_users, каждая таблица содержит пары идентификаторов (для books_clubs это будет [book_id, club_id]), и эти пары уникальны в этой таблице.Вполне разумные условия ИМО.

Построение запроса:

  1. Сначала давайте получим идентификаторы книг из данного клуба:

    SELECT book_id
    FROM books_clubs
    WHERE club_id = 1
    ORDER BY book_id
    
  2. Затем получите пользователей из данного клуба и сгруппируйте их по user.id:

    SELECT CU.user_id
    FROM clubs_users CU
      JOIN users U ON U.id = CU.user_id
      JOIN books_users BU ON BU.user_id = CU.user_id
    WHERE CU.club_id = 1
    GROUP BY CU.user_id
    
  3. Объедините эти два запроса, добавив having ко второму запросу:

    HAVING array_agg(BU.book_id ORDER BY BU.book_id) @> ARRAY(##1##)
    

    , где ##1## - первый запрос.

    Что здесь происходит: функция array_agg из левой части создает отсортированный список (типа array) book_ids.Это книги пользователя.ARRAY(##1##) из правой части возвращает отсортированный список книг клуба.И оператор @> проверяет, содержит ли 1-й массив все элементы 2-го (т.е. если у пользователя есть все книги клуба).

  4. Поскольку 1-й запрос должен бытьвыполняется только один раз, его можно переместить в предложение WITH.

Ваш полный запрос:

WITH club_book_ids AS (
  SELECT book_id
  FROM books_clubs
  WHERE club_id = :club_id
  ORDER BY book_id
)
SELECT CU.user_id
FROM clubs_users CU
  JOIN users U ON U.id = CU.user_id
  JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = :club_id
GROUP BY CU.user_id
HAVING array_agg(BU.book_id ORDER BY BU.book_id) @> ARRAY(SELECT * FROM club_book_ids);

Его можно проверить в этой песочнице: https://www.db -fiddle.com / f / cdPtRfT2uSGp4DSDywST92 / 5

Оберните его в find_by_sql и все.

Некоторые примечания:

  • заказ по book_id не требуется;Оператор @> работает и с неупорядоченными массивами.У меня просто есть подозрение, что сравнение упорядоченного массива происходит быстрее.
  • JOIN users U ON U.id = CU.user_id во втором запросе необходимо только для извлечения пользовательских свойств;в случае извлечения только идентификатора пользователя его можно удалить
0 голосов
/ 10 октября 2018

Похоже, что работает путем группировки и подсчета.

club.users.joins(:books).where(books: { id: club.books.pluck(:id) }).group('users.id').having('count(*) = ?', club.books.count)

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

...