选择记录的所有记录都存在于另一个联接表中

时间:2018-10-09 22:18:59

标签: ruby-on-rails rails-activerecord

在以下具有关联的读书俱乐部示例中:

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 :(得分:1)

在PostgreSQL中,可以通过单个查询完成。 (也许也可以在MySQL中使用,我不确定。)

因此,首先要进行一些基本假设。 3个表:clubsusersbooks,每个表都以id作为主键。 3个连接表books_clubsbooks_usersclubs_users,每个表包含一对ID(对于books_clubs,它是[book_idclub_id ]),并且这些对在该表中是唯一的。 IMO的条件相当合理。

建立查询:

  1. 首先,让我们获取给定俱乐部的书籍ID:

    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##)返回俱乐部的书籍排序列表。然后操作员@>检查第一个数组是否包含第二个元素的所有元素(即用户是否拥有俱乐部的所有书籍)。

  4. 由于第一次查询只需要执行一次,因此可以将其移到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;如果仅获取用户ID,则可以将其删除

答案 1 :(得分:0)

它似乎可以通过分组和计数来工作。

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

如果有人知道如何在不进行中间查询的情况下运行查询,那将是很好的,我会接受答案。

答案 2 :(得分:0)

看起来您要进行两个查询,一个查询获取所需的所有ID,另一个查询执行WHERE IN。