四方显神

导航

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查询成功")

posted on 2023-12-13 20:43  szdbjooo  阅读(30)  评论(0)    收藏  举报