读后笔记 -- FastAPI 构建Python微服务 Chapter5:连接到关系型数据库
5.2 数据库连接准备
PostgreSQL 14 windows exe 安装版本: https://www.enterprisedb.com/postgresql-tutorial-resources-training-1?uuid=140fdf8e-34e6-4b1b-ac32-532e5ac826c4&campaignId=Product_Trial_PostgreSQL_14
注意:安装时,Locale 选择 “English, US”,如果选择 Default/China,可能会遇到 编码问题导致无法连接数据库
该版本可与 Navicat 15 版本配合使用 (下载地址:https://www.jb51.net/article/199525.htm)
5.3 使用 SQLAlchemy 创建 CRUD 事务
# 项目结构:
## 使用 SQLAlchemy 实现同步 CRUD 事务 ### 需要安装的包 ```bash pip install psycopg2 ``` ### 项目结构 ``` The Application ├── api/ # 接口包 │ └── admin.py # router,接口定义,运行事务 │ └── ... ├── db_config/ # 数据库配置 │ └── sqlalchemy_connect.py # 设置数据库连接,创建会话工厂,创建 Base 类 ├── models/ # 模型层 │ └── data │ └── sqlalchemy_models.py # 继承 Base,构建模型,通过 relationships() 映射表关系 │ └── requests │ └── sigup.py # request 数据结构 ├── repository/ # 存储库包 │ └── sqlalchemy │ └── signup.py # 创建连接 CRUD └── main.py # 生成 app,app.include_router(api 的 router) ```
1. 定义数据库
# content of db_config.sqlalchemy_connect.py
from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker # 设置数据库连接 DB_URL = "postgresql://postgres:admin2255@localhost:5433/fcms" # engine 是一个全局对象,在整个应用程序只能创建一次 engine = create_engine(DB_URL) # 初始化会话工厂,通过 sqlalchemy.orm 模块的 sessionmaker() 绑定上面创建的 engine # 将 autocommit=False,需显式调用,如 session.commit() # autoflash=False,不自动刷新 # 应用程序可以通过 SessionFactory() 调用创建多个会话,但一般每个 APIRouter() 只有一个会话 SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine) # 创建 Base 类 Base = declarative_base()
详细介绍:
在 SQLAlchemy 中,sessionmaker 是一个工厂函数,用于创建数据库会话(Session)的配置和实例化模板。当你需要与数据库进行交互时,如查询、插入、更新或删除数据,会话提供了一个上下文管理和数据一致性控制的便利方式。 autocommit=False: 意味着由 Session 管理的数据库连接不会自动提交事务。默认情况下,SQLAlchemy 的 Session 在执行数据库操作后不会立即提交事务;你需要显式调用 session.commit() 来确认更改。
这样可以让你在多个操作之间保持原子性,如果其中一个操作失败,你可以选择回滚(session.rollback())以撤销所有更改,保证数据的一致性。 autoflush=False: 指示 Session 不应在执行查询之前自动刷新 pending(待处理)的变更到数据库。默认情况下,当执行查询时,Session 会先检查是否有未提交的变更(例如对象的属性修改),并自动执行 flush 操作将这些变更同步到数据库中,
以确保查询结果能反映出最新的状态。关闭自动刷新可以提升特定场景下的性能,或者当你需要更细粒度控制何时刷新变更时,但这也要求你更小心地管理数据同步。 bind=engine: 将之前创建的数据库引擎(engine)绑定到 SessionFactory 上。数据库引擎是 SQLAlchemy 用来实际与数据库进行通信的核心对象,它封装了连接池和Dialect(用于理解如何与特定数据库类型交流)。
通过将引擎绑定到 SessionFactory,你确保每个由这个工厂创建的 Session 实例都会自动使用正确的数据库连接配置。
在 SQLAlchemy 中,Base = declarative_base() 的作用是创建一个基类(Base class),它是所有映射到数据库表的 ORM 类(即模型类)的基类。这一行代码是使用 SQLAlchemy 的声明式方式定义 ORM 模型的基础。 具体来说,declarative_base() 函数做了以下几件事: 1. 初始化元数据(Metadata): SQLAlchemy 使用元数据来跟踪所有表、列、索引等数据库结构的信息。通过调用 declarative_base(),它会为你的模型创建一个全局的元数据对象。 2. 创建基类: 返回的 Base 类是你自定义模型类需要继承的基类。这个基类包含了将类属性映射到数据库表字段、处理表关系等功能的方法和属性。 3. 配置ORM映射: 当你定义一个继承自 Base 的子类时,比如定义一个表示用户的数据模型 class User(Base), SQLAlchemy 会自动解析该类中的属性(如列定义、表名等),并设置相应的ORM映射关系。
这意味着你不需要手动编写大量的映射代码,而只需关注定义 Python 类和它们的属性。 4. 提供便利方法: Base 类还包含了一些便利方法,如 query 方法,用于方便地从数据库查询数据,以及与数据库会话(Session)交互的方法。
2. 创建模型类及表映射关系
# 定义模型类
class Login(Base): __tablename__ = "login" # primary_key: 设置表主键 id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=False, index=False) password = Column(String, unique=False, index=False) ... # 通过 relationship() 建立表关联 trainers = relationship('Profile_Trainers', back_populates="login", uselist=False) members = relationship('Profile_Members', back_populates="login", uselist=False) class Profile_Trainers(Base): __tablename__ = "profile_trainers" # ForeignKey() 将模型类链接到其父类的引用键列对象 login.id id = Column(Integer, ForeignKey('login.id'), primary_key=True, index=True, ) firstname = Column(String, unique=False, index=False) lastname = Column(String, unique=False, index=False) age = Column(Integer, unique=False, index=False) ... login = relationship('Login', back_populates="trainers") gclass = relationship('Gym_Class', back_populates="trainers")
相关字段详细介绍:
relationship 是一个非常重要的功能,用于定义两个(或多个)ORM 类(通常是表)之间的关联关系: 'Profile_Trainers': 指定了与当前模型类建立关系的另一个ORM类的名称。在当前类中定义的 trainers 属性将关联到 Profile_Trainers 类的实例。 back_populates="login": 用来指定双向关系的。在 Profile_Trainers 类中应该也有一个 login 属性,它使用 relationship 指向当前类。back_populates 确保双方都能访问对方,也就是说,
如果你从 Profile_Trainers 实例访问其 login 属性,你可以直接获取到与之关联的主体实体实例。 uselist=False: 表明与 Profile_Trainers 的关联是一个一对一的关系,而不是一对多。因此,trainers 属性将直接存储一个 Profile_Trainers 的实例,而不是一个包含多个实例的列表。
如果没有这个参数或设置为 True,默认行为是创建一个列表来存储关联的对象集合。
3. 单表查询及连接查询
# content of .\repository\sqlalchemy\signup.py # 单表 CRUD 查询 class SignupRepository: def __init__(self, sess: Session): self.sess: Session = sess def insert_signup(self, signup: Signup) -> bool: try: # add() 处理对象的值,所以是 signup。也可以看 add() 方法,传入的是 instance self.sess.add(signup) self.sess.commit() print(signup.id) except: return False return True def update_signup(self, id: int, details: Dict[str, Any]) -> bool: try: # 执行各种查询动作时,如 query, fileter, order_by, group_by, count(), sum() 都是传入的类,主要是为了构建查询灵活性 self.sess.query(Signup).filter(Signup.id == id).update(details) self.sess.commit() except: return False return True # 连接查询1 class LoginMemberRepository: def __init__(self, sess: Session): self.sess: Session = sess def join_login_members(self): # query() 放入多张表,filter() 放入条件,覆盖 ON 条件 return self.sess.query(Login, Profile_Members).filter(Login.id == Profile_Members.id).all() # 连接查询2 class MemberAttendanceRepository: def __init__(self, sess: Session): self.sess: Session = sess # 通过 join() 方法实现内连接 def join_member_attendance(self): return self.sess.query(Profile_Members, Attendance_Member).join(Attendance_Member).all() # 通过 outerjoin() 方法实现外连接 def outer_join_member(self): return self.sess.query(Profile_Members, Attendance_Member).outerjoin(Attendance_Member).all()
4. 运行事务
# content of api.admin.py router = APIRouter() def sess_db(): # 如前面所说,最好是每个 router 仅创建一个 session db = SessionFactory() try: # 通过 yield db 将函数转变为一个 生成器函数 yield db finally: db.close() @router.post("/signup/add") def add_signup(req: SignupReq, sess: Session = Depends(sess_db)): repo: SignupRepository = SignupRepository(sess) signup = Signup(id=req.id, username=req.username, password=req.password) result = repo.insert_signup(signup) if result is True: return signup else: return JSONResponse(content={"message": "create signup problem encountered"}, status_code=500) @router.get("/signup/list", response_model=List[SignupReq]) def list_signup(sess: Session = Depends(sess_db)): repo: SignupRepository = SignupRepository(sess) result = repo.get_all_signup() return result
解说 pydantic:
# content of models.requests.signup.py from pydantic import BaseModel class SignupReq(BaseModel): # ":" 说明 id 是类属性 id: int username: str password: str class Config: # "=" 来表示,说明 orm 是配置信息 orm_mode = True
class Config: 是 Pydantic 模型内的配置类,其中 orm_mode = True 的设置意味着这个模型能够兼容ORM(对象关系映射)对象。开启 orm_mode 后,Pydantic 可以直接处理从数据库ORM获取的对象,比如 SQLAlchemy 中代表数据库记录的对象,
并能将这些对象自动转化为可以用于响应的 JSON 格式数据,同时也支持将 ORM 对象用于数据验证。
5.4 使用 SQLAlchemy 实现异步 CRUD 事务
## 使用 SQLAlchemy 实现异步 CRUD 事务 ### 需要安装的包 ```bash pip install aiopg pip install asyncpg ``` ### 项目结构 ``` The Application ├── api/ # 接口包 │ └── attendance.py # router,接口定义,运行事务 │ └── ... ├── db_config/ # 数据库配置 │ └── sqlalchemy_async_connect.py # 设置数据库连接,创建会话工厂,创建 Base 类 ├── models/ # 模型层 │ └── data │ └── sqlalchemy_async_models.py # 继承 Base,构建模型,通过 relationships() 映射表关系 │ └── requests │ └── attendance.py # request 数据结构 ├── repository/ # 存储库包 │ └── sqlalchemy │ └── attendance.py # 创建连接 CRUD └── main.py # 生成 app,app.include_router(api 的 router) ```
1. 设置数据库连接:
# content of db_config.sqlalchemy_async_connect.py import asyncio from sqlalchemy import text, select, func from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.orm import sessionmaker, declarative_base # 使用 "+asyncpg" 表明将 asyncpg 作为数据库核心驱动,而不是 pcycopg2 DB_URL = "postgresql+asyncpg://postgres:postgres@127.0.0.1:5432/postgres" # create_async_engine: 创建异步的数据引擎,future=True: 使用 SQLAlchemy 2.0 的新API风格, echo=True: SQL 语句在控制台打印,便于调试 engine = create_async_engine(DB_URL, future=True, echo=True) # 1. expire_on_commit=False: 使模型实例及其属性在事务期间可访问,即使在 commit() 之后 # 异步下,所有实体类及其列对象仍可被其他进程访问,即使在事务提交之后(这点与同步环境不同) # 2. class_ 带有类名 AsyncSession,该实体将控制 CRUD 事务。 AsyncSessionFactory = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) Base = declarative_base() # ut,验证数据库异步连接 async def check_database_connection(): session = AsyncSessionFactory() try: version_query = select(func.version()) result = await session.execute(version_query) # 获取第一条查询结果并打印 version_info = result.scalar_one() print(f"Database Connection Successful! PostgreSQL Version: {version_info}") except SQLAlchemyError as e: print(f"Error connecting to the database: {e}") finally: # 关闭会话以释放资源 await session.close() if __name__ == '__main__': asyncio.run(check_database_connection())
2. 创建存储库层
from datetime import datetime from typing import Dict, Any from sqlalchemy import insert, update, delete, select from sqlalchemy.orm import Session from models.data.sqlalchemy_async_models import Attendance_Member class AttendanceRepository: def __init__(self, sess: Session): self.sess: Session = sess async def insert_attendance(self, attendance: Attendance_Member) -> bool: try: # 方式1:用 values() 投影所有列值以插入 sql = insert(Attendance_Member).values(id=attendance.id, member_id=attendance.member_id, timein=datetime.strptime(attendance.timein, "%H:%M").time(), timeout=datetime.strptime(attendance.timeout, "%H:%M").time(), date_log=attendance.date_log) # 添加执行选项,告诉 session 始终使用 fetch() 来同步模型属性值和数据库中的更新值 sql.execution_options(synchronize_session="fetch") # 使用 execute() 来运行最终的 insert()并自动提交更改,注意,基于 sessionmaker() 配置的 expire_on_commit = False await self.sess.execute(sql) # # 方式2:类似 同步的处理逻辑,通过 sess.add() 添加记录 # attendance.timein = datetime.strptime(attendance.timein, "%H:%M").time() # attendance.timeout = datetime.strptime(attendance.timeout, "%H:%M").time() # self.sess.add(attendance) # await self.sess.flush() except Exception as e: print(f"except with {e}") return False return True
3. 运行 CRUD 事务
# content of api.attendance.py from fastapi import APIRouter from db_config.sqlalchemy_async_connect import AsyncSessionFactory from models.data.sqlalchemy_async_models import Attendance_Member from models.requests.attendance import AttendanceMemberReq from repository.sqlalchemy.attendance import AttendanceRepository router = APIRouter() @router.post("/attendance/add") async def add_attendance(req: AttendanceMemberReq): # 因为连接的 AsyncEngine 需要在每个 commit() 事务后关闭,所以由 AsynSessionFactory() 创建的 AsyncSession 需要异步 with 上下文管理器 async with AsyncSessionFactory() as sess: # AsyncSession 创建后,只会在服务调用其 begin() 开始执行所有 CRUD 事务,并且在事务执行后需要关闭,所以需要另一个异步 with 上下文管理器 async with sess.begin(): repo = AttendanceRepository(sess) attendance = Attendance_Member(id=req.id, member_id=req.member_id, timein=req.timein, timeout=req.timeout, date_log=req.date_log) return await repo.insert_attendance(attendance)
5.6 使用 Pony 实现同步 CRUD
# 项目结构
## 3. 使用 Pony 实现同步 CRUD 事务 ### 需要安装的包 ```bash pip install pony pip install psycopg2 ``` ### 项目结构 ``` The Application ├── api/ # 接口包 │ └── __init__.py │ └── member.py # router,接口定义,运行事务 │ └── ... ├── db_config/ # 数据库配置 │ └── __init__.py │ └── pony_connect.py # 设置数据库连接,创建会话工厂,创建 Base 类 ├── models/ # 模型层 │ └── data │ └── __init__.py │ └── pony_models.py # 构建模型,映射表关系 │ └── requests │ └── __init__.py │ └── member.py # request 数据结构 │ └── __init__.py ├── repository/ # 存储库包 │ └── __init__.py │ └── pony │ └── __init__.py │ └── members.py # 创建连接 CRUD │── __init__.py └── main.py # 生成 app,app.include_router(api 的 router) ```
1. 数据库配置
# content of db_config.pony_connect.py from pony.orm import Database db = Database("postgres", host="localhost", port="5432", user="postgres", password="postgres", database="postgres")
2. 模型层数据定义及表关系
# content fo models.data.pony_models.py
from datetime import date, time from pony.orm import PrimaryKey, Required, Optional, Set from db_config.pony_connect import db """ 表关系定义: 表关系 Parent Child 一对一 Optional Required / Optional 一对多 Set Required 多对一 Set Set """ class Signup(db.Entity): _table_ = "signup" id = PrimaryKey(int) username = Required(str, unique=True, max_len=100, nullable=False) password = Required(str, unique=False, max_len=100, nullable=False) class Login(db.Entity): _table_ = "login" id = PrimaryKey(int) username = Required(str) password = Required(str) date_approved = Required(date) user_type = Required(int) trainers = Optional("Profile_Trainers", reverse="id") members = Optional("Profile_Members", reverse="id") class Profile_Trainers(db.Entity): _table_ = "profile_trainers" id = PrimaryKey("Login", reverse="trainers") firstname = Required(str) lastname = Required(str) age = Required(int) position = Required(str) tenure = Required(float) shift = Required(int) members = Set("Profile_Members", reverse="trainer_id") gclass = Set("Gym_Class", reverse="trainer_id") class Profile_Members(db.Entity): _table_ = "profile_members" id = PrimaryKey("Login", reverse="members") firstname = Required(str) lastname = Required(str) age = Required(int) height = Required(float) weight = Required(float) membership_type = Required(str) trainer_id = Required("Profile_Trainers", reverse="members") attendance = Set("Attendance_Member", reverse="member_id") gclass = Set("Gym_Class", reverse="member_id")
3. 请求格式定义
# content of models.requests.members.py from typing import Any, List from pydantic import BaseModel, field_validator class GymClassReq(BaseModel): id: int member_id: int trainer_id: int approved: int class Config: orm_mode = True class ProfileMembersReq(BaseModel): id: Any firstname: str lastname: str age: int height: float weight: float membership_type: str trainer_id: Any @field_validator('trainer_id', mode="before") def trainer_object_to_int(cls, values): if isinstance(values, int): return values else: return values.id.id @field_validator('id', mode="before") def member_id_to_int(cls, values): if isinstance(values, int): return values else: return values.id class Config: from_attributes = True orm_mode = True
4. 实现 CRUD 事务
# content of repository.pony.members.py from typing import Dict, Any from pony.orm import db_session from models.data.pony_models import Profile_Members, Login from models.requests.members import ProfileMembersReq class MemberRepository: def insert_member(self, details: Dict[str, Any]) -> bool: try: with db_session: Profile_Members(**details) except: return False return True def update_member(self, id: int, details: Dict[str, Any]) -> bool: try: with db_session: # Pony 内置了对 JSON 的支持,所以查到的对象将自动转换为支持 JSON 的对象 profile = Profile_Members[id] profile.id = details["id"] profile.firstname = details["firstname"] profile.lastname = details["lastname"] profile.age = details["age"] profile.membership_type = details["membership_type"] profile.height = details["height"] profile.weight = details["weight"] profile.trainer_id = details["trainer_id"] except: return False return Truedef get_all_member(self): with db_session: members = Profile_Members.select() result = [ProfileMembersReq.from_orm(m) for m in members] return result def get_member(self, id: int): with db_session: login = Login.get(lambda l: l.id == id) member = Profile_Members.get(lambda m: m.id == login) result = ProfileMembersReq.from_orm(member) return result
5. API 接口定义
# content of api.members.py from fastapi import APIRouter from fastapi.responses import JSONResponse from models.requests.members import ProfileMembersReq from repository.pony.members import MemberRepository router = APIRouter() @router.post("/member/add") def add_member(req: ProfileMembersReq): repo = MemberRepository() mem_profile = dict() mem_profile["id"] = req.id mem_profile["firstname"] = req.firstname mem_profile["lastname"] = req.lastname mem_profile["age"] = req.age mem_profile["height"] = req.height mem_profile["weight"] = req.weight mem_profile["membership_type"] = req.membership_type mem_profile["trainer_id"] = req.trainer_id result = repo.insert_member(mem_profile) if result is True: return req else: return JSONResponse(content={"message": "create profile encountered problem"}, status_code=500) @router.patch("/member/update") def update_member(id: int, req: ProfileMembersReq): mem_profile_dict = req.dict(exclude_unset=True) repo = MemberRepository() result = repo.update_member(id, mem_profile_dict) if result is True: return req else: return JSONResponse(content={'message': 'update profile problem encountered'}, status_code=500) @router.get("/member/list") def list_members(): repo = MemberRepository() return repo.get_all_member()
Pony ORM 的优缺点:
Pony ORM 是一个 Python 的对象关系映射 (ORM) 框架,它提供了一种优雅且直观的方式来处理数据库操作。以下是 Pony ORM 的一些主要优缺点:
优点:
简洁易用的语法:Pony ORM 以其简洁、接近自然语言的查询语法而著称,使得编写数据库查询变得如同书写英文描述一样直接,提高了代码的可读性和编写效率。
自动管理会话:Pony ORM 自动管理数据库会话,减少了手动控制事务的复杂度,使得开发者可以更多地关注业务逻辑而不是数据库事务管理。
延迟加载与即时加载策略:Pony ORM 支持惰性加载,只有当数据真正需要时才从数据库加载,有助于提高程序运行时的效率,特别是处理大量数据时。
强大的查询能力:提供了丰富的查询表达式,支持复杂的查询构造,包括联接、子查询等,同时支持Pythonic的链式调用方式。
代码即查询语言:Pony ORM 允许直接在Python代码中以声明式方式编写查询,无需学习特定的查询语言(如SQL),降低了学习成本。
数据模型灵活性:Pony ORM 提供了灵活的数据模型定义,可以动态修改实体结构,适应快速迭代的需求。
缺点:
性能开销:与直接编写SQL相比,ORM的抽象层会带来一定的性能损失,尤其是在处理大数据量和复杂查询时。Pony ORM 的自动映射和高级特性可能不如手写SQL高效。
学习曲线和文档:虽然Pony ORM努力提供简洁的API,但对于习惯于其他ORM(如SQLAlchemy)或直接写SQL的开发者来说,其独特的查询语法和理念可能需要一定的学习时间。此外,相比于某些更成熟的ORM框架,Pony ORM的文档和社区资源可能相对有限。
生态和社区支持:相对于一些更为流行的ORM库,如SQLAlchemy,Pony ORM的社区较小,这意味着遇到问题时可能较难找到现成的解决方案或插件支持。
适用场景限制:Pony ORM的设计哲学更倾向于简洁和易用,对于那些需要高度定制化数据库操作或性能至上的大型项目,可能不是最佳选择。
总的来说,Pony ORM 适合那些追求开发效率、代码可读性和维护性的项目,尤其是中、小型项目或快速原型开发。然而,在性能敏感或需要高度定制化的环境中,可能需要权衡其使用。
5.6 使用 Peewee ORM 完成 CRUD 事务
Peewee ORM 优缺点介绍:
Peewee ORM 是一个轻量级的 Python ORM 框架,广泛应用于需要数据库操作的项目中。以下是 Peewee ORM 的主要优缺点:
优点:
轻量级与易用性:Peewee 设计简洁,体积小,学习曲线相对平缓,易于上手。它提供了Django风格的API,使得开发者能够快速熟悉并开始使用。
灵活性与兼容性:支持多种数据库后端,包括 SQLite、MySQL、PostgreSQL 等,适用于不同的项目需求和环境。同时,它允许直接执行原生SQL,给予开发者更大的灵活性。
高性能:相比一些全功能的ORM,如 SQLAlchemy,Peewee 因其轻量设计,在某些场景下可以提供更快的执行速度和更低的内存占用。
代码简洁:Peewee 的模型定义和查询语法较为直观,可以写出简洁、易于理解的代码。它还支持链式查询,使得构建复杂查询变得简单。
易于集成:由于其轻量级和模块化设计,Peewee 很容易与任何Web框架或非Web项目集成,不像某些ORM与特定框架绑定紧密。
缺点:
缺乏高级特性:相比于 SQLAlchemy 这样的全面ORM,Peewee 在某些高级特性和复杂数据库操作方面可能不够强大,例如自动迁移工具和复杂的数据映射功能。
文档和社区:虽然 Peewee 的文档相对完善,但由于其轻量级定位,可能不如某些大型ORM框架那样拥有庞大的社区和丰富的第三方资源。
自动化管理不足:Peewee 不支持自动化 schema 迁移,意味着当数据库模式发生变化时,需要手动调整或借助第三方工具来同步模型与数据库结构。
定制化限制:对于那些需要高度定制数据库交互逻辑的项目,Peewee 可能提供的配置选项和扩展性不如某些其他ORM框架。
总结而言,Peewee ORM 特别适合那些寻求简单、快速解决方案的小型项目或对性能有严格要求的应用。如果你的项目不需要高级ORM特性,或者你偏好简洁的工具链,Peewee 将是一个不错的选择。然而,如果你的项目规模较大,
需要复杂的数据库管理功能,可能需要考虑更全面的ORM解决方案。
# 项目结构
## 3. 使用 Peewee 实现异步 CRUD 事务 ### 需要安装的包 ```bash pip install peewee pip install psycopg2 ``` ### 项目结构 ``` The Application ├── api/ # 接口包 │ └── __init__.py │ └── login.py # router,接口定义,运行事务?? │ └── ... ├── db_config/ # 数据库配置 │ └── __init__.py │ └── peewee_connect.py # 设置数据库连接,创建会话工厂,创建 Base 类?? ├── models/ # 模型层 │ └── data │ └── __init__.py │ └── peewee_models.py # 构建模型 │ └── data │ └── __init__.py │ └── login.py # request 数据结构 │ └── __init__.py ├── repository/ # 存储库包 │ └── __init__.py │ └── peewee │ └── __init__.py │ └── login.py # 创建连接 CRUD │── __init__.py └── main.py # 生成 app,app.include_router(api 的 router) ```
1. 创建数据库连接
# content of db_config.peewee_connect.py """ 通过引入contextvars来增强Peewee的数据库连接管理能力,使其能更好地适应现代应用程序复杂多变的运行环境,特别是提高了在异步编程模型和多线程应用中的灵活性和效率 """ # 用于在不同的上下文中(例如,在异步函数或多个线程中)存储连接状态 from contextvars import ContextVar # _ConnectionState是Peewee内部用于管理数据库连接状态的类,而PostgresqlDatabase则是用于连接 PostgreSQL 数据库的具体类。 from peewee import _ConnectionState, PostgresqlDatabase # 初始化了数据库连接状态的默认值,如是否关闭(closed)、连接对象(conn)、上下文(ctx)以及事务信息(transactions) db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} # ContextVar实例,其默认值为db_state_default的浅拷贝。这个变量用于跨上下文存储数据库连接的状态信息 db_state = ContextVar("db_state", default=db_state_default.copy()) class PeeweeConnectionState(_ConnectionState): def __init__(self, **kwargs): # 在构造方法中,使用super().__setattr__设置一个名为_state的属性,将其值设为前面定义的ContextVar实例db_state,这样可以在类实例间共享状态 super().__setattr__("_state", db_state) super().__init__(**kwargs) # __setattr__和__getattr__方法使得可以像操作普通属性一样操作_state所指向的上下文变量中的数据,实现了对数据库连接状态的灵活管理和访问 def __setattr__(self, name, value): self._state.get()[name] = value def __getattr__(self, name): return self._state.get()[name] # 对于 Peewee ROM,可以仅创建 database,表会在后续自动生成 db = PostgresqlDatabase("fcms2", user="postgres", password="postgres", host="localhost", port=5432) # 将数据库的连接状态管理替换为新定义的PeeweeConnectionState实例,使得数据库连接能够在不同上下文中正确地管理和切换,特别适合处理并发请求或在需要精细控制资源生命周期的场景中使用 db._state = PeeweeConnectionState()
2. 定义模型类及映射关系
# content of models.data.peewee_models.py """ peewee 的模型类不会定义主键,其将在模式自动生成期间创建 """ from peewee import Model, CharField, DateField, IntegerField, ForeignKeyField, FloatField from db_config.peewee_connect import db class Signup(Model): username = CharField(unique=False, index=False) password = CharField(unique=False, index=False) # 每个领域类各有一个嵌套的 Meta 类,它注册了对 database 和 db_table 的引用,将 Meta 类映射到 模型类 # 在这里还可以设置其他属性如 primary_key, indexes, constraints class Meta: database = db db_table = "signup" class Login(Model): username = CharField(unique=False, index=False) password = CharField(unique=False, index=False) date_approved = DateField(unique=False, index=False) user_type = IntegerField(unique=False, index=False) class Meta: database = db db_table = "login" class Profile_Trainers(Model): # 通过 ForeignKeyField 声明外键属性,参数的含义是: # 父模型的名称 # backref 参数:引用子记录(一对一关系)或 一组子对象(一对多 或 多对一) # unique: True(一对一关系),False(非一对一关系) login = ForeignKeyField(Login, backref="trainer", unique=True) firstname = CharField(unique=False, index=False) lastname = CharField(unique=False, index=False) age = CharField(unique=False, index=False) position = CharField(unique=False, index=False) tenure = FloatField(unique=False, index=False) shift = IntegerField(unique=False, index=False) class Meta: database = db db_table = 'profile_trainers' # step2: 在定义了所有模型(包括它们的关系)后,还需要从 Peewee 的数据库实例中调用下面方法进行表映射: # 建立连接 db.connect() # 根据其模型类的列表进行模式生成,此处将会在存在 database 的情况下,自动生成表 db.create_tables([Signup, Login, Profile_Trainers], safe=True)
3. 实现 CRUD 事务
# content of repository.peewee.login.py from datetime import date from models.data.peewee_models import Login class LoginRepository: def insert_login(self, id: int, user: str, passwd: str, approved: date, type: int) -> bool: try: Login.create(id=id, username=user, password=passwd, date_approved=approved, user_type=type) except Exception as e: print(e) return False return True
4. 接口调用
# content of api.login.py """ 由于 Peewee 的数据库连接是在模型层设置的,因此 APIRouter 或 FastAPI 不需要额外的要求来运行 CRUD 事务。 API 服务可以轻松地访问所有存储库类,而无须从 db 实例中调用方法或指令。 """ from fastapi import APIRouter from fastapi.responses import JSONResponse from models.requests.login import LoginReq from repository.peewee.login import LoginRepository router = APIRouter() @router.post("/login/add") async def add_login(req: LoginReq): repo = LoginRepository() result = repo.insert_login(req.id, req.username, req.password, req.date_approved, req.user_type) if result is True: return req else: return JSONResponse(content={"message": "create login problem encountered"}, status_code=500)
请求模型
# content of models.requests.login.py from datetime import date from pydantic import BaseModel class LoginReq(BaseModel): id: int username: str password: str date_approved: date user_type: int class Config: orm_mode = True
5.8 应用 CQRS 设计模式
CQRS 是一种微服务设计模式,优点:
- 分离查询事务(读取)与 插入/更新/删除 (写入),减少了这些事务组合一起的访问,提供更少的流量和更快的执行;
- 在 API 服务 和 存储库层间创建了一个松散耦合的特性;
# 项目结构
## 4. Pony 应用 CQRS 设计模式 api.trainers -> crqs -> repository -> models ### 需要安装的包 ```bash pip install pony pip install psycopg2 ``` ### 项目结构 ``` The Application ├── api/ # 接口包 │ └── __init__.py │ └── trainers.py # 访问处理程序 │ └── ... ├── crqs/ # crqs,分离查询(query)和写入(insert, update, delete)操作 │ └── trainers/ │ └── command/ # 创建命令处理程序 │ └── __init__.py │ └── create_handlers.py │ └── query # 创建查询处理程序 │ └── __init__.py │ └── query_handlers.py │ └── __init__.py │ └── __init__.py │ └── commands.py # 创建命令类 │ └── handlers.py # 定义 处理程序接口,一个查询,一个命令事务 │ └── queries.py # 创建查询类 ├── db_config/ # 数据库配置 │ └── __init__.py │ └── pony_connect.py # 设置数据库连接,创建 Database() 连接后的实例 ├── models/ # 模型层 │ └── data │ └── __init__.py │ └── pony_models.py # 构建模型,映射表关系 │ └── requests │ └── __init__.py │ └── trainers.py # request 数据结构 │ └── __init__.py ├── repository/ # 存储库包 │ └── __init__.py │ └── pony_crqs │ └── __init__.py │ └── trainers.py # 创建连接 CRUD │── __init__.py └── main.py # 生成 app,app.include_router(api 的 router) ```
1. 数据库连接
# content of db_config.pony_connect.py from pony.orm import Database db = Database("postgres", host="localhost", port="5432", user="postgres", password="postgres", database="postgres")
2. 定义处理程序接口
# content of crqs.handlers.py class IQueryHandler: pass class ICommandHandler: pass
3. 创建命令和查询类
# content of crqs.commands.py from typing import Dict, Any class ProfileTrainerCommand: def __init__(self): self._details: Dict[str, Any] = dict() # 使用 @property 装饰器定义了 details 属性的getter方法,意味着可以直接通过实例访问 _details,如 instance.details,而不需要调用方法 @property def details(self): return self._details # 使用 @details.setter 装饰器定义了 details 属性的setter方法,名为 details,这允许你通过赋值语句修改 _details 的值 # 当你执行 instance.details = some_dict 时,setter方法会被触发,它将接收到的新字典 some_dict 直接赋值给 _details,从而更新存储的详情信息 @details.setter def details(self, details): self._details = details # content of crqs.queries.py from typing import List from models.data.pony_models import Profile_Trainers class ProfileTrainerListQuery: def __init__(self): self._records: List[Profile_Trainers] = list() @property def records(self): return self._records @records.setter def records(self, records): self._records = records
4. 创建命令和查询处理程序
# content of crqs.trainers.command.create_handlers.py from cqrs.commands import ProfileTrainerCommand from cqrs.handlers import ICommandHandler from repository.pony_cqrs.trainers import TrainerRepository class AddTrainerCommandHandler(ICommandHandler): def __init__(self): self.repo: TrainerRepository = TrainerRepository() def handle(self, command: ProfileTrainerCommand) -> bool: result = self.repo.insert_trainer(command.details) return result # content of crqs.trainers.command.delete_handlers.py from cqrs.commands import ProfileTrainerCommand from cqrs.handlers import ICommandHandler from repository.pony_cqrs.trainers import TrainerRepository class DeleteTrainerCommandHandler(ICommandHandler): def __init__(self): self.repo : TrainerRepository = TrainerRepository() def handle(self, command: ProfileTrainerCommand) -> bool: result = self.repo.delete_trainer(command.details.get("id")) return result # content of crqs.trainers.command.update_handlers.py from cqrs.commands import ProfileTrainerCommand from cqrs.handlers import ICommandHandler from repository.pony_cqrs.trainers import TrainerRepository class UpdateTrainerCommandHandler(ICommandHandler): def __init__(self): self.repo: TrainerRepository = TrainerRepository() def handle(self, id: int, command: ProfileTrainerCommand) -> bool: result = self.repo.update_trainer(id, command.details) return result
# content of crqs.trainers.query.query_handlers.py from cqrs.handlers import IQueryHandler from cqrs.queries import ProfileTrainerListQuery from repository.pony_cqrs.trainers import TrainerRepository class ListTrainerQueryHandler(IQueryHandler): def __init__(self): self.repo: TrainerRepository = TrainerRepository() self.query: ProfileTrainerListQuery = ProfileTrainerListQuery() def handle(self) -> ProfileTrainerListQuery: data = self.repo.get_all_trainers() self.query.records = data return self.query
5. repository 存储库层
# content of repository.pony_crqs.trainers.py from typing import Dict, Any from models.data.pony_models import Profile_Trainers, Login from pony.orm import db_session from models.requests.trainers import ProfileTrainersReq class TrainerRepository: def insert_trainer(self, details: Dict[str, Any]) -> bool: try: with db_session: Profile_Trainers(**details) except Exception as e: print(e) return False return True def update_trainer(self, id: int, details: Dict[str, Any]) -> bool: try: with db_session: profile = Profile_Trainers[id] profile.id = details["id"] profile.firstname = details["firstname"] profile.lastname = details["lastname"] profile.age = details["age"] profile.position = details["position"] profile.tenure = details["tenure"] profile.shift = details["shift"] except: return False return True def delete_trainer(self, id: int) -> bool: try: with db_session: Profile_Trainers[id].delete() except Exception as e: print(e) return False return True def get_all_trainers(self): with db_session: trainers = Profile_Trainers.select() result = [ProfileTrainersReq.from_orm(m) for m in trainers] return result def get_trainer(self, id: int): with db_session: login = Login.get(lambda l: l.id == id) trainer = Profile_Trainers.get(lambda m: m.id == login) result = ProfileTrainersReq.from_orm(trainer) return result
6. 模型层数据
# content of models.requests.trainers.py from pydantic import BaseModel, field_validator class ProfileTrainersReq(BaseModel): id: int firstname: str lastname: str age: int position: str tenure: float shift: int # 这个 id 必须加,在 query 请求时, id 可能非 int 类型 @field_validator('id', mode="before") def member_id_to_int(cls, values): if isinstance(values, int): return values else: return values.id class Config: orm_mode = True from_attributes = True
# content of models.data.pony_models.py class Profile_Trainers(db.Entity): _table_ = "profile_trainers" id = PrimaryKey("Login", reverse="trainers") firstname = Required(str) lastname = Required(str) age = Required(int) position = Required(str) tenure = Required(float) shift = Required(int) members = Set("Profile_Members", reverse="trainer_id") gclass = Set("Gym_Class", reverse="trainer_id")