Trong Rails, before_action
và các callback khác giúp chúng ta tổ chức logic một cách gọn gàng. Tuy nhiên, nếu dùng không đúng cách, chúng có thể khiến code khó đọc, khó debug và sinh ra lỗi không mong muốn. Bài viết này sẽ giúp bạn hiểu rõ và dùng callback đúng cách để tối ưu hóa ứng dụng Rails của mình.
1. before_action
là gì?
before_action
là một loại callback trong Rails controllers, cho phép bạn chạy một phương thức (method) trước khi một hoặc nhiều action chính được thực thi. Điều này đặc biệt hữu ích để xử lý các tác vụ tiền xử lý như xác thực người dùng, tìm kiếm bản ghi (record), hoặc thiết lập dữ liệu chung.
Ví dụ:
Trong ví dụ dưới đây, find_post
sẽ được gọi trước khi các action show
, edit
, update
, destroy
chạy, đảm bảo @post
luôn sẵn sàng.
class PostsController < ApplicationController before_action :find_post, only: [:show, :edit, :update, :destroy] def show # @post đã có sẵn từ find_post end def edit # @post cũng đã có sẵn end # ... các action khác private def find_post @post = Post.find(params[:id]) end
end
👉 Việc này giúp tránh lặp lại logic tìm post trong nhiều action, giữ cho code DRY (Don't Repeat Yourself).
2. after_action, around_action và các loại callback khác
Rails hỗ trợ một hệ thống callback phong phú không chỉ giới hạn ở before_action
:
before_action
: Chạy trước khi action được gọi.after_action
: Chạy sau khi action đã hoàn thành (bao gồm cả việc render view hoặc redirect).around_action
: "Bao quanh" việc thực thi action, cho phép bạn thực hiện các tác vụ trước và sau action trong cùng một phương thức (thường dùng cho các tác vụ cần đo thời gian hoặc quản lý transaction).
Ví dụ:
class ApplicationController < ActionController::Base before_action :authenticate_user! # Xác thực người dùng trước mọi action after_action :track_activity # Ghi lại hoạt động sau mỗi action around_action :log_timing # Đo thời gian thực thi của action private def authenticate_user! # Logic xác thực end def track_activity # Logic ghi lại end def log_timing start_time = Time.now yield # Thực thi action chính end_time = Time.now Rails.logger.info "Action took #{end_time - start_time} seconds" end
end
3. Cẩn thận khi dùng nhiều callback
Dù tiện lợi, việc lạm dụng hoặc dùng callback không có kiểm soát có thể gây ra vấn đề. Tránh dùng quá nhiều before_action
không rõ ràng, nhất là khi không phải mọi action đều cần nó.
# Bad: Chạy mọi lúc, có thể không cần thiết và giảm hiệu suất
before_action :set_timezone
before_action :track_user
before_action :load_resources # 👉 Nên dùng only hoặc except để giới hạn phạm vi rõ ràng:
before_action :track_user, only: [:show, :edit] # Chỉ track user ở show và edit
before_action :set_timezone, except: [:api_call] # Trừ các API call
Việc giới hạn phạm vi giúp callback chỉ chạy khi thực sự cần, cải thiện hiệu suất và dễ hiểu hơn.
4. Đặt thứ tự before_action
đúng
Thứ tự của các callback rất quan trọng và ảnh hưởng trực tiếp đến logic thực thi. Các callback được gọi theo thứ tự chúng được định nghĩa.
Ví dụ:
class AdminController < ApplicationController before_action :authenticate_admin! # 1. Xác thực admin trước before_action :load_data # 2. Sau đó mới tải dữ liệu def dashboard # ... end private def authenticate_admin! redirect_to root_path unless current_user.admin? end def load_data @dashboard_data = Report.generate end
end
👉 Trong ví dụ trên, nếu authenticate_admin!
chuyển hướng người dùng đi, load_data
sẽ không bao giờ được gọi. Điều này là đúng như mong muốn. Nếu thứ tự ngược lại, load_data có thể chạy không cần thiết hoặc thậm chí gây lỗi nếu người dùng không phải admin.
5. Đừng lạm dụng callback trong Model
Rails cũng cung cấp callback cho các Model (ví dụ: before_save, after_create, before_destroy). Chúng rất tiện lợi cho các tác vụ đơn giản liên quan đến vòng đời của đối tượng.
Ví dụ:
class User < ApplicationRecord before_save :normalize_email # Chạy trước khi lưu User def normalize_email self.email = email.downcase.strip # Chuẩn hóa email end
👉 Tuy nhiên, nếu logic trong callback trở nên phức tạp (ví dụ: gửi email, gọi API bên ngoài, xử lý logic nghiệp vụ phức tạp), chúng có thể ẩn logic và khó debug. Tốt hơn nên tách các tác vụ này ra khỏi Model callback và đưa vào Service Object hoặc Background Job.
6. Khi nào nên dùng callback?
✅ Nên dùng khi:
- Logic đơn giản, ngắn gọn: Ví dụ: tìm bản ghi, xác thực cơ bản, chuẩn hóa dữ liệu nhỏ.
- Áp dụng chung cho nhiều action: Giúp tránh lặp code.
- Là bước tiền xử lý rõ ràng: (authentication, authorization, setting up common resources).
🚫 Tránh dùng khi:
- Logic dài, phức tạp: Khi callback chiếm quá nhiều dòng code hoặc có nhiều điều kiện.
- Gây side effect khó thấy: Ví dụ: gửi email, gọi API bên ngoài, thay đổi dữ liệu của các đối tượng khác một cách phức tạp. Những tác vụ này nên được thực hiện rõ ràng trong action hoặc tách ra thành service/job.
- Ảnh hưởng đến hiệu suất: Khi callback chạy quá nhiều hoặc thực hiện các tác vụ tốn tài nguyên.
7. Tùy chỉnh thứ tự và điều kiện chạy
Bạn có thể kiểm soát linh hoạt hơn thời điểm và điều kiện chạy của callback bằng if
hoặc unless
với một block hoặc tên phương thức:
# Chạy set_locale nếu current_user tồn tại
before_action :set_locale, if: -> { current_user.present? } # Chạy set_locale nếu phương thức user_logged_in? trả về true
before_action :set_locale, if: :user_logged_in? # Chạy set_locale trừ khi phương thức is_api_request? trả về true
before_action :set_locale, unless: :is_api_request? private def user_logged_in? current_user.present?
end def is_api_request? request.format.json?
end
8. Dùng prepend_before_action để ưu tiên callback
Khi làm việc với kế thừa Controller, đôi khi bạn muốn một callback của Controller con chạy trước tất cả các callback của Controller cha, hoặc ưu tiên hơn các callback đã được định nghĩa. prepend_before_action
giúp bạn làm điều này.
class BaseController < ApplicationController before_action :authenticate_user! # Sẽ chạy sau log_early nếu có prepend_before_action
end class AdminController < BaseController prepend_before_action :log_early # Sẽ chạy trước tất cả các before_action khác, kể cả của BaseController before_action :verify_admin_access # Sẽ chạy sau log_early nhưng trước authenticate_user!
end
👉 Điều này rất hữu ích khi bạn cần chèn một logic kiểm tra hoặc ghi log cực kỳ sớm trong chuỗi thực thi.
9. Viết test cho callback
Mặc dù callback hoạt động "ngầm", việc viết test cho chúng là cực kỳ quan trọng để đảm bảo chúng hoạt động đúng như mong đợi và không gây ra các bug "vô hình".
RSpec.describe PostsController, type: :controller do let!(:post) { create(:post) } # Tạo một post trước khi chạy test describe "GET #show" do it "sets @post before showing the post" do get :show, params: { id: post.id } expect(assigns(:post)).to eq(post) # Kiểm tra xem @post có được gán đúng không end it "redirects if post is not found" do get :show, params: { id: 9999 } expect(response).to redirect_to(posts_path) # Hoặc trang lỗi phù hợp end end
end
👉 Việc test giúp bạn tự tin rằng các callback của mình đang hoạt động chính xác và xử lý được các trường hợp lỗi.
10. Đừng ngại refactor callback thành Service Object
Nếu một before_action hoặc callback trong Model trở nên quá dài, phức tạp, hoặc thực hiện nhiều nhiệm vụ, đó là dấu hiệu tốt để bạn nên refactor nó thành một Service Object hoặc Background Job.
Service Object là một Plain Old Ruby Object (PORO) được tạo ra để thực hiện một tác vụ nghiệp vụ cụ thể.
Ví dụ:
# Thay vì logic phức tạp trong Controller/Model callback:
# class PostsController < ApplicationController
# before_action :process_complicated_stuff
# def process_complicated_stuff
# # Rất nhiều logic ở đây
# end
# end # Refactor thành Service Object:
class FindPostService def initialize(params) @id = params[:id] end def call Post.find(@id) rescue ActiveRecord::RecordNotFound # Xử lý khi không tìm thấy nil end
end class PostsController < ApplicationController def show @post = FindPostService.new(params).call unless @post redirect_to posts_path, alert: "Bài viết không tồn tại." end end
end
👉 Việc này giúp code của bạn dễ test hơn, dễ debug hơn, dễ tái sử dụng hơn và giữ cho Controller/Model của bạn tinh gọn, chỉ tập trung vào nhiệm vụ chính của chúng.
Kết luận
before_action
và các callback là những công cụ mạnh mẽ trong Rails, giúp bạn tổ chức code một cách hiệu quả và tránh lặp lại. Tuy nhiên, chúng cũng là "con dao hai lưỡi" nếu không được sử dụng một cách có chủ đích. Hãy dùng chúng một cách rõ ràng, giới hạn phạm vi, và luôn nhớ rằng khi logic bắt đầu trở nên phức tạp, đó là lúc bạn nên cân nhắc tách chúng ra thành Service Object hoặc Background Job.
Bạn đã từng gặp rắc rối gì với callback trong các dự án Rails của mình chưa? Hãy chia sẻ kinh nghiệm và những bài học bạn rút ra được ở phần bình luận nhé!