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 ...
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à update và destroy
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à update và destroy.
- 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, after và around. 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, after và around. 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_save và after_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_save và after_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.