1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
| """ Tortoise-ORM 教学演示路由
本文件用 4 个模型演示 Tortoise-ORM 的常用操作。
模型关系速览:
Teacher(教师) ──1:N──> Course(课程) ──M:N──> Student(学生) ──N:1──> Clas(班级)
具体字段与 related_name: Student.clas = FK("models.Clas", related_name="students") → 正向: student.clas 拿到班级对象 → 反向: clas.students 拿到该班所有学生(QuerySet)
Student.courses = M2M("models.Course", related_name="students") → 正向: student.courses 拿到该生所选课程(QuerySet) → 反向: course.students 拿到选了该课的所有学生(QuerySet)
Course.teacher = FK("models.Teacher", related_name="courses") → 正向: course.teacher 拿到教师对象 → 反向: teacher.courses 拿到该教师所教课程(QuerySet)
注意:related_name 就是"反向关联的名字",定义在"多"的那一侧, 但挂在"一"的那一侧使用,初学时很容易搞混。 """ from fastapi import APIRouter, HTTPException, Body from tortoise.expressions import Q from tortoise.functions import Count from pydantic import BaseModel from typing import Optional from app.models import Student, Course, Teacher, Clas
router = APIRouter()
class StudentIn(BaseModel): name: str age: int pwd: str sno: int clas_id: int
class CourseIn(BaseModel): name: str teacher_id: int
class TeacherIn(BaseModel): name: str pwd: str tno: int
class ClasIn(BaseModel): name: str
class StudentUpdate(BaseModel): name: Optional[str] = None age: Optional[int] = None
@router.post("/students", tags=["1-基础CRUD"]) async def create_student(data: StudentIn): """ 【接口用途】:新增一名学生 【演示特性】:Model.create(),直接创建并插入数据库,返回已含 id 的新对象 【底层SQL】:INSERT INTO student (name, age, pwd, sno, clas_id) VALUES (?, ?, ?, ?, ?) 【初学提示】:create() 是"构造 + 保存"一步到位,等价于 Student(**kwargs) 再 .save() """ student = await Student.create( name=data.name, age=data.age, pwd=data.pwd, sno=data.sno, clas_id=data.clas_id, ) return {"id": student.id, "name": student.name}
@router.post("/students/bulk", tags=["2-批量操作"]) async def bulk_create_students(data: list[StudentIn] = Body(...)): """ 【接口用途】:一次性批量导入多名学生(如新生入学批量录入) 【演示特性】:Model.bulk_create(),一次调用插入多条记录 【底层SQL】:多条 INSERT INTO student (...) VALUES (...)(SQLite 不支持多行 INSERT) 【初学提示】:bulk_create 比循环 create 快,因为减少了 Python↔数据库的往返次数。 注意:传入的是"未保存的 Model 实例列表",不是字典列表。 """ objects = [ Student(name=d.name, age=d.age, pwd=d.pwd, sno=d.sno, clas_id=d.clas_id) for d in data ] await Student.bulk_create(objects) return {"created_count": len(objects)}
@router.put("/students/bulk-age", tags=["2-批量操作"]) async def bulk_update_student_age(ids: list[int] = Body(...), age: int = Body(...)): """ 【接口用途】:批量修改多名学生的年龄(如统一升级) 【演示特性】:Model.bulk_update(),批量修改指定字段 【底层SQL】:逐条 UPDATE student SET age=? WHERE id=? 【初学提示】:步骤:1) 查出对象列表 → 2) 在 Python 中修改属性 → 3) bulk_update(list, ["字段名"]) 第二个参数 ["age"] 告诉 ORM 只更新 age 字段,其他字段不动。 """ students = await Student.filter(id__in=ids) for s in students: s.age = age await Student.bulk_update(students, ["age"]) return {"updated_count": len(students)}
@router.post("/courses/bulk", tags=["2-批量操作"]) async def bulk_create_courses(data: list[CourseIn] = Body(...)): """ 【接口用途】:批量创建课程(如新学期排课) 【演示特性】:bulk_create() 同样适用于 Course 等任意模型 【底层SQL】:多条 INSERT INTO course (name, teacher_id) VALUES (?, ?) """ objects = [Course(name=d.name, teacher_id=d.teacher_id) for d in data] await Course.bulk_create(objects) return {"created_count": len(objects)}
@router.get("/students/filter", tags=["3-高级过滤"]) async def filter_students( min_age: Optional[int] = None, max_age: Optional[int] = None, clas_id: Optional[int] = None, order: str = "id", limit: int = 20, offset: int = 0, ): """ 【接口用途】:按年龄范围、班级等条件筛选学生,支持排序和分页 【演示特性】:链式调用 .filter().order_by().limit().offset() 【底层SQL】:SELECT ... FROM student WHERE age>=? AND age<=? AND clas_id=? ORDER BY id LIMIT 20 OFFSET 0 【初学提示】:Tortoise 的链式调用是"惰性求值",调用 .filter() 只是构建查询条件, 并不会真的去查数据库,直到 await 时才拼接成一条 SQL 发出。 """ qs = Student.all() if min_age is not None: qs = qs.filter(age__gte=min_age) if max_age is not None: qs = qs.filter(age__lte=max_age) if clas_id is not None: qs = qs.filter(clas_id=clas_id) students = await qs.order_by(order).limit(limit).offset(offset) return {"count": len(students), "students": [s.name for s in students]}
@router.get("/teachers/search", tags=["3-高级过滤"]) async def search_teachers(keyword: Optional[str] = None): """ 【接口用途】:按姓名模糊搜索教师(如输入"张"搜出所有张老师) 【演示特性】:__icontains,不区分大小写的模糊匹配 【底层SQL】:SELECT ... FROM teacher WHERE name LIKE '%张%'(SQLite 对 ASCII 不敏感) 【初学提示】:__icontains 中的 i 代表 insensitive(不敏感),contains 代表包含。 常用查找运算符:__exact(精确)、__contains(区分大小写包含)、__icontains(不区分)。 """ if not keyword: teachers = await Teacher.all() else: teachers = await Teacher.filter(name__icontains=keyword) return {"teachers": [t.name for t in teachers]}
@router.get("/courses/search", tags=["3-高级过滤"]) async def search_courses(keyword: Optional[str] = None, min_teacher_tno: Optional[int] = None): """ 【接口用途】:按课程名模糊搜索,并可限定授课教师的教师号下限 【演示特性】:__icontains + 跨表 __gte 组合过滤 【底层SQL】:SELECT ... FROM course WHERE name LIKE '%数学%' AND teacher.tno >= 100 【初学提示】:teacher__tno__gte 意思是"跨到 teacher 表,取 tno 字段,大于等于"。 双下划线 __ 是 Tortoise 跨表的语法分隔符,每一段代表一层关系跳转。 """ qs = Course.all() if keyword: qs = qs.filter(name__icontains=keyword) if min_teacher_tno is not None: qs = qs.filter(teacher__tno__gte=min_teacher_tno) courses = await qs return {"courses": [{"id": c.id, "name": c.name} for c in courses]}
@router.get("/students/by-classes", tags=["3-高级过滤"]) async def students_by_classes(clas_ids: str): """ 【接口用途】:查询属于多个班级的所有学生(如查"1班和2班"的学生) 【演示特性】:__in,SQL 的 IN (v1, v2, ...) 多值筛选 【底层SQL】:SELECT ... FROM student WHERE clas_id IN (1, 2) 【初学提示】:clas_ids 传逗号分隔字符串如 "1,2,3",代码中 split 后转成列表。 __in 只接受列表/元组/集合,不能传字符串。 """ ids = [int(x) for x in clas_ids.split(",")] students = await Student.filter(clas_id__in=ids) return {"students": [s.name for s in students]}
@router.get("/students/exclude", tags=["3-高级过滤"]) async def exclude_students(min_age: int = 0): """ 【接口用途】:查询年龄不在指定范围内的学生(如排除 18 岁以下的) 【演示特性】:.exclude(),排除符合条件的记录,生成 NOT 子句 【底层SQL】:SELECT ... FROM student WHERE NOT (age >= ?) 【初学提示】:.exclude() 和 .filter() 用法一样,只是逻辑取反。 多个 .exclude() 之间是 AND 关系,不是 OR。 """ students = await Student.all().exclude(age__gte=min_age) return {"students": [s.name for s in students]}
@router.get("/students/q-search", tags=["4-Q对象"]) async def q_search_students(keyword: Optional[str] = None, min_age: Optional[int] = None): """ 【接口用途】:按姓名模糊搜索 OR 按学号精确搜索,并可限定最低年龄 【演示特性】:Q() 对象实现 OR 条件,一个 .filter() 里同时满足"或"逻辑 【底层SQL】:WHERE (name LIKE '%张%' OR sno = 2021001) AND age >= 18 【初学提示】:普通 .filter(a=1, b=2) 是 AND 关系,无法表达 OR。 Q(a=1) | Q(b=2) 就是 OR;Q(a=1) & Q(b=2) 就是 AND;~Q(a=1) 就是 NOT。 Q 对象可以和普通 .filter() 混用,后者自动 AND 上去。 """ qs = Student.all() if keyword: cond = Q(name__icontains=keyword) if keyword.isdigit(): cond |= Q(sno=int(keyword)) qs = qs.filter(cond) if min_age is not None: qs = qs.filter(age__gte=min_age) students = await qs return {"students": [s.name for s in students]}
@router.get("/teachers/q-filter", tags=["4-Q对象"]) async def q_filter_teachers(name_kw: Optional[str] = None, tno_min: Optional[int] = None): """ 【接口用途】:查询姓名含关键词 AND 教师号不低于某值的教师 【演示特性】:Q() & Q(),AND 组合(演示语法,实际此处等价于链式 .filter()) 【底层SQL】:WHERE name LIKE '%李%' AND tno >= 100 【初学提示】:Q() & Q() 的 AND 组合在语义上和两次 .filter() 等价, 但 Q 对象的优势在于可以和 | (OR) 混用,如 (Q(a=1) & Q(b=2)) | Q(c=3)。 """ qs = Teacher.all() if name_kw and tno_min is not None: qs = qs.filter(Q(name__icontains=name_kw) & Q(tno__gte=tno_min)) elif name_kw: qs = qs.filter(name__icontains=name_kw) teachers = await qs return {"teachers": [t.name for t in teachers]}
@router.get("/related-name-explained", tags=["5-related_name详解"]) async def related_name_explained(): """ 【接口用途】:一次性演示所有 related_name 的正向和反向用法 【演示特性】:related_name 决定了"从对方模型反查"时用的属性名
related_name 核心规则:
定义在模型 A 的 ForeignKey/M2M 字段上: field = ForeignKeyField("models.B", related_name="xxx")
含义: 正向:A 的实例 → a.field → 拿到 B 的对象(FK)或 QuerySet(M2M) 反向:B 的实例 → b.xxx → 拿到关联 A 的 QuerySet
本项目的三个 related_name:
① Student.clas = FK("models.Clas", related_name="students") 正向:student.clas → 班级对象(一个 Clas 实例) 反向:clas.students → 该班所有学生(QuerySet[Student])
② Student.courses = M2M("models.Course", related_name="students") 正向:student.courses → 该生所选课程(QuerySet[Course]) 反向:course.students → 选了该课的学生(QuerySet[Student])
③ Course.teacher = FK("models.Teacher", related_name="courses") 正向:course.teacher → 教师对象(一个 Teacher 实例) 反向:teacher.courses → 该教师所教课程(QuerySet[Course])
初学避坑: - related_name 写在"多"方(Student),但挂在"一"方(Clas)上用 - 反向查出来的是 QuerySet,需要 await 才能拿到数据 - 如果忘了设 related_name,Tortoise 会自动生成默认名,但不推荐依赖默认值 """ example = { "Student→Clas 正向": "student.clas → Clas 对象", "Clas→Student 反向": "clas.students → QuerySet[Student]", "Student→Course 正向": "student.courses → QuerySet[Course]", "Course→Student 反向": "course.students → QuerySet[Student]", "Course→Teacher 正向": "course.teacher → Teacher 对象", "Teacher→Course 反向": "teacher.courses → QuerySet[Course]", } return {"related_name规则": example}
@router.get("/students/{student_id}/forward-fk", tags=["5-related_name详解"]) async def student_forward_fk(student_id: int): """ 【接口用途】:查一名学生,并展示正向 FK 查询,通过 student.clas 拿到班级 【演示特性】:正向 FK,用字段名直接访问关联对象 【底层SQL】:先 SELECT ... FROM student WHERE id=?,再 SELECT ... FROM clas WHERE id=? 【初学提示】:student.clas 是一个 Clas 对象,可以直接 .name 取属性。 但注意:如果没做预加载,访问 student.clas 会额外发一条 SQL(N+1 问题)。 预加载方案见下一节 select_related / prefetch_related。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") clas = await student.clas return {"student": student.name, "class": clas.name if clas else None}
@router.get("/classes/{clas_id}/reverse-fk", tags=["5-related_name详解"]) async def clas_reverse_fk(clas_id: int): """ 【接口用途】:查一个班级,并展示反向 FK 查询,通过 clas.students 拿到所有学生 【演示特性】:反向 FK,用 related_name 访问关联的多个对象 【底层SQL】:先 SELECT ... FROM clas WHERE id=?,再 SELECT ... FROM student WHERE clas_id=? 【初学提示】:clas.students 是 QuerySet,需要 await 才能拿到列表。 这就是 related_name="students" 的作用,让 Clas 模型凭空多出 .students 属性。 """ clas = await Clas.get_or_none(id=clas_id) if not clas: raise HTTPException(404, "班级不存在") students = await clas.students.all() return {"class": clas.name, "students": [s.name for s in students]}
@router.get("/students/{student_id}/forward-m2m", tags=["5-related_name详解"]) async def student_forward_m2m(student_id: int): """ 【接口用途】:查一名学生,展示正向 M2M,通过 student.courses 拿到所选课程 【演示特性】:正向 M2M,字段名 courses 直接就是 QuerySet 【底层SQL】:SELECT ... FROM course JOIN student_course ON ... WHERE student_course.student_id=? 【初学提示】:M2M 的正向和反向都是 QuerySet,因为多对多双方都是"多"。 student.courses.add(course) 选课,student.courses.remove(course) 退课。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") courses = await student.courses.all() return {"student": student.name, "courses": [c.name for c in courses]}
@router.get("/courses/{course_id}/reverse-m2m", tags=["5-related_name详解"]) async def course_reverse_m2m(course_id: int): """ 【接口用途】:查一门课程,展示反向 M2M,通过 course.students 拿到选课学生 【演示特性】:反向 M2M,related_name="students" 让 Course 拥有 .students 属性 【底层SQL】:SELECT ... FROM student JOIN student_course ON ... WHERE student_course.course_id=? 【初学提示】:虽然 Course 模型里没有写 students 字段,但 Student.courses 的 related_name="students" 让 Tortoise 自动给 Course 加上了 .students 反向关联。 """ course = await Course.get_or_none(id=course_id) if not course: raise HTTPException(404, "课程不存在") students = await course.students.all() return {"course": course.name, "students": [s.name for s in students]}
@router.get("/courses/with-teacher", tags=["6-关联预加载"]) async def courses_with_teacher(): """ 【接口用途】:查询所有课程,同时带出每门课的授课教师姓名 【演示特性】:select_related("teacher"),正向 FK 预加载,一次 JOIN 查完 【底层SQL】:SELECT ... FROM course LEFT OUTER JOIN teacher ON course.teacher_id=teacher.id 【初学提示】:不用 select_related 时,查 N 门课 → 再查 N 次教师 → 共 N+1 条 SQL。 加了 select_related 后,只需 1 条 JOIN SQL,性能大幅提升。 注意:select_related 只能用于 ForeignKey / OneToOne 的正向关系("多对一"方向)。 """ courses = await Course.all().select_related("teacher") return [ {"course": c.name, "teacher": c.teacher.name if c.teacher else None} for c in courses ]
@router.get("/students/with-class", tags=["6-关联预加载"]) async def students_with_class(): """ 【接口用途】:查询所有学生,同时带出每个学生所属的班级名 【演示特性】:select_related("clas"),学生→班级 正向 FK 预加载 【底层SQL】:SELECT ... FROM student LEFT OUTER JOIN clas ON student.clas_id=clas.id 【初学提示】:select_related 的参数是 FK 字段名(clas),不是 related_name。 预加载后,student.clas 已经在内存中,访问时不再额外发 SQL。 """ students = await Student.all().select_related("clas") return [ {"student": s.name, "class": s.clas.name if s.clas else None} for s in students ]
@router.get("/classes/with-students", tags=["6-关联预加载"]) async def classes_with_students(): """ 【接口用途】:查询所有班级,同时带出每个班级的所有学生 【演示特性】:prefetch_related("students"),反向一对多预加载 【底层SQL】:① SELECT ... FROM clas ② SELECT ... FROM student WHERE clas_id IN (1,2,...) 【初学提示】:prefetch_related 发 2 条 SQL:先查主表,再一次性查所有关联数据。 比 N+1 的 N 次查询好得多。参数 "students" 是 related_name,不是 FK 字段名。 注意:prefetch_related 用于反向关系(一对多、多对多),select_related 用于正向 FK。 """ classes = await Clas.all().prefetch_related("students") return [ {"class": c.name, "students": [s.name for s in c.students]} for c in classes ]
@router.get("/students/with-courses", tags=["6-关联预加载"]) async def students_with_courses(): """ 【接口用途】:查询所有学生,同时带出每个学生已选的课程列表 【演示特性】:prefetch_related("courses"),M2M 正向预加载 【底层SQL】:① SELECT ... FROM student ② 查中间表 ③ SELECT ... FROM course WHERE id IN (...) 【初学提示】:M2M 的 prefetch_related 会发 3 条 SQL(主表 + 中间表 + 关联表), 但仍比对每个学生单独 .courses.all() 的 N+1 模式高效得多。 """ students = await Student.all().prefetch_related("courses") return [ {"student": s.name, "courses": [c.name for c in s.courses]} for s in students ]
@router.get("/teachers/with-courses", tags=["6-关联预加载"]) async def teachers_with_courses(): """ 【接口用途】:查询所有教师,同时带出每位教师所教的所有课程 【演示特性】:prefetch_related("courses"),反向一对多预加载 【底层SQL】:① SELECT ... FROM teacher ② SELECT ... FROM course WHERE teacher_id IN (1,2,...) 【初学提示】:Teacher 模型里并没有写 courses 字段! 但 Course.teacher 的 related_name="courses" 让 Tortoise 自动在 Teacher 上 生成了 .courses 反向关联,所以 prefetch_related("courses") 可以直接用。 """ teachers = await Teacher.all().prefetch_related("courses") return [ {"teacher": t.name, "courses": [c.name for c in t.courses]} for t in teachers ]
@router.get("/students/by-teacher", tags=["7-跨表过滤"]) async def students_by_teacher(teacher_name: str): """ 【接口用途】:查询选了某位老师所教课程的所有学生(如"查选了张老师课的学生") 【演示特性】:courses__teacher__name,双下划线穿越 M2M + FK 两层关系
关系链拆解: Student →[courses]→ Course →[teacher]→ Teacher → .name
第1步:courses__ 从 Student 出发,沿 M2M 字段 courses 跳到 Course 表 第2步:teacher__ 从 Course 出发,沿 FK 字段 teacher 跳到 Teacher 表 第3步:name= 在 Teacher 表上筛选 name = "张老师"
【底层SQL】:SELECT student.* FROM student JOIN student_course ON student.id = student_course.student_id JOIN course ON student_course.course_id = course.id JOIN teacher ON course.teacher_id = teacher.id WHERE teacher.name = '张老师' 【初学提示】:.distinct() 用来去重:同一老师教多门课,同一学生可能选了其中多门, 不加 distinct 会出现重复。跨表过滤产生 INNER JOIN,未选课的学生不会出现。 """ students = await Student.filter(courses__teacher__name=teacher_name).distinct() return {"teacher": teacher_name, "students": [s.name for s in students]}
@router.get("/courses/by-class", tags=["7-跨表过滤"]) async def courses_by_class(clas_name: str): """ 【接口用途】:查询某个班级的学生选了哪些课程(如"查1班学生选的所有课") 【演示特性】:students__clas__name,从 Course 反向穿越 M2M + FK
关系链拆解: Course →[students]→ Student →[clas]→ Clas → .name
第1步:students__ 从 Course 出发,沿 related_name="students" 跳到 Student 表 第2步:clas__ 从 Student 出发,沿 FK 字段 clas 跳到 Clas 表 第3步:name= 在 Clas 表上筛选 name = "1班"
【底层SQL】:SELECT course.* FROM course JOIN student_course ON course.id = student_course.course_id JOIN student ON student_course.student_id = student.id JOIN clas ON student.clas_id = clas.id WHERE clas.name = '1班' 【初学提示】:students__ 用的是 related_name,不是字段名。 跨表时正向用字段名,反向用 related_name,和预加载的参数规则一致。 """ courses = await Course.filter(students__clas__name=clas_name).distinct() return {"class": clas_name, "courses": [c.name for c in courses]}
@router.get("/students/by-course-name", tags=["7-跨表过滤"]) async def students_by_course_name(course_name: str): """ 【接口用途】:查询选了某门课(模糊匹配课程名)的所有学生 【演示特性】:courses__name__icontains,跨 M2M + 模糊匹配
关系链拆解: Student →[courses]→ Course → .name (模糊匹配)
第1步:courses__ 从 Student 出发,沿 M2M 字段 courses 跳到 Course 表 第2步:name__icontains 在 Course 表上做模糊匹配
【底层SQL】:SELECT student.* FROM student JOIN student_course ON student.id = student_course.student_id JOIN course ON student_course.course_id = course.id WHERE course.name LIKE '%数学%' 【初学提示】:跨表过滤和跨表预加载的 __ 语法完全一致, 区别:预加载是"把关联数据一起查回来",过滤是"用关联数据做 WHERE 条件"。 """ students = await Student.filter(courses__name__icontains=course_name).distinct() return {"course_keyword": course_name, "students": [s.name for s in students]}
@router.get("/courses/by-teacher-tno", tags=["7-跨表过滤"]) async def courses_by_teacher_tno(min_tno: int): """ 【接口用途】:查询教师号不低于某值的所有教师所教的课程 【演示特性】:teacher__tno__gte,跨一层 FK + 范围查找
关系链拆解: Course →[teacher]→ Teacher → .tno (>= min_tno)
第1步:teacher__ 从 Course 出发,沿 FK 字段 teacher 跳到 Teacher 表 第2步:tno__gte 在 Teacher 表上筛选 tno >= min_tno
【底层SQL】:SELECT course.* FROM course JOIN teacher ON course.teacher_id = teacher.id WHERE teacher.tno >= 100 【初学提示】:这是最简单的跨表过滤,只跨一层 FK。 理解了这一层,多层跨表就是不断用 __ 往下接而已。 """ courses = await Course.filter(teacher__tno__gte=min_tno) return {"courses": [{"id": c.id, "name": c.name} for c in courses]}
@router.get("/classes/student-count", tags=["8-聚合统计"]) async def class_student_count(): """ 【接口用途】:统计每个班级各有多少学生 【演示特性】:.annotate(student_count=Count("students")),分组计数 【底层SQL】:SELECT clas.*, COUNT(student.id) AS student_count FROM clas LEFT OUTER JOIN student ON student.clas_id = clas.id GROUP BY clas.id 【初学提示】:annotate 的参数名 "students" 是 related_name,和 prefetch_related 一致。 annotate 不会单独发 SQL,而是把 COUNT 嵌入主查询的 SELECT 和 GROUP BY 中。 结果中每个班级对象会多出一个 .student_count 属性。 """ classes = await Clas.all().annotate(student_count=Count("students")) return [ {"class": c.name, "student_count": c.student_count} for c in classes ]
@router.get("/teachers/course-count", tags=["8-聚合统计"]) async def teacher_course_count(): """ 【接口用途】:统计每位教师各教多少门课 【演示特性】:.annotate(course_count=Count("courses")),related_name 反向计数 【底层SQL】:SELECT teacher.*, COUNT(course.id) AS course_count FROM teacher LEFT OUTER JOIN course ON course.teacher_id = teacher.id GROUP BY teacher.id 【初学提示】:未带课的教师 course_count 为 0,因为 LEFT JOIN 保留了左表所有行。 如果用 INNER JOIN 则不会出现 0,但 Tortoise 的 annotate 默认用 LEFT JOIN。 """ teachers = await Teacher.all().annotate(course_count=Count("courses")) return [ {"teacher": t.name, "course_count": t.course_count} for t in teachers ]
@router.get("/students/values", tags=["8-聚合统计"]) async def student_values(): """ 【接口用途】:只取所有学生的姓名和年龄,不要其他字段,也不要 ORM 对象 【演示特性】:.values("name", "age"),返回字典列表,不构造 Model 实例 【底层SQL】:SELECT student.name, student.age FROM student 【初学提示】:values() 比 all() 轻量,省去构造 ORM 对象的开销, 返回 [{"name": "张三", "age": 20}, ...] 纯字典,适合只读场景。 """ rows = await Student.all().values("name", "age") return {"students": rows}
@router.get("/students/values-list", tags=["8-聚合统计"]) async def student_values_list(): """ 【接口用途】:只取所有学生的姓名和年龄,返回元组列表(比 values 更轻量) 【演示特性】:.values_list("name", "age"),返回元组列表 【底层SQL】:SELECT student.name, student.age FROM student 【初学提示】:返回 [("张三", 20), ("李四", 21), ...],字段顺序与参数一致。 如果只取一个字段,可以加 flat=True 直接返回值列表:["张三", "李四", ...]。 """ rows = await Student.all().values_list("name", "age") return {"students": rows}
@router.get("/classes/student-count-values", tags=["8-聚合统计"]) async def class_student_count_values(): """ 【接口用途】:统计各班人数,只返回班级名和人数的字典,不要 ORM 对象 【演示特性】:annotate + values,聚合结果也用轻量字典返回 【底层SQL】:SELECT clas.name, COUNT(student.id) AS student_count FROM clas LEFT JOIN student ... GROUP BY clas.id 【初学提示】:annotate 算出 student_count,values 只取 name 和 student_count, 结果是 [{"name": "1班", "student_count": 30}, ...],适合直接返回给前端。 """ rows = await Clas.all().annotate(student_count=Count("students")).values("name", "student_count") return {"classes": rows}
@router.get("/students/{student_id}/enrolled/{course_id}", tags=["9-轻量判断"]) async def check_enrollment(student_id: int, course_id: int): """ 【接口用途】:判断某学生是否选了某门课(选课前查重) 【演示特性】:.exists(),只返回 True/False,不加载任何数据 【底层SQL】:SELECT 1 FROM student_course WHERE student_id=? AND course_id=? LIMIT 1 【初学提示】:exists() 是最轻量的存在性检查,不构造 ORM 对象,不返回数据。 对比"查出所有再判断 len > 0"的做法,exists() 只发一条 LIMIT 1 的 SQL。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") enrolled = await student.courses.filter(id=course_id).exists() return {"enrolled": enrolled}
@router.get("/students/count", tags=["9-轻量判断"]) async def student_count(clas_id: Optional[int] = None): """ 【接口用途】:统计学生总数,可按班级筛选 【演示特性】:.count(),只返回数字,不加载任何对象 【底层SQL】:SELECT COUNT(*) FROM student [WHERE clas_id=?] 【初学提示】:count() 比 all() 再 len() 高效得多,后者要把全部数据加载到内存。 如果只需要数字,永远用 count()。 """ qs = Student.all() if clas_id is not None: qs = qs.filter(clas_id=clas_id) total = await qs.count() return {"count": total}
@router.get("/courses/teacher-names", tags=["9-轻量判断"]) async def course_teacher_names(): """ 【接口用途】:获取所有授课教师的姓名(去重) 【演示特性】:.distinct() + .values(),去重 + 轻量字典 【底层SQL】:SELECT DISTINCT teacher.name FROM course JOIN teacher ON ... 【初学提示】:values("teacher__name") 用 __ 跨表取字段,会自动 JOIN,不需要 select_related。 同一教师教多门课时,不加 distinct 会重复出现。 distinct() 作用于整行结果,配合 values() 可对选定字段去重。 """ rows = await Course.all().distinct().values("teacher__name") return {"teacher_names": rows}
@router.get("/students/exists", tags=["9-轻量判断"]) async def student_exists(sno: int): """ 【接口用途】:判断某个学号是否已存在(注册前查重) 【演示特性】:.filter().exists(),按业务字段判断存在性 【底层SQL】:SELECT 1 FROM student WHERE sno=? LIMIT 1 【初学提示】:和 get_or_none() 相比,exists() 不构造 Model 实例,只返回布尔值, 更轻量。如果只需要知道"有没有",不需要"拿到数据",就用 exists()。 """ exists = await Student.filter(sno=sno).exists() return {"sno": sno, "exists": exists}
@router.get("/students/{student_id}", tags=["10-单条查改删"]) async def get_student(student_id: int): """ 【接口用途】:根据 id 查一名学生 【演示特性】:Model.get_or_none(),找不到时返回 None,不抛异常 【底层SQL】:SELECT ... FROM student WHERE id=? LIMIT 1 【初学提示】:get() 找不到时会抛 DoesNotExist 异常。 在接口开发中,get_or_none() + 手动判断更灵活,方便返回自定义 404 信息。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") return {"id": student.id, "name": student.name, "age": student.age}
@router.put("/students/{student_id}", tags=["10-单条查改删"]) async def update_student(student_id: int, data: StudentUpdate): """ 【接口用途】:修改学生的姓名或年龄(只改传入的字段) 【演示特性】:model_dump(exclude_unset=True) + update_from_dict() + save(),部分字段更新 【底层SQL】:UPDATE student SET name=?, age=? WHERE id=? 【初学提示】:exclude_unset=True 只保留请求体中实际传了的字段,没传的不会被覆盖成 None。 也可以用 await Student.filter(id=student_id).update(**fields), 区别是 filter().update() 不需要先查出对象,直接发 UPDATE 语句,更高效。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") update_fields = data.model_dump(exclude_unset=True) if update_fields: await student.update_from_dict(update_fields).save() return {"updated": update_fields}
@router.delete("/students/{student_id}", tags=["10-单条查改删"]) async def delete_student(student_id: int): """ 【接口用途】:删除一名学生 【演示特性】:instance.delete(),删除数据库中的这条记录 【底层SQL】:DELETE FROM student WHERE id=? 【初学提示】:删除后 Python 中的 student 对象仍然存在(内存中), 只是数据库里没了。也可以用 await Student.filter(id=xxx).delete() 批量删。 """ student = await Student.get_or_none(id=student_id) if not student: raise HTTPException(404, "学生不存在") await student.delete() return {"deleted": student_id}
|