Django开发笔记(七)四件套之四:模型层ORM(下) 表关系
这两篇其实比较死板,对函数、参数、语法熟悉了就能用,常用常新。
五、创建关联模型
这部分就是数据库表关系。因为经常用,很简单。这一节老师讲的其实是sql知识。
一对多,在多的那端创建关联字段,比如一个班级对多个学生,就在学生表里创建班级字段。
一对一,在任何一张表都可以创建关联字段。一对一比一对多区别在一个唯一约束。
多对多,通过第三张表创建关系,比如学生表和课程表,通过第三张“学生-课程”表创建关系。
平时我们在数据库中,创建关联字段+外键语法强绑逻辑,比如:
create table student{
id int primary key auto_increment,
name varchar(32),
class_id int not null, #其实很多时候这样就足够了,我们实战中后面的外键约束经常没写
foreign key (class_id) references class(id) on delete cascade #这边是逻辑约束
}
我发现这个和我们通常创建的数据库表还不一样,默认都是使用id作为桥梁字段的。那么是默认将主键作为桥梁的吗,如果我自己设置主键是不是就不会给一个默认主键id了?
解决:可以在一对多,多对多里设置自己想关联的字段,参数为to_field。
1.一对多
2.多对多
from django.db import models
# Create your models here.
'''
班级模型: 班级名称、导员。
课程模型:课程名称、讲师等。
学生模型: 学生有姓名,年龄,只有一个班级,所以和班级表是一对多的关系(one-to-many);选修了多个课程,所以和课程表是多对多的关系(many-to-many)
学生详情:学生的家庭地址,手机号,邮箱等详细信息,和学生模型应该是一对一的关系(one-to-one)
'''
class ClassRoom(models.Model):
# no = models.SmallIntegerField()
name = models.CharField(max_length=32,verbose_name="班级名称")
class Meta:
db_table="db_classroom"
class Course(models.Model):
name = models.CharField(max_length=32,verbose_name="课程名称")
class Meta:
db_table="db_course"
class Student3(models.Model):
sex_choices = (
(0, '女'),
(1, '男'),
(2, '无性别')
)
name = models.CharField(max_length=32, unique=True, verbose_name="姓名")
age = models.SmallIntegerField(verbose_name="年龄", null=True)
sex = models.SmallIntegerField(verbose_name="性别", choices=sex_choices)
birthday = models.DateField(verbose_name="生日")
# 一对多关系:在数据库创建一个关联字段classroom_id 这个字段名字是自己给你补的"_id"
# Django3.版本on_delete必须显式定义
# 外键约束实战中一般没有,Django中默认是True,不想要的话设置参数db_constraint=False即可,这也是面试会问的
# ForeignKey的参数,创建表有用的就是这三个字段,related_name反向查询可用,暂时不管。to_field字段可以设置关联字段。
classroom = models.ForeignKey(to="ClassRoom",on_delete=models.CASCADE,db_constraint=False)
# 多对多关系,创建第三张关系表
# 有了这句话,ORM数据迁移的时候会帮我们创建第三张表
# 这个关联表名字会用表名+字段名拼的student_courses,db_table字段可以自己命名这张表
# 多对多也可以在course表那边创建这个第三张表
courses = models.ManyToManyField("Course",db_table="db_student_course")
# 一对一关系 一对一就对比一对多 建立关联字段,数据库中生成的关联字段会自己给补上一个"_id"
stu_detail = models.OneToOneField("Student_detail",on_delete=models.CASCADE)
class Meta:
db_table="db_student3"
class Student_detail(models.Model):
address = models.CharField(verbose_name="地址",max_length=11)
tel = models.IntegerField(verbose_name="手机")
email = models.EmailField(verbose_name="邮箱") # EmailField有邮箱格式限制,需要加一个校验语法才有效
补充: on_delete用法总结:
SET_NULL: 最常见的置空操作,但前提是null=True: publisher = models.ForeignKey(to="Publisher",null=True,on_delete=models.SET_NULL)
CASCADE: 级联删除
PROTECT:保护模式,删除的时候会抛出ProctectedError错误
SET_DEFAULT: 置默认值
SET() :自定义一个值,该值只能是对应的实体了。
接下来数据迁徙,和上一篇一样的两个命令:
python manage.py makemigrations
python manage.py migrate
这边我启动了一个新项目,忘记配mysql了,使用默认的sqlite数据库,生成了左下角的db.sqlite3这样一个文件数据库。在右边database处配置下载一下sqlite驱动,下载完左边的红框拖到右边即可。

六、关联添加
基于以上models进行关联添加。
首先自己给classroom表、course表、student_detail表加几个数据,用作辅助数据。然后我们从student表切入学习添加数据。
知识点都在注释里了:
from django.shortcuts import render,HttpResponse
from .models import Student,Student_detail,Course,ClassRoom
# Create your views here.
def add_student(request):
# 添加记录
# 针对 一对多 与 一对一 的关联属性 很简单就是给关联字段赋值就好了
stu = Student.objects.create(name='李四',age=22,sex=1,birthday='2001-04-18',classroom_id=1,stu_detail_id=2)
print(stu.classroom_id)
# 这个很重要!打印出来该学生的班级对象,是一个模型类对象!就是ClassRoom.objects.get(id=2)的结果赋给它
print(stu.classroom)
# 因此以下两个语句效果一样
name1 = ClassRoom.objects.get(id=stu.classroom_id).name
name2 = stu.classroom.name
print(name1,name2)
#--------------------------------------------
# 多对多 关联记录的增删改查
stu2 = Student.objects.create(name='王也', age=22, sex=1, birthday='2001-04-18', classroom_id=1, stu_detail_id=3)
# 添加多对多数据,比如给这个学生绑定两门课程,机器学习和组合数学(不知道课程id只知道课程名称)
# 添加多对多对象方式一
c1 = Course.objects.get(name="机器学习")
c2 = Course.objects.get(name="组合数学")
stu2.courses.add(c1,c2)
'''
stu2.courses.add(c1,c2) 做了什么:
王也对应的student表id为3,课程机器学习id为1,组合数学id为4
就会为第三张表db_student_course表添加两条数据
这种写法,一行代码添加多条记录(为第三张表)
其实courses = models.ManyToManyField("Course",db_table="db_student_course")
这个courses提供了我们多对多增删改查的入口,集结了增删改查的方法
'''
# 添加多对多数据方式二
stu3 = Student.objects.get(name = "李四")
stu3.courses.add(1,4) # 1,4都是课程id 这种写法更简单
# 方式三
stu4 = Student.objects.get(name = "张三")
# *[]的写法和上面那个方式二单独传5,7一样的效果 这样做是因为客户端传过来大概率是个列表,没必要取出来再像方式二一样操作了
stu4.courses.add(*[1,2,3])
# 删除第三张表的记录 remove和add方法完全一样,上面add的三种,remove也可以使用
# 删除李四(选了1,4)课程1
stu3.courses.remove(1)
# 清除
# 把张三的课程清空
stu4.courses.clear()
# 重置 这个set呢,参数是一个列表,不用加* set自己是先做一个clear后做一个add的大动作
# 给李四重新绑定课程
stu3.courses.set([1,2,3,4])
# ---------------------------------
# 查
# 查询李四所报的课程名称
# 这个all()方法里其实封装了几个sql:
# 首先stu3里封装了李四的id是2,然后通过courses进入关系表里,找到student_id=2的,
# 对应的course_id也找到了,是1,2,3,4;
# 然后返回把这几个id拿到课程表里,去找对应的记录,然后把这些记录对象!放到queryset里,返回
# 也就是通过all()获取到的是一个queryset
# 对这个queryset可以使用基础查询的十个api,想干嘛就干嘛了
print(stu3.courses.all())
return HttpResponse("添加关联记录成功!")
七、关联查询
1.基于对象查询(子查询)
1)一对多查询
2)一对一查询
3)多对多查询
正向查询:通过关联属性查询属于正向查询,正向查询按字段
反向查询:按表名小写或者related_name。其实也就是定义了related_name就使用related_name,没有定义就使用默认的 “表名小写”或“表名小写_set”。
注意:
- 一对一反向查询不需要_set,一对多和多对多这样查出来是集合的加_set。
- 一对多反向查询出来后面要加.all(),一对一不需要。
- 定义了related_name后就不能用表名小写查了,会显示没有那个字段,只有定义的related_name字段。我理解的就是表名小写是默认,定义了related_name就被覆盖了。
def select_student(request):
'''
基于对象的关联查询(子查询,以一个的查询结果作为下一个的查询条件)
:param request:
:return:
'''
# 一对多的查询
# 1)正向查询 查询张三的班级名称
stu = Student.objects.get(name="张三")
print(stu.classroom.name) # 正向查询
print("--------------------")
# 2)反向查询 查询计算机技术1班有哪些学生
classroom = ClassRoom.objects.get(name="计算机技术1班")
# 方式1 .表名小写_set.all() 返回的是一个queryset
# stu_set = Student.objects.filter(classroom=classroom) # 这是我自己写的笨方法,本节课没讲
ret1 =classroom.student_set.all() # 和上面那句效果一样 反向查询 .表名小写_set 注意后面不加all()没东西出来
print(ret1)
# 方式2 related_name
# 需要在models文件里定义related_name参数:classroom = models.ForeignKey(to="ClassRoom",on_delete=models.CASCADE,db_constraint=False,related_name="student_set")
print(classroom.student_set) # 不加all()没东西
print("^^^^^^^^^")
print(classroom.student_set.all())
# -----------------------------------------------------------------------------
# 一对一查询
# 正向查询 查询李四的手机号
stu2 = Student.objects.get(name = "李四")
print(stu2.stu_detail.tel)
# 反向查询 查询15646手机号对应的学生信息
# 方式1 表名小写
stu_detail = Student_detail.objects.get(tel="15646") # 一对一不会再拼_set了,直接就是表名小写
print(stu_detail.student)
# 方式2 related_name
# models.py里加上 stu_detail = models.OneToOneField("Student_detail",on_delete=models.CASCADE,related_name="relatedstu")
ret2 = Student_detail.objects.get(tel="15646").relatedstu
print(ret2)
# --------------------------------------------------------------------------------
# 多对多查询
# 正向查询 查询王也选修的课程名称
print(Student.objects.get(name="王也").courses.all())
print("^^^^^^^^")
# 反向查询 查询选修组合数学的学生
course = Course.objects.get(name="组合数学")
print(course.student_set.all()) # 我related_name起的名字就是student_set
# 查询选修组合数学的学生的姓名和年龄
print(course.student_set.all().values("name","age"))
return HttpResponse("关联查询成功")
2.基于双下划线查询(join查询)
正向join表的时候用关联字段,反向join表的时候用related_name(没有related_name就用默认的表名小写)
def select_student2(request):
'''
基于双下划线
:param request:
:return:
'''
# 查询张三的年龄
ret = Student.objects.filter(name="张三").values("name","age")
print(ret)
print("^^^^^^^^^^")
# 查询年龄大于22的学生姓名以及所在班级名称
# select db_student.name,dc.name from db_student
# join db_classroom dc on db_student.classroom_id = dc.id
# where age>22
# filter是做where的,value是做select的,过滤条件必须是模型类对象里有的
# 方式1 Student 作为基表
ret2 = Student.objects.filter(age__gt=22).values("name","classroom__name")
print(ret2)
print("^^^^^^^^^")
# 方式2 Classroom 作为基表
ret3 = ClassRoom.objects.filter(student_set__age__gt=22).values("student_set__name","name")
print(ret3)
# 查计算机技术1班有哪些学生
# select student.name from classroom join student on classroom.id = student.classroom_id
ret4 = Student.objects.filter(classroom__name="计算机技术1班").values("name")
print(ret4)
# 生成的sql:SELECT "db_student"."name" FROM "db_student"
# INNER JOIN "db_classroom" ON ("db_student"."classroom_id" = "db_classroom"."id")
# WHERE "db_classroom"."name" = '计算机技术1班' LIMIT 21; args=('计算机技术1班',)
print("^^^^^^^^^^^^")
ret5 = ClassRoom.objects.filter(name="计算机技术1班").values("student_set__name")
print(ret5)
# 生成的sql:SELECT "db_student"."name" FROM "db_classroom"
# LEFT OUTER JOIN "db_student" ON ("db_classroom"."id" = "db_student"."classroom_id")
# WHERE "db_classroom"."name" = '计算机技术1班' LIMIT 21; args=('计算机技术1班',)
# 多对多
# 查询王也所报课程名称 这个在ORM里也太简单了,但其实sql很复杂
# SELECT "db_course"."name"
# FROM "db_student" LEFT OUTER JOIN "db_student_course"
# ON ("db_student"."id" = "db_student_course"."student_id")
# LEFT OUTER JOIN "db_course" ON ("db_student_course"."course_id" = "db_course"."id")
# WHERE "db_student"."name" = '王也' LIMIT 21; args=('王也',)
ret6 = Student.objects.filter(name="王也").values("courses__name")
print(ret6)
# 查询选修了组合数学的学生姓名及年龄
print(Student.objects.filter(courses__name="组合数学").values("name", "age"))
print(Course.objects.filter(name="组合数学").values("student_set__name", "student_set__age"))
return HttpResponse("join查询成功")
进阶的基于下划线的查询——连续跨表,比如学生详情——学生——班级,想查一个手机号是4586的学生的姓名和所在班级名称:
# 查一个手机号是4586的学生的姓名和所在班级名称:
print(Student_detail.objects.filter(tel="4586").values("student__name", "student__classroom__name"))
# 当然也可以用Student作为基表关联查询
3.关联分组查询
分组查询相关内容可以看看上一篇进阶查询里的分组查询,用的是annotate,是难点。
from django.shortcuts import render,HttpResponse
from .models import Student,Student_detail,Course,ClassRoom
# Create your views here.
def add_student(request):
# 添加记录
# 针对 一对多 与 一对一 的关联属性 很简单就是给关联字段赋值就好了
stu = Student.objects.create(name='李四',age=22,sex=1,birthday='2001-04-18',classroom_id=1,stu_detail_id=2)
print(stu.classroom_id)
# 这个很重要!打印出来该学生的班级对象,是一个模型类对象!就是ClassRoom.objects.get(id=2)的结果赋给它
print(stu.classroom)
# 因此以下两个语句效果一样
name1 = ClassRoom.objects.get(id=stu.classroom_id).name
name2 = stu.classroom.name
print(name1,name2)
#--------------------------------------------
# 多对多 关联记录的增删改查
stu2 = Student.objects.create(name='王也', age=22, sex=1, birthday='2001-04-18', classroom_id=1, stu_detail_id=3)
# 添加多对多数据,比如给这个学生绑定两门课程,机器学习和组合数学(不知道课程id只知道课程名称)
# 添加多对多对象方式一
c1 = Course.objects.get(name="机器学习")
c2 = Course.objects.get(name="组合数学")
stu2.courses.add(c1,c2)
'''
stu2.courses.add(c1,c2) 做了什么:
王也对应的student表id为3,课程机器学习id为1,组合数学id为4
就会为第三张表db_student_course表添加两条数据
这种写法,一行代码添加多条记录(为第三张表)
其实courses = models.ManyToManyField("Course",db_table="db_student_course")
这个courses提供了我们多对多增删改查的入口,集结了增删改查的方法
'''
# 添加多对多数据方式二
stu3 = Student.objects.get(name = "李四")
stu3.courses.add(1,4) # 1,4都是课程id 这种写法更简单
# 方式三
stu4 = Student.objects.get(name = "张三")
# *[]的写法和上面那个方式二单独传5,7一样的效果 这样做是因为客户端传过来大概率是个列表,没必要取出来再像方式二一样操作了
stu4.courses.add(*[1,2,3])
# 删除第三张表的记录 remove和add方法完全一样,上面add的三种,remove也可以使用
# 删除李四(选了1,4)课程1
stu3.courses.remove(1)
# 清除
# 把张三的课程清空
stu4.courses.clear()
# 重置 这个set呢,参数是一个列表,不用加* set自己是先做一个clear后做一个add的大动作
# 给李四重新绑定课程
stu3.courses.set([1,2,3,4])
# ---------------------------------
# 查
# 查询李四所报的课程名称
# 这个all()方法里其实封装了几个sql:
# 首先stu3里封装了李四的id是2,然后通过courses进入关系表里,找到student_id=2的,
# 对应的course_id也找到了,是1,2,3,4;
# 然后返回把这几个id拿到课程表里,去找对应的记录,然后把这些记录对象!放到queryset里,返回
# 也就是通过all()获取到的是一个queryset
# 对这个queryset可以使用基础查询的十个api,想干嘛就干嘛了
print(stu3.courses.all())
return HttpResponse("添加关联记录成功!")
def select_student(request):
'''
基于对象的关联查询(子查询,以一个的查询结果作为下一个的查询条件)
:param request:
:return:
'''
# 一对多的查询
# 1)正向查询 查询张三的班级名称
stu = Student.objects.get(name="张三")
print(stu.classroom.name) # 正向查询
print("--------------------")
# 2)反向查询 查询计算机技术1班有哪些学生
classroom = ClassRoom.objects.get(name="计算机技术1班")
# 方式1 .表名小写_set.all() 返回的是一个queryset
# stu_set = Student.objects.filter(classroom=classroom) # 这是我自己写的笨方法,本节课没讲
ret1 =classroom.student_set.all() # 和上面那句效果一样 反向查询 .表名小写_set 注意后面不加all()没东西出来
print(ret1)
# 方式2 related_name
# 需要在models文件里定义related_name参数:classroom = models.ForeignKey(to="ClassRoom",on_delete=models.CASCADE,db_constraint=False,related_name="student_set")
print(classroom.student_set) # 不加all()没东西
print("^^^^^^^^^")
print(classroom.student_set.all())
# -----------------------------------------------------------------------------
# 一对一查询
# 正向查询 查询李四的手机号
stu2 = Student.objects.get(name = "李四")
print(stu2.stu_detail.tel)
# 反向查询 查询15646手机号对应的学生信息
# 方式1 表名小写
stu_detail = Student_detail.objects.get(tel="15646") # 一对一不会再拼_set了,直接就是表名小写
print(stu_detail.student)
# 方式2 related_name
# models.py里加上 stu_detail = models.OneToOneField("Student_detail",on_delete=models.CASCADE,related_name="relatedstu")
ret2 = Student_detail.objects.get(tel="15646").student
print(ret2)
# --------------------------------------------------------------------------------
# 多对多查询
# 正向查询 查询王也选修的课程名称
print(Student.objects.get(name="王也").courses.all())
print("^^^^^^^^")
# 反向查询 查询选修组合数学的学生
course = Course.objects.get(name="组合数学")
print(course.student_set.all()) # 我related_name起的名字就是student_set
# 查询选修组合数学的学生的姓名和年龄
print(course.student_set.all().values("name","age"))
return HttpResponse("关联查询成功")
def select_student2(request):
'''
基于双下划线
:param request:
:return:
'''
# 分组查询
# 查不同性别的学生个数 这个是上一篇的在一个模型类里进行分组
from django.db.models import Count
ret = Student.objects.values("sex").annotate(name = Count("name"))
print(ret)
# 查询每一个班级的名称以及学生个数 这个是在关联模型里怎么分组
# 生成的sql:SELECT "db_classroom"."name", COUNT("db_student"."name") AS "stuname"
# FROM "db_classroom" LEFT OUTER JOIN "db_student" ON ("db_classroom"."id" = "db_student"."classroom_id")
# GROUP BY "db_classroom"."name" LIMIT 21;
ret2 = ClassRoom.objects.values("name").annotate(stuname = Count("student_set__name"))
print(ret2)
# 查询每个学生的姓名以及选修课程的个数
# 这样查有问题,没有课的学生没有查出来
print(Course.objects.values("student_set__name").annotate(count=Count("student_set__name")))
print("^^^^^^^^^")
# 这样查就全出来了 所以题目中每一个“学生”,每一个后面的东西是重点 这个的结果是
print(Student.objects.values("name").annotate(count=Count("courses__name")))
# 还有一种,和上面那种输出一样,也很常用 这种就加深了对 "分组查询里value在annotate前面是group by,在后面是select字段" 的理解
# 输出是<QuerySet [{'name': '李四', 'count': 4}, {'name': '王也', 'count': 2}, {'name': '张三', 'count': 0}, {'name': '冯宝宝', 'count': 0}]>
# print(Student.objects.all().annotate(count=Count("courses__name")).values("name","count"))
# 查询每一个课程名称以及选修学生的个数
print(Course.objects.all().annotate(count=Count("student_set__name")).values("name","count"))
# 扩展:查询选修课程个数大于1的学生姓名以及选修课程个数
# 在分组的基础上又做了一个过滤。having的使用在sql里就是难点。filter在annotate后面就是having的作用
ret6 = Student.objects.all().annotate(count=Count("courses__name")).filter(count__gt = 1).values("name","count")
# 生成的sql:SELECT "db_student"."name", COUNT("db_course"."name") AS "count" FROM "db_student"
# LEFT OUTER JOIN "db_student_course" ON ("db_student"."id" = "db_student_course"."student_id")
# LEFT OUTER JOIN "db_course" ON ("db_student_course"."course_id" = "db_course"."id")
# GROUP BY "db_student"."id", "db_student"."name", "db_student"."age", "db_student"."sex", "db_student"."birthday", "db_student"."classroom_id", "db_student"."stu_detail_id"
# HAVING COUNT("db_course"."name") > 1 LIMIT 21
print(ret6)
return HttpResponse("join查询成功")
浙公网安备 33010602011771号