Ruby on Rails – 有很多,通过:找到多个条件

我意识到很难用单词来解释我的问题,所以我将用一个例子来描述我想要做的事情。

例如:

#model Book has_many: book_genres has_many: genres, through: :book_genres #model Genre has_many: book_genres has_many: books, through: :book_genres 

因此,只查找属于一种类型的书籍会相对简单,例如:

 #method in books model def self.find_books(genre) @g = Genre.where('name LIKE ?' , "#{genre}").take @b = @g.books #get all the books that are of that genre end 

所以在rails控制台中我可以做Book.find_books("Fiction") ,然后我会得到所有fiction类型的书。

但是我怎样才能找到所有既是“年轻人”又是“小说”的书呢? 或者,如果我想查询有3种类型的书籍,例如“青少年”,“小说”和“浪漫”,该怎么办?

我可以做g = Genre.where(name: ["Young Adult", "Fiction", "Romance"])但是之后我不能做g.books并得到所有与这三种类型相关的书籍。

我实际上对活动记录非常糟糕所以我甚至不确定是否有更好的方式直接查询Books而不是找到Genre然后查找与之相关的所有书籍。

但是我无法理解的是我如何获得所有具有多种(特定)类型的书籍?

更新:

所以目前的答案提供了Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"])有效,但问题是它返回所有具有Young AdultFictionRomance流派。

我通过什么查询,以便书籍返回有所有3种类型而不仅仅是1或2中的3种?

匹配任何给定的类型

以下内容适用于Array和String:

 Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]) Book.joins(:genres).where("genres.name" => "Young Adult") 

通常,最好将Hash传递到where ,而不是尝试自己编写SQL代码段。

有关更多详细信息,请参阅Rails指南:

使用一个查询匹配所有给定的类型

可以构建单个查询,然后传递给.find_by_query

 def self.in_genres(genres) sql = genres. map { |name| Book.joins(:genres).where("genres.name" => name) }. map { |relation| "(#{relation.to_sql})" }. join(" INTERSECT ") find_by_sql(sql) end 

这意味着调用Book.in_genres(["Young Adult", "Fiction", "Romance"])将运行如下所示的查询:

 (SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Young Adult') INTERSECT (SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Fiction') INTERSECT (SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Romance'); 

它具有让数据库完成组合结果集的繁重工作的好处。

缺点是我们使用原始SQL,因此我们不能将其与其他ActiveRecord方法链接,例如Books.order(:title).in_genres(["Young Adult", "Fiction"])将忽略ORDER BY我们试图添加ORDER BY子句。

我们还将SQL查询操作为字符串。 我们可以使用Arel来避免这种情况,但是Rails和Arel处理绑定查询值的方式使得这非常复杂。

匹配所有给定类型的多个查询

也可以使用多个查询:

 def self.in_genres(genres) ids = genres. map { |name| Book.joins(:genres).where("genres.name" => name) }. map { |relation| relation.pluck(:id).to_set }. inject(:intersection).to_a where(id: ids) end 

这意味着调用Book.in_genres(["Young Adult", "Fiction", "Romance"])将运行四个查询,如下所示:

 SELECT id FROM books INNER JOIN … WHERE genres.name = 'Young Adult'; SELECT id FROM books INNER JOIN … WHERE genres.name = 'Fiction'; SELECT id FROM books INNER JOIN … WHERE genres.name = 'Romance'; SELECT * FROM books WHERE id IN (1, 3, …); 

这里的缺点是对于N种类型,我们正在进行N + 1次查询。 好处是它可以与其他ActiveRecord方法结合使用; Books.order(:title).in_genres(["Young Adult", "Fiction"])将进行我们的流派过滤,并按标题排序。

以下是我将如何在SQL中执行此操作:

 SELECT * FROM books WHERE id IN ( SELECT bg.book_id FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE g.name LIKE 'Young Adult' INTERSECT SELECT bg.book_id FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE g.name LIKE 'Fiction' INTERSECT ... ) 

内部查询将仅包含属于您询问的所有类型的书籍。

以下是我在ActiveRecord中的操作方法:

 # book.rb def self.in_genres(genre_names) subquery = genre_names.map{|n| <<-EOQ SELECT bg.book_id FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE g.name LIKE ? EOQ }.join("\nINTERSECT\n") where(<<-EOQ, *genre_names) id IN ( #{subquery} ) EOQ end 

请注意我正在使用? 避免SQL注入漏洞 ,这是您在问题中提出的代码中的一个问题。

另一种方法是使用具有相关子查询的多个EXISTS条件:

 SELECT * FROM books WHERE EXISTS (SELECT 1 FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE g.name LIKE 'Young Adult' AND bg.book_id = books.id) AND EXISTS (SELECT 1 FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE g.name LIKE 'Fiction' AND bg.book_id = books.id) AND ... 

与第一种方法类似,您可以在ActiveRecord中构造此查询。 我不确定哪个更快,所以如果你愿意,你可以试试两个。

这是另一种做SQL的方法---可能最快:

 SELECT * FROM books WHERE id IN ( SELECT bg.book_id FROM book_genres bg INNER JOIN genres g ON g.id = bg.genre_id WHERE (g.name LIKE 'Young Adult' OR g.name LIKE 'Fiction' OR ...) GROUP BY bg.book_id HAVING COUNT(DISTINCT bg.genre_id) >= 2 -- or 3, or whatever ) 

我没试过,但我认为它会起作用

 Book.joins(:genres).where("genres.name IN (?)", ["Young Adult", "Fiction", "Romance"])