python-web最佳实践fastapi+sqlalchemy+DDD领域驱动模型
github上 贱贱的readme
🚧 [python-web-template]:Python敏捷开发项目模板! 🚀
兄弟们,江湖救急!是不是还在为每天写CRUD,搞得头昏脑涨?是不是还在为屎山代码,半夜惊醒?别慌,老哥我今天心情好,带你们整个狠活!
别再写屎山了,让我们开始屎上雕花吧.gogogo!
敏捷开发和屎山?
国内很多项目都追求敏捷开发,快速上线,但往往就变成了堆屎山。没办法,时间紧任务重嘛。但Python天生适合敏捷开发,语法简单,开发速度快,成本低。
但是!即使是屎山,咱也要尽量让他好看点,毕竟程序员每天都要面对它。接下来分享一些我多年来的经验,让你的项目既能快速上线,又能保持一定的优雅。
这玩意儿是啥
这是一个Python敏捷开发项目模板,是我多年码代码的血泪总结,不整那些花里胡哨的,直接上干货!让你快速撸起袖子搞项目,还能保持代码的优雅,不至于将来维护的时候想砍自己。
web框架选了fastapi,支持异步,速度快,配套多,自带pydantic验证,swagger文档,挺好.
灵感来源
我承认,确实借鉴了点DDD(领域驱动设计)的思路,但没完全照搬,毕竟Python的优势是啥?快速开发!咱不能搞得太复杂,把人都给整晕了。主要吸取了 Domain(领域)、Service(服务)、Repositories(仓库)、Event(事件)这些概念,目的是让业务逻辑更清晰,代码模块化,方便后期扩展。
Repository模式
这个模式我可太喜欢了!CRUD操作都封装好了,你直接调用就行,不用关心底层逻辑,就像用遥控器控制电视一样简单。还对一些通用逻辑做了处理,比如 is_deleted
这种软删除,你不用每次都写 where is_deleted=False
,直接用就行,方便得一批!
为啥不直接用DDD?
DDD确实牛逼,但用在小型项目上就有点杀鸡用牛刀了。现在都是ai时代了,用python开发项目的基本上更倾向于敏捷开发,快速上线才是王道。如果你要搞大型项目,那把Domain就按照 infra/seedwork
的模式复制到你的子应用就行了,seedwork
里放一些全局模块。
当然我也看到一些好的关于python的DDD设计项目 例如https://github.com/pgorecki/python-ddd, 不过这个repo不是基于async的web框架写的
有些好用的DDD的精髓借鉴到本项目的
DDD有几个点我是真心觉得好用:
-
领域隔离: 这玩意儿能帮你把业务拆分得明明白白,方便后续扩展。比如,
Domain A
下面有repo A
,管理table A
的 CRUD,Domain B
下面有repo B
,管理table B
的 CRUD。 如果A
服务要更新table B
,那就调用Domain B
对外暴露的接口,而不是直接操作repo B
或者table B
。明白不? -
事件(Event): 如果
Domain A
涉及到Domain B
的业务,别傻乎乎地把Domain B
引入到Domain A
里,然后组装参数,直接调用Domain B
。你应该用事件(Event)去通知Domain B
,让它自己去搞定。
举个栗子: 你是开发Domain A
的,你只需要通知Domain B
:“嘿,哥们,你该干活了”,至于Domain B
怎么干,跟你没半毛钱关系!这样分工明确,大家都轻松,扯皮的事也少。
这里很重要,敲黑板! 这也是团队协作的关键,责任边界要划分清晰,谁的锅谁背! 代码也是一样 -
核心思想: 各司其职,职责单一,不要越界!每个人管好自己的一亩三分地,别啥都想掺一脚。
有的人我都不稀得说,一个文件写路由,写认证,写参数校验,写查库,好家伙,大锅菜,一勺全烩了. 有的人倒是知道代码分层,什么MVC啥的,就是写着写着就乱了.
编程原则
网上一搜一大堆的原则,猛地一看没啥吊用,仔细看看,嗯,有点吊用,顺便贴在下面,随意看看:
1. SOLID 原则
SOLID 原则是面向对象编程和设计中的五个基本原则,由 Robert C. Martin (Uncle Bob) 提出。这些原则可以帮助我们创建更灵活、可维护和易于扩展的代码。
S - Single Responsibility Principle (单一职责原则)
描述: 一个类或模块应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
目的: 提高类的内聚性,减少类的耦合性,使其更易于理解、修改和测试。
O - Open/Closed Principle (开闭原则)
描述: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。也就是说,当需要增加新功能时,应该通过扩展现有代码来实现,而不是修改现有代码。
目的: 减少代码的修改带来的风险,提高代码的复用性和可维护性。
L - Liskov Substitution Principle (里氏替换原则)
描述: 子类型必须能够替换它们的基类型。换句话说,任何使用基类型的地方,都可以使用其子类型,而不会出现错误
目的: 保证代码的正确性和可扩展性,符合面向对象编程的继承原则。
I - Interface Segregation Principle (接口隔离原则)
描述: 客户端不应该被迫依赖它们不使用的接口。应该将大的接口拆分为更小的、更具体的接口,客户端只需要依赖它们需要的接口。
目的: 减少类的耦合性,提高系统的灵活性和可复用性。
D - Dependency Inversion Principle (依赖倒置原则)
描述:高层模块不应该依赖低层模块,两者都应该依赖抽象。
抽象不应该依赖细节,细节应该依赖抽象。
目的: 减少模块之间的耦合性,提高代码的灵活性和可维护性。
2. DRY 原则 (Don't Repeat Yourself - 不要重复自己)
描述: 避免代码重复。 如果你发现你正在写相同的代码多次,应该考虑将其提取到一个方法或类中,并进行复用
目的: 提高代码的可维护性和复用性,减少代码的错误。
3. YAGNI 原则 (You Aren't Gonna Need It - 你不需要它)
描述: 不要添加你认为将来可能会需要的特性,除非你现在确实需要它。
目的: 避免过度设计,减少不必要的复杂性,提高开发效率。
4. KISS 原则 (Keep It Simple, Stupid - 保持简单,傻瓜式)
描述: 保持你的代码简单易懂。 避免不必要的复杂性。
目的: 提高代码的可读性和可维护性,减少错误的发生。
5. Law of Demeter (迪米特法则) / Least Knowledge Principle (最少知识原则)
描述: 一个对象应该尽可能少地了解其他对象。 对象应该只与它的直接朋友通信。
目的: 减少对象之间的耦合性,提高代码的灵活性和可维护性。
6. Composition over Inheritance (组合优于继承)
描述: 尽量使用组合的方式来实现代码复用,而不是使用继承。
目的: 避免继承带来的耦合性和灵活性问题。
目录结构🌳
… (这里放你的目录结构,用 tree 命令生成)…
屎山雕花的建议
虽然都是建屎山,但我们的目标不一样,我们得在屎上雕花,哎,还能在屎上玩花活儿的才是我们真正拉屎人的精神。
项目开始阶段一定一定要
- 项目开始就定好代码规范,特别是多人开发的时候,毕竟人和人的思维/经历都不一样,一人一个风格真是的难受啊…
- 项目开始就要接入测试,不然真的就没机会了,铁子!!! 你记住,测试接入就这一次机会,用 pytest/coverage 搞起来啊!
**此项目到底帮你做了啥?**🎁
下面才是最重要的,让我们稍微正经一点[谁TM写个readme都烧的不行啊]
alembic
帮你管理数据库迁移,告别手动修改数据库的烦恼!poetry
帮你管理依赖,再也不用担心版本冲突了!local
, dev
, prod
三种环境,配置灵活切换,想怎么跑就怎么跑!pydantic
定义 API 请求模型,类型校验不在话下!我的开发理念
记住我的口号:能不引入第三方库,就TM不引入!自己能搞定的,绝不依赖别人,这才是真男人!所以项目的基础依赖比较简单
关于环境变量
其实,fastapi引用的pydantic,默认支持.env的文件的加载. 但我感觉.env的格式报好看,哈哈^.~
所以在不引入第三方库的原则下,我用了from configparser import ConfigParser 去解析xxx.ini的配置文件.对多个环境变量配置文件的管理
关于自定义模型
- API 层基于 pydantic 的自定义 scheme 做为请求 api 的基础模型
- 也就是说,从api请求进来->到数据库orm模型->到service层操作数据->到api响应出去,你无需手动进行模型转换
- 基础模型定义在src/infra/seedwork/domain/entities.py,api模型在src/infra/seedwork/api/api_base_scheme.py,定义写好了嗷,其他逻辑你自己加嗷,别懒嗷
关于Middleware
- AccessLogMiddleware会记录所有请求的信息,工作还是要留痕的,并自动转成curl信息,如果真的报错,直接拿着curl信息可以很方便的本地模拟
- AuthMiddleware认证中间件,先基于jwt实现了一个日常api的auth,然后再对oapi实现了一个api_key的认证, 如果你觉得不够用就自己写吧,铁汁. [对了,这里默认实现了api/oapi的两套路由,分别配了不同的认证auth]
- GlobalExceptionHandlerMiddleware统一异常处理,包括fastapi的异常和自定义的异常
- CORSMiddleware 跨域配置,具体的域名啥的,你自己搞呗
关于api请求
- 用contextVar来管理请求上下文,每个请求分配request_id,记录user信息,在请求的生命周期内全局可用
- api入参的模型都用pydantic进行参数校验啊,别在router/Service写那些基础校验了,哥们儿
- api的返回值使用统一的数据结构AppResponse,包括code, message, data, request_id. 以及统一的handler,ResponseHandler.success(entity) ResponseHandler.error(entity)
实际的序列化的逻辑交由fastapi的response model来实现即 @router.get(“/student/{student_id}”, response_model=AppResponse[StudentResp])
虽然不指定response model也行,但是pydantic model毕竟可以加一层校验,还可以过过滤参数.比如有些字段没有在StudentResp声明,entity就算有字段也不会返回给前端,挺好
2025-01-15T14:57:00.239882+0800 - INFO - acess_log.py:42 - dispatch - Request as curl: curl -X GET http://127.0.0.1:8088/api/courses/3/students -H 'connection: keep-alive' -H 'sec-ch-ua-platform: "macOS"' -H 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTczNjkyNTI2MX0.73bUzIdIN0ckbWZ6EPsgEZBY2Md4Vv1dg-xINBU22SE' -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' -H 'accept: application/json, text/plain, */*' -H 'sec-ch-ua: "Chromium";v="131", "Not_A Brand";v="24"' -H 'dnt: 1' -H 'sec-ch-ua-mobile: ?0' -H 'sec-fetch-site: same-origin' -H 'sec-fetch-mode: cors' -H 'sec-fetch-dest: empty' -H 'referer: http://127.0.0.1:8088/static/index.html' -H 'accept-encoding: gzip, deflate, br, zstd' -H 'accept-language: zh-CN,zh;q=0.9
关于日志
用了loguru替代标准库的logging,支持异步记录,支持json格式,其他功能就自己google吧
关于数据库有几个优化点
- 在repository层,在create/update操作之前对pydantic模型->sqlalchemy orm模型的转换
- 同样的封装好的gets/get_by_id操作之后,查到的对象也是基于BaseEntity[pydantic]的对象,可以直接操作,不会影响orm对象.
- 实现Service和repo层的数据隔离
- 默认实现了 is_deleted, created_at, updated_at 三个必填字段,以及is_deleted的逻辑删除
还有数据库的几个坑,也说明一下
- 首先我一直觉得少要用外键约束,级联操作,用起来是简单,很容易出问题. 所以得数据库关联字段得自己维护,你只有显式的写出来才说明你做了某些操作.要不后期定位问题的时候,就得花大时间去查了.
- 当前第一条是我的个人经验,每个人经验不一样,你可以自己根据自己的经验来选择
- 这里用的sqlalchemy session是async session. 异步session在对func对象,或者relationship的对象加载时,是lazy load,只有真正取值的时候,才会进行二次查询.所有在转化成BaseEntity对象的时候会有问题,所以复杂查询自己在repo层自己写就行了
都看到这里了,别忘了给个Star!
就到这里,各位老铁,江湖再见! 😎
下面是github地址
https://github.com/pythonpray/python-web-template
国内gitee地址
https://gitee.com/prayfff/python-web-template.git
作者:pray~