RailsCasts中文版,#23 Counter Cache Column 计数器缓存字段

和上一篇一样,咱们聚焦于ActiveRecord数据库查询性能这个话题。如下图所示,页面列出一系列项目(Project)以及其中包含的任务(Task)数。

RailsCasts中文版,#23 Counter Cache Column 计数器缓存字段_第1张图片

以下是ProjectsControllerindex.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>

视图中显示记录。

在视图页面,循环每一个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的表中查询不必要的数据。项目中包含的任务数是从projectstasks_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向项目中增加任务

重新刷新页面,察看效果

RailsCasts中文版,#23 Counter Cache Column 计数器缓存字段_第2张图片

Project1的任务数发生了变化。

结果正确并且只对projects一个表进行了访问,效率得到了提升。


作者授权:Your welcome to post the translated text on your blog as well if the episode is free(not Pro). I just ask that you post a link back to the original episode on railscasts.com.

原文链接:http://railscasts.com/episodes/23-counter-cache-column


你可能感兴趣的:(RailsCasts中文版,#23 Counter Cache Column 计数器缓存字段)