通过关系查询 Active Record 以查找 Record,其中 none 个关系的值为真

Active Record query through relationship to find Record where none of its relationships have true for a value

Rails 6.0 与 Postrgres

我有一个关系模型,其中 Employee has_many Jobs 和每个工作都有一个布尔值 active

如果员工拥有的每份工作都是 active: false,那么该员工将不再受雇。因此,我想查询员工的每份工作都有 active: false 以确定哪些员工不再受雇。

我试过

class Employee < ApplicationRecord
  scope :terminated, -> { 
    includes(:jobs).
      where.not(jobs: {
        active: true,
      })
  }

但这是在寻找 任何 工作但 active 不是 true 的员工。我想找到 每个 职位都有 active false 的员工。 AR 可以原生做到这一点吗?

一种方法是加入作业 table 两次并使用别名:

SELECT "employees".* FROM "employees" 
INNER JOIN "jobs" 
  ON "jobs"."employee_id" = "employees"."id" 
INNER JOIN "jobs" "inactive_jobs" 
  ON "inactive_jobs"."employee_id" = "employees"."id" 
  AND "inactive_jobs"."active" = 0 
GROUP BY "employees"."id" 
HAVING COUNT("jobs"."id") = COUNT("inactive_jobs"."id") LIMIT ?

ActiveRecord 并没有真正直接的方法来使用别名进行连接,但是通过一些 Arel 技巧,您可以实现它。

class Employee < ApplicationRecord
  has_many :jobs
  
  def self.terminated
    jobs = Job.arel_table
    # Needed to generate a join with an alias
    inactive_jobs = Job.arel_table.alias('inactive_jobs')
    # joins all the jobs
    joins(
      :jobs
    )
    # joins jobs where active: false
    .joins(
      self.arel_table.join(
          inactive_jobs,
          Arel::Nodes::InnerJoin
        ).on(
          inactive_jobs[:employee_id].eq(self.arel_table[:id])
            .and(inactive_jobs[:active].eq(false))
        ).join_sources
    )
    # groups on employee id
    .group(:id)
    # Set a condition on the group that the jobs must equal the number of inactive jobs
    .having(jobs[:id].count.eq(inactive_jobs[:id].count))
  end
end

如果你添加一个反缓存你可以作弊并删除第二个连接:

class Job
  belongs_to :employee, counter_cache: true
end

class Employee < ApplicationRecord
  has_many :jobs

  def self.terminated
    joins(:jobs)
      .where(jobs: { active: false })
      .group(:id)
      .having(arel_table[:jobs_count].eq(Job.arel_table[Arel.star].count)) 
  end
end

此处您将缓存值与连接行数进行比较。

您不应将 scope 用于此类查询,因为如果没有匹配的记录,则 scope returns 该模型的所有记录。 你可以为此写一个class方法:

class Employee < ApplicationRecord
  has_many :jobs

  def self.terminated
    preload(:jobs)
      .reject { |employee| employee.jobs.where(active: true).present? }
  end
end
class Employee < ApplicationRecord
   has_one :jobs
   scope :terminated, ->{ joins(:jobs).merge(Job.inactive) }
end

class Job < ApplicationRecord
  belongs_to :employee
  scope :inactive, -> { where(active: false) }
end

Employee.terminated 应该 return 所有只拥有 active 为 false 的工作的员工。