12/08/2018, 16:42

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

0