Devlog

[SQLAlchemy] Alembic을 이용해서 DB버전관리를 해보자 본문

Database/SQLAlchemy

[SQLAlchemy] Alembic을 이용해서 DB버전관리를 해보자

recoma 2023. 7. 9. 18:30

DJango에서는 기본적으로 DB마이그레이션 기능이 존재합니다.

 
python manage.py makemigrations

makemigrations 명령어로 DB변경 내역을 파이썬 파일로 자동 작성하고

python manage.py migrate

migrate 명령어를 사용해서 DB내역을 기반으로 실제 데이터베이스에 마이그레이션을 합니다.

그렇다면 Flask또는 FastAPI에서 주로 사용하는 SQLAlchemy에서는 어떻게 마이그레이션을 해야 할 까요?

여기도 마찬가지로 Alembic 모듈을 사용해서 DB버전관리를 할 수가 있습니다

이번 포스트에서는 Alembic의 사용법을 간단하게 설명하려고 합니다

이미 장고로 마이그레이션 기능을 사용한 경험이 있는 분들은 DJango마이그레이션과 Alembic작동 구조가 의외로 비슷해서

아마 어느정도 alembic 사용법을 익히는데 큰 부담이 없을 것 같습니다.

 


마이그레이션 환경 세팅하기

alembic 모듈을 설치하기 전, 선행 모듈을 먼저 설치합니다

$ pip install sqlalchemy pymysql mysqlclient

 

성공적으로 설치했으면 alembic도 설치합니다.

$ pip install alembic

 

그리고 아래 명령어를 입력해서 DB마이그레이션에 필요한 alembic관련 파일들을 생성합니다.

alembic init {원하는 이름}

마이그레이션에 필요한 파일들을 생성했으니 데이터베이스를 연동하기 위해 세팅을 해야 합니다

alembic.ini 파일에 들어가서 sqlalchemy.url을 찾아서 수정합니다.

sqlalchemy.url = mysql+pymysql://root:1234@192.168.0.104/alembic

 

Alembic으로 User 테이블 만들어보기

로우 쿼리나 SQLAlchemy의 create_all 함수가 아닌 오직 alembic으로 유저 테이블을 만들어 봅시다.

alembic의 revision 명령어를 입력해서 버전내역을 생성합니다

저는 메세지에 "add user table"이라고 적었습니다.

$ alembic revision -m "{쓰고싶은 메세지}"
# versions/eacbc10c2794_add_user_table.py
"""add user table

Revision ID: eacbc10c2794
Revises: 
Create Date: 2023-07-09 11:05:06.643433

"""
from alembic import op
import sqlalchemy as sa


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


def upgrade() -> None:
    pass


def downgrade() -> None:
    pass

이 버전 파일들은 DJango의 migrations 디렉토리에 있는 DB버전로그 파이썬 파일과 그 역할이 동일하다고 보시면 됩니다.

단, 이 버전 로그 내역을 DJango에서는 자동으로 만들어줬다면, 여기에서는 우리가 직접 내역을 작성해야 합니다.

upgrade() 함수에서는 버전 내역을 작성합니다. 예를 들어 지금처럼 유저 테이블을 생성한다고 하면, 유저 테이블을 생성하는 프로세스를 작성하면 됩니다.

반대로 downgrade()는 말 그대로 적용한 부분을 다시 롤백한다는 뜻입니다. upgrade()에서 유저 테이블을 생성하는 프로세스를 작성했다면, downgrade()는 반대로 유저 테이블을 없애는 프로세스를 작성하면 됩니다.

 

upgrade()와 downgrade() 함수를 아래와 같이 입력합니다.

SQLAlchemy를 기반으로 작동하기 때문에, 컬럼을 생성할 때 SQLAlchemy에서 사용하던 문법과 똑같이 사용하면 됩니다.

def upgrade() -> None:
    op.create_table(
        'user',
        sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
        sa.Column('nickname', sa.String(16), nullable=False, unique=True),
        sa.Column('password', sa.String(16), nullable=False)
    )


def downgrade() -> None:
    op.drop_table('user')

 

아래의 upgrade() 명령어를 실행합니다.

$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> eacbc10c2794, add user table

 

head는 가장 최근의 버전로그를 의미합니다. 특정 버전로그로 업데이트를 하고 싶은 경우, 그 버전로그의 Revision ID를 입력하면 됩니다.

$ alembic upgrade eacbc10c279
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> eacbc10c2794, add user table

 

이제 데이터베이스에 user 테이블이 생겼습니다.

하지만 유저 테이블 말고도 다른 테이블도 생성이 되었습니다.

alembic_version이라는 테이블이 생겼는데, version_name에서는 최근에 적용된 버전로그의 Revision ID가 저장이 됩니다.

 

변경사항 롤백하기

하지만 반대로 upgrade() 프로세스를 잘못 작성하는 바람에 DB스키마를 잘못 생성할 때가 있습니다. 이때는 롤백을 해 줘야 하는데 upgrade가 아닌 downgrade를 사용합니다.

여기에서는 downgrade함수에 user테이블을 삭제하는 프로세스를 작성했기 때문에 downgrade를 수행하면 user테이블이 삭제가 됩니다.

단 upgrade와는 달리 downgrade에서는 최근의 버전로그를 롤백할 때, head가 아닌 -1을 입력합니다. 아니면 최근 버전로그의 Revision ID를 입력합니다

$ alembic downgrade -1
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade eacbc10c2794 -> , add user table

.

컬럼 추가하기

테이블 생성 말고도 기존의 테이블에서 컬럼을 추가하거나 삭제할 수 있습니다.

기존의 유저 테이블에서 이메일 컬럼을 추가하려고 합니다.

버전 관리를 해야 하니 버전 파일을 하나 더 만들어 봅시다.

alembic revision -m "add email in user"

 

새로 생성된 버전로그 파일에 아래와 같이 작성합니다

upgrade함수에는 email 컬럼을 생성하는 프로세스를

downgrade함수에는 반대로 email컬럼을 삭제하는 프로세스를 작성합니다.

def upgrade() -> None:
    op.add_column('user', sa.Column('email', sa.String(128), nullable=False))


def downgrade() -> None:
    op.drop_column('user', 'email')
$ alembic upgrade head
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade eacbc10c2794 -> c028cbc00cce, add email in user

email 컬럼을 추가하는 데 성공했습니다

컬럼 추가 말고도, 수정, 삭제 등, 데이터베이스 안에서의 거의 모든 작업들을

여기서 대부분 할 수 있습니다.

 

Model만 바꿔도 자동으로 버전로그 작성되게 하기

하지만 이러한 방식은 버전로그를 직접 작성해야 한다는 단점이 있기 때문에 불편한 감이 없지 않아 있습니다.

당장 DJango만 봐도 모델 클래스만 수정한 다음, makemigrations명령어만 입력하면 수정된 모델 정보에 대한

버전로그 파일을 자동으로 생성을 합니다.

운이 좋게도 "autoupgrade"라는 옵션을 사용해 Alembic에서도 버전로그를 모델 클래스 변경에 따라 자동으로 버전로그 파일을 자동으로 작성하는 방법이 있습니다.

아까 전처럼 alembic init명령어로 세팅을 한 다음 model.py 파일을 생성하고 아래와 같이 적어줍니다.

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String(16), unique=True, nullable=False)
    password = Column(String(16), nullable=False)

 

그리고 env.py로 돌아가서 target_metadata = None를 아래와 같이 바꿔줍니다.

target_metadata = Base.metadata

 

아래 명령어를 입력합니다.

$ alembic revision --autogenerate -m "create user table"
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
  Generating /home/recoma/practice/alembic/migration/versions/e42d0e1b971d_create_user_table.py ...  done

model.py에서 새 모델이 추가되었음을 감지하고 (Detected added table 'user')

이를 기반으로 버전로그 파일을 자동으로 생성했습니다.

마치 DJango의 makemigrations와 유사합니다.

생성된 버전로그 파일을 열어보면 유저 테이블을 생성하는 프로세스가 작성되어 있음을 알 수 있습니다.

"""create user table

Revision ID: e42d0e1b971d
Revises: 
Create Date: 2023-07-09 16:31:43.058976

"""
from alembic import op
import sqlalchemy as sa


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


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('user',
    sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
    sa.Column('name', sa.String(length=16), nullable=False),
    sa.Column('password', sa.String(length=16), nullable=False),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user')
    # ### end Alembic commands ###

 

이제 기존 절차와 동일하게 upgrade명령어를 입력하면 유저 테이블이 생기게 됩니다.

$ alembic upgrade head

 

모델이 변경될 때도 자동으로 버전로그 파일을 만들어주는 지 확인합니다.

기존 User 모델 클래스에 email 컬럼을 추가하고 아까와 똑같은 명령어를 입력합니다.

email = Column(String(16), unique=True, nullable=False)
$ alembic revision --autogenerate -m "add email in user"
INFO  [alembic.runtime.migration] Context impl MySQLImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added column 'user.email'
INFO  [alembic.autogenerate.compare] Detected added unique constraint 'None' on '['email']'
  Generating /home/recoma/practice/alembic/migration/versions/2fd2f7d19dc7_add_email_in_user.py ...  done

 

이메일을 추가하는 버전로그 파일이 생성되었습니다.

def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('user', sa.Column('email', sa.String(length=16), nullable=False))
    op.create_unique_constraint(None, 'user', ['email'])
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_constraint(None, 'user', type_='unique')
    op.drop_column('user', 'email')
    # ### end Alembic commands ###

이 상태에서 다시 'alembic upgrade head'를 입력하면 DB에 바로 마이그레이션이 됩니다.


이렇게 해서 Flask또는 FastAPI에서도 SQLAlchemy와 Alembic으로 DJango처럼 자동으로 마이그레이션 파일을 만들고 이를 기반으로 DB에 마이그레이션을 할 수 있게 되었습니다.

단 몇가지 유의사항이 있는데

바로 파일로 작성된 버전 로그들과 실제 데이터베이스의 구조가 동일, 즉 동기화 되어야 합니다.

구조가 서로 다르게 되면 Alembic이 제대로 작동이 되질 않기 때문에

프로젝트 구현 진행 시, Alembic을 중간에 도입하지 않고 처음 구현할 때부터 도입하는 것을 권장하고 있습니다.

Flask, FastAPI의 경우 DJango처럼 DB마이그레이션 기능 자체가 없기 때문에,

이 프레임워크를 사용한지 얼마 안된 분들에게는 유지보수를 하는 데 부담이 클 수 있습니다.

저도 작년에 FastAPI로 프로젝트를 진행하고 유지보수를 했을 때, FastAPI코드에 작성된 모델과 실제 DB 구조를 맞추는데 애를 많이 먹은 적도 있었구요

하지만 이런 환경에서도 다행이 Alembic이라는 모듈 덕분에, DJango가 아니어도 큰 부담 없이 DB마이그레이션을 쉽게 할 수 있게 되었습니다.

우리모두 Alembic으로 고민없은 행복한 코딩을 해 봅시다.

Alembic의 자세한 설명은 공식 도큐먼트에서 보실 수 있습니다

 

Welcome to Alembic’s documentation! — Alembic 1.11.1 documentation

 

alembic.sqlalchemy.org

반응형

'Database > SQLAlchemy' 카테고리의 다른 글

[SQLAlchemy] - first(), scalar(), one()  (0) 2022.04.22