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ã.

Câu hỏi phỏng vấn Ruby on Rails - Hướng dẫn đầy đủ

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 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.

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 updown 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.

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ể.

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.

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.

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 %>
  <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 %>
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
app/javascript/channels/chat_channel.jsjavascript
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.

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ẻ

#ruby on rails
#ruby
#phỏng vấn
#active record
#phỏng vấn kỹ thuật

Chia sẻ

Bài viết liên quan