All Articles

Alembic을 사용한 Offline Database Migration

이전포스트 에서 이어진다… 지금은 MySQL + SQLAlchemy 설정까지는 끝났고, 오프라인 데이터베이스 마이그레이션을 하기위해 alembic을 설정하려고 한다.

pip install alembic

pip freeze > requirements.txt

혼자 하는 프로젝트지만 나중에 dockerize해야하기 때문에 requirements.txt도 잘 관리해야 한다. 여담이지만 파이썬 레포를 생성하면 무조건 conda를 사용해서 가상환경부터 만드는 습관이 있다. 이제 alembic을 적용해본다.

alembic init alembic

그러면 아래와 같은 메세지가 나온다

  Creating directory /Users/byeongjinkang/personal/development/mukkang_server/alembic ...  done
  Creating directory /Users/byeongjinkang/personal/development/mukkang_server/alembic/versions ...  done
  Generating /Users/byeongjinkang/personal/development/mukkang_server/alembic/script.py.mako ...  done
  Generating /Users/byeongjinkang/personal/development/mukkang_server/alembic/env.py ...  done
  Generating /Users/byeongjinkang/personal/development/mukkang_server/alembic/README ...  done
  Generating /Users/byeongjinkang/personal/development/mukkang_server/alembic.ini ...  done
  Please edit configuration/connection/logging settings in '/Users/byeongjinkang/personal/development/mukkang_server/alembic.ini'
  before proceeding.

이제 디렉토리 구조는 아래와 같이 바뀐다

app
├── __init__.py
├── main.py
├── alembic
│   ├── env.py
│   ├── script.py.mako
│   └── versions
├── alembic.ini
├── routers
│   ├── __init__.py
│   └── users.py
└── sql
    ├── __init__.py
    ├── crud.py
    ├── database.py
    ├── models.py
    └── schemas.py

이제 위 메세지에서 나온 .ini 파일을 수정한다. 가장 먼저 database.py에 선언했던 데이터베이스 인터페이스 주소를 입력한다.

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

sqlalchemy.url = mysql+mysqlconnector://root:password@localhost:3306/database

나중에 환경변수화 해야겠지만 일단은 그냥 넣어둔다. 장고에서 사용하는 python manage.py makemigrations 와 유사한 기능을 하는 명령어는 alembic revision --autogenerate -m "CUSTOM MESSAGE" 이다.

autogenerate를 사용하지 않으려면 revision파일을 만들고 일일이 테이블을 선언해줘야 하는데, 이러면 models.py에 작성한 내용을 포맷에 맞게 입력해야한다. 따라서 autogenerate를 사용하기 위해서 alembic 디렉토리 내 env.pytarget_metadata라는 변수를 추가로 설정해줘야한다. 예제를 보면 model에서 Base를 import 한다. 따라서 기존에 database.py 에서 선언한 declarative_basemodels.py로 옮기고 env.py파일을 업데이트 한다

# env.py

from sql.models import Base

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata 
# models.py

from sqlalchemy import (
    Boolean,
    Column,
    Date,
    Integer,
    String,
)
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    password = Column(String)
    is_active = Column(Boolean, default=True)
    phone = Column(String, unique=True)
    birth = Column(Date, unique=True)
    gender = Column(String, default='M')

그러고 위에 명령어를 다시 날려준다

alembic revision --autogenerate -m "Added user table"

위 명령어를 실행하면 MySQL 서버에 데이터베이스가 없다는 에러가 난다. 일단 데이터베이스를 먼저 만들어주고 명령어를 다시 돌린다. 장고 마이그레이션 파일과 유사한 파일이 생긴다

# alembic/versions/

"""Added user table

Revision ID: 3829d0b7507e
Revises: 
Create Date: 2022-07-06 00:09:19.842063

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '3829d0b7507e'
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('email', sa.String(), nullable=True),
    sa.Column('password', sa.String(), nullable=True),
    sa.Column('is_active', sa.Boolean(), nullable=True),
    sa.Column('phone', sa.String(), nullable=True),
    sa.Column('birth', sa.Date(), nullable=True),
    sa.Column('gender', sa.String(), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('birth'),
    sa.UniqueConstraint('phone')
    )
    op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
    op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_index(op.f('ix_users_id'), table_name='users')
    op.drop_index(op.f('ix_users_email'), table_name='users')
    op.drop_table('users')
    # ### end Alembic commands ###

이제 마이그레이션을 디비에 적용한다. alembic upgrade head 이러면 또 에러가 발생하는데,

(in table 'users', column 'email'): VARCHAR requires a length on dialect mysql

FastAPI에서 제공한 예제는 SQLITE 용이고, mysql은 string에서 length를 지정해줘야 하기 때문이다. 따라서 models.py를 수정한다

# models.py

class User(Base):
    __tablename__ = "users"

    id        = Column(Integer, primary_key=True, index=True)
    email     = Column(String(255), unique=True, index=True)
    password  = Column(String(255))
    is_active = Column(Boolean, default=True)
    phone     = Column(String(13), unique=True)
    birth     = Column(Date, unique=True)
    gender    = Column(String(1), default='M')

기존 파일을 지우고 이제 다시 autogenerate를 해본다 alembic revision --autogenerate -m "Added user table" String들에 length 정보가 추가된 것을 확인할 수 있다.

# /versions

def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('email', sa.String(length=255), nullable=True),
    sa.Column('password', sa.String(length=255), nullable=True),
    sa.Column('is_active', sa.Boolean(), nullable=True),
    sa.Column('phone', sa.String(length=13), nullable=True),
    sa.Column('birth', sa.Date(), nullable=True),
    sa.Column('gender', sa.String(length=1), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('birth'),
    sa.UniqueConstraint('phone')
    )
    op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
    op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
    # ### end Alembic commands ###

이제 마이그레이션을 디비에 적용한다. alembic upgrade head

MySQL에도 잘 적용된 것을 확인할 수 있다.

+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int(11)      | NO   | PRI | NULL    | auto_increment |
| email     | varchar(255) | YES  | UNI | NULL    |                |
| password  | varchar(255) | YES  |     | NULL    |                |
| is_active | tinyint(1)   | YES  |     | NULL    |                |
| phone     | varchar(13)  | YES  | UNI | NULL    |                |
| birth     | date         | YES  | UNI | NULL    |                |
| gender    | varchar(1)   | YES  |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+

서버도 잘 돌아가고 running-fastapi-server Hello World!도 잘 찍힌다. hello-world 이어서 다음 포스트에서는 회원가입을 다뤄보겠다.

Jul 6, 2022

AI Enthusiast and a Software Engineer