FastAPI实战笔记(二) 数据处理

二、数据处理

SQLAlchemy数据库操作

模型定义与连接建立

py 复制代码
from sqlalchemy import (
    create_engine,
)
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
    sessionmaker,
)
# 数据库连接字符串
# 首次连接时将自动创建test.db文件
DATABASE_URL = "sqlite:///./test.db"

# 创建引擎
engine = create_engine(DATABASE_URL)

# 定义数据库服务基类
# 需要继承 DeclarativeBase
class Base(DeclarativeBase):
    pass


# 定义模型
class User(Base):
    # 对应数据库中的user表 
    __tablename__ = "user"
    # 每个类属性声明了字段的数据类型
    id: Mapped[int] = mapped_column(
        primary_key=True,
    )
    name: Mapped[str]
    email: Mapped[str]

# 通过引擎创建数据库表
Base.metadata.create_all(bind=engine)

# 建立数据库连接
# sessionmaker函数生成会话工厂
# autocommit与autoflush参数设为False,意味着需手动提交事务并管理数据刷新时机
SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine,
)

CRUD

py 复制代码
from fastapi import Depends, FastAPI, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session

from database import SessionLocal, User

 # 确保每个请求创建新会话,并在请求结束时关闭
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


app = FastAPI()


class UserBody(BaseModel):
    name: str
    email: str


@app.get("/users")
# db: Session 是参数类型注解 表示db是一个数据库会话
# Depends(get_db) 是 FastAPI 的依赖注入,自动提供数据库连接
def read_users(db: Session = Depends(get_db)):
    users = db.query(User).all()
    return users


# 创建新用户
@app.post("/user")
def add_new_user(
    user: UserBody, db: Session = Depends(get_db)
):
    new_user = User(name=user.name, email=user.email)
    # 将对象添加到当前会话
    db.add(new_user)
    # 提交事务
    db.commit()
    # 从数据库重新加载对象
    db.refresh(new_user)
    return new_user

# 读取特定用户
@app.get("/user")
def get_user(
    user_id: int, db: Session = Depends(get_db)
):
    user = (
        db.query(User)
        .filter(User.id == user_id)
        .first()
    )
    if user is None:
        raise HTTPException(
            # 用户不存在则返回404
            status_code=404, detail="User not found"
        )

    return user


# 更新用户
@app.post("/user/{user_id}")
def update_user(
    user_id: int,
    user: UserBody,
    db: Session = Depends(get_db),
):
    db_user = (
        db.query(User)
        .filter(User.id == user_id)
        .first()
    )
    if db_user is None:
        raise HTTPException(
            status_code=404, detail="User not found"
        )
    db_user.name = user.name
    db_user.email = user.email
    db.commit()
    db.refresh(db_user)
    return db_user

# 删除用户
@app.delete("/user")
def delete_user(
    user_id: int, db: Session = Depends(get_db)
):
    db_user = (
        db.query(User)
        .filter(User.id == user_id)
        .first()
    )
    if db_user is None:
        raise HTTPException(
            status_code=404, detail="User not found"
        )
    db.delete(db_user)
    db.commit()
    return {"detail": "User deleted"}
复制代码
# 在交互式文档操作
http://localhost:8000/docs

集成MongoDB实现NoSQL数据存储

模型定义与连接建立

py 复制代码
from pymongo import MongoClient

client = MongoClient()
# 等价于client = MongoClient("mongodb://localhost:27017")
database = client.mydatabase
# 定义集合 相当于对表的引用
user_collection = database["users"]

CRUD

py 复制代码
from bson import ObjectId
from fastapi import FastAPI, HTTPException
from pydantic import (
    BaseModel,
    EmailStr,
    field_validator,
)
# 导入数据库连接变量
from database import user_collection

app = FastAPI()


class Tweet(BaseModel):
    content: str
    hashtags: list[str]


class User(BaseModel):
    name: str
    # 使用Pydantic邮箱验证器
    email: EmailStr
    age: int
    # tweets可以是一个 Tweet 列表,也可以是 None, 默认值是 None 
    tweets: list[Tweet] | None = None 
	
    # 自定义了一个关于年龄的验证器
    @field_validator("age")
    def validate_age(cls, value):
        if value < 18 or value > 100:
            raise ValueError(
                "Age must be between 18 and 100"
            )
        return value

# 返回所有用户
@app.get("/users")
def read_users() -> list[User]:
    return [user for user in user_collection.find()]


# 继承自 User
class UserResponse(User):
    # 新增字段 id
    id: str


# 创建新用户
@app.post("/user")
def create_user(user: User) -> UserResponse:
    result = user_collection.insert_one(
        # MongoDB 只能存储 BSON(Binary JSON)格式 不支持 Python 特有的对象类型
        # 所以使用 model_dump 将 Pydantic 模型转换为 Python 字典
        # exclude_none=True:排除值为 None 的字段
        user.model_dump(exclude_none=True)
    )
    # result有两个属性:inserted_id 和 acknowledged
    user_response = UserResponse(
        # **user.model_dump()将原始用户数据解包为关键字参数
        id=str(result.inserted_id), **user.model_dump()
    )
#     等价于
#     user_response = UserResponse(
#     id=str(result.inserted_id),
#     name=user.name,      # 从解包的字典中获取
#     email=user.email,    # 从解包的字典中获取  
#     age=user.age         # 从解包的字典中获取
# )
    return user_response


# 读取用户数据
@app.get("/user")
def get_user(user_id: str) -> UserResponse:
    db_user = user_collection.find_one(
        {
            # MongoDB 中 _id 存储的是 ObjectId 对象,不是字符串
            # # 将用户传入的字符串ID转换为MongoDB的ObjectId对象
            "_id": ObjectId(user_id)
            if ObjectId.is_valid(user_id)
            else None
        }
    )
    if db_user is None:
        raise HTTPException(
            status_code=404, detail="User not found"
        )
    # 响应给客户端时需要将 ObjectId 转换为字符串格式
    db_user["id"] = str(db_user["_id"])
    return db_user

# FastAPI通过Pydantic模型自动处理序列化与反序列化。当端点返回Pydantic模型时,FastAPI自动将其序列化为JSON;当端点参数接收Pydantic模型时,框架将传入JSON数据反序列化为模型对象。
复制代码
# 在交互式文档操作
http://localhost:8000/docs

文件上传与下载

py 复制代码
import shutil
from pathlib import Path

from fastapi import (
    FastAPI,
    File,
    HTTPException,
    UploadFile,
)
from fastapi.responses import FileResponse

app = FastAPI()


# 接受上传文件并返回文件名
@app.post("/uploadfile")
async def upload_file(
    file: UploadFile = File(...),
):
    with open(
        # 在uploads目录以写入二进制模式打开新文件
        f"uploads/{file.filename}", "wb"
    ) as buffer:
        # 使用shutil.copyfileobj将UploadFile对象内容复制到新文件
        shutil.copyfileobj(file.file, buffer)
    return {"filename": file.filename}


# 实现文件下载服务
@app.get(
    "/downloadfile/{filename}",
    # 指定返回文件响应
    response_class=FileResponse,
)
async def download_file(filename: str):
    # 存在性检查
    if not Path(f"uploads/{filename}").exists():
        raise HTTPException(
            status_code=404,
            detail=f"file {filename} not found",
        )
    return FileResponse(
        path=f"uploads/{filename}", filename=filename
    )

异步数据处理

!CAUTION

后续需要对此处继续进行补充

py 复制代码
# main.py
import asyncio
import time

from fastapi import FastAPI

app = FastAPI()


# 创建同步阻塞端点
@app.get("/sync")
def read_sync():
    # 阻塞执行:time.sleep(2) 会阻塞整个线程2秒
    # 独占资源:在此期间,该线程无法处理其他请求
    # 串行处理:请求必须等待前一个请求完成
    time.sleep(2)
    return {
        "message": "同步阻塞端点"
    }


# 创建异步非阻塞端点
# asyncio库支持编写非阻塞代码:在等待I/O操作完成时暂停执行,完成后从断点恢复,全程不阻塞主线程
@app.get("/async")
async def read_async():
    # 非阻塞等待:await asyncio.sleep(2) 不会阻塞线程
    # 协作式并发:在等待期间,线程可以处理其他请求
    # 事件循环调度:等待期间释放控制权给事件循环
    await asyncio.sleep(2)
    return {
        "message": "异步非阻塞端点"
    }
py 复制代码
# timing_api_calls.py 进行调用
import asyncio
import time
from contextlib import contextmanager
from multiprocessing import Process

import uvicorn
from httpx import AsyncClient

from main import app


# 定义服务器启动函数
def run_server():
    uvicorn.run(app, port=8000, log_level="error")


# 通过上下文管理器启动服务器进程
# @contextmanager将普通函数转换为上下文管理器
@contextmanager
def run_server_in_process():
    # 创建一个新的进程对象并指定进程要执行的对象
    p = Process(target=run_server)
    # 启动新进程
    p.start()
    time.sleep(2)  # 等待服务器启动
    print("服务器已在独立进程中运行")
    yield
    p.terminate()


# 定义并发请求函数
async def make_requests_to_the_endpoint(
    n: int, path: str
):
    async with AsyncClient(
        base_url="http://localhost:8000"
    ) as client:
        tasks = (
            client.get(path, timeout=float("inf"))
            for _ in range(n)
        )

        await asyncio.gather(*tasks)


# 聚合操作至主函数并输出耗时
async def main(n: int = 10):
    with run_server_in_process():
        begin = time.time()
        await make_requests_to_the_endpoint(n, "/sync")
        end = time.time()
        print(
            f"完成 {n} 次同步端点请求耗时: {end - begin} 秒"
        )

        begin = time.time()
        await make_requests_to_the_endpoint(n, "/async")
        end = time.time()
        print(
            f"完成 {n} 次异步端点请求耗时: {end - begin} 秒"
        )


if __name__ == "__main__":
    asyncio.run(main(n=100))
# 完成 100 次同步端点请求耗时: 6.088550090789795 秒
# 完成 100 次异步端点请求耗时: 2.0794005393981934 秒

数据保护

核心安全实践清单

  • 数据验证与清洗
    使用Pydantic模型验证和清洗输入数据。确保数据符合预期格式与值范围,降低注入攻击或畸形数据风险。
    对输出至用户或日志的数据保持警惕,对敏感信息进行脱敏或匿名化处理,防止意外泄露。
  • 访问控制
    实施强健的访问控制机制,确保用户仅能访问其权限范围内的数据。可采用基于角色的访问控制(Role-Based Access Control, RBAC)、权限校验及完善的用户认证管理(详见第4章"认证与授权"中的RBAC方案)。
  • 安全通信
    通过HTTPS加密传输中数据,防止攻击者拦截应用收发的敏感信息。
  • 数据库安全
    确保数据库安全配置:使用加密连接、避免公开暴露数据库端口,并为数据库访问实施最小权限原则(principle of least privilege)。
  • 定期更新
    保持所有依赖项(包括FastAPI及其底层库)为最新版本,防范旧版软件中已发现的漏洞。

通用数据保护清单

  • 数据存储
    仅在必要时存储敏感数据。若无需保存信用卡号或身份证号,则坚决不存。必须存储时,确保数据加密且访问权限严格受限。
  • 数据传输
    传输敏感数据时保持高度谨慎。使用安全API,并确保所有交互的外部服务同样遵循安全最佳实践。
  • 数据保留与销毁
    制定明确的数据保留与销毁策略。数据不再需要时,通过安全方式彻底删除,确保备份或日志中不留痕迹。
  • 监控与日志
    实施监控机制检测异常访问模式或潜在入侵。但需谨慎记录日志:避免记录敏感数据,确保日志安全存储且仅限授权人员访问。
相关推荐
创新技术阁11 小时前
CryptoAiAdmin项目数据库表自动创建和初始化
后端·python·fastapi
懒人村杂货铺12 小时前
前端步入全栈第一步
前端·docker·fastapi
simon_skywalker1 天前
FastAPI实战笔记(一) 基本介绍与简单操作
fastapi
wang6021252181 天前
阿里云存储的一些简要概述
数据库·阿里云·fastapi
山沐与山1 天前
【设计模式】Python工厂模式与依赖注入:FastAPI的Depends到底在干嘛
python·设计模式·fastapi
祁思妙想2 天前
修改python后端项目名称:pycharm中fastapi框架的项目
ide·pycharm·fastapi
钱彬 (Qian Bin)2 天前
项目实践13—全球证件智能识别系统(内网离线部署大模型并调用)
数据库·postgresql·fastapi·ubuntu24.04·离线部署·qwen3大模型
安冬的码畜日常3 天前
【玩转 Postman 接口测试与开发2_020】(完结篇)DIY 实战:随书示例 API 项目本地部署保姆级搭建教程(含完整调试过程)
python·测试工具·django·接口测试·postman·fastapi·api项目
曲幽5 天前
FastAPI快速上手:请求与响应的核心玩法
python·fastapi·web·form·get·post