想象一下,你正在开发一个超酷的Web应用,比如一个博客平台或者一个在线商店。你的API不仅要能把数据(比如文章列表、商品信息)展示给用户,更要聪明到能理解用户的各种"小心思":用户可能想看最新的文章、搜索特定关键词的商品、或者只想看当前页面的10条数据等等。
这篇文章就是要带你解锁FastAPI中使用SQLModel 来实现这些高级查询功能的秘籍,主要关注两大神技:查询参数(Query Parameters) 和 数据库联接(Joins)。
1. 查询参数:让你的API听懂用户的"指令"
什么是查询参数?简单说,它就像是你给API发出的"指令",通过URL告诉服务器你想要什么样的数据。
举个例子:
- 你想看第2页的文章,每页显示5篇:
http://localhost:8000/posts?skip=5&limit=5 - 你想搜索标题包含"夏天"的文章:
http://localhost:8000/posts?search=夏天
这里的 ?skip=5&limit=5 和 ?search=夏天 就是查询参数。它们以问号 (?) 开头,用 key=value 的形式表示,多个参数之间用与号 (&) 连接。
查询参数能干啥?
- 过滤 (Filtering):就像筛子一样,根据特定条件筛选数据。例如,只看某个作者的文章。
- 排序 (Sorting):按发布时间、价格等给结果排序(虽然本文不细讲,但原理类似)。
- 分页 (Pagination) :当数据太多时,一页一页地看。
limit(每页几条)和skip(跳过几条,即(页码-1) * 每页条数)是分页的黄金搭档。
FastAPI + SQLModel 如何实现查询参数?
在FastAPI中,这简直太简单了!你只需要在你的API接口函数(路径操作函数)里定义参数,并给它们加上类型提示。FastAPI会自动把URL中的查询参数"抓"出来,并转换成你想要的类型。
python
# --- main.py (或者你的API路由文件,比如 posts_router.py) ---
from typing import Optional, List
from fastapi import APIRouter, Depends
from sqlmodel import Session, select, func # 导入SQLModel的核心组件
from .. import schemas, models, database, oauth2 # 假设你的项目结构
router = APIRouter(
prefix="/posts", # 给这个路由下的所有路径加上 /posts 前缀
tags=["Posts"] # 在API文档中分组显示
)
# 假设你的 database.py 里面有 get_db 函数来获取数据库会话
# from .database import get_db
@router.get("/", response_model=List[schemas.PostOut]) # 注意这里的 response_model
async def get_all_posts(
db: Session = Depends(database.get_db), # 依赖注入,获取数据库会话
limit: int = 10, # 默认每页10条
skip: int = 0, # 默认从第0条开始(跳过0条)
search: Optional[str] = None # 可选的搜索词,默认为None
# current_user: models.User = Depends(oauth2.get_current_user) # 如果需要登录认证
):
"""
获取所有帖子,支持分页和搜索,并统计每个帖子的点赞数。
"""
# 基础查询语句,选择帖子模型和统计投票数
statement = (
select(
models.Post, # 我们要查询帖子本身
func.count(models.Vote.post_id).label("votes") # 还要统计每个帖子的投票数,并命名为 "votes"
)
# 关键:使用 .join() 来连接 Post 表和 Vote 表
# isouter=True 表示使用 LEFT OUTER JOIN,这样即使帖子没有投票,也会被查询出来,投票数为0
.join(models.Vote, models.Vote.post_id == models.Post.id, isouter=True)
# 按帖子ID分组,这样 func.count 才能正确统计每个帖子的投票
.group_by(models.Post.id)
)
# 如果用户提供了搜索词,就在查询语句中加入过滤条件
if search:
# models.Post.title.contains(search) 表示标题包含搜索词即可
# 对于大小写不敏感的搜索,可以使用 .ilike(f"%{search}%"),但要注意数据库是否支持
statement = statement.where(models.Post.title.contains(search))
# 应用分页参数
statement = statement.limit(limit).offset(skip)
# 执行查询
# db.exec(statement).all() 会返回一个结果列表,
# 列表中的每个元素是一个元组 (Post对象, votes数量)
results = db.exec(statement).all()
# FastAPI 会根据 response_model=List[schemas.PostOut] 自动处理这个 results
# schemas.PostOut 需要被定义成能接收 Post 对象和 votes 数量的结构
return results
代码讲解:
@router.get("/", response_model=List[schemas.PostOut]):- 定义了一个GET请求的接口,路径是
/posts/(因为router有prefix="/posts")。 response_model=List[schemas.PostOut]告诉FastAPI,这个接口返回的数据会是一个列表,列表里每个元素的结构都符合schemas.PostOut这个我们接下来要定义的"响应模型"。
- 定义了一个GET请求的接口,路径是
async def get_all_posts(...):db: Session = Depends(database.get_db): FastAPI的依赖注入。get_db函数(通常在database.py中定义)会提供一个数据库会话db,让我们能和数据库打交道。limit: int = 10,skip: int = 0,search: Optional[str] = None: 这些就是我们的查询参数!FastAPI会从URL中解析它们。比如用户访问/posts?limit=5&search=python,那么limit就是5,search就是"python"。
statement = select(models.Post, func.count(models.Vote.post_id).label("votes")) ...:- 这里开始构建SQLModel的查询语句。
select()是起点。 - 我们不仅要
models.Post(帖子本身),还要func.count(models.Vote.post_id).label("votes")(统计每个帖子的投票数,并把这个统计结果命名为votes)。func来自sqlalchemy(SQLModel底层使用它),提供了数据库函数如COUNT,SUM等。
- 这里开始构建SQLModel的查询语句。
.join(models.Vote, models.Vote.post_id == models.Post.id, isouter=True):- 这是多表查询的关键!我们要把
Post表和Vote表连接起来。 - 连接条件是
models.Vote.post_id == models.Post.id(投票表中的帖子ID等于帖子表中的帖子ID)。 isouter=True表示使用 左外连接 (LEFT OUTER JOIN) 。这意味着即使一个帖子没有任何投票记录,它仍然会出现在结果中,其votes计数会是0。如果不用isouter=True(即默认的内连接 INNER JOIN),那么没有投票的帖子就不会被查出来。
- 这是多表查询的关键!我们要把
.group_by(models.Post.id):- 因为我们用了聚合函数
func.count(),所以需要告诉数据库按什么来分组统计。这里我们按帖子的ID (models.Post.id) 分组,这样就能得到每个帖子的投票数。
- 因为我们用了聚合函数
if search: statement = statement.where(models.Post.title.contains(search)):- 如果用户在URL中提供了
search参数,我们就在查询语句中添加一个where条件,筛选出标题 (models.Post.title) 包含 (contains) 搜索词的帖子。
- 如果用户在URL中提供了
statement = statement.limit(limit).offset(skip):- 将分页参数应用到查询语句上。
results = db.exec(statement).all():- 执行最终构建好的查询语句,并获取所有结果。
results会是一个列表,每个元素是(Post对象, votes数量)这样的元组。
- 执行最终构建好的查询语句,并获取所有结果。
return results:- 直接返回这个
results。FastAPI会很智能地根据我们之前定义的response_model=List[schemas.PostOut]来把这个元组列表转换成符合PostOut结构的JSON列表返回给客户端。
- 直接返回这个
2. 数据库表关系与联接:让数据"手拉手"
在真实世界中,数据很少是孤零零存在的。比如:
- 一个用户(User) 可以发布多篇帖子(Post)。(一对多关系)
- 一个用户(User) 可以给多篇帖子(Post) 点赞,一篇帖子(Post) 也可以被多个用户(User) 点赞。(多对多关系,通常通过一个中间表,如 投票(Vote) 表来实现)
SQLModel 如何定义这些关系?
SQLModel 的美妙之处在于它同时是Pydantic模型(用于数据校验和序列化)和数据库表模型。
python
# --- models.py ---
from datetime import datetime
from typing import Optional, List # 注意这里也需要 List
from sqlmodel import SQLModel, Field, Relationship # 导入SQLModel的核心组件
from sqlalchemy import text # 用于设置数据库级别的默认值
# 用户模型
class User(SQLModel, table=True):
__tablename__ = "users" # 表名
id: Optional[int] = Field(default=None, primary_key=True)
email: str = Field(unique=True, index=True, nullable=False) # 邮箱唯一且建立索引
password: str = Field(nullable=False)
created_at: datetime = Field(
default_factory=datetime.utcnow, # Python级别默认值
sa_column_kwargs={"server_default": text("now()")} # 数据库级别默认值
)
# 定义关系:一个用户可以有多篇帖子
# "Post" 是关联的模型类名(字符串形式避免循环导入问题)
# back_populates="owner" 指向 Post 模型中名为 "owner" 的关系属性
posts: List["Post"] = Relationship(back_populates="owner")
# 帖子模型
class Post(SQLModel, table=True):
__tablename__ = "posts" # 表名
id: Optional[int] = Field(default=None, primary_key=True)
title: str = Field(index=True, nullable=False) # 标题也加个索引,方便搜索
content: str = Field(nullable=False)
published: bool = Field(default=True, sa_column_kwargs={"server_default": text("true")})
created_at: datetime = Field(
default_factory=datetime.utcnow,
sa_column_kwargs={"server_default": text("now()")}
)
# 外键:这篇帖子的作者是谁
# foreign_key="users.id" 指向 users 表的 id 字段
# nullable=False 表示每篇帖子都必须有作者
owner_id: int = Field(foreign_key="users.id", nullable=False)
# 定义关系:这篇帖子的作者 (User 对象)
# back_populates="posts" 指向 User 模型中名为 "posts" 的关系属性
owner: Optional[User] = Relationship(back_populates="posts")
# 投票模型 (用于用户给帖子点赞)
class Vote(SQLModel, table=True):
__tablename__ = "votes"
# 复合主键:一个用户对一个帖子只能投一票
user_id: int = Field(foreign_key="users.id", primary_key=True, ondelete="CASCADE")
post_id: int = Field(foreign_key="posts.id", primary_key=True, ondelete="CASCADE")
# ondelete="CASCADE" 表示如果关联的 User 或 Post 被删除,这条 Vote 记录也会被自动删除
代码讲解 (models.py):
class User(SQLModel, table=True):: 定义一个User模型,它既是SQLModel(数据库表模型),也是Pydantic模型。table=True表示它对应数据库中的一张表。id: Optional[int] = Field(default=None, primary_key=True): 定义id字段,是主键,可选(数据库会自动生成)。posts: List["Post"] = Relationship(back_populates="owner"):- 这是定义关系 的关键!它告诉SQLModel,一个
User对象可以关联多个Post对象。 List["Post"]表示这个posts属性是一个Post对象的列表。用字符串"Post"是为了避免Python在解析时可能遇到的循环导入问题。back_populates="owner"指的是,在Post模型那边,有一个名为owner的属性也定义了与User的关系,并且它们是相互关联的。
- 这是定义关系 的关键!它告诉SQLModel,一个
class Post(SQLModel, table=True):: 类似地定义Post模型。owner_id: int = Field(foreign_key="users.id", nullable=False):- 这是外键 字段。它存储的是对应
User表中某条记录的id。 foreign_key="users.id"明确指定了它引用users表的id列。nullable=False表示一篇帖子必须有一个作者。
- 这是外键 字段。它存储的是对应
owner: Optional[User] = Relationship(back_populates="posts"):- 与
User模型中的posts关系相呼应。它表示一个Post对象关联一个User对象(即帖子的作者)。
- 与
class Vote(SQLModel, table=True)::user_id: int = Field(foreign_key="users.id", primary_key=True, ...)post_id: int = Field(foreign_key="posts.id", primary_key=True, ...)- 这两个字段共同构成了复合主键 ,确保一个用户对一篇帖子只能投票一次。它们也都是外键,分别引用
users表和posts表。 ondelete="CASCADE": 这是一个数据库层面的约束。如果一个用户被删除了,那么他所有的投票记录也会自动被删除。同理,如果一个帖子被删除了,关于这个帖子的所有投票记录也会被删除。这有助于保持数据的整洁和一致性。
为何需要联接 (Joins)?
想象一下,你想显示一篇帖子的详细信息,同时还要显示发帖人的用户名,以及这篇帖子有多少个赞。
- 帖子标题、内容在
Post表。 - 发帖人用户名在
User表(通过Post.owner_id关联)。 - 点赞数需要统计
Vote表中对应post_id的记录数量。
如果不用联接,你可能需要:
- 查询
Post表获取帖子信息。 - 根据
Post.owner_id再去User表查询用户信息。 - 根据
Post.id再去Vote表统计点赞数。
这样查询次数太多,效率低下!联接 (Join) 就是为了解决这个问题,它允许你在一次数据库查询中,把来自不同但相关联的表的数据"拼接"在一起。
3. 玩转 SQLModel:多表联合查询实战
我们回到之前的 get_all_posts 接口,它已经用到了联接:
python
# --- main.py (或者你的API路由文件) ---
# ... (省略之前的导入和router定义) ...
# 先定义好我们的响应模型 schemas.py
# --- schemas.py ---
from datetime import datetime
from pydantic import BaseModel # SQLModel本身就是Pydantic模型,但有时为了清晰或特定场景会用BaseModel
from .models import User # 导入你的SQLModel模型 User
class UserOut(BaseModel): # 用于在响应中展示的用户信息,不包含密码等敏感信息
id: int
email: str
created_at: datetime
class Config:
from_attributes = True # Pydantic V2中的配置,允许从ORM对象属性创建模型实例
class PostResponseBase(BaseModel): # 帖子基础信息
id: int
title: str
content: str
published: bool
created_at: datetime
owner_id: int
owner: UserOut # 嵌套UserOut,显示作者信息
class Config:
from_attributes = True
class PostOut(BaseModel): # 最终API返回的帖子结构,包含帖子信息和投票数
Post: PostResponseBase # 帖子自身的详细信息
votes: int # 这个帖子的投票总数
class Config:
from_attributes = True # 确保可以从 (Post对象, votes数量) 这样的元组构造
# ... 回到你的 API 路由文件 ...
@router.get("/", response_model=List[schemas.PostOut])
async def get_all_posts(
db: Session = Depends(database.get_db),
limit: int = 10,
skip: int = 0,
search: Optional[str] = None
):
statement = (
select(
models.Post, # 选择 Post 模型对象
func.count(models.Vote.post_id).label("votes") # 计算投票数,并命名为 "votes"
)
.join(models.Vote, models.Post.id == models.Vote.post_id, isouter=True) # 左外连接 Vote 表
.group_by(models.Post.id) # 按帖子ID分组
)
if search:
statement = statement.where(models.Post.title.contains(search))
statement = statement.limit(limit).offset(skip)
# results_from_db 的结构是 List[Tuple[Post, int]]
# 例如:[(<Post object at 0x...>, 5), (<Post object at 0x...>, 0)]
results_from_db = db.exec(statement).all()
# FastAPI 会自动根据 response_model 将 results_from_db 转换
# 它会尝试把每个 (Post, int) 元组 构造成一个 schemas.PostOut 对象
# Post对象会被用来填充 PostOut.Post 字段 (内部的 PostResponseBase 会通过 Post.owner 自动加载用户信息)
# int 会被用来填充 PostOut.votes 字段
return results_from_db
# 如果你想创建一个帖子,同时关联当前登录的用户作为作者
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.PostResponseBase) # 注意响应模型
def create_post(
post_data: schemas.PostCreate, # PostCreate 是一个只包含 title, content, published 的Pydantic模型
db: Session = Depends(database.get_db),
current_user: models.User = Depends(oauth2.get_current_user) # 获取当前登录用户
):
# **post_data.model_dump() 把 Pydantic 模型转为字典
# owner_id=current_user.id 把当前用户的ID设为帖子的作者ID
new_post = models.Post(**post_data.model_dump(), owner_id=current_user.id)
db.add(new_post)
db.commit()
db.refresh(new_post) # 刷新new_post对象,使其包含数据库生成的值(如ID, created_at)
# new_post.owner 会自动通过 relationship 加载关联的 User 对象
# FastAPI 会根据 response_model=schemas.PostResponseBase 自动序列化
return new_post
代码讲解 (schemas.py 和 create_post):
-
schemas.py的作用:UserOut: 定义了当我们需要在API响应中显示用户信息时,只显示哪些字段(比如不显示密码)。PostResponseBase: 定义了帖子的基本输出信息,并且它嵌套了owner: UserOut,这意味着在返回帖子信息时,会自动把作者的UserOut信息也包含进去。PostOut: 这是我们get_all_posts接口最终的响应结构。它包含一个Post字段(类型是PostResponseBase,即帖子详情加作者信息)和一个votes字段(帖子的点赞数)。class Config: from_attributes = True: 这个配置(在Pydantic V2中,旧版是orm_mode = True)非常重要。它允许Pydantic模型直接从数据库对象(ORM对象,比如我们的SQLModel实例)的属性来创建实例。比如,如果一个PostSQLModel对象有title和content属性,PostResponseBase就能直接用这些属性来填充自己。
-
get_all_posts返回results_from_db:- 当
db.exec(statement).all()执行后,results_from_db是一个列表,每个元素是(Post模型实例, 投票数)这样的元组。 - FastAPI看到
response_model=List[schemas.PostOut],它会尝试把每个元组(p, v)转换成一个schemas.PostOut(Post=p, votes=v)的实例。 schemas.PostOut的Post字段类型是schemas.PostResponseBase。由于from_attributes = True,PostResponseBase会从p(Post模型实例) 中读取属性。- 特别地,
PostResponseBase中的owner: UserOut字段,会因为PostSQLModel模型中定义了owner: Optional[User] = Relationship(...)而被自动填充。SQLModel(或底层的SQLAlchemy)会在需要时加载关联的User对象,然后UserOut再从这个User对象中提取信息。
- 当
-
create_post接口:post_data: schemas.PostCreate:PostCreate是一个Pydantic模型,用于接收创建帖子时客户端发来的数据(比如只有title,content)。current_user: models.User = Depends(oauth2.get_current_user): 假设你有一个oauth2.py文件,里面的get_current_user函数会验证JWT token并返回当前登录的UserSQLModel对象。new_post = models.Post(**post_data.model_dump(), owner_id=current_user.id):post_data.model_dump()将PostCreatePydantic模型转换成一个字典。**将这个字典解包,作为参数传递给models.Post的构造函数。owner_id=current_user.id明确设置了这篇新帖子的作者是当前登录用户。
db.add(new_post),db.commit(),db.refresh(new_post): 这是标准的SQLModel(或SQLAlchemy)操作,将新对象添加到会话、提交到数据库、然后刷新对象以获取数据库生成的值。return new_post: 返回创建好的models.Post对象。因为response_model=schemas.PostResponseBase,FastAPI会自动将这个Post对象(包括其通过relationship加载的owner信息)转换为PostResponseBase格式的JSON响应。
关键点总结:
- SQLModel 让模型定义更简单:一个类同时搞定Pydantic校验和数据库表结构。
Relationship很强大 :在SQLModel模型中定义好Relationship,SQLModel(底层SQLAlchemy)就能帮你处理很多关联数据的加载。select().join()是多表查询的核心 :用它来连接不同的表。isouter=True用于左外连接,确保即使没有关联数据(如帖子没有投票)主表数据也能查出来。func.count()和group_by()用于聚合统计:比如统计每个帖子的投票数。- FastAPI 的
response_model和 Pydantic 的from_attributes = True是天作之合:它们能让你轻松地将数据库查询结果(甚至是包含关联对象的复杂结果)转换成规范的JSON API响应。
总结
通过本文,你已经掌握了如何在FastAPI中利用SQLModel的强大功能,通过查询参数 让API更灵活,通过数据库联接 和Relationship高效查询和展示关联数据。
你学会了:
- 在FastAPI接口中定义查询参数(
limit,skip,search)。 - 使用SQLModel的
select()语句,并通过.where(),.limit(),.offset()来应用这些参数。 - 在SQLModel模型中通过
Field(foreign_key=...)和Relationship(back_populates=...)定义表间关系。 - 使用
select().join()进行多表查询,特别是用isouter=True实现左外连接。 - 使用
func.count().label()和.group_by()进行聚合统计。 - 设计合适的Pydantic响应模型(如
schemas.PostOut),并利用FastAPI的response_model特性自动转换查询结果。
现在,你已经具备了构建更复杂、更强大、用户体验更好的API的能力!动手试试,你会发现SQLModel和FastAPI的组合是如此优雅和高效。