Короткий ответ
Я не понимаю, зачем вам вообще что-то группировать. Я бы решил эту проблему с использованием подзапросов.
certificate_1_user_ids = CertificatesUser.select(:user_id).where(certificate_id: 1)
certificate_2_user_ids = CertificatesUser.select(:user_id).where(certificate_id: 2)
# List out users who have a certificate with the id of 1.
users_with_certificate_1 = User.where(id: certificate_1_user_ids)
# List out only users who have both certificates with the
# id of 1 and certificate with the id of 2.
users_with_certificate_1_and_2 = User.where(id: certificate_1_user_ids)
.where(id: certificate_2_user_ids)
Последний вопрос может стоить отдельного вопроса. Однако я упомяну, что вы можете сделать следующее.
# with the following associations present in app/models/user.rb
has_many :certificates_users
has_many :certificates, through: :certificates_users
# you can eager load associations to avoid the 1+N problem
users.includes(:certificates).each do |user|
# do stuff with user
user.certificates.each do |certificate|
# do stuff with certificate
end
end
Динамические идентификаторы сертификатов
В комментариях вы спрашивали, как заставить этот ответ работать для динамических идентификаторов сертификатов. Для приведенного выше ответа это можно сделать следующим образом.
certificate_ids = [1, 2]
users = certificate_ids.reduce(User) do |scope, certificate_id|
user_ids = CertificatesUser.select(:user_id).where(certificate_id: certificate_id)
scope.where(id: user_ids)
end
Результирующий запрос должен выглядеть примерно так (для MySQL).
SELECT `users`.*
FROM `users`
WHERE
`users`.`id` IN (
SELECT `certificates_users`.`user_id`
FROM `certificates_users`
WHERE `certificates_users`.`certificate_id` = 1
)
AND `users`.`id` IN (
SELECT `certificates_users`.`user_id`
FROM `certificates_users`
WHERE `certificates_users`.`certificate_id` = 2
)
Оптимизация запроса
Хотя приведенный выше пример показывает нам, как сделать все это динамичным. Выполнение этого для больших количеств идентификаторов сертификатов не дает наиболее эффективного запроса.
Еще один способ решения этой проблемы - группировка, и ее немного сложнее выполнить. Это включает в себя поиск в таблице certificate_users записей, соответствующих одному из заданных идентификаторов. Затем посчитайте количество записей для каждого пользователя. Если количество записей равно количеству выданных идентификаторов сертификатов, это означает, что у пользователя есть все эти сертификаты.
Предпосылки
Однако этот метод имеет предпосылки.
Комбинация user_id и certificate_id должна быть уникальной в таблице certificate_users . Вы можете убедиться, что это это случай, поместив уникальное ограничение на эти столбцы.
add_index :certificates_users, %i[user_id certificate_id], unique: true
Другая дополнительная рекомендация заключается в том, что столбцы user_id и certificate_id в таблице certificate_users не могут быть NULL
. Вы можете убедиться в этом, создав столбцы с помощью
t.references :user, foreign_key: true, null: false
или путем изменения столбца с помощью
change_column_null :certificates_users, :user_id, false
Конечно, приведенный выше пример необходимо повторить и для столбца certificate_id .
Оптимизация
Учитывая все необходимые предпосылки, давайте взглянем на это решение.
certificates_users = CertificatesUser.arel_table
certificate_ids = [1, 2]
users = User.joins(:certificates_users)
.where(certificate_users: { certificate_id: certificate_ids })
.group(:id)
.having(certificates_users[:id].count.eq(certificate_ids.size))
# Nested sections in a where always require the table name, not the
# association name. Here is an example that makes it more clear.
#
# Post.joins(:user).where(users: { is_admin: true })
# ^ ^- table name
# +-- association name
Приведенный выше пример должен выдать следующий запрос (для MySQL).
SELECT `users`.*
FROM `users`
INNER JOIN `certificates_users`
ON `users`.`id` = `certificates_users`.`user_id`
WHERE `certificates_users`.`certificate_id` IN (1, 2)
GROUP BY `users`.`id`
HAVING COUNT(`certificates_users`.`id`) = 2
При увеличении массива certificate_ids
запрос остается прежним, за исключением следующих двух частей.
IN (1, 2)
становится IN (1, 2, 4, 7, 8)
COUNT(...) = 2
становится COUNT(...) = 5
Ссылки
Большая часть используемого кода говорит сама за себя. Если вы еще не знакомы с reduce
, я рекомендую взглянуть на документацию . Так как этот метод присутствует во многих других языках программирования. Это отличный инструмент в вашем арсенале.
API-интерфейс arel на самом деле не предназначен для использования вне внутренних структур Rails. По этой причине его документация довольно плохая. Однако, вы можете найти большинство используемых здесь методов .