Sérialisation d’objets Django

L’infrastructure de sérialisation de Django fournit un mécanisme pour « traduire » les modèles Django en d’autres formats. En général, ces autres formats sont basés sur du texte et utilisés pour envoyer des données de Django à travers le réseau, mais il est possible qu’un sérialiseur prenne en charge n’importe quel format (basé sur du texte ou non).

Voir aussi

Si vous voulez simplement extraire certaines données de vos tables pour les sérialiser, il est possible d’utiliser la commande de gestion dumpdata.

Sérialisation de données

Au plus haut niveau, les données peuvent être sérialisées comme ceci :

from django.core import serializers
data = serializers.serialize("xml", SomeModel.objects.all())

Les paramètres de la fonction serialize sont constitués du format dans lequel la sérialisation des données doit se faire (voir Formats de sérialisation) et d’un objet QuerySet contenant les données (en fait, le second paramètre peut être n’importe quel itérateur produisant des instances de modèles Django, mais il s’agit presque toujours d’un QuerySet).

django.core.serializers.get_serializer(format)

Il est aussi possible d’utiliser directement un objet de sérialiseur :

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

C’est utile lorsque l’on veut sérialiser des données directement dans un objet de type fichier (ce qui inclut aussi HttpResponse) :

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

Note

Si vous appelez get_serializer() avec un format de sérialisation inconnu, une exception django.core.serializers.SerializerDoesNotExist est générée.

Sous-ensemble de champs

Lorsqu’on ne veut sérialiser qu’un sous-ensemble de champs, il est possible de renseigner le paramètre fields du sérialiseur :

from django.core import serializers
data = serializers.serialize('xml', SomeModel.objects.all(), fields=('name','size'))

Dans cet exemple, seuls les attributs name et size de chaque modèle seront sérialisés. La clé primaire est toujours sérialisée comme élément pk dans le résultat ; elle n’apparaît jamais dans la partie fields.

Note

Selon votre modèle, il n’est parfois pas possible de désérialiser un modèle qui n’a été sérialisé qu’avec un sous-ensemble de ses champs. Si un objet sérialisé ne comporte pas tous les champs obligatoires d’un modèle, la désérialisation ne sera pas capable d’enregistrer les instances désérialisées.

Modèles hérités

Si un modèle est défini par une classe de base abstraite, il n’y a rien de particulier à faire pour pouvoir sérialiser ce modèle. Appelez le sérialiseur sur l’objet (ou les objets) que vous voulez sérialiser et le résultat sera une représentation complète de l’objet sérialisé.

Cependant, si un modèle utilise de l’héritage multi-table, il sera alors aussi nécessaire de sérialiser toutes les classes de base du modèle. C’est parce que seuls les champs définis localement dans le modèle seront sérialisés. Par exemple, considérons les modèles suivants :

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

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

Si vous ne sérialisez que le modèle Restaurant:

data = serializers.serialize('xml', Restaurant.objects.all())

les champs de la sortie sérialisée ne contiendront que l’attribut serves_hot_dogs. L’attribut name de la classe de base sera ignoré.

Afin de sérialiser complètement les instances de Restaurant, il sera aussi nécessaire de sérialiser les modèles Place:

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

Désérialisation de données

La désérialisation de données est très semblable à la sérialisation :

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

Comme vous pouvez le voir, la fonction deserialize accepte le même paramètre format que serialize, une chaîne ou un flux de données, et renvoie un itérateur.

Cependant, ça se complique un peu à cet endroit. Les objets renvoyés par l’itérateur deserialize ne sont pas des objets Django normaux. Il s’agit d’instances DeserializedObject spéciales qui englobent un objet crée mais non enregistré ainsi que les données de liaison associées.

L’appel à DeserializedObject.save() enregistre l’objet dans la base de données.

Note

Si l’attribut pk n’existe pas dans les données sérialisées ou vaut null, une nouvelle instance est enregistrée dans la base de données.

Cela garantit que la désérialisation est une opération non destructive, même si les données de la représentation sérialisée ne correspondent pas à ce qui se trouve actuellement dans le base de données. Généralement, voici comment l’on traite ces instances de DeserializedObject:

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

En d’autres termes, l’utilisation habituelle est d’examiner les objets désérialisés pour s’assurer qu’ils sont « cohérents » avant de les enregistrer. Si vous avez confiance dans votre source de données, vous pouvez aussi enregistrer directement l’objet et passer à la suite.

L’objet Django lui-même peut être inspecté avec deserialized_object.object. Si des champs dans les données sérialisées n’existent pas dans un modèle, une exception DeserializationError est générée, sauf si le paramètre ignorenonexistent a été transmis avec la valeur True:

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

Formats de sérialisation

Django prend en charge quelques formats de sérialisation, certains dépendant de l’installation de modules Python supplémentaires :

Identifiant Information
xml Sérialise vers et à partir d’un dialecte XML simple.
json Sérialise vers et à partir du format JSON.
jsonl Sérialise vers et à partir du format JSONL.
yaml Sérialise en YAML (YAML Ain’t a Markup Language). Ce sérialiseur n’est disponible que si PyYAML est installé.

XML

Le format basique de sérialisation XML ressemble à ceci :

<?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>

La collection complète des objets sérialisés ou désérialisés est représentée par une balise <django-objects> contenant plusieurs éléments <object>. Chacun de ces objets contient deux attributs : « pk » et « model », ce dernier étant constitué du nom de l’application (« sessions ») et du nom du modèle en minuscules (« session »), séparés par un point.

Chaque champ de l’objet est sérialisé comme élément <field> contenant les attributs « type » et « name » du champ. Le contenu textuel de l’élément représente la valeur à stocker pour le champ.

Les clés étrangères et les autres champs relationnels sont traités légèrement différemment :

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

Dans cet exemple, l’objet auth.Permission ayant la clé primaire 27 possède une clé étrangère vers l’instance contenttypes.ContentType ayant la clé primaire 9.

Les relations plusieurs-à-plusieurs sont exportées pour le modèle qui fait office de lien. Par exemple, le modèle auth.User possède une telle relation vers le modèle 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>

Cet exemple lie l’utilisateur donné avec les modèles de permission ayant les clés primaires 46 et 47.

Caractères de contrôle

Si le contenu à sérialiser contient des caractères de contrôle qui ne sont pas acceptés par le standard XML 1.0, la sérialisation échoue avec une exception ValueError. Lisez également l’explication du W3C dans le document HTML, XHTML, XML and Control Codes.

JSON

Tout en conservant les mêmes données d’exemple que précédemment, voici ce que donnerait leur sérialisation au format JSON :

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

La mise en forme est ici un peu plus simple qu’en XML. L’ensemble de la collection est représentée par un seul tableau et les objets sont représentés par des objets JSON ayant trois propriétés : « pk », « model » et « fields ». « fields » est lui-même un objet contenant les noms et valeurs de chaque champ respectivement comme propriété et propriété-valeur.

Les clés étrangères ont la clé primaire de l’objet lié comme valeur de propriété. Les objets des relations plusieurs-à-plusieurs sont sérialisés avec le modèle qui les définit et sont représentés sous forme de liste de clés primaires.

Il faut savoir que certains contenus Django ne peuvent pas être transmis tels quels à json. Par exemple, si l’un de vos objets à sérialiser contient un type personnalisé, vous devrez écrire un codeur json dédié. Quelque chose comme ceci devrait fonctionner :

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)

Vous pouvez alors passer cls=LazyEncoder à la fonction serializers.serialize():

from django.core.serializers import serialize

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

Notez également que GeoDjango met à disposition un sérialiseur adapté à GeoJSON.

Changed in Django 3.1:

Toutes les données sont dorénavant codées en Unicode. Si vous avez besoin du comportement précédent, passez ensure_ascii=True à la fonction serializers.serialize().

DjangoJSONEncoder

class django.core.serializers.json.DjangoJSONEncoder

Le sérialiseur JSON utilise DjangoJSONEncoder pour le codage. C’est une sous-classe de JSONEncoder, qui ajoute le codage de types supplémentaires :

datetime
Une chaîne sous la forme AAAA-MM-JJTHH:mm:ss.sssZ ou AAAA-MM-JJTHH:mm:ss.sss+HH:MM telle que définie dans ECMA-262.
date
Une chaîne sous la forme AAAA-MM-JJ telle que définie dans ECMA-262.
time
Une chaîne sous la forme HH:MM:ss.sss telle que définie dans ECMA-262.
timedelta
Une chaîne représentant une durée telle que définie par ISO-8601. Par exemple, timedelta(days=1, hours=2, seconds=3.4) est représenté par 'P1DT02H00M03.400000S'.
Decimal, Promise (objets django.utils.functional.lazy()), UUID
Une représentation textuelle de l’objet.

JSONL

New in Django 3.2.

JSONL signifie JSON Lines. Avec ce format, les objets sont séparés par des sauts de lignes et chaque ligne contient un objet JSON valide. Les données sérialisées en JSONL ressemblent à ceci

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

JSONL peut se révéler utile pour remplir de grosses bases de données, dans la mesure où les données peuvent être traitées ligne par ligne, plutôt que d’être toutes chargées intégralement en mémoire.

YAML

La sérialisation YAML ressemble beaucoup à celle de JSON. La liste d’objets est sérialisée comme une suite de correspondances avec les clés « pk », « model » et « fields ». Chaque champ est lui-même une correspondance entre la clé contenant le nom du champ et la valeur contenant la valeur du champ :

-   fields: {expire_date: !!timestamp '2013-01-16 08:16:59.844560+00:00'}
    model: sessions.session
    pk: 4b678b301dfd8a4e0dad910de3ae245b

Les champs de référence sont également représentés par la clé primaire ou une suite de clés primaires.

Changed in Django 3.1:

Toutes les données sont dorénavant codées en Unicode. Si vous avez besoin du comportement précédent, passez allow_unicode=False à la fonction serializers.serialize().

Clés naturelles

La stratégie de sérialisation par défaut pour les clés étrangères et les relations plusieurs-à-plusieurs est de sérialiser la valeur des clés primaires des objets de la relation. Cette stratégie fonctionne bien pour la plupart des objets, mais elle peut provoquer des difficultés en certaines circonstances.

Considérons le cas d’une liste d’objets ayant une clé étrangère référençant ContentType. Si vous sérialisez un objet qui se réfère à un type de contenu, il faut d’abord pouvoir trouver un moyen de faire référence à ce type de contenu. Comme les objets ContentType sont créés automatiquement par Django durant le processus de synchronisation de la base de données, la clé primaire d’un type de contenu n’est pas facile à prédire ; cela dépend de la manière et du moment où migrate a été exécuté. C’est pareil pour tous les modèles qui génèrent automatiquement des objets, comme par exemple Permission, Group et User.

Avertissement

Vous ne devriez jamais inclure des objets générés automatiquement dans des instantanés ou d’autres données sérialisées. Il est possible qu’avec un peu de chance, les clés primaires de l’instantané correspondent à celles de la base de données et que le chargement de l’instantané passe sans autre. Mais dans le cas plus probable où elles ne correspondent pas, le chargement de l’instantané échouera avec une exception IntegrityError.

Il y a aussi des raisons plus pratiques. Un identifiant nombre entier n’est pas toujours la manière la plus intuitive de se référer à un objet ; parfois, il est bien utile de disposer d’une référence plus naturelle.

C’est pour ces raisons que Django fournit les clés naturelles. Une clé naturelle est un tuple de valeurs servant à identifier de manière unique une instance d’objet sans avoir besoin de la valeur de clé primaire.

Désérialisation des clés naturelles

Considérons les deux modèles suivants :

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:
        unique_together = [['first_name', 'last_name']]

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

En principe, les données sérialisées de Book utilisent un nombre entier pour se référer à l’auteur. Par exemple, en JSON, un objet Book pourrait être sérialisé comme :

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

Cette manière de se référer à un auteur n’est pas particulièrement intuitive. Il est nécessaire de connaître la valeur de clé primaire de l’auteur concerné ; il faut aussi que la clé primaire soit stable et prédictible.

Cependant, si nous ajoutons la capacité de clé naturelle à Person, l’instantané prend tout de suite un air plus humain. Pour ajouter la capacité de clé naturelle, il s’agit de définir un gestionnaire par défaut pour Person avec une méthode get_by_natural_key(). Dans le cas de Person, on pourrait utiliser le prénom et le nom comme bonne clé naturelle :

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:
        unique_together = [['first_name', 'last_name']]

Les livres peuvent maintenant utiliser la clé naturelle pour faire référence aux objets Person:

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

Au moment de charger ces données sérialisées, Django utilise la méthode get_by_natural_key() pour obtenir la clé primaire d’un objet Person existant à partir des informations ["Douglas", "Adams"].

Note

Quels que soient les champs utilisés pour définir une clé primaire, ils doivent pouvoir identifier un objet de manière unique. Cela signifie généralement que le modèle possède une clause d’unicité (soit unique=True pour un seul champ ou unique_together pour plusieurs champs) pour le ou les champs composant la clé naturelle. Cependant, l’unicité ne doit pas nécessairement être garantie au niveau de la base de données. Si vous êtes sûr qu’un certain ensemble de champs sera effectivement unique, vous pouvez très bien utiliser ces champs comme clé naturelle.

La désérialisation d’objets sans clé primaire vérifie toujours si le gestionnaire du modèle possède une méthode get_by_natural_key() et l’utilise le cas échéant pour compléter la clé primaire des objets désérialisés.

Sérialisation des clés naturelles

Comment donc pousser Django à émettre une clé naturelle lorsqu’il sérialise un objet ? Premièrement, vous devez ajouter une autre méthode, cette fois dans le modèle lui-même :

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:
        unique_together = [['first_name', 'last_name']]

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

Cette méthode doit toujours renvoyer un tuple de clés naturelles ; dans cet exemple, il s’agit de (first name, last name). Puis, lors de l’appel à serializers.serialize(), il faut indiquer les paramètres use_natural_foreign_keys=True ou use_natural_primary_keys=True:

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

Lorsque use_natural_foreign_keys=True est indiqué, Django utilise la méthode natural_key() pour sérialiser toute référence de clé étrangère à des objets d’un type définissant cette méthode.

Lorsque use_natural_primary_keys=True est indiqué, Django ne produit pas de clé primaire dans les données sérialisées de cet objet dans la mesure où elle peut être calculée durant la désérialisation :

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

Cela peut être utile lorsqu’il y a besoin de charger des données sérialisées dans une base de données existante et qu’il n’est pas possible de garantir que les valeurs de clés primaires sérialisées ne sont pas déjà utilisées, et qu’il n’est pas nécessaire de conserver les clés primaires exactes des objets désérialisés.

Si vous utilisez dumpdata pour générer des données sérialisées, utilisez les options de ligne de commande dumpdata --natural-foreign et dumpdata --natural-primary pour générer des clés naturelles.

Note

Il n’est pas obligatoire de définir à la fois natural_key() et get_by_natural_key(). Si vous ne souhaitez pas que Django produise des clés naturelles pendant la sérialisation mais que vous voulez conserver la possibilité de charger des clés naturelles, vous pouvez choisir de n’implémenter que la méthode natural_key().

Inversement, si (pour une raison étrange) vous voulez que Django produise des clés naturelles pendant la sérialisation, mais qu’il ne puisse pas charger ces valeurs, il suffit d’implémenter la méthode get_by_natural_key().

Clés naturelles et références en aval

Parfois, lors de l’utilisation de clés étrangères naturelles, il est nécessaire de désérialiser des données d’un objet possédant une clé étrangère se référant à un autre objet pas encore sérialisé. On appelle cela une « référence en aval ».

Par exemple, supposons que votre instantané contiennent les objets suivants

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

Afin de pouvoir gérer cette situation, il faut passer handle_forward_references=True à serializers.deserialize(). Cela marquera l’attribut deferred_fields sur les instances DeserializedObject. Vous devez ensuite garder la trace des instances DeserializedObject où cet attribute n’est pas None et appeler plus tard save_deferred_fields() sur ces instances.

Voici à quoi peut ressembler une utilisation typique :

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()

Pour que ça fonctionne, la clé ForeignKey du modèle qui y fait référence doit avoir null=True.

Dépendances durant la sérialisation

Il est souvent possible d’éviter de devoir gérer explicitement les références en aval en prenant soin de placer les objets d’un instantané dans le bon ordre.

Pour aider à faire cela, les appels à dumpdata utilisant l’option dumpdata --natural-foreign sérialisent en premier les modèles possédant une méthode natural_key(), avant de sérialiser les objets à l’aide de leur clé primaire standard.

Cependant, ce n’est pas toujours suffisant. Si une clé naturelle se réfère à un autre objet (en utilisant une clé étrangère ou une clé naturelle vers un autre objet faisant partie d’une clé naturelle), il faut être capable de garantir que les objets dont dépend une clé naturelle apparaissent dans les données sérialisées avant la clé naturelle qui en a besoin.

Pour contrôler cet ordre d’apparition, il est possible de définir des dépendances dans les méthodes natural_key(). Cela se fait en définissant un attribut dependencies sur la méthode natural_key() elle-même.

Par exemple, ajoutons une clé naturelle au modèle Book à partir de l’exemple ci-dessus :

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()

La clé naturelle d’un objet Book est une combinaison de son nom et de son auteur. Cela signifie que Person doit être sérialisé avant Book. Pour définir cette dépendance, nous ajoutons une ligne supplémentaire :

def natural_key(self):
    return (self.name,) + self.author.natural_key()
natural_key.dependencies = ['example_app.person']

Cette définition garantit que tous les objets Person sont sérialisés avant les objets Book. Sur le même principe, tout objet référençant Book sera sérialisé après les objets Person et Book.

Back to Top