#23 Counter Cache Column
和上一篇一样,咱们聚焦于ActiveRecord
数据库查询性能这个话题。如下图所示,页面列出一系列项目(Project)以及其中包含的任务(Task)数。
以下是ProjectsController
和index.html.erb
。
class ProjectsController < ApplicationController def index @projects = Project.find(:all) end end
在控制器ProjectsController
从数据库中读取出所有的项目。
<h1>Projects</h1> <ol> <% @projects.each do |project| %> <li><%= link_to project.name, project_path(project) %> (<%= pluralize project.tasks.size, ’task’ %>)</li> <% end %> </ol>The index view.
在视图页面,循环每一个Project
的时候显示项目名称,在通过调用project.tasks.size
方法显示项目中包含的任务数。这里还使用到了pluralize
方法以便自动根据项目包含任务的数量决定显示单数还是复数。
效率有待提高
察看一下页面加载时候的日志。
Rendering projects/index SQL (0.3ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 61) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 62) SQL (0.3ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 63) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 64) SQL (0.2ms) SELECT count(*) AS count_all FROM "tasks" WHERE ("tasks".project_id = 65)
为了得到项目中任务的数量,每次都需要进行一次数据库访问。如何解决这个问题呢?可以使用之前学过的贪婪加载(级连查询)技术。修改ProjectsController
代码让加载项目对象的时候将其包含的任务列表也一并加载上来。
@projects = Project.find(:all, :include => :tasks)
现在重新刷新页面察看日志可以发现,访问次数减少了,降为两次。
Processing ProjectsController#index (for 127.0.0.1 at 2009-01-26 21:24:28) [GET] Project Load (1.1ms) SELECT * FROM "projects" Task Load (7.1ms) SELECT "tasks".* FROM "tasks" WHERE ("tasks".project_id IN (61,62,63,64,65))
这么修改之后确实提升了加载效率,但是不得不承认仅仅为了获得项目中的任务数便把所有的任务加载上来有点浪费了。改进方法是使用counter cache column来代替。
实现Counter Cache Column
第一步是为Project
表增加一个专门用于存储所包含的任务数量值的字段。创建一个迁移任务
script/generate migration add_tasks_count
迁移任务的代码如下
class AddTasksCount < ActiveRecord::Migration def self.up add_column :projects, :tasks_count, :integer, :default => 0 Project.reset_column_information Project.all.each do |p| p.update_attribute :tasks_count, p.tasks.length end end def self.down remove_column :projects, :tasks_count end end
字段的命名是有讲头的,要以我们想计数的那个模型的表开头(这里是task
,后面跟上_count
,和起来是tasks_count
。缺省值也得给设上否则新创建出来的Project
对象就该不对了。增加了这个字段之后,还得给当前数据库中已有的记录更新一下这个字段的值。做法是循环每一个项目,并让tasks_count
的值等于project.tasks.length
。这里使用length
方法而没有使用size
的原因是size
方法调用的时候会来读tasks_count
字段,而这个时候值还都是0。
在修改表结构之后、更新表数据之前最好刷新一下表结构缓存,以免由于缓存与当前不匹配导致错误发生。调用Project.reset_column_information
方法完成这一工作。
检验效果
既然增加了Count Cache Column,就把贪婪加载从ProjectsController
中去掉吧。然后重新刷新看看效果
Processing ProjectsController#index (for 127.0.0.1 at 2009-01-26 22:07:13) [GET] Project Load (0.7ms) SELECT * FROM "projects"
现在只有一次查询请求发生了,同时也不需要从Tasks
的表中查询不必要的数据。项目中包含的任务数是从projects
表tasks_count
列都取的。
还没完
还没有大功告成,刚才只是通过迁移任务将数据库中的所有记录更新正确。但目前向项目中插入任务的操作还不会自动更新tasks_counter
字段的值。还得告诉Rails,tasks_count
应该作为计数列,当一个任务加入项目后被自动更新。在Task
类中进行修改。
class Task < ActiveRecord::Base belongs_to :project, :counter_cache => true has_many :comments end
通过在关联关系这里设置:counter_cache => true
便可以。如此以来,Rails就知道在任务被加入项目后该去干什么了。打开rails console通过实验验证一下。
>> p = Project.first => #<Project id: 61, name: "Project 1", created_at: "2009-01-26 20:34:36", updated_at: "2009-01-26 22:05:22", tasks_count: 20> >> p.tasks.create(:name => "New task") => #<Task id: 1201, name: "New task", project_id: 61, created_at: "2009-01-26 22:24:13", updated_at: "2009-01-26 22:24:13">
通过rails console向项目中增加任务
重新刷新页面,察看效果
Project1的任务数发生了变化。
结果正确并且只对projects
一个表进行了访问。