Django オブジェクトのシリアライズ

Django のシリアライズフレームワークは、Django のモデルを他のフォーマットに「翻訳」する仕組みを提供します。通常、このような他のフォーマットはテキストベースや Django データをネットワーク越しに送信するために使われるフォーマットになりますが、シリアライザーはどんなフォーマットも (テキストベースと非テキストベースのいずれも) 処理可能です。

参考

あるデータをテーブルからシリアライズされた形式として取得したい場合、dumpdata 管理コマンドが使用できます。

データのシリアライズ

最も高いレベルでは、次のようにデータをシリアライズできます。

from django.core import serializers

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

serialize 関数の引数は、データをシリアライズするフォーマット (シリアライズフォーマット) とシリアライズする QuerySet です。(実際、第2引数は Django モデルのインスタンスを yield するどんなイテレータでも渡せますが、ほとんど場合は 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)

もし次のように Reastaurant モデルだけをシリアライズした場合、

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 として検査できます。シリアライズされたデータ内のフィールドがモデル上に存在しない場合は、ignorenonexistent 引数に True が渡されていない限り、DeserializationError が発生します。

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

シリアライズフォーマット

Django は多数のシリアライズフォーマットをサポートしてします。一部のフォーマットはサードパーティの Python モジュールが必要です。

識別子 情報
xml シンプルな XML 方言のシリアライズ・デシリアライズを行います。
json JSON のシリアライズ・デシリアライズを行います。
jsonl JSONL のシリアライズ・デシリアライズを行います。
yaml YAML (YAML Ain't a 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> 要素を含む <djangoobjects> タグで表現されます。このようなオブジェクトはそれぞれ2つの属性を持ちます。"pk" と "model" です。後者はアプリの名前 ("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 インスタンスに対する外部キーを持つように指定しています。

多対多のリレーションはそれらを結びつけるモデルに対してエクスポートされます。例えば、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>

この例では、与えられたユーザーをPK46と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 よりも少しシンプルです。コレクション全体は array として表現され、オブジェクトは3つのプロパティ "pk"、"model"、"fields" を持つ JSON オブジェクトとして表現されています。"fields" もオブジェクトであり、各フィールド名と値がそれぞれプロパティとプロパティ値として含まれています。

外部キーは、プロパティ値としてリンクされたオブジェクトの PK を持ちます。ManyToMany リレーションは、それを定義したモデルに対してシリアライズされ、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=LazyEncoderserializers.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
ECMA-262 で定義されている YYYY-MM-DDTHH:mm:ss.sssZ または YYYY-MM-DDTHH:mm:ss.sss+HH:MM という形式の文字列。
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' と表現されます。
Decimal, Promise (django.utils.functional.lazy() objects), UUID
オブジェクトの文字列表現です。

JSONL

JSONLJSON 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 のシーケンスとして表現されます。

ナチュラルキー

デフォルトの外部キーと多対多リレーションのシリアライズ戦略は、オブジェクトのプライマリーキーの値をリレーション内にシリアライズするというものです。この戦略はほとんどのオブジェクトに対してうまく機能しますが、いくつかの状況で困難を引き起こします。

ContentType を参照する外部キーを持つオブジェクトのリストの場合を考えてみてください。もしcontent type を参照するオブジェクトをシリアライズしようとした場合、最初に content type を参照する手段が必要になります。ContentType オブジェクトは Django がデータベースの同期処理の間に自動的に作成するため、与えられた content type のプライマリーキーは簡単には予測できず、migrate が実行される方法とタイミングに依存することになってしまいます。同じことは、特に PermissionGroupUser を含む、オブジェクトを自動生成するすべてのモデルにも当てはまります。

警告

自動生成されたオブジェクトは、決してフィクスチャや他のシリアライズされたデータに含めるべきではありません。偶然、フィクスチャ内のプライマリーキーがデータベース内のものと一致して、フィクスチャの読み込みに効果がないかもしれません。より起こりえる状況はプライマリーキーが一致しなかった場合で、フィクスチャのロードは IntegrityError で失敗してしまいます。

利便性の問題もあります。integer id というのは、必ずしもオブジェクトを参照するための最も便利な方法というわけではありません。ときには、より自然な (ナチュラルな) 参照が助けになることがあります。

このような理由のために Django が提供しているのが、ナチュラルキー (natural key) です。ナチュラルキーは、オブジェクトのインスタンスをプライマリーキーの値を使用せずに一意に識別するために使える値のタプルです。

ナチュラルキーのデシリアライズ

次の2つのモデルを考えてみてください。

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 のシリアライズされたデータは、著者 (author) を参照するために整数を使うことになります。たとえば、JSON では、Book は次のようにシリアライズされるでしょう。

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

これは、著者を参照するのに特に自然な方法とは言えません。著者のプライマリーキーの値を知っていなければなりませんし、しかも、プライマリーキーの値は安定していて予測可能でなければなりません。

しかし、Person にナチュラルキーの処理を追加した場合、フィクスチャはもっとずっと人間によってわかりやすくなります。ナチュラルキーの処理を追加するには、Person のデフォルトの Manager を get_by_natural_key() を使用して定義します。Person の場合、よいナチュラルキーは、first name と last name のペアになるでしょう。

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 にナチュラルキーを発行させるにはどうすればいいのでしょうか? はじめに、もう1つのメソッドを追加する必要があります。今回は、次のようにモデル自体に追加します。

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)

メソッドは、常にナチュラルキーの他ルプを返す必要があります。この例では、(first name, 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-foreigndumpdata --natural-primary コマンドラインフラグを使用してナチュラルキーを生成します。

注釈

natural_key()get_by_natural_key() の両方を定義する必要はありません。もし Django にシリアライズ時にナチュラルキーを出力させたくないが、 ナチュラルキーを読み込む機能は残しておきたい場合は、 natural_key() メソッドを実装しなくても構いません。

逆に、(特別な理由で)Django にシリアライズ時にナチュラルキーを出力させたいが、そのキーの値を読み込ませたく ない 場合は、 get_by_natural_key() メソッドを定義しなければいいだけです。

ナチュラルキーと前方参照

ナチュラル外部キー を使用するとき、データをシリアライズする必要があるけれども、オブジェクトの外部キーが参照している他のオブジェクトが、まだシリアライズされていないような場合があります。このことを「前方参照 (forward reference)」と呼びます。

たとえば、フィクスチャに次のようなオブジェクトがあると想定してください。

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

この状況を処理するには、serializers.deserialize()handle_forward_references=True を渡す必要があります。これにより、DeserializedObject インスタンスに deferred_fields 属性が設定されます。この属性が None ではない DeserializedObject インスタンスを追跡して、後でそれらに対して 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()

これが機能するには、参照しているモデル上の ForeignKeynull=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 のナチュラルキーは名前と著者の組み合わせです。つまり、 PersonBook の前にシリアライズされなければなりません。この依存関係を定義するために、1行追加します:

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


natural_key.dependencies = ["example_app.person"]

この定義により、すべての Person オブジェクトは Book オブジェクトよりも先にシリアライズされます。また、Book を参照するオブジェクトは PersonBook の両方がシリアライズされた後にシリアライズされます。

Back to Top