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

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

网络异常,图片无法展示
|

一、ORM 对关联表的操作

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

网络异常,图片无法展示
|
然后,执行:

python manage.py makemigrations common
python manage.py migrate

产生了两个新的表,也就是我们刚加进去的:

网络异常,图片无法展示
|
在数据库中执行:
网络异常,图片无法展示
|
此时,我们在数据库中便找到了我们的文件:
网络异常,图片无法展示
|
然后,命令行中执行 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.外键表字段访问

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

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

2.外键表字段过滤

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

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

网络异常,图片无法展示
|

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

2.1 错误方法

不能这么写:

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

网络异常,图片无法展示
|
因为,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()

网络异常,图片无法展示
|
注意外键字段的id是通过后缀 _id 获取的。

或者这样,也是可以的

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

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

2.3 country__name一步到位

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

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

写起来简单,一步到位,而且只需要发送一个数据库请求,性能更好。

网络异常,图片无法展示
|

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

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

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

网络异常,图片无法展示
|

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')

网络异常,图片无法展示
|

3.外键表反向访问

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

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

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

网络异常,图片无法展示
|
通过 表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')

网络异常,图片无法展示
|
就可以使用更直观的属性名,像这样

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

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()

网络异常,图片无法展示
|
但是这样同样存在 麻烦 和性能的问题。 Django ORM 可以这样写:

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

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

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

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

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

使用 .distinct()去重

但是,我们发现,这种方式,会有重复的记录产生,如下

网络异常,图片无法展示
|

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

可以使用 .distinct() 去重:

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

网络异常,图片无法展示
|

注意: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请求'})

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

然后,我们在 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),
]

网络异常,图片无法展示
|

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})

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

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

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

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

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

OrderMedicine.objects.create(order_id=new_order.id,medicine_id=mid,amount=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)

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: "青霉素 | 红霉素 "
    }
]

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

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

问题是:‘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})

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})

网络异常,图片无法展示
|

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})

网络异常,图片无法展示
|
网络异常,图片无法展示
|

目录
相关文章
|
5月前
|
API C++ 开发者
PySide vs PyQt:Python GUI开发史诗级对决,谁才是王者?
PySide 和 PyQt 是 Python GUI 开发领域的两大利器,各有特色。PySide 采用 LGPL 协议,更灵活;PyQt 默认 GPL,商业使用需授权。两者背后团队实力雄厚,PySide 得到 Qt 官方支持,PyQt 由 Riverbank Computing 打造。API 设计上,PySide 简洁直观,贴近原生 Qt;PyQt 增加 Pythonic 接口,操作更高效。性能方面,两者表现优异,适合不同需求的项目开发。选择时可根据项目特点与开源要求决定。
342 20
|
7月前
|
前端开发 JavaScript 关系型数据库
基于Python+Vue开发的商城管理系统源码+运行步骤
基于Python+Vue开发的商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Python编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Python的网上商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
212 7
|
3月前
|
数据采集 存储 数据库
Python爬虫开发:Cookie池与定期清除的代码实现
Python爬虫开发:Cookie池与定期清除的代码实现
|
4月前
|
IDE 开发工具 Python
魔搭notebook在web IDE下,使用jupyter notebook,python扩展包无法更新升级
魔搭notebook在web IDE下,使用jupyter notebook,python扩展包无法更新升级,不升级无法使用,安装python扩展包的时候一直停留在installing
99 4
|
4月前
|
人工智能 搜索推荐 数据可视化
用 Python 制作简单小游戏教程:手把手教你开发猜数字游戏
本教程详细讲解了用Python实现经典猜数字游戏的完整流程,涵盖从基础规则到高级功能的全方位开发。内容包括游戏逻辑设计、输入验证与错误处理、猜测次数统计、难度选择、彩色输出等核心功能,并提供完整代码示例。同时,介绍了开发环境搭建及调试方法,帮助初学者快速上手。最后还提出了图形界面、网络对战、成就系统等扩展方向,鼓励读者自主创新,打造个性化游戏版本。适合Python入门者实践与进阶学习。
334 1
|
4月前
|
Linux 数据库 数据安全/隐私保护
Python web Django快速入门手册全栈版,共2590字,短小精悍
本教程涵盖Django从安装到数据库模型创建的全流程。第一章介绍Windows、Linux及macOS下虚拟环境搭建与Django安装验证;第二章讲解项目创建、迁移与运行;第三章演示应用APP创建及项目汉化;第四章说明超级用户创建与后台登录;第五章深入数据库模型设计,包括类与表的对应关系及模型创建步骤。内容精炼实用,适合快速入门Django全栈开发。
118 1
|
4月前
|
存储 算法 数据可视化
用Python开发猜数字游戏:从零开始的手把手教程
猜数字游戏是编程入门经典项目,涵盖变量、循环、条件判断等核心概念。玩家通过输入猜测电脑生成的随机数,程序给出提示直至猜中。项目从基础实现到功能扩展,逐步提升难度,适合各阶段Python学习者。
191 0
|
6月前
|
程序员 测试技术 开发工具
怎么开发Python第三方库?手把手教你参与开源项目!
大家好,我是程序员晚枫。本文将分享如何开发Python第三方库,并以我维护的开源项目 **popdf** 为例,指导参与开源贡献。Popdf是一个PDF操作库,支持PDF转Word、转图片、合并与加密等功能。文章涵盖从fork项目、本地开发、单元测试到提交PR的全流程,适合想了解开源贡献的开发者。欢迎访问[popdf](https://gitcode.com/python4office/popdf),一起交流学习!
209 21
怎么开发Python第三方库?手把手教你参与开源项目!
|
4月前
|
数据采集 存储 监控
抖音直播间采集提取工具,直播间匿名截流获客软件,Python开发【仅供学习】
这是一套基于Python开发的抖音直播间数据采集与分析系统,包含观众信息获取、弹幕监控及数据存储等功能。代码采用requests、websockets和sqlite3等...
|
6月前
|
数据采集 人工智能 测试技术
Python有哪些好用且实用的Web框架?
Python 是一门功能强大的编程语言,在多个领域中得到广泛应用,包括爬虫、人工智能、游戏开发、自动化测试和 Web 开发。在 Web 开发中,Python 提供了多种框架以提高效率。以下是几个常用的 Python Web 框架:1) Django:开源框架,支持多种数据库引擎,适合新手;2) Flask:轻量级框架,基于简单核心并通过扩展增加功能;3) Web2py:免费开源框架,支持快速开发;4) Tornado:同时作为 Web 服务器和框架,适合高并发场景;5) CherryPy:简单易用的框架,连接 Web 服务器与 Python 代码。这些框架各有特色,可根据需求选择合适的工具。
281 14

推荐镜像

更多