본문으로 건너뛰기
김신건의 로그

[Rails] ActiveRecord 기초: Model, Migration, CRUD

· 수정 · 📖 약 1분 · 348자/단어 #rails #activerecord #orm #model
activerecord basics, rails model, rails migration, create_table, AR CRUD

정의

ActiveRecord는 Rails의 ORM. 클래스 ↔ 테이블 1:1 매핑(관례), 인스턴스 = 행. Active Record 패턴 그대로(데이터 + 행동). 마이그레이션으로 스키마 버전 관리, 풍부한 Query DSL, 콜백·검증·관계 내장.

모델 생성

rails generate model Post title:string body:text published:boolean

# 만들어지는 것:
# - app/models/post.rb
# - db/migrate/20260620_create_posts.rb
# - test/models/post_test.rb
# - test/fixtures/posts.yml
# app/models/post.rb
class Post < ApplicationRecord
end

# db/migrate/20260620120000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.2]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :body
      t.boolean :published, default: false
      t.timestamps    # created_at, updated_at 자동
    end
  end
end

마이그레이션 실행

rails db:migrate                # 적용
rails db:rollback               # 마지막 1개 되돌리기
rails db:rollback STEP=3
rails db:migrate:status         # 현재 상태
rails db:reset                  # drop + create + migrate + seed
rails db:seed

CRUD

# Create
post = Post.new(title: "Hello")
post.body = "..."
post.save                       # bool 반환 (검증 실패면 false)
post.save!                      # 실패 시 예외

post = Post.create(title: "Hello", body: "...")   # new + save 한 번에
post = Post.create!(...)        # 예외

# Read
Post.find(1)                    # PK, 없으면 RecordNotFound
Post.find_by(slug: "hello")     # 단일, 없으면 nil
Post.find_by!(slug: "hello")    # 예외
Post.where(published: true)
Post.first
Post.last
Post.all                        # 보통 안 씀 (큰 테이블 위험)

# Update
post.title = "New"
post.save
post.update(title: "New", body: "...")    # 한 번에
post.update!(title: "New")

# 일괄
Post.where(published: false).update_all(featured: false)    # 검증·콜백 우회 (빠름)

# Delete
post.destroy                    # 콜백 실행
post.destroy!
Post.destroy_all                # 모든 행 (콜백 실행, 느림)
Post.delete_all                 # SQL DELETE (콜백 우회)
post.delete                     # 단일, 콜백 우회

마이그레이션 자주 쓰는 메서드

class Example < ActiveRecord::Migration[7.2]
  def change
    create_table :posts do |t|
      t.string :title, null: false, limit: 200
      t.text :body
      t.references :author, null: false, foreign_key: true   # FK
      t.references :category, foreign_key: true
      t.boolean :published, default: false, null: false
      t.integer :views, default: 0
      t.decimal :price, precision: 10, scale: 2
      t.datetime :published_at
      t.timestamps

      t.index :slug, unique: true
      t.index [:author_id, :created_at]
    end
  end
end

# 컬럼 추가
add_column :posts, :slug, :string
add_index :posts, :slug, unique: true

# 변경
change_column :posts, :title, :string, limit: 500
change_column_null :posts, :title, false
change_column_default :posts, :published, from: nil, to: false

# 삭제
remove_column :posts, :slug
remove_index :posts, :slug

# 이름 변경
rename_column :posts, :body, :content
rename_table :posts, :articles

# 참조
add_reference :posts, :author, foreign_key: true
remove_reference :posts, :author, foreign_key: true

마이그레이션 generator

rails g migration AddSlugToPosts slug:string:uniq
rails g migration AddAuthorRefToPosts author:references
rails g migration CreateJoinTableAuthorsPosts authors posts

이름 규칙으로 자동 생성.

strong migrations / safe migrations

큰 테이블에 락 거는 마이그레이션은 위험. strong_migrations gem이 dangerous한 작업 차단.

# Gemfile
gem "strong_migrations"

NULL not null 추가, default with backfill 등이 자동 감지.

트랜잭션

ActiveRecord::Base.transaction do
  user.update!(balance: user.balance - amount)
  recipient.update!(balance: recipient.balance + amount)
end
# 예외 시 rollback

# 중첩 (savepoint)
ActiveRecord::Base.transaction do
  ...
  ActiveRecord::Base.transaction(requires_new: true) do
    ...    # 이 내부 실패는 outer는 안 깨짐
  end
end

더티 추적

post.title = "New"
post.title_changed?          # true
post.title_was               # 이전 값
post.changes                 # {"title" => ["old", "new"]}
post.changed                 # ["title"]
post.changed_attributes      # {"title" => "old"}

# 저장 후
post.saved_change_to_title?
post.saved_change_to_title   # ["old", "new"]

콜백/검증에서 활용.

직렬화 / JSON

post.as_json
post.to_json
post.attributes

API에는 ActiveModel::Serializer, Jbuilder, Alba, Blueprinter 같은 라이브러리.

한 번에 가져오기 / 청크

Post.find_each(batch_size: 1000) do |p|
  process(p)
end

Post.in_batches(of: 1000) do |batch|
  batch.update_all(processed: true)
end

큰 테이블 순회. each는 모두 메모리에 로드 → 위험.

reload, refresh

post.reload          # DB 다시 읽기 (변경사항 버림)

콜백/외부에서 DB 변경 후 동기화.

자주 보는 패턴

counter cache

class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end

# Post에 comments_count 컬럼 자동 갱신
post.comments_count    # SQL 안 함

touch

class Comment < ApplicationRecord
  belongs_to :post, touch: true
end

# Comment 변경 시 post.updated_at 갱신

캐시 무효화에 유용.

enum (7.0+)

class Post < ApplicationRecord
  enum :status, { draft: 0, published: 1, archived: 2 }
end

post.draft?
post.published!
Post.published     # 스코프 자동

가상 컬럼

class User < ApplicationRecord
  attribute :full_name, :string, default: -> { "#{first_name} #{last_name}" }
end

# 또는 DB store
class User < ApplicationRecord
  store :preferences, accessors: [:theme, :language]
end

user.theme = "dark"
user.save

함정

1. find vs find_by

Post.find(1)              # 1 없으면 ActiveRecord::RecordNotFound (404 자동)
Post.find_by(id: 1)       # 없으면 nil
Post.find_by!(id: 1)      # 없으면 예외

view에서 자동 404 원하면 find.

2. N+1

Post.all.each { |p| puts p.author.name }    # N+1
Post.includes(:author).each { ... }          # 해결 (eager load)

3. update_all은 콜백 우회

Post.update_all(published: true)    # before_save 등 미실행

검증·콜백 필요하면 루프 + save.

4. save 없이 변경

post.title = "New"
# save 호출 안 함 → 변경 영원히 안 됨

update가 더 명확.

5. 마이그레이션 순서

여러 개발자가 같은 base에서 다른 마이그레이션 만들면 timestamp 순서로 적용. 충돌 시 명시적 해결.

이 개념을 사용하는 글 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기