• Python Web开发(十一):ORM 对关联表的操作


    在这里插入图片描述

    前言: 📢📢📢
    🏅🏅🏅作者简介:是Dream呀,华为云享专家、CSDN原力计划作者、Python领域优质创作者,专注分享Python领域原创系列文章。
    🌻🌻🌻热门专栏:【零基础学Python
    本课程是针对Python入门&进阶打造的一全套课程,在这里,我将会一 一更新Python基础语法、Python爬虫、Web开发、 Django框架、Flask框架以及人工智能相关知识,帮助你成为Python大神,如果你喜欢的话就抓紧收藏订阅起来吧~💘💘💘
    🍋🍋🍋如果对学习没有自制力或者没有一起学习交流的动力,欢迎私信我或者文末添加vx,拉你进群,群内有行业大佬帮大家解答疑问,我们一起学习,群内定期还会有抽奖活动和红包相送嗷,快来进入我们吧~
    💕 入门须知:这片乐园从不缺乏天才,努力才是你的最终入场券!🚀🚀🚀
    💓最后,愿我们都能在看不到的地方闪闪发光,一起加油进步🍺🍺🍺

    一、ORM 对关联表的操作

    前面我们学过 一对多,一对一,多对多,都是通过外键来实现。
    接下来,我们通过一个实例演示,Django ORM 如何 操作 外键关联关系
    请大家在 models.py 中定义这样的两个Model,对应两张表:
    在这里插入图片描述
    然后,执行:

    python manage.py makemigrations common
    python manage.py migrate
    
    • 1
    • 2

    产生了两个新的表,也就是我们刚加进去的:
    在这里插入图片描述
    在数据库中执行:
    在这里插入图片描述
    此时,我们在数据库中便找到了我们的文件:
    在这里插入图片描述
    然后,命令行中执行 python manage.py shell ,直接启动Django命令行,输入代码。
    在这里插入图片描述

    先输入如下代码,创建一些数据:

    from common.models import *
    c1 = Country.objects.create(name='中国')
    c2 = Country.objects.create(name='美国')
    c3 = Country.objects.create(name='法国')
    Student.objects.create(name='白月', grade=1, country=c1)
    Student.objects.create(name='黑羽', grade=2, country=c1)
    Student.objects.create(name='大罗', grade=1, country=c1)
    Student.objects.create(name='真佛', grade=2, country=c1)
    Student.objects.create(name='Mike', grade=1, country=c2)
    Student.objects.create(name='Gus',  grade=1, country=c2)
    Student.objects.create(name='White', grade=2, country=c2)
    Student.objects.create(name='Napolen', grade=2, country=c3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    可以看到在我们的国家表和学生表中都产生了我们添加的名单:
    在这里插入图片描述
    在这里插入图片描述

    1.外键表字段访问

    如果你已经获取了一个student对象,要得到他的国家名称只需这样:

    s1 = Student.objects.get(name='白月')
    s1.country.name
    
    • 1
    • 2

    2.外键表字段过滤

    如果,我们要查找Student表中所有 一年级 学生,很简单:

    Student.objects.filter(grade=1).values()
    
    • 1

    在这里插入图片描述

    如果现在,我们要查找Student表中所有 一年级中国 学生,该怎么写呢?

    2.1 错误方法

    不能这么写:

    Student.objects.filter(grade=1,country='中国')
    
    • 1

    在这里插入图片描述
    因为,Student表中 country 并不是国家名称字符串字段,而是一个外键字段,其实是对应 Country 表中 id 字段 。

    2.2 先获取中国的国家id,然后再通过id去找

    可能有的朋友会这样想:我可以先获取中国的国家id,然后再通过id去找,像这样

    cn = Country.objects.get(name='中国')
    Student.objects.filter(grade=1,country_id=cn.id).values()
    
    • 1
    • 2

    在这里插入图片描述
    注意外键字段的id是通过后缀 _id 获取的。

    或者这样,也是可以的

    cn = Country.objects.get(name='中国')
    Student.objects.filter(grade=1,country=cn).values()
    
    • 1
    • 2

    上面的方法,写起来麻烦一些,有两步操作。而且需要发送两次数据请求给数据库服务,性能不高

    2.3 country__name一步到位

    其实,Django ORM 中,对外键关联,有更方便的语法。

    Student.objects.filter(grade=1,country__name='中国').values()
    
    • 1

    写起来简单,一步到位,而且只需要发送一个数据库请求,性能更好。
    在这里插入图片描述

    2.4 只需要 学生姓名 和 国家名两个字段

    如果返回结果只需要 学生姓名国家名两个字段,可以这样指定values内容:

    Student.objects.filter(grade=1,country__name='中国')\
         .values('name','country__name')
    
    • 1
    • 2

    在这里插入图片描述

    2.5 重命名两个下划线

    但是这样写有个问题:选择出来的记录中,国家名是 country__name 。 两个下划线比较怪。
    有时候,前后端接口的设计者,定义好了接口格式,如果要求一定是 countryname 这样怎么办?
    可以使用 annotate 方法将获取的字段值进行重命名,像下面这样:

    from django.db.models import F
    
    # annotate 可以将表字段进行别名处理
    Student.objects.annotate(
        countryname=F('country__name'),
        studentname=F('name')
        )\
        .filter(grade=1,countryname='中国').values('studentname','countryname')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    3.外键表反向访问

    前面学过, Django ORM中,关联表 正向关系是通过表外键字段(或者多对多)表示, 比如前面例子中Student表的 country字段。

    而反向关系,是通过 表Model名转化为小写 表示的。
    比如,你已经获取了一个Country对象,如何获取到所有属于这个国家的学生呢?
    可以这样

    cn = Country.objects.get(name='中国')
    cn.student_set.all()
    
    • 1
    • 2

    在这里插入图片描述
    通过 表Model名转化为小写 ,后面加上一个 _set 来获取所有的反向外键关联对象

    Django还给出了一个方法,可以更直观的反映 关联关系。

    在定义Model的时候,外键字段使用 related_name 参数,像这样:

    # 国家表
    class Country(models.Model):
        name = models.CharField(max_length=100)
    
    # country 字段是国家表的外键,形成一对多的关系
    class Student(models.Model):
        name    = models.CharField(max_length=100)
        grade   = models.PositiveSmallIntegerField()
        country = models.ForeignKey(Country,
                    on_delete = models.PROTECT,
                    # 指定反向访问的名字
                    related_name='students')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    就可以使用更直观的属性名,像这样

    cn = Country.objects.get(name='中国')
    cn.students.all()
    
    • 1
    • 2

    4.外键表反向过滤

    如果我们要获取所有 具有一年级学生 的国家名,该怎么写?

    当然可以这样

    # 先获取所有的一年级学生id列表
    country_ids = Student.objects.filter(grade=1).values_list('country', flat=True)
    
    # 再通过id列表使用  id__in  过滤
    Country.objects.filter(id__in=country_ids).values()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述
    但是这样同样存在 麻烦 和性能的问题。
    Django ORM 可以这样写:

    Country.objects.filter(students__grade=1).values()
    
    • 1

    注意, 因为,我们定义表的时候,用 related_name=‘students’ 指定了反向关联名称 students ,所以这里是 students__grade 。 使用了反向关联名字。

    没有指定related_name, 则应该使用表名转化为小写

    如果定义时,没有指定related_name, 则应该使用 表名转化为小写 ,就是这样:

    Country.objects.filter(student__grade=1).values()
    
    • 1

    使用 .distinct()去重

    但是,我们发现,这种方式,会有重复的记录产生,如下
    在这里插入图片描述

    <QuerySet [{'id': 1, 'name': '中国'}, {'id': 1, 'name': '中国'}, {'id': 2, 'name': '美国'}, {'id': 2, 'name': '美国'}]>
    
    • 1

    可以使用 .distinct() 去重:

    Country.objects.filter(student__grade=1).values().distinct()
    
    • 1

    在这里插入图片描述

    注意:distinct()对MySQL数据库无效。

    二、实现项目代码

    1.url路由更新

    现在,我们在 mgr 目录下面新建 order.py 处理 客户端发过来的 列出订单、添加订单 的请求。
    同样,先写 dispatcher 函数,代码如下:

    from django.http import JsonResponse
    from django.db.models import F
    from django.db import IntegrityError, transaction
    
    # 导入 Order 对象定义
    from  common.models import  Order,OrderMedicine
    
    import json
    
    def dispatcher(request):
        # 根据session判断用户是否是登录的管理员用户
        if 'usertype' not in request.session:
            return JsonResponse({
                'ret': 302,
                'msg': '未登录',
                'redirect': '/mgr/sign.html'},
                status=302)
    
        if request.session['usertype'] != 'mgr':
            return JsonResponse({
                'ret': 302,
                'msg': '用户非mgr类型',
                'redirect': '/mgr/sign.html'},
                status=302)
    
    
        # 将请求参数统一放入request 的 params 属性中,方便后续处理
    
        # GET请求 参数 在 request 对象的 GET属性中
        if request.method == 'GET':
            request.params = request.GET
    
        # POST/PUT/DELETE 请求 参数 从 request 对象的 body 属性中获取
        elif request.method in ['POST','PUT','DELETE']:
            # 根据接口,POST/PUT/DELETE 请求的消息体都是 json格式
            request.params = json.loads(request.body)
    
        # 根据不同的action分派给不同的函数进行处理
        action = request.params['action']
        if action == 'list_order':
            return listorder(request)
        elif action == 'add_order':
            return addorder(request)
    
        # 订单 暂 不支持修改 和删除
    
        else:
            return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})
    
    • 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

    和以前差不多,没有什么好说的。

    然后,我们在 mgr\urls.py 里面加上 对 orders 请求处理的路由:

    from django.urls import path
    from mgr import customer,sign_in_out,medicine,order
    
    urlpatterns = [
    
        path('customers', customer.dispatcher),
        path('medicines', medicine.dispatcher),
        path('orders', order.dispatcher), # 加上这行
    
        path('signin', sign_in_out.signin),
        path('signout', sign_in_out.signout),
    
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    2.事务、多对多记录添加

    接下来,我们添加函数 addorder,来处理 添加订单 请求。
    我们添加一条订单记录,需要在2张表(Order 和 OrderMedicine )中添加记录。

    这里就有个需要特别注意的地方, 两张表的插入,意味着我们要有两次数据库操作。

    如果第一次插入成功, 而第二次插入失败, 就会出现 Order表中 把订单信息写了一部分,而OrderMedicine表中 该订单的信息 却没有写成功。
    这是个大问题: 就会造成 这个处理 做了一半。

    那么数据库中就会出现数据的不一致。术语叫 脏数据
    把一批数据库操作放在 事务 中, 该事务中的任何一次数据库操作 失败了, 数据库系统就会让 整个事务就会发生回滚,撤销前面的操作, 数据库回滚到这事务操作之前的状态。

    Django 怎么实现 事务操作呢?
    这里我们可以使用 Django 的 with transaction.atomic()
    代码如下:

    def addorder(request):
    
        info  = request.params['data']
    
        # 从请求消息中 获取要添加订单的信息
        # 并且插入到数据库中
    
        
        with transaction.atomic():
            new_order = Order.objects.create(name=info['name'] ,
                                             customer_id=info['customerid'])
    
            batch = [OrderMedicine(order_id=new_order.id,medicine_id=mid,amount=1)  
                        for mid in info['medicineids']]
    
            #  在多对多关系表中 添加了 多条关联记录
            OrderMedicine.objects.bulk_create(batch)
        return JsonResponse({'ret': 0,'id':new_order.id})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    with transaction.atomic() 下面 缩进部分的代码,对数据库的操作,就都是在 一个事务 中进行了。
    如果其中有任何一步数据操作失败了, 前面的操作都会回滚。

    这就可以防止出现 前面的 Order表记录插入成功, 而后面的 订单药品 记录插入失败而导致的数据不一致现象。
    OrderMedicine 对应的是订单和药品的多对对记录关系表。

    要在多对多表中加上关联记录,就是添加一条记录, 可以这样

    OrderMedicine.objects.create(order_id=new_order.id,medicine_id=mid,amount=1)
    
    • 1

    我们这个例子中,一个订单可能会关联多个药品,也就是需要 插入 OrderMedicine 表中的数据 可能有很多条, 如果我们循环用

    OrderMedicine.objects.create(order_id=new_order.id,medicine_id=mid,amount=1)
    
    • 1

    插入的话, 循环几次, 就会执行 几次SQL语句 插入的 数据库操作 这样性能不高。
    我们可以把多条数据的插入,放在一个SQL语句中完成, 这样会大大提高性能。
    方法就是使用 bulk_create, 参数是一个包含所有 该表的 Model 对象的 列表
    就像上面代码这样

    batch = [OrderMedicine(order_id=new_order.id,medicine_id=mid,amount=1)  
                for mid in info['medicineids']]
    
    #  在多对多关系表中 添加了 多条关联记录
    OrderMedicine.objects.bulk_create(batch)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.ORM外键关联

    接下来,我们来编写listorder 函数用来处理 列出订单请求。

    根据接口文档,我们应该返回 订单记录格式,如下:

    [
        {
            id: 1, 
            name: "华山医院订单001", 
            create_date: "2018-12-26T14:10:15.419Z",
            customer_name: "华山医院",
            medicines_name: "青霉素"
        },
        {
            id: 2, 
            name: "华山医院订单002", 
            create_date: "2018-12-27T14:10:37.208Z",
            customer_name: "华山医院",
            medicines_name: "青霉素 | 红霉素 "
        }
    ] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    其中 ‘id’,‘name’,‘create_date’ 这些字段的内容获取很简单,order表中就有这些字段,
    只需要这样写就可以了:

    def listorder(request):
        # 返回一个 QuerySet 对象 ,包含所有的表记录
        qs = Order.objects.values('id','name','create_date')
        return JsonResponse({'ret': 0, 'retlist': newlist})
    
    • 1
    • 2
    • 3
    • 4

    问题是:‘customer_name’ 和 ‘medicines_name’ 这两个字段的值怎么获取呢? 因为 订单对应的客户名字 和 药品的名字 都不在 Order 表中啊。

    Order 这个Model 中 有 ‘customer’ 字段 , 它外键关联了 Customer 表中的一个 记录,这个记录里面 的 name字段 就是我们要取的字段。

    取 外键关联的表记录的字段值,在Django中很简单,可以直接通过 外键字段 后面加 两个下划线 加 关联字段名的方式 来获取。

    比如 这里我们就可以用 下面的代码来实现

    def listorder(request):
        qs = Order.objects\
                .values(
                    'id','name','create_date',
                    # 两个下划线,表示取customer外键关联的表中的name字段的值
                    'customer__name'
                )
        
        # 将 QuerySet 对象 转化为 list 类型
        retlist = list(qs)
        return JsonResponse({'ret': 0, 'retlist': retlist})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.1双下划线问题

    首先,第一个问题, 接口文档需要的名字是 ‘customer_name’ 和 ‘medicines_name’。 里面只有一个下划线, 而我们这里却产生了 两个下划线。

    怎么办?

    可以使用 annotate 方法将获取的字段值进行重命名,像下面这样
    写好后, 大家可以运行服务 , 用我们做好的前端系统添加几条 订单记录, 然后再查看一下数据库里面的数据是否正确。

    from django.db.models import F
    
    def listorder(request):
        # 返回一个 QuerySet 对象 ,包含所有的表记录
        qs = Order.objects\
                .annotate(
                    customer_name=F('customer__name'),
                    medicines_name=F('medicines__name')
                )\
                .values(
                    'id','name','create_date',
                    'customer_name',
                    'medicines_name'
                )
    
        # 将 QuerySet 对象 转化为 list 类型
        retlist = list(qs)
    
        return JsonResponse({'ret': 0, 'retlist': retlist})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述

    3.2多个订单问题

    第二个问题,如果一个订单里面有多个药品,就会产生多条记录, 这不是我们要的。

    根据接口,一个订单里面的多个药品, 用 竖线 隔开。

    怎么办?

    我们可以用python代码来处理,像下面这样

    def listorder(request):
        # 返回一个 QuerySet 对象 ,包含所有的表记录
        qs = Order.objects\
                .annotate(
                    customer_name=F('customer__name'),
                    medicines_name=F('medicines__name')
                )\
                .values(
                    'id','name','create_date','customer_name','medicines_name'
                )
    
        # 将 QuerySet 对象 转化为 list 类型
        retlist = list(qs)
    
        # 可能有 ID相同,药品不同的订单记录, 需要合并
        newlist = []
        id2order = {}
        for one in retlist:
            orderid = one['id']
            if orderid not in id2order:
                newlist.append(one)
                id2order[orderid] = one
            else:
                id2order[orderid]['medicines_name'] += ' | ' + one['medicines_name']
    
        return JsonResponse({'ret': 0, 'retlist': newlist})
    
    • 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

    【系列好文推荐】

    🎯🎯🎯
    Python Web开发一:Web开发简介
    Python Web开发二:Django的安装和运行
    Python Web开发(三):HTTP请求的url路由

    欢迎订阅本专栏:零基础学Python 系列课程是针对Python入门&进阶打造的一全套课程,在这里,我将会一 一更新Python基础语法、Python爬虫、Web开发、 Django框架、Flask框架以及人工智能相关知识,帮助你成为Python大神,如果你喜欢的话就抓紧收藏订阅起来吧~💘💘💘
    💕💕💕 好啦,这就是今天要分享给大家的全部内容了,我们下期再见!✨ ✨ ✨
    🍻🍻🍻如果你喜欢的话,就不要吝惜你的一键三连了~
    在这里插入图片描述
    在这里插入图片描述
    ⬇️⬇️ ⬇️ 商务合作|交流学习|粉丝福利|Python全套资料⬇️ ⬇️ ⬇️ 欢迎联系~

  • 相关阅读:
    多探头球面近场天线测试效率提升方法
    普冉PY32F071单片机简单介绍,QFN64 48封装,支持 8 * 36 / 4 * 40 LCD
    React+TS学习和使用(三):React Redux和项目的路由配置
    es6过滤对象里面指定的不要的值filter过滤
    计算机组成原理习题课第四章-2(唐朔飞)
    QT商业播放器
    利用transform和border 创造简易图标,以适应uniapp中多字体大小情况下的符号问题
    民安智库(第三方市场调研公司)哪家残疾人服务满意度调研公司比较专业
    centos安装nexus3.x版本
    MPI并行编程技术
  • 原文地址:https://blog.csdn.net/weixin_51390582/article/details/126063989