从开始用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 Base=declarative_base()classUser(Base):__tablename__='users'id=Column(Integer,primary_key=True)name=Column(String(50))age=Column(Integer)看起来和Django的models挺像,但SQLAlchemy的Column类型更细,比如String明确给长度,Integer不带默认主键,得手动设primary_key=True。然后创建引擎:
engine=create_engine('sqlite:///demo.db')Base.metadata.create_all(engine)接下来通过Session来操作:
fromsqlalchemy.ormimportSessionwithSession(engine)assession:new_user=User(name='张三',age=28)session.add(new_user)session.commit()# 查询result=session.query(User).filter(User.age>20).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属性时才去数据库查,这在循环里会产生N+1查询问题。解决方案是用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:pass@localhost/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(),nullable=True))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 和 ORM,Django 的迁移就够用了。但如果你用 SQLAlchemy,或者项目比较复杂,Alembic 的选择性更多。
另一个值得一提的工具是 Flyway,这是个 Java 生态的工具,但也能用在 Python 项目里。Flyway 的思路是纯 SQL 脚本,它不像 Alembic 那样能自动生成,所有迁移都得手写 SQL。这有好有坏:好的是你对数据库操作有完全控制,坏的是工作量大,而且每换一个数据库类型(比如从 PostgreSQL 换到 MySQL),SQL 写法就得改。Alembic 因为它底层是 SQLAlchemy,所以生成的 SQL 会适配不同的数据库。
还有 Yoyo Migrations,一个更轻量级的 Python 迁移工具。它不像 Alembic 那样和 SQLAlchemy 耦合那么紧,可以用在任何数据库操作上。但正因为如此,它没有自动生成功能,所有迁移都得手写。如果项目里已经用了 SQLAlchemy,用 Alembic 会更自然。
总的来说,如果你用 SQLAlchemy,Alembic 是首选。它和 SQLAlchemy 配合得天衣无缝,自动生成能省很多事,手动编写又给了你足够的控制权。如果你的项目不用 ORM,或者想完全控制 SQL,Flyway 或者 Yoyo 可能更适合。Django 的用户就不用纠结了,直接用自带的就行。容性也有过问题,但经过这么多年迭代,现在很稳定。
最后说一点个人感受。SQLAlchemy的设计哲学是“给你控制权,而不是给你糖果”。上手那段学习曲线其实是在学数据库本身该有的思维,而不是学一个框架的花哨API。把SQLAlchemy用熟了,写出来的代码既保留了SQL的表达能力,又有了Python的优雅,算是Python世界里少数可以“既要又要”的东西。