Tìm hiểu về callback function trong Rails
Bài đăng này đã không được cập nhật trong 4 năm
1. Active record callbacks là gì? Tại sao phải dùng nó?
Callback là các method được gọi tại 1 khoảnh khắc nhất định trong vòng đời của 1 ActiveRecord Object. Với callbacks ta có thể xử lý logic ở mỗi thời điểm mà object được created, saved, updated, deleted, validated, hoặc loaded từ database.
Tại sao phải dùng nó? Chúng ta sẽ thử với 1 bài toán thực tế. Giả sử khi user đăng ký tài khoản, họ sẽ nhập vào form đăng ký, tên của mình như thế này: Tuy nhiên, trong cơ sở dữ liệu, chúng ta muốn thuộc tính "name" luôn được lưu ở định dạng này:
Hoang Trong Hieu
Để giải quyết vấn đề này, ta sử dụng 1 hàm callback để định dạng lại thuộc tính name trước khi nó được lưu vào cơ sở dữ liệu như sau:
class User < ApplicationRecord
before_save :normalize_name
private
def normalize_name
self.name = name.downcase.titleize
end
end
Và xem cách nó chạy:
$ User.create name: "hoang trong hieu"
(0.1ms) begin transaction
"Vao callback before_save"
User Create (0.3ms) INSERT INTO "users" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "Hoang Trong Hieu"], ["created_at", "2019-09-12 02:40:58.419078"], ["updated_at", "2019-09-12 02:40:58.419078"]]
(60.5ms) commit transaction
=> #<User id: 19, name: "Hoang Trong Hieu", location: nil, age: nil, image_path: nil, created_at: "2019-09-12 02:40:58", updated_at: "2019-09-19
$ User.last.name
=> "Hoang Trong Hieu"
2. Active record callbacks dùng dư nào?
Để dùng các callbacks, ta phải gọi và đăng ký chúng dưới dạng class_method.
class User < ApplicationRecord
before_validation { p "Vào callback before_validation rồi nè" }
end
3 Các loại callbacks có thể sử dụng.
3.1 Creating an Object
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
3.2 Updating an Object
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit/after_rollback
3.3 Destroying an Object
before_destroy
around_destroy
after_destroy
after_commit/after_rollback
Các callbacks bản chất là các class_method
, mỗi khi khởi tạo, chúng sẽ được xếp vào 1 hàng đợi (gọi là chuỗi callbacks) và thực hiện theo 1 thứ tự nhất định. Ví dụ:
class User < ApplicationRecord
before_save { p " Callback số before_save số 1 " }
before_validation { p " Callback before_validation số 1 " }
before_validation { p " Callback before_validation số 2 " }
before_save { p " Callback số before_save số 2 " }
end
Mình cố tình sắp xếp các callback hơi lộn xộn, và cùng xem thứ tự thực hiện của nó:
User.create name: "Hieu dep trai qua", age: 50
(0.0ms) begin transaction
" Callback before_validation số 1 "
" Callback before_validation số 2 "
" Callback số before_save số 1 "
" Callback số before_save số 2 "
User Create (0.3ms) INSERT INTO "users" ("name", "age", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Hieu dep trai qua"], ["age", 50], ["created_at", "2019-09-10 07:04:47.732217"], ["updated_at", "2019-09-10 07:04:47.732217"]]
(86.2ms) commit transaction
=> #<User id: 6, name: "Hieu dep trai qua", location: nil, age: 50, created_at: "2019-09-10 07:04:47", updated_at: "2019-09-10 07:04:47">
Như vậy các bạn có thể thấy, nếu callback là cùng loại thì thứ tự thực hiện của chúng phụ thuộc vào thứ tự xuất hiện trong code. Nếu callback là khác loại thì thứ tự thực hiện của chúng giống như mình đã nêu ở phần 3 ( như ví dụ ở trên thì ta có thể thấy before_save luôn chạy sau before_validation).
3.4 after_initialize and after_find
Callback after_find
được gọi khi ActiveRecord load 1 bản ghi từ database
User.find 2
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
" Vào callback after_find rồi nè "
Callback after_initialize
được gọi khi 1 ActiveRecord object được khởi tạo( hàm new
) hoặc khi ActiveRecord load 1 bản ghi từ database. Callback này tạo ra để tránh việc bạn phải ghi đề lại method initialize
của model.
User.find 2
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
" Vào callback after_find rồi nè "
" Vào callback after_initialize rồi nè "
User.new
" Vào callback after_initialize rồi nè "
=> #<User id: nil, name: nil, location: nil, age: nil, image_path: nil, created_at: nil, updated_at: nil>
after_find
luôn được load trước after_initialize
nếu cả 2 cùng được đăng ký.
3.5 after_touch
Chỉ được gọi ra khi có 1 Active Record objects được touch.
User.find_by(id: 2).touch
4. Các method giúp gọi callbacks
Đây là danh sách các method giúp chúng ta gọi vào callback.
create
create!
destroy
destroy!
destroy_all
save
save!
save(validate: false)
toggle!
touch
update_attribute
update
update!
valid?
Và after_find
sẽ được gọi bởi các method
sau
all
first
find
find_by
find_by_*
find_by_*!
find_by_sql
last
5. Các method không gọi callbacks (skip callback)
Với các method dưới đây, callback sẽ không được gọi
decrement!
decrement_counter
delete
delete_all
increment!
increment_counter
update_column
update_columns
update_all
update_counters
Trong đó, after_find
và after_initialize
không thể bị skip vì 2 callback này được gọi khi dữ liệu được load từ DB.
Ngoài ra để skip 1 callback cụ thể, ta có thể đặt trạng thái cho callback.
Ví dụ, ta muốn có 1 callback before_validation :show_age
như sau.
class User < ApplicationRecord
before_validation :show_age
private
def show_age
p "User is #{age} years old"
end
end
##
User.first.update name: "Hieu dep trai qua 2", age: 50
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
(0.1ms) begin transaction
"User is 50 years old"
(0.0ms) commit transaction
=> true
Ta có thể skip callback nói trên với điều kiện tuổi của user lớn hơn 30 bằng cách sau:
class User < ApplicationRecord
before_validation :show_age, unless: ->{ self.age > 30 }
private
def show_age
p "User is #{age} years old"
end
end
User.first.update name: "Hieu dep trai qua 2", age: 50
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
(0.1ms) begin transaction
"User is 50 years old"
(0.0ms) commit transaction
=> true
6. Halting Execution
Toàn bộ chuỗi callback được gói trong 1 transaction. Nếu có 1 callback trong chuỗi raise exception, toàn bộ chuỗi sẽ dừng lại và transaction sẽ bị rollback. Ví dụ:
class User < ApplicationRecord
before_validation { p "Vào trong callback before_validation" }
before_validation { a }
end
######
User.create name: "Hieu dep trai", age: 50
(0.0ms) begin transaction
" Vào trong callback before_validation"
(0.0ms) rollback transaction
Traceback (most recent call last):
2: from (irb):39
1: from app/models/user.rb:8:in `block in <class:User>'
NameError (undefined local variable or method `a' for #<User:0x00005571eec85e00>)
Ở trong ví dụ nói trên, do a
không được định nghĩa, nên callback before_validation { a }
đã trả về 1 exception
và transaction
bị rollback
.
Để chủ động dừng chuỗi callback và tạo ra 1 rollback
, ta có thể sử dụng throw :abort
class User < ApplicationRecord
before_validation { p "Vào trong callback before_validation" }
before_validation { throw :abort }
end
###############
User.create name: "Hieu dep trai", age: 50
(0.1ms) begin transaction
" Vào trong callback before_validation"
(0.0ms) rollback transaction
=> #<User id: nil, name: "Hieu dep trai", location: nil, age: 50, created_at: nil, updated_at: nil>
7. Relational callback
Callback được thực hiện kể cả qua relationship giữa các model. Ví dụ với 1 user
có nhiều articles
, khi xóa user
thì các articles
thuộc về nó cũng bị xóa theo. Vì vậy , các callback thuộc về bảng Article cũng sẽ được gọi.
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
after_destroy :log_destroy_action
def log_destroy_action
puts 'Article destroyed'
end
end
>> user = User.first
=> #<User id: 1>
>> user.articles.create!
=> #<Article id: 1, user_id: 1>
>> user.destroy
Article destroyed
=> #<User id: 1>
8. Conditional callback
Có thể đặt trạng thái để xảy ra callback với symbol
:if
và :unless
.
Value
truyền vào sau key :if
hoặc :unless
có thể là Symbol
hoặc Proc
.
class User < ApplicationRecord
before_validation :show_age, if: :is_teen?
private
def is_teen?
age < 20
end
end
class User < ApplicationRecord
before_validation :show_age, if: Proc.new { age < 20 }
end
Cho trường hợp cần sử dụng nhiều trạng thái cho callback,
chúng ta có thể truyền mảng các Symbol
, Proc
vào sau key :if
(tương tự với :unless
)
class User < ApplicationRecord
before_validation :show_age, if: [:is_teen?, :is_integer?, Proc.new { age > 10 }]
private
def is_teen?
age < 20
end
def is_integer?
age.integer?
end
end
Callback sẽ được chạy khi tất cả các phần tử trong mảng đứng sau :if
đều trả về giá trị true
( tương tự các phần tử trong mảng đứng sau:unless
cần trả về giá trị false
) .
Ngoài ra, Rails cũng tạo ra cú pháp để mix :unless
và :if
, tuy nhiên mình không thích dùng cú pháp này vì nó khiến cho code không được clean.
class User < ApplicationRecord
before_validation :show_age, if: [:is_teen?, :is_integer?], unless: Proc.new { age < 10 }
private
def is_teen?
age < 20
end
def is_integer?
age.integer?
end
end
Với cú pháp mix nói trên, callback sẽ chạy nếu tất cả phần tử trong mảng sau :if
trả về giá trị true VÀ tất cả phần tử trong mảng sau :unless
trả về giá trị false
. ( Dở hơi nhỉ . Với cá nhân mình thì mình sẽ chỉ dùng 1 trong 2 key :if
hoặc :unless
)
Đó là toàn bộ bài report của mình về vấn đề này.
References:
All rights reserved