You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

FastAPI接口使用Generator实现分页未达预期的问题排查及正确方案咨询

FastAPI分页问题:生成器为何一次性返回所有数据?怎么正确实现?

为什么你的生成器接口会一次性返回所有数据?

兄弟,这是FastAPI的默认行为导致的。你以为用yield就能流式返回数据,但实际上FastAPI会把生成器的所有产出全部收集起来,打包成一个列表后一次性返回给客户端。

简单说,当你在路径操作函数里用生成器时,FastAPI并不会把每个yield的内容单独发送给客户端,而是等生成器跑完所有迭代,把所有page子列表拼成一个大列表,再作为完整的HTTP响应返回。这就是你看到二维列表的原因——每个yield page都被放进了外层列表里。

正确实现分页的两种方式

方式一:传统分页(推荐,性能更好)

这是Web开发里最常用的分页方案,让前端传递页码和每页条数,后端只查询并返回对应页的数据,不用把所有数据加载到内存里。

步骤如下:

  • 给接口添加page(页码,默认1)和limit(每页条数,默认你之前的1000)参数
  • 核心是在数据库查询阶段就做分页,而不是先把所有数据捞出来再切分(这会浪费内存,数据量大的时候直接崩)
  • 返回结果里带上总条数、当前页码、每页条数,方便前端做分页控件

示例代码:

from fastapi import HTTPException, Query
from sqlalchemy.orm import Session

@overlaps_router.get("/query")
def query_overlaps(
    query: str,
    testdrive_id_list: List[str] = Query(None),
    predecessor: bool = False,
    page: int = Query(1, ge=1),  # 页码,最小1
    limit: int = Query(1000, ge=1, le=10000),  # 每页条数,限制范围
    db: Session = Depends(get_db)
):
    try:
        # CRUD层直接做分页查询,返回总条数和当前页数据
        total_records, results = crud.query_overlaps_paginated(
            query=query,
            db=db,
            testdrive_id_list=testdrive_id_list,
            page=page,
            limit=limit
        )
        
        # 处理数据(和你之前的逻辑一致)
        if predecessor:
            processed_data = calculate_predecessor(results)
        else:
            processed_data = process_result(results)
        
        # 返回标准分页响应
        return {
            "total": total_records,
            "current_page": page,
            "page_size": limit,
            "data": processed_data
        }
    except Exception as e:
        logger.exception(f"Query failed: {str(e)}")
        raise HTTPException(status_code=500, detail=f"查询失败:{str(e)}")

对应的CRUD层分页实现(以SQLAlchemy为例):

def query_overlaps_paginated(query, db, testdrive_id_list, page, limit):
    # 构建基础查询
    query_obj = db.query(OverlapModel).filter(OverlapModel.content.contains(query))  # 你的查询条件
    
    # 添加testdrive_id过滤
    if testdrive_id_list:
        query_obj = query_obj.filter(OverlapModel.testdrive_id.in_(testdrive_id_list))
    
    # 计算总条数
    total = query_obj.count()
    # 分页查询:offset是跳过的条数,limit是取的条数
    paginated_results = query_obj.offset((page - 1) * limit).limit(limit).all()
    
    return total, paginated_results

方式二:流式响应(适合超大批量数据逐步返回)

如果确实需要客户端逐步接收数据(比如导出超大文件),可以用FastAPI的StreamingResponse包装生成器,让HTTP响应流式返回。

注意:流式返回JSON需要处理格式,比如每个分页数据作为单独的JSON对象,用换行分隔,不然前端解析会出问题。

示例代码:

from fastapi.responses import StreamingResponse
import json

@overlaps_router.get("/query/stream")
def query_overlaps_stream(
    query: str,
    testdrive_id_list: List[str] = Query(None),
    predecessor: bool = False,
    db: Session = Depends(get_db)
):
    try:
        # 先获取所有数据(如果数据量太大,不建议这么做,最好还是数据库分页流式查)
        results = crud.query_overlaps(query, db, testdrive_id_list)
        if predecessor:
            data_to_paginate = calculate_predecessor(results)
        else:
            data_to_paginate = process_result(results)
        
        limit = int(environment.env_variables["PAGINATION_OFFSET"])
        
        # 定义生成器函数,每次返回一个JSON字符串(带换行)
        def data_generator():
            page = []
            for datum in data_to_paginate:
                if len(page) < limit:
                    page.append(datum)
                else:
                    yield json.dumps(page) + "\n"
                    page = []
            if page:
                yield json.dumps(page) + "\n"
        
        # 返回流式响应,指定媒体类型为换行分隔的JSON
        return StreamingResponse(data_generator(), media_type="application/x-ndjson")
    except Exception as e:
        logger.exception(f"Stream query failed: {str(e)}")
        raise HTTPException(status_code=500, detail=f"流式查询失败:{str(e)}")

客户端接收流式响应时,需要逐行解析每个JSON字符串,而不是一次性解析整个响应。

全局变量存储数据的方案是否合理?

完全不合理,绝对不要这么做! 原因有三个:

  1. 并发冲突:多个用户同时调用接口时,会共享这个全局变量。比如用户A刚把数据放进data_to_paginate,用户B的请求就把它覆盖了,导致A拿到的是B的数据,完全混乱。
  2. 内存泄漏:如果数据量很大(比如几万条),全局变量会一直占用内存,直到应用重启,时间久了会导致服务器内存耗尽。
  3. 数据不一致:如果数据库里的数据更新了,全局变量里的旧数据不会自动同步,用户会拿到过时的结果。

总结

  • 生成器被FastAPI一次性收集返回是默认行为,不是生成器本身的问题
  • 绝大多数场景下,用传统数据库分页是最优解,性能好、逻辑清晰
  • 流式响应只适合超大批量数据的场景,而且要注意客户端的解析逻辑
  • 全局变量存储分页数据是大坑,绝对要避免

内容的提问来源于stack exchange,提问作者tkarahan

火山引擎 最新活动