FastAPI+TortoiseORM 异步开发员工管理接口|CRUD + 多条件分页 踩坑实录
FastAPITortoiseORM 异步开发员工管理接口CRUD 多条件分页 踩坑实录前言最近一直在练习 FastAPI TortoiseORM 异步后端开发搭建了一套简易的员工管理接口包含部门、员工、员工档案三组关联数据。表关系设计部门与员工一对多员工和员工档案一对一。所有接口实现基础 CRUD员工列表支持多条件模糊搜索 分页。写代码的时候图快踩了不少典型坑比如分层混乱、分页统计错误、异常处理不规范等。把完整代码和踩坑经验整理出来当作学习记录也给正在入门这套异步技术栈的小伙伴做参考。项目采用经典分层结构router路由层接收请求、参数转发、统一返回结果service业务层所有数据库操作、业务校验逻辑modelsTortoise ORM 数据表模型schemasPydantic 请求校验结构体个人习惯路由层尽量轻量化绝不直接写查询语句业务逻辑全部下沉到 Service。一、路由代码 day03_router.pyimportdatetimeimportmathfromfastapiimportAPIRouter,Queryfromtortoise.expressionsimportQfromapp.models.day03importDepartment,Employee,EmployeeProfilefromapp.schemas.day03importCreateDepartment,UpdateDepartment,CreateEmployee,UpdateEmployee,CreateEmployeeProfile,UpdateEmployeeProfilefromapp.services.day03importDay03Service day03_routerAPIRouter(prefix/day03,tags[day03])day03_router.get(/department)asyncdefdepartment():dataawaitDay03Service.department()return{msg:ok,code:200,data:data}day03_router.post(/create_department)asyncdefcreate_department(department:CreateDepartment):awaitDay03Service.create_department(department)return{msg:ok,code:200}day03_router.put(/update_department/{id})asyncdefupdate_department(id:int,department:UpdateDepartment):awaitDay03Service.update_department(id,department)return{msg:ok,code:200}day03_router.delete(/delete_department/{id})asyncdefdelete_department(id:int):awaitDay03Service.delete_department(id)return{msg:ok,code:200}day03_router.get(/employee)asyncdefemployee(name:strNone,department:strNone,status:strNone,page:intQuery(1,ge1,title页码,description页码),size:intQuery(1,ge1,le10,title每页条数,description每页条数)):data_list,page_infoawaitDay03Service.employee(name,department,status,page,size)return{msg:ok,code:200,data:{data_list:data_list,page_info:page_info}}day03_router.post(/create_employee)asyncdefcreate_employee(employee:CreateEmployee):awaitDay03Service.create_employee(employee)return{msg:ok,code:200}day03_router.put(/update_employee/{id})asyncdefupdate_employee(id:int,employee:UpdateEmployee):awaitDay03Service.update_employee(id,employee)return{msg:ok,code:200}day03_router.delete(/delete_employee/{id})asyncdefdelete_employee(id:int):awaitDay03Service.delete_employee(id)return{msg:ok,code:200}day03_router.get(/profile)asyncdefprofile():dataawaitDay03Service.profile()return{msg:ok,code:200,data:data}day03_router.get(/employee_profile/{id})asyncdefemployee_profile(id:int):infoawaitDay03Service.employee_profile(id)return{msg:ok,code:200,data:info}day03_router.post(/create_profile)asyncdefcreate_profile(employee_profile:CreateEmployeeProfile):awaitDay03Service.create_employee_profile(employee_profile)return{msg:ok,code:200}day03_router.put(/update_profile/{id})asyncdefupdate_profile(id:int,employee_profile:UpdateEmployeeProfile):awaitDay03Service.update_employee_profile(id,employee_profile)return{msg:ok,code:200}day03_router.delete(/delete_profile/{id})asyncdefdelete_profile(id:int):awaitDay03Service.delete_employee_profile(id)return{msg:ok,code:200}二、业务服务层 day03_service.py核心业务逻辑全部放在这里最初版本存在不少问题下文统一梳理坑点。importmathfromfastapiimportQueryfromtortoise.expressionsimportQfromapp.models.day03importDepartment,Employee,EmployeeProfilefromapp.schemas.day03importCreateDepartment,UpdateDepartment,CreateEmployee,UpdateEmployee,CreateEmployeeProfile,UpdateEmployeeProfileclassDay03Service:staticmethodasyncdefdepartment():dataawaitDepartment.all()returndatastaticmethodasyncdefcreate_department(department:CreateDepartment):dataawaitDepartment.get_or_none(namedepartment.name)ifdataisnotNone:raiseException(部门已存在)awaitDepartment.create(**dict(department))return200staticmethodasyncdefupdate_department(id:int,department:UpdateDepartment):dataawaitDepartment.get_or_none(idid)ifdataisNone:raiseException(部门不存在)department_dictdepartment.dict(exclude_unsetTrue)awaitDepartment.filter(idid).update(**department_dict)return200staticmethodasyncdefdelete_department(id:int):dataawaitDepartment.get_or_none(idid)ifdataisNone:raiseException(部门不存在)ifawaitEmployee.filter(departmentdata).exists():raiseException(部门下有员工无法删除)awaitDepartment.filter(idid).delete()return200staticmethodasyncdefemployee(name:strNone,department:strNone,status:strNone,page:intQuery(1,ge1,title页码,description页码),size:intQuery(1,ge1,le10,title每页条数,description每页条数)):offset(page-1)*size queryEmployee.all()ifname:queryquery.filter(Q(name__icontainsname))ifdepartment:queryquery.filter(departmentdepartment)ifstatus:queryquery.filter(statusstatus)dataawaitquery.prefetch_related(department).offset(offset).limit(size)data_list[]foriindata:data_info{id:i.id,name:i.name,emp_no:i.emp_no,gender:i.gender,age:i.age,phone:i.phone,email:i.email,hire_date:i.hire_date,salary:i.salary,department:i.department.name,status:i.status,created_at:i.created_at,updated_at:i.updated_at}data_list.append(data_info)page_info{page:page,size:size,total:awaitEmployee.all().count(),total_page:math.ceil(awaitEmployee.all().count()/size)}returndata_list,page_infostaticmethodasyncdefcreate_employee(employee:CreateEmployee):datadict(employee)data[department_id]data.pop(department)awaitEmployee.create(**dict(data))return200staticmethodasyncdefupdate_employee(id:int,employee:UpdateEmployee):dataawaitEmployee.get_or_none(idid)ifdataisNone:raiseException(员工不存在)employee_dictemployee.dict(exclude_unsetTrue)employee_dict[department_id]employee_dict.pop(department)awaitEmployee.filter(idid).update(**employee_dict)return200staticmethodasyncdefdelete_employee(id:int):dataawaitEmployee.get_or_none(idid)ifdataisNone:raiseException(员工不存在)awaitEmployee.filter(idid).delete()awaitEmployeeProfile.filter(employeeid).delete()return200staticmethodasyncdefprofile():dataawaitEmployeeProfile.all()returndatastaticmethodasyncdefemployee_profile(id:int):dataawaitEmployeeProfile.get_or_none(employeeid)ifdataisNone:raiseException(员工档案不存在)infoawaitEmployeeProfile.filter(employeeid)returninfostaticmethodasyncdefcreate_employee_profile(employee_profile:CreateEmployeeProfile):dataawaitEmployeeProfile.get_or_none(employeeemployee_profile.employee)ifdataisnotNone:raiseException(员工档案已存在)infodict(employee_profile)info[employee_id]info.pop(employee)awaitEmployeeProfile.create(**dict(info))return200staticmethodasyncdefupdate_employee_profile(id:int,employee_profile:UpdateEmployeeProfile):dataawaitEmployeeProfile.get_or_none(idid)ifdataisNone:raiseException(员工档案不存在)employee_profile_dictemployee_profile.dict(exclude_unsetTrue)awaitEmployeeProfile.filter(idid).update(**employee_profile_dict)return200staticmethodasyncdefdelete_employee_profile(id:int):dataawaitEmployeeProfile.get_or_none(idid)ifdataisNone:raiseException(员工档案不存在)awaitEmployeeProfile.filter(idid).delete()return200三、开发自测发现的问题重点写完基础功能进行接口测试陆续发现不少隐藏 bug这里记录下来。Query 依赖不能写在 Service 层最开始图省事直接把Query()定义在 service 方法参数上。后来才意识到Query是 FastAPI 路由专用依赖对象只能在接口函数使用。Service 属于通用业务层如果后续单元测试、被其他地方调用直接会报错。解决方案分页参数 page、size 仅保留在 routerservice 只接收普通 int 变量。分页统计总数逻辑错误原始代码统计总数写法totalawaitEmployee.all().count()这里有两个问题统计的是整张表所有员工没有拼接搜索条件用户筛选数据后分页总数不会变化前端页码错乱。连续两次执行 count ()发起两次 SQL 查询浪费数据库性能。解决方案count 调用基于拼接完条件的 query 对象await query.count()用变量接收一次结果复用。3. 直接抛出原生 Exception返回格式不统一代码里直接 raise Exception(“提示文字”)。FastAPI 捕获原生异常默认返回 500 错误页面无法和项目统一 JSON 返回格式。优化方向自定义业务异常类搭配全局异常处理器捕获后统一返回 {“code”:400,“msg”:“xxx”}。4. 删除员工手动清理档案存在数据不一致风险删除员工时先删员工记录再手动删除员工档案。如果两条 SQL 执行中途程序崩溃会出现员工删除成功、档案残留脏数据。两种优化方案任选其一使用in_transaction()事务包裹删除逻辑保证原子性ORM 模型外键定义 on_deleteONDELETE.CASCADE数据库层面自动级联删除。5. employee_profile 重复查询数据库dataawaitEmployeeProfile.get_or_none(employeeid)infoawaitEmployeeProfile.filter(employeeid)先查询一次判断数据是否存在紧接着再次查询同一条记录产生多余 IO。员工和档案是一对一关系可以简化查询逻辑。6. Pydantic v2 中 dict () 方法存在警告我本地环境使用 Pydantic v2.dict() 官方已经标记为过时运行时会输出警告。新项目建议统一替换为 .model_dump()。7. 外键字段转换代码重复新增、修改员工时需要手动将department重命名为department_id。多处重复相同逻辑后期可以封装公共方法简化代码。四、后续优化规划增加事务管理保证增删改操作的数据原子性封装通用分页工具类避免每个列表接口重复编写分页代码实现自定义业务异常 全局异常捕获使用 Pydantic 模型序列化 ORM 对象替代手动循环组装字典增加业务字段校验例如薪资不能为负数将重复的 “数据存在性校验” 抽取成公共工具函数。总结FastAPI 搭配 TortoiseORM 开发异步 CRUD 接口效率很高但是分层规范一定要守住。很多新手习惯直接在路由写数据库查询短期开发速度快后期迭代、维护成本极高。异步 ORM 和传统同步 ORM 写法上有不少区别需要留意避免不必要的重复查询。分页、联表查询、外键映射都是新手高频踩坑点。如果你也在学习这套技术栈可以拿这份代码当作基础模板进行改造。遇到问题欢迎评论区一起交流。

相关新闻

Python+Flask构建轻量级服务器监控系统

Python+Flask构建轻量级服务器监控系统

1. 项目背景与核心需求最近在管理公司600多台服务器时,经常遇到需要快速确认哪些服务器离线的情况。传统方式是通过SSH逐个连接或手动ping测试,效率极低且容易遗漏。于是决定用Python开发一个轻量级的Web监控系统,实现以下核心功能&#xff1…

2026/7/22 2:34:11阅读更多 →
DeepL翻译插件终极指南:3分钟安装,畅享专业级网页翻译体验

DeepL翻译插件终极指南:3分钟安装,畅享专业级网页翻译体验

DeepL翻译插件终极指南:3分钟安装,畅享专业级网页翻译体验 【免费下载链接】deepl-chrome-extension A DeepL Translator Chrome extension 项目地址: https://gitcode.com/gh_mirrors/de/deepl-chrome-extension 在浏览外文网页时,你…

2026/7/22 2:34:11阅读更多 →
C++动态链接库(DLL)运行时加载:Boost.DLL实战与插件系统开发

C++动态链接库(DLL)运行时加载:Boost.DLL实战与插件系统开发

1. 项目概述:为什么需要从DLL中动态获取函数? 在Windows平台的C开发中,动态链接库(DLL)是模块化设计和代码复用的基石。我们通常有两种使用DLL中函数的方式:一种是 隐式链接 ,在编译时通过 …

2026/7/22 2:34:11阅读更多 →
LangChain 零基础快速上手:从 Hello World 到智能文档问答助手

LangChain 零基础快速上手:从 Hello World 到智能文档问答助手

一、引言:大模型浪潮下的开发困境 随着 ChatGPT 的爆火,大模型(Large Language Model, LLM)已成为开发者工具箱中的新宠。然而,当我们兴奋地拿到 OpenAI API Key,准备大干一场时,却常常陷入这样…

2026/7/22 4:24:28阅读更多 →
NLP语义分析与结构优化在内容创作中的应用

NLP语义分析与结构优化在内容创作中的应用

1. 项目概述:语义分析与结构优化的专业价值"专业版付费服务提供更深层次的语义分析与段落结构优化建议"这个标题背后,反映的是当前内容创作领域对高质量文本处理的刚性需求。作为从业十年的文字工作者,我深刻体会到:普通…

2026/7/22 4:24:28阅读更多 →
C++字符串处理实战:从“斯诺登密码”题解看映射、分割与组合算法

C++字符串处理实战:从“斯诺登密码”题解看映射、分割与组合算法

1. 项目概述:从“斯诺登密码”到算法实战最近在洛谷上刷题,又看到了P1603这道经典题目——“斯诺登的密码”。乍一看标题挺唬人,又是“斯诺登”又是“密码破译”,感觉像是什么高深的谍战技术。但实际做下来,你会发现它…

2026/7/22 4:24:28阅读更多 →
YOLO11模型零停机切换实战:架构设计与生产优化

YOLO11模型零停机切换实战:架构设计与生产优化

1. YOLO11模型版本切换的核心挑战在实时目标检测系统中,模型版本切换从来都不是简单的文件替换。当我在2023年负责某智慧园区的人车识别系统升级时,就深刻体会到了这一点。当时我们需要从YOLOv8升级到YOLO11,系统要求724小时不间断运行&#…

2026/7/22 4:24:28阅读更多 →
深入解析eHRPWM核心寄存器:从时基同步到死区配置的嵌入式电机驱动实战

深入解析eHRPWM核心寄存器:从时基同步到死区配置的嵌入式电机驱动实战

1. 项目概述与核心价值在嵌入式电机驱动和数字电源设计的圈子里,eHRPWM(增强型高分辨率脉宽调制器)模块是绕不开的核心。它远不止是一个简单的“开关”信号发生器,而是一个高度可编程、具备精密时序控制能力的数字引擎。很多工程师…

2026/7/22 4:24:28阅读更多 →
HDVPSS GRPX模块:嵌入式图形叠加与混合的硬件加速实践

HDVPSS GRPX模块:嵌入式图形叠加与混合的硬件加速实践

1. 项目概述:深入理解HDVPSS的图形处理核心在嵌入式视频处理系统的开发中,图形叠加与混合是绕不开的核心需求。无论是安防监控中的OSD(屏幕显示)信息叠加,还是医疗影像中的标注与测量,亦或是工业HMI界面中的…

2026/7/22 4:22:27阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 0:53:59阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 0:53:59阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 0:53:59阅读更多 →
中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业小程序开发公司怎么选:预算、上手和售后避坑指南

中小企业做小程序,最常见的矛盾是预算有限,但又不希望功能太单薄;没有技术团队,但又希望后续能自己运营;想快速上线,又担心隐性收费和售后失联。选型时如果只看“低价套餐”或“案例数量”,很容…

2026/7/22 0:01:17阅读更多 →
GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

GEO优化如何沉淀长期内容资产?广拓时代谈AI搜索时代的内容ROI

企业做营销,最怕钱花完了,资产没有留下。 效果广告能带来一段时间的曝光,但预算停止后,流量往往也随之停止。短视频内容可能在几天内冲高,也可能很快沉下去。AI搜索时代,企业需要重新思考一个问题&#xff…

2026/7/22 0:01:17阅读更多 →
Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复

Agent 终态判定:何时该停止思考、给出最终回复 一、你的 Agent 在"再想想"的循环里绕了 12 轮,用户已经关窗口了 Agent 与人最大的区别是:人知道什么时候该停下来给答案,Agent 会一直"想"下去。你给 Agent 接…

2026/7/22 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/21 22:53:50阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/21 18:53:30阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/21 18:53:30阅读更多 →