# 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ã. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, phỏng vấn, active record, phỏng vấn kỹ thuật - Reading time: 22 min --- 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. > **Lời khuyên phỏng vấn** > > 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ã. ```ruby # 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 ``` ```ruby # 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 ``` ```erb <%# app/views/articles/show.html.erb %> <%# View hiển thị dữ liệu ở định dạng HTML %>

<%= @article.title %>

Bởi <%= @article.author.name %> • <%= l @article.created_at, format: :long %>

<%= simple_format @article.body %>
<%# Partial cho phần bình luận %> <%= render @comments %>
``` 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. ```ruby # 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 ``` ```ruby # 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) end ``` Active 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. ```ruby # 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 ``` ```ruby # 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 ``` ```bash # 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, seed ``` Migration 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 cache ``` Quy 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. ```ruby # 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 ``` ```ruby # 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. ## 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ể. ```ruby # 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 ``` ```bash # 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#drafts ``` Cá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. ```ruby # 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 ``` ```ruby # 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 end ``` Callback đượ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. ```ruby # 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 ``` ```ruby # 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 end ``` Pattern 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ẽ. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Concern 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Phươ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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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: CleanupJob ``` Active 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. ## 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Phươ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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Cho 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. ```ruby # 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 } ``` ```erb <%# 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 %>

<%= @category.name %>

<% @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 %> ``` ```ruby # 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 ``` ```ruby # 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Cô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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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] %> ``` ```ruby # 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 ``` ```ruby # 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:' end ``` Kiể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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Pundit 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Repository 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` CQRS 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. ```ruby # 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 ``` ```ruby # 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 end ``` ```javascript // app/javascript/channels/chat_channel.js import 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Cấ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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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 end ``` Cho 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. ```ruby # 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 # 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 --from=builder /app /app COPY --from=builder /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"] ``` ```yaml # 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. ```ruby # 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 } %> ``` ```ruby # 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() } } ``` ```ruby # 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" ``` ```ruby # 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... ``` ```ruby # 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 end ``` Rails 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/ruby-on-rails/ruby-on-rails-interview-questions