序列化 Django 对象

Django 的序列化框架提供了一种将 Django 模型“翻译”为其他格式的机制。通常,这些其他格式将基于文本,并用于在网络上发送 Django 数据,但是序列化程序可以处理任何格式(无论是否基于文本)。

另见

如果你只是想将表中的某些数据转换为序列化形式,你可以使用 dumpdata 管理命令。

序列化数据

从最基本的层面,可以这样序列化数据:

from django.core import serializers

data = serializers.serialize("json", SomeModel.objects.all())

serialize 函数的参数是数据序列化的目标格式 (查看 序列化格式)和用来序列化的 django.db.models.query.QuerySet。(实际上,第二个参数可以是任何生成 Django 模型实例的迭代器,但它几乎总是一个QuerySet)。

django.core.serializers.get_serializer(format)

也可以直接使用序列化器对象:

JSONSerializer = serializers.get_serializer("json")
json_serializer = JSONSerializer()
json_serializer.serialize(queryset)
data = json_serializer.getvalue()

如果要直接序列化到类文件对象,包括 django.http.HttpResponse,这种方式很有用:

with open("file.json", "w") as out:
    json_serializer.serialize(SomeModel.objects.all(), stream=out)

注意

以未知 格式 调用 django.core.serializers.get_serializer 将引发 django.core.serializers.SerializerDoesNotExist 异常。

字段子集

如果只想序列化部分字段,可以向序列化器传入 fields 参数:

from django.core import serializers

data = serializers.serialize("json", SomeModel.objects.all(), fields=["name", "size"])

在此示例中,将仅序列化每个模型的 name 和 size 属性。主键总是序列化为结果输出中的 pk 元素;它永远不会出现在 fields 部分。

注意

根据你的模型,你可能会发现无法反序列化一个仅序列化了其字段子集的模型。如果已序列化的对象未指定模型所需的所有字段,则反序列化器将无法保存反序列化的实例。

继承来的模型

如果你有一个使用 抽象基类 定义的模型,那么你不必做任何特殊的事情来序列化该模型。对要序列化的一个(或多个)对象调用序列化程序,输出将是序列化对象的完整表示形式。

但是,如果模型使用多表继承,还必须序列化该模型的所有基类。因为序列化时只会包含本模型直接定义的字段。例如,考虑以下模型:

class Place(models.Model):
    name = models.CharField(max_length=50)


class Restaurant(Place):
    serves_hot_dogs = models.BooleanField(default=False)

如果只序列化 Restaurant 模型:

data = serializers.serialize("json", Restaurant.objects.all())

序列化输出上的字段将仅包含 serves_hot_dogs 属性。基类的 name 属性将被忽略。

要完整序列化 Restaurant 实例,还需要序列化 Place 模型:

all_objects = [*Restaurant.objects.all(), *Place.objects.all()]
data = serializers.serialize("json", all_objects)

反序列化数据

反序列化数据与序列化数据非常相似:

for obj in serializers.deserialize("json", data):
    do_something_with(obj)

如你所见,deserialize 函数与 serialize 函数采用相同的格式参数,字符串或数据流,并返回一个迭代器。

不过,这里有点复杂。deserialize 迭代器返回的对象 不是 常规的 Django 对象。相反,它们是特殊的 DeserializedObject 实例,实例封装了一个已创建 — 但未保存 — 的对象和任何相关联的数据。

调用 DeserializedObject.save() 保存对象到数据库。

注意

如果序列化数据中的 pk 属性不存在或为 null,则会将新实例保存到数据库中。

这样,即使序列化表示中的数据与数据库当前的数据不一致,反序列化操作也不会破坏现有数据。通常,使用这些 DeserializedObject 实例的方式如下:

for deserialized_object in serializers.deserialize("json", data):
    if object_should_be_saved(deserialized_object):
        deserialized_object.save()

换句话说,通常的用途是检查反序列化的对象,以确保它们“适合”保存。如果你信任数据源,则可以直接保存对象并继续前进。

可以通过 deserialized_object.object 检查 Django 对象本身。如果序列化数据中的字段在模型上不存在,会引发 DeserializationError,除非将 ignorenonexistent 参数设为 True:

serializers.deserialize("json", data, ignorenonexistent=True)

序列化格式

Django 支持多种序列化格式,其中一些格式要求你安装第三方 Python 模块:

标识符 说明
xml 序列化和反序列化为一种简单的 XML 方言。
json 序列化和反序列化为 JSON。
jsonl 序列化和反序列化为 JSONL。
yaml 序列化为 YAML(YAML Ain’t Markup Language)。需安装 PyYAML。

XML

基本的 XML 序列化格式如下:

<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">
    <object pk="123" model="sessions.session">
        <field type="DateTimeField" name="expire_date">2013-01-16T08:16:59.844560+00:00</field>
        <!-- ... -->
    </object>
</django-objects>

序列化或反序列化的整个对象集合由一个包含多个 <object> – 元素的 <django-objects> – 标签标识。每个这样的对象都有两个属性:“pk”和“model”,后者由用点号分隔的 app 名称(“sessions”)和模型的小写名称(“session”)来代替。

对象的每个字段都序列化为一个 <field> 元素,带有 type 和 name 属性。元素中的文本内容表示应存储的值。如果元素包含子标签,会引发 django.core.exceptions.SuspiciousOperation。

外键和其他关系字段的处理略有不同:

<object pk="27" model="auth.permission">
    <!-- ... -->
    <field to="contenttypes.contenttype" name="content_type" rel="ManyToOneRel">9</field>
    <!-- ... -->
</object>

在本例中,我们指定具有 PK 27 的 auth.Permission 对象有一个指向 PK 9 的 contenttypes.ContentType 实例的外键。

ManyToMany 关系是针对绑定它们的模型导出的。例如,auth.User 模型与 auth.Permission 模型有这样的关系:

<object pk="1" model="auth.user">
    <!-- ... -->
    <field to="auth.permission" name="user_permissions" rel="ManyToManyRel">
        <object pk="46"></object>
        <object pk="47"></object>
    </field>
</object>

此示例将给定用户与 PK 46 和 47 的权限模型链接起来。

控制字符

如果要序列化的内容包含 XML 1.0 标准不接受的控制字符,则序列化将失败,并出现 ValueError 异常。另请阅读 W3C 对 HTML, XHTML, XML and Control Codes 的解释。

版本变化:6.1

如果发现意料之外的嵌套标签,会引发 django.core.exceptions.SuspiciousOperation。

JSON

沿用之前的示例数据,序列化为 JSON 后如下:

[
    {
        "pk": "4b678b301dfd8a4e0dad910de3ae245b",
        "model": "sessions.session",
        "fields": {
            "expire_date": "2013-01-16T08:16:59.844Z",
            # ...
        },
    }
]

这里的格式比 XML 简单一些。整个集合只是表示为一个数组,对象由具有三个属性的 JSON 对象表示:“pk”,“model”和“fields”。“fields”又是一个对象,其中分别包含每个字段的名称和值作为属性和属性值。

外键将链接对象的 PK 作为属性值。多对多关系对于定义它们的模型进行了序列化,并表示为 PK 列表。

注意,并非 Django 的所有输出都能原样传给 json。例如,如果待序列化对象包含自定义类型,需要为其编写自定义 JSON 编码器。如下实现可以工作:

from django.core.serializers.json import DjangoJSONEncoder


class LazyEncoder(DjangoJSONEncoder):
    def default(self, obj):
        if isinstance(obj, YourCustomType):
            return str(obj)
        return super().default(obj)

随后,可以向 serializers.serialize() 传入 cls=LazyEncoder:

from django.core.serializers import serialize

serialize("json", SomeModel.objects.all(), cls=LazyEncoder)

还要注意 GeoDjango 提供了一个 定制的 GeoJSON 序列化器.

DjangoJSONEncoder

django.core.serializers.json.DjangoJSONEncoder

JSON 序列化器使用 DjangoJSONEncoder 进行编码。作为 json.JSONEncoder 的子类,它可以处理这些附加类型:

datetime.datetime 格式为 YYYY-MM-DDTHH:mm:ss.sssZ 或 YYYY-MM-DDTHH:mm:ss.sss+HH:MM 的字符串,如 ECMA-262 中定义。

datetime.date 格式为 YYYY-MM-DD 的字符串,如 ECMA-262 中定义。

datetime.time 格式为 HH:MM:ss.sss 的字符串,如 ECMA-262 中定义。

datetime.timedelta 代表 ISO-8601 中定义的持续时间的字符串。例如,timedelta(days=1, hours=2, seconds=3.4) 代表 'P1DT02H00M03.400000S'。

decimal.Decimal,Promise ( django.utils.functional.lazy() 对象),uuid.UUID 对象的字符串表示形式。

JSONL

JSONL 即 JSON Lines。在这种格式中,对象之间以换行分隔,每一行包含一个有效的 JSON 对象。JSONL 序列化数据如下:

{"pk": "4b678b301dfd8a4e0dad910de3ae245b", "model": "sessions.session", "fields": {...}}
{"pk": "88bea72c02274f3c9bf1cb2bb8cee4fc", "model": "sessions.session", "fields": {...}}
{"pk": "9cf0e26691b64147a67e2a9f06ad7a53", "model": "sessions.session", "fields": {...}}

JSONL 可以用于填充大型数据库,因为数据可以逐行处理,而不必一次性加载到内存中。

YAML

YAML 序列化看起来与 JSON 相似。对象列表被序列化为一个序列映射,其中包括 “pk”、”model” 和 “fields” 键。每个字段都是一个映射,键是字段的名称,值是字段的值:

- model: sessions.session
  pk: 4b678b301dfd8a4e0dad910de3ae245b
  fields:
    expire_date: 2013-01-16 08:16:59.844560+00:00

引用字段再次由 PK 或 PK 序列表示。

自定义序列化格式

除默认格式外,还可以创建自定义序列化格式。

例如,考虑一个 CSV 序列化器和反序列化器。首先定义 Serializer 和 Deserializer 类,可以在这些类中覆盖已有序列化格式类的行为:

import csv

from django.apps import apps
from django.core import serializers
from django.core.serializers.base import DeserializationError


class Serializer(serializers.python.Serializer):
    def get_dump_object(self, obj):
        dumped_object = super().get_dump_object(obj)
        row = [dumped_object["model"], str(dumped_object["pk"])]
        row += [str(value) for value in dumped_object["fields"].values()]
        return ",".join(row), dumped_object["model"]

    def end_object(self, obj):
        dumped_object_str, model = self.get_dump_object(obj)
        if self.first:
            fields = [field.name for field in apps.get_model(model)._meta.fields]
            header = ",".join(fields)
            self.stream.write(f"model,{header}\n")
        self.stream.write(f"{dumped_object_str}\n")

    def getvalue(self):
        return super(serializers.python.Serializer, self).getvalue()


class Deserializer(serializers.python.Deserializer):
    def __init__(self, stream_or_string, **options):
        if isinstance(stream_or_string, bytes):
            stream_or_string = stream_or_string.decode()
        if isinstance(stream_or_string, str):
            stream_or_string = stream_or_string.splitlines()
        try:
            objects = csv.DictReader(stream_or_string)
        except Exception as exc:
            raise DeserializationError() from exc
        super().__init__(objects, **options)

    def _handle_object(self, obj):
        try:
            model_fields = apps.get_model(obj["model"])._meta.fields
            obj["fields"] = {
                field.name: obj[field.name]
                for field in model_fields
                if field.name in obj
            }
            yield from super()._handle_object(obj)
        except (GeneratorExit, DeserializationError):
            raise
        except Exception as exc:
            raise DeserializationError(f"Error deserializing object: {exc}") from exc

然后,把包含序列化器定义的模块加入 SERIALIZATION_MODULES 设置:

SERIALIZATION_MODULES = {
    "csv": "path.to.custom_csv_serializer",
    "json": "django.core.serializers.json",
}

自然键

外键和多对多关系的默认序列化策略是序列化在关系中对象主键的值。这种策略对大多数对象都有效,但在某些情况下可能会造成困难。

考虑一个对象列表,这些对象的外键引用 django.contrib.contenttypes.models.ContentType。如果要序列化引用内容类型的对象,那么首先需要有一种引用该内容类型的方法。由于 ContentType 对象是由 Django 在数据库同步过程中自动创建的,所以给定内容类型的主键不容易预测;这将取决于 migrate 的执行方式和时间。对于自动生成对象的所有模型都是如此,特别是包括 django.contrib.auth.models.Permission,django.contrib.auth.models.Group,和 django.contrib.auth.models.User。

警告

永远不要在数据夹具和其它序列化数据中包含自动生成的对象。偶尔,数据夹具中加载的主键可能与数据库中的相匹配而加载的数据夹具可能没有起到任何作用。更可能的情况是它们并不匹配,数据夹具将加载失败,并出现 django.db.IntegrityError 错误。

还有一个便捷性的问题。整数 id 并不总是引用对象的最方便方式;有时,更自然的引用会有所帮助。

正是由于这些原因 Django 提供了 自然键。自然键是一组值,可以用来唯一标识对象实例,而不使用主键值。

自然键反序列化

考虑下面两个模型:

from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    birthdate = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]


class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

通常,Book 的序列化数据用整数引用作者。例如,一本书的 JSON 序列化表示可能如下:

...
{"pk": 1, "model": "store.book", "fields": {"name": "Mostly Harmless", "author": 42}}
...

这不是一个特别自然的方式来指代作者。它要求你知道作者的主键值;它还要求这个主键值是稳定的和可预测的。

但是,如果为 Person 增加自然键处理,数据夹具就会更易读。为此,需要为 Person 定义一个默认 Manager,并提供 get_by_natural_key() 方法。对于 Person,名字和姓氏的组合可以作为合适的自然键:

from django.db import models


class PersonManager(models.Manager):
    def get_by_natural_key(self, first_name, last_name):
        return self.get(first_name=first_name, last_name=last_name)


class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()

    objects = PersonManager()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]

现在,书籍就能用这个自然键引用 Person 对象:

...
{
    "pk": 1,
    "model": "store.book",
    "fields": {"name": "Mostly Harmless", "author": ["Douglas", "Adams"]},
}
...

当你试图加载此序列化数据时,Django 将使用 get_by_natural_key() 方法将 ["Douglas", "Adams"] 解析为 Person 对象实际的主键。

注意

用于自然键的字段必须能够唯一标识一个对象。这通常意味着你的模型将对自然键的一个字段或一组字段(可以是单个字段上的 unique=True,也可以是多个字段上的 UniqueConstraint 或 unique_together)有一个唯一性约束。但是,唯一性并不一定要在数据库级别进行强制执行。如果你确定一组字段将有效地保持唯一性,仍然可以将这些字段用作自然键。

对没有主键的对象的反序列化将始终检查模型的管理器是否具有 get_by_natural_key() 方法,如果有,则使用它填充反序列化对象的主键。

自然键序列化

如何让 Django 在序列化对象时输出自然键?首先需要增加另一个方法,这次把它定义在模型本身上:

class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    birthdate = models.DateField()

    objects = PersonManager()

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"],
                name="unique_first_last_name",
            ),
        ]

    def natural_key(self):
        return (self.first_name, self.last_name)

该方法应该始终返回一个自然键元组 — 在这个示例中是 (名,姓)。然后,在调用 serializers.serialize() 时,你提供 use_natural_foreign_keys=True 或 use_natural_primary_keys=True 参数:

>>> serializers.serialize(
...     "json",
...     [book1, book2],
...     indent=2,
...     use_natural_foreign_keys=True,
...     use_natural_primary_keys=True,
... )

当指定 use_natural_foreign_keys=True 时,Django 将使用 natural_key() 方法将任何外键引用序列化为定义该方法的类型的对象。

指定 use_natural_primary_keys=True 时,Django 不会在该对象的序列化数据中提供主键,因为主键可以在反序列化时计算出来:

...
{
    "model": "store.person",
    "fields": {
        "first_name": "Douglas",
        "last_name": "Adams",
        "birth_date": "1952-03-11",
    },
}
...

当需要将序列化数据加载到现有数据库中,并且无法保证序列化的主键值尚未使用,并且不需要确保反序列化对象保留相同的主键时,这一点非常有用。

如果你使用 dumpdata 生成序列化数据,使用 dumpdata –natural-foreign 和 dumpdata –natural-primary 命令行标志生成自然键。

注意

你不需要同时定义 natural_key() 和 get_by_natural_key()。如果你不想要 Django 在序列化期间输出自然键,但希望保留加载自然键的能力,那你可以选择不实现 natural_key() 方法。

相反,如果(出于某些奇怪的原因)你想要 Django 在序列化时输出自然键,但是 不 加载那些键值,只需要不定义 get_by_natural_key() 方法。

子类可以通过让 natural_key() 返回空元组 (),选择不使用自然键序列化。这会通知序列化器退回到标准主键。

版本变化:6.1

新增支持:通过返回空元组选择退出自然键序列化。

自然键和前向引用

有时当你使用 自然外键 时,您需要反序列化数据,其中一个对象的外键引用了另一个尚未反序列化的对象。这称之为“前向引用”。

例如,假设数据夹具中包含以下对象:

...
{
    "model": "store.book",
    "fields": {"name": "Mostly Harmless", "author": ["Douglas", "Adams"]},
},
...
{"model": "store.person", "fields": {"first_name": "Douglas", "last_name": "Adams"}},
...

为了处理这种情况,你需要将 handle_forward_references=True 传入 serializers.deserialize()。这将在 DeserializedObject 实例上设置 deferred_fields 属性。你需要保持追踪该属性不是 None 的 DeserializedObject 实例并在之后调用它们的 save_deferred_fields()。

典型用法如下:

objs_with_deferred_fields = []

for obj in serializers.deserialize("json", data, handle_forward_references=True):
    obj.save()
    if obj.deferred_fields is not None:
        objs_with_deferred_fields.append(obj)

for obj in objs_with_deferred_fields:
    obj.save_deferred_fields()

要使其工作,引用模型上的 ForeignKey 必须具有 null=True。

序列化期间的依赖项

通过注意数据夹具中对象的顺序,通常可以避免显式地处理前向引用。

为了帮助实现这一点,在序列化标准主键对象之前,使用 dumpdata –natural-foreign 选项对 dumpdata 的调用将使用 natural_key() 方法对任何模型进行序列化。

但是,这可能并不总是足够的。如果您的自然键引用了另一个对象(通过使用外键或另一个对象的自然键作为自然键的一部分),那么你需要确保自然键所依赖的对象出现在序列化数据中在自然键要求它们之前。

要控制此顺序,你可以在 natural_key() 方法中定义依赖。为此可以在 natural_key() 方法本身上设置一个 dependencies 属性。

例如,为前述示例中的 Book 模型增加一个自然键:

class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

    def natural_key(self):
        return (self.name,) + self.author.natural_key()

Book 的自然键由书名和作者组合而成。这意味着必须先序列化 Person,再序列化 Book。要定义该依赖关系,增加一行:

def natural_key(self):
    return (self.name,) + self.author.natural_key()


natural_key.dependencies = ["example_app.person"]

这个定义确保了所有 Person 对象在任何 Book 对象之前序列化。反过来,任何对象引用了 Book 都将在 Person 和 Book 被序列化完后再序列化。

来源与许可

原文:Serializing Django objects。Django Software Foundation 及贡献者版权所有。版本范围 Django 6.1。基于 stable/6.1.x 官方源文档与官方中文 PO 全文准备,补译未译段落,调整排版。适用 BSD 3-Clause;完整许可随交接包附于 django-LICENSE.txt。代码保持原文,仅核对结构与来源,未在本任务中运行。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容