Counter cache cho quan hệ many-to-many
Bài đăng này đã không được cập nhật trong 6 năm
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
All rights reserved