数据库表的关联
您需要高效学习,找工作? 点击咨询 报名实战班
点击查看学员就业情况
后端系统开发中, 数据库设计是重中之重。
特别是前后端分离的系统, 后端的职责基本就是数据管理, 开发的代码几乎都是围绕数据操作的。
虽然,我们教程不是专门讲数据库的, 但也必须讲解常用的 数据库表 和 表之间的关系 的设计 。
目前 使用的数据库系统 主要还是 关系型数据库
。
什么是关系型数据库?就是建立在关系模型基础上的数据库。
大家耳熟能详的mysql、oracle、 sqlserver、SQLite 都是,而 mongodb、Cassandra不是
而关系型数据库,设计的一个难点就是 各种表之间的关联关系
。
常见的3种关联关系就是: 一对多
, 一对一
, 多对多
一对多
表之间 一对多
的关系,就是 外键
关联关系
比如,我们的 BYSMS 系统中, 已经定义了 客户(Customer)
这张表 。如下所示
class Customer(models.Model):
# 客户名称
name = models.CharField(max_length=200)
# 联系电话
phonenumber = models.CharField(max_length=200)
# 地址
address = models.CharField(max_length=200)
现在我们还需要定义
药品(Medicine)
这张表,包括药品名称、编号和描述 这些信息。
这个也很简单,添加如下的类定义
class Medicine(models.Model):
# 药品名
name = models.CharField(max_length=200)
# 药品编号
sn = models.CharField(max_length=200)
# 描述
desc = models.CharField(max_length=200)
接下来我们要定义 订单(Order)
这张表,这个Order表 包括 创建日期、客户、药品、数量。
其中:
客户字段对应的客户 只能是 Customer
中的某个客户记录
可以说:
Order表里面 一条订单记录的客户 对应 Customer表里面的一条客户记录
而 多条 Order记录里面的客户 是可以对应 Customer 表里面 同一个客户记录的,
反过来说,就是:一个客户记录可以对应多条订单记录
这就是一对多的关系,可以用如下图片表示
像这种一对多的关系,数据库中是用 外键 来表示的。
如果一个表中 的 某个字段是外键,那就意味着 这个外键字段的记录的取值,只能是它关联表的某个记录的主键的值。
我们定义表的 Model类的时候,如果没有指定主键字段,migrate 的时候 Django 会为该Model对应的数据库表自动生成一个id字段,作为主键。
比如,我们这里,Customer、Medicine表均没有主键,但是在migrate之后,查看数据库记录就可以发现有一个id字段,且该字段是 主键 (primary key)
现在我们要定义 订单 表 Order,
其中客户字段就应该是一个外键,对应Customer表的主键,也就是id字段
Django中定义外键 的方法就是 Model类的该属性字段 值为 ForeignKey
对象,如下所示
import datetime
class Order(models.Model):
# 订单名
name = models.CharField(max_length=200,null=True,blank=True)
# 创建日期
create_date = models.DateTimeField(default=datetime.datetime.now)
# 客户
customer = models.ForeignKey(Customer,on_delete=models.PROTECT)
大家可以发现, customer 字段 是外键, 指向 Customer 类。 意思就是告诉Django: Order表的 customer 字段 指向 Customer表的主键 的一个外键。
另外一个参数 on_delete 指定了 当我们想 删除
外键指向的主键 记录时, 系统的行为。
比如 我们要删除客户记录, 那么 Order表中 对应这个客户的订单记录 该如何处理呢?
on_delete 不同取值对应不同的做法,常见的做法如下
- CASCADE
删除主键记录和 相应的外键表记录。
比如,我们要删除客户张三,在删除了客户表中张三记录同时,也删除Order表中所有这个张三的订单记录
- PROTECT
禁止删除记录。
比如,我们要删除客户张三,如果Order表中有张三的订单记录,Django系统 就会抛出ProtectedError类型的异常,当然也就禁止删除 客户记录和相关的订单记录了。
除非我们将Order表中所有张三的订单记录都先删除掉,才能删除该客户表中的张三记录。
- SET_NULL
删除主键记录,并且将外键记录中外键字段的值置为null。 当然前提是外键字段要设置为值允许是null。
比如,我们要删除客户张三时,在删除了客户张三记录同时,会将Order表里面所有的 张三记录里面的customer字段值置为 null。 但是上面我们并没有设置 customer 字段有 null=True
的参数设置,所以,是不能取值为 SET_NULL的。
注意: 外键字段,实际在数据库表中的 字段名, 是 Django ForeignKey 定义 字段名加上后缀 _id
。
比如这里,在执行了 migrate 命令更新数据库后, customer 这个外键字段实际上在 数据库表中的字段名 是 customer_id
一对一
外键是 一对多 的关系, 也可以说是 多对一 的关系。
有的时候,表之间是 一对一
的关系。
比如,某个学校的学生 表 和学生的地址表,就形成一对一的关系,即 一条主键所在表的记录 只能对应一条 外键所在表的记录。
Django 中 用 OneToOneField
对象 实现 一对一 的关系,如下
class Student(models.Model):
# 姓名
name = models.CharField(max_length=200)
# 班级
classname = models.CharField(max_length=200)
# 描述
desc = models.CharField(max_length=200)
class ContactAddress(models.Model):
# 一对一 对应学生
student = models.OneToOneField(Student, on_delete=models.PROTECT)
# 家庭
homeaddress = models.CharField(max_length=200)
# 电话号码
phone = models.CharField(max_length=200)
Django发现这样一对一定定义,它会在migrate的时候,在数据库中定义该字段为外键的同时, 加上 unique=True
约束,表示在此表中,所有记录的该字段 取值必须唯一,不能重复。
多对多
数据库表还有一种 多对多 的关系。
在我们的 BYSMS系统中, 一个订单可以采购多种药品,就对应 Medicine表里面的多种药品;而一种药品也可以被多个订单采购, 那么Order表 和 Medicine表 之间就形成了多对多的关系。
其对应关系可以用下图来表示
Django是通过 ManyToManyField
对象 表示 多对多的关系的。
如下所示:
import datetime
class Order(models.Model):
# 订单名
name = models.CharField(max_length=200,null=True,blank=True)
# 创建日期
create_date = models.DateTimeField(default=datetime.datetime.now)
# 客户
customer = models.ForeignKey(Customer,on_delete=models.PROTECT)
# 订单购买的药品,和Medicine表是多对多 的关系
medicines = models.ManyToManyField(Medicine, through='OrderMedicine')
class OrderMedicine(models.Model):
order = models.ForeignKey(Order, on_delete=models.PROTECT)
medicine = models.ForeignKey(Medicine, on_delete=models.PROTECT)
# 订单中药品的数量
amount = models.PositiveIntegerField()
像这样
指定Order表和 Medicine 表 的多对多关系, 其实Order表中并不会产生一个 叫 medicines 的字段。
Order表和 Medicine 表 的多对多关系 是 通过另外一张表, 也就是 through 参数 指定的 OrderMedicine 表 来确定的。
migrate的时候,Django会自动产生一张新表 (这里就是 common_ordermedicine)来 实现 order 表 和 medicine 表之间的多对多的关系。
大家可以执行下面两行命令 migrate 试一下
就会发现产生如下的一张新表 common_ordermedicine:
可以发现这张表中有 order_id 和 medicine_id 两个字段。
比如一个order表的订单id 为 1, 如果该订单中对应的药品有3种,它们的id分别 为 3,4,5。 那么就会有类似这样的这样3条记录在 common_order_medicine 表中。
order_id | medicine_id |
---|---|
1 | 3 |
1 | 4 |
1 | 5 |
实现代码
现在我们要实现 药品管理 和 订单管理 的服务端代码了。
药品管理
其中药品管理部分比较简单, 和前面的 customer.py 的代码 基本类似。
我们在 mgr 目录下面新建 medicine.py,处理 客户端发过来的 列出药品、添加药品、修改药品、删除药品 的请求。
如下所示
from django.http import JsonResponse
# 导入 Medicine 对象定义
from common.models import Medicine
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_medicine':
return listmedicine(request)
elif action == 'add_medicine':
return addmedicine(request)
elif action == 'modify_medicine':
return modifymedicine(request)
elif action == 'del_medicine':
return deletemedicine(request)
else:
return JsonResponse({'ret': 1, 'msg': '不支持该类型http请求'})
def listmedicine(request):
# 返回一个 QuerySet 对象 ,包含所有的表记录
qs = Medicine.objects.values()
# 将 QuerySet 对象 转化为 list 类型
# 否则不能 被 转化为 JSON 字符串
retlist = list(qs)
return JsonResponse({'ret': 0, 'retlist': retlist})
def addmedicine(request):
info = request.params['data']
# 从请求消息中 获取要添加客户的信息
# 并且插入到数据库中
medicine = Medicine.objects.create(name=info['name'] ,
sn=info['sn'] ,
desc=info['desc'])
return JsonResponse({'ret': 0, 'id':medicine.id})
def modifymedicine(request):
# 从请求消息中 获取修改客户的信息
# 找到该客户,并且进行修改操作
medicineid = request.params['id']
newdata = request.params['newdata']
try:
# 根据 id 从数据库中找到相应的客户记录
medicine = Medicine.objects.get(id=medicineid)
except Medicine.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{medicineid}`的药品不存在'
}
if 'name' in newdata:
medicine.name = newdata['name']
if 'sn' in newdata:
medicine.sn = newdata['sn']
if 'desc' in newdata:
medicine.desc = newdata['desc']
# 注意,一定要执行save才能将修改信息保存到数据库
medicine.save()
return JsonResponse({'ret': 0})
def deletemedicine(request):
medicineid = request.params['id']
try:
# 根据 id 从数据库中找到相应的药品记录
medicine = Medicine.objects.get(id=medicineid)
except Medicine.DoesNotExist:
return {
'ret': 1,
'msg': f'id 为`{medicineid}`的客户不存在'
}
# delete 方法就将该记录从数据库中删除了
medicine.delete()
return JsonResponse({'ret': 0})
实现了请求处理的模块后,我们可以在 mgr\urls.py 里面加上 对 medicine 的 请求处理 路由设置
from django.urls import path
from mgr import customer,sign_in_out,medicine,order
urlpatterns = [
path('customers', customer.dispatcher),
path('medicines', medicine.dispatcher), # 加上这行
path('signin', sign_in_out.signin),
path('signout', sign_in_out.signout),
]
我的前端代码已经开发好了对药品的 增删改查处理, 所以可以和我们上面的代码进行集成测试了。
大家可以运行服务,在界面上操作测试一下。