什么是序列化
对于Django而言,模型是一个重要的部分,我们从中获取或更改数据,都是用Django的模型对象。而前端并不能直接读取这个对象,于是需要把对象映射为JSON数据进行传递。
把模型映射到JSON数据,这一过程就称之为序列化。当然,我们只需要把模型对象转为Dict,剩下的交给json
标准库就可以了。
如何序列化
在进行序列化之前,我们首先需要明白,Model的字段大致可分为三种,一种是直接存储的字段(Field),一种是关系字段(Relation Field),一种是Django帮助我们反向映射过来的关系(Relation)。
第一种很容易获取,直接从Model对象的属性中读取即可,我们只需要注意,把时间字段,文件字段等无法被json库直接序列化的字段转为字符串即可。
第二种与第三种,看起来不同,一个是我们自己定义,一个是Django自动生成。但实际上,使用过Django的ORM的人都知道,它们操作起来完全是一样的。那么,序列化的时候我们可以把这两种归为一类——关系字段。
对于关系字段而言,并不能直接序列化,因为它们也都是对象。那么需要一种递归的方法去获取它们的数据。
但不能无限递归下去
class ModelSerializationMixin:
def to_dict(self):
pass
当我们定义Model模型的时候,直接同时继承这个类,即可让Model拥有对应方法。
获取字段
Django的Model提供了一个很棒的属性_meta
,里面有get_fields()
,get_field()
两个方法。可从前者获取模型全部字段,可以使用字段名(字符串)从后者那里拿到对应的的字段。
class ModelSerializationMixin:
def to_dict(self):
def get_all_fields():
for field in self._meta.get_fields():
print(field)
此时可以看到该对象的全部的字段。这看起来是很棒的第一步,但先等等,如果我只想获取指定的几个字段,或者不想获取指定的某几个字段。结合get_field()
,稍微修改一下。
def to_dict(self, fields=None, exclude=None):
exclude = exclude if exclude else []
def get_all_fields():
if fields is None:
for field in self._meta.get_fields():
if field.name in exclude:
continue
yield field
else:
for field_name in fields:
if field_name in exclude:
continue
yield self._meta.get_field(field_name)
这看起来就很完美了,当不指定fields
时,可以获取全部的字段,当给出了字段名的一个列表或集合之后,只返回指定的字段。
处理普通字段
对于普通的字段,例如CharField
等,我们直接获取即可。不过对于DateTimeField等字段,我们需要进行适当转换才行。
def to_dict(self, fields=None, exclude=None, raw_data=False):
exclude = exclude if exclude else []
def get_all_fields():
if fields is None:
for field in self._meta.get_fields():
if field.name in exclude:
continue
yield field
else:
for field_name in fields:
if field_name in exclude:
continue
yield self._meta.get_field(field_name)
for field in get_all_fields():
result[field.name] = getattr(model, field.name)
if not raw_data:
# 将不能直接用json解析的对象转为字符串
if isinstance(result[field.name], datetime.datetime):
result[field.name] = result[field.name].strftime("%Y-%m-%d %H:%M:%S")
elif isinstance(result[field.name], datetime.date):
result[field.name] = result[field.name].strftime("%Y-%m-%d")
elif isinstance(result[field.name], (File,)):
result[field.name] = settings.MEDIA_URL + str(result[field.name])
return result
可以看到,我们使用了field
对象的name
属性,它可以获取你在定义Model时定义的字段名。在这里我们使用它去获取Model对象的属性。
处理关系字段
在Django里,关系字段有四种many_to_many, many_to_one, one_to_many, one_to_one
。我们可以从django.db.models.fields.__init__.Field
这个类里看到,每个Field都有这四个属性。这里我们不得不使用四个条件判断语句,但为了整体代码清晰,每种不同的关系字段都由一个单独的函数来处理。
- 多对多的关系字段,或者反向映射的多对多关系,
many_to_many
为True。
def m2m(field):
if isinstance(field, models.ManyToManyRel):
if field.related_name is None:
field_name = field.name + '_set'
else:
field_name = field.related_name
else:
field_name = field.name
# 自定义规则筛选
query_set = self.get_qs(field_name, getattr(self, field_name))
if relation_data:
result[field.name] = [self._to_dict(each) for each in query_set]
else:
result[field.name] = [each.id for each in query_set]
- 一对一的关系字段,或者反响映射的一对一关系,
one_to_one
为True。
def o2o(field):
try:
if relation_data:
result[field.name] = self._to_dict(getattr(self, field.name))
else:
result[field.name] = getattr(self, field.name + '_id')
except ObjectDoesNotExist:
result[field.name] = None
- 一个指向另一个model的外键关系,
one_to_many
为True。
def o2m(field):
if field.related_name is None:
field_name = field.name + '_set'
else:
field_name = field.related_name
# 自定义规则筛选
query_set = self.get_qs(field_name, getattr(self, field_name))
if relation_data:
result[field.name] = [self._to_dict(each) for each in query_set]
else:
result[field.name] = [each.id for each in query_set]
- 其他model指向本Model的外键关系,
many_to_one
为True。
def m2o(field):
if relation_data:
result[field.name] = self._to_dict(getattr(self, field.name))
else:
result[field.name] = getattr(self, field.name + '_id')
把一切组合起来再稍加修改就变成如下代码:
import datetime
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings
from django.db import models
from django.core.files import File
class ModelSerializationMixin:
def get_qs(self, field_name: str, field):
return field.all()
def get_exclude(self, exclude: [str]) -> []:
return exclude if exclude else []
@staticmethod
def _to_dict(
self: models.Model,
fields: [str] = None,
exclude: [str] = None,
relation: bool = False,
relation_data: bool = None,
raw_data: bool = False
):
"""
将Model对象序列化
* int, str, float, bool 等对象正常解析
* DateTimeField 解析为 get_datetime_format 的格式,
* DateField 解析为 get_date_format 格式
* FileField, ImageField 等解析为路径字符串
:param self: 需要序列化的对象
:param fields: str[] 需要序列化的字段名, 不给值时默认序列化所有字段
:param exclude: str[] 不需要序列化的字段名
:param relation: Boolean 为真时序列化关系字段
:param relation_data: Boolean 为假时仅序列化关系字段的 id, 默认值等于 relation
:param raw_data: Boolean 为真时将不再将 datetime 等特殊对象转为字符串,而提供原始对象
:return: dict
"""
if self is None:
return None
result = dict() # 返回结果
exclude = getattr(self, "get_exclude", lambda x: [] if x is None else x)(exclude)
relation_data = relation_data if relation_data is not None else relation
def get_all_fields():
if fields is None:
for field in self._meta.get_fields():
if field.name in exclude:
continue
yield field
else:
for field_name in fields:
if field_name in exclude:
continue
yield self._meta.get_field(field_name)
def if_relation(func):
# 仅在relation为真时才执行
def inline(field):
if not relation:
return None
return func(field)
return inline
@if_relation
def m2m(field):
if isinstance(field, models.ManyToManyRel):
if field.related_name is None:
field_name = field.name + '_set'
else:
field_name = field.related_name
else:
field_name = field.name
# 自定义规则筛选
query_set = self.get_qs(field_name, getattr(self, field_name))
if relation_data:
result[field.name] = [self._to_dict(each) for each in query_set]
else:
result[field.name] = [each.id for each in query_set]
@if_relation
def o2m(field):
if field.related_name is None:
field_name = field.name + '_set'
else:
field_name = field.related_name
# 自定义规则筛选
query_set = self.get_qs(field_name, getattr(self, field_name))
if relation_data:
result[field.name] = [self._to_dict(each) for each in query_set]
else:
result[field.name] = [each.id for each in query_set]
@if_relation
def o2o(field):
try:
if relation_data:
result[field.name] = self._to_dict(getattr(self, field.name))
else:
result[field.name] = getattr(self, field.name + '_id')
except ObjectDoesNotExist:
result[field.name] = None
@if_relation
def m2o(field):
if relation_data:
result[field.name] = self._to_dict(getattr(self, field.name))
else:
result[field.name] = getattr(self, field.name + '_id')
def normal(field):
result[field.name] = getattr(self, field.name)
if raw_data:
return
# 将不能直接用json解析的对象转为字符串
if isinstance(result[field.name], datetime.datetime):
result[field.name] = result[field.name].strftime("%Y-%m-%d %H:%M:%S")
elif isinstance(result[field.name], datetime.date):
result[field.name] = result[field.name].strftime("%Y-%m-%d")
elif isinstance(result[field.name], (File,)):
result[field.name] = settings.MEDIA_URL + str(result[field.name])
for field in get_all_fields():
if field.many_to_many:
m2m(field)
elif field.one_to_many:
o2m(field)
elif field.one_to_one:
o2o(field)
elif field.many_to_one:
m2o(field)
else:
normal(field)
return result
def to_dict(self, *, fields=None, exclude=None, relation=False, relation_data=None, raw_data=False):
return self._to_dict(
self,
fields=fields,
exclude=exclude,
relation=relation,
relation_data=relation_data,
raw_data=raw_data
)
当我们需要使用的时候,只需要像下面这样定义Model即可
class Article(models.Model, ModelSerializationMixin):
title = models.CharField(max_length=20)