ORM关联表、事务
您需要高效学习,找工作? 点击咨询 报名实战班
点击查看学员就业情况
ORM 对关联表的操作
前面我们学过 一对多,一对一,多对多,都是通过外键来实现。
接下来,我们通过一个实例演示,Django ORM 如何 操作 外键关联关系
请大家在 models.py
中定义这样的两个Model,对应两张表
# 国家表
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)
然后,执行
使定义生效到数据库中。
然后,命令行中执行 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)
外键表字段访问
如果你已经获取了一个student对象,要得到他的国家名称只需这样
外键表字段过滤
如果,我们要查找Student表中所有 一年级
学生,很简单
如果现在,我们要查找Student表中所有 一年级中国
学生,该怎么写呢?
不能这么写:
因为,Student表中 country
并不是国家名称字符串字段,而是一个外键字段,其实是对应 Country 表中 id
字段 。
可能有的朋友会这样想:我可以先获取中国的国家id,然后再通过id去找,像这样
注意外键字段的id是通过后缀 _id
获取的。
或者这样,也是可以的
上面的方法,写起来麻烦一些,有两步操作。而且需要发送两次数据请求给数据库服务,性能不高。
其实,Django ORM 中,对外键关联,有更方便的语法。
可以这样写
写起来简单,一步到位,而且只需要发送一个数据库请求,性能更好。
如果返回结果只需要 学生姓名 和 国家名两个字段,可以这样指定values内容
但是这样写有个问题:选择出来的记录中,国家名是 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')
外键表反向访问
前面学过, Django ORM中,关联表 正向关系是通过表外键字段(或者多对多)表示, 比如前面例子中Student表的 country字段。
而反向关系,是通过 表Model名转化为小写
表示的。
比如,你已经获取了一个Country对象,如何获取到所有属于这个国家的学生呢?
可以这样
通过 表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')
就可以使用更直观的属性名,像这样
外键表反向过滤
如果我们要获取所有 具有一年级学生 的国家名,该怎么写?
当然可以这样
# 先获取所有的一年级学生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 可以这样写
注意, 因为,我们定义表的时候,用 related_name='students'
指定了反向关联名称 students
,所以这里是 students__grade
。 使用了反向关联名字。
如果定义时,没有指定related_name, 则应该使用 表名转化为小写
,就是这样
但是,我们发现,这种方式,会有重复的记录产生,如下
<QuerySet [{'id': 1, 'name': '中国'}, {'id': 1, 'name': '中国'}, {'id': 2, 'name': '美国'}, {'id': 2, 'name': '美国'}]>
可以使用 .distinct()
去重
注意:据说 .distinct()
对MySQL数据库无效,我没有来得及验证。实测 SQLite,Postgresql有效。
实现项目代码
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),
]
事务、多对多记录添加
接下来,我们添加函数 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 表中的数据 可能有很多条, 如果我们循环用
插入的话, 循环几次, 就会执行 几次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)
写好后, 大家可以运行服务 , 用我们做好的前端系统添加几条 订单记录, 然后再查看一下数据库里面的数据是否正确。
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})
我们可以 浏览器访问一下 订单管理界面, F12 查看 浏览器抓包。
同样的道理 , 订单对应 的药品 名字段,是 多对多 关联, 也同样可以用 两个下划线 获取 关联字段的值, 如下所示:
def listorder(request):
qs = Order.objects\
.values(
'id','name','create_date',
'customer__name',
# 两个下划线,表示取medicines 关联的表中的name字段的值
# 如果有多个,就会产生多条记录
'medicines__name'
)
# 将 QuerySet 对象 转化为 list 类型
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
我们可以 浏览器访问一下 订单管理界面, F12 查看 浏览器抓包。
首先,第一个问题, 接口文档需要的名字是 '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})
第二个问题,如果一个订单里面有多个药品,就会产生多条记录, 这不是我们要的。
根据接口,一个订单里面的多个药品, 用 竖线 隔开。
怎么办?
我们可以用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)
# retlist里的一个订单中,多个药品,会有多条记录, 需要合并
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})
如果前端开发工程师也完成了他们的前端开发,就可以进行集成测试了。
目前为止,我们项目代码,在如下百度网盘中的 bysms_08.zip
百度网盘链接:https://pan.baidu.com/s/1nUyxvq6IYykBNtPUf4Ho6w
提取码:9w2u