database.py 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
  1. """请求和后台各自拥有会话;短写事务避免模型等待期间占用 SQLite 写锁。"""
  2. import asyncio
  3. from contextlib import asynccontextmanager
  4. from sqlalchemy import event, text
  5. from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
  6. from app.shared.config import settings
  7. from app.domain.models import Base
  8. settings.data_path.mkdir(parents=True, exist_ok=True)
  9. engine = create_async_engine(settings.db_url, connect_args={"timeout": 30})
  10. Session = async_sessionmaker(engine, expire_on_commit=False)
  11. write_lock = asyncio.Lock()
  12. @event.listens_for(engine.sync_engine, "connect")
  13. def configure_sqlite(connection, record):
  14. """外键和 WAL 不依赖客户端设置,否则不同启动路径会产生不一致数据。"""
  15. cursor = connection.cursor()
  16. cursor.execute("PRAGMA foreign_keys=ON")
  17. cursor.execute("PRAGMA journal_mode=WAL")
  18. cursor.execute("PRAGMA busy_timeout=30000")
  19. cursor.close()
  20. @asynccontextmanager
  21. async def transaction():
  22. """先领取写锁再读取要修改的数据,避免 deferred 事务读后升级引发快照冲突。"""
  23. async with write_lock:
  24. async with Session() as session:
  25. await session.execute(text("BEGIN IMMEDIATE"))
  26. try:
  27. yield session
  28. await session.commit()
  29. except Exception:
  30. await session.rollback()
  31. raise
  32. async def initialize():
  33. """首次建表并标记初始迁移;新版本变更应通过 Alembic,不能靠 create_all 改旧表。"""
  34. async with engine.begin() as connection:
  35. await connection.run_sync(Base.metadata.create_all)
  36. await connection.execute(text("CREATE TABLE IF NOT EXISTS alembic_version (version_num VARCHAR(32) NOT NULL PRIMARY KEY)"))
  37. await connection.execute(text("INSERT OR IGNORE INTO alembic_version(version_num) VALUES ('0001_initial')"))
  38. from app.application.seed import seed
  39. async with transaction() as session:
  40. await seed(session)