我使用以下代码进行了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个。
我的目标是能够列出具有某种证书类型的用户。
我尝试使用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
有什么主意我可以做这个工作吗?
答案 0 :(得分:2)
我的目标是能够列出具有某种证书类型的用户。
您可以使用joins
进行操作:
certificate_ids = [1,2] # or anything other ids
User.joins(:certificates_users).where('certificates_users.certificate_id' => certificate_ids)
关于group
和group_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。对于上面给出的答案,可以通过以下方式完成。
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使用的大多数方法。