
I. database.py에 BaseModel 추가
class BaseModel(Base):
__abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
- 백엔드에서 넘어오는 값의 끝에 Z 또는 +HH:MM/-HH:MM → 타임존 있음 → 프론트엔드에서 moment(value).local().format(...) 사용
- 백엔드에서 넘어오는 값의 끝에 접미사 없음(예: 2025-11-05T16:12:44) → naive
- 의미가 UTC → 프론트엔드에서 moment.utc(value).local().format(...) 사용 (본 프로젝트에서는 이것을 사용)
- 의미가 특정 타임존 → 프론트엔드에서 moment.tz(value, '...').local().format(...) 사용
# Svelte_0.0.2/app/core/database.py
from datetime import datetime, timezone
from typing import AsyncGenerator
from sqlalchemy import Integer, DateTime
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import declarative_base, Mapped, mapped_column
from app.core.settings import CONFIG
DATABASE_URL = f"{CONFIG.DB_TYPE}+{CONFIG.DB_DRIVER}://{CONFIG.DB_USER}:{CONFIG.DB_PASSWORD}@{CONFIG.DB_HOST}:{CONFIG.DB_PORT}/{CONFIG.DB_NAME}?charset=utf8"
print("2. DATABASE_URL:", DATABASE_URL.split("//")[0])
ASYNC_ENGINE = create_async_engine(DATABASE_URL,
echo=CONFIG.DEBUG,
future=True,
pool_size=10, max_overflow=0, pool_recycle=300, # 5분마다 연결 재활용
# encoding="utf-8"
)
# 세션 로컬 클래스 생성
AsyncSessionLocal = async_sessionmaker(
ASYNC_ENGINE,
class_=AsyncSession, # add
expire_on_commit=False,
autocommit=False,
autoflush=False,
# poolclass=NullPool, # SQLite에서는 NullPool 권장
)
"""
autocommit=False 부분은 주의하자.
autocommit=False로 설정하면 데이터를 변경했을때 commit 이라는 사인을 주어야만 실제 저장이 된다.
만약 autocommit=True로 설정할 경우에는 commit이라는 사인이 없어도 즉시 데이터베이스에 변경사항이 적용된다.
그리고 autocommit=False인 경우에는 데이터를 잘못 저장했을 경우 rollback 사인으로 되돌리는 것이 가능하지만
autocommit=True인 경우에는 commit이 필요없는 것처럼 rollback도 동작하지 않는다는 점에 주의해야 한다.
"""
Base = declarative_base() # Base 클래스 (모든 모델이 상속)
class BaseModel(Base):
__abstract__ = True
id: Mapped[int] = mapped_column(Integer, primary_key=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
async def get_db() -> AsyncGenerator[AsyncSession, None]:
session: AsyncSession = AsyncSessionLocal()
print(f"[get_session] new session: {id(session)}")
try:
yield session
except Exception as e:
print(f"Session rollback triggered due to exception: {e}")
await session.rollback()
raise
finally:
print(f"[get_session] close session: {id(session)}")
await session.close()
II, User Model 작성
# Svelte_0.0.1/app/models/user.py
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import BaseModel
class User(BaseModel):
__tablename__ = "users"
# String은 제한 글자수를 지정해야 한다.
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, nullable=False)
email: Mapped[str] = mapped_column(String(100), unique=True, index=True, nullable=False)
password: Mapped[str] = mapped_column(String(100), nullable=False)
is_admin: Mapped[bool] = mapped_column(default=False, nullable=False)
"""
- unique 와 index 중복
- unique=True만으로도 고유 인덱스가 생성됩니다. index=True는 중복이므로 제거해도 됩니다(권장).
- 비밀번호 길이
- password는 해시를 저장하므로 길이를 넉넉히 두는 것이 안전합니다(예: String(255)). 해시 종류에 따라 100자를 넘길 수 있습니다.
- 기본 키/타입
- id는 Integer(primary_key=True)면 충분합니다. 아주 큰 규모를 예상하면 BigInteger도 고려할 수 있습니다.
"""
III, 첫 alembic init migrations
1. Svelte_0.0.1/migrarions/env.py와 Svelte_0.0.1/alembic.ini 파일의 수정
alembic init migration을 시행하면, 프로젝트 디렉토리 바로 아래에 alembic.ini 파일과 migration 폴더가 생성된다. 여기에서 migrarions 폴더 밑에 있는 env.py와 프로젝트 폴더 밑에 잇는 alembic.ini 파일의 수정해야 한다.
alembic.ini 파일의 "sqlalchemy.url = " 을 실제 사용할 DB_URL로 설정해줘야 한다. 하지만, 이 "sqlalchemy.url = "은 보안 사항이기 때문에 환경변수에서 불러와서 사용하는 것이 좋다. 그러나, Alembic은 alembic.ini 파일 자체에서 .env 파일의 내용을 직접 불러오는 기능을 기본적으로 제공하지는 않습니다.
alembic.ini 파일은 정적 문자열만을 사용하며 파이썬 코드를 실행할 수 없습니다. 하지만, Alembic은 마이그레이션 실행 시 항상 거치게 되는 env.py 파일을 수정하여 이 문제를 해결할 수 있는 방법을 제공합니다. env.py 파일은 파이썬 스크립트이므로, 여기에서 .env 파일을 읽고 환경 변수를 로드한 다음 데이터베이스 URL을 설정할 수 있습니다.
앞에서 Svelte_0.0.2/app/core/database.py에서 개발과 배포과정을 전부 커버할 수 있는 DATABASE_URL을 환경변수에서 불러와 만들어 두었다. 이것을 활용하여 env.py를 수정하면 된다. alembic.ini 파일에서 sqlalchemy.url 항목을 주석 처리하거나 비워둡니다. env.py에서 이 설정을 덮어쓸 것이다.
# alembic.ini 파일 내용 중
# ...
# sqlalchemy.url = driver://user:pass@localhost/dbname
sqlalchemy.url =
# Svelte_0.0.2/app/core/database.py의 DATABASE_URL 임포트
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode for an async application."""
# 환경 변수가 설정되지 않았다면 alembic.ini의 기본값을 사용하도록 폴백 설정 (선택 사항)
db_url = DATABASE_URL
if not db_url:
db_url = config.get_main_option("sqlalchemy.url")
if not db_url:
raise ValueError("데이터베이스 URL을 찾을 수 없습니다. .env 파일이나 환경 변수를 확인하세요.")
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True, # SQLAlchemy 2.0
url=db_url # 여기에 환경 변수에서 가져온 URL을 전달 (개발 및 배포모드 모두 커버)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
위의 내용 이외에도 수정할 부분이 여럿 있는데, 설명은 피하겠다. 그냥 복붙해서 사용하기로 하자. 완성된 env.py는 아래와 같다.
# Svelte_0.0.2/migrations/env.py
import asyncio
from logging.config import fileConfig
import os
import sys
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
from app.core.database import Base, DATABASE_URL
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
# if config.config_file_name is not None:
# fileConfig(config.config_file_name)
if config.config_file_name is not None:
try:
fileConfig(config.config_file_name, encoding='utf-8')
except TypeError:
# older versions of fileConfig don't support the encoding parameter
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
''' 반드시 모델들이 추가될 때마다 임포트 하라. '''
from app.models import user
# from app.test import exam
# target_metadata = mymodel.Base.metadata
# target_metadata = None
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode for an async application."""
# 환경 변수가 설정되지 않았다면 alembic.ini의 기본값을 사용하도록 폴백 설정 (선택 사항)
db_url = DATABASE_URL
if not db_url:
db_url = config.get_main_option("sqlalchemy.url")
if not db_url:
raise ValueError("데이터베이스 URL을 찾을 수 없습니다. .env 파일이나 환경 변수를 확인하세요.")
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True, # SQLAlchemy 2.0
url=db_url # 여기에 환경 변수에서 가져온 URL을 전달 (개발 및 배포모드 모두 커버)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())
2. 이제 DB에 테이블을 생성하는 과정을 실행하면 된다. 그전에, MySQL에 DATABASE: my_db(db명)를 생성해 놓아야 한다.
alembic revision --autogenerate -m "create users table"
alembic upgrade head
이때, 데이터베이스의 alembic_version 테이블에 version_num이 생성된다.
alembic downgrade -1 하면, 바로 전 단계의 version_num이 삭제된다.
3. 전 단계의 alembic upgrade head를 되돌리려면,
alembic downgrade -1 하고, 바로 전 단계의
alembic revision --autogenerate -m "메시지"때 만들어진 version 폴더의 파일을 삭제하면 된다.
이 순서를 바꾸면 안된다. 추후에 하게되는 alembic revision --autogenerate 명령어가 먹지 않는다.
당연히 alembic upgrade head를 해도 추가 칼럼이나 테이블이 만들어지지 않는다.
여기서, alembic downgrade 123abcdef 와 같이 특정 버전으로 되돌릴 수도 있다.
번호를 입력하는 것 같은데 잘 안된다. string이나 integer로 둘다 넣어봐도 안된다.
'FastAPI' 카테고리의 다른 글
| FastAPI + Jinja Templates 기반 APP개발 시작 (0) | 2025.11.13 |
|---|---|
| alembic.ini파일과 migrations폴더를 삭제해도 되는가? (0) | 2025.11.06 |
| DB 서버와 Redis 서버 설정 및 연결/프론트엔드 렌더링 (0) | 2025.11.05 |
| main.py 수정 및 Swagger 커스터마이징 (0) | 2025.11.05 |
| 앱 개발을 위한 설정파일 만들기 (0) | 2025.11.05 |