尧图精选

SQLAlchemy实践指南:从ORM原理到性能优化与异步落地

🕒 发布时间:2026/9/28 9:07:59 📁 来源:尧图网络
如果你写过一段时间的Python大概率遇到过这样的场景代码里散落着一堆sqlite3或pymysql的裸SQL调用字符串拼条件、手动处理连接、查询结果返回的是元组还得自己转成字典。数据量小的时候还能忍一旦业务逻辑复杂起来这种写法的维护成本简直想让人直接摔键盘。SQLAlchemy就是来解决这个问题的——它是Python生态里最成熟的ORM工具让你用类和对象去操作数据库表同时又保留手写SQL的灵活性。这篇文章不会只讲API怎么调我会把实际项目里踩过的坑、总结出的经验一起放出来适合刚接触ORM的初学者也适合已经用了一段时间但想搞懂原理的人。先明确一个概念ORM的全称是Object Relational Mapping对象关系映射。它的核心思想是把数据库表看成类把表中的一行记录看成对象实例把表之间的关联关系看成对象之间的引用关系。你操作一个User对象SQLAlchemy在背后帮你翻译成INSERT、UPDATE、SELECT这类SQL语句。好处是代码可读性高、业务逻辑里不用混入大量SQL字符串、不同数据库之间迁移方便、参数绑定还能降低SQL注入风险。但ORM也不是没有代价一层抽象意味着你有时候不清楚底层到底执行了什么SQL性能调优时需要能“看穿”这层映射。所以我一直认为用ORM的正确姿势是先理解SQL再让ORM帮你省时间。1. 为什么偏偏是SQLAlchemyPython生态里能用的ORM其实不少Django自带ORM、Peewee也很轻量但SQLAlchemy的定位是“全都要”。它既有高层的ORM也有底层的Core表达式语言既能像Django ORM那样声明式地定义模型也能在需要复杂查询的时候手写SQL甚至直接操作表对象。更关键的是它的成熟度和社区规模从Flask-SQLAlchemy到Alembic迁移工具整个生态都是围绕它构建的。如果你只是写一个简单的脚本Peewee可能更轻快如果你在用Django那直接用Django ORM显然更顺手。但如果你需要一个跟框架解耦、能应对复杂业务场景、还能平滑迁移到异步方案的工具SQLAlchemy几乎是绕不过去的选项。另外一个推荐它的理由是它对数据库方言的兼容做得很好。同样的select()写法在PostgreSQL、MySQL、SQLite上都能跑遇到特殊需求还能用方言特定的表达式。比如我用PostgreSQL做JSONB字段查询jsonb_path_query这样的功能SQLAlchemy提供了方言层面的支持。这种“写一套代码多库兼容”的能力在项目从开发库切到生产库的时候特别值钱。还有一点可能被忽略SQLAlchemy的学习曲线虽然比Peewee陡一点但它的底层设计思路和SQL标准贴合得很紧密。你学会用它生成SQL其实也是在加深对SQL本身的理解这比学会某个框架的魔法API有更长久的价值。2. 环境准备与策略选型2.1 安装与版本选择安装很简单pip install sqlalchemy如果用的是PostgreSQL需要额外装驱动比如psycopg2同步或psycopg3支持异步MySQL用pymysqlSQLite则用Python内置的sqlite3不需要额外安装。这里有个容易忽略的点SQLAlchemy 2.x和1.x的API风格差异很大。2.0版本从2023年开始成为主流原来db.query(User).all()这种写法在2.0里推荐改成了select(User)这种基于Core表达式的写法。我建议新项目直接上2.x别再用老写法否则后面迁移还要重新折腾一遍。怎么确认安装的版本在Python环境里执行import sqlalchemy print(sqlalchemy.__version__)如果显示2.0以上的版本号就可以放心用新API了。对了如果你用的是Flask-SQLAlchemy扩展3.0以上版本对应的就是SQLAlchemy 2.x语法风格也要跟着调整。2.2 Engine、Session、Model三件套刚接触SQLAlchemy的人经常被一堆概念绕晕其实核心就三样Engine、Session、Model。Engine负责连接管理你可以把它理解成一个连接池的调度中心。它读入数据库URL维护底层连接按需分配。创建方式from sqlalchemy import create_engine engine create_engine( postgresqlpsycopg2://user:passwordlocalhost:5432/mydb, echoTrue, # 打印SQL日志 pool_size5, max_overflow10 )Session是真正干活的东西。它代表一次工作单元你可以把它理解成一个缓冲区和执行器的结合体通过session.add()把对象纳入管理通过session.commit()统一提交到数据库通过session.execute()发送查询。Session不是线程安全的所以一个线程里用同一个session没问题跨线程就必须分开。Model就是你定义的表结构用声明式方式from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): pass class User(Base): __tablename__ users id: Mapped[int] mapped_column(primary_keyTrue) name: Mapped[str] mapped_column(String(50)) email: Mapped[str] mapped_column(String(120), uniqueTrue)注意2.0里的Mapped和mapped_column这套类型注解风格让模型定义更接近Python原生类型标注IDE自动补全也更友好。老版的Column写法虽然还能用但新项目就不推荐了。2.3 连接配置背后的讲究数据库URL的格式是dialectdriver://user:passwordhost:port/dbname这里面门道不少dialect是数据库类型比如postgresql、mysql、sqlite。driver是驱动库不同驱动的性能和行为差很多。PostgreSQL选psycopg2还是psycopg3MySQL选pymysql还是mysqlclient都会影响并发和异步支持。SQLite的连接串比较特殊sqlite:///data.db表示本地文件数据库sqlite:////absolute/path/data.db才是绝对路径sqlite:///:memory:是内存库。配连接池的时候pool_size和max_overflow是关键参数。我用PostgreSQL时习惯设pool_size5, max_overflow10意思是池子保持5个连接不够时最多再开10个临时连接。这个值不是越大越好数据库服务器上每个连接都占用内存资源连接数太多反而会拖慢数据库。还有一点必须提醒密码里如果带着或/这些特殊字符URL会解析出错。正确做法是用URL.create()来生成连接串或者对特殊字符做百分号编码。我见过不止一个同事因为密码里有导致连接失败排查半天还在怀疑数据库配置。3. 模型设计从对象思维理解表结构3.1 声明式模型的更多细节模型定义了表结构字段类型、约束、默认值都在这层声明。除了最基础的Integer和String实际项目里经常用到的还有DateTime时间字段注意要不要带时区。Boolean布尔值。JSONPostgreSQL和MySQL都支持JSON类型SQLAlchemy也有对应的JSON类型。Enum枚举类型从数据库层约束值范围。默认值这块有个容易踩的坑default和server_default是两回事。defaultfunc.now()是Python端生成值再插入server_defaulttext(now())是让数据库帮你生成。后者在批量插入和大数据量场景下性能更好因为不需要把时间戳从应用层传过去。但如果表结构里有非空约束、又要兼容SQLite就得小心server_default在SQLite下的兼容性有些函数名对不上。约束条件里uniqueTrue会创建唯一索引indexTrue会创建普通索引。多字段联合唯一可以用UniqueConstraint写在__table_args__里查询频率高的组合条件别忘了加联合索引这是后面性能优化最容易忽略的环节。3.2 关系映射与级联行为表与表之间的关联是ORM的精髓。一对多是最常见的场景比如一个用户有多篇文章class User(Base): __tablename__ users id: Mapped[int] mapped_column(primary_keyTrue) posts: Mapped[list[Post]] relationship(back_populatesuser) class Post(Base): __tablename__ posts id: Mapped[int] mapped_column(primary_keyTrue) user_id: Mapped[int] mapped_column(ForeignKey(users.id)) user: Mapped[User] relationship(back_populatesposts)relationship()声明的是对象层面的关系ForeignKey声明的是数据库层面的外键。有人会误解为二选一其实这两者是配合使用的。back_populates让两个方向都能互访省得只在一个类里写。级联删除是关系设计里最容易翻车的地方。cascadeall, delete-orphan表示删除父对象时子对象一并删除。但如果你误把这条规则用在不该用的关系上比如一个产品下有订单订单删了你把产品也删了这在业务上往往是灾难。我的习惯是外键加上ondeleteCASCADE的数据库级约束ORM级联只保留必要的删除行为。还要注意SQLite默认不开外键约束需要手动执行PRAGMA。多对多关系需要一张中间表在SQLAlchemy里用Table定义然后relationship(secondary中间表)挂上去。这个中间表如果还需要额外字段比如用户和群组的关系里有角色字段那就不能只挂Table了得把中间表也设计成一个模型再通过两个ForeignKey关联原表。3.3 表结构迁移Alembic是标配直接Base.metadata.create_all(engine)建表适合demo真实项目里表结构会一直变加字段、改类型、加索引都是家常便饭。这时候Alembic就派上用场了。它和SQLAlchemy是同一团队开发的专做数据库迁移。基本流程是alembic init alembic # 修改alembic/env.py里的target_metadata为你的Base.metadata alembic revision --autogenerate -m create users table alembic upgrade headAutogenerate能根据模型的变更自动生成迁移脚本但它不是万能的。改字段类型、改约束名这些操作经常生成不准需要手动review生成的迁移文件。我踩过最惨的一次是在生产环境跑upgrade head时自动生成出来的脚本把几万行的表做了一次全表重建锁表导致业务停了十几分钟。从那以后我养成了规矩迁移脚本必须人工review大表的变更先降级到低峰期执行必要时先在Staging环境跑一遍。4. 查询操作实战4.1 基础查询写法2.0推荐用select()来查询风格和SQL更像了from sqlalchemy import select # 查所有用户 stmt select(User) result session.execute(stmt).scalars().all() # 带条件 stmt select(User).where(User.name 张三, User.age 18) users session.execute(stmt).scalars().all() # 排序和分页 stmt select(User).order_by(User.created_at.desc()).limit(20).offset(40)这里注意session.execute(stmt)返回的是一个Result对象。只取一行用.scalar_one_or_none()取多行用.scalars().all()。如果你直接result.all()拿到的是一条条Row而不是模型实例很多人第一次用会懵。条件判断有个细节要留意User.name None在SQLAlchemy里生成的是name IS NULL不是 NULL这个设计是为了贴近SQL标准。同样的User.name ! None生成的是name IS NOT NULL。如果你要严格用去跟NULL比较反而要绕个弯但实际业务里大多数时候这个直观写法是符合预期的。4.2 聚合、分组与子查询聚合查询也不难sqlalchemy.func提供了常用的SQL函数from sqlalchemy import func stmt select(User.department, func.count(User.id)).group_by(User.department)这会生成SELECT department, count(users.id) FROM users GROUP BY department。如果想把结果绑定到模型的属性上可以用.label()给聚合列起别名。注意聚合查询返回的通常不是模型实例而是Row对象访问方式可以是row.department这种属性式访问也可以用row[0]下标式访问。子查询在SQLAlchemy里叫subquery()用法是先把一个select语句包成子查询再在外层引用它的列。逻辑上跟SQL一致只是嵌套在一层Python代码里。写复杂查询时我会先直接在数据库客户端里把SQL调通再翻译成SQLAlchemy代码这样排查问题会快得多。4.3 关联查询优化查用户时顺便拿到他的文章列表有两种做法一种是用joinedload相当于SQL里的JOIN一次性把关联数据查出来from sqlalchemy.orm import joinedload stmt select(User).options(joinedload(User.posts)).all()另一种是selectinload它先生成查主表的SQL再根据主表的ID生成一条WHERE id IN (...)的关联查询。这两种各有用处数据量大、关联层级深的时候selectinload往往更稳因为它不会产生笛卡尔积爆炸关联表数据量小且查询条件简单时joinedload效率更高。我用的经验是默认用selectinload遇到短期热点查询再临时换joinedload实测对比。没有绝对的好坏你要做的是把生成的SQL拿来看一遍再决定。5. 事务管理与并发控制5.1 理解事务边界数据库操作里最怕的事情之一是数据写到一半程序崩了留下一半脏数据。事务就是用来保证“要么全成功要么全失败”的。SQLAlchemy里事务的边界是通过Session来管理的。session.commit()提交事务session.rollback()回滚事务。一个Session从开始操作到commit之间所有的变更都在事务里。如果业务中途发生异常一定要做rollback否则Session会进入一个“半死不活”的状态后续所有操作都可能报错。经典的处理模式是from sqlalchemy.orm import Session with Session(engine) as session: try: user User(name张三) session.add(user) session.commit() except Exception: session.rollback() raise很多初学的人看到session.add()就把数据写进去了其实没有它只在内存里。真正的落库点是commit()那一下。这也是为什么有些代码调了半天数据没变化先查一下是不是忘了commit。5.2 并发控制并发场景下两个人同时改一条数据总有一个人的修改会被覆盖。SQLAlchemy的解决方法主要是两种乐观锁和悲观锁。乐观锁靠版本号或时间戳字段。模型里定义一个version列更新时SQLAlchemy自动带上WHERE version 你读到的那一版如果执行过程中发现有别的事务已经改了version更新影响行数为0你就可以感知到冲突并重试。这个方案并发读多写少的场景很合适不会阻塞读操作。悲观锁是直接锁定数据库行用with_for_update()stmt select(User).where(User.id 1).with_for_update() user session.execute(stmt).scalar_one()这会在数据库层面对这行加锁直到事务提交。适合写多读少的场景但锁等待会增加请求延迟死锁风险也更高。具体选哪种取决于业务里冲突频繁不频繁。我一般在订单、库存这类强一致地方用悲观锁在通用资料修改上用乐观锁。5.3 Session生命周期管理Session不是一次请求一个也不是全局单例最合适的是“一个工作单元一个Session”。Web框架里一般是请求开始创建、请求结束关闭。Flask-SQLAlchemy帮你做了这个事scoped_session也实现了线程隔离。如果你在FastAPI里用SQLAlchemy可以写一个依赖注入来管理Sessiondef get_db(): db SessionLocal() try: yield db finally: db.close()这样做的好处是异常发生时依赖关闭阶段会把资源回收掉不用你在每个路由里手动处理。Session管理混乱是很多SQLAlchemy项目出现“连接泄漏”的根源——连接池被占满了数据库连接数暴涨最后应用卡死。别问我是怎么知道的我就亲眼看过一个服务因为session没close把PostgreSQL的连接池拖垮过一次。6. 性能优化与常见陷阱6.1 N1查询是怎么发生的N1查询是ORM新手最容易踩的性能大坑。场景是这样的你查出了100个用户然后在循环里对每个用户访问user.postsORM发现posts没有被预先加载就为每个用户单独发一条查询结果总查询数是1100条。数据库连接的压力一下子增大了100倍。避免的方法就是前面说的懒加载改预加载stmt select(User).options(selectinload(User.posts)) users session.execute(stmt).scalars().all()这里又有一个细节如果你的关联集合非常大比如一个用户有上万篇文章joinedload可能导致主查询返回的行数膨胀每次加载都很慢。这时候考虑分页或者明确地只加载需要的字段。不要试图用ORM解决所有数据展示问题报表类需求直接写SQL反而更清醒。6.2 批量插入与更新一次性插入几万条数据如果用循环逐个session.add()再统一commit效率会很低因为每条记录都要走一遍ORM的完整生命周期。SQLAlchemy提供了bulk_insert_mappings和bulk_update_mappings可以走更高效的批量路径session.bulk_insert_mappings( User, [{name: fuser{i}, age: i} for i in range(10000)] ) session.commit()但注意bulk操作不会触发ORM的很多事件和级联行为也不会自动维护关系。如果你的模型里有复杂的relationship级联或者需要在插入时自动写入审计字段bulk就不适合了。数据量没到千级的话直接循环add也有可接受的性能。另一种思路是用方言自带的批量插入。比如PostgreSQL支持INSERT INTO ... VALUES (...), (...), ...的扩展语法SQLAlchemy 2.0提供了insert()配合参数列表也能自动生成批量插入from sqlalchemy.dialects.postgresql import insert stmt insert(User).values([{name: fu{i}} for i in range(1000)]) session.execute(stmt)6.3 原生SQL与ORM的取舍SQLAlchemy从来不排斥原生SQL操作。session.execute(text(SELECT ...))可以执行任意原生SQL返回的结果集同样可以映射到模型。这在处理窗口函数、复杂JOIN、CTE等ORM表达能力不足的场景时非常好用。我的原则是90%的常规CRUD用ORM10%的复杂报表、数据清洗类SQL直接写原生text或视图。关键是要记住——ORM帮你省的是模板代码不是帮你省SQL功底。你有时间把EXPLAIN分析弄明白比研究ORM的奇技淫巧有用得多。7. 异步支持与真实场景落地7.1 同步还是异步到底怎么选“异步同步比较”在热搜里出现频率很高说明很多人已经在用异步框架比如FastAPI了。SQLAlchemy 1.4以后引入了asyncio扩展到了2.x已经比较成熟。核心思路是使用AsyncEngine和AsyncSessionfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker engine create_async_engine(postgresqlpsycopg://user:passlocalhost/db) SessionLocal async_sessionmaker(engine) async def main(): async with SessionLocal() as session: stmt select(User).where(User.name 张三) result await session.execute(stmt) user result.scalar_one_or_none()注意PostgreSQL的异步驱动要用psycopg即psycopg3或者asyncpgpsycopg2本身没有异步接口。MySQL则是aiomysql或asyncmy。SQLite也有异步驱动但它本质是本地IO用异步收益有限。同步和异步到底怎么选我的看法是如果你的Web框架本身就是异步的FastAPI为了不阻塞事件循环数据库访问最好也异步如果框架是同步的Flask、Django默认硬上异步反而多余还会增加代码复杂度。异步带来的性能收益主要体现在高并发IO场景如果业务是简单的CRUD且并发量不大同步代码的调试维护成本低得多。7.2 爬虫数据落库的实操方案“SQLAlchemy储存爬虫数据”这个需求很常见。爬虫场景有个特点数据结构动态变化、数据量大、字段不规范。用ORM设计模型的时候要给“脏数据”留余地。我的建议是建一张带JSON字段的主表全字段存dict加上一些关键索引字段。比如爬商品信息可以这样设计class Product(Base): __tablename__ products id: Mapped[int] mapped_column(primary_keyTrue) source: Mapped[str] mapped_column(String(50), indexTrue) external_id: Mapped[str] mapped_column(String(100), indexTrue) raw_data: Mapped[dict] mapped_column(JSONB) created_at: Mapped[datetime] mapped_column(server_defaultfunc.now())这样既不丢失原始数据又能用索引字段做去重和增量更新。爬虫去重可以用external_id加唯一约束来实现幂等写入避免跑二次任务时产生重复数据。插入大批量数据时注意Session的flush频率。爬虫每爬一页就commit一次速度会非常难看攒够一定数量再commit一次效果完全不同。但也不能攒太多如果服务在commit之前挂了这段时间内存里的数据就全丢了。稳妥做法是做分批落库每批控制在1000到5000条之间并在批处理边界打上日志方便中断之后接着爬。7.3 JSON字段与动态结构处理爬虫数据另一个常见问题是结构不固定。比如某天的数据多了一个字段或者某个字段的值类型变了。如果你一开始就把每个字段都建一个数据库列后续每次改版都要跑迁移成本很高。用JSON或JSONB字段存储原始数据就灵活多了。PostgreSQL的JSONB还支持GIN索引可以直接对JSON内部的键值做查询。SQLAlchemy里用JSONB类型配合text()条件可以很方便地实现stmt select(Product).where(Product.raw_data[category].astext 手机)不过JSON字段也有个弊端里面的数据没法用数据库约束来保证完整性。所以我的做法是“关键字段提列、原始字段存JSON”——把经常要查询、排序、去重的字段提取成数据库列剩下的原始信息整个丢进JSON字段。这样既保留了灵活性又保证了查询性能。8. 常见问题与排查技巧实录8.1 会话状态与连接问题的排查SQLAlchemy的错误信息有时候挺唬人比如DetachedInstanceError——通常是模型实例已经脱离了Session的管辖你还去访问它的懒加载属性导致的。解决思路是要么在Session生命周期内把需要的数据提前加载好要么配置lazyraise来强制自己尽早发现此类问题。连接池报错TimeoutError: QueuePool limit of size 5 overflow 10 reached第一反应应该是查session是否泄漏而不是调大连接池。用pool_pre_pingTrue可以自动剔除已被数据库断开的失效连接这是生产环境必开的设置。还有一类报错出现在数据库连接被网络抖动断掉时SQLAlchemy默认会不断重试如果重试也不行就直接抛异常了这类问题只能靠监控和重试机制兜住。8.2 模型变更没生效很多人改完模型跑代码发现数据库表结构还是老样子第一反应是重启进程。其实Base.metadata.create_all(engine)只会在表不存在的时候创建表它不会同步修改已经存在的表结构。表结构变更需要走Alembic生成迁移脚本。排查表结构问题时直接用数据库管理工具会更直观。比如实践中常用Navicat查看字段定义、索引信息、外键关系。有人问“怎么用Navicat给数据库设置只读权限”——其实这属于数据库账号权限管理在用户管理里给应用账号只授SELECT权限就行让应用只能读不能写或者让测试人员只能查看数据而不能误改生产库。这类权限管理配合Alembic的迁移流程能有效避免生产环境被手贱操作改坏。8.3 常用错误速查表错误现象常见原因解决办法AttributeError: User object has no attribute posts模型里没定义relationship在模型类中添加relationship定义MissingGreenlet异常异步Session中同步访问了懒加载属性用await加载或配置selectinload/joinedloadDetachedInstanceError实例脱离了Session后访问懒加载属性在session内预加载或配置lazyraiseOperationalError: no such table数据库路径或模型映射错误检查URL和__tablename__是否正确TimeoutError from QueuePool连接池耗尽通常session未关闭检查session.close()是否每次都有执行类型错误无法绑定参数字段类型与SQL类型不匹配检查模型字段类型与数据库列类型是否一致8.4 排查问题的几个通用技巧我在调试SQLAlchemy问题时常用的手段有三个第一开echoTrue看生成的SQL。把模型映射问题、条件生成问题直接用SQL日志暴露出来很多时候问题一眼就能看出来。2.0里用logging配置也能做到只打SQL日志不打调试噪音。第二多用inspect()看对象状态。Session里某个对象是pending、persistent还是detached通过inspect(user)可以看到对象和Session的关系状态。理解了这三种状态很多“为什么没写入数据库”“为什么报Detached”的疑问就迎刃而解。第三把SQLAlchemy生成的SQL复制到数据库客户端里手动执行对比结果。如果手动执行能查到ORM查不到多半是session的filter条件判断或事务状态有问题。这种“对比法”排查效率很高。9. 服务集成与微服务体系里的定位热搜词里有一条“python应用融入spring cloud alibaba微服务体系”这个话题值得展开说说。很多人觉得Python做微服务就是异类其实在数据访问层SQLAlchemy完全可以作为独立的持久化模块嵌入到任何服务架构里。Spring Cloud Alibaba体系里的Nacos、Sentinel、Gateway这些组件Python都有对应的客户端或协议适配方案。Python服务可以作为独立服务注册到Nacos通过HTTP或gRPC跟Java服务通信。SQLAlchemy在这里的角色依然是数据访问层工具每个服务管理自己的模型和数据库表结构变更通过Alembic单独管理。跨服务的数据一致性通常用消息队列解决Python这边也有成熟的消息客户端。这个架构里的关键是服务边界要清晰每个服务只管自己的数据和模型不要跨越服务边界去直接操作别人的数据库表。如果你正准备在Java微服务旁边部署一个Python数据服务我的建议是数据访问层老老实实用SQLAlchemy做ORM对外暴露gRPC或REST接口数据库连接池参数根据服务并发量单独调优。别想着让Python服务去直接读Java服务维护的那张表跨服务读库虽然省了接口调用但一旦表结构变更两边都要跟着变耦合度直线上升。10. 我在实际项目里的几句经验话按我这些年用SQLAlchemy的经验有三件事想单独拎出来说。第一不要神话ORM也不要妖魔化ORM。SQLAlchemy的抽象层做得很扎实但它依然是个工具不是万能钥匙。该写原生SQL的时候就写原生SQL该拆表就拆表别因为“ORM更方便”就永远不加索引。性能问题大多数时候是数据模型的问题不是ORM的问题。第二数据库连接和事务管理是重中之重。很多线上事故追根究底就是Session没关、事务没提交、连接池耗尽。SQLAlchemy提供的功能越强大越要在工程规范上约束自己统一管理session生命周期、生产环境开pool_pre_ping、定时监控慢查询这些比研究某个高级API用法更值得投入。第三框架和工具永远在变底层的SQL能力和数据模型设计能力才是长期竞争力。SQLAlchemy 2.0的写法比1.x清爽不少但如果你不理解join、不理解索引、不理解事务隔离级别换哪个ORM你都会遇到同样的问题。反过来把这三样吃透了SQLAlchemy对你来说就是一层顺手的皮想怎么换都行。这篇指南里的代码片段和操作经验都是我实际跑过、踩过的坑的复盘。如果你在项目中遇到了SQLAlchemy相关的奇怪问题不妨按前面第8章的思路一步步排查大概率能省下不少时间。我自己最大的体会是别急着把写代码的时间省下来先把概念搞清楚后面所有事情都会顺很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →