选择has_many关联中包含某些条件的所有记录 - Ruby On Rails

时间:2017-07-28 21:12:38

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

我有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

依此类推,我怎样才能查询所有的个人资料,比方说,“会计”&amp;考虑到上述重新编码,“管理”技能将是id为1的概要。到目前为止,我已经尝试过以下

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

但不是查找包含id 1的个人资料 - 它会让我[ 1, 2 ],因为ID为2的个人资料会保留"accounting" skills并且它在数据库中执行"IN" operation

注意:我正在使用postgresql并且问题不仅仅是描述的配置文件的特定ID(我仅用作示例) - 原始问题是获取包含这两个的所有配置文件提到的技能。

我的activerecord join会在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列进行过滤。相反,任何人都希望只选择符合5 records的{​​{1}}并匹配技能而不检索数千条不相关的记录,然后再过滤它们。

5 个答案:

答案 0 :(得分:7)

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

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

如果您需要个人资料详细信息,可以将此查询放在Profile的{​​{1}}的where子句中。

请注意查询中的数字id,它是在where子句中使用的数组的长度。在这种情况下2

UPDATE ::

根据更新的问题说明,您可以使用["accounting", "administration"].length而不是pluck添加子查询,以确保它在一个查询中发生。

select

您可以控制排序,分页和其他where子句。在编辑问题中没有看到任何关注的问题。

更新2 ::

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

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

答案 1 :(得分:1)

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 :(得分:1)

>>> my_list = [1,0,2,0,3,0,4,0] >>> my_index = my_list.index(3) >>> del my_list[my_index:my_index+2] >>> my_list [1, 0, 2, 0, 4, 0] 依赖解决方案:

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的{​​{1}} | array列 - 按索引添加快速搜索,不需要子选择或加入。

答案 3 :(得分:1)

我会将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,但在过去它曾经返回一个数组。)

答案 4 :(得分:-1)

以下查询的问题

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}")

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