计算 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 语句中把它弄出来?
我可以想出几种方法...
(这不是单个 sql 语句,而是两个,但仍然非常高效)...
Task.where(status: 1).group(:project_id).count
将为您提供一个散列,其中键是项目 ID,值是任务计数。然后您可以将其与项目列表结合起来。
您可以使用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。
我有一个名为 Project 的模型,Project 有很多任务
任务可以有 3 个不同的状态(整数)。
我想获取状态为 1、2 和 3 的相关任务计数的项目列表。
我能得到的最好的是在 Project
上有一个方法def open_tasks
self.tasks.where(:status => 1).count
end
但是每次计数都会产生另一个 SQL,加载 100 个项目时性能非常差。
有没有办法在一个 SQL 语句中把它弄出来?
我可以想出几种方法...
(这不是单个 sql 语句,而是两个,但仍然非常高效)...
Task.where(status: 1).group(:project_id).count
将为您提供一个散列,其中键是项目 ID,值是任务计数。然后您可以将其与项目列表结合起来。您可以使用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。