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_at
과 updated_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.rb
는 CreateProducts
클래스를 정의해야 하고, 20240502101659_add_details_to_products.rb
는 AddDetailsToProducts
클래스를 정의해야 합니다. 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_id
는 users
테이블의 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_to
는 references
의 별칭이므로, 위 내용은 다음과 같이 작성할 수도 있습니다:
$ 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_table
은 HABTM (has and belongs to many) join table을 생성합니다. 일반적인 사용 방법은 다음과 같습니다:
create_join_table :products, :categories
이는 categories_products
join table을 생성합니다. 이 테이블은 category_id
와 product_id
컬럼을 가지게 됩니다. 이 두 컬럼은 서로에 대한 외래 키입니다. join table의 이름은 첫 번째 테이블과 두 번째 테이블의 이름을 알파벳 순으로 결합해서 만듭니다.
이 migration은 category_id
와 product_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은 description
과 name
컬럼들을 제거하고, part_number
라는 새로운 string 컬럼을 생성한 뒤 이에 대한 인덱스를 추가합니다. 마지막으로 upccode
컬럼을 upc_code
로 이름을 변경합니다.
3.4 컬럼 변경하기
앞서 다룬 remove_column
과 add_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_null
과 change_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
컬럼으로 설정합니다. 이 변경사항은 기존 레코드에도 적용되므로, 모든 기존 레코드의 :name
이 NOT 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
decimal
과numeric
컬럼의 소수점 이하 자릿수를 나타내는 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_id
는 roles
테이블의 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_type
과 taggable_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_key
는 name
, 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를 사용하기 위해서는 execute
와 structure.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
가 지원하는 동작들입니다:
add_check_constraint
- [
add_column
][] add_foreign_key
- [
add_index
][] - [
add_reference
][] add_timestamps
change_column_comment
(:from
과:to
옵션을 반드시 제공해야 함)change_column_default
(:from
과:to
옵션을 반드시 제공해야 함)change_column_null
change_table_comment
(:from
과:to
옵션을 반드시 제공해야 함)create_join_table
- [
create_table
][] disable_extension
- [
drop_join_table
][] drop_table
(테이블 생성 옵션과 블록을 반드시 제공해야 함)enable_extension
remove_check_constraint
(원래의 제약 조건 표현식을 반드시 제공해야 함)- [
remove_column
] remove_columns
(원래의 타입과 컬럼 옵션을 반드시 제공해야 함)remove_foreign_key
(다른 테이블과 원래의 옵션을 반드시 제공해야 함)remove_index
(컬럼과 원래의 옵션을 반드시 제공해야 함)remove_reference
(원래의 옵션을 반드시 제공해야 함)remove_timestamps
(원래의 옵션을 반드시 제공해야 함)rename_column
rename_index
rename_table
change_table
도 블록이 위에 나열된 것과 같은 가역 동작만 호출하는 한 가역적입니다.
다른 method를 사용해야 하는 경우, change
method를 사용하는 대신 reversible
을 사용하거나 up
과 down
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.up
과 direction.down
에 각각 전달되는 블록을 주목하세요.
또는 change
대신 up
과 down
을 사용할 수 있습니다:
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
메서드 대신 up
과 down
메서드를 사용하는 예전 방식의 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
를 사용하지 않고도 작성할 수 있었지만, 이 경우 다음과 같은 몇 가지 단계가 더 필요했을 것입니다:
create_table
과reversible
의 순서를 반대로 합니다.create_table
을drop_table
로 교체합니다.- 마지막으로,
up
을down
으로, 그 반대로 교체합니다.
이 모든 것이 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:up
과 db: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 확장을 활성화해야 할 수 있습니다.
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를 사용하도록 지시합니다.
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과 데이터베이스에 대해서도 마찬가지로 구문을 조정하세요.
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 마이그레이션을 방해하지 않으면서 데이터 마이그레이션과 기타 유지보수 작업을 안전하고 쉽게 관리할 수 있는 방법으로 생성하고 관리하기 위한 프레임워크를 제공합니다.