Có nên sử dụng counter cache cho quan hệ many to many trong Rails ???



  • Counter cache giúp tăng performance bằng cách tránh việc query N+1. Tuy nhiên chúng ta có nên sử dụng nó với quan hệ many to many trong rails không? Thông qua bài viết này tôi sẽ trả lời cho câu hỏi trên.

    Kịch bản đưa ra chúng ta có Post và Tag có quan hệ many to many thông qua Tagging. Post sẽ lưu lại số lượng Tag của nó:

    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
    

    Chú ý. dependent: :destroy khai báo trong Post sẽ xóa hết Tagging chứ không phải Tag của Post. tags_count sẽ tự động update mỗi khi post được tạo, update hoặc delete.

    Multiple SQL updates

    Counter cache làm việc bằng cách chạy SQL update mỗi khi một quan hệ được tạo hoặc xóa.
    Khi mà một Post mới được khởi tạo với một vài Tag (ví dụ ở đây là 99 Tag), Active Record sẽ chạy các câu query như dưới đây:

    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
    -- ======================
    

    Vậy đối với những Post có hàng chục hoặc hàng trăm Tag sẽ sinh ra rất nhiều câu query khiến việc thực thi trở nên chậm đi.

    Thay thế counter cache bằng callbacks

    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
    

    Bằng cách thay thế taggings table với post_tags join table (bạn cũng có thể đổi tên tags_counts thành total_tags để tránh việc tự động sử dụng counter cache).

    Từ giờ chúng ta sử dụng before_save để update giá trị của total_count. Với việc đếm tag_ids có thể tránh được count query trên Tag. Hãy xem query khi mà tạo một Post với nhiều Tag

    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
    -- ======================
    

    Khi xóa Post có nhiều Tag:

    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
    -- ======================
    

    Tổng kết

    Thông qua ví dụ trên chắc hẳn các bạn đã có câu trả lời cho câu hỏi và đầu bài viết đã đưa ra. Có thể counter cache là phương án tốt nhất khi sử đụng cho quan hệ one to many nhưng có vẻ lại không thích hợp khi bạn có quan hệ many to many.
    Nguồn: Viblo


Hãy đăng nhập để trả lời
 

Có vẻ như bạn đã mất kết nối tới LaptrinhX, vui lòng đợi một lúc để chúng tôi thử kết nối lại.