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