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

时间:2017-02-25 12:14:32

标签: 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并获得与这3种类型相关的所有书籍。

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

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

更新:

因此,Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"])提供的当前答案有效,但问题是它会返回所有类型为Young AdultFictionRomance的图书。

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

3 个答案:

答案 0 :(得分:2)

匹配任何给定类型

以下内容适用于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子句。 / p>

我们还将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"])将执行我们的流派过滤,并按标题排序。

答案 1 :(得分:1)

以下是我将如何在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 injection vulnerabilities,这是您在问题中提出的代码中的一个问题。

另一种方法是使用多个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
)

答案 2 :(得分:0)

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

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