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, la sérialisation de données est une opération très simple :

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. Il suffit d’appeler 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 = list(Restaurant.objects.all()) + list(Place.objects.all())
data = serializers.serialize('xml', all_objects)

Désérialisation de données

La désérialisation de données est également une opération relativement simple :

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 de simples objets Django. 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. Naturellement, si vous avez confiance dans votre source de données, vous pouvez simplement enregistrer 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.

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 est très simple :

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

Changed in Django 1.9.

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 simplement 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.utils.encoding import force_text
from django.core.serializers.json import DjangoJSONEncoder

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

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

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.

Decimal, Promise (objets django.utils.functional.lazy()), UUID

Une représentation textuelle de l’objet.

Changed in Django 1.10:

La prise en charge de Promise a été ajoutée.

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 ne sont également représentés que par la clé primaire ou une suite de clés primaires.

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):
    objects = PersonManager()

    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'),)

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):
    objects = PersonManager()

    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)

    birthdate = models.DateField()

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

    class Meta:
        unique_together = (('first_name', '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().

Dépendances durant la sérialisation

Comme les clés naturelles dépendent de requêtes dans la base de données pour résoudre les références, il est important que les données existent avant qu’elles puissent être référencées. Il n’est pas possible de produire des « références en aval » avec les clés naturelles, les données référencées doivent exister avant de pouvoir inclure une référence de clé naturelle vers ces données.

Pour respecter cette contrainte, 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