操作系统: windows
IDE: Pycharm
前面数据库的一对多,一对一,多对多,都是通过外键来实现。
接下来,通过一个实例演示,Django ORM如何操作外键关联关系,来实现根据条件进行查询。
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)
还是老套路,在根目录cmd中:
python manage.py makemigrations common
python manage.py migrate
在数据库中生成两张表。
前面往数据库中填入消息在前端实现,其实也可以通过shell来实现:
在命令行执行: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=3, country=c1)
Student.objects.create(name='Sheldon', grade=1, country=c2)
Student.objects.create(name='Penny', grade=1, country=c2)
Student.objects.create(name='Leonard', grade=3, country=c2)
Student.objects.create(name='Howard', grade=2, country=c2)
Student.objects.create(name='Raj', grade=2, country=c3)
通过对象访问外键表:
如果你已经获取了一个student对象,要得到他的国家名称只需这样:
s1 = Student.objects.get(name='小明')
s1.country.name
.object.get()
获得了一个实例,s1
就是一个Student类的对象,s1.country
就是外键所对应的对象,s1.country.name
就获得了小明对应的国家的名称:
根据外键表数据过滤
如果需要查找Student表中所有一年级学生:
Student.objects.filter(grade=1).values()
现在如果需要查找Student表中一年级的中国学生,该怎么做呢?
Student.objects.filter(grade=1,country='中国')
然而这样做并不可行。因为Student表中没有名为country的字段,只有一个名为country_id的外键。
正确的做法:可以先获取中国的国家id,然后再通过id去找,像这样:
cn = Country.objects.get(name='中国')
Student.objects.filter(grade=1,country_id=cn.id).values()
# 上面的cn.id直接用cn也是可以的
注意外键字段的id是通过后缀_id
获取的。
上面的写法,有两步操作,会比较麻烦,事实上在Django ORM中,对外键关联,有更方便的语法:
Student.objects.filter(grade=1,country__name='中国').values()
注意:是两个下划线__
,ORM会自动找country外键对应的表里面name='中国'
的记录。一步到位,并且只需要发送一个数据库请求。
如果返回结果中只需要学生姓名
和国家名
两个字段,可以这样指定values()内容:
Student.objects.filter(grade=1,country__name='中国').values('name','country__name')
在某些情况下,接口里规定的可能就是countryname,而不是country__name,可以使用 annotate
方法将获取的字段值进行重命名:
(annotate写在filter之前,annotate(新名字=F(‘老名字’)))
from django.db.models import F
# annotate 可以将表字段进行别名处理
Student.objects.annotate(
countryname=F('country__name'),studentname=F('name')
).filter(grade=1,countryname='中国').values('studentname','countryname')
当接口要求和当前命名不一样的时候就可以用annotate
来进行转换
反向访问:
如果已经获取了一个Country对象,如何访问到所有属于这个国家的学生呢?
cn = Country.objects.get(name='中国')
cn.student_set.all()
将Student转化为小写,然后加上_set
来获取所有的反向外键关联对象。
然后可以用cn.student_set.all()[0].name
这样来获取第一条对象的name的值,像对对象的操作一样。
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()
反向过滤:
如果需要获取所有一年级学生的国家名可以这样:
# 先获取所有一年级学生的id列表
country_ids=Student.objects.filter(grade=1).values_list('country',flat=True)
# 再通过id列表使用id__in 过滤
Country.objects.filter(id__in=country_ids).values()
注:
flat=True
就是直接把值拿出来组成QuerySet,而不是'键':'值'
这样的形式。id__in
,两个下划线,过滤用
这个写法需要访问数据库两次,性能也不佳。
反向访问也有Django ORM支持的更方便的写法:
没有定义反向related_name,就将类名Student改为小写,然后加两个下划线:
Country.objects.filter(students__grade=1).values()
注意: 这里由于在Models定义表的时候,用related_name='students'
指定了反向关联名称students
,所以这里是students__grade
(两个下划线)。
但是会有重复字段,可以用.distinct()
去重:
注意: 据说.distinct()
对MySql数据库无效,实测对SQLite有效
应用到项目中:
首先先浏览订单相关的api文档:
列出所有订单
请求消息
GET /api/mgr/orders HTTP/1.1
请求参数
http 请求消息 url 中 需要携带如下参数:
- action 填写值为 list_order
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,如果获取信息成功,返回如下:
{
"ret": 0,
"retlist": [
{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: "青霉素 | 红霉素 "}
]
}
- ret 为 0 表示登录成功
- retlist 里面包含了所有的订单信息列表。
每个订单信息以如下格式存储:
{
id: 2,
name: "华山医院订单002",
create_date: "2018-12-27T14:10:37.208Z",
customer_name: "华山医院",
medicines_name: "青霉素 | 红霉素 "
}
其中 medicines_name 表示对应的药品,如果该订单有多个药品, 中间用 竖线隔开
添加一个订单
请求消息
POST /api/mgr/orders HTTP/1.1
Content-Type: application/json
请求参数
http 请求消息 body 携带添加订单的信息
消息体的格式是json,如下示例:
{
"action":"add_order",
"data":{
"name":"华山医院订单002",
"customerid":3,
"medicineids":[1,2]
}
}
其中
- action 字段固定填写 add_order 表示添加一个订单
- data 字段中存储了要添加的订单的信息
- medicineids 是 该订单中药品的id 列表
服务端接受到该请求后,应该在系统中增加这样的订单。
响应消息
HTTP/1.1 200 OK
Content-Type: application/json
响应内容
http 响应消息 body 中, 数据以json格式存储,
如果添加成功,返回如下
{
"ret": 0,
"id" : 677
}
- ret 为 0 表示成功。
- id 为 添加订单的id号。
如果添加失败,返回失败的原因,示例如下
{
"ret": 1,
"msg": "订单名已经存在"
}
- ret 不为 0 表示失败, msg字段描述添加失败的原因
修改订单
暂不支持
删除订单
暂不支持
在 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请求'})
记得添加路由!
接下来要具体的写listorder
和addorder
函数
添加订单addorder:
现在需要函数 addorder
,来处理添加订单请求。
根据api文档,添加一条订单记录,需要在2张表(Order 和 OrderMedicine )中添加记录。
特别注意: 两张表的插入,意味着要有两次数据库操作。
如果第一次插入成功, 而第二次插入失败, 就会出现 Order表中把订单信息写了一部分,而OrderMedicine表中该订单的信息却没有写成功。
出大问题: 就会造成这个处理做了一半,那么数据库中就会出现数据的不一致。术语叫脏数据,数据库的事务机制来解决这个问题。
把一批数据库操作放在事务中, 该事务中的任何一次数据库操作失败了, 数据库系统就会让整个事务就会发生回滚,撤销前面的操作, 数据库回滚到这事务操作之前的状态,事务具有原子性,一个事务不可分割。
使用Django的 with transaction.atomic()
来进行事务操作:
def addorder(request):
info = request.params['data']
# 从请求消息中获取要添加订单的信息 并且插入到数据库中
# atomic原子性的,不可分割。 with transaction.atomic()下的都视为一次事务操作
with transaction.atomic():
new_order = Order.objects.create(name=info['name'],
customer_id=info['customerid'])
# batch列表用于存放药品记录,medicineids里有多少药品id就插入多少条记录
batch = [OrderMedicine(order_id=new_order.id, medicine_id=mid, amount=1)
for mid in info['medicineids']]
# bulk_create比用for循环来实现create性能更好,bulk_create参数是一个包含所有该表的 Model 对象的列表
OrderMedicine.objects.bulk_create(batch)
return JsonResponse({'ret': 0, 'id': new_order.id})
列出订单listorder:
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表中就有这些字段,而:‘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可以看到确实是能获取到客户名称的,但为什么不能显示呢?
答:因为ORM获取到的客户名称是customer__name
,接口定义的是customer_name
,药品名称medicines_name
也是同理。可以用annotate()
方法将获取:
from django.db.models import F
def listorder(request):
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})
这是现在的成果:
然后需要修改一下数据的格式:
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})
就实现了有外键关联时候表的列出数据和添加数据的处理