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

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

以下是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向项目中增加任务

重新刷新页面,察看效果

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


优化以下代码实现def get_cell_value_with_merge_in_sheet(ws, cell): """获取单元格值,考虑合并单元格情况(独立函数)""" for merged_range in ws.merged_cells.ranges: if cell.coordinate in merged_range: # 返回合并区域左上角单元格的值 return ws.cell(merged_range.min_row, merged_range.min_col).value return cell.value def process_sheet(ws_value, ws_formula, output_ws, keywords, sheet_name): """处理单个工作表(优化版本)""" #计数器从第4行开始(跳过标题行) row_counter = output_ws.max_row + 1 if output_ws.max_row > 3 else 4 start_row = row_counter found_count = 0 #缓存行合并信息(避免重复计算) merged_info_cache = {} # 预获取两个特殊列的列索引 line1_col_idx = None line2_col_idx = None for col_idx in range(1, ws_value.max_column + 1): if not is_column_visible(ws_value, col_idx): continue col_title = get_column_title(ws_value, col_idx) if col_title == "表示文言一行目": line1_col_idx = col_idx elif col_title == "表示文言二行目": line2_col_idx = col_idx # 只遍历有内容的行(性能优化) visible_rows = [row_idx for row_idx in range(1, ws_value.max_row + 1) if is_row_visible(ws_value, row_idx)] # 只处理包含变更类型的列(性能优化) change_columns = [] for col_idx in range(1, ws_value.max_column + 1): if is_column_visible(ws_value, col_idx): change_columns.append(col_idx) # 遍历所有可见行 for row_idx in visible_rows: # 获取当前行的两个TXTID值 txtid_line1 = "" txtid_line2 = "" # 获取表示文言一行目 if line1_col_idx: cell = ws_value.cell(row=row_idx, column=line1_col_idx) cell_value = get_cell_value_with_merge_in_sheet(ws_value, cell) if cell_value and isinstance(cell_value, str) and cell_value[:5].upper() == "TXTID": txtid_line1 = cell_value # 获取表示文言二行目 if line2_col_idx: cell = ws_value.cell(row=row_idx, column=line2_col_idx) cell_value = get_cell_value_with_merge_in_sheet(ws_value, cell) if cell_value and isinstance(cell_value, str) and cell_value[:5].upper() == "TXTID": txtid_line2 = cell_value # 缓存当前行的合并信息(避免重复调用get_merged_info) if row_idx not in merged_info_cache: merged_info_cache[row_idx] = get_merged_info(ws_value, row_idx, sheet_name) merged_info = merged_info_cache[row_idx] # 遍历所有可见列 for col_idx in change_columns: try: # 获取单元格值 value_cell = ws_value.cell(row=row_idx, column=col_idx) change_type = value_cell.value # 只处理变更类型单元格 if change_type not in CHANGE_TYPES: continue # 获取对应的公式单元格 formula_cell = ws_formula.cell(row=row_idx, column=col_idx) formula_text = formula_cell.value # 解析EXACT函数 new_pos, old_pos = extract_exact_references(formula_text) if not new_pos or not old_pos: cell_ref = f"{get_column_letter(col_idx)}{row_idx}" logging.debug( f"在 {sheet_name} 工作表中无法解析公式: {formula_text} 在 {cell_ref}" ) continue # 获取OLD和NEW单元格的完整内容 old_cell_content = get_cell_content(ws_formula, old_pos) new_cell_content = get_cell_content(ws_formula, new_pos) # 获取列标题 column_title = get_column_title(ws_value, col_idx) # 获取变更原位置 change_cell_ref = f"{get_column_letter(col_idx)}{row_idx}" # 写入输出 write_output_row( output_ws, row_counter, merged_info, column_title, change_type, change_cell_ref, old_pos, new_pos, old_cell_content, new_cell_content, txtid_line1, txtid_line2 ) row_counter += 1 found_count += 1 except Exception as e: cell_ref = f"{get_column_letter(col_idx)}{row_idx}" logging.error(f"处理单元格 {cell_ref} 时出错: {str(e)}") continue # 添加边框和END标记 if found_count > 0: apply_cell_borders(output_ws, start_row, row_counter - 1) row_counter = add_end_marker(output_ws, row_counter, sheet_name) logging.info(f"在 &#39;{sheet_name}&#39; 工作表中找到 {found_count} 个变更") return found_count # 更新写入输出行的函数 def write_output_row(output_ws, row, merged_info, column_title, change_type, change_cell_ref, old_pos, new_pos, old_cell_content, new_cell_content, txtid_line1, txtid_line2): """写入输出行(只保留TextID拆分列)""" # 基础列(B到I列) output_ws.cell(row=row, column=2, value=merged_info) # B列: 项目 output_ws.cell(row=row, column=3, value=column_title) # C列: 言語类型 output_ws.cell(row=row, column=4, value=change_type) # D列: 变更类型 output_ws.cell(row=row, column=5, value=change_cell_ref) # E列: 变更原位置 output_ws.cell(row=row, column=6, value=old_pos) # F列: OLD位置 output_ws.cell(row=row, column=7, value=new_pos) # G列: NEW位置 output_ws.cell(row=row, column=8, value=str(old_cell_content) if old_cell_content is not None else "") # H列: OLD式样 output_ws.cell(row=row, column=9, value=str(new_cell_content) if new_cell_content is not None else "") # I列: NEW式样 # TextID拆分列(J到O列) # 拆分行目1 (J, K, L列) line1_parts = txtid_line1.split("_") if txtid_line1 else [] for i, part in enumerate(line1_parts[:3]): # 最多取前三部分 output_ws.cell(row=row, column=10 + i, value=part) # 拆分行目2 (M, N, O列) line2_parts = txtid_line2.split("_") if txtid_line2 else [] for i, part in enumerate(line2_parts[:3]): # 最多取前三部分 output_ws.cell(row=row, column=13 + i, value=part) def process_workbook(filepath, output_wb): """处理单个工作簿""" filename = os.path.basename(filepath) logging.info(f"处理文件: {filename}") wb_value = None wb_formula = None found_total = 0 try: # 加载工作簿 - 值版本 wb_value = load_workbook(filepath, data_only=True) # 加载工作簿 - 公式版本 wb_formula = load_workbook(filepath, data_only=False) # 按照指定顺序处理工作表 for sheet_name in SHEET_ORDER: if sheet_name not in wb_value.sheetnames: logging.info(f"文件 {filename} 中不存在工作表 &#39;{sheet_name}&#39;,跳过处理") continue ws_value = wb_value[sheet_name] if sheet_name not in wb_formula.sheetnames: logging.warning( f"文件 {filename} 中工作表 &#39;{sheet_name}&#39; 在公式版本中不存在,跳过处理" ) continue ws_formula = wb_formula[sheet_name] # 获取输出工作表名称 output_sheet_name = OUTPUT_SHEET_NAMES[sheet_name] # 如果输出工作簿中不存在对应工作表,则创建 if output_sheet_name not in output_wb.sheetnames: output_ws = create_output_sheet(output_wb, output_sheet_name) if output_ws is None: continue logging.info(f"创建输出工作表: {output_sheet_name}") else: output_ws = output_wb[output_sheet_name] # 处理当前工作表 keywords = SHEET_KEYWORDS[sheet_name] found_count = process_sheet( ws_value, ws_formula, output_ws, keywords, sheet_name ) found_total += found_count return found_total > 0 except Exception as e: logging.error(f"处理文件 {filepath} 时出错: {str(e)}", exc_info=True) return False finally: # 确保关闭工作簿 if wb_value: try: wb_value.close() except Exception: pass if wb_formula: try: wb_formula.close() except Exception: pass①在获取变更原位置change_cell_ref = f"{get_column_letter(col_idx)}{row_idx}"后,分别获取变更原位置当前行的TXTID值,提取TXTID值的所在位置,获取一个TXTID后判断该位置对应的列是否包含以下5种情况 1.输入:首个非空单元格内容为“表示文言一行目”(可能存在合并单元格),第二个非空单元格内容为“TextID”, 2.输入:首个非空单元格内容为“表示文言一行目”(可能存在合并单元格),第二个非空单元格内容为“TextID_02” 3.输入:首个非空单元格内容为“表示文言一行目”(可能存在合并单元格),第二个非空单元格内容为“TextID_03” 4.输入:首个非空单元格内容为“表示文言二行目”(可能存在合并单元格),第二个非空单元格内容为“TextID” 5.输入:首个非空单元格内容为“表示文言二行目”(可能存在合并单元格),第二个非空单元格内容为“TextID_02” 6.输入:首个非空单元格内容为“表示文言二行目”(可能存在合并单元格),第二个非空单元格内容为“TextID_03” ②如果TEXTID对应列首个非空单元格内容为“表示文言一行目”第二个非空单元格内容为“TextID”,并且该TEXTID数据对应的sheet为“割入画面”或者“基本画面”或者“OP_ED画面” 则将当前TEXTID所对应的内容写到output对应文件对应sheet的J列,其他情况依次为2对应K列,3对应L列,4对应M列,5对应N列,6对应O列 ③然后判断变更原位置当前的第二个TEXTID是否包含以上5种情况 ④完成变更原位置所有的TXTID的判断后在执行下一个变更原位置的其他信息输出
12-31
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值