From 1b7d46c805a90ec24f01147086691bfe53ff682d Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Mon, 3 Aug 2026 14:33:11 +0800 Subject: [PATCH] =?UTF-8?q?modify:=20=E5=BB=BA=E8=A1=A8=E5=86=B2=E7=AA=81w?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 89 ++++++++++++++++++++++++++++++ src/chongming_database/__init__.py | 26 ++++++++- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e69de29..a732071 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,89 @@ +# chongming-database + +异步 SQLite 数据库封装,基于 `aiosqlite` + `SQLModel`(SQLAlchemy),提供简洁的初始化与会话管理接口。 + +--- + +## 安装 + +```bash +uv pip install -e utils/database +``` + +--- + +## API + +### `Database` 类 + +```python +from pathlib import Path +from chongming_database import Database + +# 创建实例 +db = Database(Path("data/storage/chongming/app.db")) + +# 初始化(创建引擎 + 自动建表) +await db.initialize() + +# 获取异步会话 +async with db.get_session() as session: + # 使用 SQLModel / SQLAlchemy 进行查询 + result = await session.execute(...) + ... + +# 关闭连接 +await db.dispose() +``` + +### 方法说明 + +| 方法 | 签名 | 说明 | +| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `__init__` | `(db_path: Path)` | 传入 SQLite 文件路径,自动创建父目录 | +| `initialize` | `async () -> None` | 创建异步 SQLAlchemy 引擎(`sqlite+aiosqlite:///`),执行 `SQLModel.metadata.create_all` 自动建表,构建 `sessionmaker`(`expire_on_commit=False`) | +| `get_session` | `() -> AsyncSession` | 返回一个新的异步会话。如果 `initialize()` 未调用则抛出 `RuntimeError` | +| `dispose` | `async () -> None` | 关闭异步引擎并清除内部状态 | + +--- + +## 使用示例 + +### 配合 SQLModel 定义表 + +```python +from sqlmodel import SQLModel, Field +from datetime import datetime, timezone + +class User(SQLModel, table=True): + id: str = Field(primary_key=True) + username: str = Field(max_length=50, unique=True, index=True) + created_at: datetime = Field( + default_factory=lambda: datetime.now(timezone.utc) + ) +``` + +### 完整生命周期 + +```python +from chongming_database import Database +from pathlib import Path + +db = Database(Path("./data/myapp.db")) +await db.initialize() + +try: + async with db.get_session() as session: + user = User(id="u1", username="john") + session.add(user) + await session.commit() +finally: + await db.dispose() +``` + +--- + +## 依赖 + +- `aiosqlite` — 异步 SQLite 驱动 +- `sqlmodel` — SQLAlchemy + Pydantic 融合 ORM diff --git a/src/chongming_database/__init__.py b/src/chongming_database/__init__.py index 43f9bf1..ebea8d3 100644 --- a/src/chongming_database/__init__.py +++ b/src/chongming_database/__init__.py @@ -1,6 +1,7 @@ from pathlib import Path from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession from sqlalchemy.orm import sessionmaker +from sqlalchemy.exc import OperationalError from sqlmodel import SQLModel class Database: @@ -11,14 +12,33 @@ class Database: self._db_path.parent.mkdir(parents=True, exist_ok=True) async def initialize(self): - """异步初始化,创建引擎并建立表""" + """异步初始化,创建引擎并建立表 + + 注意:DDL(CREATE TABLE)在 SQLite 中会自动提交,无需包裹在事务中。 + 使用 connect() 而非 begin() 可以避免事务失败状态导致的连锁异常。 + + checkfirst=True 会在建表前检查表是否已存在(通过 sqlite_master), + 大幅减少多进程并发启动时的 "table already exists" 冲突。 + 即使 checkfirst 与实际 CREATE 之间存在竞态窗口,后续的 try/except + 也会兜底捕获漏网的 OperationalError。 + """ self._engine = create_async_engine( f"sqlite+aiosqlite:///{self._db_path}", echo=False, future=True, ) - async with self._engine.begin() as conn: - await conn.run_sync(SQLModel.metadata.create_all) + async with self._engine.connect() as conn: + try: + await conn.run_sync(SQLModel.metadata.create_all, checkfirst=True) + except OperationalError as e: + # 多进程并发启动时,checkfirst 与实际 CREATE 之间仍有极小的竞态窗口: + # 多个进程同时判断"表不存在"并执行 CREATE TABLE, + # 后到者会报 "table ... already exists"。此时表已由其他进程建好,忽略即可。 + # + # 注意:使用 connect() 而非 begin(),DDL 在 SQLite 中自动提交, + # 无需手动 commit/rollback,避免事务失败状态的连锁异常。 + if "already exists" not in str(e).lower(): + raise self._session_maker = sessionmaker( self._engine, # type: ignore class_=AsyncSession,