Ruby on Rails - Has Many, Through: 查找多个条件

3
我是一名有用的助手,我可以翻译文本。

我意识到仅凭文字来解释我的问题相当困难,因此我将使用示例来描述我所要做的事情。

例如:

#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"),然后将获得所有属于小说类型的书籍。但是如何查找所有既属于“青少年”又属于“小说”类型的书籍?或者如果我想查询具有 3 种类型(如“青少年”,“小说”和“浪漫”)的图书,该怎么办?
我可以执行 g = Genre.where(name: ["Young Adult", "Fiction", "Romance"]),但随后我无法执行 g.books 并获取与这三种类型相关联的所有书籍。
实际上,我对 Active Record 不太熟悉,所以我甚至不确定是否有更好的方法直接通过 Books 查询,而不是找到 Genre 然后找到所有与其相关的书籍。
但是我无法理解的是,如何获取具有多个(特定)类型的所有书籍?
更新:
当前提供的答案 Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]) 是有效的,但问题在于它返回了所有具有“青少年”OR“小说”OR“浪漫”类型的书籍。
要获得具有所有 3 种类型而不仅仅是其中 1 种或 2 种类型的书籍,请传递什么查询?

我建议在书和类型之间使用HABTM关联。 - Sajan
@Sajan,这仍然没有解释/回答我的问题。 我如何实现我所需的查询结果? - angkiki
也许(未经测试的代码):ActiveRecord :: Base.connection.execute(“select * from books inner join genres on books.genre_id = genres.id where genres.name like'fiction'intersect select * from books inner join genres on books.genre_id = genres.id where genres.name like'romance'”)。您可能需要调整内部连接条件。 - dimid
然后,您可以使用“值”来获取图书数组。 - dimid
传递查询的唯一方式是使用 SQL,而不是使用 Active Record 方法吗? - angkiki
据我所知,ActiveRecord 不支持 intersect,但你可以尝试使用 arel - dimid
3个回答

3

匹配给定的任意类型

以下内容适用于数组和字符串:

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

通常情况下,最好将哈希传递给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 JOINWHERE genres.name = 'Young Adult')
INTERSECT
(SELECT books.* FROM books INNER JOINWHERE genres.name = 'Fiction')
INTERSECT
(SELECT books.* FROM books INNER JOINWHERE genres.name = 'Romance');

这种方法的好处是让数据库来完成结果集的组合。

缺点在于我们使用原始SQL,因此无法与其他ActiveRecord方法链接,例如Books.order(:title).in_genres(["Young Adult", "Fiction"])将忽略我们尝试添加的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 JOINWHERE genres.name = 'Young Adult';
SELECT id FROM books INNER JOINWHERE genres.name = 'Fiction';
SELECT id FROM books INNER JOINWHERE genres.name = 'Romance';
SELECT * FROM books WHERE id IN (1, 3, …);

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

它的运行结果不完全符合我的期望,虽然我能理解原因。它返回了所有至少属于这些类型之一 ["青少年", "小说", "浪漫"] 的书籍。是否可能返回包含这三种类型的所有图书呢? - angkiki
我已经更新了我的答案,包括一些其他选项。 - georgebrock
运行得非常好。而且干净简洁。感谢您提供全面的答案(并付出更新答案的努力)!干杯 :) - angkiki

1

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

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

抱歉,但是不起作用。它会抛出语法错误。在“genres.name”之后不接受“:”。我尝试了其他可能有效的变化,唯一有效的似乎是Book.joins(:genres).where("genres.name LIKE ?", "Fiction")。尝试传递一个数组也会引发错误。 - angkiki
与georgebrock提供的答案相同的“问题”。返回具有“青少年/小说/浪漫”中任何一种类型的书籍,而不仅仅是具有所有3种类型的书籍。有没有办法返回必须包含这3种类型的所有书籍? - angkiki

1
这是SQL的实现方式:

Here is how I would do it in 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
)

你应该学习SQL。ActiveRecord非常棒,因为它有许多“逃生口”,让你可以将原始的SQL与表达式混合使用,这样你就可以同时使用两者。我的三个SQL解决方案都与AR兼容。在第一个解决方案中,我展示了如何定义一个AR scope,你可以将其与其他AR方法结合使用,例如 Book.in_genres(["Fiction"]).where(author: a)author.books.in_genres(["Fiction"])。你可以以同样的方式将另外两种方法封装到一个scope中。 - Paul A Jungwirth
另外,调试 AR 问题的好方法是使用 to_sql 方法打印它正在使用的 SQL。例如:q = Book.in_genres(["Fiction"]).where(author: a).includes(publisher: [:city]); Rails.logger.debug q.to_sql - Paul A Jungwirth
是的,我肯定会学习SQL的基本原理和语法,并实施您的解决方案。谢谢! :) - angkiki
这是我关于使用SQL和ActiveRecord的演讲:https://github.com/pjungwir/rails-and-sql-talk - Paul A Jungwirth
个人而言,当我有一个复杂的查询时,我首先会用SQL编写它,然后再进行AR翻译。一次只处理一层似乎不那么复杂。此外,了解SQL对您的职业生涯很有好处,因为它是后端的通用语言,就像JavaScript是前端的通用语言一样。无论您使用Rails、Django、JEE、.Net还是其他任何东西,您都可能会使用一些SQL。 - Paul A Jungwirth
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接