Почему group или group_by не работают с этой версией таблицы rails? - PullRequest
0 голосов
/ 15 января 2019

У меня есть эта миграция рельсов со следующим кодом

class CreateCertificatesUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :certificates_users do |t|
      t.references :user, foreign_key: true
      t.references :certificate, foreign_key: true
    end
  end
end

В модели сертификата и модели пользователя у меня есть has_many :certificates_users

Это позволяет мне добавить сертификат в профиль пользователя. В настоящее время у меня есть 2 пользователя, и я добавил сертификат для них обоих.

2.5.3 :010 > CertificatesUser.all
  CertificatesUser Load (0.6ms)  SELECT  "certificates_users".* FROM "certificates_users" LIMIT $1  [["LIMIT", 11]]
 => #<ActiveRecord::Relation [#<CertificatesUser id: 8, user_id: 1, certificate_id: 1, expiry_date: "2020-01-14", certificate_number: "1122", renewal_date: "2019-01-14", ispublic: 1>, #<CertificatesUser id: 9, user_id: 2, certificate_id: 1, expiry_date: "2020-01-16", certificate_number: "123", renewal_date: "2019-01-16", ispublic: 1>, #<CertificatesUser id: 10, user_id: 2, certificate_id: 2, expiry_date: "2019-02-28", certificate_number: "123", renewal_date: "2019-01-16", ispublic: 1>]>  

Вот таблица Certificate_users для ясности.

id | user_id | certificate_id | ...
---+---------+----------------+----
 8 |       1 |              1 |
 9 |       2 |              1 |
10 |       2 |              2 |

Один пользователь имеет 2 сертификата, а другой - один.

Моя цель - иметь возможность перечислять пользователей, имеющих определенный тип сертификата.

  1. Вывести список пользователей, имеющих сертификат с идентификатором 1 (должен перечислить обоих пользователей). - раскрывающийся список 1
  2. Вывести список только тех пользователей, у которых есть оба сертификата с идентификатором 1 и сертификат с идентификатором 2 (должен быть указан только 1 пользователь). - раскрывающийся список 2
  3. Когда какой-либо из этих пользователей выбирается из их отдельных выпадающих списков, в другом выпадающем списке указывается его сертификат.

Я пытался использовать group и group_by для достижения этого результата, но он не работал. Идея в том, что я пытаюсь сгруппировать CertificatesUser по user_id, но это не сработало. Он вернул эту ошибку:

2.5.3 :011 > CertificatesUser.group_by(&:user_id)
Traceback (most recent call last):
        1: from (irb):11
NoMethodError (undefined method `group_by' for #<Class:0x00007fa8dda7c668>)
Did you mean?  group

Затем я использовал group метод и получил это:

2.5.3 :012 > CertificatesUser.group('users.id')
  CertificatesUser Load (7.2ms)  SELECT  "certificates_users".* FROM "certificates_users" GROUP BY users.id LIMIT $1  [["LIMIT", 11]]
Traceback (most recent call last):
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "users")
LINE 1: ...cates_users".* FROM "certificates_users" GROUP BY users.id L...
                                                             ^
: SELECT  "certificates_users".* FROM "certificates_users" GROUP BY users.id LIMIT $1

Есть идеи, как я могу заставить это работать?

Ответы [ 2 ]

0 голосов
/ 15 января 2019

Короткий ответ

Я не понимаю, зачем вам вообще что-то группировать. Я бы решил эту проблему с использованием подзапросов.

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. По этой причине его документация довольно плохая. Однако, вы можете найти большинство используемых здесь методов .

0 голосов
/ 15 января 2019

Моя цель - иметь возможность перечислять пользователей, имеющих определенный тип сертификата.

Вы можете сделать это, используя joins:

certificate_ids = [1,2] # or anything other ids
User.joins(:certificates_users).where('certificates_users.certificate_id' => certificate_ids)

Относительно group и group_by.

group_by можно вызвать на Enumerable, поэтому вы не можете вызвать его на модели AR напрямую. Вы можете сделать это так:

CertificatesUser.all.group_by(&:user_id)

но это ужасно неэффективно - вы загружаете всю таблицу в память.

Для использования group необходимо указать правильное имя столбца. Вы используете users.id, а ваше certificates_users не имеет такого поля. Вы, вероятно, можете использовать

CertificatesUser.group('user_id')
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...