Рубиновый запрос с двумя различными совместными условиями

Я хочу найти всех пользователей, у которых есть определенные записи LanguagesUsers. В настоящее время я пытаюсь запросить таблицу соединений, LanguagesUsers для всех случаев, когда у пользователя есть 2 соответствующих языка_ид и уровень

Чтобы найти, где есть только одна ассоциация, это работает:

User.joins(:languages_users).where(languages_users: {language_id: 2, level: 5})

Как я могу включить в запрос 2 языка_узлов, где они ОБЫЧНЫ? (Например, я хочу language_id: 2 level: 1 И language_id: 3 level: 5)

Я пробовал это, но он возвращает ошибку:

User.joins(:languages_users).where(languages_users: {language_id: 2, level: 5} AND languages_users: {language_id: 1, level: 3})

Этот возвращает пустой массив, хотя определенно есть пользователи, у которых есть 2 languages_users, которые соответствуют этим критериям:

User.joins(:languages_users).where(languages_users: {language_id: 2, level: 5}).where(languages_users: {language_id: 1, level: 3})

Каков правильный формат для этого? Спасибо!!

UPDATE: LanugagesUser - таблица соединений между Языки и Пользователи и имеет язык_ид, уровень, user_id

gem 'sqlite3', group: :development
group :production do
    gem 'pg'
    gem 'rails_12factor'
end

ОБНОВЛЕНИЕ 2: Поскольку оба ответа предложили или утверждение, я не думаю, что правильно передаю этот факт:

ОБЩИЕ ЗАПИСИ МОГУТ БЫТЬ НАСТОЯЩИМ. Например. Я хочу знать, что у пользователей есть UserUser (language_id: 1, level: 5) AND LanguagesUser (language_id: 2, level: 1)

Ответ 1

Вы пытались:

User.joins(:languages_users).where(languages_users: [{language_id: 2, level: 5}, {language_id: 1, level: 3}])

EDIT: Поскольку вы используете ActiveRecord >= 5, вы можете использовать синтаксис .or. Результат немного подробный, но он должен работать, и я уверен, что вы можете это сделать:

User.joins(:languages_users).where(
  languages_users: {language_id: 2, level: 5}).
  or(
    User.joins(:languages_users).where(
      languages_users: {language_id: 1, level: 3}
  )

ИЗМЕНИТЬ 2: Я думаю, что понимаю, что вы сейчас делаете, более сложный, чем я думал, но думаю, вам понадобится вложенный запрос. Если вы сначала запросите таблицу languages_users и сгруппируете ее с помощью user_id, вы можете применить предложение HAVING, чтобы найти тех пользователей, которые имеют оба языка. Затем вы выбираете user_ids из этого запроса и используете их в простом запросе в таблице пользователей. Поскольку вы используете postgres, может работать следующее.

languages = [{ language_id: 2, level: 5 }, { language_id: 1, level: 3 }]
User.where(users_with_all_languages(languages)

def users_with_all_languages(languages)
  LanguagesUser.
    select(:user_id).
    group(:user_id).
    having('array_agg(language_id) @> ARRAY[?] AND array_agg(level) @> ARRAY[?]',
      languages.map { |x| x[:language_id] },
      languages.map { |x| x[:level] }
    )
end

Или, проще говоря, если у вас есть гибкость с форматом языковых данных:

language_ids = [2, 1]
language_levels = [5, 3]
User.where(users_with_all_languages(language_ids, language_levels)

def users_with_all_languages(ids, levels)
  LanguagesUser.
    select(:user_id).
    group(:user_id).
    having('array_agg(language_id) @> ARRAY[?] AND array_agg(level) @> ARRAY[?]', ids, levels)
end

Я сделал что-то подобное, но не с двумя условиями в таблице соединений, поэтому AND использование двух условий array_agg - это единственный шаблон, который я думаю...

Ответ 2

2 решения (примечание: рельсы не поддерживают запросы OR)

1. Простой SELECT WHERE

Используйте длинную строку кода следующим образом:

User.
  joins(:languages_users).
  where('(`languages_users`.`language_id` = ? AND `languages_users`.`level` = ?) ' + 
        'OR (`languages_users`.`language_id` = ? AND `languages_users`.`level` = ?)', 
        2, 1, 5, 3)

2. 2 лайнера WHERE SELECT

Сначала вы готовите запрос для выбора списка languages_users.id s

languages_users_ids = LanguagesUser.
  select(:id).
  where('(language_id = ? AND level = ?) OR (language_id = ? AND level = ?)', 
        2, 1, 5, 3)

Затем вы используете его в запросе в таблице users:

User.joins(:languages_users).where(languages_users: {id: languages_users_ids})

Это приведет к одному запросу "WHERE SELECT" на ваш сервер БД:

SELECT * FROM users JOIN languages_users 
  WHERE languages_users.id IN (
    SELECT id FROM languages_users  
    WHERE (language_id = 2 AND level = 1) OR (language_id = 5 AND level = 3)
    )

Ответ 3

попробуйте это

User.joins('LEFT OUTER JOIN languages_users lu1 ON lu1.user_id = users.id AND lu1.language_id = 2 AND lu1.level = 5')
    .joins('LEFT OUTER JOIN languages_users lu2 ON lu2.user_id = users.id AND lu2.language_id = 1 AND lu2.level = 3')
    .group('users.id')

У меня нет настроек postgres, поэтому я быстро протестировал это в моей настройке mysql.

При появлении более сложного запроса полезно начинать думать в raw sql (по крайней мере для меня), а затем посмотреть, можете ли вы преобразовать его в AR-запрос.

То, что вы пытаетесь выполнить, выглядит так: raw sql.

SELECT *
FROM users
LEFT OUTER JOIN
  languages_users lu1 ON users.id = lu1.user_id AND lu1.language_id = 2 AND lu1.level = 5
LEFT OUTER JOIN
  languages_users lu2 ON users.id = lu2.user_id AND lu2.language_id = 1 AND lu1.level = 3
GROUP BY
  users.id

Таким образом, вы создадите таблицу следующим образом:

users.id | users.* | lu1.user_id | lu1.language_id | lu1.level | lu2.user_id | lu2.language_id | lu2.level
   1        ...            1             2                5            1            1                3

Два предложения where с одним join не будут работать, потому что вы настраиваете два условия, которые должны выполняться таблицей languages_users в одной строке. Но он всегда будет удовлетворять только один или менее.

Edit

Итак, это идет дальше с точки зрения производительности. Я предполагаю, что у вас есть правильный индекс в таблице languages_users (вам нужно будет индексировать user_id и комбинацию language_id and level.

Но я тестировал аналогичную ситуацию с 90-метровыми строками в эквиваленте ваших language_users и 13m строк в эквиваленте вашей таблицы users. Либо запрос в моем ответе, либо ответ на подзапрос займет очень много времени.

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

Я не могу проверить производительность моего запроса на соединение, потому что он не возвратит меня до истечения времени.

Ответ 4

Отпустите шаг за шагом Первое условие - иметь language_id: 2, level: 5 Таким образом, у нас может быть запрос только для этого условия

users_first_condition = LanguagesUser.where(language_id: 2, level: 5).pluck(:user_id)

Тогда мы можем сделать то же самое для второго условия {language_id: 1, level: 3}

users_second_condition = LanguagesUser.where(language_id: 1, level: 3).pluck(:user_id)

Поскольку у нас есть user_ids для обоих условий, мы можем пересечь оба массива и запустить новый запрос:

User.where(id: users_first_condition & users_second_condition)

Если мы хотим предотвратить запуск нескольких запросов, мы можем использовать подзапросы, поэтому вместо получения только одного идентификатора из каждого условия мы можем напрямую передать отношение к запросу пользователей:

users_first_condtion = Languagesuser.select(:user_id).where(language_id: 2, level: 5)
users_second_condition = Languageuser.select(:user_id).where(language_id: 1, level: 3)
User.where(id: users_first_condition).where(id: users_second_condition)

Ответ 5

Попробуйте следующее:

User.
  joins("LEFT OUTER JOIN languages_users AS l1 ON 
          l1.user_id = users.id AND l1.language_id = 1 AND l1.level = 3").
  joins("LEFT OUTER JOIN languages_users AS l2 ON 
          l2.user_id = users.id AND l2.language_id = 2 AND l2.level = 5").
  where("l1.id IS NOT NULL AND l2.id IS NOT NULL")

Ответ 6

Вы пробовали:

User.joins(:languages_users).where(languages_users: {language_id: 2, level: 5}).where(languages_users: {language_id: 1, level: 3})