Tortoise-ORM 单表 CRUD 操作

Tortoise-ORM 的单表 CRUD 操作(Create-创建、Read-查询、Update-更新、Delete-删除),以 Student(学生)模型为实战案例,提供完整可运行的模型定义和 CRUD 函数,包含异常处理、日志输出和细节说明,适配 Tortoise-ORM 0.25.0 版本。

一、前置准备:Student 模型定义

首先定义 Student模型,对应数据库中的 students 表,包含学生核心信息(ID、姓名、年龄、邮箱),字段添加约束和描述,便于后续 CRUD 操作和维护。

from tortoise import fields, models

class Student(models.Model):
    """
    学生模型,用于演示 Tortoise-ORM 单表 CRUD 操作
    对应数据库中的 students 表,包含学生基本信息
    """
    # 主键字段:学生ID,自增整数,必填,作为唯一标识
    id = fields.IntField(pk=True, description="学生ID,主键(自增)")
    # 学生姓名:字符串,最大长度50,必填(默认不允许为空)
    name = fields.CharField(max_length=50, description="学生姓名(必填)")
    # 学生年龄:整数,可选(允许为空)
    age = fields.IntField(null=True, description="学生年龄(可选,可为空)")
    # 学生邮箱:字符串,最大长度100,唯一约束,可选(允许为空)
    email = fields.CharField(
        max_length=100, 
        unique=True, 
        null=True, 
        description="学生邮箱(可选,需唯一,可为空)"
    )

    # 模型元数据配置
    class Meta:
        table = "students"  # 自定义数据库表名,固定为 students

    # 自定义字符串表示:打印 Student 对象时,显示核心信息(便于调试)
    def __str__(self):
        return f"Student: {self.name}, Age: {self.age}, Email: {self.email}"

模型关键说明

  • 模型必须继承 tortoise.models.Model,是 Tortoise-ORM 识别模型的核心;
  • pk=True 标记主键,IntField(pk=True) 自动设为自增整数,无需手动赋值;
  • unique=True 给邮箱添加唯一约束,确保所有学生的邮箱不重复,重复插入会抛出异常;
  • null=True表示字段可选,创建学生时可不用传入 age 和 email,数据库中对应字段为 NULL;
  • __str__ 方法:自定义对象的字符串输出格式,调试时打印学生对象可快速查看核心信息。

二、CRUD 操作完整实现

以下是针对 Student 模型的完整 CRUD 函数,每个函数对应一种操作,包含参数说明、返回值、异常捕获和日志输出,确保操作安全、可追溯。所有函数均为异步函数(async def),需结合 await 调用(适配 Tortoise-ORM 异步特性)。

2.1 导入依赖与日志配置

先导入所需模块、模型和异常类,配置日志(便于查看操作记录和错误信息):

from tortoise import Tortoise
from tortoise.exceptions import DoesNotExist, IntegrityError
from models import Student  # 导入上面定义的 Student 模型(路径根据实际项目调整)
import logging

# 配置日志:设置日志级别为 INFO,输出操作记录和错误信息
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)  # 创建日志对象,用于输出CRUD操作日志

2.2创建操作(Create):create_student

功能:创建一条学生记录,支持可选字段(age、email),自动校验邮箱唯一性,捕获创建过程中的异常(如邮箱重复)。

async def create_student(name: str, age: int = None, email: str = None) -> Student:
    """
    创建学生记录(核心:新增数据)
    Args:
        name: 学生姓名,必填(字符串,最大长度50)
        age: 学生年龄,可选(整数,可为空,无默认值)
        email: 学生邮箱,可选(字符串,最大长度100,需唯一,可为空)
    Returns:
        Student: 创建成功后的学生对象(包含所有字段信息,如自动生成的id)
    Raises:
        IntegrityError: 邮箱重复(违反unique约束)或其他数据库完整性错误
        Exception: 未知错误(如字段格式错误)
    """
    try:
        # 调用 Student.create() 异步创建学生记录,返回创建后的对象
        student = await Student.create(name=name, age=age, email=email)
        # 输出日志:记录创建成功的学生信息
        logger.info(f"创建学生成功 | 姓名: {student.name}, 年龄: {student.age}, 邮箱: {student.email}, ID: {student.id}")
        return student
    except IntegrityError as e:
        # 捕获邮箱重复等完整性约束错误
        logger.error(f"创建学生失败 | 原因: {str(e)}(大概率是邮箱重复,请更换邮箱)")
        raise  # 重新抛出异常,让调用者处理(如返回错误响应)
    except Exception as e:
        # 捕获其他未知错误
        logger.error(f"创建学生时发生未知错误 | 原因: {str(e)}")
        raise

2.3查询操作(Read):4个常用查询函数

提供4种查询场景,覆盖「精确查询、模糊查询、全量查询」,满足日常开发中大部分查询需求,均捕获「学生不存在」等异常。

(1)get_student:根据ID精确查询单个学生

async def get_student(student_id: int) -> Student:
    """
    根据学生ID精确查询单个学生(核心:单条数据查询)
    Args:
        student_id: 学生ID(主键,整数)
    Returns:
        Student: 查询到的学生对象
    Raises:
        DoesNotExist: 学生ID不存在,查询不到数据
        Exception: 未知查询错误
    """
    try:
        # 调用 Student.get() 异步查询:根据id精确匹配(主键查询效率最高)
        student = await Student.get(id=student_id)
        logger.info(f"查询学生成功 | ID: {student.id}, 姓名: {student.name}")
        return student
    except DoesNotExist:
        # 捕获「学生不存在」错误
        logger.error(f"查询学生失败 | 原因: 学生ID {student_id} 不存在")
        raise
    except Exception as e:
        logger.error(f"查询学生时发生未知错误 | 原因: {str(e)}")
        raise

(2)get_students_by_name:根据姓名模糊查询学生列表

async def get_students_by_name(name: str) -> list[Student]:
    """
    根据姓名模糊查询学生列表(核心:模糊查询,支持部分匹配)
    Args:
        name: 学生姓名(字符串,支持部分匹配,如传入"张",查询所有姓张的学生)
    Returns:
        list[Student]: 匹配成功的学生对象列表(无匹配时返回空列表)
    Raises:
        Exception: 未知查询错误
    """
    try:
        # 调用 Student.filter() 异步过滤:name__contains 表示「包含」(模糊匹配)
        # 其他模糊匹配方式:name__startswith(以...开头)、name__endswith(以...结尾)
        students = await Student.filter(name__contains=name)
        logger.info(f"模糊查询学生成功 | 姓名包含: '{name}', 共查询到 {len(students)} 名学生")
        return students
    except Exception as e:
        logger.error(f"模糊查询学生时发生错误 | 原因: {str(e)}")
        raise

(3)get_all_students:查询所有学生列表

async def get_all_students() -> list[Student]:
    """
    查询所有学生列表(核心:全量查询)
    Returns:
        list[Student]: 所有学生对象列表(无学生时返回空列表)
    Raises:
        Exception: 未知查询错误
    """
    try:
        # 调用 Student.all() 异步查询所有学生,返回列表
        students = await Student.all()
        logger.info(f"查询所有学生成功 | 共查询到 {len(students)} 名学生")
        return students
    except Exception as e:
        logger.error(f"查询所有学生时发生错误 | 原因: {str(e)}")
        raise

2.4更新操作(Update):update_student

功能:根据学生ID更新学生信息,支持「部分字段更新」(无需传入所有字段),允许将age、email设为NULL,校验邮箱唯一性。

async def update_student(student_id: int, name: str = None, age: int = None, email: str = None) -> Student:
    """
    更新学生信息(核心:部分字段更新,不改变未传入的字段)
    Args:
        student_id: 学生ID(主键,必须传入,指定要更新的学生)
        name: 新姓名,可选(不传入则不更新姓名)
        age: 新年龄,可选(传入None则将年龄设为NULL,不传入则不更新)
        email: 新邮箱,可选(传入None则将邮箱设为NULL,不传入则不更新,需唯一)
    Returns:
        Student: 更新后的学生对象
    Raises:
        DoesNotExist: 学生ID不存在
        IntegrityError: 邮箱重复(违反unique约束)
        Exception: 未知更新错误
    """
    try:
        # 1. 先根据ID查询到要更新的学生对象(必须先查询,再修改)
        student = await Student.get(id=student_id)
        
        # 2. 按需更新字段:只更新传入的非None字段
        if name:  # 姓名不为空则更新
            student.name = name
        if age is not None:  # 允许将年龄设为NULL(传入None时执行更新)
            student.age = age
        if email is not None:  # 允许将邮箱设为NULL(传入None时执行更新)
            student.email = email
        
        # 3. 调用 save() 异步保存更新(仅更新修改过的字段,效率高)
        await student.save()
        logger.info(f"更新学生成功 | ID: {student.id}, 新信息: 姓名={student.name}, 年龄={student.age}, 邮箱={student.email}")
        return student
    except DoesNotExist:
        logger.error(f"更新学生失败 | 原因: 学生ID {student_id} 不存在")
        raise
    except IntegrityError as e:
        logger.error(f"更新学生失败 | 原因: {str(e)}(大概率是邮箱重复,请更换邮箱)")
        raise
    except Exception as e:
        logger.error(f"更新学生时发生未知错误 | 原因: {str(e)}")
        raise

2.5删除操作(Delete):delete_student

功能:根据学生ID删除学生记录,先校验学生是否存在,避免无效删除操作,删除后输出日志。

async def delete_student(student_id: int) -> None:
    """
    删除学生记录(核心:删除单条数据)
    Args:
        student_id: 学生ID(主键,指定要删除的学生)
    Returns:
        None: 无返回值(删除成功即完成)
    Raises:
        DoesNotExist: 学生ID不存在,无法删除
        Exception: 未知删除错误
    """
    try:
        # 1. 先根据ID查询到要删除的学生对象(校验学生是否存在)
        student = await Student.get(id=student_id)
        # 2. 调用 delete() 异步删除学生记录
        await student.delete()
        logger.info(f"删除学生成功 | ID: {student.id}, 姓名: {student.name}")
    except DoesNotExist:
        logger.error(f"删除学生失败 | 原因: 学生ID {student_id} 不存在,无需删除")
        raise
    except Exception as e:
        logger.error(f"删除学生时发生未知错误 | 原因: {str(e)}")
        raise

三、CRUD 操作调用示例

所有 CRUD 函数均为异步函数,需在「异步环境」中调用(如结合 FastAPI 接口、asyncio 主函数)。以下是完整的调用示例,可直接复制运行测试(需先初始化 Tortoise-ORM):

import asyncio

# Tortoise-ORM 初始化配置(需与项目配置一致)
TORTOISE_ORM = {
    "connections": {
        "default": "sqlite://db.sqlite3"  # 开发环境使用SQLite,无需启动数据库服务
    },
    "apps": {
        "models": {
            "models": ["models", "aerich.models"],  # 导入Student模型和Aerich迁移模型
            "default_connection": "default",
        }
    }
}

async def main():
    """测试CRUD操作的主函数(异步)"""
    # 1. 初始化 Tortoise-ORM
    await Tortoise.init(config=TORTOISE_ORM)
    # 2. 生成数据库表(开发环境,仅首次运行需执行)
    await Tortoise.generate_schemas()

    try:
        # ------------------- 测试创建学生 -------------------
        student1 = await create_student(
            name="张三",
            age=18,
            email="zhangsan@test.com"
        )

        # ------------------- 测试查询操作 -------------------
        # 按ID查询
        get_stu = await get_student(student1.id)
        print("按ID查询结果:", get_stu)

        # 模糊查询(查询姓张的学生)
        like_stu = await get_students_by_name("张")
        print("模糊查询结果:", [str(stu) for stu in like_stu])

        # 查询所有学生
        all_stu = await get_all_students()
        print("所有学生:", [str(stu) for stu in all_stu])

        # ------------------- 测试更新学生 -------------------
        update_stu = await update_student(
            student_id=student1.id,
            age=19,  # 只更新年龄,其他字段不变
            email="zhangsan_update@test.com"  # 更新邮箱
        )
        print("更新后学生:", update_stu)

        # ------------------- 测试删除学生 -------------------
        await delete_student(student_id=student1.id)
        print("删除学生成功")

    except Exception as e:
        print(f"测试失败: {str(e)}")
    finally:
        # 关闭 Tortoise-ORM 连接
        await Tortoise.close_connections()

# 运行异步主函数
if __name__ == "__main__":
    asyncio.run(main())

四、关键注意事项(避坑重点)

  • 异步调用:所有 CRUD 函数和 Tortoise-ORM 方法(如 creategetsave)均为异步,必须用 await 调用,且需在异步函数/异步环境中执行(如 async def 函数、FastAPI 接口)。
  • 异常处理:函数中已捕获常见异常(DoesNotExistIntegrityError),但调用时仍需捕获异常(如在 FastAPI 接口中返回错误响应),避免程序崩溃。
  • 邮箱唯一性:email 字段有 unique=True 约束,创建/更新时传入重复邮箱会抛出 IntegrityError,需提前校验或捕获异常处理。
  • 部分更新update_student 函数中,age is not Noneemail is not None 是关键——若需将字段设为 NULL,传入 age=None 即可;若不更新该字段,不传入参数即可。
  • 数据库初始化:测试前需初始化 Tortoise-ORM,并生成 students 表(可通过 generate_schemas=True 或 Aerich 迁移工具)。
  • 生产环境:生产环境中,禁止使用generate_schemas=True,需通过 Aerich 迁移工具管理表结构;同时需加强异常捕获,返回友好的错误提示。

五、常见问题排查

  • 问题1:调用函数时报「RuntimeWarning: coroutine ... was never awaited」:忘记给异步函数加await,需在调用时写 await create_student(...)
  • 问题2:创建/更新学生时报「IntegrityError」:大概率是邮箱重复,检查邮箱是否已存在,或更换邮箱值。
  • 问题3:查询/更新/删除时报「DoesNotExist」:传入的 student_id 不存在,检查 ID 是否正确,或先通过 get_all_students 查看所有学生 ID。
  • 问题4:提示「No module named 'models'」from models import Student 路径错误,根据实际项目结构调整(如 from app.models import Student)。
  • 问题5:无法生成 students 表:确保 Tortoise 初始化时,模型所在模块已添加到 apps.models 列表中。
posted @ 2026-02-04 16:21  向闲而过  阅读(0)  评论(0)    收藏  举报