Tortoise-ORM 海龟ORM -- pd的FastAPI笔记
文章目录
-
- [Tortoise-ORM 海龟ORM -- pd的FastAPI笔记](#Tortoise-ORM 海龟ORM -- pd的FastAPI笔记)
- ORM框架
ORM框架
ORM(Object-Relational Mapping,对象关系映射)是一种编程技术,用于在面向对象编程语言和关系型数据库之间建立映射。它允许开发者通过操作对象的方式来与数据库进行交互,而无需直接编写复杂的 SQL 语句。主要特点包括:
- 对象与数据库表的映射:ORM 将数据库中的表映射为编程语言中的类,每一行数据对应一个对象,表的列对应对象的属性
- 简化数据库操作:开发者可以使用面向对象的方法(如创建、查询、更新、删除对象)来操作数据库,而无需手动编写 SQL
- 跨数据库兼容:ORM 通常支持多种数据库(如 MySQL、PostgreSQL、SQLite),通过统一的接口减少数据库切换的成本
工作原理
- ORM 框架定义了一个映射层,将类和数据库表关联起来
- 开发者通过 ORM 的 API 操作对象,ORM 自动将操作翻译成 SQL 语句,执行数据库操作并返回结果
缺点
- 性能可能略低于原生 SQL(因抽象层开销)
- 对于非常复杂的查询,可能需要直接编写 SQL
ORM工具介绍
- SQLAlchemy(同步/异步):≈80% 企业项目首选,功能完备、社区成熟,支持复杂查询和事务管理。
- Tortoise(异步):语法类似 Django ORM,适合异步优先项目,集成简便
- GINO(异步):轻量级,基于 SQLAlchemy Core 的异步扩展,适合高性能 API
选型建议
- 传统企业项目 → SQLAlchemy(成熟稳定)
- 全异步微服务 → Tortoise-ORM 或 GINO
注意:FastAPI没有官方ORM
Tortoise-ORM
为什么选择 Tortoise-ORM
- 异步支持,与 FastAPI 无缝集成
- 简单易用的模型定义,类似 Django ORM
- 支持复杂关系(一对一、一对多、多对多)
- 自动生成表结构,适合快速开发
环境配置
python
pip install tortoise-orm==0.25
pip install aerich==0.9.0
pip install aiomysql==0.2.0
pip install tomlkit==0.13.2
- tortoise-orm:用于异步Python应用的关系对象映射(ORM)框架(基于Django ORM设计,但完全异步)
- aerich: 数据库迁移工具(类似Django的migrations)
- aiomysql:异步MySQL驱动,为asyncio提供异步MySQL客户端,是Tortoise-ORM连接MySQL的底层驱动
- tomlkit:配置文件管理,读写和维护TOML格式文件,Aerich使用pyproject.toml存储配置,管理迁移版本信息
python
from tortoise.contrib.fastapi import register_tortoise
from typing import Dict
from fastapi import FastAPI
TORTOISE_ORM: Dict = {
"connections": {
# 开发环境使用 SQLite(基于文件,无需服务器)
# "default": "sqlite://db.sqlite3",
# 生产环境示例:PostgreSQL
# "default": "postgres://user:password@localhost:5432/dbname",
# 生产环境示例:MySQL
"default": "mysql://root:xxxxxx@localhost:3306/blog_project",
},
"apps": {
"models": {
"models": [ # "app.models",
"aerich.models"], # 模型模块和 Aerich 迁移模型
"default_connection": "default",
}
},
# 连接池配置(推荐)
"use_tz": False, # 是否使用时区
"timezone": "UTC", # 默认时区
"db_pool": {
"max_size": 10, # 最大连接数
"min_size": 1,
# 最小连接数
"idle_timeout": 30 # 空闲连接超时(秒)
}
}
app = FastAPI(debug=True)
# 配置 Tortoise ORM
register_tortoise(app,
config=TORTOISE_ORM,
generate_schemas=True, # 开发环境自动生成表结构
add_exception_handlers=True # 添加异常处理
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("16ORMsetting:app", host="127.0.0.1",
port=8003, reload=True)
Aerich使用
在项目根目录运行以下命令:
- 初始化
python
# main表示项目的入口文件,TORTOISE_ORM为Tortoise-ORM的配置字典
aerich init -t main.TORTOISE_ORM
# aerich init -t 16ORMsetting.TORTOISE_ORM
这将生成:
-
pyproject.toml:Aerich 配置文件,指定迁移配置。
-
migrations/:迁移文件目录,存放生成的 .sql 文件。
-
生成和创建迁移脚本
python
aerich init-db
后续操作:生成和迁移文件
- 生成迁移文件: 当模型发生变更时,运行以下命令生成迁移文件:
python
aerich migrate --name "info"
-
应用迁移: 运行以下命令将迁移应用到数据库:
aerich upgrade
- 验证迁移: 检查迁移历史
python
aerich history
- 回滚迁移:回退到指定版本
python
aerich downgrade -n 20230705_000000
创建模型
python
# models.py 因为我数据库中的那个配置文件写在了16ORM配置文件中
# 对应修改配置文件中的"app.models" --> "models"
from tortoise.models import Model
from tortoise import fields
from tortoise.contrib.postgres.fields import ArrayField
from datetime import datetime
from typing import Optional
class User(Model):
"""用户模型"""
# 基本信息
id = fields.IntField(pk=True, description="主键ID")
username = fields.CharField(
max_length=50,
unique=True,
null=False,
description="用户名"
)
email = fields.CharField(
max_length=100,
unique=True,
null=True,
description="邮箱"
)
phone = fields.CharField(
max_length=20,
unique=True,
null=True,
description="手机号"
)
# 密码和安全
password_hash = fields.CharField(
max_length=255,
null=False,
description="密码哈希值"
)
salt = fields.CharField(
max_length=50,
null=False,
description="密码盐值"
)
is_active = fields.BooleanField(
default=True,
description="是否激活"
)
is_superuser = fields.BooleanField(
default=False,
description="是否超级用户"
)
is_verified = fields.BooleanField(
default=False,
description="是否已验证邮箱"
)
# 个人信息
nickname = fields.CharField(
max_length=50,
null=True,
default="",
description="昵称"
)
avatar = fields.CharField(
max_length=500,
null=True,
description="头像URL"
)
bio = fields.TextField(
null=True,
description="个人简介"
)
# 时间信息
date_joined = fields.DatetimeField(
auto_now_add=True,
description="注册时间"
)
last_login = fields.DatetimeField(
null=True,
description="最后登录时间"
)
updated_at = fields.DatetimeField(
auto_now=True,
description="更新时间"
)
# 状态信息
login_count = fields.IntField(
default=0,
description="登录次数"
)
failed_login_attempts = fields.IntField(
default=0,
description="连续登录失败次数"
)
locked_until = fields.DatetimeField(
null=True,
description="锁定直到(用于账户锁定)"
)
# 权限相关
permissions = fields.JSONField(
null=True,
default=list,
description="权限列表"
)
# 元数据
meta_data = fields.JSONField(
null=True,
default=dict,
description="元数据(扩展字段)"
)
class Meta:
table = "users"
table_description = "用户表"
# 复合索引
indexes = [
("username", "email"), # 查询优化
]
# 排序规则
ordering = ["-date_joined"]
def __str__(self):
return f"User(id={self.id}, username={self.username})"
def to_dict(self) -> dict:
"""转换为字典(排除敏感信息)"""
return {
"id": self.id,
"username": self.username,
"email": self.email,
"phone": self.phone,
"nickname": self.nickname,
"avatar": self.avatar,
"bio": self.bio,
"is_active": self.is_active,
"is_verified": self.is_verified,
"date_joined": self.date_joined.isoformat() if self.date_joined else None,
"last_login": self.last_login.isoformat() if self.last_login else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
@classmethod
async def get_by_username(cls, username: str) -> Optional["User"]:
"""通过用户名获取用户"""
return await cls.get_or_none(username=username)
@classmethod
async def get_by_email(cls, email: str) -> Optional["User"]:
"""通过邮箱获取用户"""
return await cls.get_or_none(email=email)
@classmethod
async def exists_by_username(cls, username: str) -> bool:
"""检查用户名是否存在"""
return await cls.filter(username=username).exists()
@classmethod
async def exists_by_email(cls, email: str) -> bool:
"""检查邮箱是否存在"""
return await cls.filter(email=email).exists()
执行完以下操作后,就可以在数据库中看到user和aerich(存储迁移历史)表了
python
aerich init -t 16ORMsetting.TORTOISE_ORM
aerich init-db
# 更新模型
aerich migrate -t 16ORMsetting.TORTOISE_ORM
aerich upgrade
CRUD操作
python
# utils_user.py
from models import User
from tortoise.exceptions import IntegrityError
from typing import Optional
async def create_user(
username: str,
email: str,
phone: Optional[str] = None,
password_hash: str = "",
salt: str = "",
nickname: str = "",
avatar: Optional[str] = None,
bio: Optional[str] = None,
is_active: bool = True,
is_superuser: bool = False,
is_verified: bool = False,
permissions: Optional[list] = None,
meta_data: Optional[dict] = None,
) -> Optional[User]:
"""
创建新用户
参数:
username (str): 用户名(唯一)
email (str): 邮箱(唯一)
phone (Optional[str]): 手机号(唯一,可选)
password_hash (str): 密码哈希值
salt (str): 密码盐值
nickname (str): 昵称
avatar (Optional[str]): 头像URL
bio (Optional[str]): 个人简介
is_active (bool): 是否激活
is_superuser (bool): 是否超级用户
is_verified (bool): 是否已验证邮箱
permissions (Optional[list]): 权限列表
meta_data (Optional[dict]): 元数据(扩展字段)
返回:
Optional[User]: 成功返回用户对象,失败返回 None
"""
try:
# 构造用户数据
user_data = {
"username": username,
"email": email,
"phone": phone,
"password_hash": password_hash,
"salt": salt,
"nickname": nickname,
"avatar": avatar,
"bio": bio,
"is_active": is_active,
"is_superuser": is_superuser,
"is_verified": is_verified,
"permissions": permissions or [],
"meta_data": meta_data or {},
}
# 创建用户
user = await User.create(**user_data)
return user
except IntegrityError as e:
# 捕获唯一约束冲突(如重复的 username 或 email)
print(f"创建用户失败:{e}")
return None
except Exception as e:
# 捕获其他异常
print(f"未知错误:{e}")
return None
async def delete_user(user_id: int) -> bool:
"""
根据用户 ID 删除用户
参数:
user_id (int): 用户主键 ID
返回:
bool: 成功返回 True,失败返回 False
"""
try:
# 查找并删除用户
user = await User.get(id=user_id)
await user.delete()
return True
except DoesNotExist:
# 用户不存在
print(f"用户 ID {user_id} 不存在")
return False
except Exception as e:
# 捕获其他异常
print(f"删除用户失败:{e}")
return False
async def update_user(
user_id: int,
username: Optional[str] = None,
email: Optional[str] = None,
phone: Optional[str] = None,
password_hash: Optional[str] = None,
salt: Optional[str] = None,
nickname: Optional[str] = None,
avatar: Optional[str] = None,
bio: Optional[str] = None,
is_active: Optional[bool] = None,
is_superuser: Optional[bool] = None,
is_verified: Optional[bool] = None,
permissions: Optional[list] = None,
meta_data: Optional[dict] = None,
) -> Optional[User]:
"""
根据用户 ID 更新用户信息
参数:
user_id (int): 用户主键 ID
其他字段为可选参数,仅传递需要更新的字段
返回:
Optional[User]: 成功返回更新后的用户对象,失败返回 None
"""
try:
# 查找用户
user = await User.get(id=user_id)
# 动态更新字段(仅更新非 None 的字段)
update_fields = []
field_mapping = {
"username": username,
"email": email,
"phone": phone,
"password_hash": password_hash,
"salt": salt,
"nickname": nickname,
"avatar": avatar,
"bio": bio,
"is_active": is_active,
"is_superuser": is_superuser,
"is_verified": is_verified,
"permissions": permissions,
"meta_data": meta_data,
}
for field_name, value in field_mapping.items():
if value is not None:
setattr(user, field_name, value)
update_fields.append(field_name)
# 保存更改
if update_fields:
await user.save(update_fields=update_fields)
return user
except DoesNotExist:
# 用户不存在
print(f"用户 ID {user_id} 不存在")
return None
except Exception as e:
# 捕获其他异常
print(f"更新用户失败:{e}")
return None
通过接口调用:
python
from utils_user import create_user, update_user, delete_user
@app.post("/create_user")
async def create_user(request):
return create_user(**request.json)
@app.post("/update_user")
async def update_user(request):
return update_user(**request.json)
@app.post("/delete_user/{id}")
async def delete_user(id: int):
return delete_user(id)
查询操作
python
python
from models import User
from tortoise.exceptions import DoesNotExist
from typing import Optional
async def get_user_by_id(user_id: int) -> Optional[User]:
"""
根据用户 ID 查询单条用户数据
参数:
user_id (int): 用户主键 ID
返回:
Optional[User]: 成功返回用户对象,失败返回 None
"""
try:
user = await User.get(id=user_id)
return user
except DoesNotExist:
print(f"用户 ID {user_id} 不存在")
return None
except Exception as e:
print(f"查询用户失败:{e}")
return None
async def search_users(keyword: str) -> List[User]:
"""
根据关键字模糊搜索用户(用户名、邮箱、手机号)
参数:
keyword (str): 搜索关键字
返回:
List[User]: 匹配的用户对象列表
"""
try:
users = await User.filter(
Q(username__icontains=keyword)
| Q(email__icontains=keyword)
| Q(phone__icontains=keyword)
)
return users
except Exception as e:
print(f"搜索用户失败:{e}")
return []
async def list_users(
limit: Optional[int] = None,
offset: Optional[int] = None,
is_active: Optional[bool] = None,
is_superuser: Optional[bool] = None,
is_verified: Optional[bool] = None,
) -> List[User]:
"""
查询多条用户数据(支持分页和条件过滤)
参数:
limit (Optional[int]): 限制返回数量
offset (Optional[int]): 偏移量(用于分页)
is_active (Optional[bool]): 是否激活
is_superuser (Optional[bool]): 是否超级用户
is_verified (Optional[bool]): 是否已验证邮箱
返回:
List[User]: 用户对象列表
"""
try:
# 要是不需要过滤条件,就返回所有用户
query = User.all()
# 添加过滤条件
if is_active is not None:
query = query.filter(is_active=is_active)
if is_superuser is not None:
query = query.filter(is_superuser=is_superuser)
if is_verified is not None:
query = query.filter(is_verified=is_verified)
# 分页处理
if limit is not None:
query = query.limit(limit)
if offset is not None:
query = query.offset(offset)
users = await query
return users
except Exception as e:
print(f"查询用户列表失败:{e}")
return []
ORM关联关系
模拟一个学校系统,包含以下实体:
- 学生(Student):每个学生有唯一的个人信息档案(1对1)。
- 成绩(Grade):一个学生可以有多份成绩记录(1对多)。
- 课程(Course):学生和课程之间是多对多关系(通过成绩表关联)。
python
# models_school.py
from tortoise.models import Model
from tortoise import fields
from typing import Optional, List
from datetime import datetime
class Student(Model):
"""学生模型"""
id = fields.IntField(pk=True, description="主键ID")
name = fields.CharField(max_length=50, null=False, description="姓名")
age = fields.IntField(null=False, description="年龄")
gender = fields.CharField(max_length=10, null=False, description="性别")
email = fields.CharField(max_length=100, unique=True, null=True, description="邮箱")
phone = fields.CharField(max_length=20, unique=True, null=True, description="手机号")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
# 一对一关系:学生 <-> 个人信息档案
profile = fields.OneToOneField("models.StudentProfile", related_name="student", description="个人信息档案")
class Meta:
table = "students"
table_description = "学生表"
ordering = ["-created_at"]
def __str__(self):
return f"Student(id={self.id}, name={self.name})"
def to_dict(self) -> dict:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"age": self.age,
"gender": self.gender,
"email": self.email,
"phone": self.phone,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
class StudentProfile(Model):
"""学生个人信息档案模型(与 Student 一对一)"""
id = fields.IntField(pk=True, description="主键ID")
address = fields.CharField(max_length=200, null=True, description="地址")
emergency_contact = fields.CharField(max_length=50, null=True, description="紧急联系人")
emergency_phone = fields.CharField(max_length=20, null=True, description="紧急联系电话")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
class Meta:
table = "student_profiles"
table_description = "学生个人信息档案表"
def __str__(self):
return f"StudentProfile(id={self.id})"
def to_dict(self) -> dict:
"""转换为字典"""
return {
"id": self.id,
"address": self.address,
"emergency_contact": self.emergency_contact,
"emergency_phone": self.emergency_phone,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
class Course(Model):
"""课程模型"""
id = fields.IntField(pk=True, description="主键ID")
name = fields.CharField(max_length=100, null=False, description="课程名称")
description = fields.TextField(null=True, description="课程描述")
credits = fields.IntField(default=0, description="学分")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
# 多对多关系:学生 <-> 课程(通过 Grade 表关联)
students = fields.ManyToManyField(
"models.Student",
through="models.Grade",
related_name="courses",
description="选课学生"
)
class Meta:
table = "courses"
table_description = "课程表"
ordering = ["name"]
def __str__(self):
return f"Course(id={self.id}, name={self.name})"
def to_dict(self) -> dict:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"description": self.description,
"credits": self.credits,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
class Grade(Model):
"""成绩模型(学生 <-> 课程的中间表)"""
id = fields.IntField(pk=True, description="主键ID")
score = fields.FloatField(null=False, description="分数")
grade_letter = fields.CharField(max_length=2, null=True, description="等级(如 A, B, C)")
exam_date = fields.DateField(null=False, description="考试日期")
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
# 外键:关联学生和课程
student = fields.ForeignKeyField(
"models.Student",
related_name="grades",
on_delete=fields.CASCADE,
description="学生"
)
course = fields.ForeignKeyField(
"models.Course",
related_name="grades",
on_delete=fields.CASCADE,
description="课程"
)
class Meta:
table = "grades"
table_description = "成绩表"
unique_together = ("student", "course", "exam_date") # 防止重复记录
ordering = ["-exam_date"]
def __str__(self):
return f"Grade(id={self.id}, score={self.score})"
def to_dict(self) -> dict:
"""转换为字典"""
return {
"id": self.id,
"score": self.score,
"grade_letter": self.grade_letter,
"exam_date": self.exam_date.isoformat() if self.exam_date else None,
"student_id": self.student_id,
"course_id": self.course_id,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
关联关系的增删改查
创建实体:
python
from models_school import Student, StudentProfile
from datetime import date
async def create_student_with_profile(
name: str,
age: int,
gender: str,
email: Optional[str] = None,
phone: Optional[str] = None,
address: Optional[str] = None,
emergency_contact: Optional[str] = None,
emergency_phone: Optional[str] = None,
) -> Student:
"""
创建学生及其个人信息档案(1对1关系)
参数:
name (str): 姓名
age (int): 年龄
gender (str): 性别
email (Optional[str]): 邮箱
phone (Optional[str]): 手机号
address (Optional[str]): 地址
emergency_contact (Optional[str]): 紧急联系人
emergency_phone (Optional[str]): 紧急联系电话
返回:
Student: 创建的学生对象
"""
try:
# 创建个人信息档案
profile = await StudentProfile.create(
address=address,
emergency_contact=emergency_contact,
emergency_phone=emergency_phone,
)
# 创建学生并关联档案
student = await Student.create(
name=name,
age=age,
gender=gender,
email=email,
phone=phone,
profile=profile,
)
return student
except Exception as e:
print(f"创建学生失败:{e}")
raise
async def create_course(
name: str,
description: Optional[str] = None,
credits: int = 0,
) -> Course:
"""
创建课程
参数:
name (str): 课程名称
description (Optional[str]): 课程描述
credits (int): 学分
返回:
Course: 创建的课程对象
"""
try:
course = await Course.create(
name=name,
description=description,
credits=credits,
)
return course
except Exception as e:
print(f"创建课程失败:{e}")
raise
创建关联关系:
python
from models import Student, Course
from typing import List
async def enroll_student_in_course(student_id: int, course_id: int) -> bool:
"""
学生选课(建立学生与课程的多对多关系)
参数:
student_id (int): 学生 ID
course_id (int): 课程 ID
返回:
bool: 成功返回 True,失败返回 False
"""
try:
student = await Student.get(id=student_id)
course = await Course.get(id=course_id)
# 建立关联关系
await student.courses.add(course)
return True
except Exception as e:
print(f"学生选课失败:{e}")
return False
async def get_student_courses(student_id: int) -> List[Course]:
"""
查询学生选修的所有课程
参数:
student_id (int): 学生 ID
返回:
List[Course]: 课程列表
"""
try:
student = await Student.get(id=student_id).prefetch_related("courses")
return list(student.courses)
except Exception as e:
print(f"查询学生课程失败:{e}")
return []
async def get_course_students(course_id: int) -> List[Student]:
"""
查询选修某门课程的所有学生
参数:
course_id (int): 课程 ID
返回:
List[Student]: 学生列表
"""
try:
course = await Course.get(id=course_id).prefetch_related("students")
return list(course.students)
except Exception as e:
print(f"查询课程学生失败:{e}")
return []
async def create_grade(
student_id: int,
course_id: int,
score: float,
exam_date: date,
grade_letter: Optional[str] = None,
) -> Grade:
"""
创建成绩并关联学生和课程(多对多关系)
参数:
student_id (int): 学生 ID
course_id (int): 课程 ID
score (float): 分数
exam_date (date): 考试日期
grade_letter (Optional[str]): 等级(如 A, B, C)
返回:
Grade: 创建的成绩对象
"""
try:
grade = await Grade.create(
student_id=student_id,
course_id=course_id,
score=score,
exam_date=exam_date,
grade_letter=grade_letter,
)
return grade
except Exception as e:
print(f"创建成绩失败:{e}")
raise