Hướng dẫn sử dụng Alembic trong Python: Quản lý Database Migration chuẩn cho SQLAlchemy

Python tutorial - IT technology blog
Python tutorial - IT technology blog

1. Ba cách quản lý Database Schema phổ biến trong Python

Làm thế nào để cập nhật bảng trong database khi bạn vừa thêm một trường vào SQLAlchemy model? Đây là câu hỏi kinh điển mà bất kỳ ai làm Python backend cũng từng đau đầu. Về cơ bản, chúng ta có 3 hướng đi:

  • Cách 1: Chạy raw SQL thủ công — Thêm cột nào thì mở DBeaver hay pgAdmin gõ lệnh ALTER TABLE cột đó. Cách này nhanh với dự án cá nhân 1-2 bảng, nhưng cực kỳ rủi ro khi làm theo nhóm.
  • Cách 2: Dùng Base.metadata.drop_all() rồi create_all() — Thường thấy trong các tutorial nhập môn. Mỗi lần sửa model, bạn xóa sạch database và tạo lại. Trên production với 100.000 dòng dữ liệu khách hàng, làm vậy là mất trắng.
  • Cách 3: Dùng công cụ Migration chuyên dụng (Alembic) — Mọi thay đổi schema được lưu thành từng file mã nguồn (revision file). Bạn có thể commit vào Git, review trước khi áp dụng, nâng cấp (upgrade) hoặc rollback (downgrade) linh hoạt.

2. So sánh ưu và nhược điểm từng giải pháp

Tùy giai đoạn và quy mô dự án, mỗi phương pháp sẽ có bài toán riêng:

Tiêu chí Thủ công (Raw SQL) drop_all / create_all Alembic Migration
Thời gian setup ban đầu 0 phút, không cần cài thêm package Chưa đầy 1 phút, chỉ cần 1 dòng lệnh Tốn khoảng 5-10 phút cấu hình ban đầu
An toàn dữ liệu Nguy cơ cao do thao tác tay Mất 100% dữ liệu mỗi lần chạy Giữ nguyên dữ liệu, rollback linh hoạt
Làm việc nhóm (Teamwork) Dễ lệch schema giữa máy dev và server Không thể áp dụng Đồng bộ tuyệt đối qua Git branch
Khả năng theo dõi (Audit) Không có lịch sử tập trung Hoàn toàn không hỗ trợ Quản lý từng revision ID rõ ràng

3. Vì sao Alembic là chuẩn mực cho SQLAlchemy?

Alembic được phát triển bởi chính Mike Bayer — tác giả của SQLAlchemy. Vì vậy, khả năng bắt cặp giữa hai công cụ này gần như hoàn hảo.

Khi dự án chỉ có 2-3 bảng, bạn sửa tay vẫn ổn. Nhưng hãy thử tưởng tượng hệ thống phát triển lên 50 bảng với 5 lập trình viên cùng commit mỗi ngày. Lúc này, quản lý database bằng tay chắc chắn dẫn đến thảm họa lệch schema giữa Local, Staging và Production.

Alembic đọc metadata trực tiếp từ code Python của bạn. Sau đó, nó tự động đối chiếu với database thực tế để sinh script cập nhật. Nhờ vậy, schema database luôn bám sát mã nguồn một cách có kỷ luật.

4. Hướng dẫn thực hành từng bước với Alembic

Bước 1: Cài đặt thư viện

Cài đặt Alembic cùng driver database tương ứng vào virtualenv:

pip install sqlalchemy alembic psycopg2-binary

Bước 2: Khởi tạo cấu trúc thư mục migration

Tại thư mục gốc của project, bạn gõ lệnh:

alembic init alembic

Cấu trúc thư mục tạo ra sẽ trông như sau:

project_root/
├── alembic/
│   ├── versions/        # Nơi lưu trữ các file migration script
│   ├── env.py           # File điều khiển luồng thực thi migration
│   └── script.py.mako   # Template sinh file migration
├── alembic.ini          # File cấu hình database URL và logging
├── models.py            # Nơi khai báo SQLAlchemy models
└── main.py

Bước 3: Cấu hình kết nối và trỏ metadata trong env.py

Giả sử file models.py của bạn chứa model User:

# models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, func
from sqlalchemy.orm import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    email = Column(String(255), unique=True, nullable=False)
    username = Column(String(50), nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, server_default=func.now())

Tiếp theo, mở file alembic/env.py. Bạn cần import Base từ models.py và gán vào target_metadata để Alembic nhận diện:

# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# Import Base từ file models
from models import Base

config = context.config
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Chỉ định metadata cho Alembic so sánh
target_metadata = Base.metadata

Trong file alembic.ini, điền chuỗi kết nối đến cơ sở dữ liệu:

# alembic.ini
sqlalchemy.url = postgresql://postgres:secretpassword@localhost:5432/app_db

Bước 4: Tự động phát hiện thay đổi (Autogenerate)

Sau khi sửa model trong code Python, hãy để Alembic tự so sánh và sinh file migration bằng cờ --autogenerate:

alembic revision --autogenerate -m "create users table"

Một file mới sẽ xuất hiện trong alembic/versions/ (ví dụ: 1a2b3c4d5e_create_users_table.py):

"""create users table

Revision ID: 1a2b3c4d5e
Revises: 
Create Date: 2026-10-02 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa

def upgrade() -> None:
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('email', sa.String(length=255), nullable=False),
        sa.Column('username', sa.String(length=50), nullable=False),
        sa.Column('is_active', sa.Boolean(), nullable=True),
        sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email')
    )
    op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)

def downgrade() -> None:
    op.drop_index(op.f('ix_users_id'), table_name='users')
    op.drop_table('users')

Bước 5: Áp dụng thay đổi vào Database

Chạy lệnh sau để apply mọi migration đang chờ lên phiên bản mới nhất (head):

alembic upgrade head

Nếu gặp lỗi cần lùi lại phiên bản trước đó ngay lập tức, bạn chỉ cần gõ:

alembic downgrade -1

Kiểm tra trạng thái schema hiện tại và toàn bộ lịch sử:

alembic current
alembic history --verbose

5. Kinh nghiệm thực chiến khi chạy trên Production

  • Đừng tin 100% vào autogenerate: Alembic rất thông minh, nhưng nó không đọc được suy nghĩ của bạn. Nếu bạn đổi tên cột từ fullname thành name, nó sẽ hiểu là bạn vừa xóa cột cũ và thêm cột mới. Hậu quả là toàn bộ dữ liệu cột cũ bay màu. Hãy luôn mở file revision lên kiểm tra trước khi chạy.
  • Không hardcode database credentials: Đừng bao giờ lưu mật khẩu database trong alembic.ini rồi đẩy lên Git. Hãy cấu hình env.py để đọc dynamic URL từ biến môi trường qua os.getenv("DATABASE_URL") hoặc thư viện pydantic-settings.
  • Xử lý xung đột migration (Branch Conflict): Khi hai lập trình viên cùng tạo migration trên hai nhánh Git độc lập, lệnh alembic upgrade head sẽ báo lỗi Multiple head revisions. Bạn chỉ cần chạy lệnh alembic merge heads -m "merge branch migrations" để gộp hai nhánh lại trước khi deploy.
Share: