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"])
この例では、各モデルの name
と size
属性だけがシリアライズされます。主キーは常に出力結果内の 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 方言のシリアライズ・デシリアライズを行います。 |
|
JSON のシリアライズ・デシリアライズを行います。 |
|
JSONL のシリアライズ・デシリアライズを行います。 |
|
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=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
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¶
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 のシーケンスとして表現されます。
ナチュラルキー¶
デフォルトの外部キーと多対多リレーションのシリアライズ戦略は、オブジェクトのプライマリーキーの値をリレーション内にシリアライズするというものです。この戦略はほとんどのオブジェクトに対してうまく機能しますが、いくつかの状況で困難を引き起こします。
ContentType
を参照する外部キーを持つオブジェクトのリストの場合を考えてみてください。もしcontent type を参照するオブジェクトをシリアライズしようとした場合、最初に content type を参照する手段が必要になります。ContentType
オブジェクトは Django がデータベースの同期処理の間に自動的に作成するため、与えられた content type のプライマリーキーは簡単には予測できず、migrate
が実行される方法とタイミングに依存することになってしまいます。同じことは、特に Permission
、Group
、User
を含む、オブジェクトを自動生成するすべてのモデルにも当てはまります。
警告
自動生成されたオブジェクトは、決してフィクスチャや他のシリアライズされたデータに含めるべきではありません。偶然、フィクスチャ内のプライマリーキーがデータベース内のものと一致して、フィクスチャの読み込みに効果がないかもしれません。より起こりえる状況はプライマリーキーが一致しなかった場合で、フィクスチャのロードは 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
、または複数フィールドにわたる UniqueConstraint
や unique_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-foreign
と dumpdata --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()
これが機能するには、参照しているモデル上の 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
の前にシリアライズされなければなりません。この依存関係を定義するために、1行追加します:
def natural_key(self):
return (self.name,) + self.author.natural_key()
natural_key.dependencies = ["example_app.person"]
この定義により、すべての Person
オブジェクトは Book
オブジェクトよりも先にシリアライズされます。また、Book
を参照するオブジェクトは Person
と Book
の両方がシリアライズされた後にシリアライズされます。