RaySystem Vol.007:从 Peewee 到 SQLModel、aiosqlite 异步数据库
Peewee 是我很喜欢的一个 Python ORM,但是它不支持异步,与已经是异步架构的 RaySystem 不匹配了。因此,在本文中我更换为 SQLModel 这个基于 SQLAlchemy 的 ORM,并改用 aiosqlite 这个 SQLite 的异步封装库。
Peewee 介绍
Peewee 是我很喜欢的一个 Python ORM,特点是简单且小巧,API 设计得非常直观、好用。
Peewee 的功能是非常强大的,美中不足之处,就是他对异步的支持不佳。按照 FastAPI 文档的说法:
❝Peewee 并没有为异步框架设计,也没有考虑到它们。
如果你正在使用较旧的非异步框架开发应用程序,并且可以处理其所有默认设置,那么它可能是一个很好的工具。
但如果你需要更改一些默认设置,支持多个预定义数据库,与异步框架(如 FastAPI)合作等,你将需要添加相当复杂的额外代码以覆盖这些默认设置。
❞
文档给出了让 Peewee 支持异步的方法,但非常复杂、冗长。由于 RaySystem 是基于异步设计的,因此只好换掉 Peewee。
SQLModel 介绍
SQLModel 也是一个 Python ORM 库,由 FastAPI 的作者开发,其底层基于 Pydantic 和 SQLAlchemy,特性包括:
直观易写:优秀的编辑器支持。处处自动补全。减少调试时间。设计易于使用和学习。减少阅读文档时间。 易于使用:它具有合理的默认设置,并在后台执行大量工作以简化您编写的代码。 兼容性:它设计为与 FastAPI、Pydantic 和 SQLAlchemy 兼容。 可扩展:您拥有 SQLAlchemy 和 Pydantic 的所有功能。 简短:减少代码重复。单一类型注解能完成大量工作。无需在 SQLAlchemy 和 Pydantic 中重复模型。
从介绍中,我感觉到这个库十分优秀:
编辑器补全:光这一点就很吸引我了,因为 Python 开发代码补全是个很恼人的问题。 底层基于 SQLAlchemy:SQLAlchemy 是一个十分强大的 ORM,许多商业化项目也基于它,因此可靠性和功能上有保障 减少重复代码:写 API 时,Model 不需要重复编写多套,这可太方便了 异步支持:能与 FastAPI 共同工作,说明它「异步支持较好,这正是我所需要的」
安装依赖:uv add sqlmodel
为了添加异步支持,还需要安装 aiosqlite:uv add aiosqlite
单一全局数据库
RaySystem 未来会有多个模块需要使用数据库,我选择使用 SQLite,因此这些数据库都是我们数据存储目录下的文件。
我在思考一个问题:我该使用一个集中的数据库文件呢,还是允许每个模块使用自己的文件?
经过考虑之后,我打算使用一个集中的数据库文件。也就是说,全局只有一个 SQLite 数据库,每个模块声明的 Model 都是数据库下的表。
为什么要这么做呢?我的原因如下:
首先是代码实现简单,只需要维护一套数据库连接
模块之间数据共享方便,比如,我可以在新的 Model 中建立与老的 Model 之间的关系。这在未来的设计中有很大的价值。
数据库模块
数据库模块位于 module/db/db.py
,数据库创建代码如下:
from pathlib import Path
from typing import AsyncGenerator, Union
from sqlmodel import SQLModel
from module.base.constants import DB_MODULE_NAME
from module.fs.fs import fs_get_module_data_path, fs_make_sure_module_data_path_exists
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine, async_sessionmaker
DB_ENGINE: Union[AsyncEngine, None] = None
def db_get_db_path() -> Path:
return fs_get_module_data_path(DB_MODULE_NAME) / "db.sqlite3"
async def init_db():
global DB_ENGINE
# Create the data directory if it doesn't exist
fs_make_sure_module_data_path_exists(DB_MODULE_NAME)
DB_ENGINE = create_async_engine(f"sqlite+aiosqlite:///{db_get_db_path()}")
async with DB_ENGINE.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
print("DB module initialized")
在这段代码中:
DB_ENGINE
的类型是AsyncEngine
,这是 sqlalchemy 提供的异步数据库能力。db_get_db_path
方法用于获取数据库文件,数据路径由文件系统模块(fs)的fs_get_module_data_path
方法给出。init_db
为数据库初始化方法。最核心的是调用 sqlalchemy 提供的create_async_engine
来创建异步数据库。其中协议指定的是sqlite+aiosqlite
,这会在底层自动调用 aiosqlite。以异步方式获取数据库链接,调用
SQLModel.metadata.create_all
,该方法能自动创建好相关 ORM 数据表。
至此数据库创建完成了。
异步 Session
在 SQLModel 和 sqlalchemy 中,需要先创建 Session,再操作数据,异步 Session 创建方法如下:
def db_async_session() -> AsyncSession:
return async_sessionmaker(DB_ENGINE, class_=AsyncSession, expire_on_commit=False)()
这种写法的好处:
自动管理 session 生命周期 防止资源泄露 支持异常处理 便于未来 FastAPI 依赖注入
对于 expire_on_commit=False
,它是 SQLAlchemy session 的一个配置参数。True 和 False 的行为对比:
默认行为 (expire_on_commit=True):
当 session.commit() 执行后,所有对象会被标记为"过期" 下次访问对象属性时会自动从数据库重新加载 这确保了数据的一致性,但可能导致额外的数据库查询
设置 expire_on_commit=False 的效果:
commit 后对象不会被标记为过期 可以继续使用已加载的对象,无需重新查询 性能更好,但需要自己确保数据一致性
「注意,这里说的缓存,指的是 Session 内。」
修改 Info 模块的数据表
在 Info 模块中包含几个用 Peewee 创建的数据表,全部改用 SQLModel 的写法重新实现:
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel, Relationship
class Site(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
url: str
favicon: Optional[str] = None
infos: list["Info"] = Relationship(back_populates="site")
class Info(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
url: str
published: Optional[datetime] = None
created_at: datetime = Field(default_factory=datetime.now)
description: Optional[str] = None
image: Optional[bytes] = None
site_id: Optional[int] = Field(default=None, foreign_key="site.id")
is_new: bool = Field(default=True)
is_mark: bool = Field(default=False)
site: Optional["Site"] = Relationship(back_populates="infos")
其中:
table=True
是 SQLModel 中的一个参数,用于指示该类应该被映射到数据库表中。从写法可以看出,Model 是带有 Python 类型声明的,这是我最喜欢 SQLModel 的原因,未来在使用的时候,相当方便。
对于关系,
infos
、site_id
、site
,这是 sqlmodel 中的固定写法。尤其是site_id
、site
,表面上看是声明了两次,实际上在底层是一个东西,开发者根据业务场景,能获取到 id 的时候就用 id,能获取到对象的时候就用对象。
运行程序,可以看到生成的新数据表:
创建第一个数据!
使用下面代码来添加数据:
async with db_async_session() as session:
site = Site(name="新浪微博", url="https://weibo.com")
session.add(site)
await session.commit()
print("Site added")
数据添加成功:
总结
「进展比想象中要顺利」:对于这次切换数据库,因为面对的都是未知的技术,我是有畏难思想的。但是实际进展比预想中要顺利。可见,很多事情如同小马过河,光害怕没有用,得付诸行动。
「深入学习 SQLModel 和 sqlalchemy」:现在对于 ORM 好多特性还不熟悉,需要继续深入学习。
「下一步做什么?」 现在各种异步、底层能力都就位了,我想的是下一步开发 Feed 服务,第一步是支持 RSS Feed 订阅功能。在 RaySystem 中,Feed 与传统的订阅不同,是结合了我的个人思考。比如,我会按照领域建模,先创建 Site、Channel、User 实体,然后每个实体支持添加到 Feed 的关系。这样,Feed 只是 Site 的数据源,当一个 Feed 失效后,只需要换 Feed 地址。在浏览资讯的时候,是按照 Site 下的 Info 进行浏览。
AI 点评
批判性思考与深入洞见
以下是对核心观点的深入分析和思考:
「1. Peewee 的异步问题及替换为 SQLModel」
批判性分析:
「优点分析:」
作者正确识别了 Peewee 不支持异步的问题,并结合实际需求(RaySystem 的异步架构)选择了更适合的工具。这种基于技术需求选择工具的方法是值得肯定的。 SQLModel 结合了 Pydantic 和 SQLAlchemy 的优势,确实是现代 Python 应用的优秀选择,特别是在异步支持和开发效率上。 「潜在问题:」
替换 ORM 的成本分析不足。迁移 Peewee 到 SQLModel 涉及了重写所有数据模型和相关代码,虽然文章提到替换过程“比想象中顺利”,但实际迁移的细节没有讨论,例如可能的性能影响、复杂的查询需求是否会因 ORM 的变化而受到限制。 对 Peewee 的异步支持存在一定的误解。虽然 Peewee 本身不直接支持异步,但有社区支持的扩展(如 peewee-async
),其复杂性未必高于完整迁移到 SQLModel。
深层洞见:
「工具选型需要综合考虑」:选择技术栈不仅需要考虑当下的功能匹配,还需评估未来维护成本、社区支持和团队熟悉度。如果团队对 SQLAlchemy 的熟悉度较低,可能会导致未来开发效率下降。 「多工具组合可能更灵活」:如果仅仅是为了满足异步需求,或许可以探索 Peewee 的异步扩展,而不是直接迁移到全新的 ORM。
「2. 使用单一全局数据库设计」
批判性分析:
「优点分析:」
单一数据库文件简化了代码实现,减少了多文件管理的复杂度。 数据共享方便,这一点尤其重要,跨模块的数据关联需求可以通过共享数据库更高效地实现。 「潜在问题:」
「性能瓶颈」:SQLite 在多并发写操作下可能会出现性能问题。RaySystem 作为一个异步架构,未来可能需要高频的读写操作,集中在一个数据库文件可能导致性能下降。 「模块耦合风险」:如果所有模块都共享一个数据库文件,那么模块之间的强耦合性可能增加,未来模块独立性需求会受到限制。 「数据库文件的可靠性」:单文件设计可能导致单点故障。如果数据库文件损坏,整个系统可能无法正常运行。
深层洞见:
「设计应考虑未来扩展性」:如果未来需要支持更高的并发,可能需要探索分布式数据库或多文件设计,例如每个模块独立存储数据,并通过 API 或查询聚合实现跨模块数据关联。 「SQLite 的局限性需提前预警」:SQLite 更适合轻量级应用。如果系统规模和并发需求增长,需要尽早评估迁移到 PostgreSQL 或其他分布式数据库的可行性。
「3. 使用 SQLModel 和异步 Session」
批判性分析:
「优点分析:」
SQLModel 的类型注解和 Pydantic 的结合极大提升了开发效率,特别是在需要高频定义和验证数据模型的场景中。 异步 Session 的封装代码符合现代 Python 编程的最佳实践,尤其是自动管理生命周期的设计,有助于减少开发错误。 「潜在问题:」
「SQLModel 的局限性」:尽管 SQLModel 易用性较高,但其核心仍是 SQLAlchemy。如果需要高级功能,可能需要直接操作 SQLAlchemy 的底层,增加学习曲线。 「事务处理问题」:异步操作下事务管理尤为重要,但文章中未提及如何保证事务一致性(例如如何处理嵌套事务或回滚策略)。
深层洞见:
「学习深度决定工具价值」:SQLModel 适合快速上手,但如果系统复杂性提高,开发者需要对 SQLAlchemy 的底层机制有深入理解,以避免被工具的抽象所限制。 「自动化工具优化」:可以引入更高层次的自动化封装,例如通过装饰器或中间件简化异步事务管理。
「4. Feed 服务的设计理念」
批判性分析:
「优点分析:」
作者提到通过领域建模实现订阅服务的创新性设计,这是一个非常有潜力的方向。将 Feed 数据作为 Site 的一种数据源,并通过实体关联实现扩展,显示了对系统架构的深思熟虑。 与传统订阅模式的区别在于,作者将 Feed 和 Site 的关系设计为可替换的动态结构,体现了对模块化和扩展性的关注。 「潜在问题:」
「复杂性控制」:领域建模的设计非常灵活,但也可能导致数据结构和业务逻辑的复杂性大幅增加。如果设计过于复杂,后期维护成本可能大幅上升。 「性能问题」:动态关系的管理可能会引入性能瓶颈,特别是在处理大规模 Feed 数据时。
深层洞见:
「设计需平衡灵活性与复杂性」:领域建模在初期设计时需要明确核心业务逻辑,避免为追求灵活性而引入不必要的复杂性。 「数据建模的可扩展性」:建议引入缓存机制或预计算策略,减少动态关系查询对性能的影响。
「5. 总体改进建议」
「多技术路线评估」:在技术选型时,建议对 Peewee 的异步扩展方案进行深入测试,而不仅仅因为初看复杂性较高就放弃。更全面的评估可以帮助减少大规模迁移成本。 「模块化数据库设计」:虽然单文件设计简化了开发,但在异步系统中,引入多文件或分布式存储方案可能更具长期价值。 「事务和并发管理」:需要明确 RaySystem 的事务处理机制,特别是在并发写入场景下,如何保证数据一致性和完整性。 「关注未来扩展性」:SQLModel 的当前功能能满足需求,但需提前规划如何应对高复杂度场景,如自定义 SQL 查询或跨数据库操作。
总之,当前设计整体思路清晰、技术选型合理,但需要进一步评估性能、扩展性和长期维护成本,以保证系统能够支持未来的复杂需求。