Counter Cache trong rails
Bài đăng này đã không được cập nhật trong 7 năm
I. Giới thiệu
Bạn đã bao giờ đếm số lượng từ một ActiveRecordRelation ở trong rails và nhìn vào console log, bạn thấy vấn đề n+1 queries đập vào mắt. Bạn đã khác phục nó như thế nào, bạn có thể viết 1 scope loằng ngoằng cho cái việc đó hoặc sử dụng EagerLoading, nhưng nếu bạn đoán được nó sẽ xảy ra trong tương lai ở thời điểm bạn mới bắt đầu xây dựng các table ở trong dự án, sau đây tôi sẽ chỉ cho bạn cách dùng counter cache như thế nào.
II. Cách sử dụng
Ta sẽ tạo 1 ứng dụng mới để thử:
Giả sử Project của chúng ta đã có 2 thực thể là project
và task
Quan hệ của chúng là 1 project thì có nhiều task
class Project < ApplicationRecord
has_many :tasks
end
Ta có hàm index trong ProjectsController như sau:
def index
@projects = Project.all
end
Trong file projects/index.html.erb
ta đếm số lượng task
của mỗi project
như sau:
<% @projects.each do |project| %>
<tr>
<td><%= project.name %></td>
<td><%= pluralize(project.tasks.size, 'task') %></td>
</tr>
<% end %>
Và sau đó chúng ta truy cập vào trang: http://localhost:3000/projects
Và hãy nhìn vào rails console log:
Started GET "/projects" for ::1 at 2016-03-16 23:47:08 -0700
ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"
Processing by ProjectsController#index as HTML
Project Load (0.1ms) SELECT "projects".* FROM "projects"
(0.2ms) SELECT COUNT(*) FROM "tasks" WHERE "tasks"."project_id" = ? [["project_id", 1]]
(0.1ms) SELECT COUNT(*) FROM "tasks" WHERE "tasks"."project_id" = ? [["project_id", 2]]
(0.1ms) SELECT COUNT(*) FROM "tasks" WHERE "tasks"."project_id" = ? [["project_id", 3]]
Rendered projects/index.html.erb within layouts/application (25.4ms)
Completed 200 OK in 202ms (Views: 190.5ms | ActiveRecord: 1.3ms)
Và vấn đề n+1 queries đã xảy ra.
Chúng ta muốn đếm số lượng trong 1 câu truy vấn duy nhất, sử dụng EagerLoading có thể là 1 cách. Nó sẽ lấy được nhiều cột hơn là đếm. Điều này sẽ sử dụng băng thông và thời gian nhiều hơn. Nhưng cách sau sẽ giảm điều đó.
Chúng ta thêm migration như sau :
rails g migration add_tasks_count
Sửa file migration vừa thêm thành:
class AddTasksCount < ActiveRecord::Migration[5.0]
def change
add_column :projects, :tasks_count, :integer, default: 0
end
end
Và chạy lệnh migrate
Sau đó tạo file rake cho counter cache, thêm file task_counter.rake
:
desc 'Counter cache for project has many tasks'
task task_counter: :environment do
Project.reset_column_information
Project.pluck(:id).find_each do |p|
Project.reset_counters p.id, :tasks
end
end
Vùa xong là việc tạo ra việc đếm task của các project có trong database. Phương thức reset_counters tránh được lỗi readonly nếu bạn sử dụng phương thức update_attributes, Điều này làm cho các project cũ trong database có thể cập nhật được đúng số lượng task.
Sau đó chúng ta chạy rake task_counter
.
Sau đó chúng ta có thể bật console lên kiểm tra xem tasks_count đã được cập nhật vào trong database chưa:
p = Project.first
Project Load (0.2ms) SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Project id: 1, name: "Wealth Building", created_at: "2016-03-16 19:07:26", updated_at: "2016-03-16 20:55:29", tasks_count: 2>
> Project.last
Project Load (0.2ms) SELECT "projects".* FROM "projects" ORDER BY "projects"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Project id: 3, name: "Cooking", created_at: "2016-03-16 20:06:26", updated_at: "2016-03-16 20:06:26", tasks_count: 0>
Nó sẽ ra kết quả tương tự bên trên
Ở trong file schema.rb
bạn có thể nhìn thấy cột tasks_count
đã được thêm vào bảng projects
create_table "projects", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "tasks_count", default: 0
end
Sau đó chúng ta thử áp dụng ở trên views, như sau:
<td><%= pluralize(project.tasks_count, 'task') %></td>
Và hãy F5 trình duyệt rồi nhìn vào rails console log, bạn sẽ thấy:
Started GET "/projects" for ::1 at 2016-03-16 24:00:00 -0700
Processing by ProjectsController#index as HTML
Project Load (0.1ms) SELECT "projects".* FROM "projects"
Rendered projects/index.html.erb within layouts/application (1.6ms)
Completed 200 OK in 48ms (Views: 45.6ms | ActiveRecord: 0.1ms)
Chúng ta sẽ thử thêm 1 task vào trong 1 project ở trong rails console:
Project.last
Project Load (0.2ms) SELECT "projects".* FROM "projects" ORDER BY "projects"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Project id: 3, name: "Cooking", created_at: "2016-03-16 20:06:26", updated_at: "2016-03-16 20:06:26", tasks_count: 0>
> p.tasks.create(name: 'Add counter cache')
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "tasks" ("name", "created_at", "updated_at", "project_id") VALUES (?, ?, ?, ?) [["name", "Add counter cache"], ["created_at", 2016-03-16 21:09:25 UTC], ["updated_at", 2016-03-16 21:09:25 UTC], ["project_id", 1]]
(0.7ms) commit transaction
=> #<Task id: 11, name: "Add counter cache", complete: nil, created_at: "2016-03-16 21:09:25", updated_at: "2016-03-16 21:09:25", project_id: 1, priority: nil>
Reload lại trình duyệt mà xem, task của project cuối sẽ không tăng đâu, bây giờ chúng ta phải fix lỗi đó Trong model Task, chúng ta sửa lại thành
belongs_to :project, counter_cache: true
Và chúng ta thử lại việc ở trên ở trong console:
f = Project.first
Project Load (0.1ms) SELECT "projects".* FROM "projects" ORDER BY "projects"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Project id: 1, name: "Wealth Building", created_at: "2016-03-16 19:07:26", updated_at: "2016-03-16 20:55:29", tasks_count: 2>
f.tasks.map(&:id)
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" = ? [["project_id", 1]]
=> [1, 2, 11]
> f.name
=> "Wealth Building"
> Task.destroy(1)
Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
(0.0ms) begin transaction
SQL (0.6ms) DELETE FROM "tasks" WHERE "tasks"."id" = ? [["id", 1]]
(0.5ms) commit transaction
=> #<Task id: 1, name: "Get rich quick", complete: false, created_at: "2016-03-16 19:07:26", updated_at: "2016-03-16 19:07:26", project_id: 1, priority: 4>
Reload lại trình duyệt, nó sẽ ra kết quả đúng cho bạn .
All rights reserved