目录
-
- 一,查询操作
-
- 1.查询基础与结果处理 (Execution/Scalars)
- 2.条件查询
- 3.聚合操作 (Count / Group By / Avg / Sum)
- [4. 分页查询](#4. 分页查询)
- 5.连接查询
-
- [5.1 relationship的简单说明](#5.1 relationship的简单说明)
- [5.2 连接查询](#5.2 连接查询)
-
- [5.2.1 显示JOIN](#5.2.1 显示JOIN)
- 二,新增,更新和删除操作
-
- [1. 新增数据 (Create)](#1. 新增数据 (Create))
- 2.更新数据 (Update)
- 3.删除数据
- 总结
-
- [🚀 FastAPI ORM 操作速查表](#🚀 FastAPI ORM 操作速查表)
一,查询操作
方便演示,下面统一定义一个基础模型:
python
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, Integer, Float
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
price: Mapped[float] = mapped_column(Float)
stock: Mapped[int] = mapped_column(Integer)
category: Mapped[str] = mapped_column(String(20))
1.查询基础与结果处理 (Execution/Scalars)
在异步中,所有的查询都通过 db.execute()(db就是我们注入的AsyncSession对象) 发出,但返回的是一个包装过的 Result 对象。
- scalars(): 最常用。把原始的行记录转换成模型对象(如 Product 对象)。
- first(): 拿第一条,没找到返回 None。
- all(): 拿全部,返回列表。
基础使用如下:
python
from sqlalchemy import select
@app.get("/products")
async def get_products(db: AsyncSession = Depends(get_db)):
# 1. 构造语句
stmt = select(Product)
# 2. 异步执行,得到 Result 对象
result = await db.execute(stmt)
# 3. 提取模型对象列表
products = result.scalars().all()
return products
-
await db.execute(stmt) 返回的是什么?
- 数据库实际跑了一条 SELECT products.id, products.name, ... FROM products;
- SQLAlchemy 把每一行原始数据(5 个字段)打包成 Row 对象。
- result 是一个 Result 对象,里面装着很多个 Row。
此时你还没有得到任何一个 Product 实例,只是拿到了一堆长得像元组 + 字典的混合体(Row)。
-
select(Product) 写下的"蓝图"决定了什么?
你在 select() 里把什么放在第一个参数,SQLAlchemy 就会把那一行的第一个位置标记成什么类型的东西。
- 写 select(Product):SQLAlchemy 心里说:"第一列是一个 Product 实体"。于是当收到数据库返回的原始行时,它会用整行数据自动组装出一个 Product 实例,塞到第一个位置。
- 写 select(Product.name):第一个参数只是一个字段,那第一个位置就是一个普通字符串,不会组装对象。
这个蓝图在你调用 db.execute(stmt) 之前就已经定好了。
-
scalars() 到底干了什么?
Result.scalars() 的工作非常简单:
把每一行的第一个元素拿出来,丢掉其他列,返回一个 ScalarResult。
- 如果蓝图里第一个位置是 Product 实体,那么拿出来的就是 Product 对象。
- 如果蓝图里第一个位置是 Product.name 字段,那么拿出来的就是字符串。
python# 第一个参数是 Product 实体 → scalars() 返回 List[Product] result = await db.execute(select(Product)) products = result.scalars().all() # 每个元素都是 Product 实例 # 第一个参数是字段 → scalars() 返回 List[str] result = await db.execute(select(Product.name)) names = result.scalars().all() # 每个元素都是字符串注意:不是看什么"主键"或"字段个数",只取决于你在 select() 的第一个参数是字段还是整个模型类。
之后用得到的ScalarResult的方法,根据需求选择first还是all就行了。
其实Result上也可以直接用first或all,只不过这样拿到的是Row对象。
除去上面这种方式外,还能用AsyncSession直接提供的快捷方法,通过db直接使用。
- db.get(Model, primary_key) ------ 按主键查一个
- db.add(instance) ------ 把新对象挂到会话
- db.delete(instance) ------ 标记删除
- db.refresh(instance) ------ 重新从数据库拉取最新数据
这些方法是直接在会话层提供的"语法糖",省去手写 SQL 的步骤。
2.条件查询
条件查询是 SQLAlchemy 里最核心的"筛选"技能。无论查单条还是查列表,99% 的业务逻辑都要靠它。
python
from sqlalchemy import select, and_, or_
# 基础条件
stmt = select(Product).where(Product.price > 1000)
# AND:多次 where 或用 and_
stmt = select(Product).where(Product.price > 1000, Product.stock < 50) # 逗号默认 AND
stmt = select(Product).where(and_(Product.price > 1000, Product.stock < 50)) # 显式 and_
# OR:必须用 or_
stmt = select(Product).where(or_(Product.category == "电子", Product.category == "家电"))
# AND 与 OR 混合
stmt = select(Product).where(
and_(
or_(Product.category == "电子", Product.category == "家电"),
Product.price > 1000
)
)
# SQL: WHERE (category = '电子' OR category = '家电') AND price > 1000
# IN
stmt = select(Product).where(Product.id.in_([1, 3, 5]))
# BETWEEN
stmt = select(Product).where(Product.price.between(500, 2000))
# 模糊查询,%代表多个字符,_代表一个字符
stmt = select(Product).where(Product.name.contains("手机")) # LIKE '%手机%'
stmt = select(Product).where(Product.name.startswith("华为")) # LIKE '华为%'
stmt = select(Product).where(Product.name.endswith("Pro")) # LIKE '%Pro'
stmt = select(Product).where(Product.name.ilike("%phone%")) # ILIKE (不区分大小写)
# 动态条件
conditions = []
if min_price:
conditions.append(Product.price >= min_price)
if category:
conditions.append(Product.category == category)
if conditions:
stmt = stmt.where(and_(*conditions))
其它<>=!= 比较运算符的用法和python差不多。
需要注意,条件本身是同步构建的:Product.price > 1000 只是个表达式对象,不涉及 IO,所以不需要 await。
真正异步的只有 db.execute(stmt) 这一步。
所有条件运算符都返回新的条件对象,可任意组合、赋值给变量,再动态拼进 where()。
还有,SQLAlchemy 支持用 | 和 & 来表示 OR 和 AND,因为条件对象重载了这些位运算符。
如:
python
stmt = select(Product).where(
((Product.category == "电子") | (Product.category == "家电"))
& (Product.price > 1000)
)
这会生成 WHERE (category='电子' OR category='家电') AND price > 1000,看起来比 or_、and_ 更直观。
呃,虽然这里很方便,但貌似会有些陷阱之类的,更推荐使用or_和and_
再补充一个,one_or_none()方法, 是 scalars() 之后才能用的结果提取方法之一,返回 0 条或 1 条记录。如果查出多条,直接抛异常。
3.聚合操作 (Count / Group By / Avg / Sum)
聚合查询和普通查询最本质的区别在于:它返回的不再是完整的模型对象,而是经过计算后的统计值(数字、分组字段等)。因此,整个处理流程都不同。
如下:
python
from sqlalchemy import func
@app.get("/stats")
async def get_stats(db: AsyncSession = Depends(get_db)):
# 统计每个类别的平均价格和总数
stmt = (
select(
Product.category,
func.avg(Product.price).label("avg_price"),
func.count(Product.id).label("total")
)
.group_by(Product.category)
.having(func.count(Product.id) > 1) # 过滤分组
)
result = await db.execute(stmt)
# 使用 mappings() 可以直接转换成字典格式,非常方便返回 JSON
return result.mappings().all()
-
为什么不能用 scalars()?
像上面说的,scalars() 的工作原理是提取每一行中的第一个元素,并期望这个元素是 ORM 实体。但聚合查询的 select() 里放的是:
pythonselect(Product.category, func.avg(...), func.count(...))第一个元素是 Product.category,只是一个普通字符串字段。如果强行调用 scalars(),你只能拿到每行的第一个字段(category),后面的平均值、计数全部丢失。
更关键的是:聚合查询的结果没有 ORM 实体,所以本来就不该用 scalars()。
-
正确的处理方式:mappings()
mappings() 把每一行结果转换成一个 RowMapping 对象,行为类似字典。调用 .all() 后得到字典列表,直接就能被 FastAPI 序列化成 JSON。
pythonresult = await db.execute(stmt) data = result.mappings().all() # 返回示例: # [ # {"category": "电子", "avg_price": 3500.0, "total": 12}, # {"category": "家电", "avg_price": 2100.0, "total": 8} # ]如果你想直接获取元组,也可以用 result.all(),但处理起来不如字典方便,所以 mappings() 是聚合查询的标配。
-
func 是什么?
func 是 SQLAlchemy 提供的SQL 函数生成器。你调用 func.xxx() 就相当于生成一个 SQL 函数表达式。
pythonfrom sqlalchemy import func func.count(Product.id) # COUNT(products.id) func.avg(Product.price) # AVG(products.price) func.sum(Product.stock) # SUM(products.stock) func.min(Product.price) # MIN(products.price) func.max(Product.price) # MAX(products.price) func.now() # 数据库当前时间(不是聚合函数,但同样用法)重要:这些函数的返回值就是计算后的数值,不是 ORM 对象,再次印证不能用 scalars()。
-
label()------给计算结果起别名
python.group_by(Product.category)聚合函数的返回值在行里默认没有易读的名字,必须用 .label("别名") 给它命名,这样到了 mappings() 返回的字典里,键名就是你指定的别名。如果不加 label(),字典的键会直接是数据库生成的奇怪名字(如 avg_1),不利于前端对接。
-
group_by() 分组
告诉数据库按哪个字段分组进行统计。例如:
python.group_by(Product.category)SQL 会变成 GROUP BY products.category。
你可以在 select() 中同时选择分组字段和多个聚合函数,但要注意:非聚合字段必须出现在 group_by 里(SQL 严格模式要求),否则数据库会报错。
-
having()------过滤分组结果
python.having(func.count(Product.id) > 1)和数据库中的类似。。。
4. 分页查询
-
基本公式
分页由
offset和limit配合实现:pythonoffset = (page - 1) * size stmt = select(Product).offset(offset).limit(size)page:页码,从 1 开始 (如果是第一页,那就不用跳过数据,所以offset是0,以此类推)size:每页条数offset:跳过前 N 行limit:最多取 M 行
如果只写
offset不加limit,数据库会跳过前 N 行后返回所有剩余行,无法控制返回量。limit和size的数值一样,它们的意义也一样。
-
必须加上
order_by()保证结果稳定没有排序的分页是"随机分页"。数据库在不指定
ORDER BY时,返回顺序不确定,可能导致同一记录出现在不同页,或分页结果重复、遗漏。务必加上稳定的排序字段(如主键
id,或创建时间created_at):pythonstmt = ( select(Product) .order_by(Product.id.asc()) .offset((page - 1) * size) .limit(size) )排序字段应唯一,若业务排序字段可能重复,可以用第二个字段兜底,例如
.order_by(Product.price.desc(), Product.id.asc())。desc降序,asc升序
5.连接查询
5.1 relationship的简单说明
多表关系在 SQLAlchemy 里靠两个东西表达:
- ForeignKey:在数据库层面建立外键约束,说明"这列引用另一张表的哪一列"。
- relationship:在 Python 层面声明模型之间的关联,让你能通过属性直接访问关联对象(如 product.category、category.products)。
像下面这样写:
python
from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
class Category(Base):
__tablename__ = "categories"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50), unique=True)
# relationship:让 Category 能直接拿到它的所有 Product
products: Mapped[list["Product"]] = relationship(
back_populates="category",
lazy="selectin", # 异步安全
)
class Product(Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
price: Mapped[float]
# 外键 ------ 数据库里实际存在的列
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
# relationship:让 Product 能直接拿到它所属的 Category
category: Mapped["Category"] = relationship(
back_populates="products",
lazy="selectin",
)
relationship的back_populates参数后面填的是对方模型relationship的属性名,字符串形式连接。
ForeignKey 是数据库里实际存储的"硬约束";relationship 是 Python 代码里让你方便导航对象图的"软通道"。
relationship 完全不影响表结构,它只影响 Python 对象的属性导航。
比如说,在上述例子中建立了这两个连接之后,我们在路由或者其它地方中就可以像下面这样使用。
python
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
@app.get("/categories/{category_id}")
async def get_category(category_id: int, db: AsyncSession = Depends(get_db)):
# 用 db.get 按主键查分类
category = await db.get(Category, category_id)
if not category:
raise HTTPException(status_code=404, detail="分类不存在")
# 直接通过 category.products 就能拿到商品列表(已经预加载好了)
return {
"id": category.id,
"name": category.name,
"products": [
{"id": p.id, "name": p.name, "price": p.price}
for p in category.products
]
}
5.2 连接查询
ORM 中的连接查询本质上就是在映射 SQL 的多表查询。在 SQLAlchemy 异步里,我们会用不同方式来实现 JOIN、子查询、IN 过滤等,目的都是把原本分散在多条 SQL 中的数据,用尽量少的数据库往返拿回来。
连接查询分为两类:关系预加载(selectinload / joinedload)和显式 JOIN。
5.2.1 显示JOIN
不仅可以连表,还能只投影部分字段、使用复杂连接条件、基于关联表过滤主表等。它就是传统 SQL 里 JOIN 的直接映射。
python
stmt = select(要查的实体或列).join(目标表/实体, 连接条件).where(...)
-
内连接
.join(Post, User.id == Post.user_id) 就对应 SQL 的 INNER JOIN ... ON。
-
外连接
只需要在join中添加isouter=True,就表示外连接(左外连接),也可以使用 .outerjoin(Post, 条件) 简写,效果相同。
pythonstmt = ( select(User.name, Post.title) .join(Post, User.id == Post.user_id, isouter=True) # isouter=True 表示外连接 ) result = await session.execute(stmt) rows = result.all()需要 右外连接(Rare),交换表位置即可。
-
自连接
都是一样的,用aliased取个别名就行。
pythonfrom sqlalchemy.orm import aliased UserAlias = aliased(User) # 相当于起别名 u2 stmt = ( select(User.name, UserAlias.name) .join(UserAlias, (User.name == UserAlias.name) & (User.id != UserAlias.id)) ) result = await session.execute(stmt) rows = result.all()
二,新增,更新和删除操作
1. 新增数据 (Create)
新增数据通常分为三步:创建实例、添加到 Session、提交事务。
python
from sqlalchemy.ext.asyncio import AsyncSession
from .models import User
from .schemas import UserCreate
async def create_user(db: AsyncSession, user_in: UserCreate):
# 1. 创建模型实例(同步,不需要 await)
new_user = User(
name=user_in.name,
email=user_in.email,
hashed_password="fake_password" # 实际开发中需加密
)
# 2. 将对象添加到数据库会话(同步操作,不需要 await)
db.add(new_user)
# 3. 提交事务(异步,需要 await)
await db.commit()
# 4. 刷新对象,获取数据库生成的字段(如自增 ID)(异步,需要 await)
await db.refresh(new_user)
return new_user
如果需要提前拿到数据库生成的id等字段,才需要refresh。
而且如果使用注入依赖的方法,且依赖会自动提交事务,就需要flush才能返回又自动生成的字段的对象。
像是下面这样写:
python
@router.post("/users")
async def create_user(user_in: UserCreate, db: AsyncSession = Depends(get_db)):
new_user = User(name=..., email=..., hashed_password=...)
db.add(new_user)
await db.flush() # ① 发送 INSERT,id 被数据库生成
await db.refresh(new_user) # ② 从数据库重新读取对象,id 被填充
return new_user # 此时 new_user.id 有值
2.更新数据 (Update)
更新通常有两种方式:先查询后修改(最常用)或 直接批量更新。
方式 A:先查询,再修改属性(推荐用于单个对象)
python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
async def update_user_email(db: AsyncSession, user_id: int, new_email: str):
stmt = select(User).where(User.id == user_id)
# 2. 执行查询,获取结果
result = await db.execute(stmt)
db_user = result.scalar_one_or_none() # 返回单个对象或 None
# 3. 如果存在,修改属性
if db_user:
db_user.email = new_email
# 4. 提交事务(自动检测变更并生成 UPDATE)
await db.commit()
# 5. 刷新对象(如有必要,例如获取数据库默认值)
await db.refresh(db_user)
return db_user
方式 B:使用 update() 语句(批量或高性能)
语法如下:
python
stmt = update(表模型).where(条件).values(要更新的字段=新值)
python
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
async def update_multiple_users(db: AsyncSession):
stmt = (
update(User)
.where(User.name == "张三")
.values(name="老张")
)
await db.execute(stmt) # 异步执行更新语句
await db.commit() # 异步提交事务
3.删除数据
删除同样分为"查询后删除"和"批量删除"。
方式 A:先查询后删除
python
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
async def delete_user(db: AsyncSession, user_id: int) -> bool:
# 1. 构建查询语句,查找用户
stmt = select(User).where(User.id == user_id)
result = await db.execute(stmt)
db_user = result.scalar_one_or_none() # 获取单个对象或 None
# 2. 如果存在,删除
if db_user:
await db.delete(db_user) # 异步删除(标记删除)
await db.commit() # 提交事务
return True
return False
方式 B:使用 delete() 语句
python
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import AsyncSession
async def bulk_delete_users(db: AsyncSession) -> int:
stmt = delete(User).where(User.id > 100)
result = await db.execute(stmt) # 异步执行
await db.commit() # 异步提交
return result.rowcount # 可选:返回被删除的行数
总结
🚀 FastAPI ORM 操作速查表
| 操作类型 | 核心函数/方法 | 关键返回处理 | 注意点 |
|---|---|---|---|
| 查询对象 | select(Model) |
.scalars().all() / .first() |
第一个参数决定了 scalars() 拿到的类型 |
| 条件过滤 | .where() |
支持 & 和 ` |
` 符号 |
| 聚合统计 | func.count() / avg() |
.mappings().all() |
必须 配合 .label(),否则 JSON 键名会乱码 |
| 分页 | .limit().offset() |
配合 .order_by() |
没有排序的分页是"逻辑炸弹",结果会随机跳变 |
| 新增/删除 | db.add() / db.delete() |
await db.commit() |
不 commit 不生效 ;想拿新 ID 就 refresh |
| 更新 | obj.attr = value |
await db.commit() |
推荐"先查后改",这样能触发 SQLAlchemy 的属性监听 |