适当的数据库冗余

现在我们的 mgr/order.py 里面用 listorder 函数列出订单。

如果一个订单里面有多个药品,就会产生多条记录。

为了解决这个问题,我们不得不用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})



这样做其实有一些问题。

首先,它使得我们的代码增加了去除重复记录的处理,比较麻烦。

另外,还会带来性能问题:当用户大量登录,需要列出订单信息 的时候,服务程序从数据库获取到数据后,还得还要执行去除重复记录的任务。

那么怎样解决这个问题呢?



我们可以修改数据库表的设计, 就在 Order 表里面 直接存入 订单包含的药品信息。

这样,就不需要 从 OrderMedicine 表里面 去获取关联药品信息了,当然也不需要去除重复的代码了。

那么我们怎样在 Order 表 里面存储订单包含的药品信息呢?

这要包括 药品的ID,名称,和数量。而且订单中可能包含多种药品。

关键是:不同的订单记录 药品的数量 是 不同 的。

对于这种情况,我们通常可以使用一个字段, 里面存储 json格式的字符串,记录可变数量的数据信息。

修改 Order 表对应的类, 如下

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

    # 为了提高效率,这里存放 订单 medicines 冗余数据
    medicinelist =  models.CharField(max_length=2000,null=True,blank=True)


在最后 添加的 medicinelist 是一个字符串,里面用json格式来存储订单中的药品。像这样:

[
    {"id": 1, "name": "青霉素", "amount": 20}, 
    {"id": 2, "name": "来适可", "amount": 100}
]

其中:

id 表示药品的id,

name 表示 药品的名字,

amount 表示 药品的数量

上面的例子就表示该订单中有id 为 1 和 2 的两种药品 数量分别是 20 和 100

这个字段最大长度可达2000个字符,通常足以存储订单中的药品信息了。


修改了数据库类的定义, 别忘了migrate到数据库。 执行下面的命令

python manage.py makemigrations common
python manage.py migrate


我们发现,现在 Order表里面需要存储订单中药品的名字和数量, 而当前的接口的定义是没法满足这个需求的。

API v1.0 文档,添加消息体的格式如下:

{
    "action":"add_order",
    "data":{
        "name":"华山医院订单002",
        "customerid":3,
        "medicineids":[1,2]
    }
}

这里面只有药品的id,没有药品的 名称和数量。

我们在开发的时候,经常会遇到 当前的接口设计不能满足新的需求,需要修改 的情况。

这时候就要和 接口的设计者 , 以及接口对接的开发团队进行沟通, 说明为什么你需要修改接口。

讨论通过后,接口的设计者 就要修改接口文档。

我们这里,修改后就是的文档:API接口文档1.1, 点击查看


我们这样修改接口定义:

添加订单的格式为

{
    "action":"add_order",
    "data":{
        "name":"华山医院订单002",
        "customerid":3,
        "medicinelist":[
            {"id":16,"amount":5,"name":"环丙沙星"},
            {"id":15,"amount":5,"name":"克林霉素"}
        ]
    }
}

列出订单中,每个订单的格式为

    {
        "id": 2, 
        "name": "华山医院订单002", 
        "create_date": "2018-12-27T14:10:37.208Z", 
        "customer_name": "华山医院",
        "medicinelist":[
            {"id":16,"amount":5,"name":"环丙沙星"},
            {"id":15,"amount":5,"name":"克林霉素"}
        ]
    }


好的,既然接口变动了,前端的开发团队 要根据修改后的接口,修改他们的代码,保证按照新的接口实现消息格式。

想象一下,前端开发人员领命 重新修改代码去了。。。


当然,我们作为后端开发团队,也要立即动工。

修改 mgr/order.py 中 添加订单的代码,如下

def addorder(request):
    info = request.params['data']

    with transaction.atomic():
        medicinelist  = info['medicinelist']

        new_order = Order.objects.create(name=info['name'],
            customer_id=info['customerid'],
            # 写入json格式的药品数据到 medicinelist 字段中
            medicinelist=json.dumps(medicinelist,ensure_ascii=False),)

        batch = [OrderMedicine(order_id=new_order.id,
                               medicine_id=medicine['id'],
                               amount=medicine['amount'])
                 for medicine in medicinelist]

        OrderMedicine.objects.bulk_create(batch)

    return JsonResponse({'ret': 0, 'id': new_order.id})

基本只有一处修改,把药品数据信息写入到 新增的medicinelist表字段中


还需要修改 列出订单函数,如下:

def listorder(request):
    qs = Order.objects \
        .annotate(
                customer_name=F('customer__name')
        )\
        .values(
        'id', 'name', 'create_date',
        'customer_name',
        'medicinelist'
    )

    # 将 QuerySet 对象 转化为 list 类型
    retlist = list(qs)

    return JsonResponse({'ret': 0, 'retlist': retlist})


哈哈,打完收工了。

好处很明显,列出订单的代码就比较简单了,不需要执行去重的任务。

性能也提高了, 只要查询一张表,并且不要执行去重的任务。



那么有没有什么坏处呢?大家自己先思考一下,再看后面的内容。


当然也有, 就是出现了 冗余数据 在数据库中。

订单里面订购了什么药品,我们本来记录在 OrderMedicine 表 中的, 现在我们还需要记录在 Order 表中。

这就意味着, 我们 修改 订单中关联的药品, 必须修改两张表。

可能你疑惑了, 既然 修改 的时候需要更新2张表,那么这也会造成性能的降低啊? 凭什么你说 这种方案比原来的性能好呢?

这就要看这张表 最频繁的操作是什么, 是查询还是更新订单中的药品。

我们订单数据要被大量的用户查看, 而更新订单中 关联的 药品 的情况相对较少, 所以权衡利弊后,采用冗余数据的方案。


可能你还有一个疑问, 我们能不能就只在 Order 表中记录 订单信息,不需要 OrderMedicine 表 中记录呢。

这样不是更简单了吗?

这样做,最大的问题是, 如果以后我们需要统计药品被采购的信息就非常麻烦了。

因为,现在我们在数据库中存储的订单中的药品信息,是以字符串类型存储的 json信息,不能直接使用SQL语句进行过滤查询,只能把所有的记录读取出来进行分析,那样性能会非常低。

新版本的 PostGresql 和 Mysql 提供了json格式的字段 ( 但是目前 Django 只支持 PostGresql 里面的json字段 ),可以部分解决这个问题,感兴趣的朋友可以自行研究一下。


好的,我们的代码完成了,大家可以先使用 requests构建前端请求,自己测试一下。

等待前端开发人员也修改好前端代码,就可以集成在一起发布了 :)


目前为止,我们系统的完整代码(包括前端发布的更新),点击这里下载



上一页 下一页