"""SQLAlchemy engine + Session 工厂。 FastAPI 路由通过 `Depends(get_db)` 拿到一个 request-scope 的 Session, 请求结束自动 close。 """ from __future__ import annotations import os from collections.abc import Iterator from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker from app.core.config import settings def _ensure_sqlite_dir(url: str) -> None: """SQLite 文件路径如果在子目录,首启时自动 mkdir,避免 sqlalchemy 报 unable to open。""" prefix = "sqlite:///" if not url.startswith(prefix): return path = url[len(prefix):] dirname = os.path.dirname(path) if dirname: os.makedirs(dirname, exist_ok=True) _ensure_sqlite_dir(settings.DATABASE_URL) _is_sqlite = settings.DATABASE_URL.startswith("sqlite") # SQLite 跨线程访问要 check_same_thread=False;PG/MySQL 不需要这个参数 _connect_args: dict = {} if _is_sqlite: _connect_args["check_same_thread"] = False _engine_kwargs: dict = { "connect_args": _connect_args, # echo 在 dev 下打 SQL,生产关掉 "echo": settings.APP_DEBUG and not settings.is_prod, "future": True, "pool_pre_ping": True, } # SQLite 用单文件不需要池;PG/MySQL 必须显式池化 + recycle 防 idle 断连 if not _is_sqlite: _engine_kwargs.update(pool_size=10, max_overflow=20, pool_recycle=3600) engine = create_engine(settings.DATABASE_URL, **_engine_kwargs) SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False) def get_db() -> Iterator[Session]: """FastAPI 依赖。请求开始建 session,请求结束自动 close。 出异常时 SQLAlchemy 默认会标记 transaction 为 invalid,close 时自动 rollback。 """ db = SessionLocal() try: yield db finally: db.close()