คำถามสัมภาษณ์ Ruby on Rails: 25 อันดับในปี 2026

25 คำถามสัมภาษณ์ Ruby on Rails ที่ถูกถามบ่อยที่สุด สถาปัตยกรรม MVC, Active Record, migration, การทดสอบ RSpec, REST API พร้อมคำตอบและตัวอย่างโค้ดอย่างละเอียด

คำถามสัมภาษณ์ Ruby on Rails - คู่มือฉบับสมบูรณ์

การสัมภาษณ์ Ruby on Rails ประเมินความเชี่ยวชาญในเฟรมเวิร์ก Ruby ที่ได้รับความนิยมที่สุด ความเข้าใจในสถาปัตยกรรม MVC, ORM Active Record และความสามารถในการสร้างเว็บแอปพลิเคชันที่แข็งแกร่งตามปรัชญา "Convention over Configuration" คู่มือนี้ครอบคลุม 25 คำถามที่ถูกถามบ่อยที่สุด ตั้งแต่พื้นฐานของ Rails ไปจนถึงรูปแบบการใช้งานจริงขั้นสูง

คำแนะนำสำหรับการสัมภาษณ์

ผู้สรรหาให้ความสำคัญกับผู้สมัครที่เข้าใจปรัชญาของ Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) และรูปแบบ Rails Way การอธิบายว่าทำไม Rails จึงเลือกแนวทางสถาปัตยกรรมแบบใดแบบหนึ่งจะสร้างความแตกต่าง

พื้นฐาน Ruby on Rails

คำถามที่ 1: อธิบายรูปแบบ MVC ใน Ruby on Rails

รูปแบบ Model-View-Controller (MVC) เป็นแกนกลางของสถาปัตยกรรม Rails โดยแบ่งความรับผิดชอบออกเป็นสามชั้นที่ชัดเจน เพื่อให้บำรุงรักษาและทดสอบโค้ดได้ดียิ่งขึ้น

ruby
# app/models/article.rb
# Model จัดการข้อมูลและตรรกะทางธุรกิจ
class Article < ApplicationRecord
  # การตรวจสอบความถูกต้องของข้อมูล
  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true

  # ความสัมพันธ์กับโมเดลอื่น
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  has_many :tags, through: :article_tags

  # Scope สำหรับ query ที่ใช้ซ้ำได้
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  # Callback ของวงจรชีวิต
  before_save :generate_slug

  private

  def generate_slug
    self.slug = title.parameterize if title_changed?
  end
end
ruby
# app/controllers/articles_controller.rb
# Controller รับคำขอและประสานการตอบกลับ
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: 'สร้างบทความสำเร็จแล้ว'
    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 แสดงข้อมูลในรูปแบบ HTML %>
<article class="article-detail">
  <header>
    <h1><%= @article.title %></h1>
    <p class="meta">
      โดย <%= @article.author.name %>      <%= l @article.created_at, format: :long %>
    </p>
  </header>

  <div class="content">
    <%= simple_format @article.body %>
  </div>

  <%# Partial สำหรับความคิดเห็น %>
  <%= render @comments %>
</article>

ขั้นตอนปกติ: คำขอเข้าสู่ Router ซึ่งจะส่งต่อไปยัง Controller ที่เหมาะสม Controller จะโต้ตอบกับ Model เพื่อดึงหรือแก้ไขข้อมูล จากนั้นส่งข้อมูลให้ View เพื่อทำการเรนเดอร์ HTML

คำถามที่ 2: Active Record คืออะไร และ ORM ของ Rails ทำงานอย่างไร?

Active Record คือ ORM (Object-Relational Mapping) ของ Rails ที่นำรูปแบบ Active Record มาใช้ คลาส Model แต่ละคลาสแทนตารางในฐานข้อมูล และอินสแตนซ์แต่ละตัวแทนแถวเดียว

ruby
# app/models/user.rb
# Active Record จับคู่คอลัมน์กับแอตทริบิวต์โดยอัตโนมัติ
class User < ApplicationRecord
  # ตาราง 'users' จะถูกเชื่อมโยงโดยอัตโนมัติ
  # คอลัมน์: id, email, name, created_at, updated_at

  has_secure_password # BCrypt สำหรับรหัสผ่าน

  has_many :articles, foreign_key: :author_id
  has_one :profile, dependent: :destroy
  has_and_belongs_to_many :roles

  # การตรวจสอบความถูกต้อง
  validates :email, presence: true,
                    uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }

  # Callback
  before_save :normalize_email

  # เมธอดของคลาสสำหรับ query
  def self.admins
    joins(:roles).where(roles: { name: 'admin' })
  end

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end
ruby
# ตัวอย่าง query ของ Active Record
# Rails console หรือภายใน service

# การสร้าง
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')

# การอ่านพร้อมเงื่อนไข
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')

# Query แบบเชื่อมต่อกัน (lazy evaluation)
recent_admins = User.admins
                    .where('created_at > ?', 1.month.ago)
                    .includes(:profile)
                    .limit(10)

# ป้องกัน N+1 ด้วย eager loading
articles = Article.includes(:author, :comments).published

# การอัปเดต
user.update!(name: 'Alice Martin')

# Transaction
User.transaction do
  user.debit_balance!(100)
  recipient.credit_balance!(100)
  Payment.create!(from: user, to: recipient, amount: 100)
end

Active Record แปลงเมธอด Ruby ให้เป็น query SQL ที่ปรับให้เหมาะสม เมธอดอย่าง where, joins, includes เป็นแบบ lazy: query จะถูกประมวลผลเมื่อมีการวนซ้ำหรือเรียก to_a เท่านั้น

คำถามที่ 3: อธิบายระบบ migration ของ Rails

Migration ช่วยให้สามารถจัดการเวอร์ชันของ schema ฐานข้อมูลด้วย Ruby ได้ Migration สามารถย้อนกลับได้ และช่วยให้โครงสร้างข้อมูลพัฒนาได้อย่างมีการควบคุม

ruby
# db/migrate/20260203100000_create_products.rb
# Migration สำหรับสร้างตาราง
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 และ updated_at โดยอัตโนมัติ
    end

    # Index เพื่อประสิทธิภาพ
    add_index :products, :name
    add_index :products, [:category_id, :active]
  end
end
ruby
# db/migrate/20260203110000_add_slug_to_products.rb
# Migration สำหรับแก้ไขตารางที่มีอยู่
class AddSlugToProducts < ActiveRecord::Migration[7.1]
  def change
    add_column :products, :slug, :string
    add_index :products, :slug, unique: true

    # เติม slug ที่มีอยู่
    reversible do |dir|
      dir.up do
        Product.find_each do |product|
          product.update_column(:slug, product.name.parameterize)
        end
      end
    end

    # ทำให้ NOT NULL หลังเติมแล้ว
    change_column_null :products, :slug, false
  end
end
bash
# คำสั่ง migration ที่จำเป็น
rails db:migrate              # รัน migration ที่ค้างอยู่
rails db:rollback             # ย้อนกลับ migration ล่าสุด
rails db:rollback STEP=3      # ย้อนกลับ migration 3 รายการล่าสุด
rails db:migrate:status       # ดูสถานะ migration
rails db:seed                 # รัน db/seeds.rb
rails db:reset                # Drop, create, migrate, seed

Migration ต้องสามารถย้อนกลับได้ เมธอด change ฉลาดและสามารถย้อนกลับการดำเนินการทั่วไปได้โดยอัตโนมัติ สำหรับกรณีที่ซับซ้อน ใช้ up และ down แยกกัน

Active Record ขั้นสูง

คำถามที่ 4: จะปรับ query N+1 ใน Rails ได้อย่างไร?

ปัญหา N+1 เกิดขึ้นเมื่อ query เริ่มต้นตามมาด้วย query เพิ่มเติมอีก N รายการเพื่อโหลดความสัมพันธ์ Rails มีเมธอด eager loading หลายแบบเพื่อแก้ปัญหานี้

ruby
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    # ❌ ปัญหา N+1: 1 query + N query ต่อหนึ่งคำสั่งซื้อ
    # @orders = Order.all
    # ใน view: order.user.name สร้าง query ต่อแต่ละคำสั่งซื้อ

    # ✅ วิธีแก้ด้วย includes (eager loading)
    @orders = Order.includes(:user, :items)
                   .where(status: 'completed')
                   .order(created_at: :desc)
    # สร้างเพียง 3 query ทั้งหมด
  end

  def show
    # includes: โหลดความสัมพันธ์แยกกัน (2-3 query)
    @order = Order.includes(items: :product).find(params[:id])

    # preload: บังคับให้โหลดแยกกัน
    @order = Order.preload(:items, :user).find(params[:id])

    # eager_load: บังคับใช้ LEFT OUTER JOIN (1 query)
    @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 พร้อม includes ค่าเริ่มต้น
  scope :with_details, -> { includes(:user, items: :product) }

  # Counter cache เพื่อหลีกเลี่ยง query COUNT
  # ต้องการ: add_column :users, :orders_count, :integer, default: 0
  belongs_to :user, counter_cache: true
end
ruby
# ตรวจจับ N+1 ด้วย 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 จะแสดงการแจ้งเตือนเมื่อ:
# - ตรวจพบ query N+1
# - มี eager loading ที่ไม่จำเป็น
# - ควรใช้ counter cache

หลักการ: ใช้ includes เป็นค่าเริ่มต้น (Rails จะเลือกกลยุทธ์ที่เหมาะสมที่สุด) ใช้ preload เมื่อต้องการบังคับ query แยก และ eager_load เมื่อกรองตามความสัมพันธ์

คำถามที่ 5: อธิบาย Scope และ Query Object ใน Rails

Scope ห่อหุ้มเงื่อนไข query ที่ใช้ซ้ำได้ สำหรับ query ที่ซับซ้อน Query Object ให้การจัดระเบียบและความสามารถในการทดสอบที่ดีกว่า

ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Scope แบบง่าย
  scope :active, -> { where(active: true) }
  scope :in_stock, -> { where('stock_quantity > 0') }
  scope :featured, -> { where(featured: true) }

  # Scope ที่มีพารามิเตอร์
  scope :cheaper_than, ->(price) { where('price < ?', price) }
  scope :in_category, ->(category) { where(category: category) }

  # Scope ที่เชื่อมต่อกันได้
  scope :available, -> { active.in_stock }

  # Scope ที่มี joins
  scope :with_recent_orders, -> {
    joins(:order_items)
      .where('order_items.created_at > ?', 30.days.ago)
      .distinct
  }

  # Scope ที่มี subquery
  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 สำหรับการค้นหาที่ซับซ้อน
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

# การใช้งานใน controller
@products = ProductsSearchQuery.new(Product.active).call(params)

Scope เหมาะสำหรับเงื่อนไขที่เรียบง่ายและใช้ซ้ำได้ Query Object เหมาะสำหรับการค้นหาที่ซับซ้อนซึ่งมีฟิลเตอร์ทางเลือกหลายแบบและตรรกะการประกอบ

พร้อมที่จะพิชิตการสัมภาษณ์ Ruby on Rails แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Routing และ Controller

คำถามที่ 6: RESTful routing ใน Rails ทำงานอย่างไร?

Rails ส่งเสริมเส้นทางแบบ RESTful ซึ่งจับคู่ HTTP verb กับการกระทำ CRUD Router แปลง URL ให้เป็นการเรียก controller ที่เฉพาะเจาะจง

ruby
# config/routes.rb
Rails.application.routes.draw do
  # เส้นทาง RESTful มาตรฐาน (7 การกระทำ)
  resources :articles do
    # เส้นทางซ้อนกัน
    resources :comments, only: [:create, :destroy]

    # เส้นทาง member (ทำงานบนอินสแตนซ์)
    member do
      post :publish
      delete :archive
    end

    # เส้นทาง collection (ทำงานบนชุดข้อมูล)
    collection do
      get :drafts
      get :search
    end
  end

  # เส้นทาง API พร้อม namespace
  namespace :api do
    namespace :v1 do
      resources :products, only: [:index, :show, :create, :update] do
        resources :reviews, shallow: true
      end
    end
  end

  # เส้นทางกำหนดเอง
  get 'dashboard', to: 'dashboard#index'

  # ข้อจำกัดเส้นทาง
  constraints(SubdomainConstraint.new) do
    resources :admin_settings
  end

  # เส้นทางหลัก
  root 'home#index'
end
bash
# rails routes - แสดงเส้นทางทั้งหมดที่สร้างขึ้น
#
# 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

Helper เส้นทางที่สร้างขึ้น (article_path(@article), new_article_path) ช่วยให้สามารถอ้างอิง URL ได้อย่างไดนามิกและง่ายต่อการบำรุงรักษา

คำถามที่ 7: อธิบาย callback และ filter ใน controller

Callback (before_action, after_action, around_action) ช่วยให้สามารถรันโค้ดก่อน หลัง หรือรอบ ๆ การกระทำของ controller

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # การป้องกัน CSRF เปิดใช้งานโดยเริ่มต้น
  protect_from_forgery with: :exception

  # Callback สากลสำหรับการยืนยันตัวตน
  before_action :authenticate_user!

  # การจัดการข้อผิดพลาดสากล
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private

  def not_found
    render json: { error: 'ไม่พบทรัพยากร' }, 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 พร้อมตัวเลือก
  before_action :require_admin
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  after_action :log_activity, only: [:create, :update, :destroy]

  # Callback แบบมีเงื่อนไข
  before_action :check_stock, only: [:update], if: :stock_changed?

  def create
    @product = Product.new(product_params)

    if @product.save
      redirect_to [:admin, @product], notice: 'สร้างสินค้าแล้ว'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def update
    if @product.update(product_params)
      redirect_to [:admin, @product], notice: 'อัปเดตสินค้าแล้ว'
    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 จะถูกรันตามลำดับการประกาศ ใช้ skip_before_action ในคลาสย่อยเพื่อปิดการใช้งาน callback ที่สืบทอดมา หลีกเลี่ยง callback ที่มีตรรกะทางธุรกิจมากเกินไป ให้เลือกใช้ Service Object แทน

Service และสถาปัตยกรรม

คำถามที่ 8: จะนำ Service Object มาใช้ใน Rails ได้อย่างไร?

Service Object ห่อหุ้มตรรกะทางธุรกิจที่ซับซ้อนซึ่งไม่ควรอยู่ใน Model หรือ Controller ช่วยให้ทดสอบได้ดีขึ้นและสอดคล้องกับหลักการความรับผิดชอบเดียว

ruby
# app/services/order_processor.rb
# Service Object ที่มีอินเทอร์เฟซมาตรฐาน
class OrderProcessor
  def initialize(order, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  def call
    return failure('คำสั่งซื้อถูกประมวลผลแล้ว') 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("การชำระเงินล้มเหลว: #{e.message}")
  rescue InsufficientStockError => e
    failure("สต็อกไม่เพียงพอ: #{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: "คำสั่งซื้อ ##{@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: 'ยืนยันคำสั่งซื้อแล้ว!'
      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

รูปแบบ Service Object ปฏิบัติตามแบบแผนง่าย ๆ คือ คลาสเดียว ความรับผิดชอบเดียว เมธอด call สาธารณะหนึ่งเมธอด การคืนค่าออบเจกต์ Result ช่วยให้จัดการความสำเร็จและความล้มเหลวได้อย่างเป็นระเบียบ

คำถามที่ 9: อธิบาย Concern ใน Rails

Concern ช่วยให้สามารถดึงและแชร์โค้ดระหว่าง Model หรือ Controller ได้ ใช้ ActiveSupport::Concern เพื่อให้ syntax การ include สะอาด

ruby
# app/models/concerns/sluggable.rb
# Concern ที่ใช้ซ้ำได้สำหรับสร้าง slug
module Sluggable
  extend ActiveSupport::Concern

  included do
    # โค้ดที่รันเมื่อ include
    before_validation :generate_slug, if: :should_generate_slug?
    validates :slug, presence: true, uniqueness: true
  end

  # เมธอดของคลาส
  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

  # เมธอดของอินสแตนซ์
  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 # ทางเลือก ค่าเริ่มต้นคือ :title
end

# app/models/product.rb
class Product < ApplicationRecord
  include Sluggable

  sluggable_source :name
end
ruby
# app/controllers/concerns/pagination.rb
# Concern สำหรับ 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 มีประโยชน์สำหรับโค้ดที่แชร์กันจริง ๆ หลีกเลี่ยงการสร้าง Concern เพียงเพื่อ "ทำให้ Model สั้นลง" เพราะจะซ่อนความซับซ้อนโดยไม่ลดมัน

การทดสอบด้วย RSpec

คำถามที่ 10: จะจัดโครงสร้างการทดสอบ RSpec ใน Rails อย่างไร?

RSpec คือเฟรมเวิร์กการทดสอบมาตรฐานสำหรับ Rails โครงสร้างการทดสอบที่ดีรวมถึง Model spec, Controller spec, Service spec และการทดสอบการรวม

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # Factory ด้วย 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 'ตรวจสอบรูปแบบของ 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 'คืนค่าชื่อจริงและนามสกุลรวมกัน' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end

    it 'จัดการกรณีที่ไม่มีนามสกุล' do
      user = build(:user, first_name: 'John', last_name: nil)
      expect(user.full_name).to eq('John')
    end
  end

  describe '.active' do
    it 'คืนเฉพาะผู้ใช้ที่ยังใช้งาน' 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 'เมื่อคำสั่งซื้อถูกต้อง' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: true, transaction_id: 'txn_123')
        )
      end

      it 'ประมวลผลคำสั่งซื้อสำเร็จ' do
        result = subject.call

        expect(result).to be_success
        expect(order.reload.status).to eq('completed')
      end

      it 'ลดสต็อกของสินค้า' do
        expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
      end

      it 'ส่งอีเมลยืนยัน' do
        expect { subject.call }
          .to have_enqueued_mail(OrderMailer, :confirmation)
          .with(order)
      end
    end

    context 'เมื่อการชำระเงินล้มเหลว' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: false, error: 'Card declined')
        )
      end

      it 'คืนค่าผลลัพธ์ว่าล้มเหลว' do
        result = subject.call

        expect(result).to be_failure
        expect(result.error).to include('Card declined')
      end

      it 'ไม่อัปเดตสถานะคำสั่งซื้อ' 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 'คืนค่ารายการสินค้า' do
      get '/api/v1/products', headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(3)
    end

    it 'กรองตามหมวดหมู่' 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: 'สินค้าใหม่', price: 99.99, category_id: create(:category).id } }
    end

    it 'สร้างสินค้าใหม่' 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

แนวปฏิบัติที่ดี: ใช้ let สำหรับข้อมูล describe สำหรับเมธอด/บริบท context สำหรับเงื่อนไข และ it สำหรับ assertion ที่เฉพาะเจาะจง การทดสอบหนึ่งควรทดสอบเพียงเรื่องเดียว

คำถามที่ 11: จะใช้ factory กับ FactoryBot อย่างไร?

FactoryBot ช่วยสร้างข้อมูลทดสอบในรูปแบบ declarative และง่ายต่อการบำรุงรักษา Factory แทนที่ fixture แบบสถิต

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # Sequence เพื่อรับประกันความเป็นเอกลักษณ์
    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 สำหรับรูปแบบต่าง ๆ
    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 ที่สืบทอด
    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
# การใช้งานในการทดสอบ
RSpec.describe OrderProcessor do
  # build: อินสแตนซ์ที่ไม่บันทึก
  let(:user) { build(:user) }

  # create: บันทึกในฐานข้อมูล
  let(:order) { create(:order, :with_items, user: user) }

  # create_list: หลายอินสแตนซ์
  let(:products) { create_list(:product, 5) }

  # รวม trait
  let(:admin) { create(:user, :admin, :with_profile) }

  # เขียนทับแอตทริบิวต์
  let(:expensive_order) { create(:order, :with_items, items_count: 10) }

  # build_stubbed: เร็วกว่า สำหรับ unit test
  let(:stubbed_user) { build_stubbed(:user) }
end

ใช้ build หรือ build_stubbed แทน create เมื่อไม่จำเป็นต้องบันทึก: สิ่งนี้ทำให้การทดสอบเร็วขึ้นอย่างมาก

งานเบื้องหลัง

คำถามที่ 12: จะใช้ Active Job และ Sidekiq ใน Rails อย่างไร?

Active Job ให้อินเทอร์เฟซที่เป็นหนึ่งเดียวสำหรับงานเบื้องหลัง โดยไม่ขึ้นอยู่กับ backend (Sidekiq, Resque ฯลฯ) Sidekiq เป็นตัวเลือกที่ได้รับความนิยมเนื่องจากประสิทธิภาพกับ Redis

ruby
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  queue_as :default

  # การกำหนดค่าการลองใหม่
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
  discard_on ActiveJob::DeserializationError

  # ตัวเลือก Sidekiq (หาก backend เป็น Sidekiq)
  sidekiq_options retry: 5, backtrace: true

  def perform(order_id)
    order = Order.find(order_id)

    OrderProcessor.new(order).call
  rescue ActiveRecord::RecordNotFound
    # คำสั่งซื้อถูกลบระหว่างคิวและการรัน
    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

  # จำกัดอัตราด้วย Sidekiq Enterprise หรือ 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
# จัดคิวงาน
# ทันที
ProcessOrderJob.perform_later(order.id)

# ล่าช้า
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)

# ในเวลาที่กำหนด
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)

# คิวเฉพาะ
ProcessOrderJob.set(queue: :critical).perform_later(order.id)

# แบบซิงโครนัส (สำหรับการทดสอบหรือดีบัก)
ProcessOrderJob.perform_now(order.id)
ruby
# config/sidekiq.yml
:concurrency: 10
:queues:
  - [critical, 3]    # ลำดับความสำคัญสูง น้ำหนัก 3
  - [default, 2]     # ลำดับความสำคัญปานกลาง น้ำหนัก 2
  - [mailers, 1]     # ลำดับความสำคัญต่ำ น้ำหนัก 1
  - [low, 1]

:schedule:
  cleanup_job:
    cron: '0 3 * * *'  # ทุกวันเวลา 3 นาฬิกา
    class: CleanupJob

Active Job แยกตัว backend ออก แต่การเข้าถึงคุณสมบัติเฉพาะ (batch, rate limiting) มักต้องการการเชื่อมโยงกับ backend ที่เลือก

พร้อมที่จะพิชิตการสัมภาษณ์ Ruby on Rails แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การพัฒนา API

คำถามที่ 13: จะสร้าง RESTful API ด้วย Rails อย่างไร?

Rails ทำให้การสร้าง API JSON ง่ายขึ้นด้วย Controller แบบ API-only และ serializer API ที่ดีต้องมีเวอร์ชัน เอกสารและความปลอดภัย

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: 'ไม่พบทรัพยากร', details: exception.message },
               status: :not_found
      end

      def unprocessable_entity(exception)
        render json: { error: 'การตรวจสอบล้มเหลว', details: exception.record.errors },
               status: :unprocessable_entity
      end

      def bad_request(exception)
        render json: { error: 'คำขอไม่ถูกต้อง', 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
# ด้วย 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

แนวปฏิบัติ API ที่ดี: จัดเวอร์ชันผ่าน namespace ใช้รหัส HTTP ที่เหมาะสม แบ่งหน้าชุดข้อมูล และให้ข้อความข้อผิดพลาดที่ชัดเจน

คำถามที่ 14: จะนำการยืนยันตัวตนแบบ JWT มาใช้ใน Rails อย่างไร?

JWT (JSON Web Tokens) เป็นวิธีการยืนยันตัวตนแบบ stateless ที่ได้รับความนิยมสำหรับ API Token เข้ารหัสตัวตนและความถูกต้องของผู้ใช้

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 หมดอายุ'
    rescue JWT::DecodeError
      raise AuthenticationError, 'Token ไม่ถูกต้อง'
    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: 'ข้อมูลรับรองไม่ถูกต้อง' }, 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, 'ไม่มี 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: 'ไม่พบผู้ใช้' }, status: :unauthorized
  end

  def current_user
    @current_user
  end
end

สำหรับ production พิจารณา: refresh token, การ blacklist token เมื่อออกจากระบบ และเวลาหมดอายุที่สั้น Gem เช่น devise-jwt ทำให้การติดตั้งใช้งานง่ายขึ้น

การ Cache และประสิทธิภาพ

คำถามที่ 15: จะนำการ cache มาใช้ใน Rails อย่างไร?

Rails มีระดับการ cache หลายระดับ: fragment caching, Russian Doll caching, low-level caching การเลือกขึ้นอยู่กับกรณีการใช้งาน

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 ด้วยกุญแจ cache อัตโนมัติ %>
<% @products.each do |product| %>
  <%# Cache อิงตาม updated_at ของสินค้า %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

<%# Russian Doll caching - cache ซ้อนกัน %>
<% cache ['v1', @category] do %>
  <h2><%= @category.name %></h2>

  <% @category.products.each do |product| %>
    <% cache ['v1', product] do %>
      <%= render product %>
    <% end %>
  <% end %>
<% end %>

<%# Cache แบบมีเงื่อนไข %>
<% cache_if current_user.nil?, @product do %>
  <%= render @product %>
<% end %>
ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Touch parent เพื่อยกเลิก cache แบบ Russian Doll
  belongs_to :category, touch: true

  # กุญแจ cache แบบกำหนดเอง
  def cache_key_with_version
    "#{super}/#{reviews.maximum(:updated_at)&.to_i}"
  end
end
ruby
# Low-level caching ใน 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 พร้อมป้องกัน race condition
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  Product.bestsellers.limit(10).to_a
end

# การยกเลิกอย่างชัดเจน
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')

Russian Doll caching มีประสิทธิภาพเพราะมีการสร้างใหม่เฉพาะส่วนที่แก้ไขเท่านั้น ใช้ touch: true บนความสัมพันธ์เพื่อกระจายการยกเลิก

คำถามที่ 16: จะปรับประสิทธิภาพของแอปพลิเคชัน Rails ได้อย่างไร?

การปรับ Rails ครอบคลุมหลายแง่มุม: query DB, cache, asset และสถาปัตยกรรม วิธีการที่เป็นระบบพร้อมการมอนิเตอร์เป็นสิ่งสำคัญ

ruby
# การปรับฐานข้อมูล
# 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 แบบรวมสำหรับ query ที่ใช้บ่อย
  # add_index :orders, [:user_id, :status, :created_at]

  # เลือกเฉพาะคอลัมน์ที่จำเป็น
  scope :summary, -> { select(:id, :status, :total, :created_at) }

  # ประมวลผลแบบ batch สำหรับปริมาณมาก
  def self.process_pending
    pending.find_each(batch_size: 1000) do |order|
      ProcessOrderJob.perform_later(order.id)
    end
  end

  # หลีกเลี่ยงการคำนวณซ้ำ
  def self.revenue_by_month
    completed
      .group("DATE_TRUNC('month', created_at)")
      .sum(:total)
  end
end
ruby
# การปรับหน่วยความจำ
# 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 ด้วย 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 และการแบ่งหน้า
class ProductsController < ApplicationController
  def index
    @products = Product.active
                       .includes(:category, :primary_image)
                       .page(params[:page])
                       .per(24)

    # Prefetch สำหรับหน้าถัดไป
    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

เครื่องมือสำคัญ: rack-mini-profiler สำหรับ profiling, bullet สำหรับตรวจหา N+1, New Relic หรือ Scout สำหรับมอนิเตอร์ production

ความปลอดภัย

คำถามที่ 17: แนวปฏิบัติที่ดีด้านความปลอดภัยใน Rails คืออะไร?

Rails มีการป้องกันค่าเริ่มต้นต่อช่องโหว่ที่พบบ่อย การเข้าใจและกำหนดค่าการป้องกันเหล่านี้อย่างถูกต้องเป็นสิ่งสำคัญ

ruby
# การป้องกัน CSRF
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # เปิดใช้งานโดยเริ่มต้น โยน exception หาก token ไม่ถูกต้อง
  protect_from_forgery with: :exception

  # สำหรับ API ใช้ :null_session
  # protect_from_forgery with: :null_session
end

# ใน view, token จะถูกแทรกในแบบฟอร์มโดยอัตโนมัติ
# <%= form_with ... %> รวม authenticity_token

# สำหรับคำขอ AJAX
# เพิ่ม header X-CSRF-Token พร้อมค่าจาก csrf_meta_tags
ruby
# การป้องกัน SQL Injection
# ✅ พารามิเตอร์แบบ interpolation จะถูก escape อัตโนมัติ
User.where('email = ?', params[:email])
User.where(email: params[:email])

# ❌ อันตราย - การ interpolation โดยตรง
User.where("email = '#{params[:email]}'")

# ✅ สำหรับ ORDER แบบไดนามิก
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)
ruby
# การป้องกัน XSS
# Rails escape HTML ใน view โดยอัตโนมัติ

# ✅ Escape อัตโนมัติ
<%= user.name %>

# ❌ อันตราย - เนื้อหาที่ไม่ escape
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>

# ✅ สำหรับ HTML ที่ปลอดภัย ใช้ 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 ที่ชัดเจนของ attribute ที่อนุญาต
    params.require(:user).permit(:name, :email, :avatar)

    # เฉพาะสำหรับผู้ดูแลระบบ
    if current_user.admin?
      params.require(:user).permit(:name, :email, :role, :active)
    else
      params.require(:user).permit(:name, :email)
    end
  end
end
ruby
# Header ความปลอดภัย
# 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

ตรวจสอบเป็นประจำด้วย brakeman (การวิเคราะห์ความปลอดภัยแบบสถิต) และอัปเดต gem ด้วย bundle audit

คำถามที่ 18: จะจัดการการยืนยันตัวตนและการอนุญาตใน Rails อย่างไร?

การยืนยันตัวตนตรวจสอบตัวตน การอนุญาตควบคุมสิทธิ์ Devise จัดการการยืนยันตัวตน Pundit หรือ CanCanCan จัดการการอนุญาต

ruby
# การตั้งค่า 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 ของ 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 สำหรับชุดข้อมูล
  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 พร้อม 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: 'อัปเดตบทความแล้ว'
    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: 'เผยแพร่บทความแล้ว'
  end

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "คุณไม่ได้รับอนุญาตให้ดำเนินการนี้"
    redirect_back(fallback_location: root_path)
  end
end

Pundit ชัดเจนและทดสอบได้ง่ายกว่า CanCanCan การกระทำแต่ละอย่างมีเมธอด policy ที่สอดคล้องกัน และ scope จะกรองชุดข้อมูลโดยอัตโนมัติ

Rails ขั้นสูง

คำถามที่ 19: อธิบายรูปแบบ Repository ใน Rails

รูปแบบ Repository แยกตรรกะการเข้าถึงข้อมูลออกจากส่วนที่เหลือของแอปพลิเคชัน แม้ว่า Rails จะใช้ Active Record (รูปแบบที่แตกต่างกัน) แต่ Repository อาจมีประโยชน์ในกรณีที่ซับซ้อน

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
# การใช้งานใน 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

# ทำให้ทดสอบด้วย mock ได้สะดวก
RSpec.describe ProductSearchService do
  let(:repository) { instance_double(ProductRepository) }
  let(:service) { described_class.new(repository: repository) }

  it 'กรองตามหมวดหมู่' 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 เป็นทางเลือกใน Rails เนื่องจาก Active Record เป็นรูปแบบที่ยอดเยี่ยมอยู่แล้ว ใช้สำหรับ query ที่ซับซ้อนหรือเมื่อการแยกส่วนเก็บข้อมูลเป็นสิ่งสำคัญ

คำถามที่ 20: จะนำรูปแบบ CQRS มาใช้ใน Rails อย่างไร?

CQRS (Command Query Responsibility Segregation) แยกการอ่านและเขียนข้อมูล ใน Rails หมายถึงคลาสแยกกันสำหรับ query และ 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, "สินค้า #{item[:product_id]} ไม่พร้อมใช้งาน")
        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 ที่ใช้ 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: 'สร้างคำสั่งซื้อแล้ว!'
    else
      flash.now[:alert] = result.errors.join(', ')
      render :new, status: :unprocessable_entity
    end
  end
end

CQRS เด่นในแอปพลิเคชันที่ซับซ้อนซึ่งมีความต้องการอ่าน/เขียนไม่สมมาตร สำหรับ CRUD ทั่วไปจะเป็นการ over-engineering

คำถามที่ 21: จะจัดการ WebSocket ด้วย Action Cable อย่างไร?

Action Cable รวม WebSocket เข้ากับ Rails สำหรับการสื่อสารสองทางแบบเรียลไทม์ ใช้ Redis เพื่อซิงค์ระหว่างเซิร์ฟเวอร์

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
      # ผ่าน cookie ของเซสชัน
      if verified_user = User.find_by(id: cookies.encrypted[:user_id])
        verified_user
      # ผ่าน JWT สำหรับ 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])

    # ตรวจสอบสิทธิ์
    unless @room.accessible_by?(current_user)
      reject
      return
    end

    stream_for @room

    # แจ้งคนอื่นเรื่องการเข้ามา
    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']
    )

    # กระจายไปยังผู้สมัครรับทุกคน
    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 จัดการการเชื่อมต่อใหม่และการซิงค์โดยอัตโนมัติ ใน production ให้ตั้ง Redis เป็น adapter และปรับขนาดตามจำนวนการเชื่อมต่อพร้อมกัน

คำถามที่ 22: จะนำ multi-tenancy มาใช้ใน Rails อย่างไร?

Multi-tenancy ช่วยให้แอปพลิเคชันให้บริการลูกค้า (tenant) หลายรายที่แยกออกจากกัน มีสามวิธีหลัก: ระดับฐานข้อมูล, schema หรือแถว

ruby
# Multitenancy ระดับแถวด้วย ActsAsTenant หรือทำเอง
# app/models/concerns/tenant_scoped.rb
module TenantScoped
  extend ActiveSupport::Concern

  included do
    belongs_to :tenant

    # Scope เริ่มต้นที่ tenant ปัจจุบัน
    default_scope -> { where(tenant: Current.tenant) if Current.tenant }

    # การตรวจสอบ 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
    # ผ่าน subdomain
    if request.subdomain.present? && request.subdomain != 'www'
      Tenant.find_by!(subdomain: request.subdomain)
    # ผ่าน header (สำหรับ API)
    elsif request.headers['X-Tenant-ID'].present?
      Tenant.find(request.headers['X-Tenant-ID'])
    # ผ่านผู้ใช้
    elsif current_user
      current_user.tenant
    end
  rescue ActiveRecord::RecordNotFound
    redirect_to root_url(subdomain: 'www'), alert: 'ไม่พบ 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

  # ผู้ดูแลสามารถเป็นสมาชิกของหลาย tenant
  has_many :tenant_memberships
  has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end
ruby
# ระดับ schema ด้วย gem Apartment (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
  config.excluded_models = %w[Tenant User]
  config.tenant_names = -> { Tenant.pluck(:subdomain) }
end

# การใช้งาน
Apartment::Tenant.switch('acme') do
  # query ทั้งหมดในบล็อกนี้ใช้ schema 'acme'
  Project.all # SELECT * FROM acme.projects
end

ระดับแถวง่ายที่สุดแต่ต้องเฝ้าระวังการรั่วไหลตลอดเวลา ระดับ schema ให้การแยกที่ดีกว่าแต่ทำให้ migration ซับซ้อน เลือกตามความต้องการด้านความปลอดภัยและความสามารถในการขยายขนาด

คำถามที่ 23: จะตั้งค่าสถาปัตยกรรม microservice ด้วย Rails อย่างไร?

Rails สามารถใช้เป็นพื้นฐานสำหรับสถาปัตยกรรม microservice ที่สื่อสารผ่าน HTTP/gRPC หรือ message queue กุญแจคือการกำหนดขอบเขตที่ชัดเจน

ruby
# Client ของบริการ 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('บริการไม่พร้อมใช้งาน', code: response.code)
    end
  rescue Net::OpenTimeout, Net::ReadTimeout
    ServiceResult.failure('Timeout ของบริการ')
  end
end
ruby
# การสื่อสารแบบ event-driven ด้วย 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
# รูปแบบ API Gateway
# app/controllers/api/v1/gateway_controller.rb
module Api
  module V1
    class GatewayController < BaseController
      # รวมหลายบริการ
      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} ไม่พร้อมใช้งาน", message: e.message }
      end
    end
  end
end

สำหรับ microservice ของ Rails: กำหนดสัญญา API ที่ชัดเจน (OpenAPI), ติดตั้ง circuit breaker (gem circuitbox) และใช้ tracing แบบกระจาย (gem opentelemetry)

คำถามที่ 24: จะ deploy แอปพลิเคชัน Rails ไปยัง production อย่างไร?

การ deploy Rails สมัยใหม่ใช้คอนเทนเนอร์หรือ PaaS การกำหนดค่า production ที่แข็งแรงครอบคลุม asset, ฐานข้อมูลและการมอนิเตอร์

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
  }

  # บังคับใช้ 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:

Checklist สำหรับ production: บังคับใช้ SSL, secret ผ่าน ENV, health check, สำรองข้อมูล DB อัตโนมัติ, มอนิเตอร์ (APM + log + metric) และตั้งค่าการแจ้งเตือน

คำถามที่ 25: คุณสมบัติใหม่ใน Rails 7+ ที่ควรรู้มีอะไรบ้าง?

Rails 7+ นำมาซึ่งการเปลี่ยนแปลงสำคัญ: Hotwire ค่าเริ่มต้น, import map, credentials ที่เข้ารหัสปรับปรุงและการเพิ่มประสิทธิภาพมากมาย

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 "โหลดเพิ่ม", articles_path(page: @page + 1),
              data: { turbo_frame: "articles" } %>
<% end %>

# Turbo Stream สำหรับการอัปเดตเรียลไทม์
# 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 (โดยไม่ใช้ JavaScript bundler)
# 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 จาก 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  # อนุญาตให้ค้นหา
  encrypts :phone_number                 # ไม่กำหนดได้โดยเริ่มต้น
  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
# การปรับปรุงอินเทอร์เฟซ query
# Rails 7.1+

# Query แบบ asynchronous
users = User.where(active: true).load_async
# ทำงานต่อขณะ query ทำงานอยู่
# เข้าถึงผลลัพธ์ด้วย 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')

# ตรวจจับ inverse_of อัตโนมัติ
class Author < ApplicationRecord
  has_many :books # inverse_of ตรวจจับโดยอัตโนมัติ
end

# Strict loading โดยเริ่มต้น (ป้องกัน N+1)
class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true
end

Rails 7+ เน้นความเรียบง่าย (ไม่มี Webpack โดยเริ่มต้น) และ HTML-over-the-wire ด้วย Hotwire แนวทางนี้ลดความซับซ้อนของ JavaScript ในขณะที่ให้ประสบการณ์ผู้ใช้ที่ทันสมัย

บทสรุป

การสัมภาษณ์ Ruby on Rails ประเมินความเชี่ยวชาญในเฟรมเวิร์กทั้งหมดและความเข้าใจในแบบแผนต่าง ๆ จุดสำคัญที่ควรจดจำ:

พื้นฐาน: MVC, Active Record, migration, การตรวจสอบและความสัมพันธ์

สถาปัตยกรรม: Service Object, Concern, Query Object และรูปแบบ CQRS

ประสิทธิภาพ: query N+1, caching (fragment, Russian Doll, low-level), eager loading

การทดสอบ: RSpec, FactoryBot, request spec และแนวปฏิบัติที่ดีในการทดสอบ

ความปลอดภัย: CSRF, SQL injection, XSS, Strong Parameters และการยืนยันตัวตน/อนุญาต

API: การออกแบบ RESTful, JWT, serializer และการจัดการเวอร์ชัน

Production: งานเบื้องหลัง, WebSocket, การ deploy และการมอนิเตอร์

ปรัชญาของ Rails (Convention over Configuration, DRY และ Rails Way) ชี้นำการตัดสินใจด้านสถาปัตยกรรมทั้งหมด การเชี่ยวชาญในหลักการเหล่านี้และการรู้ว่าเมื่อใดควรเบี่ยงเบนจากมัน แสดงให้เห็นถึงความเชี่ยวชาญที่มั่นคง

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#ruby on rails
#ruby
#สัมภาษณ์
#active record
#สัมภาษณ์เทคนิค

แชร์

บทความที่เกี่ยวข้อง