Counter cache cho quan hệ many-to-many
Giới thiệu qua về Counter cache trong Active Record. Có 1 ví dụ như sau: Giả sử chúng ta có các Post có Tag thông qua Taggings. Posts có sử dụng counter cache Tag: class Tagging # FIELDS: post_id, tag_id belongs_to :tag belongs_to :post , counter_cache : :tags_count # ...
Giới thiệu qua về Counter cache trong Active Record.
Có 1 ví dụ như sau:
- Giả sử chúng ta có các Post có Tag thông qua Taggings. Posts có sử dụng counter cache Tag:
class Tagging # FIELDS: post_id, tag_id belongs_to :tag belongs_to :post, counter_cache: :tags_count # updates tags_count in Post end class Tag # FIELDS: title has_many :taggings has_many :posts, through: :taggings, dependent: :destroy end class Post # FIELDS: content, tags_count has_many :taggings has_many :tags, through: :taggings, dependent: :destroy end
Trường tags_count sẽ được cập nhật tự động bất cứ lúc nào một Post được tạo, cập nhật hoặc xóa. Nhưng tin xấu là counter cache hoạt động bằng cách thực hiện cập nhật SQL mỗi khi một Post mới được tạo hoặc xóa.
Ví dụ ở đây 1 Post mới có 99 tags thì SQL sẽ hoạt động như sau:
INSERT INTO posts (content) VALUES ("Lorem ipsum") INSERT INTO taggings (post_id, tag_id) VALUES (1, 1); UPDATE posts SET tags_count = tags_count + 1 WHERE posts.id = 1; INSERT INTO taggings (post_id, tag_id) VALUES (1, 2); UPDATE posts SET tags_count = tags_count + 1 WHERE posts.id = 1; ... INSERT INTO taggings (post_id, tag_id) VALUES (1, 99); UPDATE posts SET tags_count = tags_count + 1 WHERE posts.id = 1; -- ====================== -- TOTAL QUERIES: 2×N + 1 -- ======================
Khi vào destroy Post mới tạo kia:
SELECT * FROM posts INNER JOIN posts_tags ON posts.id = posts_tags.post_id WHERE posts_tags.tag_id = 1 DELETE FROM posts_tags WHERE posts_tags.tag_id = 1 AND posts_tags.post_id = 1 UPDATE posts SET tags_count = tags_count - 1 WHERE posts.id = 1; DELETE FROM posts_tags WHERE posts_tags.tag_id = 1 AND posts_tags.post_id = 2 UPDATE posts SET tags_count = tags_count - 1 WHERE posts.id = 2; ... DELETE FROM posts_tags WHERE posts_tags.tag_id = 1 AND posts_tags.post_id = 99 UPDATE posts SET tags_count = tags_count - 1 WHERE posts.id = 99; DELETE FROM tags WHERE tags.id = 1 -- ====================== -- TOTAL QUERIES: 2×N + 2 -- ======================
Điều này sẽ bất lợi nếu có hàng trăm bản ghi như vậy
Thử bằng việc sử dụng callback:
class Post # FIELDS: content, total_tags has_and_belongs_to_many :tags before_save :update_total_tags def update_total_tags self.total_tags = tag_ids.count end end class Tag # FIELDS: title has_and_belongs_to_many :posts before_destroy :update_posts def update_posts Post.where(id: post_ids).update_all('total_tags = total_tags - 1') end end
Tôi cũng đã thay thế column tags_count bằng column total_tags để tránh counter_cache truy cập tự động
Khi tạo Post có 99 tags
INSERT INTO posts (content, total_tags) VALUES ("Lorem ipsum", 99) INSERT INTO posts_tags (post_id, tag_id) VALUES (1, 1); INSERT INTO posts_tags (post_id, tag_id) VALUES (1, 2); ... INSERT INTO posts_tags (post_id, tag_id) VALUES (1, 99); -- ====================== -- TOTAL QUERIES: N + 1 -- ======================
SELECT posts.id FROM posts INNER JOIN posts_tags ON posts.id = posts_tags.post_id WHERE posts_tags.tag_id = 1 UPDATE posts SET total_tags = total_tags - 1 WHERE posts.id IN (1, 2, ..., 99) DELETE FROM posts_tags WHERE posts_tags.tag_id = 1 DELETE FROM tags WHERE tags.id = 1 -- ====================== -- TOTAL QUERIES: 4 -- ======================
Hy vọng có ích cho các bạn