惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
A
About on SuperTechFans
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
D
Docker
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Register - Security
The Register - Security
J
Java Code Geeks
S
SegmentFault 最新的问题
月光博客
月光博客
G
Google Developers Blog
美团技术团队
Last Week in AI
Last Week in AI
L
LangChain Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
The Blog of Author Tim Ferriss
腾讯CDC
Recent Announcements
Recent Announcements
Recorded Future
Recorded Future
The Cloudflare Blog
有赞技术团队
有赞技术团队
博客园_首页
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
B
Blog
I
InfoQ
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Fortinet All Blogs
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
爱范儿
爱范儿
D
DataBreaches.Net
F
Full Disclosure
M
MIT News - Artificial intelligence
博客园 - 司徒正美
H
Help Net Security

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解 【002】HTTPS 粗解:证书、TLS 握手与对后端配置的影响 Hermes Agent 一周暴涨五万 Star,但我劝你别急着追 明明连接的是Redis的DB0,为什么能查到DB3的数据? 【从0到1构建一个ClaudeAgent】协作-Agent团队 熟悉电子元器件之后,电子小白下一步该怎么走? MAF快速入门(23)通过C#类定义Skills .NET 高级开发 | 手写一个对象映射框架 FastAPI数据库ORM怎么选?我肝了三个Demo后,终于不再纠结了 mysqldump 参数拾遗:在遗忘与铭记之间 C# .NET 周刊|2026年3月5期 Claude code入门 - 陈彦斌 一文学习入门 ThingsBoard 开源物联网平台 GitHub 热门项目 | 2026年04月16日 如何为GIT设置全局勾子,为每次提交追加信息 Number.isFinite和isFinite与isNaN()和Number.isNaN的区别 PortSwigger SQL注入LAB2 推荐一个测试人必备的Skills,从功能到性能全搞定(附详细实操和安装下载方式) 筑基期:掌握Odoo基础核心知识点02(Odoo XML 开发方式详解) GLM模型这么火,咱们用vllm也咧一个呗! 深入理解 AbortController:从底层原理到跨语言设计哲学 字符串学习笔记 多租户系统框架的基础模块设计和分析设计 Apache SeaTunnel Zeta 为什么能做到“又快又稳”? AI开发-python-LangGraph框架(3-26-LangGraph基本概念及第一个简单样例) Vue 3 组件通信,别只会用 Props 和 Emits 了,这几个狠活儿你得看看 ElasticSearch7.X版本配置密码 用Manim实现动态交点计算--从一个动点问题说起 团结引擎+Addressable+Instant Game打包抖音小游戏 function call 实战:让 LLM 自动判断 pod 异常、调用日志工具并完成故障分析 bubseek —— 让 Agent 的足迹,变成团队的洞察 通过 C# 读取并导出 PDF 书签 如何用 GitHub Actions 实现 Steam 自动化发布 【从0到1构建一个ClaudeAgent】并发-后台任务 .NET 高级开发 | 定制 ASP.NET Core 框架 电子小白:什么是运算放大器(运放) zero2Agent:面向大厂面试的 Agent 工程教程,从概念到生产的完整学习路线 堆上的ORW HC32F460 USB CDC通信异常:非对齐访问异常排查 20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过 那些喊着AI 要淘汰你的人,正在靠你的焦虑赚大钱! 深度学习进阶(八)Swin Transformer 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 SnapTranslate 3.0 正式发布:全局划词翻译 + 完整英语学习闭环,一站式搞定查词、记词、复习 工作的意义、工作的困难认知再思考 .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 开了 TUN 模式还是直连?90% 的人都踩过这个坑 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术
FastAPI + SQLAlchemy 2.0 通用CRUD操作手册 —— 从同步到异步,一次讲透
一名程序媛呀 · 2026-04-24 · via 博客园_首页

你是不是也遇到过这样的场景——FastAPI 项目一开始用同步写得好好的,接口响应压测也漂亮,后来心血来潮想全面拥抱异步,结果 Session 报错、延迟加载直接炸了,甚至连一个简单的 refresh 都能把你卡半天?

今天这篇,就是想跟你好好聊聊,在 FastAPI 里用 SQLAlchemy 做 CRUD,同步和异步到底怎么玩、怎么选、怎么避开那些让人头秃的深坑。🎯

📌 本文能帮你解决什么

• 彻底搞懂 SQLAlchemy 2.0 的核心变化,告别旧式 Column 写法

• 搭建同步/异步双引擎,掌握连接池的最佳配置

• 亲手写出通用 CRUD,从单条插入到复杂关联查询全涵盖

• 给出“同步还是异步”的务实答案,让你不再盲目跟风

• 奉上我踩过的坑和排查清单,出了问题可以直接照单抓药

🧭 内容主要脉络

🔹 问题与背景:异步真的更快吗?
🔹 核心原理:从模型定义到引擎配置
🔹 实战 CRUD:增删改查 + 高级查询
🔹 事务与异常:显式边界 + FastAPI 依赖注入
🔹 注意事项:同步异步共存策略 & 常见翻车现场

🐢 第一部分:问题与背景——你其实不用那么焦虑

先说一个反直觉的结论:异步不会自动让你的接口变快

如果你的接口只是查一下数据库然后返回,计算量很小,那同步和异步的性能差距微乎其微。

真正让异步闪光的是高并发 I/O 密集场景——比如你的服务要同时请求多个外部 API、读写大量 WebSocket,这时候异步才能把事件循环的优势发挥出来。

很多团队一上来就非异步不可,结果发现 SQLAlchemy 1.x 时代异步支持根本是半残废,各种 hack,维护得想哭。2.0 之后异步终于能打了,但也有一堆前置条件。

所以,简单项目、低并发、团队对异步不熟,同步完全够用。如果你正准备起一个新项目,而且能保证全链路异步(FastAPI + async DB driver + 异步任务队列),那上异步很香。否则,别给自己找麻烦。

⚙️ 第二部分:核心原理与基础配置

SQLAlchemy 2.0 把声明式映射彻底革新了。现在模型定义长这样:

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50))
    version: Mapped[int] = mapped_column(default=1)  # 乐观锁版本号

老式的 Column() 也能用,但我强烈建议你全部换上 Mapped + mapped_column ,类型提示更清晰,IDE 提示也舒服多了。

接下来是引擎配置,同步异步的差别主要在这:

# 同步引擎
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

sync_engine = create_engine(
    "postgresql://user:pass@localhost/db",
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
)
SyncSessionLocal = sessionmaker(bind=sync_engine)

# 异步引擎
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

async_engine = create_async_engine(
    "postgresql+asyncpg://user:pass@localhost/db",
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,
)
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)

你可能会问:为毛异步 URL 要变成 postgresql+asyncpg ?因为 asyncpg 是真正的异步驱动,psycopg2 是同步的,混用必定翻车。MySQL 用户请选 aiomysql

连接池参数 pool_sizemax_overflow 在两种模式下意义一样,记得加上 pool_pre_ping=True ,防止连接被数据库回收后应用还傻傻复用。

🛠️ 第三部分:全程实战——CRUD 一把梭

下面我会同步异步对照着写,你会发现核心逻辑几乎一样,区别只在于 async/await 关键字和会话对象

✅ CREATE 操作

# 同步单条插入
def create_user_sync(session, name: str):
    user = User(name=name)
    session.add(user)
    session.commit()
    session.refresh(user)  # 获取数据库生成字段
    return user

# 异步单条插入
async def create_user_async(session: AsyncSession, name: str):
    user = User(name=name)
    session.add(user)
    await session.commit()
    await session.refresh(user)  # 千万别忘 await
    return user

批量插入时, add_all 很直观,但对于大数据量,我更喜欢 bulk_insert_mappings ,速度快得多。
但注意:异步下 bulk 操作不支持自动刷新和关系绑定,用完记得手动 commit。

🛎️ async with session.begin() 事务管理

# 异步批量插入最佳实践:使用 async with session.begin() 
# 更新和删除同理,写操作一律用 begin() 管事务,读操作直接用裸 session(因为读一般不需要事务边界)
async def bulk_create_users(session: AsyncSession, names: list[str]):
    async with session.begin():
        session.add_all([User(name=n) for n in names])
    # 事务已提交,无需显式 commit
    
# 当然,同步时也可使用 with session.begin() 事务自动提交,user 已持有数据库生成字段

✅ READ 操作

主键查询用 session.get() ,简单粗暴。高级查询统一用 select() 构造,再也没 query() 什么事了。

from sqlalchemy import select, and_, or_, func
from sqlalchemy.orm import selectinload

# 异步条件查询 + 排序 + 分页
async def search_users(session: AsyncSession, keyword: str, page: int, size: int):
    stmt = (
        select(User)
        .where(User.name.ilike(f"%{keyword}%"))
        .order_by(User.id.desc())
        .limit(size)
        .offset((page - 1) * size)
    )
    result = await session.execute(stmt)
    return result.scalars().all()

关联查询时,强烈建议显式预加载,用 selectinloadjoinedload 把 N+1 问题扼杀在摇篮里。
异步下尤其注意,懒加载属性一访问就会抛出 MissingGreenlet 错误,血的教训啊。

stmt = select(User).options(selectinload(User.posts)).where(User.id == uid)

聚合查询用 func

stmt = select(func.count(User.id)).where(User.name.like("%小%"))
total = (await session.execute(stmt)).scalar()

🧩 再来一个复杂综合案例:搜索用户及其文章统计

需求:搜用户名或邮箱包含关键词的活跃用户,带出他们发表的文章数量,按文章数降序,分页。

async def search_users_with_post_count(
    session: AsyncSession,
    keyword: str,
    page: int = 1,
    size: int = 10,
):
    # 子查询:每篇文章所属作者
    post_count_sub = (
        select(Post.author_id, func.count(Post.id).label("post_count"))
        .group_by(Post.author_id)
        .subquery()
    )
    
    # 主查询:用户 left join 文章统计
    stmt = (
        select(User, func.coalesce(post_count_sub.c.post_count, 0).label("post_count"))
        .outerjoin(post_count_sub, User.id == post_count_sub.c.author_id)
        .where(
            and_(
                User.is_active == True,
                or_(
                    User.username.ilike(f"%{keyword}%"),
                    User.email.ilike(f"%{keyword}%"),
                ),
            )
        )
        .order_by(desc("post_count"))   # 按别名排
        .limit(size)
        .offset((page - 1) * size)
    )
    
    result = await session.execute(stmt)
    rows = result.all()
    
    return [
        {"user": row.User, "post_count": row.post_count}
        for row in rows
    ]

这个案例一口气用上了 子查询、outerjoin、and_/or_、ilike、coalesce、别名排序、分页,基本上日常查询的"天花板难度"也就这样了。你把它拆开看,每一块都是前面讲过的基础零件拼起来的。

✅ UPDATE 操作

方式一:查出对象,改属性,commit。直观,适合小更新。

方式二:批量更新 update() ,直接发 SQL,不经过 ORM 对象。适合批量改状态。

from sqlalchemy import update

async def deactivate_users(session: AsyncSession, user_ids: list[int]):
    async with session.begin():
        stmt = update(User).where(User.id.in_(user_ids)).values(active=False)
        result = await session.execute(stmt)
        return result.rowcount

再说个容易被忽略的:乐观锁更新。利用版本号字段 version ,修改时带上版本条件,防止并发覆盖。

stmt = (
    update(User)
    .where(User.id == uid, User.version == old_version)
    .values(name=new_name, version=old_version + 1)
)
result = await session.execute(stmt)
if result.rowcount == 0:
    raise Exception("数据已被修改,请重试")

✅ DELETE 操作

from sqlalchemy import delete

async def remove_user(session: AsyncSession, uid: int):
    async with session.begin():
        stmt = delete(User).where(User.id == uid)
        await session.execute(stmt)

🔐 第四部分:事务与异常——别把连接池打爆了

在 FastAPI 中,我习惯用依赖注入管理会话生命周期,这样每个请求都会自动获取、自动关闭。

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

关键一条:显式事务边界。哪怕你用了依赖注入,也建议在业务逻辑里用 async with session.begin() 包裹写操作,这样出错了自动回滚,绝对不会出现连接未释放的惨案。

有时候你需要设置事务隔离级别,特别是报表类查询,加 with_for_update() 或修改 session 连接参数就行,异步下完全一致。

🔄 第五部分:同步异步共存——俩种方案,一个原则

项目大了,难免有遗留模块还在同步,新的又想用异步。怎么搞?

如果在异步路由里调用同步数据库方法,可以用 asyncio.to_thread 把同步操作扔进线程池,避免阻塞事件循环。

import asyncio

async def async_endpoint():
    user = await asyncio.to_thread(sync_get_user, user_id=1)
    return user

反过来,在同步路由里调异步方法,千万别用 asyncio.run() ,它会新建事件循环,和 FastAPI 当前循环冲突,结果就是玄学报错。要么老实用同步,要么把整个路由改成 async。

归根结底,我的建议是:新项目从一开始就全异步栈——FastAPI + SQLAlchemy 2.0 async + asyncpg/aiomysql。即使现在并发不高,也能免去未来重构的痛苦。

💣 第六部分:常见翻车现场 & 排查指南

1. 异步 refresh 前没 await

现象:获取不到数据库默认值或自增 ID。
原因: session.refresh() 是协程,必须 await

2. MissingGreenlet 错误

现象:异步会话访问懒加载属性直接炸。
解决:把所有要用的关联全部 selectinloadjoinedload 。别偷懒。

3. 连接池耗尽

现象:突然所有请求卡住,日志显示“等待连接超时”。
大概率是事务没关闭,连接一直占着。马上检查是否有 commit/rollback 缺失。

4. 同步异步引擎混用

现象:在同一个模块里不小心把异步 session 传给了同步方法。
对策:严格分层,文件名带 _sync_async 后缀,写清楚。

🏁 总结:别让选择本身变成负担

说到底,CRUD 就是那几个固定动作,2.0 的 API 也帮我们抹平了很多心智负担。同步还是异步,真正要看的只有两件事:你的并发模型团队的肌肉记忆。选定一套栈,写清楚规范,然后沉下心写业务就好。

老规矩,如果这篇文章帮你少踩了一个坑,或者帮你下定了用同步/异步的决心,点个赞加关注不过分吧?☺️ 转发给那个还在纠结的同事,说不定会请你喝奶茶呢。🙌