12/08/2018, 18:04

Theo dõi thay đổi trong Active record transaction và after_commit callback

Sau khi sử dụng các ActiveRecord Transaction trong ứng dụng rails, mình nhận thấy một số callbacks after_commit đã không được gọi. Vậy vấn đề này có ảnh hưởng gì đến quá trình hoạt động của ứng dụng? Chúng ta cùng nhau đi qua các nội dung trong bài này nhé. Sau khi lục vài trang trên google, vài ...

Sau khi sử dụng các ActiveRecord Transaction trong ứng dụng rails, mình nhận thấy một số callbacks after_commit đã không được gọi. Vậy vấn đề này có ảnh hưởng gì đến quá trình hoạt động của ứng dụng? Chúng ta cùng nhau đi qua các nội dung trong bài này nhé.

Sau khi lục vài trang trên google, vài bài post trên stackoverflow, mình nhận thấy là vấn đề này chỉ xảy ra với transaction có lưu record nhiều lần. Từ đó mình thấy là việc sử dụng sai after_commit có thể sinh thêm bugs khó kiểm soát, đặc biệt trong các dự án đã hoàn thiện.

Cùng nhìn một ví dụ sau để hiểu rõ hơn vấn đề này.

Giả sử chúng ta muốn thông báo cho quản trị viên biết tất cả các thay đổi đã xảy ra đối với mỗi một model trong ứng dụng của chúng ta, một vài trong số thay đổi đó xảy ra trong DB Transaction

class SuperImportantThingsController < ApplicationController
  # ...

  def update
    thing = SuperImportantThing.find(params[:id])

    SuperImportantThing.transaction do
      if thing.update(thing_params)
        thing.update! last_updater: current_user
        redirect_to thing_path(thing)
      else
        render :edit
      end
    end
  end

Giả sử trong trường hợp chúng ta không muốn gửi email khi transaction có lỗi vì lý do nào đó, chúng ta chọn cách chỉ gửi email trong một callback after_commit, như sau:

class SuperImportantThing < ActiveRecord::Base
  after_commit :notify_update, on: :update

  private

  def notify_update
    return if previous_changes.empty?
    SuperImportantThingMailer.update_email(self.id, previous_changes).deliver_later
  end
end

Có thể các bạn chưa biết, phương thức previous_changes trong ActiveRecord trả về thay đổi tới model trước khi nó được lưu.

Đó là một phần của module theo dõi trong ActiveRecord ActiveModel::Dirty. Module này giúp chúng ta theo dõi các bản ghi trong ActiveRecord, nó có nhiều phương thức khá hữu ích để sử dụng.

Trong một ứng dụng, chủ yếu các thay đổi duy nhất mà các quản trị viên được thông báo là một thay đổi đối với trường hợp last_updater - cập nhật cuối cùng.

Vậy thì điều gì đã xảy ra?? Họ không thể biết tất cả quá trình thay đổi đã xảy ra với các object trong ứng dụng.

Lời giải thích khá đơn giản, rằng phương thức previous_changes được thiết lập lại mỗi khi chúng ta lưu object, không chỉ khi Transaction kết thúc.

Chính vì thế, khi callback method after_commit được thực hiện, phương thức previous_changes chỉ chứa các thay đổi gần đây nhất, thay vì tất cả các thay đổi.

Bạn có thể test thử điều này bằng cách kiểm tra trả về của previous_changes sau lần cập nhật đầu và lần cập nhật thứ hai.

Sau các phần trên, chắc hẳn các bạn đã hiểu tính năng theo dõi bẩn trong ActiveRecord và Transaction.

Giải pháp - gem ArTransactionChanges

Gem này hỗ trợ lưu tất cả các attributes thay đổi trong active record object trong suốt quá trình transaction xảy ra, và nó cũng có thể sử dụng trong cả after_commit callbacks.

Sử dụng previous_changes trong after_commit callback sẽ chỉ trả về các trường thay đổi từ lần lưu cuối cùng, có nghĩa là nó không lưu tất cả các thay đổi được áp dụng cho bản ghi trong transaction khi mà bản ghi đó đã được lưu nhiều lần.

Gem ArTransactionChanges sẽ giúp chúng ta giải quyết vấn đề này.

Cách sử dụng cực kỳ đơn giản:

Cài đặt

Trong Gemfile:

gem "ar_transaction_changes"
bundle

Hoặc cài đặt trực tiếp bằng lệnh:

gem install ar_transaction_changes

Sử dụng

class User < ActiveRecord::Base
  include ArTransactionChanges

  after_commit :print_transaction_changes

  def print_transaction_changes
    transaction_changed_attributes.each do |name, old_value|
      puts "attribute #{name}: #{old_value.inspect} -> #{send(name).inspect}"
    end
  end
end

Lưu ý: Gem này có thể bị xng đột với một số gem có thay đổi phương thức active_record#write_attribute, như gem globalize

để đồng thời dùng 2 gem này thì giải pháp là require gem này trước:

gem 'ar_transaction_changes', require: false
gem 'globalize', require: ['ar_transaction_changes', 'globalize']

Hy vọng bài viết này có thể bổ ích cho ứng dụng của bạn. Cảm ơn đã đọc bài viết này             </div>
            
            <div class=

0