为什么group或group_by在此版本的rails表上不起作用?

时间:2019-01-15 11:07:05

标签: ruby-on-rails

我使用以下代码进行了Rails迁移

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>]>  

为清楚起见,这是 certificates_users 表。

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

一个用户有2个证书,另一个用户有1个。

我的目标是能够列出具有某种证书类型的用户。

  1. 列出具有ID为1的证书的用户(应列出两个用户)。 -下拉菜单1
  2. 仅列出同时拥有ID为1的证书和ID为2的证书的用户(应仅列出1个用户)。 -下拉菜单2
  3. 从单独的下拉列表中选择该用户中的任何一个时,会在另一个下拉列表中列出其证书。

我尝试使用groupgroup_by来达到这个结果,但是没有用。我的想法是我正在尝试按CertificatesUseruser_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 :(得分:2)

  

我的目标是能够列出具有某种证书类型的用户。

您可以使用joins进行操作:

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

关于groupgroup_by

可以在Enumerable上调用

group_by,因此您不能直接在AR模型上调用它。您可以这样:

CertificatesUser.all.group_by(&:user_id)

但效率极低-您将整个表加载到内存中。

要使用group,您需要指定正确的列名。您使用users.id,而您的certificates_users没有这样的字段。您可能可以使用

CertificatesUser.group('user_id')

答案 1 :(得分:1)

简短回答

我看不到您为什么需要对所有东西进行分组。我将通过使用子查询来解决此问题。

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

动态证书ID

在注释中,您询问了如何使此答案适用于动态证书ID。对于上面给出的答案,可以通过以下方式完成。

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
  )

优化查询

尽管以上示例向我们展示了如何使整个过程变得动态。对于大量的证书ID这样做不会产生最有效的查询。

解决此问题的另一种方法确实涉及分组,并且执行起来稍微困难一些。这涉及在 certificates_users 表中搜索与给定ID之一匹配的记录。然后计算每个用户的记录量。如果记录数量等于给定的证书ID数量,则意味着用户拥有所有这些证书。

先决条件

但是,此方法确实具有先决条件。

  • user_id certificate_id 的组合在 certificates_users 表中必须是唯一的。通过在这些列上放置唯一约束来确保是这种情况。

    add_index :certificates_users, %i[user_id certificate_id], unique: true
    
  • 另一个可选建议是, certificates_users 表中的 user_id certificate_id 列不能为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,建议您看看documentation。由于此方法存在于许多其他编程语言中。这是您武器库中的绝佳工具。

arel gem API并不是真的要在Rails内部内部使用。因此,它的文档非常糟糕。但是,您可以找到here使用的大多数方法。