计算在 Ruby on Rails 中具有状态的关联数



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

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

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

我能得到的最好的就是在项目上有一个方法

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在项目记录中保存打开任务数的值。活动记录将自动更新。我相信您需要像这样向项目模型添加一个关联:

# 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 字符串执行左外连接:

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宝石。

https://github.com/magnusvk/counter_culture

最新更新