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?

453   3  

3 ответов:

Соответствие любому из заданных жанров

Следующее должно работать как для массива, так и для строки:

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

В общем, лучше передать Хэш в where, чем пытаться написать фрагмент SQL самостоятельно.

Смотрите направляющие рельсов для получения более подробной информации:

Сопоставление всех заданных жанров одним запросом

Можно построить один запрос и затем передать его в .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

    Ничего не найдено.