说说序列化那些事

什么是序列化?

狭义上的序列化,是指将对象数据转换为可以存储或传输的形式的过程,这里引用维基的定义。

In computer science, in the context of data storage, serialization is the process of translating data structures or object state into a format that can be stored (for example, in a file or memory buffer) or transmitted (for example, across a network connection link) and reconstructed later (possibly in a different computer environment).

我这里要讲的,却是不只是关于数据的转换,而是指在典型的web应用中,数据从发送请求,到后端解析,再入库,最后返回的整个过程。

这里以具体的一个论坛为例:

一个典型的论坛应用,可能有threads帖子这个模型。 比如新建一个帖子时,前端会向后端发送一个请求,略过http协议的定义与处理,这里假定后端已经可以拿到数据,是这样的:{'title': '测试标题', 'content': '这是内容'}

后端如何处理这些数据,通常来讲,会根据不同的技术栈,有不同的实现:比如django的表单(form)的实例化对象,比如drf(django rest framework)的serializer对象取。

取完了数据,通常还需要对数据进行校验,比如title不能超过20个汉字。校验过了没有问题,就可以入库了。 入库完成后,还需要给前端的请求返回数据,以便于前端渲染数据。比如通常这里会带这些信息:{'id': 1, 'title': '测试标题', 'content': '', 'create_time': '2017-06-09 11:00:01', 'owner_id': 1}

到此为止,一整个流程算是完毕了,我们来梳理一下,这个流程应该是这样的:

网络请求(request)-->解析(parse)-->校验与转换(validate and format)-->入库/出库(storage)-->序列化(serialize)-->响应(response)

这篇文章要讲的内容,是抛开了除网络请求解析响应这三个过程,重点在于中间部分的校验与转换-->入库/出库-->序列化过程。

校验与转换

如以下是一个创建用户的请求后端处理伪代码:

def create_user(request):

    def validate_email(email_str):
        if "@" in email_str and "." in email_str:
            return email
        else:
            raise 

    try:
        user_id = int(request.post['id'])
    except ValueError:
        raise 
    username = request.post['name']
    email = validate(request.post['email'])
    # doing something to create

可以看到,这里有三个不同的字段(field)需要处理。 每个field的校验与格式转换都是相互独立的,如果校验失败,则应该抛出异常,返回给前端。 针对不同的请求,来写不同的校验规则,好处是灵活性高,坏处就是代码不优雅,重复性高。 这样的类似的代码会出现很多次,还需要手动捕获异常。

如何将这段逻辑写的更优雅?

在这个基础上,弄一个简单的字段似乎就能解决问题:

cfg_dict = {
    'name': ('username', str, None),
    'id': ('id', int, None),
    'email': ('email', str, validate_email)
}

像这样,配一个简单的映射:

  • name,需要转换成username,类型是str,不需要校验
  • id,类型是int,不需要校验
  • email,类型是str,需要校验是否包含有@与.

这样可以把需要处理的地方,都通过读取这个配置来完成数据的转换与格式化。以下是通用的逻辑处理伪代码:

for attr, cfg in cfg_dict.items():
    temp_v = request.post.get(cfg[0])
    try:
        temp_v = cfg[1](temp_v) # 类型转换
        if cfg[2]:
            temp_v = cfg[2](temp_v) # 如需要校验则校验
    except ValueError:
        raise

    locals()[attr] = tempv

这样虽然能够解决大部分问题了,但是还是有一些问题没有解决的。

字典的重复定义

比如用户的创建的字典,就是上面的字典里再增加一个键值对,来处理password。

对于请求只带有部分字段的处理

比如用户的修改资料,可以修改邮箱,昵称,但是提交过来的可能只有昵称字段。

批量校验

比如需要批量创建用户。

对象校验

比如商品有两个价格,一个是优惠价格,一个是标价,这里优惠价格应该低于标价。

当然呢,我认为,好的解决方案不一定要解决所有的问题,只要足够简单和灵活,能够解决大部分的问题就行。

有哪些现成的解决方案?

github上有不少的开源组件,有的只做了校验,有的包括上面的基本流程。由于本文立意是为了讲清楚整个过程,因此不在这里一一展开了,只讲以下三个。

  1. django的form
  2. drf的serializer
  3. marshmallow

除此之外,还有一些像google的protobuf,apache的thrift协议,这种数据结构的协议,更多侧重于数据的校验与转换,对于数据的中间流动并没有直接或者间接参与。

django的表单(form)组件

由于django的MTV架构,后端与前端相对有较强的耦合,form甚至可以在后端配置前端的显示样式。 但是除开这个,表单的使用还是很方便的,特别是模型表单(modelform),能够直接关联到模型(model),使用model的定义来作转换与校验。 并且也提供了很好的扩展方法。

关键是与orm的结合非常好,数据经过校验就可以直接入库了。对于传统的表单提交,这个组件确实非常方便无痛。 但是由于前后端分离(如移动app)的开发模式流行,这种传统交互的方式使用的越来越少了。

drf的serializer

drf是基于django用于专门开发restful风格的api的一套组件。其中就包括了serializer组件。

serializer负责的东西还是挺多的,像校验与转换不在话下,对于入库/出库的操作,也可以直接使用django的orm(object relation mapping),但是它还负责响应的数据的序列化,即上面过程中提到的序列化的过程。

serializer也可以直接关联到对应的模型,跟上面的模型表单(modelform)类似。

并且对于批量的转换和局部的校验,也是很方便的。基本上能够满足上面所有需求了。

这么说起来,似乎话题到这里就可以完结了?

当然不,本文想探讨的是整个流程中遇到的各种问题和解决方案,这里drf确实能够满足大部分需求,但是,仍然会有一部分需求待提出和解决。

比如,如果不是使用django呢?如果django结合mongodb呢(mongodb尚没有django的官方orm支持)。

marshmallow

查看链接

这个组件我没有实际使用,但是听到了好几次。

它脱离了orm的模型定义,自己弄了一个模式(schema)。模式的实例也能够校验与序列化对象/数据。

入库/出库(storage)

入库与出库,简单来讲,确实好像就是读写数据库,但是实际上还包括一个模式的定义。

一般来讲,一个orm应该包括好的模式的定义与设计,也包括database的管理(包括索引创建、表创建删除等)。

为什么要有ORM呢?

orm并非是凭空出来的,在没有orm的年代,一般会怎么操作呢?

  1. 建立连接
  2. 编写sql
  3. 取出数据
  4. 数据转换

这样每个操作,都要直接建立连接,并且要拼装sql,最后再转换。

这种写法当然也没有太大的问题,足够灵活,但是有大量重复的代码。

基于DRY原则,这里自然是想减少这部分代码的重复,orm的功能也就呼之欲出了。

以django的orm为例,简单讲一下它的基本功能:

  1. 将定义好的模型映射到数据库
  2. 数据库连接管理
  3. 数据库操作解析映射(DML)
  4. 数据库管理(DDL)
  5. 数据的转换

这里主要讲1,3,5。

定义好的模型映射到数据库,即通过类的定义,转换成ddl,比如

class User(models.Model):
    username= models.CharField(verbose_name="username", max_length=30)

按这样定义,会被转换成对应的建表语句create table user ...

然后对于sql的操作,也直接由对象的操作完成,如User.objects.create(username="abc")会被转换成insert into table user(username) values('abc')

再就是对于数据的转换,比如要取出数据User.objects.filter(username="abc"),返回的可不是冷冰冰的[(1, 'abc')],而是经由它转换成了一个User实例,它的id为1,username为abc。

but how?

ORM是如何做到这一点的呢?

这里有一个概念,叫做『模板编程』,即用户的模型,跟用户组的模型,对于数据库的操作,其实是一样的,所不同的,就是在于各自的模型定义,表名称上。

在python里,有一个概念,叫做metaclass,即元类,直观理解的话,就是生成类的类。

关于它的使用实践,可以参考我的另一篇文章

通常来讲,它有两个概念模型(Model)字段(Field)

模型下面会定义许多的字段,不同的字段会映射不同的类型,并且一般会带有校验方法链。以下一段伪代码。

class Field(object):

    def __init__(self, name, validators=None):
        self.name = name
        self.validators = validators

class IntField(Field):
    pass

class StrField(Field):
    pass


class Model(object):
    pass

class User(Model):
    id = IntField(name="user_id")
    name = StrField(name="name")

像这样,可以在Model里弄很多通用的方案,比如建数据库连接,操作映射,取到数据后映射等。

有哪些orm呢?

  1. django orm
  2. sqlalchemy
  3. mongoengine

其中django的orm与sqlalchemy类似,均是给关系型数据库使用的,其中sqlalchemy可以结合其它的框架使用,如webpy, flask。 mongoengine可以结合mongodb与django使用。

序列化

序列化,似乎没有那么值得说的了,直接json dumps一下不就好了么?

理论上是这样的,但是……

校验与转换一样,会需要字段的映射,会需要字段的转换,比如价格在数据库中存储时,是以分的形式保存的,前端显示需要转换成为¥168.00 元这样的显示。 还有的是数据库中的状态码显示,比如用户的性别,可能是以int的形式存储的,在页面上显示的时候,要转换成男/女或者male/female这样的显示。

还有就是,不同的请求,返回的数据字段也有不同,比如一般创建数据,比如帖子,可能会返回全部的字段,但是创建用户的时候,密码这个字段不能返回吧。

说到这里,应该也有人想到,跟上面的校验与转换一样,弄一个modelserializer,映射哪些字段,并且定义不同字段的转换方法。

有哪些库支持?

  1. drf serializer
  2. marshmallow

drf serializer

serializer其实本身包括valiate与serialize的功能的,并且做了强耦合,当然它可以定义字段的read_only或者write_only,在创建或者返回的时候字段不同。

marshmallow

marshmallow由于没有model的概念,它只有schema的概念,即可以定义不同的schema,来做序列化和反序列化。

但是也会有面临相同的困扰,即用户创建和用户修改时,两个schema非常类似,但也必须定义两份。

上面这两种,其实都是校验与转换序列化强耦合的,相对来说,不够灵活,并且可能会带来一定的序列化的性能消耗。

小结?

在实践过程中,笔者尝试过django, drf的这一套体系,确实开发起来非常爽:

class UserSerializer(serializers.ModelSerializer):
    """用于创建用户"""

    id = serializers.CharField(read_only=True)
    password = serializers.CharField(write_only=True)
    is_active = serializers.BooleanField(read_only=True)

    class Meta:
        model = User
        fields = ('id', 'username', 'password', 'email', 'phone', 'is_active')


class UserViewSet(viewsets.ModelViewSet):
    """用户的操作:列表、详情、修改、删除、创建"""
    queryset = User.objects.all()
    serializer_class = UserSerializer

像这样定义的,就能够处理好列表、详情、修改、删除、创建这五个功能了。

这样的功能足够满足大部分需求了,但是仍然有一些复杂的需求,这里列举一下:

嵌套的结构处理

数据不是一层不变的平铺数据,很有可能是有嵌套的数据,需要校验的逻辑能够递归校验。

校验的一种特殊操作

创建的时候,根据参数的不同,会有不同的额外参数,这里类似于有几个固定的子类,然后子类有不同字段属性。

上面的机制不能很好的处理,当时选择的是手动处理的这些数据,最后再手动处理的。

返回数据的关联处理

这一点,比如虽然创建了用户,但是希望返回的时候,带上用户所属的组。

除此之外,笔者遇到的一个更大的问题,就是:在django的体系里,这一套用起来很爽,可是一旦切换到其它不同的组件后,就没法移植了。 比如使用flask时,django与drf的这一套均无法使用了,但是业务中还是需要这些东西的。 也有例如使用mongodb时,原来的orm这一套可能不能使用了,不好与原来的序列化结合

真·小结

似乎在这里,应该造一个轮子了,将校验与转换序列化这两个从功能上分开,毕竟需要校验的数据,与需要返回的数据并不完全相同。

而且面对不同的数据库/存储,也有不同的orm实现,模式应该是能够适配不同的orm的,即适配器模式,根据不同的orm,适配不同的模式出来。

不过,这个坑,也不知道哪一天会填……