【问题标题】:select all records holding some condition in has_many association - Ruby On Rails选择在 has_many 关联中持有某些条件的所有记录 - Ruby On Rails
【发布时间】:2017-07-28 21:12:37
【问题描述】:

我有一个model profile.rb 具有以下关联

class User < ActiveRecord::Base
   has_one :profile
end

class Profile < ActiveRecord::Base
    has_many :skills
    belongs_to :user
end

我有一个model skills.rb 与以下关联

class Skill < ActiveRecord::Base
    belongs_to :profile
end

我在技能表中有以下条目

id:         name:           profile_id:
====================================================
1           accounting          1
2           martial arts        2
3           law                 1
4           accounting          2
5           journalist          3
6           administration      1

等等,我如何查询所有具有“会计”和“管理”技能的个人资料,考虑到上述重新编码,这些技能将是 ID 为 1 的个人资料。到目前为止,我已尝试关注

Profile.includes(:skills).where(skills: {name: ["accounting" , "administration"]} )

但不是使用id 1 查找配置文件-它让我得到[ 1, 2 ],因为ID 为2 的配置文件包含"accounting" skills 并且它正在数据库中执行"IN" operation

注意:我正在使用postgresql,问题不仅是关于所描述的配置文件的特定 ID(我仅用作示例) - 最初的问题是获取包含这两个提到的技能的所有配置文件.

我的 activerecord 连接会在 postgres 中触发以下查询

SELECT FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" IN ('Accounting', 'Administration')

在下面Vijay Agrawal 的答案是我在我的应用程序中已经拥有的东西,他和我的查询都使用IN 通配符,这导致配置文件ID 包含任何一种技能,而我的问题是获取配置文件包含这两种技能的ID。我确信必须有一种方法可以以原始问题中列出的相同查询方式修复此问题,我很想学习这种方式。希望能帮到大家,谢谢

为了清楚起见,我想在一个模型中查询所有具有多种技能的配置文件,该模型与配置文件模型具有 has_many 关系 - 使用 Profile 作为主表而不是 skills

使用个人资料作为主表的原因是在分页中我不想从相关表中获取所有技能,比如 20_000 或更多行,然后根据profile.state 列进行过滤。相反,任何人都想只选择满足profile.state , profile.user.is_active and other columns condition5 records并匹配技能,而不检索数千条不相关的记录,然后再次过滤它们。

【问题讨论】:

  • 您的问题不清楚。请改写你的问题。 “找到所有具有会计技能且配置文件 ID 为 1 的配置文件”是没有意义的,因为这只能是一个配置文件。如果这就是你的意思,那么你通过关联:profile = Profile.find(1); profile.skills.where(name: %w(accounting administration))
  • @anothermh 这很清楚。我没有找到个人资料(1)的技能-虽然我想获取包含这两个提到的技能的所有个人资料-但是我也为其他成员更新了我的问题,并为其他成员提供了相同的评论
  • 我认为您必须提供有关您所看到的分页问题的更多详细信息。如果没有这些信息,这两个答案似乎都是合理的。
  • @Max 这并不重要。这就是为什么我最后提到了真正的问题——你可以忽略分页问题
  • 不确定您所说的“真正的问题”是指哪一部分。您的意思是“我想在与配置文件模型有 has_many 关系的模型中查询所有具有多种技能的配置文件 - 使用配置文件作为主表而不是技能”?我认为 Vijay 在他的更新中给了你这个。 anothermh 的更新答案也很好,但不符合您使用“has_many”关系的标准。虽然我同意他的观点,但 has_and_belongs_to_many 在这里看起来是一个更好的选择。

标签: ruby-on-rails postgresql ruby-on-rails-4 activerecord


【解决方案1】:

您应该这样做以获得所有具有会计和管理技能的profile_ids:

Skill.where(name: ["accounting", "administration"]).group(:profile_id).having("count('id') = 2").pluck(:profile_id)

如果您需要配置文件详细信息,可以将此查询放在 Profile 的 where 子句中,用于 id

注意查询中的数字2,它是where子句中使用的数组的长度。在这种情况下["accounting", "administration"].length

更新::

根据更新的问题描述,您可以使用select 代替pluck 并添加子查询以确保它发生在一个查询中。

Profile.where(id: Skill.where(name: ["accounting", "administration"]).group(:profile_id).having("count('id') = 2").select(:profile_id))

您可以控制排序、分页和附加的 where 子句。没有看到问题编辑中提到的任何问题。

更新 2::

另一种让配置文件与这两种技能相交的方法(可能效率低于上述解决方案):

profiles = Profile

["accounting", "administration"].each do |name|
  profiles = profiles.where(id: Skill.where(name: name).select(:profile_id))
end

【讨论】:

  • 很好的答案,但 ActiveRecord 方法不是group 而不是group_by
  • 谢谢,已修复。
  • @ImranNaqvi 就像我提到的,一旦你得到profile_id,你可以查询Profile
  • 浏览您的回复,您似乎在做出您不理解的假设。如果您不明白IN 的含义,我只能尝试解决方案。 IN 与您在评论中描述的方式不同。
  • @ImranNaqvi @VijayAgrawal 的更新答案有效,此表达式 Profile.where(id: Skill.where(name: ["accounting", "administration"]).group(:profile_id).having("count('id') = 2").select(:profile_id)) 产生此查询 SELECT "profiles".* FROM "profiles" WHERE "profiles"."id" IN (SELECT "skills"."profile_id" FROM "skills" WHERE "skills"."name" IN ('accounting', 'administration') GROUP BY "skills"."profile_id" HAVING (count('id') = 2))
【解决方案2】:
Profile.includes(:skills).where("skills.name" => %w(accounting administration))

如需了解更多信息,请阅读finding through ActiveRecord associations

更新

如果这对您不起作用,那么您可能没有正确配置数据库和模型,因为在全新的 Rails 应用程序中它可以正常工作。

class CreateProfiles < ActiveRecord::Migration[5.1]
  def change
    create_table :profiles do |t|
      t.timestamps
    end
  end
end

class CreateSkills < ActiveRecord::Migration[5.1]
  def change
    create_table :skills do |t|
      t.string :name
      t.integer :profile_id
      t.timestamps
    end
  end
end

class Profile < ApplicationRecord
  has_many :skills
end

class Skill < ApplicationRecord
  belongs_to :profile
end

Profile.create
Profile.create
Skill.create(name: 'foo', profile_id: 1)
Skill.create(name: 'bar', profile_id: 1)
Skill.create(name: 'baz', profile_id: 2)

Profile.includes(:skills).where("skills.name" => %w(foo))
  SQL (0.3ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'foo' LIMIT ?  [["LIMIT", 11]]
  SQL (0.1ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'foo' AND "profiles"."id" = 1
 => #<ActiveRecord::Relation [#<Profile id: 1, created_at: "2017-07-28 21:52:56", updated_at: "2017-07-28 21:52:56">]>

Profile.includes(:skills).where("skills.name" => %w(bar))
  SQL (0.3ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'bar' LIMIT ?  [["LIMIT", 11]]
  SQL (0.1ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'bar' AND "profiles"."id" = 1
 => #<ActiveRecord::Relation [#<Profile id: 1, created_at: "2017-07-28 21:52:56", updated_at: "2017-07-28 21:52:56">]>

Profile.includes(:skills).where("skills.name" => %w(baz))
  SQL (0.3ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'baz' LIMIT ?  [["LIMIT", 11]]
  SQL (0.1ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "skills" ON "skills"."profile_id" = "profiles"."id" WHERE "skills"."name" = 'baz' AND "profiles"."id" = 2
 => #<ActiveRecord::Relation [#<Profile id: 2, created_at: "2017-07-28 21:53:34", updated_at: "2017-07-28 21:53:34">]>

更新 2

因为您后来更改了问题而拒绝回答是不好的形式。

您应该将模型关系从 has_manybelongs_to 更改为 has_and_belongs_to_many。这将允许您每次都停止录制新技能;如果有人添加了技能administration,然后其他人添加了该技能,则您不必创建新技能。您只需重新使用现有技能并将其与多个配置文件相关联:

class Profile < ApplicationRecord
  has_and_belongs_to_many :skills
end

class Skill < ApplicationRecord
  has_and_belongs_to_many :profiles
end

添加一个带有唯一索引的连接表(这样每个配置文件就可以拥有每个技能一次且仅一次):

class Join < ActiveRecord::Migration[5.1]
  def change
    create_table :profiles_skills, id: false do |t|
      t.belongs_to :profile, index: true
      t.belongs_to :skill, index: true
      t.index ["profile_id", "skill_id"], name: "index_profiles_skills_on_profile_id_skill_id", unique: true, using: :btree
    end
  end
end

创建你的模型:

Profile.create
Profile.create
Skill.create(name: 'foo')
Skill.create(name: 'bar')
Skill.create(name: 'baz')
Profile.first.skills << Skill.first
Profile.first.skills << Skill.second
Profile.second.skills << Skill.second
Profile.second.skills << Skill.third

然后运行您的查询以仅返回第一个配置文件:

skills = %w(foo bar).uniq
Profile.includes(:skills).where('skills.name' => skills).group(:id).having("count(skills.id) >= #{skills.size}")
  SQL (0.4ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" IN ('foo', 'bar') GROUP BY "profiles"."id" HAVING (count(skills.id) = 2) LIMIT ?  [["LIMIT", 11]]
  SQL (0.2ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" IN ('foo', 'bar') AND "profiles"."id" = 1 GROUP BY "profiles"."id" HAVING (count(skills.id) = 2)
 => #<ActiveRecord::Relation [#<Profile id: 1, created_at: "2017-07-28 21:52:56", updated_at: "2017-07-28 21:52:56">]>

通过额外测试确认:

应该返回两个配置文件:

skills = %w(bar).uniq
Profile.includes(:skills).where('skills.name' => skills).group(:id).having("count(skills.id) >= #{skills.size}")
  SQL (0.4ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" = 'bar' GROUP BY "profiles"."id" HAVING (count(skills.id) >= 1) LIMIT ?  [["LIMIT", 11]]
  SQL (0.3ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" = 'bar' AND "profiles"."id" IN (1, 2) GROUP BY "profiles"."id" HAVING (count(skills.id) >= 1)
 => #<ActiveRecord::Relation [#<Profile id: 1, created_at: "2017-07-28 21:52:56", updated_at: "2017-07-28 21:52:56">, #<Profile id: 2, created_at: "2017-07-28 21:53:34", updated_at: "2017-07-28 21:53:34">]>

应该只返回第二个配置文件:

skills = %w(bar baz).uniq
  SQL (0.3ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" IN ('bar', 'baz') GROUP BY "profiles"."id" HAVING (count(skills.id) >= 2) LIMIT ?  [["LIMIT", 11]]
  SQL (0.2ms)  SELECT "profiles"."id" AS t0_r0, "profiles"."created_at" AS t0_r1, "profiles"."updated_at" AS t0_r2, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."profile_id" AS t1_r2, "skills"."created_at" AS t1_r3, "skills"."updated_at" AS t1_r4 FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" IN ('bar', 'baz') AND "profiles"."id" = 2 GROUP BY "profiles"."id" HAVING (count(skills.id) >= 2)
 => #<ActiveRecord::Relation [#<Profile id: 2, created_at: "2017-07-28 21:53:34", updated_at: "2017-07-28 21:53:34">]>

不应返回任何配置文件:

skills = %w(foo baz).uniq
Profile.includes(:skills).where('skills.name' => skills).group(:id).having("count(skills.id) >= #{skills.size}")
  SQL (0.3ms)  SELECT  DISTINCT "profiles"."id" FROM "profiles" LEFT OUTER JOIN "profiles_skills" ON "profiles_skills"."profile_id" = "profiles"."id" LEFT OUTER JOIN "skills" ON "skills"."id" = "profiles_skills"."skill_id" WHERE "skills"."name" IN ('foo', 'baz') GROUP BY "profiles"."id" HAVING (count(skills.id) >= 2) LIMIT ?  [["LIMIT", 11]]
 => #<ActiveRecord::Relation []>

【讨论】:

  • 除了您以其他方式使用它之外,它是相同的查询 - 但产生的结果与我的相同 - 如果它可以使它变得更好,我会质疑查询结果
  • 您一次检查一项技能......它有效,但搜索超过 2 项技能 - 失败
  • 我认为问题在于寻找所有同时具备会计和管理技能的个人资料,而不是所有具备这两种技能的个人资料。
【解决方案3】:

PostgreSQL依赖解决方案:

where_clause = <<~SQL
  ARRAY(
    SELECT name FROM skills WHERE profile_id = profiles.id
  ) @> ARRAY[?]
SQL
Profile.where(where_clause, %w[skill1 skill2])

它可以工作,但是为了加速而改变数据库结构是有意义的。有两种选择:

  • has_and_belongs_to_many 方式增加了一致性(skills 表格变成了字典)和使用索引的能力
  • skills as array|jsonb 列的 profile - 添加快速搜索索引,无需子选择或连接。

【讨论】:

  • 我对您的回答很感兴趣。您想为上述问题添加确切的样本吗?澄清where_clause
  • @LucasH。什么不清楚?键入 Profile.where(where_clause, %w[skill1 skill2]).to_sql 以获取 SQL 查询。阅读PostgreSQL 文档,了解ARRAYs 和@&gt; 运算符。
【解决方案4】:

我会将EXISTScorrelated sub-query 一起使用,如下所示:

required_skills = %w{accounting administration}
q = Profile.where("1=1")
required_skills.each do |sk|
  q = q.where(<<-EOQ, sk)
    EXISTS (SELECT 1
            FROM   skills s
            WHERE  s.profile_id = profiles.id
            AND    s.name = ?)
  EOQ
end

this similar question 有其他一些想法,但我认为在您的情况下,多个 EXISTS 子句是最简单且最可能最快的。

(顺便说一下,在 Rails 4+ 中,您可以从 Profile.all 开始而不是 Profile.where("1=1"),因为 all 返回一个 Relation,但在过去它曾经返回一个数组。)

【讨论】:

  • 它会为每个required_skills 做一次技能表的“通过”。可能比已经发布的组/具有解决方案要慢。另一方面,你不需要做 .all.where.... 只是 q = Profile 应该工作
  • q = q.where(&lt;&lt;-EOQ, sk)这是什么意思?
  • @LucasH。 EOQheredoc,继承自 shell 和 Perl。它将所有内容都引用到匹配的令牌(此处为EOQ)。所以我说q.where("EXISTS (. . . s.name = ?)", sk)
【解决方案5】:

以下查询的问题

Profile.includes(:skills).where(skills: { name: ["accounting" , "administration"] } ) 是它使用IN 运算符创建查询,例如IN ('Accounting', 'Administration')

现在根据 SQA 标准,它将匹配匹配任何值的所有记录,而不是数组中的所有值。

这是一个最简单的解决方案

skills = ["accounting" , "administration"]

Profile.includes(:skills).where(skills: { name: skills }).group(:profile_id).having("count(*) = #{skills.length}")

附:这假设您将拥有至少一项技能。根据您的用例调整 having 条件

【讨论】:

  • ActiveRecord::StatementInvalid: PG::GroupingError: 无论我更改哪个组的 id
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多