rubyonrails.org에서 더 보기:

GitHub에서 이 파일을 읽지 마세요. 가이드는 https://guides.rubyonrails.org 에서 제공됩니다.

Active Record Migrations

Migration은 시간이 지남에 따라 데이터베이스 스키마를 발전시킬 수 있게 해주는 Active Record의 기능입니다. 순수 SQL로 스키마 수정을 작성하는 대신, migration을 사용하면 Ruby Domain Specific Language(DSL)를 사용하여 테이블 변경사항을 설명할 수 있습니다.

이 가이드를 읽고 나면 다음과 같은 내용을 알 수 있습니다:

  • Migration을 생성하는데 사용할 수 있는 generator들
  • Active Record가 데이터베이스를 조작하기 위해 제공하는 메서드들
  • 기존 migration을 변경하고 스키마를 업데이트하는 방법
  • Migration이 schema.rb와 어떤 관계가 있는지
  • 참조 무결성을 유지하는 방법

1 Migration 개요

Migration은 시간이 지남에 따라 데이터베이스 스키마를 발전시키는 편리한 방법입니다. Ruby DSL을 사용하므로 직접 SQL을 작성할 필요가 없으며, 스키마와 변경사항을 데이터베이스에 독립적으로 만들 수 있습니다. 여기서 언급된 일부 개념에 대해 자세히 알아보려면 Active Record 기초Active Record 연관관계 가이드를 읽어보시기를 추천합니다.

각 migration을 데이터베이스의 새로운 '버전'이라고 생각할 수 있습니다. 스키마는 아무것도 없는 상태에서 시작하며, 각 migration은 테이블, 컬럼 또는 인덱스를 추가하거나 제거하여 이를 수정합니다. Active Record는 이 타임라인을 따라 스키마를 업데이트하는 방법을 알고 있어서, 현재 시점에서 최신 버전까지 가져올 수 있습니다. Rails가 타임라인에서 어떤 migration을 실행할지 아는 방법에 대해 자세히 알아보세요.

Active Record는 데이터베이스의 최신 구조와 일치하도록 db/schema.rb 파일을 업데이트합니다. 다음은 migration의 예시입니다:

# db/migrate/20240502100843_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    # products 테이블 생성
    create_table :products do |t|
      t.string :name        # name 열 생성(string 타입) 
      t.text :description   # description 열 생성(text 타입)

      t.timestamps         # created_at과 updated_at 열 생성
    end
  end
end

이 migration은 name이라는 string 컬럼과 description이라는 text 컬럼을 가진 products 테이블을 추가합니다. id라는 primary key 컬럼도 암묵적으로 추가되는데, 이는 모든 Active Record model의 기본 primary key이기 때문입니다. timestamps 매크로는 created_atupdated_at 두 개의 컬럼을 추가합니다. 이러한 특별한 컬럼들은 존재할 경우 Active Record에 의해 자동으로 관리됩니다.

# db/schema.rb
ActiveRecord::Schema[8.1].define(version: 2024_05_02_100843) do
  # 이 데이터베이스를 지원하기 위해 활성화되어야 하는 확장기능들입니다
  enable_extension "plpgsql"

  create_table "products", force: :cascade do |t|
    t.string "name"
    t.text "description" 
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end

우리는 시간이 지나면서 일어나기를 원하는 변화를 정의합니다. 이 migration이 실행되기 전에는 테이블이 없을 것입니다. 실행된 후에는 테이블이 존재할 것입니다. Active Record는 이 migration을 되돌리는 방법도 알고 있습니다; 이 migration을 롤백하면 테이블이 제거될 것입니다. migration 롤백에 대한 자세한 내용은 Rolling Back 섹션에서 확인하세요.

시간이 지나면서 일어나길 원하는 변화를 정의한 후에는 migration의 가역성을 고려하는 것이 필수적입니다. Active Record가 migration의 전진 진행을 관리하여 테이블 생성을 보장할 수 있지만, 가역성의 개념이 매우 중요해집니다. 가역적인 migration을 사용하면 migration이 적용될 때 테이블을 생성할 뿐만 아니라 원활한 롤백 기능도 가능하게 합니다. 위의 migration을 되돌리는 경우, Active Record는 전체 프로세스에서 데이터베이스 일관성을 유지하면서 테이블 제거를 지능적으로 처리합니다. 자세한 내용은 Reversing Migrations 섹션을 참조하세요.

2 Migration 파일 생성하기

2.1 독립적인 Migration 생성하기

Migration은 db/migrate 디렉토리에 파일로 저장되며, 각 migration 클래스당 하나의 파일이 생성됩니다.

파일 이름은 YYYYMMDDHHMMSS_create_products.rb 형식으로 되어 있으며, migration을 식별하는 UTC 타임스탬프 뒤에 언더스코어와 migration 이름이 이어집니다. migration 클래스의 이름(CamelCase 버전)은 파일 이름의 후반부와 일치해야 합니다.

예를 들어, 20240502100843_create_products.rbCreateProducts 클래스를 정의해야 하고, 20240502101659_add_details_to_products.rbAddDetailsToProducts 클래스를 정의해야 합니다. Rails는 이 타임스탬프를 사용하여 어떤 migration을 어떤 순서로 실행해야 하는지 결정하므로, 다른 애플리케이션에서 migration을 복사하거나 직접 파일을 생성하는 경우 순서상의 위치를 주의해야 합니다. 타임스탬프가 어떻게 사용되는지에 대해서는 Rails Migration 버전 관리 섹션에서 자세히 알아볼 수 있습니다.

migration을 생성할 때, Active Record는 자동으로 현재 타임스탬프를 migration 파일 이름 앞에 추가합니다. 예를 들어, 아래 명령을 실행하면 타임스탬프가 migration의 언더스코어된 이름 앞에 추가된 빈 migration 파일이 생성됩니다.

$ bin/rails generate migration AddPartNumberToProducts
# db/migrate/20240502101659_add_part_number_to_products.rb  
class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
  end
end

이 generator는 파일 이름 앞에 타임스탬프를 추가하는 것 이외에도 더 많은 작업을 수행할 수 있습니다. 명명 규칙과 추가적인(선택적) 인수를 기반으로 migration을 구체화하기 시작할 수도 있습니다.

다음 섹션에서는 규칙과 추가 인수를 기반으로 migration을 생성할 수 있는 다양한 방법을 다룰 것입니다.

2.2 새로운 Table 생성하기

데이터베이스에 새로운 table을 생성하고자 할 때, "CreateXXX" 형식과 함께 column 이름과 타입 목록을 작성한 migration을 사용할 수 있습니다. 이는 지정된 column들로 table을 설정하는 migration 파일을 생성할 것입니다.

$ bin/rails generate migration CreateProducts name:string part_number:string

생성합니다

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products do |t|  
      t.string :name
      t.string :part_number

      t.timestamps # created_at과 updated_at 타임스탬프를 생성합니다
    end
  end
end

생성된 파일과 그 내용은 시작점일 뿐이며, db/migrate/YYYYMMDDHHMMSS_create_products.rb 파일을 편집하여 필요에 따라 내용을 추가하거나 제거할 수 있습니다.

2.3 컬럼 추가하기

데이터베이스의 기존 테이블에 새로운 컬럼을 추가하고 싶을 때는, "AddColumnToTable" 형식의 migration을 사용할 수 있으며, 뒤에 컬럼 이름과 타입 목록을 추가하면 됩니다. 이는 적절한 [add_column][] 구문이 포함된 migration 파일을 생성할 것입니다.

$ bin/rails generate migration AddPartNumberToProducts part_number:string

이것은 다음과 같은 migration을 생성할 것입니다:

class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string
  end
end

새로운 column에 index를 추가하고 싶다면, 그렇게 할 수도 있습니다.

$ bin/rails generate migration AddPartNumberToProducts part_number:string:index

이것은 적절한 [add_column][]과 [add_index][] 구문을 생성할 것입니다:

class AddPartNumberToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string  
    add_index :products, :part_number
  end
end

단일 magically generated 컬럼에만 제한되지 않습니다. 예를 들어:

$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal

이것은 products 테이블에 두 개의 추가 컬럼을 추가하는 schema migration을 생성할 것입니다.

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :part_number, :string # products 테이블에 part_number라는 string 컬럼을 추가
    add_column :products, :price, :decimal # products 테이블에 price라는 decimal 컬럼을 추가
  end
end

2.4 Removing Columns

마찬가지로, migration의 이름이 "RemoveColumnFromTable" 형식이고 그 뒤에 column 이름과 type들의 목록이 오는 경우 적절한 [remove_column][] statement를 포함하는 migration이 생성됩니다.

$ bin/rails generate migration RemovePartNumberFromProducts part_number:string

이는 적절한 [remove_column][] statements를 생성할 것입니다:

class RemovePartNumberFromProducts < ActiveRecord::Migration[8.1]
  def change
    remove_column :products, :part_number, :string
  end
end

2.5 연관관계 생성하기

Active Record 연관관계는 애플리케이션의 서로 다른 model들 간의 관계를 정의하는 데 사용되며, 이를 통해 model들이 서로의 관계를 통해 상호작용할 수 있고 관련 데이터를 더 쉽게 다룰 수 있게 됩니다. 연관관계에 대해 더 자세히 알아보려면 Association Basics 가이드를 참조하세요.

연관관계의 일반적인 사용 사례 중 하나는 테이블 간의 foreign key 참조를 생성하는 것입니다. generator는 이 프로세스를 용이하게 하기 위해 references와 같은 column 타입을 허용합니다. References는 column, index, foreign key 또는 polymorphic association column을 생성하기 위한 단축 표현입니다.

예를 들어,

$ bin/rails generate migration AddUserRefToProducts user:references

다음의 [add_reference][] 호출을 생성합니다:

class AddUserRefToProducts < ActiveRecord::Migration[8.1]
  def change
    add_reference :products, :user, null: false, foreign_key: true
  end
end

위 코드는 products 테이블에 user foreign key 참조를 추가하는 migration입니다. 여기서는 참조 필드 user_id가 null이 될 수 없고(null: false), foreign_key 제약조건이 true로 설정됩니다.

위 마이그레이션은 products 테이블에 user_id라는 foreign key를 만듭니다. 여기서 user_idusers 테이블의 id 컬럼에 대한 참조입니다. 또한 user_id 컬럼에 대한 인덱스도 생성합니다. 스키마는 다음과 같습니다:

  create_table "products", force: :cascade do |t|
    t.bigint "user_id", null: false
    t.index ["user_id"], name: "index_products_on_user_id" # user_id에 대한 인덱스를 생성합니다
  end

belongs_toreferences의 별칭이므로, 위 내용은 다음과 같이 작성할 수도 있습니다:

$ bin/rails generate migration AddUserRefToProducts user:belongs_to

위와 동일한 migration과 schema를 생성합니다.

JoinTable이 이름의 일부인 경우 join table을 생성하는 generator도 있습니다:

$ bin/rails generate migration CreateJoinTableUserProduct user product

다음과 같은 migration을 생성할 것입니다:

class CreateJoinTableUserProduct < ActiveRecord::Migration[8.1]
  def change
    create_join_table :users, :products do |t|
      # t.index [:user_id, :product_id] 
      # t.index [:product_id, :user_id]
    end
  end
end

링크된 API 문서를 참고하세요:

[add_column]: column을 추가하려면 https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column [add_index]: index를 추가하려면 https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index [add_reference]: reference를 추가하려면 https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference [remove_column]: column을 제거하려면 https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_column

2.6 Migration을 생성하는 다른 Generator들

migration generator 외에도 model, resource, scaffold generator들이 새로운 모델을 추가하기 위한 적절한 migration을 생성합니다. 이 migration에는 이미 관련 테이블을 생성하기 위한 지침이 포함되어 있습니다. Rails에 원하는 column을 지정하면 이러한 column들을 추가하기 위한 구문도 함께 생성됩니다. 예를 들어 다음을 실행하면:

$ bin/rails generate model Product name:string description:text

다음과 같은 migration이 생성될 것입니다:

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products do |t|
      t.string :name
      t.text :description

      t.timestamps
    end
  end
end

원하는만큼 많은 column 이름/타입 쌍을 추가할 수 있습니다.

2.7 수정자 전달하기

마이그레이션을 생성할 때 일반적으로 사용되는 타입 수정자를 커맨드 라인에서 직접 전달할 수 있습니다. 필드 타입 뒤에 중괄호로 둘러싸인 이러한 수정자들을 사용하면, 마이그레이션 파일을 나중에 수동으로 편집할 필요 없이 데이터베이스 컬럼의 특성을 조정할 수 있습니다.

예를 들어, 다음과 같이 실행할 수 있습니다:

$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}

다음과 같은 migration을 생성할 것입니다

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :price, :decimal, precision: 5, scale: 2
    add_reference :products, :supplier, polymorphic: true
  end
end

NOT NULL 제약은 명령줄에서 ! 단축키를 사용하여 추가할 수 있습니다:

$ bin/rails generate migration AddEmailToUsers email:string!

migration을 다음과 같이 생성합니다

class AddEmailToUsers < ActiveRecord::Migration[8.1]
  def change
    add_column :users, :email, :string, null: false 
  end
end

제너레이터에 대한 자세한 도움말은 bin/rails generate --help를 실행하세요. 또는 특정 제너레이터에 대한 도움말을 보려면 bin/rails generate model --help 또는 bin/rails generate migration --help를 실행할 수도 있습니다.

3 Migration 업데이트하기

섹션의 제너레이터 중 하나를 사용하여 migration 파일을 생성한 후에는, db/migrate 폴더에 있는 생성된 migration 파일을 수정하여 데이터베이스 스키마에 추가로 변경하고 싶은 내용을 정의할 수 있습니다.

3.1 테이블 생성하기

[create_table][] 메서드는 가장 기본적인 migration 타입 중 하나입니다만, 대부분의 경우 model, resource, 또는 scaffold generator를 사용할 때 자동으로 생성됩니다. 일반적인 사용법은 다음과 같습니다:

create_table :products do |t|
  t.string :name
end

이 메소드는 name이라는 컬럼이 있는 products 테이블을 생성합니다.

3.1.1 Associations

association이 있는 모델의 테이블을 생성하는 경우, :references 타입을 사용하여 적절한 컬럼 타입을 생성할 수 있습니다. 예를 들어:

create_table :products do |t|
  t.references :category
end

product에 대한 테이블을 생성하고 category에 대한 reference를 추가합니다.

이것은 category_id 컬럼을 생성합니다. 또는 references 대신 belongs_to를 별칭으로 사용할 수 있습니다:

create_table :products do |t|
  t.belongs_to :category
end

product가 category에 속하는 관계를 생성합니다.

또한 :polymorphic 옵션을 사용하여 column type과 index 생성을 지정할 수 있습니다:

create_table :taggings do |t|
  t.references :taggable, polymorphic: true
end

이렇게 하면 taggable_id 컬럼과 taggable_type 컬럼이 둘 다 생성됩니다.

이것은 taggable_id, taggable_type 컬럼과 적절한 인덱스들을 생성할 것입니다.

3.1.2 Primary Keys

기본적으로 create_table은 암묵적으로 id라는 이름의 primary key를 생성합니다. 아래와 같이 :primary_key 옵션을 사용해서 컬럼의 이름을 변경할 수 있습니다:

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, primary_key: "user_id" do |t|
      t.string :username
      t.string :email 
      t.timestamps
    end
  end
end

위의 migration은 user_id를 primary key로 가진 users 테이블을 생성할 것입니다.

이는 다음과 같은 schema를 생성할 것입니다:

create_table "users", primary_key: "user_id", force: :cascade do |t|
  t.string "username" # 사용자명
  t.string "email" # 이메일  
  t.datetime "created_at", precision: 6, null: false # 생성 시각
  t.datetime "updated_at", precision: 6, null: false # 수정 시각
end

:primary_key에 배열을 전달하여 composite primary key를 설정할 수 있습니다. composite primary keys에 대해 자세히 알아보세요.

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, primary_key: [:id, :name] do |t| # 여러 컬럼을 primary key로 사용
      t.string :name
      t.string :email 
      t.timestamps
    end
  end
end

만약 primary key를 전혀 사용하고 싶지 않다면, id: false 옵션을 전달할 수 있습니다.

class CreateUsers < ActiveRecord::Migration[8.1]
  def change
    create_table :users, id: false do |t|
      t.string :username 
      t.string :email
      t.timestamps
    end
  end
end

위 migration은 id primary key 칼럼이 없는 users 테이블을 생성합니다.

3.1.3 데이터베이스 옵션

데이터베이스별 옵션을 전달해야 하는 경우 :options 옵션에 SQL 프래그먼트를 배치할 수 있습니다. 예를 들면:

create_table :products, options: "ENGINE=BLACKHOLE" do |t|
  t.string :name, null: false
end

이렇게 하면 테이블을 생성하는 SQL 문장에 ENGINE=BLACKHOLE이 추가됩니다.

create_table 블록 내에서 생성된 컬럼에 대해 index: true를 전달하거나 :index 옵션에 옵션 해시를 전달하여 인덱스를 생성할 수 있습니다:

create_table :users do |t|
  t.string :name, index: true
  t.string :email, index: { unique: true, name: "unique_emails" }
end

3.1.4 Comments

테이블에 대한 설명을 :comment 옵션으로 전달할 수 있으며, 이는 데이터베이스 자체에 저장되어 MySQL Workbench나 PgAdmin III와 같은 데이터베이스 관리 도구에서 볼 수 있습니다. 코멘트는 팀원들이 데이터 모델을 더 잘 이해하고 대규모 데이터베이스가 있는 애플리케이션에서 문서를 생성하는 데 도움이 될 수 있습니다. 현재는 MySQL과 PostgreSQL adapter만 코멘트를 지원합니다.

class AddDetailsToProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :products, :price, :decimal, precision: 8, scale: 2, comment: "USD로 표시된 상품 가격" 
    add_column :products, :stock_quantity, :integer, comment: "현재 상품의 재고 수량"
  end
end

3.2 Join Table 생성하기

migration 메서드 create_join_tableHABTM (has and belongs to many) join table을 생성합니다. 일반적인 사용 방법은 다음과 같습니다:

create_join_table :products, :categories

이는 categories_products join table을 생성합니다. 이 테이블은 category_idproduct_id 컬럼을 가지게 됩니다. 이 두 컬럼은 서로에 대한 외래 키입니다. join table의 이름은 첫 번째 테이블과 두 번째 테이블의 이름을 알파벳 순으로 결합해서 만듭니다.

이 migration은 category_idproduct_id라는 두 개의 컬럼이 있는 categories_products 테이블을 생성합니다.

이러한 컬럼들은 기본적으로 :null 옵션이 false로 설정되어 있습니다. 이는 이 테이블에 레코드를 저장하기 위해서는 반드시 값을 제공해야 한다는 것을 의미합니다. 이는 :column_options 옵션을 지정하여 재정의할 수 있습니다.

create_join_table :products, :categories, column_options: { null: true }

테이블 컬럼에 대해 null 값을 허용하려면 :column_options 옵션을 사용할 수 있습니다.

기본적으로 join 테이블의 이름은 create_join_table에 제공된 첫 두 인자를 사전 순서로 결합하여 만들어집니다. 이 경우 테이블 이름은 categories_products가 됩니다.

모델 이름 간의 우선순위는 String에 대한 <=> 연산자를 사용하여 계산됩니다. 이는 문자열의 길이가 다르고, 짧은 길이까지 비교했을 때 문자열이 동일한 경우, 더 긴 문자열이 더 짧은 것보다 사전순으로 우선순위가 높다고 간주됨을 의미합니다. 예를 들어, "paper_boxes"와 "papers" 테이블은 "paper_boxes"라는 이름의 길이 때문에 "papers_paper_boxes"라는 join 테이블 이름을 생성할 것으로 예상되지만, 실제로는 "paper_boxes_papers"라는 join 테이블 이름을 생성합니다(일반적인 인코딩에서 밑줄 '_'이 's'보다 사전순으로 앞에 오기 때문입니다).

테이블의 이름을 커스터마이즈하려면 :table_name 옵션을 제공하세요:

create_join_table :products, :categories, table_name: :categorization

products와 categories에 대한 join table을 생성합니다. 테이블 이름은 :categorization으로 지정됩니다.

이는 categorization이라는 이름의 join table을 생성합니다.

또한 create_join_table은 블록을 받을 수 있으며, 이를 통해 indices(기본적으로 생성되지 않음)나 원하는 추가 컬럼들을 추가할 수 있습니다.

create_join_table :products, :categories do |t|
  t.index :product_id  
  t.index :category_id
end

products와 categories를 위한 join table을 생성합니다. 테이블에는 product_id와 category_id 컬럼에 대한 index가 포함됩니다.

3.3 테이블 변경

기존 테이블을 변경하려면 change_table을 사용할 수 있습니다.

이는 create_table과 비슷한 방식으로 사용되지만, 블록 내부에서 전달되는 객체는 다음과 같은 특별한 기능들에 접근할 수 있습니다:

change_table :products do |t|
  t.remove :description, :name # description과 name 컬럼을 제거
  t.string :part_number      # string 타입의 part_number 컬럼 추가
  t.index :part_number      # part_number 컬럼에 인덱스 추가 
  t.rename :upccode, :upc_code # upccode 컬럼을 upc_code로 이름 변경
end

이 migration은 descriptionname 컬럼들을 제거하고, part_number라는 새로운 string 컬럼을 생성한 뒤 이에 대한 인덱스를 추가합니다. 마지막으로 upccode 컬럼을 upc_code로 이름을 변경합니다.

3.4 컬럼 변경하기

앞서 다룬 remove_columnadd_column 메서드살펴보기와 비슷하게, Rails는 change_column migration 메서드도 제공합니다.

change_column :products, :part_number, :text

products 테이블의 part_number 컬럼의 타입을 text로 변경합니다.

products 테이블의 part_number 컬럼을 :text 필드로 변경합니다.

change_column 명령어는 되돌릴 수 없습니다. 마이그레이션을 안전하게 되돌릴 수 있도록 하려면 직접 reversible 마이그레이션을 제공해야 합니다. 자세한 내용은 Reversible Migrations 섹션을 참조하세요.

change_column 외에도 change_column_nullchange_column_default 메서드를 사용하여 컬럼의 null 제약조건과 기본값을 변경할 수 있습니다.

change_column_default :products, :approved, from: true, to: false

products 테이블의 approved 컬럼의 default 값을 true에서 false로 변경합니다.

이는 :approved 필드의 기본값을 true에서 false로 변경합니다. 이 변경사항은 향후 생성되는 레코드에만 적용되며, 기존 레코드들은 변경되지 않습니다. null 제약조건을 변경하려면 change_column_null을 사용하세요.

change_column_null :products, :name, false

products 테이블의 name 컬럼을 NOT NULL로 설정합니다.

이는 products의 :name 필드를 NOT NULL 컬럼으로 설정합니다. 이 변경사항은 기존 레코드에도 적용되므로, 모든 기존 레코드의 :nameNOT NULL인지 확인해야 합니다.

null 제약조건을 true로 설정하면 해당 컬럼이 null 값을 허용한다는 의미이며, 그렇지 않으면 NOT NULL 제약조건이 적용되어 데이터베이스에 레코드를 저장하기 위해서는 값을 전달해야 합니다.

위의 change_column_default migration을 change_column_default :products, :approved, false로 작성할 수도 있지만, 이전 예시와 달리 이는 되돌릴 수 없는 migration이 됩니다.

3.5 Column Modifiers

컬럼 수정자는 컬럼을 생성하거나 변경할 때 적용할 수 있습니다:

  • comment 컬럼에 대한 주석을 추가합니다.
  • collation string 또는 text 컬럼의 collation을 지정합니다.
  • default 컬럼에 기본값을 설정할 수 있습니다. 동적 값(예: 날짜)을 사용하는 경우, 기본값은 처음 한 번만 계산됩니다(즉, migration이 적용되는 시점). NULL을 위해서는 nil을 사용하세요.
  • limit string 컬럼의 최대 문자 수와 text/binary/integer 컬럼의 최대 바이트 수를 설정합니다.
  • null 컬럼에서 NULL 값을 허용하거나 허용하지 않습니다.
  • precision decimal/numeric/datetime/time 컬럼의 정밀도를 지정합니다.
  • scale decimalnumeric 컬럼의 소수점 이하 자릿수를 나타내는 scale을 지정합니다.

add_column이나 change_column에는 index를 추가하는 옵션이 없습니다. Index는 add_index를 사용하여 별도로 추가해야 합니다.

일부 adapter는 추가 옵션을 지원할 수 있습니다. 자세한 내용은 adapter별 API 문서를 참조하세요.

migration을 생성할 때 명령줄을 통해 default를 지정할 수 없습니다.

3.6 참조

add_reference 메소드는 하나 이상의 association들 사이의 연결로 작용하는 적절한 이름의 컬럼을 생성할 수 있게 해줍니다.

add_reference :users, :role

users 테이블에 role이라는 reference를 추가합니다.

이 migration은 users 테이블에 role_id라고 하는 foreign key 컬럼을 생성할 것입니다. role_idroles 테이블의 id 컬럼에 대한 참조입니다. 또한 index: false 옵션으로 명시적으로 금지하지 않는 한, role_id 컬럼에 대한 인덱스도 생성합니다.

더 자세한 내용은 [Active Record Associations][] 가이드를 참고하세요.

add_belongs_to 메서드는 add_reference의 별칭입니다.

add_belongs_to :taggings, :taggable, polymorphic: true

에서 taggable polymorphic association을 taggings 테이블에 추가합니다.

polymorphic 옵션은 taggings 테이블에 polymorphic 관계에서 사용할 수 있는 두 개의 컬럼(taggable_typetaggable_id)을 생성합니다.

[polymorphic associations][]에 대해 더 자세히 알아보려면 이 가이드를 참고하세요.

foreign key는 foreign_key 옵션으로 생성할 수 있습니다.

add_reference :users, :role, foreign_key: true

:users 테이블에 role_id를 foreign key로 추가합니다.

add_reference의 더 많은 옵션을 보려면 API documentation을 방문하세요.

References는 제거할 수도 있습니다:

:products 테이블에서 user 참조를 제거하고 foreign key가 있는 경우 함께 제거합니다.  경우에는 인덱스 생성을 건너뜁니다.

Active Record Associations: Active Record에서는 모델들을 서로 연결할 수 있습니다. [Active Record Associations]를 참고하세요.

[polymorphic associations]: 단일 모델이 다수의 다른 모델과 belong하는 관계를 가질 수 있게 합니다. [polymorphic associations]를 참고하세요.

3.7 Foreign Keys

필수는 아니지만, 참조 무결성을 보장하기 위해 foreign key 제약조건을 추가하고 싶을 수 있습니다.

add_foreign_key :articles, :authors

articles 테이블에 authors 테이블을 참조하는 foreign key를 추가합니다.

add_foreign_key 호출은 articles 테이블에 새로운 제약 조건을 추가합니다. 이 제약 조건은 articles.author_id와 일치하는 id 컬럼을 가진 행이 authors 테이블에 존재함을 보장하여, articles 테이블에 나열된 모든 reviewer들이 authors 테이블에 등록된 유효한 author임을 보장합니다.

마이그레이션에서 references를 사용할 때, 테이블에 새로운 컬럼을 생성하고 foreign_key: true를 사용하여 해당 컬럼에 foreign key를 추가할 수 있습니다. 하지만 기존 컬럼에 foreign key를 추가하고 싶다면 add_foreign_key를 사용할 수 있습니다.

참조된 primary key를 가진 테이블로부터 foreign key를 추가하려는 테이블의 컬럼 이름을 유추할 수 없는 경우, :column 옵션을 사용하여 컬럼 이름을 지정할 수 있습니다. 또한, 참조된 primary key가 :id가 아닌 경우 :primary_key 옵션을 사용할 수 있습니다.

예를 들어, authors.email을 참조하는 articles.reviewer에 foreign key를 추가하려면:

add_foreign_key :articles, :authors, column: :reviewer, primary_key: :email

articles 테이블의 reviewer 컬럼을 authors 테이블의 email 컬럼을 참조하는 foreign key로 추가합니다.

이는 authors 테이블에서 email 컬럼이 articles.reviewer 필드와 일치하는 row가 반드시 존재함을 보장하는 제약조건을 articles 테이블에 추가할 것입니다.

add_foreign_keyname, on_delete, if_not_exists, validate, deferrable와 같은 다른 옵션들도 지원합니다.

Foreign key는 remove_foreign_key를 사용하여 제거할 수도 있습니다.

# Active Record가 column 이름을 찾도록 하기 
remove_foreign_key :accounts, :branches

# 특정 column의 foreign key 제거하기
remove_foreign_key :accounts, column: :owner_id

Active Record는 단일 컬럼 foreign key만을 지원합니다. 복합 foreign key를 사용하기 위해서는 executestructure.sql이 필요합니다. Schema 덤프와 당신을 참고하세요.

3.8 Composite Primary Keys

때로는 테이블의 모든 행을 고유하게 식별하기 위해 단일 컬럼의 값만으로는 충분하지 않지만, 두 개 이상의 컬럼을 조합하면 고유하게 식별할 수 있습니다. 이는 단일 id 컬럼을 primary key로 사용하지 않는 레거시 데이터베이스 스키마를 사용할 때나, sharding 또는 multitenancy를 위해 스키마를 변경할 때 발생할 수 있습니다.

create_table:primary_key 옵션을 배열 값으로 전달하여 composite primary key가 있는 테이블을 생성할 수 있습니다:

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    create_table :products, primary_key: [:customer_id, :product_sku] do |t|
      t.integer :customer_id
      t.string :product_sku
      t.text :description
    end
  end
end

복합 primary key가 있는 테이블은 많은 메서드에서 정수 ID 대신 배열 값을 전달해야 합니다. 자세한 내용은 Active Record Composite Primary Keys 가이드를 참조하세요.

3.9 SQL 실행

Active Record에서 제공하는 헬퍼들이 충분하지 않다면, execute 메서드를 사용하여 SQL 명령을 직접 실행할 수 있습니다. 예를 들어:

class UpdateProductPrices < ActiveRecord::Migration[8.1]
  def up
    # products 테이블의 price를 'free'로 업데이트
    execute "UPDATE products SET price = 'free'"
  end

  def down
    # price가 'free'인 레코드의 price를 'original_price'로 되돌림
    execute "UPDATE products SET price = 'original_price' WHERE price = 'free';"
  end
end

이 예시에서는 products 테이블의 모든 레코드에 대해 price 컬럼을 'free'로 업데이트하고 있습니다.

migration에서 직접 데이터를 수정하는 것은 주의해서 접근해야 합니다. 이것이 사용 사례에 가장 적합한 접근 방식인지 고려하고, 복잡성 증가와 유지보수 부담, 데이터 무결성 및 데이터베이스 이식성에 대한 위험과 같은 잠재적인 단점을 인지하시기 바랍니다. 자세한 내용은 Data Migrations 문서를 참조하세요.

개별 메서드에 대한 자세한 내용과 예시는 API 문서를 확인하세요.

특히 change, up, down 메서드에서 사용 가능한 메서드를 제공하는 ActiveRecord::ConnectionAdapters::SchemaStatements 문서를 참조하세요.

create_table에 의해 생성되는 객체에 대해 사용 가능한 메서드는 ActiveRecord::ConnectionAdapters::TableDefinition을 참조하세요.

그리고 change_table에 의해 생성되는 객체에 대해서는 ActiveRecord::ConnectionAdapters::Table을 참조하세요.

3.10 change Method 사용하기

change method는 migration을 작성하는 주된 방법입니다. Active Record가 migration의 동작을 자동으로 되돌리는 방법을 알고 있는 대부분의 경우에 작동합니다. 아래는 change가 지원하는 동작들입니다:

change_table도 블록이 위에 나열된 것과 같은 가역 동작만 호출하는 한 가역적입니다.

다른 method를 사용해야 하는 경우, change method를 사용하는 대신 reversible을 사용하거나 updown method를 작성해야 합니다.

[drop_join_table]:

3.11 reversible 사용하기

만약 Active Record가 되돌리는 방법을 모르는 작업을 migration에서 수행하고 싶다면, reversible을 사용하여 migration을 실행할 때 수행할 작업과 되돌릴 때 수행할 작업을 지정할 수 있습니다.

class ChangeProductsPrice < ActiveRecord::Migration[8.1]
  def change
    reversible do |direction|
      change_table :products do |t|
        # 업 방향으로 마이그레이션할 때는 price를 string 타입으로 변경합니다
        direction.up   { t.change :price, :string }
        # 다운 방향으로 마이그레이션할 때는 price를 integer 타입으로 변경합니다  
        direction.down { t.change :price, :integer }
      end
    end
  end
end

이 migration은 price 컬럼의 타입을 string으로 변경하거나, migration이 되돌려질 때 integer로 되돌립니다. direction.updirection.down에 각각 전달되는 블록을 주목하세요.

또는 change 대신 updown을 사용할 수 있습니다:

class ChangeProductsPrice < ActiveRecord::Migration[8.1]
  def up
    change_table :products do |t|
      t.change :price, :string
    end
  end

  def down
    change_table :products do |t|
      t.change :price, :integer
    end
  end
end

이 Migration은 products 테이블의 price 컬럼 타입을 integer에서 string으로 변경합니다. down 메서드는 변경을 되돌릴 때 필요한 반대 작업을 수행합니다.

reversible은 원시 SQL 쿼리를 실행하거나 ActiveRecord 메서드에 직접적인 대응이 없는 데이터베이스 작업을 수행할 때 유용합니다. reversible을 사용하여 마이그레이션을 실행할 때와 되돌릴 때 각각 무엇을 할지 지정할 수 있습니다. 예를 들면:

class ExampleMigration < ActiveRecord::Migration[8.1]
  def change
    create_table :distributors do |t|
      t.string :zipcode  
    end

    reversible do |direction|
      direction.up do
        # distributors view 생성
        execute <<-SQL
          CREATE VIEW distributors_view AS
          SELECT id, zipcode
          FROM distributors;
        SQL
      end
      direction.down do
        execute <<-SQL
          DROP VIEW distributors_view;
        SQL
      end
    end

    add_column :users, :address, :string
  end
end

reversible을 사용하면 지시사항들이 올바른 순서로 실행되도록 보장합니다. 이전 예제의 migration이 revert되면, down 블록은 users.address 컬럼이 제거된 후와 distributors 테이블이 삭제되기 전에 실행됩니다.

3.12 up/down 메서드 사용하기

change 메서드 대신 updown 메서드를 사용하는 예전 방식의 migration도 사용할 수 있습니다.

up 메서드는 스키마에 적용하고 싶은 변환을 설명해야 하고, migration의 down 메서드는 up 메서드에서 수행한 변환을 되돌려야 합니다. 다시 말해서, up을 실행한 다음 down을 실행하면 데이터베이스 스키마는 변경되지 않은 상태여야 합니다.

예를 들어, up 메서드에서 테이블을 생성했다면 down 메서드에서는 그것을 삭제해야 합니다. up 메서드에서 변환을 수행한 순서의 정확히 반대 순서로 수행하는 것이 현명합니다. reversible 섹션의 예제는 다음과 동일합니다:

class ExampleMigration < ActiveRecord::Migration[8.1]
  def up
    create_table :distributors do |t|
      t.string :zipcode
    end

    # distributors view 생성
    execute <<-SQL
      CREATE VIEW distributors_view AS
      SELECT id, zipcode
      FROM distributors;
    SQL

    add_column :users, :address, :string
  end

  def down
    remove_column :users, :address

    execute <<-SQL
      DROP VIEW distributors_view;
    SQL

    drop_table :distributors
  end
end

3.13 revert를 방지하기 위한 에러 발생

때로는 migration이 되돌릴 수 없는 작업을 수행할 수 있습니다. 예를 들어, 일부 데이터를 삭제하는 경우가 있습니다.

이러한 경우, down 블록에서 ActiveRecord::IrreversibleMigration을 발생시킬 수 있습니다.

class IrreversibleMigrationExample < ActiveRecord::Migration[8.1]
  def up
    drop_table :example_table
  end

  def down
    raise ActiveRecord::IrreversibleMigration, "이 migration은 데이터를 제거하기 때문에 되돌릴 수 없습니다."
  end
end

누군가 당신의 migration을 revert하려고 하면, 그것이 불가능하다는 에러 메시지가 표시될 것입니다.

3.14 이전 Migration 되돌리기

Active Record의 revert 메소드를 이용하여 migration을 롤백할 수 있습니다:

require_relative "20121212123456_example_migration"

class FixupExampleMigration < ActiveRecord::Migration[8.1]
  def change
    revert ExampleMigration

    create_table(:apples) do |t|
      t.string :variety
    end
  end
end

위 코드에서는 기존의 ExampleMigration을 revert하고 새로운 apples 테이블을 생성합니다.

revert 메서드는 역순으로 실행될 명령들의 블록도 허용합니다. 이는 이전 마이그레이션의 선택된 부분들을 되돌리는 데 유용할 수 있습니다.

예를 들어, ExampleMigration이 커밋되었고 나중에 Distributors view가 더 이상 필요하지 않다고 결정되었다고 가정해봅시다.

class DontUseDistributorsViewMigration < ActiveRecord::Migration[8.1]
  def change
    revert do
      # ExampleMigration에서 복사한 코드
      create_table :distributors do |t|
        t.string :zipcode
      end

      reversible do |direction|
        direction.up do
          # distributors view 생성
          execute <<-SQL
            CREATE VIEW distributors_view AS
            SELECT id, zipcode
            FROM distributors;
          SQL
        end
        direction.down do
          execute <<-SQL
            DROP VIEW distributors_view;
          SQL
        end
      end

      # Migration의 나머지 부분은 문제 없음
    end
  end
end

동일한 migration은 revert를 사용하지 않고도 작성할 수 있었지만, 이 경우 다음과 같은 몇 가지 단계가 더 필요했을 것입니다:

  1. create_tablereversible의 순서를 반대로 합니다.
  2. create_tabledrop_table로 교체합니다.
  3. 마지막으로, updown으로, 그 반대로 교체합니다.

이 모든 것이 revert로 처리됩니다.

4 Migrations 실행하기

Rails는 특정 migration 세트를 실행하기 위한 명령어들을 제공합니다.

아마도 처음 사용하게 될 migration 관련 rails 명령어는 bin/rails db:migrate일 것입니다. 가장 기본적인 형태로, 아직 실행되지 않은 모든 migration에 대해 change 또는 up 메서드를 실행합니다. 실행할 migration이 없다면 종료됩니다. 이러한 migration들은 migration 날짜를 기준으로 순서대로 실행됩니다.

db:migrate 명령을 실행하면 db:schema:dump 명령도 함께 호출되어 db/schema.rb 파일을 데이터베이스 구조와 일치하도록 업데이트한다는 점에 주의하세요.

특정 버전을 지정하면, Active Record는 지정된 버전에 도달할 때까지 필요한 migration(change, up, down)을 실행합니다. 버전은 migration 파일 이름의 숫자 접두사입니다. 예를 들어, 버전 20240428000000으로 migrate하려면 다음과 같이 실행합니다:

$ bin/rails db:migrate VERSION=20240428000000

버전 20240428000000이 현재 버전보다 더 높다면(즉, 위로 마이그레이션하는 경우), 20240428000000까지의 모든 마이그레이션에서 change(또는 up) 메서드를 실행하고, 그 이후의 마이그레이션은 실행하지 않습니다. 아래로 마이그레이션하는 경우에는 20240428000000까지의 모든 마이그레이션에서 down 메서드를 실행하지만, 20240428000000은 포함하지 않습니다.

4.1 Rolling Back

가장 최근의 migration을 롤백하는 것은 흔한 작업입니다. 예를 들어, migration에서 실수를 했고 이를 수정하고 싶을 때입니다. 이전 migration과 관련된 버전 번호를 찾을 필요 없이 다음과 같이 실행할 수 있습니다:

$ bin/rails db:rollback

이는 change 메서드를 되돌리거나 down 메서드를 실행하여 최신 migration을 롤백합니다. 여러 migration을 취소해야 하는 경우 STEP 파라미터를 제공할 수 있습니다:

$ bin/rails db:rollback STEP=3

마지막 3개의 migration이 롤백됩니다.

로컬 migration을 수정하고 다시 위로 migrate하기 전에 해당 migration을 특정해서 롤백하고 싶은 경우, db:migrate:redo 명령어를 사용할 수 있습니다. db:rollback 명령어와 마찬가지로 한 번에 여러 버전을 되돌리고 싶을 때는 STEP 파라미터를 사용할 수 있습니다. 예를 들면:

$ bin/rails db:migrate:redo STEP=3

db:migrate를 사용해서 동일한 결과를 얻을 수 있습니다. 하지만 이러한 명령어들은 마이그레이션할 버전을 명시적으로 지정할 필요가 없도록 편의를 위해 제공됩니다.

4.1.1 Transactions

DDL transaction을 지원하는 데이터베이스에서는 스키마를 변경할 때 각 migration이 하나의 transaction으로 감싸집니다.

Transaction은 migration이 도중에 실패할 경우 성공적으로 적용된 변경사항들이 롤백되어 데이터베이스 일관성을 유지하도록 보장합니다. 이는 transaction 내의 모든 작업이 성공적으로 실행되거나 아무것도 실행되지 않음을 의미하며, transaction 중에 오류가 발생했을 때 데이터베이스가 불일치 상태로 남는 것을 방지합니다.

데이터베이스가 스키마를 변경하는 구문에 대한 DDL transaction을 지원하지 않는 경우, migration이 실패하면 성공했던 부분들이 롤백되지 않습니다. 이 경우 변경사항을 수동으로 롤백해야 합니다.

transaction 내부에서 실행할 수 없는 쿼리들이 있는데, 이러한 상황을 위해 disable_ddl_transaction!을 사용하여 자동 transaction을 비활성화할 수 있습니다:

class ChangeEnum < ActiveRecord::Migration[8.1]
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

PostgreSQL enum에 새 값을 추가하는 방법을 보여주는 예시입니다. disable_ddl_transaction!은 해당 변경이 하나의 트랜잭션으로 실행될 수 없기 때문에 필요합니다.

self.disable_ddl_transaction!을 사용하는 Migration에 있더라도, 여전히 당신만의 transaction을 열 수 있다는 점을 기억하세요.

4.2 데이터베이스 설정하기

bin/rails db:setup 명령어는 데이터베이스를 생성하고, schema를 로드하며, seed 데이터로 초기화합니다.

4.3 데이터베이스 준비하기

bin/rails db:prepare 명령어는 bin/rails db:setup과 유사하지만, 멱등성을 가지고 동작하므로 여러 번 안전하게 호출될 수 있습니다. 하지만 필요한 작업은 한 번만 수행됩니다.

  • 데이터베이스가 아직 생성되지 않은 경우, 이 명령어는 bin/rails db:setup처럼 실행됩니다.
  • 데이터베이스는 존재하지만 테이블이 생성되지 않은 경우, 이 명령어는 schema를 로드하고, 대기 중인 migration을 실행하고, 업데이트된 schema를 덤프한 다음, 마지막으로 seed 데이터를 로드합니다. 자세한 내용은 Seeding Data 문서를 참조하세요.
  • 데이터베이스와 테이블이 이미 존재하는 경우, 이 명령어는 아무것도 수행하지 않습니다.

데이터베이스와 테이블이 존재하면, 이전에 로드된 seed 데이터나 기존 seed 파일이 변경 또는 삭제되었더라도 db:prepare 작업은 seed 데이터를 다시 로드하지 않습니다. seed 데이터를 다시 로드하려면 수동으로 bin/rails db:seed를 실행할 수 있습니다.

이 작업은 생성된 데이터베이스나 테이블 중 하나가 해당 환경의 주 데이터베이스이거나 seeds: true로 구성된 경우에만 seed를 로드합니다.

4.4 데이터베이스 리셋하기

bin/rails db:reset 명령어는 데이터베이스를 삭제하고 다시 설정합니다. 이는 bin/rails db:drop db:setup과 기능적으로 동일합니다.

이는 모든 migration을 실행하는 것과는 다릅니다. 현재 db/schema.rb 또는 db/structure.sql 파일의 내용만을 사용합니다. 만약 migration을 롤백할 수 없는 경우, bin/rails db:reset은 도움이 되지 않을 수 있습니다. schema 덤프에 대해 더 자세히 알아보려면 Schema Dumping and You 섹션을 참조하세요.

4.5 특정 Migration 실행하기

특정 migration을 up 또는 down으로 실행해야 하는 경우, db:migrate:updb:migrate:down 명령어를 사용할 수 있습니다. 적절한 버전을 지정하면 해당 migration의 change, up 또는 down 메소드가 호출됩니다. 예를 들어:

$ bin/rails db:migrate:up VERSION=20240428000000

이 명령어를 실행하면 버전 "20240428000000"의 migration에 대한 change 메서드(또는 up 메서드)가 실행됩니다.

먼저, 이 명령어는 migration이 존재하는지, 그리고 이미 수행되었는지 확인하며, 이미 수행되었다면 아무 작업도 하지 않습니다.

지정된 버전이 존재하지 않으면 Rails는 exception을 발생시킵니다.

$ bin/rails db:migrate VERSION=00000000000000
rails aborted!
ActiveRecord::UnknownMigrationVersionError:

버전 번호 00000000000000인 migration이 없습니다.

4.6 다른 Environment에서 Migration 실행하기

기본적으로 bin/rails db:migrate를 실행하면 development environment에서 실행됩니다.

다른 environment에서 migration을 실행하려면, 명령어를 실행할 때 RAILS_ENV environment 변수를 지정하면 됩니다. 예를 들어 test environment에서 migration을 실행하려면 다음과 같이 실행할 수 있습니다:

$ bin/rails db:migrate RAILS_ENV=test

4.7 Migration 실행 출력 변경하기

기본적으로 migration은 수행 중인 작업과 소요 시간을 정확히 알려줍니다. 테이블을 생성하고 index를 추가하는 migration은 다음과 같은 출력을 생성할 수 있습니다:

==  CreateProducts: 마이그레이션 시작 =================================================
-- create_table(:products) 
   -> 0.0028s
==  CreateProducts: 마이그레이션 완료 (0.0028s) ========================================

마이그레이션에서는 이를 모두 제어할 수 있는 몇 가지 메서드를 제공합니다:

메서드 목적
suppress_messages 블록을 인수로 받아 블록에서 생성되는 모든 출력을 억제합니다.
say 메시지 인수를 받아 그대로 출력합니다. 들여쓰기 여부를 지정하는 두 번째 불리언 인수를 전달할 수 있습니다.
say_with_time 블록 실행에 걸린 시간과 함께 텍스트를 출력합니다. 블록이 정수를 반환하면 영향을 받은 행의 수로 간주합니다.

예를 들어, 다음과 같은 마이그레이션을 살펴보겠습니다:

class CreateProducts < ActiveRecord::Migration[8.1]
  def change
    suppress_messages do
      create_table :products do |t|
        t.string :name
        t.text :description
        t.timestamps
      end
    end

    say "테이블이 생성되었습니다"

    suppress_messages { add_index :products, :name }
    say "그리고 인덱스도!", true

    say_with_time "잠시만 기다려주세요" do
      sleep 10
      250
    end
  end
end

이는 다음과 같은 출력을 생성할 것입니다:

==  CreateProducts: migrating =================================================
-- 테이블 생성됨 
   -> 인덱스도 생성됨!
-- 잠시 대기 중 
   -> 10.0013초
   -> 250 행
==  CreateProducts: migrated (10.0054s) =======================================

Active Record가 아무것도 출력하지 않기를 원한다면, bin/rails db:migrate VERBOSE=false를 실행하면 모든 출력이 숨겨집니다.

4.8 Rails Migration 버전 관리

Rails는 schema_migrations 데이터베이스 테이블을 통해 어떤 migration이 실행되었는지 추적합니다. migration을 실행하면 Rails는 migration의 버전 번호를 version 컬럼에 저장하여 schema_migrations 테이블에 행을 삽입합니다. 이를 통해 Rails는 어떤 migration이 이미 데이터베이스에 적용되었는지 파악할 수 있습니다.

예를 들어, 20240428000000_create_users.rb라는 migration 파일이 있다면, Rails는 파일 이름에서 버전 번호(20240428000000)를 추출하여 migration이 성공적으로 실행된 후 schema_migrations 테이블에 삽입합니다.

데이터베이스 관리 도구나 Rails 콘솔을 사용하여 schema_migrations 테이블의 내용을 직접 볼 수 있습니다:

rails dbconsole

이 명령어를 실행하면 Rails 환경에서 설정된 database의 command line client를 시작합니다. 예를 들어 MySQL을 사용한다면 mysql client를 시작하고, PostgreSQL을 사용한다면 psql client를 실행합니다.

그런 다음 database console 내에서 schema_migrations 테이블을 조회할 수 있습니다:

SELECT * FROM schema_migrations;

이는 데이터베이스에 적용된 모든 migration 버전 번호 목록을 보여줍니다. Rails는 rails db:migrate 또는 rails db:migrate:up 명령을 실행할 때 어떤 migration을 실행해야 하는지 결정하기 위해 이 정보를 사용합니다.

5 기존 Migration 변경하기

때때로 migration을 작성할 때 실수를 할 수 있습니다. 이미 migration을 실행했다면 단순히 migration을 수정하고 다시 실행할 수는 없습니다: Rails는 이미 migration이 실행되었다고 판단하기 때문에 bin/rails db:migrate를 실행해도 아무것도 하지 않을 것입니다. migration을 롤백하고(예: bin/rails db:rollback으로), migration을 수정한 다음, bin/rails db:migrate를 실행하여 수정된 버전을 실행해야 합니다.

일반적으로 이미 소스 컨트롤에 커밋된 기존 migration을 수정하는 것은 좋지 않습니다. 기존 버전의 migration이 이미 프로덕션 머신에서 실행된 경우, 자신과 동료들에게 추가 작업을 만들고 큰 골치거리를 초래할 수 있습니다. 대신 필요한 변경사항을 수행하는 새로운 migration을 작성해야 합니다.

하지만 아직 소스 컨트롤에 커밋되지 않은(또는 더 일반적으로, 개발 머신 이상으로 전파되지 않은) 새로 생성된 migration을 수정하는 것은 일반적입니다.

revert 메소드는 이전 migration을 전체적으로 또는 부분적으로 취소하는 새로운 migration을 작성할 때 도움이 될 수 있습니다(이전 Migration 되돌리기 참조).

6 Schema 덤프와 당신

6.1 Schema 파일은 무엇을 위한 것인가?

Migration이 아무리 강력하다 하더라도, 데이터베이스 schema의 권위있는 소스가 될 수는 없습니다. 데이터베이스가 진실의 원천으로 남아있습니다.

기본적으로 Rails는 현재 데이터베이스 schema 상태를 캡처하려는 db/schema.rb를 생성합니다.

전체 migration 히스토리를 다시 실행하는 것보다 bin/rails db:schema:load를 통해 schema 파일을 로드하여 애플리케이션의 새로운 데이터베이스 인스턴스를 생성하는 것이 더 빠르고 오류가 적습니다. 오래된 migration은 변경되는 외부 의존성을 사용하거나 migration과는 별개로 발전하는 애플리케이션 코드에 의존하는 경우 올바르게 적용되지 않을 수 있습니다.

Schema 파일은 Active Record 객체가 어떤 속성을 가지고 있는지 빠르게 살펴보고 싶을 때도 유용합니다. 이 정보는 모델의 코드에는 없으며 여러 migration에 걸쳐 분산되어 있지만, schema 파일에는 깔끔하게 요약되어 있습니다.

6.2 Schema Dump의 종류

Rails가 생성하는 schema dump의 형식은 config/application.rb에 정의된 config.active_record.schema_format 설정으로 제어됩니다. 기본값은 :ruby이며, 대안으로 :sql로 설정할 수 있습니다.

6.2.1 기본값인 :ruby schema 사용하기

:ruby가 선택되면 schema는 db/schema.rb에 저장됩니다. 이 파일을 보면 하나의 매우 큰 migration처럼 보이는 것을 발견할 수 있습니다:

ActiveRecord::Schema[8.1].define(version: 2008_09_06_171750) do
  create_table "authors", force: true do |t|
    t.string   "name"
    t.datetime "created_at" 
    t.datetime "updated_at"
  end

  create_table "products", force: true do |t|
    t.string   "name"  # 이름
    t.text     "description"  # 설명
    t.datetime "created_at"  # 생성일시
    t.datetime "updated_at"  # 수정일시 
    t.string   "part_number"  # 부품번호
  end
end

이는 여러 면에서 정확히 그러한 것입니다. 이 파일은 데이터베이스를 검사하고 create_table, add_index 등을 사용하여 그 구조를 표현함으로써 생성됩니다.

6.2.2 :sql 스키마 덤퍼 사용하기

하지만 db/schema.rb는 트리거, 시퀀스, 저장 프로시저 등과 같이 데이터베이스가 지원하는 모든 것을 표현할 수는 없습니다.

마이그레이션이 Ruby 마이그레이션 DSL에서 지원하지 않는 데이터베이스 구조를 생성하기 위해 execute를 사용할 수 있지만, 이러한 구조는 스키마 덤퍼에 의해 재구성되지 못할 수 있습니다.

이러한 기능들을 사용하고 있다면, 새로운 데이터베이스 인스턴스를 생성하는 데 유용한 정확한 스키마 파일을 얻기 위해 스키마 포맷을 :sql로 설정해야 합니다.

스키마 포맷이 :sql로 설정되면, 데이터베이스 구조는 데이터베이스별 도구를 사용하여 db/structure.sql로 덤프됩니다. 예를 들어, PostgreSQL의 경우 pg_dump 유틸리티가 사용됩니다. MySQL과 MariaDB의 경우, 이 파일은 다양한 테이블에 대한 SHOW CREATE TABLE의 출력을 포함합니다.

db/structure.sql에서 스키마를 로드하려면, bin/rails db:schema:load를 실행하세요. 이 파일을 로드하는 것은 그 안에 포함된 SQL 문을 실행하는 것으로 이루어집니다. 정의상, 이는 데이터베이스 구조의 완벽한 복사본을 생성할 것입니다.

6.3 스키마 덤프와 소스 컨트롤

스키마 파일이 일반적으로 새로운 데이터베이스를 생성하는 데 사용되기 때문에, 스키마 파일을 소스 컨트롤에 포함시키는 것을 강력히 권장합니다.

두 개의 브랜치가 스키마를 수정할 때 스키마 파일에서 머지 충돌이 발생할 수 있습니다. 이러한 충돌을 해결하려면 bin/rails db:migrate를 실행하여 스키마 파일을 재생성하세요.

새로 생성된 Rails 앱은 이미 migrations 폴더가 git tree에 포함되어 있으므로, 새로운 migration을 추가할 때마다 이를 추가하고 커밋하기만 하면 됩니다.

7 Active Record와 참조 무결성

Active Record 패턴은 데이터베이스보다는 모델에 주요 로직이 있어야 한다고 제안합니다. 따라서, 일부 로직을 데이터베이스로 위임하는 트리거나 제약조건과 같은 기능들은 항상 선호되지는 않습니다.

validates :foreign_key, uniqueness: true와 같은 유효성 검사는 모델이 데이터 무결성을 강제할 수 있는 한 가지 방법입니다. 연관관계의 :dependent 옵션을 사용하면 부모 객체가 삭제될 때 자식 객체들을 자동으로 삭제할 수 있습니다. 애플리케이션 레벨에서 작동하는 모든 것처럼, 이것들은 참조 무결성을 보장할 수 없기 때문에 일부 사람들은 데이터베이스의 foreign key constraints로 이를 보완합니다.

실제로, foreign key 제약조건과 unique 인덱스는 일반적으로 데이터베이스 레벨에서 강제될 때 더 안전한 것으로 간주됩니다. Active Record는 이러한 데이터베이스 레벨 기능들을 직접적으로 지원하지는 않지만, execute 메소드를 사용하여 임의의 SQL 명령을 실행할 수 있습니다.

Active Record 패턴이 모델 내에 로직을 유지하는 것을 강조하지만, 데이터베이스 레벨에서 foreign key와 unique 제약조건을 구현하지 않으면 무결성 문제가 발생할 수 있다는 점을 강조할 필요가 있습니다. 따라서, AR 패턴을 적절한 데이터베이스 레벨 제약조건으로 보완하는 것이 좋습니다. 이러한 제약조건들은 애플리케이션과 데이터베이스 계층 모두에서 데이터 무결성을 보장하기 위해 연관관계와 유효성 검사를 사용하여 코드에서 명시적으로 정의되어야 합니다.

8 Migrations와 시드 데이터

Rails migration 기능의 주요 목적은 일관된 프로세스를 사용하여 스키마를 수정하는 명령을 실행하는 것입니다. Migration은 데이터를 추가하거나 수정하는 데도 사용할 수 있습니다. 이는 프로덕션 데이터베이스와 같이 삭제하고 다시 생성할 수 없는 기존 데이터베이스에서 유용합니다.

class AddInitialProducts < ActiveRecord::Migration[8.1]
  def up
    5.times do |i|
      Product.create(name: "Product ##{i}", description: "A product.")
    end
  end

  def down
    Product.delete_all
  end
end

위 migration은 5개의 product를 데이터베이스에 추가하고 모든 product들을 삭제하는 down 메서드를 가지고 있습니다.

데이터베이스가 생성된 후 초기 데이터를 추가하기 위해, Rails는 프로세스를 빠르게 처리할 수 있는 내장 'seeds' 기능을 제공합니다. 이는 개발 및 테스트 환경에서 데이터베이스를 자주 리로드하거나, production 환경에서 초기 데이터를 설정할 때 특히 유용합니다.

이 기능을 시작하려면 db/seeds.rb를 열고 Ruby 코드를 추가한 다음, bin/rails db:seed를 실행하세요.

여기에 작성되는 코드는 모든 환경에서 언제든지 실행될 수 있도록 멱등성(idempotent)을 가져야 합니다.

["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
  MovieGenre.find_or_create_by!(name: genre_name)
end

db/schema.rb 또는 db/structure.sql는 데이터베이스의 현재 상태 스냅샷이며 데이터베이스를 재구축하는 데 있어 신뢰할 수 있는 소스입니다. 이는 기존 migration 파일들을 삭제하거나 정리하는 것을 가능하게 합니다.

db/migrate/ 디렉토리에서 migration 파일을 삭제할 때, 해당 파일이 여전히 존재했을 때 bin/rails db:migrate가 실행된 모든 환경에서는 schema_migrations라는 Rails 내부 데이터베이스 테이블에 해당 migration의 타임스탬프에 대한 참조를 유지합니다. 이에 대해 Rails Migration Version Control 섹션에서 더 자세히 읽어볼 수 있습니다.

각 migration의 상태(up 또는 down)를 표시하는 bin/rails db:migrate:status 명령을 실행하면, 특정 환경에서 한 번 실행되었지만 더 이상 db/migrate/ 디렉토리에서 찾을 수 없는 삭제된 migration 파일 옆에 ********** NO FILE **********가 표시되는 것을 볼 수 있습니다.

이는 일반적으로 빈 애플리케이션의 데이터베이스를 설정하는 더 깔끔한 방법입니다.

8.1 Engine에서의 Migration

[Engine][]의 migration을 다룰 때 주의해야 할 점이 있습니다. Engine에서 migration을 설치하는 Rake task는 멱등성을 가집니다. 이는 몇 번을 실행하더라도 동일한 결과를 얻는다는 것을 의미합니다. 이전 설치로 인해 부모 애플리케이션에 이미 존재하는 migration은 건너뛰고, 누락된 migration은 새로운 타임스탬프를 붙여 복사됩니다. 만약 이전 engine migration을 삭제하고 설치 task를 다시 실행하면, 새로운 타임스탬프가 있는 새 파일을 얻게 되고 db:migrate는 이를 다시 실행하려고 시도할 것입니다.

따라서 일반적으로 engine에서 가져온 migration은 보존하는 것이 좋습니다. 이러한 migration에는 다음과 같은 특별한 주석이 있습니다:

# 이 migration은 blorgh에서 유래했습니다 (원래 20210621082949)

9 기타

9.1 Primary Keys로 ID 대신 UUID를 사용하기

기본적으로 Rails는 데이터베이스 레코드의 primary keys로 자동 증가하는 정수를 사용합니다. 하지만 분산 시스템이나 외부 서비스와의 통합이 필요한 경우처럼 primary keys로 UUID(Universally Unique Identifier)를 사용하는 것이 유리한 시나리오가 있습니다. UUID는 ID를 생성하기 위해 중앙 집중식 권한에 의존하지 않고 전역적으로 고유한 식별자를 제공합니다.

9.1.1 Rails에서 UUID 활성화하기

Rails 애플리케이션에서 UUID를 사용하기 전에, 데이터베이스가 UUID를 저장할 수 있도록 지원하는지 확인해야 합니다. 추가로 UUID와 작동하도록 데이터베이스 어댑터를 구성해야 할 수도 있습니다.

PostgreSQL 13 이전 버전을 사용하는 경우, gen_random_uuid() 함수에 접근하기 위해 pgcrypto 확장을 활성화해야 할 수 있습니다.

  1. Rails 설정

    Rails 애플리케이션 설정 파일(config/application.rb)에 다음 라인을 추가하여 기본적으로 primary keys로 UUID를 생성하도록 Rails를 구성하세요:

    config.generators do |g|
      g.orm :active_record, primary_key_type: :uuid
    end
    

    이 설정은 Rails에게 ActiveRecord 모델의 기본 primary key 타입으로 UUID를 사용하도록 지시합니다.

  2. UUID로 References 추가하기:

    references를 사용하여 모델 간 연관 관계를 생성할 때, primary key 타입과의 일관성을 유지하기 위해 데이터 타입을 :uuid로 지정해야 합니다. 예를 들면:

    create_table :posts, id: :uuid do |t|
      t.references :author, type: :uuid, foreign_key: true
      # 다른 컬럼들...
      t.timestamps
    end
    

    이 예제에서 posts 테이블의 author_id 컬럼은 authors 테이블의 id 컬럼을 참조합니다. 타입을 :uuid로 명시적으로 설정함으로써 foreign key 컬럼이 primary key의 데이터 타입과 일치하는 것을 보장합니다.

참조하는 key에 맞게 구문을 조정하세요. 다른 association과 데이터베이스에 대해서도 마찬가지로 구문을 조정하세요.

  1. Migration 변경사항

    모델에 대한 migration을 생성할 때, id가 uuid 타입으로 지정된 것을 볼 수 있습니다:

      $ bin/rails g migration CreateAuthors
    
    class CreateAuthors < ActiveRecord::Migration[8.1]
      def change
        create_table :authors, id: :uuid do |t|
          t.timestamps
        end
      end
    end
    

    이는 다음과 같은 schema가 생성됩니다:

    create_table "authors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
      t.datetime "created_at", precision: 6, null: false
      t.datetime "updated_at", precision: 6, null: false
    end
    

    이 migration에서 id 컬럼은 gen_random_uuid() 함수로 생성된 기본값을 가진 UUID primary key로 정의됩니다.

UUID는 서로 다른 시스템에서도 전역적으로 유일함이 보장되어, 분산 아키텍처에 적합합니다. 또한 중앙 집중식 ID 생성에 의존하지 않는 고유 식별자를 제공하여 외부 시스템이나 API와의 통합을 단순화하며, 자동 증가하는 정수와 달리 테이블의 총 레코드 수에 대한 정보를 노출하지 않아 보안 목적으로도 유용할 수 있습니다.

하지만 UUID는 크기로 인해 성능에 영향을 미칠 수 있으며 인덱싱하기가 더 어렵습니다. UUID는 정수 primary key와 foreign key에 비해 쓰기와 읽기 성능이 더 낮습니다.

따라서 UUID를 primary key로 사용할지 결정하기 전에 trade-off를 평가하고 애플리케이션의 특정 요구사항을 고려하는 것이 중요합니다.

9.2 데이터 마이그레이션

데이터 마이그레이션은 데이터베이스 내의 데이터를 변환하거나 이동하는 것을 포함합니다. Rails에서는 일반적으로 migration 파일을 사용하여 데이터 마이그레이션을 수행하는 것을 권장하지 않습니다. 그 이유는 다음과 같습니다:

  • 관심사의 분리: Schema 변경과 데이터 변경은 서로 다른 생명주기와 목적을 가지고 있습니다. Schema 변경은 데이터베이스의 구조를 변경하는 반면, 데이터 변경은 내용을 변경합니다.
  • 롤백 복잡성: 데이터 마이그레이션은 안전하고 예측 가능한 방식으로 롤백하기 어려울 수 있습니다.
  • 성능: 데이터 마이그레이션은 실행하는 데 오랜 시간이 걸릴 수 있으며 테이블을 잠글 수 있어 애플리케이션 성능과 가용성에 영향을 미칠 수 있습니다.

대신, maintenance_tasks gem을 사용하는 것을 고려해보세요. 이 gem은 schema 마이그레이션을 방해하지 않으면서 데이터 마이그레이션과 기타 유지보수 작업을 안전하고 쉽게 관리할 수 있는 방법으로 생성하고 관리하기 위한 프레임워크를 제공합니다.



맨 위로