📖 Ruby on Rails Toàn tập - ActiveRecord và Migrations
80 phút

ActiveRecord và Migrations

Migrations

Tạo migration

rails g migration CreateProducts name:string price:decimal stock:integer

Migration file

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: 12, scale: 2, null: false
      t.integer :stock, default: 0, null: false
      t.references :category, null: false, foreign_key: true
      t.timestamps
    end

    add_index :products, :name
    add_index :products, [:category_id, :name], unique: true
  end
end

Migration commands

rails db:migrate
rails db:rollback
rails db:migrate:status
rails db:reset
rails db:seed

Modify table

class AddPublishedToPosts < ActiveRecord::Migration[7.1]
  def change
    add_column :posts, :published_at, :datetime
    add_index :posts, :published_at

    add_reference :posts, :user, foreign_key: true

    change_column_null :posts, :title, false
    change_column_default :posts, :views, from: nil, to: 0
  end
end

Models

Basic Model

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy

  has_secure_password

  validates :name, presence: true, length: { in: 2..100 }
  validates :email, presence: true, uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }

  before_save :downcase_email
  after_create :send_welcome_email

  scope :active, -> { where(active: true) }
  scope :recent, -> { order(created_at: :desc) }

  private

  def downcase_email
    self.email = email.downcase
  end

  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

Associations

has_many / belongs_to

class Author < ApplicationRecord
  has_many :books, dependent: :destroy
  has_many :reviews, through: :books
end

class Book < ApplicationRecord
  belongs_to :author
  has_many :reviews, dependent: :destroy
end

class Review < ApplicationRecord
  belongs_to :book
  belongs_to :user
end

has_one

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
end

class Profile < ApplicationRecord
  belongs_to :user
end

has_many :through

class Doctor < ApplicationRecord
  has_many :appointments
  has_many :patients, through: :appointments
end

class Patient < ApplicationRecord
  has_many :appointments
  has_many :doctors, through: :appointments
end

class Appointment < ApplicationRecord
  belongs_to :doctor
  belongs_to :patient
end

Polymorphic

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Post < ApplicationRecord
  has_many :comments, as: :commentable
end

class Photo < ApplicationRecord
  has_many :comments, as: :commentable
end

Querying

Basic queries

# Find
User.all
User.first
User.last
User.find(1)
User.find([1, 2, 3])
User.find_by(email: 'user@example.com')
User.find_by!(email: 'user@example.com')

# Where
User.where(active: true)
User.where(age: 18..65)
User.where.not(role: 'admin')
User.where(created_at: 1.week.ago..)

# Order
User.order(:name)
User.order(created_at: :desc)
User.order(name: :asc, created_at: :desc)

# Limit và Offset
User.limit(10).offset(20)

# Select
User.select(:id, :name)

# Group và Having
Order.group(:user_id).having('count(*) > 5').count
Order.group(:status).sum(:total)

# Joins
User.joins(:posts).where(posts: { published: true })
User.left_joins(:posts).where(posts: { id: nil })

# Includes (N+1 prevention)
Post.includes(:comments, :author).all

Find or create

User.find_or_create_by(email: 'user@example.com') do |u|
  u.name = 'John'
end

User.find_or_initialize_by(email: 'user@example.com')

User.create_or_find_by(email: 'user@example.com')

Batch processing

User.find_each(batch_size: 100) do |user|
  # process user
end

User.find_in_batches(batch_size: 100) do |users|
  # process batch
end

Scopes

class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
  scope :by_author, ->(author_id) { where(author_id: author_id) }
  scope :popular, -> { where('views > ?', 1000) }

  # Default scope (dùng cẩn thận)
  # default_scope { where(deleted_at: nil) }
end

Post.published.recent.by_author(1)
Post.published.where('views > ?', 100)

Callbacks

class Post < ApplicationRecord
  before_validation :normalize_title
  before_save :calculate_word_count
  after_save :update_search_index
  before_destroy :check_can_delete
  after_commit :send_notification, on: :create

  private

  def normalize_title
    self.title = title.strip if title.present?
  end

  def calculate_word_count
    self.word_count = body.split.size
  end

  def update_search_index
    SearchIndexJob.perform_later(self)
  end

  def check_can_delete
    throw :abort if locked?
  end

  def send_notification
    NotificationJob.perform_later(id)
  end
end

Seeds

# db/seeds.rb
10.times do |i|
  User.create!(
    name: "User #{i}",
    email: "user#{i}@example.com",
    password: "password123"
  )
end

User.find_each do |user|
  5.times do |i|
    user.posts.create!(
      title: "Post #{i} by #{user.name}",
      body: Faker::Lorem.paragraphs(number: 3).join("\n\n"),
      published: [true, false].sample
    )
  end
end
rails db:seed
rails db:reset  # drop + create + migrate + seed

Bài tập thực hành

Hãy tạo model với associations và queries!

📝 Bài tập (1)

  1. Tạo models với associations