我意识到很难用单词来解释我的问题,所以我将用一个例子来描述我想要做的事情。
例如:
#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 Adult
或Fiction
或Romance
的图书。
我传递了什么查询,以便书籍返回所有3种类型而不仅仅是3种中的1种或2种?
答案 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"])