Counter Cache trong rails
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ẽ ...
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