SQLAlchemy ORM Thực Chiến: Hướng Dẫn Toàn Diện Từ Thiết Lập Đến Quản Lý Giao Dịch

SQLAlchemy là một thư viện ORM (Object-Relational Mapping) mạnh mẽ và linh hoạt cho Python, giúp chuyển đổi giữa các đối tượng Python và cấu trúc bảng quan hệ một cách tự nhiên. Bài viết này trình bày cách triển khai thực tế với SQLAlchemy — từ khởi tạo kết nối, định nghĩa mô hình dữ liệu, đến thao tác CRUD nâng cao, truy vấn phức tạp và kiểm soát giao dịch an toàn.

1. Cài đặt và thiết lập môi trường

Cài đặt cơ bản:

pip install sqlalchemy

Tùy vào hệ quản trị cơ sở dữ liệu (DBMS), cài thêm driver tương ứng:

  • PostgreSQL: pip install psycopg2-binary
  • MySQL/MariaDB: pip install mysqlclient hoặc pip install PyMySQL
  • SQLite: Đã tích hợp sẵn trong Python — không cần cài thêm.

2. Các thành phần cốt lõi

  • Engine: Đối tượng điều khiển kết nối vật lý tới cơ sở dữ liệu.
  • Session: Đại diện cho một "phiên làm việc" với DB — nơi thực hiện mọi thao tác đọc/ghi.
  • Declarative Base: Lớp nền để khai báo các mô hình (model classes).
  • Query: Cơ chế xây dựng câu lệnh SELECT thông qua API hướng đối tượng.

3. Thiết lập kết nối và phiên làm việc

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Kết nối tới SQLite (dùng cho phát triển nhanh)
db_url = "sqlite:///app_data.db"

# Với PostgreSQL hoặc MySQL, dùng định dạng:
# db_url = "postgresql+psycopg2://user:pass@localhost:5432/myapp"
# db_url = "mysql+pymysql://user:pass@localhost:3306/myapp"

engine = create_engine(db_url, echo=False, pool_pre_ping=True)

# Tạo factory để sinh ra các phiên độc lập
Session = sessionmaker(bind=engine)
db_session = Session()

4. Định nghĩa mô hình dữ liệu với quan hệ đa chiều

from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Table
from sqlalchemy.orm import relationship, declarative_base
from datetime import datetime

Base = declarative_base()

# Bảng trung gian cho quan hệ nhiều-nhiều
article_category = Table(
    "article_category",
    Base.metadata,
    Column("article_id", Integer, ForeignKey("articles.id"), primary_key=True),
    Column("category_id", Integer, ForeignKey("categories.id"), primary_key=True),
)

class Author(Base):
    __tablename__ = "authors"
    
    id = Column(Integer, primary_key=True)
    full_name = Column(String(120), nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    articles = relationship("Article", back_populates="writer", cascade="all, delete-orphan")

class Category(Base):
    __tablename__ = "categories"
    
    id = Column(Integer, primary_key=True)
    label = Column(String(60), unique=True, index=True)
    
    articles = relationship(
        "Article",
        secondary=article_category,
        back_populates="categories"
    )

class Article(Base):
    __tablename__ = "articles"
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    body = Column(String(2000))
    published = Column(Boolean, default=False)
    written_at = Column(DateTime, default=datetime.utcnow)
    
    author_id = Column(Integer, ForeignKey("authors.id"))
    writer = relationship("Author", back_populates="articles")
    
    categories = relationship(
        "Category",
        secondary=article_category,
        back_populates="articles"
    )

5. Sinh lược đồ cơ sở dữ liệu

# Chạy một lần để tạo bảng
Base.metadata.create_all(engine)

# (Tùy chọn) Xóa toàn bộ bảng — chỉ dùng trong môi trường dev
# Base.metadata.drop_all(engine)

6. Thao tác CRUD cơ bản

Thêm bản ghi:

author = Author(full_name="Nguyễn Văn A", is_active=True)
db_session.add(author)
db_session.flush()  # Đảm bảo có ID trước khi gán cho article

article = Article(
    title="Giới thiệu SQLAlchemy",
    body="ORM mạnh mẽ cho Python...",
    writer=author,
    published=True
)
article.categories.append(Category(label="Lập trình"))
db_session.add(article)
db_session.commit()

Truy vấn đơn giản:

# Lấy tất cả bài viết chưa xuất bản
drafts = db_session.query(Article).filter_by(published=False).all()

# Lấy tác giả cùng số bài viết của họ
from sqlalchemy import func
stats = db_session.query(
    Author.full_name,
    func.count(Article.id).label("article_count")
).join(Article).group_by(Author.id).all()

Cập nhật và xóa:

# Cập nhật hàng loạt
db_session.query(Article).filter(Article.title.contains("Python")).update(
    {"published": True},
    synchronize_session=False
)

# Xóa theo điều kiện
db_session.query(Author).filter(Author.is_active == False).delete(
    synchronize_session=False
)
db_session.commit()

7. Truy vấn nâng cao

Join và subquery:

from sqlalchemy import exists

# Tìm tác giả có ít nhất một bài đã xuất bản
subq = db_session.query(Article.author_id).filter(Article.published == True).subquery()
active_authors = db_session.query(Author).filter(Author.id.in_(subq)).all()

# Join trái để lấy cả tác giả không có bài viết
result = db_session.query(Author, Article.title).outerjoin(Article).all()

Sử dụng hàm tổng hợp và phân nhóm:

from sqlalchemy import func

report = db_session.query(
    Category.label,
    func.count(Article.id).label("total_articles"),
    func.avg(func.length(Article.body)).label("avg_body_length")
).join(article_category).join(Article).group_by(Category.id).all()

8. Quản lý giao dịch một cách an toàn

from contextlib import contextmanager

@contextmanager
def transaction_scope():
    session = Session()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

# Sử dụng
with transaction_scope() as tx:
    a1 = Author(full_name="Tác giả mới")
    tx.add(a1)
    tx.flush()  # Đảm bảo ID được cấp
    
    art = Article(title="Bài viết thử nghiệm", writer=a1)
    tx.add(art)
    # Không gọi commit — sẽ tự động commit nếu không có ngoại lệ

9. Một số lưu ý thực tiễn

  • Dùng session.expire_on_commit=False nếu cần truy cập thuộc tính sau khi commit.
  • Tránh N+1 bằng cách dùng joinedload() hoặc selectinload() khi truy vấn quan hệ.
  • Kích hoạt pool_pre_ping=True trong create_engine() để kiểm tra kết nối trước mỗi query.
  • Với ứng dụng web, nên gắn Session vào request lifecycle — ví dụ: dùng dependency trong FastAPI hoặc middleware trong Flask.

Thẻ: sqlalchemy orm python Database PostgreSQL

Đăng vào ngày 14 tháng 6 lúc 04:20