序列化 Django 对象

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

参见

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

序列化数据

在最高层面,你可以像这样序列化数据:

from django.core import serializers

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

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

django.core.serializers.get_serializer(format)

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

XMLSerializer = serializers.get_serializer("xml")
xml_serializer = XMLSerializer()
xml_serializer.serialize(queryset)
data = xml_serializer.getvalue()

如果要将数据直接序列化到类似文件的对象(包括 HttpResponse)这将很有用:

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

备注

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

字段子集

如果你只希望序列化字段的子集,则可以为序列化程序指定 fields 参数:

from django.core import serializers

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

在此示例中,将仅序列化每个模型的 namesize 属性。主键总是序列化为结果输出中的 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("xml", Restaurant.objects.all())

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

为了完全序列化你的 Restaurant 实例,你还需要将 Place 模型序列化:

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

反序列化数据

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

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

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

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

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

备注

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

这可以确保反序列化是一个非破坏性操作,即使序列化表示中的数据与数据库中当前的数据不匹配。通常,使用这些 DeserializedObject 实例看起来像:

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

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

Django 对象本身可以被像 deserialized_object.object 一样检查。如果模型中不存在序列化字段,将引发 DeserializationError 错误,除非将 ignorenonexistent 参数为 True 传入:

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

序列化格式

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

标识符 信息
xml 序列化和反序列化为一种简单地 XML 方言。
json 序列化和反序列化为 JSON
jsonl 序列化和反序列化为 JSONL
yaml 序列化为 YAML(YAML 不是标记语言)。此序列化器仅在 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”)来代替。

对象的每个字段都序列化为一个具有“type”和“name”的 <field>- 元素 。元素的文本内容表示应该存储的值。

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

<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 的解释。

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)

你可以将 cls=LazyEncoder 传入 serializers.serialize() 函数:

from django.core.serializers import serialize

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

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

DjangoJSONEncoder

class django.core.serializers.json.DjangoJSONEncoder

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

datetime
格式为 YYYY-MM-DDTHH:mm:ss.sssZYYYY-MM-DDTHH:mm:ss.sss+HH:MM 的字符串,如 ECMA-262 中定义。
date
格式为 YYYY-MM-DD 的字符串,如 ECMA-262 中定义。
time
格式为 HH:MM:ss.sss 的字符串,如 ECMA-262 中定义。
timedelta
代表 ISO-8601 中定义的持续时间的字符串。例如,timedelta(days=1, hours=2, seconds=3.4) 代表 'P1DT02H00M03.400000S'
DecimalPromisedjango.utils.functional.lazy() 对象),UUID
对象的字符串表示形式。

JSONL

JSONL 代表 JSON 行。使用这种格式,对象之间由换行符分隔,每一行包含一个有效的 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 序列表示。

自然键

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

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

警告

永远不要在辅助工具和其它序列化数据中包含自动生成的对象。偶尔,辅助工具中加载的主键可能与数据库中的相匹配而加载的辅助工具可能没有起到任何作用。更可能的情况是它们并不匹配,辅助工具将加载失败,并出现 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 中,一个 Book 可以序列化为:

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

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

然而,如果我们向 Person 添加自然键处理,则辅助工具将变得更加人性化。要添加自然键处理, 你可以为 Person 定义一个有着 get_by_natural_key() 方法的默认 Manager。对于 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,也可以是多个字段上的 UniqueConstraintunique_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=Trueuse_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-foreigndumpdata --natural-primary 命令行标志生成自然键。

备注

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

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

自然键和前向引用

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

例如,假设辅助工具中有以下对象:

...
{
    "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 属性。你需要保持追踪该属性不是 NoneDeserializedObject 实例并在之后调用它们的 save_deferred_fields()

典型用法如下:

objs_with_deferred_fields = []

for obj in serializers.deserialize("xml", 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 都将在 PersonBook 被序列化完后再序列化。

Back to Top