在許多情況下,會需要統計一對多關聯的資料數量。舉例來說像是 User has_many
Post。這時如果要統計該 user 擁有的 post 數量時,若直接使用 User.posts.size
則每次讀取頁面都會重新再統計一次,影響網站效能,而 ActiveRecord's counter_cache 可以來幫助解決這個問題。
# 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
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_counters
為ActiveRecord::CounterCache
method
也可以將重新計算的 counter 的程式寫成 rake 來執行比放在 migration 內來的適合
# 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
。
再來在 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
,數量也都會自動更新!