Callback trong Rails hoạt động như thế nào?

Với bất cứ lập trình viên nào hẳn từ khóa Callback cũng đã quá quen thuộc, nó xuất hiện ở gần như mọi ngôn ngữ lập trình, và với Rails cũng vậy, khi bạn sử dụng các phương thức như before_create, after_save, ... chính là đang sử dụng callback trong ứng dụng của mình. Tuy nhiên không phải ai cũng hiểu rõ được rằng các hàm callback này được tạo ra như thế nào?, Khi nào thì callback được gọi đến?, Có những loại callback nào?, ... thì trong bài này chúng ta sẽ cùng đi làm rõ từng vấn đề một nhé.

1. Callback là gì ?

Rất đơn giản đúng như tên gọi của nó Callback nghĩa là gọi lại, Callback cho phép bạn thực thi các phương thức đã khai báo trước đó một cách tự động trước khi (hoặc sau khi, hoặc cả trước và sau khi) một đoạn code khác trong chương trình được chạy. Một số callback phổ biến của Active Record được sử dụng before_validation, after_validation, before_save, around_save, before_create, around_create,... còn rất nhiều các phương thức khác nữa có thể xem tại Active Record Callback

2. Khi nào cần sử dụng Callback.

Cách tốt nhất và nhanh nhất để hiểu một vấn đề đó là xem xét một ví dụ: trong EventsController đang xử lý việc sửa và xóa một sự kiện, có 2 action là updatedestroy

class EventsController < ApplicationController
  def update
    @event = current_user.events.find_by id: params[:id]
    
    if @event.update_attributes event_params
      // Do somethings
    else
      // Do somethings
    end
  end

  def destroy
    @event = current_user.events.find_by id: params[:id]
    
    if @event.destroy
      // Do somethings
    else
      // Do somethings
    end
  end
end

Cả 2 action ta thấy đều phải thực hiện cùng một công việc trước khi update hoặc xóa đi là tìm sự kiện đó trong bảng events dựa vào params id được truyền vào sau đó gán sự kiện tìm được cho biến @event. Thay vì việc bị trùng lặp code như vậy ta có thể xử lý như sau với Callback:

class EventsController < ApplicationController
  before_action :find_event, only: [:update, :destroy]
  
  def update
    if @event.update_attributes event_params
      // Do somethings
    else
      // Do somethings
    end
  end

  def destroy
    if @event.destroy
      // Do somethings
    else
      // Do somethings
    end
  end
  
  private
  
  def find_event
    @event = current_user.events.find_by id: params[:id]
  end
end

Với đoạn code trên ta đã thực hiện một số thao tác:

  • Khai báo phương thức find_event để tìm ra sự kiện dựa vào param id truyền vào.
  • Dùng callback before_action gọi đến find_event đã được khai báo, và chỉ callback đối với 2 phương thức là updatedestroy.
  • So với code cũ có thể dài hơn nhưng nhìn rất gọn gàng và clean, chương trình cũng rất dễ để sửa đổi hoặc fix lỗi, nếu như có vấn đề với việc tìm event ta chỉ cần xem xét và sửa trong hàm find_event thay vì phải đi sửa ở tất cả các action như trước.

3. Cơ chế hoạt động của callback trong Rails

3.1. ActiveSupport là gì ?

Đến đây, chúng ta sẽ cùng xem xét kĩ càng hơn trong source code của Rails để hiểu cặn kẽ về Callback.

  • Các phương thức callback trong Active Record không tự ngẫu nhiên mà có, hay các callback trong ActionPack cũng vậy, hãy thử hình dung giả sử tất cả các module trong rails đều cung cấp các phương thức callback hoặc khi ta muốn tự tạo các callback riêng cho mình thì một câu hỏi lớn được đặt ra là Nên bằt đầu từ đâu để tạo cho mình một Callback?, bản thân Active Record hay ActionPack cũng vậy, nó cũng đã tự hỏi chính mình là tôi phải đi đâu để tìm các công cụ xây nên những Callback cho chính mình. Câu trả lời chính là ActiveSupport, đây là một module trong Rails cung cấp các công cụ cần thiết nhất cho phép các module khác sau khi include ActiveSupport có thể tạo callback riêng cho nó.
  • Trong ActiveSupport khai báo 3 phương thức quan trọng và là cốt lõi nhất cho phép tạo nên một callback đó là:
    • define_callbacks : Định nghĩa ra các sự kiện trong một chu kì hoạt động của đối tượng sẽ được hỗ trợ callback, vdu như save, destroy, update, ... là các sự kiện khá phổ biến, với define_callback ta có thể tự custom một callback cho riêng mình. (Hàm define_callback)
    • set_callback: Thiết lập các instance method hoặc proc, ... để sẵn sàng được gọi lại, có thể hiểu là install các phương thức đã khai báo trước đó và sẵn sàng đợi cho đến khi được callback. ( Hàm set_callback)
    • run_callback: Chạy các phương thức đã được install trước đó bởi set_callback vào một thời điểm thích hợp. ( Hàm run_callback)
  • Có 3 loại callback được hỗ trợ đó là before, afteraround. before callback chạy trước một sự kiện, after callback chạy sau khi sự kiện xảy ra và around callback chạy cả trước và sau khi sự kiện xảy ra.

3.2. ActiveRecord dùng ActiveSupport để tạo Callback như thế nào?

Tới đây ta đã biết được công cụ cho phép các module khác tạo ra Callback đó là ActiveSupport. Nhưng có vẻ vẫn còn mơ hồ nên hãy cùng xem xét cụ thể hơn nữa các Callback của active record được tạo ra như thế nào? Đầu tiên, xem xét ActiveRecord::Callbacks module:

module Callbacks
    extend ActiveSupport::Concern

    CALLBACKS = [
      :after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
      :before_save, :around_save, :after_save, :before_create, :around_create,
      :after_create, :before_update, :around_update, :after_update,
      :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
    ]

    included do
      extend ActiveModel::Callbacks
      include ActiveModel::Validations::Callbacks

      define_model_callbacks :initialize, :find, :touch, :only => :after
      define_model_callbacks :save, :create, :update, :destroy
    end

    def destroy #:nodoc:
      run_callbacks(:destroy) { super }
    end

    def touch(*) #:nodoc:
      run_callbacks(:touch) { super }
    end

  private

    def create_or_update #:nodoc:
      run_callbacks(:save) { super }
    end

    def create #:nodoc:
      run_callbacks(:create) { super }
    end

    def update(*) #:nodoc:
      run_callbacks(:update) { super }
    end
  end
end
  • Có thể thấy phương thức define_model_callbacks truyền vào các tham số initialize, :find, :touch, :save, :create, :update, :destroy, đây có vẻ là nơi các callback được tạo ra.
  • Khai báo các phương thức create, update, destroy, ... mỗi phương thức đều gọi đến run_callback có nghĩa là mỗi khi các phương thức này được kích hoạt thì nó sẽ gọi đến callback trước, chạy xong callback mới chạy đến các lệnh bên trong. Tiếp theo, xem xét kĩ hơn một chút bên trong define_model_callbacks có gì:
def define_model_callbacks(*callbacks)
      options = callbacks.extract_options!
      options = {
         :terminator => "result == false",
         :scope => [:kind, :name],
         :only => [:before, :around, :after]
      }.merge(options)

      types   = Array.wrap(options.delete(:only))

      callbacks.each do |callback|
        define_callbacks(callback, options)

        types.each do |type|
          send("_define_#{type}_model_callback", self, callback)
        end
      end
    end

    def _define_before_model_callback(klass, callback) #:nodoc:
      klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
        def self.before_#{callback}(*args, &block)
          set_callback(:#{callback}, :before, *args, &block)
        end
      CALLBACK
    end

    def _define_around_model_callback(klass, callback) #:nodoc:
      klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
        def self.around_#{callback}(*args, &block)
          set_callback(:#{callback}, :around, *args, &block)
        end
      CALLBACK
    end

    def _define_after_model_callback(klass, callback) #:nodoc:
      klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
        def self.after_#{callback}(*args, &block)
          options = args.extract_options!
          options[:prepend] = true
          options[:if] = Array.wrap(options[:if]) << "!halted && value != false"
          set_callback(:#{callback}, :after, *(args << options), &block)
        end
      CALLBACK
    end
  end
end
  • Ta hãy chú ý đến
callbacks.each do |callback|
  define_callbacks(callback, options)

  types.each do |type|
    send("_define_#{type}_model_callback", self, callback)
  end
end
  • callbacks ở đây là một mảng các phương thức được truyển vào hàm define_model_callbacks, cụ thể là initialize, find, touch, save, create, update, destroy. Tại đây define_callback sẽ lần lượt định nghĩa từng phương thức này sẽ là những phương thức được hỗ trợ callback
  • Còn cụ thể hàm callback mà ta vẫn thường sử dụng được tạo ra bởi send("_define_#{type}_model_callback", self, callback), type bao gồm before, afteraround. Lấy vị dụ với save, sau khi được define bởi hàm define_callback, sẽ có 3 phương thức tương ứng với 3 types được tạo ra cho save
def _define_before_model_callback(klass, callback) #:nodoc:
  klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
    def self.before_#{callback}(*args, &block)
      set_callback(:#{callback}, :before, *args, &block)
    end
  CALLBACK
end

def _define_around_model_callback(klass, callback) #:nodoc:
  klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
    def self.around_#{callback}(*args, &block)
      set_callback(:#{callback}, :around, *args, &block)
    end
  CALLBACK
end

def _define_after_model_callback(klass, callback) #:nodoc:
  klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
    def self.after_#{callback}(*args, &block)
      options = args.extract_options!
      options[:prepend] = true
      options[:if] = Array.wrap(options[:if]) << "!halted && value != false"
      set_callback(:#{callback}, :after, *(args << options), &block)
    end
  CALLBACK
end
  • Lần lượt 3 hàm trên sẽ tạo ra 3 phương thức before_save, around_saveafter_save, để ý bên trong mỗi hàm trên còn có gọi đến set_callback, điều này có nghĩa là phương thức bạn truyền vào before_save, around_saveafter_save chính là truyền vào cho hàm set_callback này.
  • Trên đây là toàn bộ vòng đời của một callback, được tạo ra như thế nào, thiết lập các phương thức ra sao, khi nào thì chạy, hi vọng qua bài viết này mọi người sẽ có một cái nhìn rõ hơn về callback trong Rails.

4. Tài liệu tham khảo

  1. http://guides.rubyonrails.org/active_record_callbacks.html
  2. https://github.com/rails/rails/blob/master/activesupport/lib/active_support/callbacks.rb#L94