SQLModel: Giải pháp ‘2 trong 1’ giúp dọn dẹp đống đổ nát giữa SQLAlchemy và Pydantic

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

Cơn ác mộng mang tên “Nhân đôi Model”

Nếu đã từng build API với FastAPI và SQLAlchemy, chắc hẳn bạn đã nếm trải cảm giác khó chịu khi phải định nghĩa một cấu trúc dữ liệu tận hai lần.

Đầu tiên, bạn viết một class SQLAlchemy để map với Database. Ngay sau đó, bạn lại phải copy gần như y hệt class đó sang Pydantic để validate dữ liệu đầu vào và format JSON đầu ra.

# SQLAlchemy Model (cho Database)
class UserDB(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String, index=True)
    email = Column(String, unique=True)

# Pydantic Model (cho API)
class UserOut(BaseModel):
    id: int
    username: str
    email: str

Với 1-2 table thì không sao. Nhưng khi dự án scale lên 50-100 tables, việc duy trì sự đồng bộ trở thành một gánh nặng khủng khiếp. Chỉ cần bạn đổi kiểu dữ liệu từ String sang Text ở DB mà quên cập nhật Pydantic, hệ thống sẽ quăng lỗi 500 vào mặt user ngay lập tức.

Tại sao chúng ta lại phải khổ như vậy?

Vấn đề nằm ở sự khác biệt về mục tiêu. SQLAlchemy tập trung vào việc chuyển đổi object thành các câu lệnh SQL. Trong khi đó, Pydantic lại cực giỏi trong việc parse dữ liệu và ép kiểu JSON. Hai thư viện này vốn không được thiết kế để hiểu nhau.

Trong một dự án thực tế tôi từng tham gia, việc refactor lại 30 field trong database đã ngốn mất cả một buổi chiều. Phần lớn thời gian chỉ dành cho việc tìm và sửa các class Pydantic tương ứng để tránh lỗi lệch schema (schema mismatch). Đó là lúc chúng ta cần một sự hợp nhất.

Những phương pháp chữa cháy thông thường

Trước khi SQLModel xuất hiện, giới dev thường chọn một trong hai cách:

  • Copy-Paste thủ công: Cách này nhanh lúc đầu nhưng là “bom nổ chậm” cho việc bảo trì.
  • Dùng library chuyển đổi (như pydantic-sqlalchemy): Công cụ này hoạt động khá ổn nhưng thường hụt hơi với các tính năng mới của Pydantic v2 hoặc các mối quan hệ (relationship) phức tạp.

Cả hai cách đều không giải quyết được gốc rễ: Bạn vẫn đang quản lý hai nguồn sự thật (Single Source of Truth) khác nhau cho cùng một loại thực thể.

SQLModel: Một class, hai nhiệm vụ

SQLModel là đứa con cưng của Tiangolo (tác giả FastAPI). Thư viện này kế thừa trực tiếp từ cả SQLAlchemy và Pydantic. Mục tiêu của nó rất rõ ràng: Biến một class Python duy nhất vừa thành model database, vừa thành schema validate dữ liệu.

Cài đặt nhanh

Bạn chỉ cần cài đặt gói duy nhất. Nếu muốn test nhanh với SQLite, không cần cài thêm driver phức tạp.

pip install sqlmodel

Cách định nghĩa Model “lai”

Phép màu nằm ở tham số table=True. Nó báo hiệu cho SQLModel biết đây là class cần được map vào database.

from typing import Optional
from sqlmodel import Field, SQLModel, create_engine, Session, select

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None

Lúc này, Hero đóng vai trò kép. Bạn dùng nó để nhận dữ liệu từ request body của FastAPI, đồng thời dùng nó để lưu trực tiếp vào database mà không cần convert qua lại.

Thao tác dữ liệu an toàn

Kinh nghiệm xương máu là hãy luôn dùng Session trong context manager (khối lệnh with). Cách này đảm bảo kết nối được đóng ngay khi xong việc, tránh tình trạng treo database do leak connection.

def create_hero():
    hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
    
    with Session(engine) as session:
        session.add(hero_1)
        session.commit()
        session.refresh(hero_1) # Lấy ID vừa được tạo tự động từ DB
        print("Hero mới:", hero_1)

Kỹ thuật phân tầng Model trong thực tế

Đừng bao giờ phơi bày toàn bộ database ra API. Những thông tin nhạy cảm như hashed_password cần được ẩn đi. SQLModel hỗ trợ kế thừa cực kỳ linh hoạt để giải quyết việc này.

Tôi thường tổ chức code theo 3 lớp để đảm bảo tính bảo mật:

  1. Base: Chứa các field chung mà ai cũng thấy.
  2. Create: Chứa các field chỉ dùng khi đăng ký (như password).
  3. Table: Class thực sự map với DB, chứa thêm các field private.
class HeroBase(SQLModel):
    name: str
    age: Optional[int] = None

class HeroCreate(HeroBase):
    password: str # Chỉ dùng khi nhận data từ client

class Hero(HeroBase, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    hashed_password: str # Lưu vào DB, không trả về API

Tại sao bạn nên dùng SQLModel ngay?

Nếu bạn đang khởi tạo dự án FastAPI mới, hãy chọn SQLModel. Nó giúp giảm tới 50% lượng code boilerplate. Quan trọng hơn, khả năng gợi ý code (IntelliSense) của nó trên VS Code hoạt động cực kỳ mượt mà. Bạn sẽ không bao giờ gặp phải lỗi gõ sai tên field mà IDE không báo trước.

SQLModel không chỉ giúp code ngắn hơn. Nó giúp code của bạn sạch hơn, dễ đọc hơn và quan trọng nhất là dễ bảo trì hơn về lâu dài. Hãy thử áp dụng vào một module nhỏ, bạn sẽ thấy sự khác biệt ngay lập tức.

Share: