zai / app /database.py
bg5's picture
Upload 85 files
e0b68d2 verified
Raw History Blame Contribute Delete
5.32 kB
import os
import ssl as _stdlib_ssl
from typing import AsyncGenerator
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.core.config import settings
def _clean_db_url(url: str) -> tuple[str, dict]:
"""Strip unsupported parameters from a PostgreSQL URL and return (cleaned_url, connect_args).
- asyncpg does not accept `sslmode` as a query-string parameter (that is a
libpq / psycopg2 convention). We translate it into asyncpg's `ssl`
connect-arg instead.
- asyncpg does not accept `channel_binding`. We strip it.
"""
parsed = urlparse(url)
qs = parse_qs(parsed.query)
sslmode = qs.pop("sslmode", [None])[0]
# Remove channel_binding to prevent TypeError in asyncpg connect()
qs.pop("channel_binding", None)
# Rebuild the URL without the stripped parameters
new_query = urlencode({k: v[0] for k, v in qs.items()})
cleaned_url = urlunparse(parsed._replace(query=new_query))
connect_args: dict = {}
if sslmode:
if sslmode == "disable":
connect_args["ssl"] = False
elif sslmode in ("require", "prefer"):
# asyncpg accepts an ssl.SSLContext or the string "require" (>= 0.29)
# Use a permissive SSLContext that skips certificate verification,
# which matches the behavior of libpq's `sslmode=require`.
ctx = _stdlib_ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = _stdlib_ssl.CERT_NONE
connect_args["ssl"] = ctx
elif sslmode in ("verify-ca", "verify-full"):
connect_args["ssl"] = True # uses default SSL context with verification
return cleaned_url, connect_args
def get_db_url(db_url: str = None) -> tuple[str, dict]:
"""获取数据库连接 URL 和 connect_args"""
url = db_url or settings.DATABASE_URL
connect_args: dict = {}
if url:
if url.startswith("postgres://"):
url = url.replace("postgres://", "postgresql+asyncpg://", 1)
elif url.startswith("postgresql://"):
url = url.replace("postgresql://", "postgresql+asyncpg://", 1)
# 如果已经是 postgresql+asyncpg 或者是 sqlite+aiosqlite,则直接使用
elif url.startswith("sqlite"):
if not url.startswith("sqlite+aiosqlite"):
url = url.replace("sqlite://", "sqlite+aiosqlite://", 1)
# Strip unsupported parameters for asyncpg compatibility
if "asyncpg" in url:
url, connect_args = _clean_db_url(url)
return url, connect_args
db_path = settings.DB_PATH
if not os.path.isabs(db_path):
db_path = os.path.abspath(db_path)
return f"sqlite+aiosqlite:///{db_path}", {}
# 1. 声明全局变量为 None,绝不在这里直接实例化
engine = None
async_session = None
def get_session_maker():
"""懒加载:获取真实的 session_maker,如果不存在则在当前进程初始化"""
global engine, async_session
if engine is None:
_db_url, _connect_args = get_db_url()
engine = create_async_engine(
_db_url,
echo=False,
pool_pre_ping=True,
pool_size=5,
max_overflow=10,
pool_recycle=3600,
pool_timeout=30,
connect_args=_connect_args
)
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
return async_session
@asynccontextmanager
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
"""推荐的上下文管理器用法,自动获取 maker 并管理 session"""
maker = get_session_maker()
async with maker() as session:
yield session
async def init_db():
"""初始化数据库表"""
from app.utils.logger import logger, log_exception
# 确保 engine 已被当前 Worker 进程创建
get_session_maker()
global engine
async with engine.begin() as conn:
try:
from alembic.config import Config
from alembic import command
import os
logger.debug("自动数据库迁移: 准备执行 Alembic...")
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
alembic_cfg = Config(os.path.join(base_dir, "alembic.ini"))
alembic_cfg.set_main_option("script_location", os.path.join(base_dir, "migrations"))
def run_upgrade(connection):
alembic_cfg.attributes['connection'] = connection
command.upgrade(alembic_cfg, "head")
await conn.run_sync(run_upgrade)
logger.debug("迁移成功")
except Exception as e:
log_exception(logger, "自动迁移过程中出错")
async def close_db():
"""关闭数据库连接"""
global engine, async_session
if engine is not None:
await engine.dispose()
engine = None # 释放后重置为 None
async_session = None