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 жанрам.
Я на самом деле довольно плохо с active record, так что я даже не уверен, есть ли лучший способ запросить через Books напрямую вместо того, чтобы найти Genre, а затем найти все книги, которые связаны с ним.
Но то, что я не могу обернуть своим как мне получить все книги, которые имеют несколько (конкретных)жанров?
Обновление:
Таким образом, текущие ответы, предоставленные
Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]), работают, но проблема в том, что он возвращает все книги, которые имеют жанр Young Adult или Fiction или Romance. Какой запрос я передаю, чтобы книга возвращала все 3 жанра, а не только 1 или 2 из 3?
3 ответов:
Соответствие любому из заданных жанров
Следующее должно работать как для массива, так и для строки:
Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]) Book.joins(:genres).where("genres.name" => "Young Adult")В общем, лучше передать Хэш в
where, чем пытаться написать фрагмент SQL самостоятельно.Смотрите направляющие рельсов для получения более подробной информации:
- http://guides.rubyonrails.org/active_record_querying.html#hash-conditions
- http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables
Сопоставление всех заданных жанров одним запросом
Можно построить один запрос и затем передать его в
.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, которое мы пытались добавить.Мы также манипулируем 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"])
Comments