Câu hỏi phỏng vấn Ruby on Rails: Top 25 năm 2026
25 câu hỏi phỏng vấn Ruby on Rails phổ biến nhất. Kiến trúc MVC, Active Record, migration, kiểm thử RSpec, REST API kèm câu trả lời chi tiết và ví dụ mã.

Phỏng vấn Ruby on Rails đánh giá khả năng làm chủ framework Ruby phổ biến nhất, hiểu biết về kiến trúc MVC, ORM Active Record và khả năng xây dựng các ứng dụng web vững chắc theo triết lý "Convention over Configuration". Hướng dẫn này bao gồm 25 câu hỏi được hỏi nhiều nhất, từ kiến thức cơ bản về Rails đến các pattern production nâng cao.
Nhà tuyển dụng đánh giá cao những ứng viên hiểu triết lý Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) và các pattern Rails Way. Giải thích tại sao Rails đưa ra một số lựa chọn kiến trúc nhất định sẽ tạo nên khác biệt.
Cơ bản về Ruby on Rails
Câu hỏi 1: Hãy giải thích pattern MVC trong Ruby on Rails
Pattern Model-View-Controller (MVC) là cốt lõi kiến trúc của Rails. Nó tách các trách nhiệm thành ba lớp riêng biệt để cải thiện khả năng bảo trì và kiểm thử mã.
# app/models/article.rb
# Model quản lý dữ liệu và logic nghiệp vụ
class Article < ApplicationRecord
# Xác thực dữ liệu
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Liên kết với các model khác
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scope cho các truy vấn tái sử dụng
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Callback vòng đời
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# Controller nhận yêu cầu và điều phối phản hồi
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.published.recent.includes(:author)
end
def show
@comments = @article.comments.includes(:user)
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: 'Đã tạo bài viết thành công.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
end<%# app/views/articles/show.html.erb %>
<%# View hiển thị dữ liệu ở định dạng HTML %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
Bởi <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial cho phần bình luận %>
<%= render @comments %>
</article>Luồng điển hình: yêu cầu đến Router, Router chuyển nó tới Controller phù hợp. Controller tương tác với Model để lấy hoặc sửa đổi dữ liệu, sau đó chuyển dữ liệu cho View để render HTML.
Câu hỏi 2: Active Record là gì và ORM của Rails hoạt động như thế nào?
Active Record là ORM (Object-Relational Mapping) của Rails, triển khai pattern Active Record. Mỗi class Model đại diện cho một bảng cơ sở dữ liệu, mỗi instance đại diện cho một dòng.
# app/models/user.rb
# Active Record tự động ánh xạ cột sang thuộc tính
class User < ApplicationRecord
# Bảng 'users' được liên kết tự động
# Cột: id, email, name, created_at, updated_at
has_secure_password # BCrypt cho mật khẩu
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Xác thực
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callback
before_save :normalize_email
# Phương thức class cho truy vấn
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Ví dụ truy vấn Active Record
# Console Rails hoặc trong service
# Tạo
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Đọc với điều kiện
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Truy vấn nối chuỗi (lazy evaluation)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# Phòng tránh N+1 với eager loading
articles = Article.includes(:author, :comments).published
# Cập nhật
user.update!(name: 'Alice Martin')
# Giao dịch
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record chuyển các phương thức Ruby thành các truy vấn SQL được tối ưu. Các phương thức như where, joins, includes là lazy: truy vấn chỉ thực thi khi lặp hoặc khi gọi to_a.
Câu hỏi 3: Hãy giải thích hệ thống migration của Rails
Migration cho phép phiên bản hóa schema cơ sở dữ liệu bằng Ruby. Chúng có thể đảo ngược và cho phép tiến hóa có kiểm soát của cấu trúc dữ liệu.
# db/migrate/20260203100000_create_products.rb
# Migration để tạo bảng
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.integer :stock_quantity, default: 0
t.references :category, null: false, foreign_key: true
t.boolean :active, default: true
t.timestamps # created_at và updated_at tự động
end
# Index cho hiệu năng
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migration để sửa đổi bảng đã tồn tại
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Điền slug cho dữ liệu hiện có
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Đặt NOT NULL sau khi điền
change_column_null :products, :slug, false
end
end# Các lệnh migration cần thiết
rails db:migrate # Chạy các migration đang chờ
rails db:rollback # Hoàn tác migration cuối
rails db:rollback STEP=3 # Hoàn tác 3 migration cuối
rails db:migrate:status # Xem trạng thái migration
rails db:seed # Chạy db/seeds.rb
rails db:reset # Drop, create, migrate, seedMigration phải có thể đảo ngược. Phương thức change thông minh và có thể đảo ngược các thao tác phổ biến tự động. Cho các trường hợp phức tạp, sử dụng up và down riêng biệt.
Active Record nâng cao
Câu hỏi 4: Làm thế nào để tối ưu truy vấn N+1 trong Rails?
Vấn đề N+1 xảy ra khi sau truy vấn ban đầu là N truy vấn bổ sung để tải các liên kết. Rails cung cấp nhiều phương pháp eager loading để giải quyết vấn đề này.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ VẤN ĐỀ N+1: 1 truy vấn + N truy vấn cho mỗi đơn hàng
# @orders = Order.all
# Trong view: order.user.name tạo một truy vấn cho mỗi đơn hàng
# ✅ GIẢI PHÁP với includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Chỉ tạo 3 truy vấn tổng cộng
end
def show
# includes: tải các liên kết riêng (2-3 truy vấn)
@order = Order.includes(items: :product).find(params[:id])
# preload: ép tải riêng
@order = Order.preload(:items, :user).find(params[:id])
# eager_load: ép LEFT OUTER JOIN (1 truy vấn)
@order = Order.eager_load(:items).find(params[:id])
end
end# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
has_many :items, class_name: 'OrderItem'
has_many :products, through: :items
# Scope với includes mặc định
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache để tránh truy vấn COUNT
# Yêu cầu: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# Phát hiện N+1 với gem Bullet (development)
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
# Bullet sẽ hiển thị cảnh báo khi:
# - Phát hiện truy vấn N+1
# - Có eager loading không cần thiết
# - Nên sử dụng counter cacheQuy tắc: dùng includes mặc định (Rails chọn chiến lược tối ưu), dùng preload khi muốn ép truy vấn riêng, dùng eager_load khi lọc theo các liên kết.
Câu hỏi 5: Hãy giải thích Scope và Query Object trong Rails
Scope đóng gói các điều kiện truy vấn có thể tái sử dụng. Cho các truy vấn phức tạp, Query Object cung cấp tổ chức và khả năng kiểm thử tốt hơn.
# app/models/product.rb
class Product < ApplicationRecord
# Scope đơn giản
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scope có tham số
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Scope nối chuỗi
scope :available, -> { active.in_stock }
# Scope với joins
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope với truy vấn con
scope :bestsellers, -> {
where(id: OrderItem.group(:product_id)
.order('COUNT(*) DESC')
.limit(10)
.select(:product_id))
}
end# app/queries/products_search_query.rb
# Query Object cho tìm kiếm phức tạp
class ProductsSearchQuery
def initialize(relation = Product.all)
@relation = relation
end
def call(params)
@relation = filter_by_category(params[:category])
@relation = filter_by_price_range(params[:min_price], params[:max_price])
@relation = filter_by_search(params[:q])
@relation = apply_sorting(params[:sort])
@relation
end
private
def filter_by_category(category)
return @relation if category.blank?
@relation.where(category_id: category)
end
def filter_by_price_range(min, max)
@relation = @relation.where('price >= ?', min) if min.present?
@relation = @relation.where('price <= ?', max) if max.present?
@relation
end
def filter_by_search(query)
return @relation if query.blank?
@relation.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def apply_sorting(sort)
case sort
when 'price_asc' then @relation.order(price: :asc)
when 'price_desc' then @relation.order(price: :desc)
when 'newest' then @relation.order(created_at: :desc)
else @relation.order(:name)
end
end
end
# Sử dụng trong controller
@products = ProductsSearchQuery.new(Product.active).call(params)Scope hoàn hảo cho các điều kiện đơn giản, tái sử dụng. Query Object phù hợp cho các tìm kiếm phức tạp với nhiều bộ lọc tùy chọn và logic kết hợp.
Sẵn sàng chinh phục phỏng vấn Ruby on Rails?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Routing và Controller
Câu hỏi 6: Routing RESTful trong Rails hoạt động như thế nào?
Rails khuyến khích các tuyến RESTful ánh xạ động từ HTTP với hành động CRUD. Router dịch các URL thành các lệnh gọi controller cụ thể.
# config/routes.rb
Rails.application.routes.draw do
# Tuyến RESTful chuẩn (7 hành động)
resources :articles do
# Tuyến lồng nhau
resources :comments, only: [:create, :destroy]
# Tuyến member (tác động lên một instance)
member do
post :publish
delete :archive
end
# Tuyến collection (tác động lên tập hợp)
collection do
get :drafts
get :search
end
end
# Tuyến API với namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Tuyến tùy chỉnh
get 'dashboard', to: 'dashboard#index'
# Ràng buộc tuyến
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Tuyến gốc
root 'home#index'
end# rails routes - Hiển thị tất cả các tuyến được tạo
#
# Verb URI Pattern Controller#Action
# GET /articles articles#index
# POST /articles articles#create
# GET /articles/new articles#new
# GET /articles/:id/edit articles#edit
# GET /articles/:id articles#show
# PATCH /articles/:id articles#update
# DELETE /articles/:id articles#destroy
# POST /articles/:id/publish articles#publish
# GET /articles/drafts articles#draftsCác route helper được tạo (article_path(@article), new_article_path) cho phép tham chiếu URL một cách động và dễ bảo trì.
Câu hỏi 7: Hãy giải thích callback và filter trong controller
Callback (before_action, after_action, around_action) cho phép thực thi mã trước, sau hoặc xung quanh các hành động của controller.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Bảo vệ CSRF được kích hoạt mặc định
protect_from_forgery with: :exception
# Callback toàn cục cho xác thực
before_action :authenticate_user!
# Xử lý lỗi toàn cục
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Không tìm thấy tài nguyên' }, status: :not_found
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end# app/controllers/admin/products_controller.rb
class Admin::ProductsController < ApplicationController
# Callback với tùy chọn
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Callback có điều kiện
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Đã tạo sản phẩm.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Đã cập nhật sản phẩm.'
else
render :edit, status: :unprocessable_entity
end
end
private
def require_admin
redirect_to root_path unless current_user&.admin?
end
def set_product
@product = Product.find(params[:id])
end
def stock_changed?
params[:product][:stock_quantity].present?
end
def log_activity
ActivityLog.create!(
user: current_user,
action: action_name,
resource: @product
)
end
def product_params
params.require(:product).permit(:name, :price, :description, :stock_quantity)
end
endCallback được thực thi theo thứ tự khai báo. Sử dụng skip_before_action trong các lớp con để vô hiệu hóa callback kế thừa. Tránh callback chứa quá nhiều logic nghiệp vụ - hãy ưu tiên Service Object.
Service và kiến trúc
Câu hỏi 8: Làm thế nào để triển khai Service Object trong Rails?
Service Object đóng gói logic nghiệp vụ phức tạp không thuộc về Model hay Controller. Chúng cải thiện khả năng kiểm thử và tuân theo nguyên tắc một trách nhiệm duy nhất.
# app/services/order_processor.rb
# Service Object với giao diện chuẩn hóa
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Đơn hàng đã được xử lý') if @order.processed?
ActiveRecord::Base.transaction do
validate_stock!
process_payment!
update_inventory!
send_confirmation!
@order.update!(status: 'completed', processed_at: Time.current)
end
success(@order)
rescue PaymentError => e
failure("Thanh toán thất bại: #{e.message}")
rescue InsufficientStockError => e
failure("Không đủ hàng tồn: #{e.message}")
end
private
def validate_stock!
@order.items.each do |item|
unless item.product.stock_quantity >= item.quantity
raise InsufficientStockError, item.product.name
end
end
end
def process_payment!
result = PaymentGateway.charge(
amount: @order.total,
method: @payment_method,
description: "Đơn hàng ##{@order.id}"
)
raise PaymentError, result.error unless result.success?
@order.update!(payment_reference: result.transaction_id)
end
def update_inventory!
@order.items.each do |item|
item.product.decrement!(:stock_quantity, item.quantity)
end
end
def send_confirmation!
OrderMailer.confirmation(@order).deliver_later
end
def success(data)
Result.new(success: true, data: data)
end
def failure(error)
Result.new(success: false, error: error)
end
Result = Struct.new(:success, :data, :error, keyword_init: true) do
def success? = success
def failure? = !success
end
end# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = current_user.orders.build(order_params)
if @order.save
result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call
if result.success?
redirect_to @order, notice: 'Đơn hàng đã được xác nhận!'
else
@order.update!(status: 'payment_failed')
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
else
render :new, status: :unprocessable_entity
end
end
endPattern Service Object tuân theo một quy ước đơn giản: một class, một trách nhiệm, một phương thức công khai call. Trả về một đối tượng Result cho phép xử lý thành công và thất bại một cách rõ ràng.
Câu hỏi 9: Hãy giải thích Concern trong Rails
Concern cho phép trích xuất và chia sẻ mã giữa các Model hoặc Controller. Chúng sử dụng ActiveSupport::Concern để có cú pháp include sạch sẽ.
# app/models/concerns/sluggable.rb
# Concern tái sử dụng để tạo slug
module Sluggable
extend ActiveSupport::Concern
included do
# Mã được thực thi khi include
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Phương thức class
class_methods do
def find_by_slug!(slug)
find_by!(slug: slug)
end
def sluggable_source(column = :title)
@sluggable_source = column
end
def sluggable_source_column
@sluggable_source || :title
end
end
# Phương thức instance
def to_param
slug
end
private
def should_generate_slug?
slug.blank? || send("#{self.class.sluggable_source_column}_changed?")
end
def generate_slug
source = send(self.class.sluggable_source_column)
return if source.blank?
base_slug = source.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base)
slug = base
counter = 1
while self.class.where(slug: slug).where.not(id: id).exists?
slug = "#{base}-#{counter}"
counter += 1
end
slug
end
end# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
sluggable_source :title # Tùy chọn, mặc định là :title
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern cho controller
module Pagination
extend ActiveSupport::Concern
included do
helper_method :page_param, :per_page_param
end
private
def paginate(relation)
relation.page(page_param).per(per_page_param)
end
def page_param
params[:page]&.to_i || 1
end
def per_page_param
[params[:per_page]&.to_i || 25, 100].min
end
endConcern hữu ích cho mã thực sự được chia sẻ. Tránh tạo Concern chỉ để "rút gọn" một Model: việc đó che giấu sự phức tạp mà không giảm bớt nó.
Kiểm thử với RSpec
Câu hỏi 10: Làm thế nào để cấu trúc các bài kiểm thử RSpec trong Rails?
RSpec là framework kiểm thử chuẩn cho Rails. Một cấu trúc kiểm thử tốt bao gồm Model spec, Controller spec, Service spec và các bài kiểm thử tích hợp.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factory với FactoryBot
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
describe 'validations' do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it 'xác thực định dạng email' do
user.email = 'invalid'
expect(user).not_to be_valid
expect(user.errors[:email]).to include('is invalid')
end
end
describe 'associations' do
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to have_one(:profile) }
it { is_expected.to belong_to(:organization).optional }
end
describe '#full_name' do
it 'trả về tên và họ kết hợp' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'xử lý khi thiếu họ' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'chỉ trả về người dùng đang hoạt động' do
active = create(:user, active: true)
inactive = create(:user, active: false)
expect(User.active).to include(active)
expect(User.active).not_to include(inactive)
end
end
end# spec/services/order_processor_spec.rb
require 'rails_helper'
RSpec.describe OrderProcessor do
let(:user) { create(:user) }
let(:product) { create(:product, stock_quantity: 10, price: 100) }
let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) }
subject { described_class.new(order, payment_method: 'card') }
describe '#call' do
context 'khi đơn hàng hợp lệ' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'xử lý đơn hàng thành công' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'giảm tồn kho sản phẩm' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'gửi email xác nhận' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'khi thanh toán thất bại' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'trả về kết quả thất bại' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'không cập nhật trạng thái đơn hàng' do
expect { subject.call }.not_to change { order.reload.status }
end
end
end
end# spec/requests/api/v1/products_spec.rb
require 'rails_helper'
RSpec.describe 'API V1 Products', type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe 'GET /api/v1/products' do
let!(:products) { create_list(:product, 3, :active) }
it 'trả về danh sách sản phẩm' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'lọc theo danh mục' do
category = create(:category)
categorized = create(:product, category: category)
get '/api/v1/products', params: { category_id: category.id }, headers: headers
expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id])
end
end
describe 'POST /api/v1/products' do
let(:valid_params) do
{ product: { name: 'Sản phẩm mới', price: 99.99, category_id: create(:category).id } }
end
it 'tạo một sản phẩm mới' do
expect {
post '/api/v1/products', params: valid_params, headers: headers
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
end
endPhương pháp tốt: sử dụng let cho dữ liệu, describe cho phương thức/ngữ cảnh, context cho điều kiện và it cho khẳng định cụ thể. Một bài test nên kiểm tra một thứ.
Câu hỏi 11: Làm thế nào để sử dụng factory với FactoryBot?
FactoryBot cho phép tạo dữ liệu kiểm thử theo cách khai báo và dễ bảo trì. Factory thay thế các fixture tĩnh.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Sequence để đảm bảo tính duy nhất
sequence(:email) { |n| "user#{n}@example.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
password { 'password123' }
confirmed_at { Time.current }
# Trait cho biến thể
trait :admin do
role { 'admin' }
after(:create) do |user|
user.permissions.create!(name: 'admin_access')
end
end
trait :unconfirmed do
confirmed_at { nil }
end
trait :with_profile do
after(:create) do |user|
create(:profile, user: user)
end
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
# Factory kế thừa
factory :admin_user do
admin
with_profile
end
end
end# spec/factories/orders.rb
FactoryBot.define do
factory :order do
user
status { 'pending' }
trait :with_items do
transient do
items_count { 2 }
end
after(:create) do |order, evaluator|
create_list(:order_item, evaluator.items_count, order: order)
order.recalculate_total!
end
end
trait :completed do
status { 'completed' }
processed_at { Time.current }
with_items
end
trait :high_value do
after(:create) do |order|
create(:order_item, order: order, quantity: 10, unit_price: 500)
order.recalculate_total!
end
end
end
end# Sử dụng trong các bài kiểm thử
RSpec.describe OrderProcessor do
# build: instance không lưu vào DB
let(:user) { build(:user) }
# create: lưu vào DB
let(:order) { create(:order, :with_items, user: user) }
# create_list: nhiều instance
let(:products) { create_list(:product, 5) }
# Kết hợp các trait
let(:admin) { create(:user, :admin, :with_profile) }
# Ghi đè thuộc tính
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed: nhanh hơn, cho unit test
let(:stubbed_user) { build_stubbed(:user) }
endƯu tiên build hoặc build_stubbed thay vì create khi không cần lưu trữ: điều này tăng tốc đáng kể các bài test.
Background Job
Câu hỏi 12: Làm thế nào để sử dụng Active Job và Sidekiq trong Rails?
Active Job cung cấp một giao diện thống nhất cho các job nền, không phụ thuộc backend (Sidekiq, Resque, v.v.). Sidekiq là lựa chọn phổ biến vì hiệu năng với Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Cấu hình thử lại
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Tùy chọn Sidekiq (nếu backend là Sidekiq)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Đơn hàng đã bị xóa giữa lúc xếp hàng và thực thi
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Giới hạn tốc độ với Sidekiq Enterprise hoặc gem throttle
sidekiq_options throttle: { threshold: 100, period: 1.minute }
def perform(user_ids, template_id)
template = EmailTemplate.find(template_id)
User.where(id: user_ids).find_each do |user|
UserMailer.custom_email(user, template).deliver_later
end
end
end# Xếp hàng job
# Ngay lập tức
ProcessOrderJob.perform_later(order.id)
# Trì hoãn
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# Vào thời điểm cụ thể
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Hàng đợi cụ thể
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Đồng bộ (cho test hoặc debug)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # Ưu tiên cao, trọng số 3
- [default, 2] # Ưu tiên trung bình, trọng số 2
- [mailers, 1] # Ưu tiên thấp, trọng số 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Mỗi ngày lúc 3 giờ sáng
class: CleanupJobActive Job trừu tượng hóa backend, nhưng truy cập các tính năng cụ thể (batch, rate limiting) thường yêu cầu liên kết chặt với backend được chọn.
Sẵn sàng chinh phục phỏng vấn Ruby on Rails?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Phát triển API
Câu hỏi 13: Làm thế nào để xây dựng RESTful API với Rails?
Rails giúp việc xây dựng API JSON dễ dàng với các Controller API-only và serializer. Một API tốt được phiên bản hóa, tài liệu hóa và an toàn.
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_token!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def authenticate_token!
authenticate_or_request_with_http_token do |token, options|
@current_user = User.find_by(api_token: token)
end
end
def current_user
@current_user
end
def not_found(exception)
render json: { error: 'Không tìm thấy tài nguyên', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Xác thực thất bại', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Yêu cầu không hợp lệ', details: exception.message },
status: :bad_request
end
end
end
end# app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Product.active
.includes(:category)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: ProductSerializer.new(@products).serializable_hash,
meta: pagination_meta(@products)
}
end
def show
render json: ProductSerializer.new(@product, include: [:category, :reviews])
end
def create
@product = Product.new(product_params)
@product.save!
render json: ProductSerializer.new(@product), status: :created
end
def update
@product.update!(product_params)
render json: ProductSerializer.new(@product)
end
def destroy
@product.destroy!
head :no_content
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end# app/serializers/product_serializer.rb
# Với gem jsonapi-serializer
class ProductSerializer
include JSONAPI::Serializer
attributes :id, :name, :description, :price, :created_at
attribute :formatted_price do |product|
"$#{product.price.to_f.round(2)}"
end
belongs_to :category
has_many :reviews
link :self do |product|
Rails.application.routes.url_helpers.api_v1_product_url(product)
end
endPhương pháp tốt cho API: phiên bản hóa thông qua namespace, sử dụng mã HTTP phù hợp, phân trang tập hợp và cung cấp thông báo lỗi rõ ràng.
Câu hỏi 14: Làm thế nào để triển khai xác thực JWT trong Rails?
JWT (JSON Web Tokens) là phương pháp xác thực stateless phổ biến cho API. Token mã hóa danh tính và tính hợp lệ của người dùng.
# app/services/jwt_service.rb
class JwtService
SECRET_KEY = Rails.application.credentials.secret_key_base
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
payload[:iat] = Time.current.to_i
JWT.encode(payload, SECRET_KEY, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
HashWithIndifferentAccess.new(decoded.first)
rescue JWT::ExpiredSignature
raise AuthenticationError, 'Token đã hết hạn'
rescue JWT::DecodeError
raise AuthenticationError, 'Token không hợp lệ'
end
end
end# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ActionController::API
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JwtService.encode(user_id: user.id)
render json: {
token: token,
user: UserSerializer.new(user),
expires_at: 24.hours.from_now
}
else
render json: { error: 'Thông tin đăng nhập không hợp lệ' }, status: :unauthorized
end
end
def refresh
token = JwtService.encode(user_id: current_user.id)
render json: { token: token, expires_at: 24.hours.from_now }
end
end
end
end# app/controllers/concerns/jwt_authenticatable.rb
module JwtAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_jwt!
end
private
def authenticate_jwt!
header = request.headers['Authorization']
token = header&.split(' ')&.last
raise AuthenticationError, 'Thiếu token' unless token
decoded = JwtService.decode(token)
@current_user = User.find(decoded[:user_id])
rescue AuthenticationError => e
render json: { error: e.message }, status: :unauthorized
rescue ActiveRecord::RecordNotFound
render json: { error: 'Không tìm thấy người dùng' }, status: :unauthorized
end
def current_user
@current_user
end
endCho production, hãy cân nhắc: refresh token, blacklist token khi đăng xuất và thời gian hết hạn ngắn. Các gem như devise-jwt đơn giản hóa việc triển khai.
Cache và hiệu năng
Câu hỏi 15: Làm thế nào để triển khai cache trong Rails?
Rails cung cấp nhiều cấp độ cache: fragment caching, Russian Doll caching, low-level caching. Lựa chọn phụ thuộc vào trường hợp sử dụng.
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp:cache',
expires_in: 1.day,
race_condition_ttl: 10.seconds
}<%# app/views/products/index.html.erb %>
<%# Fragment caching với khóa cache tự động %>
<% @products.each do |product| %>
<%# Cache dựa trên updated_at của sản phẩm %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - cache lồng nhau %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Cache có điều kiện %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch parent để vô hiệu hóa Russian Doll cache
belongs_to :category, touch: true
# Khóa cache tùy chỉnh
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching trong service
class DashboardStatsService
def call
Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do
{
total_users: User.count,
active_users: User.where('last_sign_in_at > ?', 30.days.ago).count,
total_orders: Order.completed.count,
revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total)
}
end
end
end
# Cache với bảo vệ chống race condition
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Vô hiệu hóa rõ ràng
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Russian Doll caching hiệu quả vì chỉ tái tạo các fragment đã sửa đổi. Sử dụng touch: true trên các liên kết để lan truyền vô hiệu hóa.
Câu hỏi 16: Làm thế nào để tối ưu hiệu năng ứng dụng Rails?
Tối ưu Rails bao gồm nhiều khía cạnh: truy vấn DB, cache, asset và kiến trúc. Cách tiếp cận có hệ thống với giám sát là điều thiết yếu.
# Tối ưu hóa cơ sở dữ liệu
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Index kết hợp cho truy vấn thường xuyên
# add_index :orders, [:user_id, :status, :created_at]
# Chỉ chọn các cột cần thiết
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Xử lý theo lô cho khối lượng lớn
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Tránh tính toán lặp lại
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Tối ưu bộ nhớ
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect!
end
on_worker_boot do
ActiveRecord::Base.establish_connection
end# Profiling với rack-mini-profiler
# Gemfile
group :development do
gem 'rack-mini-profiler'
gem 'memory_profiler'
gem 'stackprof'
end
# config/initializers/mini_profiler.rb
if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = true
end# Lazy loading và phân trang
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch cho trang tiếp theo
if @products.next_page
Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do
Product.active.page(@products.next_page).per(24).to_a
end
end
end
endCông cụ thiết yếu: rack-mini-profiler cho profiling, bullet cho phát hiện N+1, New Relic hoặc Scout cho giám sát production.
Bảo mật
Câu hỏi 17: Đâu là các best practice bảo mật trong Rails?
Rails bao gồm các bảo vệ mặc định chống lại các lỗ hổng phổ biến. Việc hiểu và cấu hình đúng các bảo vệ này là rất quan trọng.
# Bảo vệ CSRF
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Bật mặc định, ném ngoại lệ nếu token không hợp lệ
protect_from_forgery with: :exception
# Cho API, sử dụng :null_session
# protect_from_forgery with: :null_session
end
# Trong view, token được tự động thêm vào form
# <%= form_with ... %> bao gồm authenticity_token
# Cho yêu cầu AJAX
# Thêm header X-CSRF-Token với giá trị từ csrf_meta_tags# Phòng ngừa SQL Injection
# ✅ Tham số nội suy được tự động escape
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ NGUY HIỂM - Nội suy trực tiếp
User.where("email = '#{params[:email]}'")
# ✅ Cho mệnh đề ORDER động
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# Bảo vệ XSS
# Rails tự động escape HTML trong view
# ✅ Tự động escape
<%= user.name %>
# ❌ Nguy hiểm - nội dung không escape
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ Cho HTML an toàn, sử dụng sanitize
<%= sanitize user.bio, tags: %w[p br strong em] %># Strong Parameters
class UsersController < ApplicationController
def update
@user.update!(user_params)
end
private
def user_params
# Whitelist rõ ràng các thuộc tính được phép
params.require(:user).permit(:name, :email, :avatar)
# Chỉ cho admin
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Header bảo mật
# config/initializers/secure_headers.rb
Rails.application.config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
# Content Security Policy
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.img_src :self, :data, 'https:'
endKiểm toán định kỳ với brakeman (phân tích bảo mật tĩnh) và giữ các gem cập nhật với bundle audit.
Câu hỏi 18: Làm thế nào để xử lý xác thực và phân quyền trong Rails?
Xác thực kiểm tra danh tính, phân quyền điều khiển quyền hạn. Devise xử lý xác thực, Pundit hoặc CanCanCan xử lý phân quyền.
# Cài đặt Devise
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable
enum role: { user: 0, moderator: 1, admin: 2 }
def admin?
role == 'admin'
end
end# Policy của Pundit
# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || owner_or_admin?
end
def create?
user.present?
end
def update?
owner_or_admin?
end
def destroy?
owner_or_admin?
end
def publish?
user&.admin? || user&.moderator?
end
# Scope cho tập hợp
class Scope < Scope
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(author: user))
else
scope.where(published: true)
end
end
end
private
def owner_or_admin?
user&.admin? || record.author == user
end
end# Controller với Pundit
class ArticlesController < ApplicationController
include Pundit::Authorization
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@articles = policy_scope(Article).includes(:author).page(params[:page])
end
def show
@article = Article.find(params[:id])
authorize @article
end
def update
@article = Article.find(params[:id])
authorize @article
if @article.update(article_params)
redirect_to @article, notice: 'Đã cập nhật bài viết.'
else
render :edit, status: :unprocessable_entity
end
end
def publish
@article = Article.find(params[:id])
authorize @article
@article.update!(published: true, published_at: Time.current)
redirect_to @article, notice: 'Đã xuất bản bài viết.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "Bạn không được phép thực hiện hành động này."
redirect_back(fallback_location: root_path)
end
endPundit rõ ràng và dễ kiểm thử hơn CanCanCan. Mỗi hành động có một phương thức policy tương ứng và các scope tự động lọc tập hợp.
Rails nâng cao
Câu hỏi 19: Hãy giải thích pattern Repository trong Rails
Pattern Repository tách biệt logic truy cập dữ liệu khỏi phần còn lại của ứng dụng. Mặc dù Rails sử dụng Active Record (một pattern khác), Repository có thể hữu ích trong các trường hợp phức tạp.
# app/repositories/base_repository.rb
class BaseRepository
def initialize(model_class)
@model_class = model_class
end
def all
@model_class.all
end
def find(id)
@model_class.find(id)
end
def find_by(attributes)
@model_class.find_by(attributes)
end
def create(attributes)
@model_class.create(attributes)
end
def update(record, attributes)
record.update(attributes)
end
def delete(record)
record.destroy
end
end# app/repositories/product_repository.rb
class ProductRepository < BaseRepository
def initialize
super(Product)
end
def active
@model_class.where(active: true)
end
def in_category(category_id)
@model_class.where(category_id: category_id)
end
def search(query)
@model_class.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def with_stock
@model_class.where('stock_quantity > 0')
end
def bestsellers(limit: 10)
@model_class
.joins(:order_items)
.group(:id)
.order('COUNT(order_items.id) DESC')
.limit(limit)
end
def for_homepage
active
.with_stock
.includes(:category, :primary_image)
.order(featured: :desc, created_at: :desc)
.limit(12)
end
end# Sử dụng trong service
class ProductSearchService
def initialize(repository: ProductRepository.new)
@repository = repository
end
def call(params)
products = @repository.active
products = products.in_category(params[:category]) if params[:category]
products = products.search(params[:query]) if params[:query].present?
products = products.with_stock if params[:in_stock]
products
end
end
# Tạo điều kiện thuận lợi cho việc test với mock
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'lọc theo danh mục' do
products = double('products')
allow(repository).to receive(:active).and_return(products)
allow(products).to receive(:in_category).with(1).and_return(products)
service.call(category: 1)
expect(products).to have_received(:in_category).with(1)
end
endRepository là tùy chọn trong Rails vì Active Record đã là một pattern xuất sắc. Sử dụng cho truy vấn phức tạp hoặc khi việc cô lập storage là quan trọng.
Câu hỏi 20: Làm thế nào để triển khai pattern CQRS trong Rails?
CQRS (Command Query Responsibility Segregation) tách các thao tác đọc và ghi. Trong Rails, điều này nghĩa là các class riêng cho query và command.
# app/commands/base_command.rb
class BaseCommand
include ActiveModel::Validations
def self.call(*args)
new(*args).call
end
def call
return failure(errors) unless valid?
execute
end
private
def execute
raise NotImplementedError
end
def success(data = nil)
CommandResult.success(data)
end
def failure(errors)
CommandResult.failure(errors)
end
end
CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do
def success? = success
def failure? = !success
def self.success(data)
new(success: true, data: data, errors: [])
end
def self.failure(errors)
new(success: false, data: nil, errors: Array(errors))
end
end# app/commands/orders/create_order_command.rb
module Orders
class CreateOrderCommand < BaseCommand
attr_reader :user, :items, :shipping_address
validates :user, presence: true
validates :items, presence: true
validate :validate_items_availability
def initialize(user:, items:, shipping_address:)
@user = user
@items = items
@shipping_address = shipping_address
end
private
def execute
order = nil
ActiveRecord::Base.transaction do
order = Order.create!(
user: user,
shipping_address: shipping_address,
status: 'pending'
)
items.each do |item|
order.items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
unit_price: Product.find(item[:product_id]).price
)
end
order.calculate_total!
end
OrderCreatedEvent.broadcast(order)
success(order)
rescue ActiveRecord::RecordInvalid => e
failure(e.message)
end
def validate_items_availability
items.each do |item|
product = Product.find_by(id: item[:product_id])
unless product&.stock_quantity&.>= item[:quantity]
errors.add(:items, "Sản phẩm #{item[:product_id]} không khả dụng")
end
end
end
end
end# app/queries/orders/user_orders_query.rb
module Orders
class UserOrdersQuery
def initialize(user, params = {})
@user = user
@params = params
end
def call
orders = @user.orders.includes(:items, items: :product)
orders = apply_status_filter(orders)
orders = apply_date_filter(orders)
orders = apply_sorting(orders)
orders.page(@params[:page]).per(@params[:per_page] || 20)
end
private
def apply_status_filter(orders)
return orders unless @params[:status]
orders.where(status: @params[:status])
end
def apply_date_filter(orders)
orders = orders.where('created_at >= ?', @params[:from]) if @params[:from]
orders = orders.where('created_at <= ?', @params[:to]) if @params[:to]
orders
end
def apply_sorting(orders)
case @params[:sort]
when 'oldest' then orders.order(created_at: :asc)
when 'total_desc' then orders.order(total: :desc)
else orders.order(created_at: :desc)
end
end
end
end# Controller sử dụng CQRS
class OrdersController < ApplicationController
def index
@orders = Orders::UserOrdersQuery.new(current_user, filter_params).call
end
def create
result = Orders::CreateOrderCommand.call(
user: current_user,
items: order_params[:items],
shipping_address: order_params[:shipping_address]
)
if result.success?
redirect_to result.data, notice: 'Đã tạo đơn hàng!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS tỏa sáng trong các ứng dụng phức tạp với nhu cầu đọc/ghi không đối xứng. Đối với CRUD đơn giản, đây là kỹ thuật quá mức.
Câu hỏi 21: Làm thế nào để xử lý WebSocket với Action Cable?
Action Cable tích hợp WebSocket vào Rails để giao tiếp hai chiều theo thời gian thực. Nó sử dụng Redis để đồng bộ giữa các server.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
# Qua cookie phiên
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Qua JWT cho API
elsif verified_user = verify_jwt_token
verified_user
else
reject_unauthorized_connection
end
end
def verify_jwt_token
token = request.params[:token]
return nil unless token
decoded = JwtService.decode(token)
User.find(decoded[:user_id])
rescue
nil
end
end
end# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
# Kiểm tra quyền
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Thông báo cho người khác về sự hiện diện
broadcast_presence(:joined)
end
def unsubscribed
broadcast_presence(:left) if @room
end
def send_message(data)
message = @room.messages.create!(
user: current_user,
content: data['content']
)
# Phát đến tất cả người đăng ký
ChatChannel.broadcast_to(@room, {
type: 'message',
message: MessageSerializer.new(message).as_json
})
end
def typing
ChatChannel.broadcast_to(@room, {
type: 'typing',
user: current_user.name
})
end
private
def broadcast_presence(action)
ChatChannel.broadcast_to(@room, {
type: 'presence',
action: action,
user: current_user.name,
online_count: @room.online_users_count
})
end
endimport consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
switch(data.type) {
case 'message':
this.appendMessage(data.message)
break
case 'typing':
this.showTypingIndicator(data.user)
break
case 'presence':
this.updatePresence(data)
break
}
},
sendMessage(content) {
this.perform('send_message', { content: content })
},
notifyTyping() {
this.perform('typing')
}
}
)Action Cable tự động xử lý kết nối lại và đồng bộ. Trong production, cấu hình Redis làm adapter và mở rộng theo các kết nối đồng thời.
Câu hỏi 22: Làm thế nào để triển khai multi-tenancy trong Rails?
Multi-tenancy cho phép một ứng dụng phục vụ nhiều khách hàng (tenant) tách biệt. Ba phương pháp chính: cấp database, schema hoặc dòng.
# Multitenancy cấp dòng với ActsAsTenant hoặc thủ công
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Scope mặc định trên tenant hiện tại
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Xác thực tenant
before_validation :set_tenant, on: :create
end
private
def set_tenant
self.tenant ||= Current.tenant
end
end
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :tenant, :user
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = resolve_tenant
Current.user = current_user
end
def resolve_tenant
# Qua subdomain
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Qua header (cho API)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Qua người dùng
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Không tìm thấy tenant'
end
end# app/models/project.rb
class Project < ApplicationRecord
include TenantScoped
has_many :tasks
belongs_to :owner, class_name: 'User'
end
# app/models/user.rb
class User < ApplicationRecord
include TenantScoped
has_many :projects, foreign_key: :owner_id
# Quản trị viên có thể thuộc nhiều tenant
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# Cấp schema với gem Apartment (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Sử dụng
Apartment::Tenant.switch('acme') do
# Tất cả truy vấn trong block này dùng schema 'acme'
Project.all # SELECT * FROM acme.projects
endCấp dòng đơn giản nhất nhưng đòi hỏi sự chú ý liên tục đến rò rỉ dữ liệu. Cấp schema cung cấp cô lập tốt hơn nhưng làm phức tạp migration. Lựa chọn dựa trên yêu cầu bảo mật và khả năng mở rộng.
Câu hỏi 23: Làm thế nào để thiết lập kiến trúc microservice với Rails?
Rails có thể đóng vai trò nền tảng cho kiến trúc microservice với giao tiếp qua HTTP/gRPC hoặc hàng đợi tin nhắn. Chìa khóa là định nghĩa rõ các ranh giới.
# Client dịch vụ HTTP
# app/services/payment_service_client.rb
class PaymentServiceClient
include HTTParty
base_uri ENV.fetch('PAYMENT_SERVICE_URL')
def initialize
@options = {
headers: {
'Content-Type' => 'application/json',
'X-Service-Token' => ENV.fetch('SERVICE_TOKEN')
},
timeout: 10
}
end
def create_charge(amount:, currency:, source:, metadata: {})
response = self.class.post('/charges', @options.merge(
body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json
))
handle_response(response)
end
def get_charge(charge_id)
response = self.class.get("/charges/#{charge_id}", @options)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200..299
ServiceResult.success(response.parsed_response)
when 400..499
ServiceResult.failure(response.parsed_response['error'], code: response.code)
else
ServiceResult.failure('Dịch vụ không khả dụng', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Timeout dịch vụ')
end
end# Giao tiếp hướng sự kiện với Sidekiq/Redis
# app/events/order_events.rb
module OrderEvents
class Created
include Wisper::Publisher
def call(order)
broadcast(:order_created, order)
end
end
end
# app/listeners/inventory_listener.rb
class InventoryListener
def order_created(order)
order.items.each do |item|
InventoryServiceClient.new.reserve_stock(
product_id: item.product_id,
quantity: item.quantity,
reference: order.id
)
end
end
end
# config/initializers/wisper.rb
Wisper.subscribe(InventoryListener.new, async: true)
Wisper.subscribe(NotificationListener.new, async: true)# Pattern API Gateway
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Tổng hợp nhiều dịch vụ
def dashboard
results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service|
fetch_from_service(service)
end
render json: {
orders: results[0],
inventory: results[1],
analytics: results[2]
}
end
private
def fetch_from_service(service)
case service
when :orders
OrderServiceClient.new.recent_orders(limit: 5)
when :inventory
InventoryServiceClient.new.low_stock_alerts
when :analytics
AnalyticsServiceClient.new.daily_summary
end
rescue => e
{ error: "#{service} không khả dụng", message: e.message }
end
end
end
endCho microservice Rails: định nghĩa hợp đồng API rõ ràng (OpenAPI), triển khai circuit breaker (gem circuitbox) và sử dụng tracing phân tán (gem opentelemetry).
Câu hỏi 24: Làm thế nào để triển khai một ứng dụng Rails lên production?
Triển khai Rails hiện đại sử dụng container hoặc PaaS. Cấu hình production vững chắc bao gồm asset, cơ sở dữ liệu và giám sát.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Asset
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.assets.compile = false
config.assets.digest = true
# Logging
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
config.log_tags = [:request_id]
config.logger = ActiveSupport::Logger.new(STDOUT)
.tap { |logger| logger.formatter = Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
# Cache
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.day
}
# Bắt buộc SSL
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true } }
# Action Mailer
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USER'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
end# Dockerfile
FROM ruby:3.3-alpine AS builder
RUN apk add --no-cache build-base postgresql-dev nodejs yarn
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
bundle config set --local without 'development test' && \
bundle install
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN bundle exec rails assets:precompile
# Image production
FROM ruby:3.3-alpine
RUN apk add --no-cache postgresql-client tzdata
WORKDIR /app
COPY /app /app
COPY /usr/local/bundle /usr/local/bundle
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]# docker-compose.production.yml
version: '3.8'
services:
web:
build: .
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
sidekiq:
build: .
command: bundle exec sidekiq
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Danh sách kiểm tra production: SSL bắt buộc, secret qua ENV, health check, sao lưu DB tự động, giám sát (APM + log + metric) và cảnh báo được cấu hình.
Câu hỏi 25: Đâu là những tính năng mới của Rails 7+ cần biết?
Rails 7+ mang đến những thay đổi đáng kể: Hotwire mặc định, import map, credentials mã hóa cải tiến và nhiều tối ưu hóa.
# Hotwire - Turbo Frame
# app/views/articles/index.html.erb
<%= turbo_frame_tag "articles" do %>
<% @articles.each do |article| %>
<%= turbo_frame_tag dom_id(article) do %>
<%= render article %>
<% end %>
<% end %>
<%= link_to "Tải thêm", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Stream cho cập nhật thời gian thực
# app/controllers/comments_controller.rb
def create
@comment = @article.comments.create!(comment_params.merge(user: current_user))
respond_to do |format|
format.turbo_stream
format.html { redirect_to @article }
end
end
# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comments_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %># Controller Stimulus
# app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
import { debounce } from "lodash-es"
export default class extends Controller {
static targets = ["input", "results"]
static values = { url: String }
connect() {
this.search = debounce(this.search.bind(this), 300)
}
async search() {
const query = this.inputTarget.value
if (query.length < 2) return
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
this.resultsTarget.innerHTML = await response.text()
}
}# Import Map (không có bundler JavaScript)
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
# Pin từ CDN
pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js"# Active Record Encryption (Rails 7+)
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # Cho phép tìm kiếm
encrypts :phone_number # Không xác định mặc định
encrypts :ssn, deterministic: true, downcase: true
end
# config/credentials.yml.enc
active_record_encryption:
primary_key: abc123...
deterministic_key: def456...
key_derivation_salt: ghi789...# Cải tiến giao diện truy vấn
# Rails 7.1+
# Truy vấn bất đồng bộ
users = User.where(active: true).load_async
# Tiếp tục xử lý trong khi truy vấn chạy
# Truy cập kết quả với users.to_a
# Common Table Expressions (CTE)
User.with(
recent_orders: Order.where('created_at > ?', 30.days.ago)
).joins('JOIN recent_orders ON recent_orders.user_id = users.id')
# Phát hiện inverse_of tự động
class Author < ApplicationRecord
has_many :books # inverse_of phát hiện tự động
end
# Strict loading mặc định (tránh N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ ưa chuộng sự đơn giản (không có Webpack mặc định) và HTML-over-the-wire với Hotwire. Cách tiếp cận này giảm sự phức tạp của JavaScript trong khi cung cấp trải nghiệm người dùng hiện đại.
Kết luận
Phỏng vấn Ruby on Rails đánh giá khả năng làm chủ toàn bộ framework và hiểu biết về các quy ước của nó. Các điểm chính cần ghi nhớ:
✅ Cơ bản: MVC, Active Record, migration, xác thực và liên kết
✅ Kiến trúc: Service Object, Concern, Query Object và pattern CQRS
✅ Hiệu năng: truy vấn N+1, cache (fragment, Russian Doll, low-level), eager loading
✅ Kiểm thử: RSpec, FactoryBot, request spec và best practice kiểm thử
✅ Bảo mật: CSRF, SQL injection, XSS, Strong Parameters và xác thực/phân quyền
✅ API: thiết kế RESTful, JWT, serializer và phiên bản hóa
✅ Production: background job, WebSocket, triển khai và giám sát
Triết lý Rails (Convention over Configuration, DRY và Rails Way) định hướng mọi quyết định kiến trúc. Làm chủ những nguyên tắc này và biết khi nào nên đi chệch khỏi chúng cho thấy chuyên môn vững vàng.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

ActiveRecord: Khắc phục các vấn đề truy vấn N+1 trong Ruby on Rails
Hướng dẫn đầy đủ về cách phát hiện và xử lý các truy vấn N+1 trong Rails với ActiveRecord. Làm chủ includes, preload, eager_load và các công cụ phát hiện tự động.

Ruby on Rails 7: Hotwire va Turbo cho Ung Dung Phan Hoi
Huong dan day du ve Hotwire va Turbo trong Rails 7. Xay dung ung dung phan hoi khong can JavaScript voi Turbo Drive, Frames va Streams.

Rails API Mode năm 2026: Xây Dựng RESTful API, Serialization và Câu Hỏi Phỏng Vấn
Hướng dẫn Rails API Mode 2026: RESTful route, serialization Alba vs jsonapi-serializer, JWT, xử lý lỗi và RSpec testing.