从开始用Python写点正经的业务代码那天起只要涉及到数据库很大概率会碰上SQLAlchemy。这东西一开始接触会觉得有点“重”不就是操作个数据库嘛搞那么复杂。但真在项目里待个一年半载天天跟表结构和查询打交道慢慢就品出它的好处来。先说他是什么。简单讲SQLAlchemy是Python生态里一个相当成熟的数据库工具包核心是两样东西一个叫Core另一个叫ORM。Core接近底层的SQL抽象层写出来的东西看着还是SQL的样子但换数据库不用改代码。ORM则是把数据库里的表映射成Python的类一行数据就是一个对象查出来的结果是列表每个元素是对象实例。很多人以为SQLAlchemy就等于ORM其实不是Core才是它立身的根本。那些把ORM玩得很溜却对底层SQL执行计划一窍不通的人往往在生产环境踩过不少坑。能做什么日常的增删改查不说SQLAlchemy最省心的地方是连接池管理和事务控制。一个Web应用每天成百上千次数据库操作每次手动创建连接再销毁性能上很亏。SQLAlchemy替你管着连接池用完的放回去下次拿来复用。事务这块也处理得干净session.commit()和session.rollback()异常一抛自动回滚不用到处写try except。再有就是它支持几乎所有主流数据库——PostgreSQL、MySQL、SQLite、Oracle、SQL Server。业务初期用SQLite做原型测试跑通了直接改个连接串切到PostgreSQL这种感觉就像穿了双万能鞋什么地都能走。还有一点是它允许用原生的text()写复杂SQL碰到ORM表达起来别扭的查询随时可以降级到纯SQL这种“想亲手写就亲手写”的空间很重要。怎么用刚入门的人最容易犯的错是把SQLAlchemy当成完全自动化的ORM来用。正经的方式是先用Core把表结构定义清楚再在Core之上搭ORM。举个例子先这样定义一张用户表fromsqlalchemyimportcreate_engine,MetaData,Table,Column,Integer,Stringfromsqlalchemy.ormimportdeclarative_base Basedeclarative_base()classUser(Base):__tablename__usersidColumn(Integer,primary_keyTrue)nameColumn(String(50))ageColumn(Integer)看起来和Django的models挺像但SQLAlchemy的Column类型更细比如String明确给长度Integer不带默认主键得手动设primary_keyTrue。然后创建引擎enginecreate_engine(sqlite:///demo.db)Base.metadata.create_all(engine)接下来通过Session来操作fromsqlalchemy.ormimportSessionwithSession(engine)assession:new_userUser(name张三,age28)session.add(new_user)session.commit()# 查询resultsession.query(User).filter(User.age20).all()foruserinresult:print(user.name)这一段看着简单但实际用它的时候有一个地方很容易忽略——Session的生命周期。不少新手在函数开头创建Session函数结束了也不关闭连接一直在那占着。正确的做法是用上下文管理器with块一结束自动关闭。甚至更专业的做法是用scoped_session在Web应用里绑定到每个请求线程请求结束自动清理。最佳实践这方面值得说的东西不少。一是尽量不用ORM做复杂的多表关联查询。ORM写出来的join可能性能很差因为中间会生成很多次查询。比如User和Order一对多要查用户和订单数量用ORM写联结后查出来的可能是全部字段再在应用层聚合效率很低。这时果断用text()写原生SQL或者用Core的select()拼出精确的字段列表。二是合理使用lazy loading和eager loading。默认关系是lazy只有你访问relation属性时才去数据库查这在循环里会产生N1查询问题。解决方案是用joinedload或者subqueryload一次查询把关联数据带回来用不用全凭场景判断。三是管理好数据库迁移。SQLAlchemy本身不做自动迁移但社区有Alembic专门做版本控制每次改表结构都得生成迁移脚本、跑一遍升级这样团队的数据库结构才可控。四是别把业务逻辑写在ORM模型里。模型定义好字段和关系就够了复杂的业务规则放到service层去否则模型变成大杂烩维护起来头疼。跟同类技术比最容易拿出来对比的是Django ORM。Django ORM的好处是跟框架深度绑定写起来快一个models.py下来model、form、admin全自动生成。但它的自动迁移有时暗藏风险遇到大批量数据时迁移速度慢得离谱甚至改字段名会删重新创建列而不是rename。SQLAlchemy则更“尊重”SQL本身你写的查询是什么实际执行的也就是什么没有太多魔法。另外还有一个叫Peewee的轻量级ORM小项目用它感觉很清爽设计上更简洁但多表关联、复杂查询和连接池管理上比SQLAlchemy差了一截。像一些数据仓库或者有CTE、窗口函数的场景Peewee基本用不了SQLAlchemy的Core层可以比较自然地表达这些。至于SQLAlchemy本身也不是没缺点学习曲线确实比Django ORM陡峭文档写得略微晦涩早期版本不同方言的兼# # Python Alembic 详解他是什么Alembic 本质上是一个数据库迁移工具专门为 SQLAlchemy 设计。如果你用过 SQLAlchemy 来操作数据库你会发现一个很麻烦的问题当你的模型Model发生变化时比如新增了一个字段或者修改了某个字段的类型你都得手动去修改数据库表结构。更糟糕的是如果项目已经线上运行数据库里存着用户数据你总不能把表删了重建吧Alembic 就是来解决这个问题的。打个比方数据库表就像你的衣柜模型就是衣柜的设计图。刚开始你设计了一个衣柜有挂衣服的地方有放袜子的抽屉。后来你发现还需要一个放领带的格子于是修改了设计图。Alembic 就像是一个专业的裁缝他能把现有的衣柜改造成新的设计图而且不破坏里面已经放好的衣服。这就是 Alembic 的核心价值。和 Git 有点像Alembic 会对每次数据库结构的变化做一个快照也就是生成一个迁移脚本。这些脚本记录了从上一个版本到当前版本的所有变化。当团队协作时每个人拉取代码后运行一下迁移数据库结构就统一了。他能做什么Alembic 的功能其实很集中但做得很深。最基础的就是自动检测模型和数据库之间的差异生成迁移脚本。比如你在模型里加了一个字段运行 alembic autogenerate它会自动生成一个包含 ALTER TABLE 语句的迁移文件。当然不是所有变化都能自动检测比如字段重命名在 Alembic 看来就是删除一个字段再加上一个新字段这会导致数据丢失。所以自动生成的脚本需要人工检查。除了自动生成Alembic 也允许你手动编写迁移脚本。这在做复杂的数据迁移时特别有用。比如你需要把某个字段的数据从 JSON 格式改成独立的关联表这种逻辑自动检测是做不到的需要手动写 Python 代码来完成数据的转换。Alembic 还能管理多个数据库版本。你可以回滚到任意历史版本这在测试环境调试时很实用。比如你先升级到了版本 5 发现有问题直接回滚到版本 3 就好了。Alembic 维护了一个叫 alembic_version 的表记录了当前数据库所处的版本所以它知道该往哪个方向迁移。怎么使用使用 Alembic 的第一步是初始化。在你的项目根目录下运行alembic init alembic这会创建一个 alembic 目录里面有个 env.py 文件和一个 versions 目录。env.py 是核心配置你需要在这里告诉 Alembic 你的数据库连接字符串和 SQLAlchemy 的模型元数据。配置大概是这样的在 env.py 里设置target_metadata your_model.Base.metadata然后在alembic.ini里设置sqlalchemy.url postgresql://user:passlocalhost/dbname。注意密码不要硬编码可以用环境变量。接下来就是生成迁移脚本。假设你改了模型运行alembic revision --autogenerate -m add username field。Alembic 会比较当前数据库结构和你模型定义的结构生成一个类似xxxx_add_username_field.py的文件。这个文件里有两个函数upgrade() 和 downgrade()。upgrade 是向前迁移downgrade 是回滚。举个实际的例子假设你给 User 模型加了个 age 字段defupgrade():op.add_column(users,sa.Column(age,sa.Integer(),nullableTrue))defdowngrade():op.drop_column(users,age)生成脚本后一定要检查特别是自动生成的。有时候 Alembic 会产生一些不必要的操作比如把 nullable 从 False 改为 True 时如果你的数据库里已经有数据它可能会报错。检查完后运行alembic upgrade head来执行迁移。head 指的是最新版本。如果需要回滚运行alembic downgrade -1回退一个版本或者alembic downgrade xxxx回退到指定版本。最佳实践用了几年 Alembic有些经验值得分享。首先是版本控制的粒度。很多人喜欢一次改了好多模型然后生成一个巨大的迁移脚本。这其实不太好因为如果出了问题不好排查。我的习惯是一次改动只涉及一个模型的少数字段然后生成一个迁移。这样每个脚本的功能单一回滚也方便。其次是永远不要修改已经提交过的迁移脚本。这就像 git 里不要修改已经 push 的 commit 一样。如果迁移脚本已经合并到主分支其他人已经运行过你再修改它会导致版本混乱。正确的做法是创建一个新的迁移脚本来修复问题。还有一个容易被忽略的点是数据迁移。当你需要改字段类型时要考虑已有数据的处理。比如把字符串字段改成整数类型如果数据库里已经有 “abc” 这样的数据迁移肯定会失败。我一般会在迁移脚本里先处理数据再改字段类型。比如写个循环把不合法的数据改成默认值或者报错。对了版本命名要规范。我习惯用项目名_改动描述的格式比如user_add_age_field。这样看 versions 目录时能快速知道每个脚本是干嘛的不用一个个点开看。另外生产环境执行迁移时一定要备份数据库。虽然 Alembic 有回滚功能但有些操作是不可逆的比如删除了表。我见过有人在生产环境误操作把整个 users 表删了虽然用 downgrade 可以重建表但数据已经没了。和同类技术对比说到数据库迁移工具Django 的迁移系统是最直接的竞争对手。Django 的迁移完全自动化你几乎不用写任何代码运行python manage.py makemigrations和python manage.py migrate就搞定了。相比之下Alembic 需要更多手动操作但这也意味着更灵活。如果你用 Django 和 ORMDjango 的迁移就够用了。但如果你用 SQLAlchemy或者项目比较复杂Alembic 的选择性更多。另一个值得一提的工具是 Flyway这是个 Java 生态的工具但也能用在 Python 项目里。Flyway 的思路是纯 SQL 脚本它不像 Alembic 那样能自动生成所有迁移都得手写 SQL。这有好有坏好的是你对数据库操作有完全控制坏的是工作量大而且每换一个数据库类型比如从 PostgreSQL 换到 MySQLSQL 写法就得改。Alembic 因为它底层是 SQLAlchemy所以生成的 SQL 会适配不同的数据库。还有 Yoyo Migrations一个更轻量级的 Python 迁移工具。它不像 Alembic 那样和 SQLAlchemy 耦合那么紧可以用在任何数据库操作上。但正因# # Python Peewee一个轻量级ORM工具的真实使用体验1. Peewee是什么Peewee是Python生态里一个有意思的ORM框架。跟Django ORM这种重型武器不同它更像一把瑞士军刀——小巧、实用没有那么多花哨的配件但足以解决大多数日常数据库操作。我第一次接触Peewee是在一个凌晨三点还在改bug的深夜。当时项目用着SQLAlchemy那配置文件和session管理真是让人头大。有人推荐了Peewee说它“没那么多架子”。试了之后确实如此它是一个纯Python实现的ORM核心代码只有几千行没有那种“为了让框架看起来更酷而加入的不必要特性”。它的设计哲学挺实在的做一件事把它做好。简单查询直接写复杂查询也不遮遮掩掩甚至可以混合使用原生SQL。这种透明感让我觉得它不是个黑盒子。2. Peewee能做什么说起来Peewee能做的事情比很多人想象的多。基本上常规的数据库操作都能覆盖表结构定义和迁移CRUD操作关联查询一对一、一对多、多对多事务管理数据库连接池字段验证信号机制不过它的强项其实在于中小型项目。比如一个内部管理后台、API服务的数据库层、或者Flask框架的配合使用这些都是Peewee拿手的地方。有意思的是Peewee对SQLite支持得特别好。如果你在写桌面应用或者轻量级Web应用直接用SQLite加Peewee连数据库管理软件都省了。我有个小项目就是用的这个组合代码量不到两千行数据库操作的部分写得很清爽。3. 怎么使用先看个最基本的例子。定义一个用户模型importpeewee dbpeewee.SqliteDatabase(my_app.db)classUser(peewee.Model):usernamepeewee.CharField(max_length50,uniqueTrue)emailpeewee.CharField(max_length100)created_atpeewee.DateTimeField(defaultdatetime.datetime.now)classMeta:databasedb创建表直接调方法User.create_table()增删改查的写法很自然# 创建用户userUser.create(username张三,emailzhangsanexample.com)# 查询usersUser.select().where(User.username张三)# 或者用get取单个userUser.get_or_none(User.id1)# 更新user.emailnew_emailexample.comuser.save()# 删除user.delete_instance()关联查询这块Peewee的做法挺直观的。比如用户和订单的一对多关系classOrder(peewee.Model):userpeewee.ForeignKeyField(User,backreforders)amountpeewee.DecimalField(max_digits10,decimal_places2)created_atpeewee.DateTimeField(defaultdatetime.datetime.now)classMeta:databasedb# 查询某个用户的所有订单ordersuser.orders# 预加载关联数据users_with_ordersUser.select().join(Order,on(User.idOrder.user)).prefetch(Order)4. 最佳实践实战中我总结出几个有用的模式连接管理。别在每次请求时都重新创建数据库连接。用连接池或者单例模式来管理。对于Web应用可以考虑用Peewee的_state机制在请求开始时获取连接结束时归还。查询优化。prefetch方法值得关注。它能解决N1查询的问题。比如你要显示用户列表和他们的订单不用prefetch的话每取一个用户就多一次查询性能堪忧。迁移策略。Peewee没有Django那种自带的迁移系统。但可以借助pwiz工具从现有数据库反向生成模型或者手动管理迁移脚本。我习惯在项目里建一个migrations目录每个文件记录DDL变更。日志调试。开启SQL日志能帮你快速定位问题importlogging loggerlogging.getLogger(peewee)logger.addHandler(logging.StreamHandler())logger.setLevel(logging.DEBUG)错误处理。Peewee的异常体系比较完善。PeeweeException是基类下面有DoesNotExist、IntegrityError等。处理数据库操作时建议捕获特定异常而不是统一处理。5. 和同类技术对比说实话选ORM框架有点像选工具的偏好没有绝对的优劣。SQLAlchemy。目前最主流的Python ORM。它强大能处理企业级的复杂场景但代价就是学习曲线陡峭。session、scoped_session、declarative_base这些概念需要时间消化。Peewee的配置简单很多文档也短适合快速上手。Django ORM。Django自带的ORM功能完整且与框架深度集成。如果你的项目用了Django那自然用它的ORM。但如果你只是用Flask或FastAPI硬搬Django ORM就显得臃肿。Peewee可以独立使用不绑定任何框架。PonyORM。它的亮点是用生成器语法写查询看着很酷。但我用过发现问题在于异常信息不够清晰社区活跃度也不如Peewee。生产环境还是保守点好。Tortoise-ORM。这是异步ORM的代表和FastAPI配合得很好。如果你主要做异步Web开发它可能更适合。Peewee的异步支持是通过第三方扩展实现的不如Tortoise原生。说到底选Peewee的主要场景是项目规模不大想要快速搭建不希望在ORM上投入太多学习成本。如果项目复杂度高、查询逻辑特别复杂、需要精细控制SQL生成SQLAlchemy更合适。选择的关键在于对透明度和灵活性的平衡需求。为如此它没有自动生成功能所有迁移都得手写。如果项目里已经用了 SQLAlchemy用 Alembic 会更自然。总的来说如果你用 SQLAlchemyAlembic 是首选。它和 SQLAlchemy 配合得天衣无缝自动生成能省很多事手动编写又给了你足够的控制权。如果你的项目不用 ORM或者想完全控制 SQLFlyway 或者 Yoyo 可能更适合。Django 的用户就不用纠结了直接用自带的就行。容性也有过问题但经过这么多年迭代现在很稳定。最后说一点个人感受。SQLAlchemy的设计哲学是“给你控制权而不是给你糖果”。上手那段学习曲线其实是在学数据库本身该有的思维而不是学一个框架的花哨API。把SQLAlchemy用熟了写出来的代码既保留了SQL的表达能力又有了Python的优雅算是Python世界里少数可以“既要又要”的东西。