关键词搜索与分页
本章你将学会用 SQLAlchemy 实现联合搜索,并用 offset/limit 实现分页。
ilike 大小写不敏感搜索
实例
# 单字段搜索
posts = db.query(Post).filter(Post.title.ilike(f'%{keyword}%')).all()
# 多字段联合搜索(OR 关系)
from sqlalchemy import or_
posts = db.query(Post).filter(
or_(
Post.title.ilike(f'%{keyword}%'),
Post.summary.ilike(f'%{keyword}%')
)
).all()
posts = db.query(Post).filter(Post.title.ilike(f'%{keyword}%')).all()
# 多字段联合搜索(OR 关系)
from sqlalchemy import or_
posts = db.query(Post).filter(
or_(
Post.title.ilike(f'%{keyword}%'),
Post.summary.ilike(f'%{keyword}%')
)
).all()
offset/limit 分页
Flask-SQLAlchemy 提供了便捷的 .paginate() 方法。
FastAPI 使用的是原生 SQLAlchemy,需要用 offset + limit 手动实现分页。
| 分页方式 | Flask-SQLAlchemy | FastAPI(原生 SQLAlchemy) |
|---|---|---|
| 获取当前页数据 | .paginate(page, per_page).items | .offset(skip).limit(per_page).all() |
| 总记录数 | pagination.total | query.count() |
| 总页数 | pagination.pages | 手动计算 math.ceil(total / per_page) |
分页计算公式
实例
import math
from fastapi import Query
def get_pagination(page: int = 1, size: int = 6):
"""
计算分页参数
page:当前页码(从 1 开始)
size:每页条数
"""
skip = (page - 1) * size # 跳过的记录数
return skip, size
# 使用示例
skip, limit = get_pagination(page=2, size=6)
# skip = 6, limit = 6 → 跳过前 6 条,取第 7-12 条
from fastapi import Query
def get_pagination(page: int = 1, size: int = 6):
"""
计算分页参数
page:当前页码(从 1 开始)
size:每页条数
"""
skip = (page - 1) * size # 跳过的记录数
return skip, size
# 使用示例
skip, limit = get_pagination(page=2, size=6)
# skip = 6, limit = 6 → 跳过前 6 条,取第 7-12 条
完整的搜索 + 分页视图
实例
# 文件路径:routers/posts.py(扩展 index 路由)
from sqlalchemy import or_
from math import ceil
from fastapi import APIRouter, Depends, Request, Query
@router.get("/", name="index")
def index(
request: Request,
category: str | None = Query(None),
q: str | None = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
size: int = Query(6, ge=1, le=50, description="每页条数"),
db: Session = Depends(get_db)
):
"""首页:分类筛选 + 关键词搜索 + 分页"""
posts_query = db.query(Post).order_by(Post.created_at.desc())
# 分类筛选
if category:
posts_query = posts_query.join(Category).filter(Category.slug == category)
# 关键词搜索
if q:
keyword = f'%{q.strip()}%'
posts_query = posts_query.filter(
or_(
Post.title.ilike(keyword),
Post.summary.ilike(keyword)
)
)
# 分页
total = posts_query.count()
total_pages = ceil(total / size)
skip = (page - 1) * size
posts = posts_query.offset(skip).limit(size).all()
return templates.TemplateResponse("index.html", {
"request": request,
"posts": posts,
"categories": db.query(Category).all(),
"category_slug": category or "",
"keyword": q or "",
"page": page,
"total_pages": total_pages,
"total": total
})
from sqlalchemy import or_
from math import ceil
from fastapi import APIRouter, Depends, Request, Query
@router.get("/", name="index")
def index(
request: Request,
category: str | None = Query(None),
q: str | None = Query(None, description="搜索关键词"),
page: int = Query(1, ge=1, description="页码"),
size: int = Query(6, ge=1, le=50, description="每页条数"),
db: Session = Depends(get_db)
):
"""首页:分类筛选 + 关键词搜索 + 分页"""
posts_query = db.query(Post).order_by(Post.created_at.desc())
# 分类筛选
if category:
posts_query = posts_query.join(Category).filter(Category.slug == category)
# 关键词搜索
if q:
keyword = f'%{q.strip()}%'
posts_query = posts_query.filter(
or_(
Post.title.ilike(keyword),
Post.summary.ilike(keyword)
)
)
# 分页
total = posts_query.count()
total_pages = ceil(total / size)
skip = (page - 1) * size
posts = posts_query.offset(skip).limit(size).all()
return templates.TemplateResponse("index.html", {
"request": request,
"posts": posts,
"categories": db.query(Category).all(),
"category_slug": category or "",
"keyword": q or "",
"page": page,
"total_pages": total_pages,
"total": total
})
Query(1, ge=1)中的ge是 greater than or equal 的缩写。FastAPI 的查询参数支持完整的数值约束:ge、le、gt、lt。如果用户传入 page=0,FastAPI 会自动返回 422 错误。
模板中的分页导航
实例
<!-- 在 index.html 的文章列表下方追加分页导航 -->
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="/?page={{ page - 1 }}&category={{ category_slug }}&q={{ keyword }}">← 上一页</a>
{% endif %}
<span class="page-info">第 {{ page }} 页 / 共 {{ total_pages }} 页</span>
{% if page < total_pages %}
<a href="/?page={{ page + 1 }}&category={{ category_slug }}&q={{ keyword }}">下一页 →</a>
{% endif %}
</div>
{% endif %}
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="/?page={{ page - 1 }}&category={{ category_slug }}&q={{ keyword }}">← 上一页</a>
{% endif %}
<span class="page-info">第 {{ page }} 页 / 共 {{ total_pages }} 页</span>
{% if page < total_pages %}
<a href="/?page={{ page + 1 }}&category={{ category_slug }}&q={{ keyword }}">下一页 →</a>
{% endif %}
</div>
{% endif %}
本章小结
本章你实现了列表页的实用功能:ilike 大小写不敏感搜索、or_() 多字段联合搜索、offset/limit 手动分页(替代 Flask 的 paginate)、查询参数用 Query() 约束范围。
搜索 + 分类筛选 + 分页可以任意组合,条件不会丢失。
