Можно ли запросить запись с несколькими связанными записями, которые соответствуют определенным критериям?

У меня есть две таблицы, Show, Character. Каждое шоу имеет несколько символов.

class Show < ActiveRecord::Base
  has_many :characters

class Character < ActiveRecord::Base
  belongs_to :show

Что я хочу сделать, так это вернуть результаты Show, связанные с несколькими символами, которые соответствуют определенным критериям.

Например, я хочу иметь возможность возвращать список шоу, которые имеют как символы, так и Бэтмен и Робин. Не Бэтмен ИЛИ Робин, Бэтмен и Робин.

Таким образом, запрос должен выглядеть примерно как

Show.includes(:characters).where(characters: {'name = ? AND name = ?', "Batman", "Robin"})

Но это возвращает ошибку. Есть ли синтаксис для этого, который будет работать?

UPDATE

Запрос

Show.includes(:characters).where('characters.name = ? AND characters.name = ?', "Batman", "Robin")

возвращает значение 0, даже если есть определенные шоу, связанные как с Batman, так и с Robin.

Ответы

Ответ 1

С помощью простого SQL одно решение:

select s. * 
from shows as s 
join characters as c1 on (s.id=c1.show_id) 
join characters as c2 on (s.id=c2.show_id) 
where c1.name='Batman' 
and   c2.name='Robin';

Используя Arel, я бы перевел как:

Show.joins('join characters as c1 on shows.id=c1.show_id').joins('join
characters as c2 on shows.id=c2.show_id').where('c1.name = "Batman"').where(
'c2.name="Robin"')

Ответ 2

Итак, вам нужно немного приукрашивать SQL; особенно если вы хотите, чтобы он был исполнен и обрабатывал различные типы матчи.

select count(distinct(characters.name)) as matching_characters_count, shows.* from shows
  inner join characters on characters.show_id = shows.id and (characters.name = 'Batman' or characters.name = 'Robin')
group by shows.id
having matching_characters_count = 2

Перевести на ActiveRecord

Show.select('count(distinct(characters.name)) as matching_characters_count, shows.*').
   joins('inner join characters on characters.show_id = shows.id and (characters.name = "Batman" or characters.name = "Robin")').
   group('shows.id').
   having('matching_characters_count = 2')

Тогда вы, вероятно, сделаете проход с интерполяцией, а затем AREL его; поэтому вы не будете строить строковые запросы.

Ответ 3

Простое решение SQL с использованием агрегатных функций. Операторы SQL возвращают значения идентификатора записей, которые вы ищете.

SELECT c.show_id
  FROM characters c
 WHERE c.name IN ('Batman','Robin')
 GROUP BY c.show_id
HAVING COUNT(DISTINCT c.name) = 2

Вы можете поместить этот оператор в вызов select_values(), а затем захватить shows со значениями возвращаемого массива.

Ответ 4

Я думаю, что это должно работать:

super_heroes = ['Batman', 'Robin']

Show.where(
  id: Character.select(:show_id).where(name: super_heroes).group(:show_id)
       .having("count(distinct characters.name) = #{super_heroes.size}")
)

Я просто пишу запрос @sschmeck в Rails-способе как подзапрос и добавляю super_heroes var, чтобы показать, как его можно масштабировать.

Ответ 5

Сократить количество записей в запросе как можно скорее, это основная идея, чтобы получить лучший запрос в производительности.

Show.where("id IN (( SELECT show_id FROM characters WHERE name = 'Batman') INTERSECT (SELECT show_id FROM characters WHERE name = 'Robin'))")

В вышеприведенном запросе 1-й подзапрос получает 'show_id' для 'Batman', следующий получает 'show_id' для 'Robin'. На мой взгляд, всего несколько символов соответствуют условию после выполнения "INTERSECT". Btw, вы можете использовать explain в 'rails db' для выбора лучшего решения.

Ответ 6

Я думаю, вы ищете это:

Show.includes(:characters).where(characters: {name: ["Batman", "Robin"]})

или

Show.includes(:characters).where('characters.name IN ?', ["Batman", "Robin"]).references(:characters)

references требуется, потому что мы используем строку в методе where.

Ответ 7

Итак, прежде всего, почему ваш подход ошибочен? Поэтому в таблице "characters_shows" вы можете найти записи, которые выглядят следующим образом:

show_id    name    character_id    character_name
 1     first show        1         batman
 2     second            1         batman
 1     first show        2         robin
 1     first show        3         superman

Как вы можете видеть, никогда не будет случая, когда имя символа - битман и robin в той же строке

Другим подходом будет что-то вроде этого

names = ["Batman", "Robin"]

Show.joins(:characters).where(characters: {name: names}).group("shows.id").having("COUNT(*) = #{names.length}")