iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0

14 - counter cache

在許多情況下,會需要統計一對多關聯的資料數量。舉例來說像是 User has_many Post。這時如果要統計該 user 擁有的 post 數量時,若直接使用 User.posts.size 則每次讀取頁面都會重新再統計一次,影響網站效能,而 ActiveRecord's counter_cache 可以來幫助解決這個問題。


以 User has_many post 的例子來看:

  # app/controllers/users_controller.rb
  class UsersController < ApplicationController
    def index
      @users = User.all
    end
  end
  # app/views/users/index.html.erb
  <% @users.each do |user| %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.email %></td>
      <td><%= user.tel %></td>
      <td><%= user.posts.size %></td>
      <td><%= link_to 'Show', user %></td>
      <td><%= link_to 'Edit', edit_user_path(user) %></td>
    </tr>
  <% end %>
  # app/models/user.rb
  class User < ApplicationRecord
    has_many :posts
  end

  # app/models/post.rb
  class Post < ApplicationRecord
    belongs_to :user
  end

從 server console 中可以看到 N+1 問題,會重複的一直去撈資料庫。

  SELECT "users".* FROM "users"
  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 1]]
  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 2]]
  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 3]]
  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 4]]
  SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = $1  [["user_id", 5]]

接下來就來新增欄位 field_count(慣例為 xxx_count 結尾) 來透過 counter_cache 來把存數字進資料庫,解決 SQL count 查詢造成的 N+1 問題,神奇的是 Post 數量有更新時,欄位也會自動增減。

產生欄位

  rails g migration add_posts_count_to_user

migration

  class AddPostsCountToUser < ActiveRecord::Migration[6.1]
    def change
      add_column :users, :posts_count, :integer, default: 0

      User.pluck(:id).each do |id|
        User.reset_counters(id, :posts)
      end
    end
  end

註: reset_countersActiveRecord::CounterCache method

也可以將重新計算的 counter 的程式寫成 rake 來執行比放在 migration 內來的適合

rake task

  # lib/migrate/reset_user_posts_count.rb
  namespace :migrate do
    desc 'Reset user posts_count counter cache'
    task reset_user_posts_count: :environment do
      User.pluck(:id).each do |id|
        User.reset_counters(id, :posts)
      end
    end
  end

並記得要跑 bundle exec rails migrate:reset_user_posts_count

model

再來在 Post model 修正加上 counter_cache: true 即可。

  # app/models/post.rb
  class Post < ApplicationRecord
    belongs_to :user, counter_cache: true
  end

這時候就會發現原本的 N+1 問題解決了~user.posts.size 這段就會被自動改成 user.posts_count,數量也都會自動更新!

參考來源

My blog


上一篇
冒險村13 - migration notes
下一篇
冒險村15 - customize tooltips with data attribute
系列文
冒險村-30 Day Ruby on Rails Tips Challenge30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言