调研:浅淡Django中多种序列化的方案及其性能

调研:浅淡Django中多种序列化的方案及其性能,第1张

调研:浅淡Django中多种序列化的方案及其性能

目前,web系统在前后端分离,通过API通信的大环境下,JSON序列化是后端必不可少的工作,在Jango中常用的序列化方案有多种,我很好奇它们之间有何种区别,什么情况下性能最佳,于是有了这次的调研。

  • 首先弄清楚什么是序列化:

python已经集成了自己的序列化模块,如 json,其中的 json.dumps、和json.loads,就能实现字典和json字符串的转换。

实际上所谓的序列化,就是将字典转换成符合字典格式的字符串,反序列化则就是将其倒过来。

# type:dict
dict_object = {"id": 1, "name": "王二狗", "age": 18}

# type:str
json_str = '{"id": 1, "name": "王二狗", "age": 18}'
  • 介绍序列化方案前,展示下我定义的model:
from django.db import models


class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()
    hireDate = models.DateField()

    def __str__(self):
        return str(self.id)

    class meta:
        verbose_name = '人员信息'


class Vocation(models.Model):
    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20)
    title = models.CharField(max_length=20)
    payment = models.IntegerField()
    name_id = models.IntegerField()

    def __str__(self):
        return str(self.id)

    class meta:
        verbose_name = '职业信息'
  • Djano中自带的序列化方案和常见的第三方序列化方案:
  1. JsonResponse 
  2. from django.core import serializers  
  3. from rest_framework import serializers

其中,前两种,是Django自带的序列化方案,后者则是rest_framwork中所带的序列化方案,当然想要序列化还有很多方案,这里只列出几种常用的。


  • 基础介绍:

JsonResponse  使用起来十分简单,只要将你要返回的内容以字典的形式传入其中,就能给前端返回相对应的Json字符串,如下图。

class TestView(View):
    sql = "SELECt * FROM index_vocation"
    column_list = ['id', 'job', 'title', 'payment', 'name_id']

    def get(self, request):
        raws, sql_code = django_sql(self.sql, dict_keys=self.column_list, types='list_dict')
        return JsonResponse(data={'data': raws}, json_dumps_params={'ensure_ascii': False}, status=sql_code)

其中的django_sql是我自己封装的一个数据库查询模块,它会执行SQL,并将结果格式化成:列表包裹着字典的形式 [{}...],结果如下:

这里需要注意的是,json_dumps_params={'ensure_ascii': False} 这个参数,如果不关闭的话,它会将返回参数中的中文,转换成ascii码。


from django.core import serializers:

        serializers是django中自带的序列化方法,用法如下:

class TestViewDjango(View):

    def get(self, request):
        result = Vocation.objects.all()
        data = serializers.serialize('json', result, ensure_ascii=False)
        return HttpResponse(content=data, status=200)

serializers.serialize() 的必填参数有两个。其一是你需要序列化的格式,输入的是字符串,这里用的是json,此外还支持xml等格式的转换。其二是你需要转换的内容,必须是一个 QuerySet 对象,也就是你从 ORM 中直接获取的数据集对象,而后面的 ensure_ascii=False ,上面刚讲过,不再介绍。

执行结果如下:

可见这种序列化方案,得到的结果比较乱,还带有很多不需要的数据内容,转换起来挺麻烦。


from rest_framework import serializers:

这个是Django中用于编写 REST FULL API 的成熟框架:django_rest_framework,所带的序列化方案,它需要你自己定义序列化模型:

from rest_framework import serializers
from .models import PersonInfo, Vocation

class MySerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    job = serializers.CharField(max_length=10)
    title = serializers.CharField(max_length=10)
    payment = serializers.IntegerField()
    name_id = serializers.IntegerField()

当然你也可以直接继承你的model,用法和Django的FORM模块极为相似,不懂的可以看一下官方文档,不过多介绍,之后是view.py中的代码。

from rest_framework.response import Response
from index.serializers import MySerializer

class TestViewRest(APIView):

    def get(self, request):
        result = Vocation.objects.all()
        serializer = MySerializer(instance=result, many=True)
        return Response(serializer.data)

其中 instance 是你需要序列化的对象,这里也是一个 QuerySet 对象,而后面的 many =True 是指序列化多个数据对象,与上面的 Vocation.objects.all() 匹配,如果你这里是 Vocation.objects.first(),则就该使用 many = False。

而return中使用的Response,也是rest_framwork定义的特殊返回函数,它只支持由rest_framwork框架自己序列化的内容,同时它能美化返回结果。

执行结果如下,某种层次来说,这已经有点像是个接口文档了,rest_framwork 也确实可以根据接口自己生成接口文档:


  • 性能测试        

接下来,我们来看看这三种序列化的方法的性能究竟如何,我们先往数据库里插入100条数据,通过 line_profiler(一个统计函数耗时的包,我有文章单独介绍过如何使用) 进行序列化性能测试,结果如下。

sql + JsonResponse  :0.0381s

ORM + Django serializers : 0.1446s

 ORM + django_rest_framework serializers : 0.0424s

根据耗时可见,第一种方式的性能和后两者远不在同一个等级,后续我们加入更多的数据,进行测试,统计了表如下。

数据量:100数据量:1000数据量:10000数据量:100000JsonResponse  0.0381s0.0861s0.4029s2.8002sDjango serializers0.1446s0.4086s4.5337s24.083srest_framework0.0424s0.2608s2.4303s15.752s

十万条数据:

由此可见,django自带的序列化方案性能很不理想,虽然在较少的数据量下,三者给用户的体验并没有多大区别,而且一次性序列化十几万数据在正常的生产模式中也几乎不可见,但这却并不能掩饰它们的性能差异,哪怕是极为成熟的第三方框架 rest_framwork 性能也并非十分的理想。


  • 基于外键的性能测试

上面的测试,只是在一种比较理想的模型下进行的,实际的开发情况下,还会有很多其他影响性能的因素,这里我们先试试外键对序列化性能的影响。

目标:name_id 改为名为 name 的外键,使用另外一个表(PersonInfo)的用户名字,替代序列化中的 name_id。

首先修改model:

from django.db import models


class PersonInfo(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20)
    age = models.IntegerField()
    hireDate = models.DateField()
    
    # 这里需要改一下
    def __str__(self):
        return self.name

    class meta:
        verbose_name = '人员信息'


class Vocation(models.Model):
    id = models.AutoField(primary_key=True)
    job = models.CharField(max_length=20)
    title = models.CharField(max_length=20)
    payment = models.IntegerField()

    # 原来的 name_id 改成外键 
    name = models.ForeignKey(PersonInfo, on_delete=models.Case, db_column='name')

    def __str__(self):
        return str(self.id)

    class meta:
        verbose_name = '职业信息'

这里需要注意的有两点:

Vocation 中的外键中必须要自己重新命名列名,也就是: db_column='name' ,不然修改后的数据库列名会变成 name_id,这会影响返回的结果。

其次是需要重写 PersonInfo 的 __str__() 方法,让直接调用 PersonInfo 的实例化对象时候,返回的是用户名字,这会影响到 rest_framwork 序列化时候的内容。


修改:SQL + JsonResponse:  

这里只需要修改一下SQL,使用 JOIN 从另一表中获取到用户的名字

class TestView(View):
    sql = "SELECT a.id, a.job, a.title, a.payment, b.name FROM index_vocation AS a LEFT JOIN index_Personinfo " 
          "AS b ON a.name_id = b.id"
    column_list = ['id', 'job', 'title', 'payment', 'name']

    def get(self, request):
        raws, sql_code = django_sql(self.sql, dict_keys=self.column_list, types='list_dict')
        return JsonResponse(data={'data': raws}, status=sql_code)

结果如下:


修改:Django serializers

class TestViewDjango(View):

    def get(self, request):
        results = Vocation.objects.all()
        for result in results:
            result.name_id = result.name.name
        data = serializers.serialize('json', results, ensure_ascii=False)
        return HttpResponse(content=data, status=200)

这里有一个大大大坑,需要着重记录一下。

这里要解释下 Vocation 实例对象,也就是上文中的 result 的 name_id 属性是从哪里来的。因为 model 中根本没有定义这个属性。实际上还是 Django ORM 会给外键列名加上 _id 后缀导致的,这里我们重新命名外键的列名为 name ,不然数据库中这一列也会以 name_id 命名。

何以见得?直接打印下 QureySet 内部的值。

 results = Vocation.objects.all().valeus()
 print(results)

但 name_id 不会影响到 Django serializers 序列化的内容,如下图。

经过测试,我发现这里只会以你定义外键时候的名字做为序列化基础。

也就是 name = models.ForeignKey(PersonInfo, on_delete=models.Case, db_column='name') 这行伪代码中最左边这个 name ,而右边这个  db_column='name' 我认为是无影响的,因在不设置它时候,数据库里的列名是 name_id,但序列化后依旧是 name,有兴趣的同学可以自己试试。

最后得到结果:


修改:rest_framwork 的序列化类

from rest_framework import serializers
from .models import PersonInfo, Vocation

class MySerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    job = serializers.CharField(max_length=10)
    title = serializers.CharField(max_length=10)
    payment = serializers.IntegerField()
    
    # 把原来的 name_id 改成了 name
    name = serializers.CharField(max_length=10)

结果如下:


记录在外键下的序列化耗时:

数据量:100数据量:1000数据量:10000数据量:100000JsonResponse  0.0091s0.0434s0.2958s3.1124sDjango serializers0.5234s6.77842s23.740s240.11srest_framework0.3249s5.99866s27.343s217.31s

首先,为什么使用了JOIN 的 SQL 的耗时会比之前的少,那是因为之前的SQL使用了 SELECT *,而后面的SQL明确给出了列名。

后续的测试数据,已经无需多说了,十万条十的时候,我等的快睡着了,相比不带外键,耗时接近提升了十倍。


  • 为什么会出现这种情况呢?

分析一下函数的计时情况。

可见里面最耗时的地方是将外键的值,重新赋值到 result.name_id 所制。而在reset_framwork中则是在 Response 阶段导致了大量的耗时。

我们首先调研下第二套方案的性能问题,我们尝试修改函数:

class TestViewDjango(View):

    def get(self, request):
        results = Vocation.objects.all().values()
        return JsonResponse(data={'data': list(results)}, status=200)

在不用外键的情况下,直接提取values 然后通过 JsonResponse 序列化,10W数据居然只要2.2s!和之前的24s相比,简直就是起飞,可见 JsonResponse 的性能,可以说是碾压 Djano serializers。

那我想用过这种方法加入外键的值,我要怎么做呢,继续修改函数: 

class TestViewDjango(View):

    def get(self, request):
        results = Vocation.objects.all().values()
        new_results_list = []
        for result in list(results):
            name_id = result.get('name_id')
            person_info_object = PersonInfo.objects.get(id=name_id)
            del (result['name_id'])
            result['name'] = person_info_object.name
            new_results_list.append(result)
        return JsonResponse(data={'data': new_results_list}, status=200)

耗时如下:

所以你发现了啥?238s秒和之前的240s,为何会如此的接近?

实际上问题也在于这里,那就是 ORM 的惰性查询,它并不会一次把外键中所有的 name 给你查出来,而是每一次你要获取 外键中的 name 的时候,都需要进行一次查询,10w条数据,等于查询了10W次,而我上面的代码等于还原了这个过程,所以能快起来,那就真的是有鬼了。

至于 rest_framwork 使用外键的时候,为什么会这么慢,也同样是因为这个原因,但框架似乎做过优化,比我们直接粗暴的查询,要快了那么一点点。

但真实的开发环境中,是不会处理这么大量的数据的,开发者会用到分页,先说到这,这文章已经足够长了,再写下去怕是没人看了。


总结: 

        1、单从序列化来看,JsonRespense 的性能无疑是最佳的。

        2、序列化的过程中,能不用外键就绝对不用外键,如果你用SQL,请注意防注入。

        3、对于序列化数据较少的情况下,编写规范的 REST FULL API时,我还是推荐使用

        rest_framewrok,它还有很多其他的强大功能,能大大减少你的工作量。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/zaji/5679750.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存