ORM을 사용하는 여러가지 이유가 있겠지만 나같은 경우에 ORM을 사용하는 가장 큰 이유는 별다른 노력을 들이지 않고도 개발자간에 스키마 구조를 공유할 수 있다는 점을 가장 좋아한다.
전통적 방식으로 코드 상에서 DB를 쓰게되면 다른 개발자가 참여하게 됐을 때 스키마 구조 파악을 위해 DB를 조회해보거나 따로 첨부된 ERD를 확인해야하고, 사용된 쿼리도 어떤 내용인지 읽어봐야한다.
ORM을 사용하는 경우 대부분 ORM 자체의 모델 정의 코드만 봐도 DB 구조를 알 수 있고, 해당 DB의 SQL 문법을 몰라도 코드상 어떤 쿼리를 하는 것인지 파악하기 쉽다.
SQLAlchemy는 Python에서 대표적으로 사용되는 ORM 패키지라 선택하게 되었다.
Flask에서 SQLAlchemy를 사용할 땐 보통 Flask-SQLAlchemy를 많이 사용하는 것 같았다.
실제로 Flask 공식 문서에서도 Flask-SQLAlchemy를 소개하고 있다.
Flask-SQLAlchemy를 사용하면 편리하게 연동 설정을 할 수 있지만 SQLAlchemy 버전을 1.0.10까지만 지원하고 있다.
출시되어 있는 SQLAlchemy의 최신버전은 2.0이므로 최신 버전 사용을 원한다면 직접 설정할 것을 권하는 바이다.
내가 써봤던 ORM 툴들은 보통 migration 기능을 포함했었는데 SQLAlchemy의 경우 Alembic이라는 별도 패키지를 함께 써야한다고 한다.
모델을 수정할 때마다 일일이 따로 migration을 하는 일은 매우 성가신 일이기에 Alembic도 사용하기로 결정했다.
(migration 기능은 모델을 정의하거나 수정할 때 변경내역을 저장하고 그에 맞춰 실제 DB에 적용해주는 기능을 말한다.)
관련해서 검색해보다 보니 Flask, SQLAlchemy, Alembic을 한번에 설정해 사용할 수 있는 Flask-Migrate라는 패키지가 있어 이를 사용하기로 했다.
pip를 이용해 Flask-Migrate를 설치해준다.
Flask, Flask-SQLAlchemy, Flask-Script가 함께 설치된다. (기존에 설치되지 않았을 경우)
MySQL을 이용할 예정이므로, Mysql connector인 mysqlclient도 설치해준다.
$ pip install Flask-Migrate
$ pip install mysqlclient
app.py 혹은 Flask 메인파일에서 DB 연결을 설정해준다.
이 때 보안을 위해 DB 정보는 별도 파일에 작성 후 .gitignore에 등록하여 업로드를 막도록 하자.
app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from models import db # 아래에서 작성
from config import DB_USERNAME, DB_PASSWORD, DB_HOST, DB_NAME, DB_PORT
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql://{DB_USERNAME}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
config.py
DB_USERNAME = 'USERNAME'
DB_PASSWORD = 'PASSWORD'
DB_HOST = 'localhost'
DB_NAME = 'DBNAME'
DB_PORT = '3306'
코드 작성 완료 후 아래 명령어를 통해 실제 DB를 생성하거나,
이미 생성된 DB의 경우 migration을 활성화 해준다.
$ flask db init
정상적으로 실행될 경우 아래 이미지처럼 migrations 디렉토리가 생성되고 하위에 다음과 같은 파일들이 생긴다.
versions 디렉토리는 migration 파일들을 생성한적이 없으므로 비어있다.
테이블과 맵핑될 모델을 작성해준다.
app.py
from flask_sqlalchemy import SQLAlchemy
...
db = SQLAlchemy(app)
migrate = Migrate(app, db)
...
class Post(db.Model):
__tablename__ = 'blogposts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120))
content = db.Column(db.Text)
date = db.Column(db.DateTime)
tag = db.Column(db.String(120))
cover = db.Column(db.String(120))
def __repr__(self):
return '<Post: %r>' % (self.title)
작성한 모델을 토대로 아래 명령어를 통해 migration version 파일을 생성해준다.
$ flask db migrate
문제없이 실행되면 다음과같은 로그가 뜬다.
모델과 기존 DB의 차이를 확인하여, migrations/versions 하위에 파일을 생성한다.
파일을 확인해보면 upgrade시 해당 컬럼들을 가진 테이블을 생성해주고 downgrade하면 해당 테이블을 삭제해주는 것을 볼 수 있다.
cc59fedc5e62_.py
"""empty message
Revision ID: cc59fedc5e62
Revises:
Create Date: 2022-01-04 15:05:05.135042
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cc59fedc5e62'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('blogposts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=120), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('tag', sa.String(length=120), nullable=True),
sa.Column('cover', sa.String(length=120), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('blogposts')
# ### end Alembic commands ###
upgrade 명령어를 통해 실제 DB에 적용해준다.
$ flask db upgrade
다음과 같은 로그가 뜨면 적용 완료된 것이다.
실제 MySQL workbench에서 확인해보면 테이블이 잘 생성된 것을 볼 수 있다.
migration 버전은 DB내 alembic_version
테이블을 통해 관리된다.
upgrade를 적용할 때 마다 테이블에 최신 revision id가 저장되고, downgrade 할 경우 migration 파일의 down_revision 변수에 저장된 이전 revision id가 저장된다.
Flask-Migrate 사용법을 공식 문서를 참고하여 간단하게 익혀봤다.
실제 사용할 때에는 model 코드와 view 코드, 설정 코드 등등 다양한 내용을 한 파일에 다 때려박으면 유지보수가 불편하기 때문에 파일 구조를 나눠서 사용해야 할 것 이다.
해당 내용은 스택오버플로우와 github issue를 참조하려고 한다.
원래 이 포스트에 함께 쓰려고 했는데 글이 너무 길어진 것 같아 참조 링크만 남기고 여기서 줄인다.