计算 Rails 上 Ruby 状态的关联数

Count number of associations with a status in Ruby on Rails

我有一个名为 Project 的模型,Project 有很多任务

任务可以有 3 个不同的状态(整数)。

我想获取状态为 1、2 和 3 的相关任务计数的项目列表。

我能得到的最好的是在 Project

上有一个方法
def open_tasks
  self.tasks.where(:status => 1).count
end

但是每次计数都会产生另一个 SQL,加载 100 个项目时性能非常差。

有没有办法在一个 SQL 语句中把它弄出来?

我可以想出几种方法...

  1. (这不是单个 sql 语句,而是两个,但仍然非常高效)... Task.where(status: 1).group(:project_id).count 将为您提供一个散列,其中键是项目 ID,值是任务计数。然后您可以将其与项目列表结合起来。

  2. 您可以使用ActiveRecord counter_cache在项目记录中保存一个用于打开任务数的值。 ActiveRecord 会自动为你更新这个。我相信您需要像这样向项目模型添加关联:

# app/models/project.rb
# needs to include a column called open_task_count
class Project < ActiveRecord::Base
  has_many :open_tasks, class_name: Task, -> { where status: 1 }
end

class Task < ActiveRecord::Base
  belongs_to :project, counter_cache: true
end
Project.select(
  'projects.*',
  '(SELECT COUNT(tasks.*) FROM tasks WHERE tasks.project_id = projects.id AND tasks.status = 0) AS status_0_count',
  '(SELECT COUNT(tasks.*) FROM tasks WHERE tasks.project_id = projects.id AND tasks.status = 1) AS status_1_count'
).left_joins(:tasks)

虽然有更优雅的方式(如横向连接和 CTE)子查询适用于大多数数据库。如果 statuses 是 ActiveRecord::Enum 你可以通过遍历枚举映射来构造子查询:

class Project < ApplicationRecord
  has_many :tasks
  def self.with_task_counts
    # constucts an array of SQL strings
    statuses = Task.statuses.map do |key, int|
      sql = Task.select('COUNT(*)')
             .where('tasks.project_id = projects.id')
             .where(status: key)
             .to_sql
      "(#{sql}) AS #{key}_tasks_count"
    end
    select(
      'projects.*',
      *statuses # * turns the array into a list of args
    ).left_joins(:tasks)
  end
end

在 Rails 4 中,您仍然可以使用 SQL 字符串进行 LEFT OUTER JOIN:

class Project
  def self.left_joins_tasks(*args)
    deprecator = ActiveSupport::Deprecation.new("5.0", "MyApp")
    deprecator.deprecation_warning("left_joins_tasks is deprecated, use `.left_joins(:tasks)` instead")
    joins('LEFT OUTER JOIN tasks ON tasks.project_id = projects.id')
  end
end

使用 .joins 也可以,但提供了一个 INNER 连接,因此没有任务的行被过滤掉。您也可以使用 .includes.

我最终使用了 counter_culture gem。

https://github.com/magnusvk/counter_culture