说说序列化那些事
什么是序列化?
狭义上的序列化,是指将对象数据转换为可以存储或传输的形式的过程,这里引用维基的定义。
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上有不少的开源组件,有的只做了校验,有的包括上面的基本流程。由于本文立意是为了讲清楚整个过程,因此不在这里一一展开了,只讲以下三个。
- django的form
- drf的serializer
- 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的年代,一般会怎么操作呢?
- 建立连接
- 编写sql
- 取出数据
- 数据转换
这样每个操作,都要直接建立连接,并且要拼装sql,最后再转换。
这种写法当然也没有太大的问题,足够灵活,但是有大量重复的代码。
基于DRY原则,这里自然是想减少这部分代码的重复,orm的功能也就呼之欲出了。
以django的orm为例,简单讲一下它的基本功能:
- 将定义好的模型映射到数据库
- 数据库连接管理
- 数据库操作解析映射(DML)
- 数据库管理(DDL)
- 数据的转换
这里主要讲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呢?
- django orm
- sqlalchemy
- mongoengine
其中django的orm与sqlalchemy类似,均是给关系型数据库使用的,其中sqlalchemy可以结合其它的框架使用,如webpy, flask。 mongoengine可以结合mongodb与django使用。
序列化
序列化,似乎没有那么值得说的了,直接json dumps一下不就好了么?
理论上是这样的,但是……
与校验与转换
一样,会需要字段的映射,会需要字段的转换,比如价格在数据库中存储时,是以分的形式保存的,前端显示需要转换成为¥168.00 元这样的显示。
还有的是数据库中的状态码显示,比如用户的性别,可能是以int的形式存储的,在页面上显示的时候,要转换成男/女或者male/female这样的显示。
还有就是,不同的请求,返回的数据字段也有不同,比如一般创建数据,比如帖子,可能会返回全部的字段,但是创建用户的时候,密码这个字段不能返回吧。
说到这里,应该也有人想到,跟上面的校验与转换
一样,弄一个modelserializer,映射哪些字段,并且定义不同字段的转换方法。
有哪些库支持?
- drf serializer
- 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,适配不同的模式出来。
不过,这个坑,也不知道哪一天会填……