AberSheeran
Aber Sheeran

Django ORM 模型序列化

起笔自
所属文集: Django-Simple-Api
共计 9826 个字符
落笔于

什么是序列化

对于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)
如果你觉得本文值得,不妨赏杯茶
没有上一篇
Django 解析非POST请求