FastAPI + SQLAlchemy ORM 实战:实现查询操作

4 阅读10分钟

在FastAPI与SQLAlchemy ORM的协同开发中,查询操作是数据库交互的核心环节。基于已创建的书籍表,我们可通过ORM提供的简洁API,无需编写原生SQL,就能高效实现各类查询需求。本文将严格按照规范的文件命名逻辑,分5个核心场景拆解查询操作,每个场景对应独立的实现代码与思路,全程结合可直接运行的示例,仅围绕查询操作核心展开,确保内容简洁、可落地,助力开发者快速掌握各类查询技巧。

一、通用前置配置(所有查询场景共用)

所有查询操作均依赖统一的基础配置,包括数据库引擎创建、模型定义、会话管理及建表操作,确保查询能正常连接数据库并映射到书籍表。以下是完整的通用配置代码,可直接复制运行。

from datetime import datetime
from fastapi import FastAPI, Depends
from sqlalchemy import DateTime, func, String, Float, select
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

# 初始化FastAPI应用
app = FastAPI(debug=True)

# 1. 创建异步数据库引擎(适配MySQL,修改为自身数据库配置)
ASYNC_DATABASE_URL = "mysql+aiomysql://root:654321@localhost:3306/book_manage?charset=utf8"
async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,          # 输出SQL日志,便于调试查询语句
    pool_size=8,        # 连接池活跃连接数,适配查询并发
    max_overflow=15     # 额外允许的临时连接数
)

# 2. 定义模型基类(包含公共字段)
class Base(DeclarativeBase):
    # 公共字段:创建时间、修改时间,自动填充
    create_time: Mapped[datetime] = mapped_column(
        DateTime,
        insert_default=func.now(),
        default=func.now,
        comment="创建时间"
    )
    update_time: Mapped[datetime] = mapped_column(
        DateTime,
        insert_default=func.now(),
        default=func.now,
        onupdate=func.now(),
        comment="修改时间"
    )

# 3. 定义书籍表模型(查询操作的核心载体)
class Book(Base):
    __tablename__ = "book"  # 数据表名,与数据库中表一致

    id: Mapped[int] = mapped_column(primary_key=True, comment="书籍主键ID")
    book_title: Mapped[str] = mapped_column(String(255), comment="书籍标题")
    book_author: Mapped[str] = mapped_column(String(100), comment="书籍作者")
    book_price: Mapped[float] = mapped_column(Float, comment="书籍售价")
    book_publisher: Mapped[str] = mapped_column(String(255), comment="出版社")
    book_pages: Mapped[int] = mapped_column(comment="书籍页数")

# 4. 建表操作(确保书籍表已存在,为查询提供数据载体)
async def create_book_table():
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

# 启动FastAPI时自动建表
@app.on_event("startup")
async def startup():
    await create_book_table()

# 5. 创建异步会话工厂与依赖项(查询操作必须通过会话实现)
AsyncSessionLocal = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession,
    expire_on_commit=False  # 提交后会话不过期,提升查询效率
)

# 会话依赖项:自动管理会话的创建、提交、回滚与关闭
async def get_db_session():
    async with AsyncSessionLocal() as session:
        try:
            yield session  # 将会话传递给路由查询函数
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

# 基础测试接口,验证配置是否正常
@app.get("/")
async def index():
    return {"status": "success", "message": "查询环境配置完成,可进行各类查询操作"}

二、分场景查询实现

场景1:基础查询数据

基础查询主要实现“单条数据查询”和“全部数据查询”,是最常用、最基础的查询场景,通过简单API即可快速获取书籍数据,无需添加复杂条件。

# 1. 基础查询:根据主键ID查询单本书籍(最常用)
@app.get("/book/query_single/{book_id}")
async def query_single_book(book_id: int, db: AsyncSession = Depends(get_db_session)):
    # 核心:通过db.get()方法,根据主键快速查询单条记录
    book = await db.get(Book, book_id)
    # 处理查询不到数据的情况,返回友好提示
    return book if book else {"message": "未查询到对应ID的书籍"}

# 2. 基础查询:查询所有书籍数据
@app.get("/book/query_all")
async def query_all_books(db: AsyncSession = Depends(get_db_session)):
    # 构建查询语句:查询书籍表所有记录
    query_stmt = select(Book)
    # 执行查询,获取结果集
    result = await db.execute(query_stmt)
    # 提取所有书籍记录,返回列表
    all_books = result.scalars().all()
    return all_books if all_books else {"message": "书籍表暂无数据"}

# 3. 基础查询:查询第一条书籍数据
@app.get("/book/query_first")
async def query_first_book(db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(Book)
    result = await db.execute(query_stmt)
    # 提取查询结果中的第一条数据
    first_book = result.scalars().first()
    return first_book if first_book else {"message": "书籍表暂无数据"}

关键说明:基础查询核心依赖db.get()select(Book),前者适用于主键查询,简洁高效;后者适用于批量查询,搭配scalars().all()scalars().first()提取结果。

场景2:带基础条件查询

实际开发中,常需要根据具体条件筛选数据,如根据价格、出版社、页数等字段筛选书籍,通过where()方法添加查询条件,实现精准筛选。

# 1. 条件查询:根据出版社筛选书籍
@app.get("/book/query_by_publisher")
async def query_book_by_publisher(publisher: str, db: AsyncSession = Depends(get_db_session)):
    # 构建带条件的查询语句:筛选指定出版社的书籍
    query_stmt = select(Book).where(Book.book_publisher == publisher)
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到该出版社的书籍"}

# 2. 条件查询:根据价格范围筛选书籍(大于等于指定价格)
@app.get("/book/query_by_price")
async def query_book_by_price(min_price: float, db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(Book).where(Book.book_price >= min_price)
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到符合价格条件的书籍"}

# 3. 条件查询:根据页数筛选书籍(小于指定页数)
@app.get("/book/query_by_pages")
async def query_book_by_pages(max_pages: int, db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(Book).where(Book.book_pages < max_pages)
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到符合页数条件的书籍"}

关键说明:where()方法中可直接使用字段与值的比较关系(==、>=、<等),实现基础条件筛选,多个条件可叠加(后续场景详解)。

场景3:模糊查询与与非逻辑查询

当需要模糊匹配(如根据作者姓名模糊搜索)或多条件组合(与、或、非)筛选时,可使用like()in_()及逻辑运算符(&、|、~)实现,满足更灵活的查询需求。

# 1. 模糊查询:根据作者姓名模糊搜索(支持前缀、后缀、包含模糊)
@app.get("/book/query_by_author_like")
async def query_book_by_author_like(author_keyword: str, db: AsyncSession = Depends(get_db_session)):
    # 模糊匹配:作者姓名包含关键词(%表示任意个字符)
    query_stmt = select(Book).where(Book.book_author.like(f"%{author_keyword}%"))
    # 若需前缀匹配(以关键词开头),使用 f"{author_keyword}%"
    # 若需后缀匹配(以关键词结尾),使用 f"%{author_keyword}"
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到匹配作者的书籍"}

# 2. 包含查询:查询ID在指定列表中的书籍(in_()方法)
@app.get("/book/query_by_id_in")
async def query_book_by_id_in(db: AsyncSession = Depends(get_db_session)):
    target_ids = [2, 5, 8, 10]  # 目标ID列表,可根据需求修改
    query_stmt = select(Book).where(Book.id.in_(target_ids))
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到指定ID的书籍"}

# 3. 与逻辑查询:同时满足两个条件(出版社为某值 且 价格大于指定值)
@app.get("/book/query_and_condition")
async def query_book_and_condition(publisher: str, min_price: float, db: AsyncSession = Depends(get_db_session)):
    # 多个条件用 & 连接,注意每个条件需用括号包裹
    query_stmt = select(Book).where((Book.book_publisher == publisher) & (Book.book_price > min_price))
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到同时满足条件的书籍"}

# 4. 或逻辑查询:满足任意一个条件(作者包含关键词 或 页数小于指定值)
@app.get("/book/query_or_condition")
async def query_book_or_condition(author_keyword: str, max_pages: int, db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(Book).where((Book.book_author.like(f"%{author_keyword}%")) | (Book.book_pages< max_pages))
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "未查询到满足条件的书籍"}

# 5. 非逻辑查询:排除指定条件的书籍(排除某出版社的书籍)
@app.get("/book/query_not_condition")
async def query_book_not_condition(publisher: str, db: AsyncSession = Depends(get_db_session)):
    # ~ 表示非逻辑,排除指定出版社的书籍
    query_stmt = select(Book).where(~(Book.book_publisher == publisher))
    result = await db.execute(query_stmt)
    books = result.scalars().all()
    return books if books else {"message": "除指定出版社外,无其他书籍"}

关键说明:模糊查询使用like(),搭配%(任意字符)、_(单个字符);包含查询使用in_(),传入列表;逻辑运算需用&(与)、|(或)、~(非),且每个条件需用括号包裹。

场景4:聚合查询

聚合查询用于对书籍表数据进行统计分析,如统计书籍总数、平均价格、最高价格、总页数等,通过func模块提供的聚合函数实现,无需手动计算。

# 1. 聚合查询:统计书籍总数
@app.get("/book/aggregate/count")
async def count_books(db: AsyncSession = Depends(get_db_session)):
    # func.count() 统计指定字段的非空记录数,通常用主键ID
    query_stmt = select(func.count(Book.id))
    result = await db.execute(query_stmt)
    # 聚合查询结果用 scalar() 提取单个数值
    book_count = result.scalar()
    return {"书籍总数": book_count}

# 2. 聚合查询:查询书籍平均价格
@app.get("/book/aggregate/avg_price")
async def avg_book_price(db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(func.avg(Book.book_price))
    result = await db.execute(query_stmt)
    avg_price = result.scalar()
    # 保留2位小数,提升显示效果
    return {"书籍平均价格": round(avg_price, 2) if avg_price else 0.0}

# 3. 聚合查询:查询书籍最高价格与最低价格
@app.get("/book/aggregate/max_min_price")
async def max_min_book_price(db: AsyncSession = Depends(get_db_session)):
    # 同时查询最高价格和最低价格
    query_stmt = select(func.max(Book.book_price), func.min(Book.book_price))
    result = await db.execute(query_stmt)
    max_price, min_price = result.first()
    return {
        "书籍最高价格": max_price if max_price else 0.0,
        "书籍最低价格": min_price if min_price else 0.0
    }

# 4. 聚合查询:统计书籍总页数
@app.get("/book/aggregate/sum_pages")
async def sum_book_pages(db: AsyncSession = Depends(get_db_session)):
    query_stmt = select(func.sum(Book.book_pages))
    result = await db.execute(query_stmt)
    total_pages = result.scalar()
    return {"书籍总页数": total_pages if total_pages else 0}

关键说明:聚合函数需导入func模块,常用聚合函数包括count(计数)、avg(平均)、max(最大)、min(最小)、sum(求和),结果用scalar()first()提取。

场景5:分页查询

当书籍表数据量较大时,一次性查询所有数据会影响性能,分页查询可将数据分批次返回,通过offset()(跳过指定条数)和limit()(限制返回条数)实现,适配大量数据场景。

# 分页查询:根据页码和每页条数,返回分页数据
@app.get("/book/paginate")
async def paginate_books(
    page: int = 1,          # 页码,默认第1页
    page_size: int = 5,     # 每页条数,默认5条
    db: AsyncSession = Depends(get_db_session)
):
    # 计算跳过的记录数:(当前页码 - 1) * 每页条数
    skip = (page - 1) * page_size
    
    # 构建分页查询语句:跳过skip条,返回page_size条
    query_stmt = select(Book).offset(skip).limit(page_size)
    result = await db.execute(query_stmt)
    paginate_books = result.scalars().all()
    
    # 统计总条数,用于前端分页显示(可选,提升体验)
    total = await db.execute(select(func.count(Book.id)))
    total_count = total.scalar()
    
    return {
        "分页信息": {
            "当前页码": page,
            "每页条数": page_size,
            "总条数": total_count,
            "总页数": (total_count + page_size - 1) // page_size  # 向上取整计算总页数
        },
        "分页数据": paginate_books if paginate_books else []
    }

关键说明:offset(skip)用于跳过前skip条记录,limit(page_size)用于限制本次返回的记录数;统计总条数可帮助前端计算总页数,提升用户体验,按需添加即可。

三、查询操作注意事项

  1. 所有查询操作必须通过异步会话实现,不可直接操作引擎,避免出现连接泄露或阻塞事件循环;

  2. 模糊查询中,%和_的使用需注意:%表示任意个字符,_表示单个字符,根据需求选择合适的匹配方式;

  3. 多条件逻辑查询时,每个条件需用括号包裹,避免逻辑优先级错误;

  4. 聚合查询结果需用scalar()first()提取,不可用scalars().all(),否则会返回异常;

  5. 分页查询中,skip的计算需准确,避免出现页码错乱或数据重复/遗漏的情况。

总结

通过SQLAlchemy ORM的查询API,我们无需编写复杂的原生SQL,即可实现高效、规范的查询操作,大幅提升开发效率。在实际开发中,可根据具体业务需求,灵活组合各类查询方法,实现精准的数据筛选与统计。