Ruby on Rails

ActiveRecord モデル

Ruby on Rails

マイグレーション・バリデーション・コールバック

マイグレーション

テーブル作成・変更・カラム型

generate.sh bash
# マイグレーション生成
rails generate model User name:string email:string:uniq age:integer
rails generate migration AddPhoneToUsers phone:string
rails generate migration CreateJoinTableUsersRoles users roles

# カラム型
# string       短い文字列(255文字)
# text         長い文字列
# integer      整数
# bigint       大きな整数(ID に使用)
# float        浮動小数点
# decimal      精度指定の小数(金額など)
# boolean      true/false
# date         日付
# datetime     日時
# timestamp    タイムスタンプ
# json / jsonb JSON(PostgreSQL)
# references   外部キー(belongs_to と対応)
db/migrate/20251024_create_users.rb ruby
class CreateUsers < ActiveRecord::Migration[8.0]
  def change
    create_table :users do |t|
      t.string   :name,  null: false
      t.string   :email, null: false
      t.string   :password_digest
      t.integer  :age
      t.decimal  :balance, precision: 10, scale: 2, default: 0
      t.boolean  :active, default: true, null: false
      t.text     :bio
      t.jsonb    :preferences, default: {}
      t.references :role, null: false, foreign_key: true
      t.timestamps  # created_at, updated_at を自動追加
    end

    add_index :users, :email, unique: true
    add_index :users, [:name, :active]  # 複合インデックス
  end
end

# テーブルの変更
class AddPhoneToUsers < ActiveRecord::Migration[8.0]
  def change
    add_column    :users, :phone,      :string
    add_column    :users, :deleted_at, :datetime
    remove_column :users, :bio,        :text
    rename_column :users, :active,     :is_active
    change_column :users, :age,        :bigint
    add_index     :users, :phone
    remove_index  :users, :phone
  end
end

バリデーション

validates・カスタムバリデーター・エラーメッセージ

app/models/user.rb ruby
class User < ApplicationRecord
  # 存在チェック
  validates :name,  presence: true
  validates :email, presence: true

  # 長さ
  validates :name,     length: { minimum: 2, maximum: 50 }
  validates :password, length: { in: 8..128 }
  validates :bio,      length: { maximum: 500 }, allow_blank: true

  # 一意性
  validates :email, uniqueness: { case_sensitive: false }
  validates :username, uniqueness: { scope: :organization_id,
                                     message: 'はこの組織内で重複しています' }

  # フォーマット
  VALID_EMAIL = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, format: { with: VALID_EMAIL }

  # 数値
  validates :age, numericality: { greater_than_or_equal_to: 0,
                                   less_than: 150,
                                   only_integer: true },
                  allow_nil: true

  # 包含
  validates :role, inclusion: { in: %w[user admin moderator] }

  # 確認
  validates :password, confirmation: true  # password_confirmationフィールドを使用

  # 条件付きバリデーション
  validates :company, presence: true, if: :corporate_account?
  validates :phone,   presence: true, unless: -> { email.present? }

  # カスタムバリデーター(メソッド)
  validate :email_not_banned

  private

  def email_not_banned
    if BannedEmail.exists?(domain: email.split('@').last)
      errors.add(:email, 'のドメインは使用できません')
    end
  end
end

# バリデーションの確認
user = User.new(name: '', email: 'invalid')
user.valid?          # false
user.invalid?        # true
user.errors.full_messages  # ['Name can't be blank', 'Email is invalid']
user.errors[:email]        # ['is invalid']
user.save            # false(バリデーション失敗)
user.save!           # ActiveRecord::RecordInvalid を raise

コールバック

before_save・after_create・around_・条件付き

app/models/post.rb ruby
class Post < ApplicationRecord
  belongs_to :user

  # コールバックの種類
  before_validation :normalize_title
  after_validation  :log_errors, if: :invalid?

  before_save  :set_slug
  after_save   :update_search_index

  before_create  :set_published_at
  after_create   :send_notification
  after_create_commit :broadcast_post  # トランザクションコミット後

  before_update  :track_changes
  after_update   :invalidate_cache

  before_destroy :check_destroyable
  after_destroy  :cleanup_attachments

  # around_コールバック
  around_save :measure_save_time

  private

  def normalize_title
    self.title = title.to_s.strip.capitalize
  end

  def set_slug
    self.slug = title.parameterize if title_changed?
  end

  def set_published_at
    self.published_at ||= Time.current if status == 'published'
  end

  def check_destroyable
    if comments.exists?
      errors.add(:base, 'コメントがあるため削除できません')
      throw(:abort)  # コールバックチェーンを中断・ロールバック
    end
  end

  def measure_save_time
    start = Time.current
    yield  # 保存処理を実行
    Rails.logger.info "Save took #{Time.current - start}s"
  end
end

# コールバックの実行順序(作成時)
# before_validation → after_validation
# → before_save → before_create
# → INSERT INTO ...
# → after_create → after_save
# → after_commit / after_create_commit