Chargement des données initiales avec Django 1.7 et migrations de données


95

Je suis récemment passé de Django 1.6 à 1.7, et j'ai commencé à utiliser les migrations (je n'ai jamais utilisé South).

Avant la 1.7, j'avais l'habitude de charger les données initiales avec un fixture/initial_data.jsonfichier, qui était chargé avec la python manage.py syncdbcommande (lors de la création de la base de données).

Maintenant, j'ai commencé à utiliser les migrations, et ce comportement est obsolète:

Si une application utilise des migrations, il n'y a pas de chargement automatique des appareils. Étant donné que des migrations seront nécessaires pour les applications dans Django 2.0, ce comportement est considéré comme obsolète. Si vous souhaitez charger les données initiales d'une application, envisagez de le faire dans une migration de données. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

La documentation officielle n'a pas d'exemple clair sur la façon de le faire, donc ma question est:

Quelle est la meilleure façon d'importer ces données initiales à l'aide de migrations de données:

  1. Écrire du code Python avec plusieurs appels à mymodel.create(...),
  2. Utilisez ou écrivez une fonction Django ( comme l'appelloaddata ) pour charger des données à partir d'un fichier de fixture JSON.

Je préfère la deuxième option.

Je ne veux pas utiliser South, car Django semble être capable de le faire nativement maintenant.


3
Aussi, je veux ajouter une autre question à la question initiale du PO: comment devrions-nous faire des migrations de données pour des données n'appartenant pas à nos applications. Par exemple, si quelqu'un utilise le framework de sites, il doit avoir un appareil avec les données des sites. Étant donné que le cadre des sites n'est pas lié à nos applications, où devrions-nous placer cette migration de données? Merci !
Serafeim

Un point important qui n'a encore été abordé par personne ici est ce qui se passe lorsque vous devez ajouter des données définies dans une migration de données vers une base de données sur laquelle vous avez simulé des migrations. Étant donné que les migrations ont été truquées, votre migration de données ne s'exécutera pas et vous devez le faire manuellement. À ce stade, vous pouvez également appeler loaddata sur un fichier fixture.
hekevintran

Un autre scénario intéressant est ce qui se passe si vous avez une migration de données pour créer des instances auth.Group par exemple et que plus tard vous avez un nouveau groupe que vous souhaitez créer en tant que données de départ. Vous devrez créer une nouvelle migration de données. Cela peut être ennuyeux car les données de départ de votre groupe seront dans plusieurs fichiers. De plus, si vous souhaitez réinitialiser les migrations, vous devrez rechercher les migrations de données qui configurent les données d'amorçage et les portent également.
hekevintran

@Serafeim La question "Où placer les données initiales pour une application tierce" ne change pas si vous utilisez une migration de données au lieu de fixtures, puisque vous ne modifiez que la façon dont les données sont chargées. J'utilise une petite application personnalisée pour des choses comme celle-ci. Si l'application tierce s'appelle "foo", j'appelle mon application simple contenant la migration / fixture de données "foo_integration".
guettli

@guettli oui, utiliser probablement une application supplémentaire est la meilleure façon de le faire!
Serafeim

Réponses:


81

Mise à jour : Voir le commentaire de @ GwynBleidD ci-dessous pour les problèmes que cette solution peut causer, et voir la réponse de @ Rockallite ci-dessous pour une approche plus durable pour les futurs changements de modèle.


En supposant que vous ayez un fichier de fixture dans <yourapp>/fixtures/initial_data.json

  1. Créez votre migration vide:

    Dans Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    Dans Django 1.8+, vous pouvez fournir un nom:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Modifier votre fichier de migration <yourapp>/migrations/0002_auto_xxx.py

    2.1. Implémentation personnalisée, inspirée de Django ' loaddata(réponse initiale):

    import os
    from sys import path
    from django.core import serializers
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
    
        fixture = open(fixture_file, 'rb')
        objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
        for obj in objects:
            obj.save()
        fixture.close()
    
    def unload_fixture(apps, schema_editor):
        "Brutally deleting all entries for this model..."
    
        MyModel = apps.get_model("yourapp", "ModelName")
        MyModel.objects.all().delete()
    
    class Migration(migrations.Migration):  
    
        dependencies = [
            ('yourapp', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(load_fixture, reverse_code=unload_fixture),
        ]

    2.2. Une solution plus simple pour load_fixture(selon la suggestion de @ juliocesar):

    from django.core.management import call_command
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
        call_command('loaddata', fixture_file) 

    Utile si vous souhaitez utiliser un répertoire personnalisé.

    2.3. Simplest: appeler loaddataavec app_labelaccessoires volonté de charge du <yourapp>« s fixturesdir automatiquement:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 

    Si vous ne spécifiez pas app_label, loaddata essaiera de charger le fixturenom de fichier à partir de tous les répertoires de fixtures des applications (ce que vous ne voulez probablement pas).

  3. Exécuter

    python manage.py migrate <yourapp>

1
ok, vous avez raison ... L'appel loaddata('loaddata', fixture_filename, app_label='<yourapp>')ira également directement dans le répertoire de l'app fixture (donc pas besoin de construire le chemin complet du fixture)
n__o

15
En utilisant cette méthode, le sérialiseur fonctionnera sur l'état des modèles à partir des models.pyfichiers actuels , qui peuvent avoir des champs supplémentaires ou d'autres modifications. Si des modifications ont été apportées après la création de la migration, celle-ci échouera (nous ne pouvons donc même pas créer de migrations de schéma après cette migration). Pour résoudre ce problème, nous pouvons modifier le registre des applications sur lequel le sérialiseur travaille en registre fourni à la fonction de migration sur le premier paramètre. Le registre du chemin se trouve à l'adresse django.core.serializers.python.apps.
GwynBleidD

3
Pourquoi fait-on ça? Pourquoi Django devient-il de plus en plus difficile à exécuter et à maintenir? Je ne veux pas y aller, je veux une interface de ligne de commande simple qui résout ce problème pour moi, c'est-à-dire comme avant avec les appareils. Django est censé rendre cela plus facile, pas plus difficile :(
CpILL

1
@GwynBleidD C'est un point très important que vous soulevez, et je pense qu'il devrait apparaître dans cette réponse acceptée. C'est la même remarque qui apparaît comme commentaire dans l' exemple de code de migration de données de la documentation . Connaissez-vous une autre façon d'utiliser les sérialiseurs avec le fourni app registry, sans modifier une variable globale (ce qui pourrait causer des problèmes dans un avenir hypothétique avec des migrations de bases de données parallèles).
Annonce N le

3
Cette réponse étant votée pour kazoo avec l'acceptation est exactement pourquoi je recommande aux gens de ne pas utiliser stackoverflow. Même maintenant, avec les commentaires et les anecdotes, j'ai encore des gens dans #django qui font référence à cela.
shangxiao

50

Version courte

Vous ne devez PAS utiliser loaddatala commande de gestion directement dans une migration de données.

# Bad example for a data migration
from django.db import migrations
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # No, it's wrong. DON'T DO THIS!
    call_command('loaddata', 'your_data.json', app_label='yourapp')


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]

Version longue

loaddatautilise django.core.serializers.python.Deserializerqui utilise les modèles les plus à jour pour désérialiser les données historiques dans une migration. C'est un comportement incorrect.

Par exemple, supposons qu'il existe une migration de données qui utilise loaddatala commande de gestion pour charger les données d'un appareil, et qu'elle est déjà appliquée sur votre environnement de développement.

Plus tard, vous décidez d'ajouter un nouveau champ obligatoire au modèle correspondant, donc vous le faites et effectuez une nouvelle migration par rapport à votre modèle mis à jour (et éventuellement fournissez une valeur unique au nouveau champ lorsque ./manage.py makemigrationsvous y êtes invité).

Vous exécutez la prochaine migration et tout va bien.

Enfin, vous avez terminé de développer votre application Django et vous la déployez sur le serveur de production. Il est maintenant temps pour vous d'exécuter toutes les migrations à partir de zéro sur l'environnement de production.

Cependant, la migration des données échoue . En effet, le modèle désérialisé de la loaddatacommande, qui représente le code actuel, ne peut pas être enregistré avec des données vides pour le nouveau champ obligatoire que vous avez ajouté. L'appareil d'origine ne dispose pas des données nécessaires pour cela!

Mais même si vous mettez à jour l'appareil avec les données requises pour le nouveau champ, la migration des données échoue toujours . Lorsque la migration des données est en cours d'exécution, la prochaine migration qui ajoute la colonne correspondante à la base de données n'est pas encore appliquée. Vous ne pouvez pas enregistrer de données dans une colonne qui n'existe pas!

Conclusion: dans une migration de données, laloaddatacommande introduit une éventuelle incohérence entre le modèle et la base de données. Vous ne devez certainement PAS l' utiliser directement dans une migration de données.

La solution

loaddataLa commande s'appuie sur la django.core.serializers.python._get_modelfonction pour obtenir le modèle correspondant à partir d'un appareil, qui renverra la version la plus à jour d'un modèle. Nous devons le patcher de manière singulière pour qu'il obtienne le modèle historique.

(Le code suivant fonctionne pour Django 1.8.x)

# Good example for a data migration
from django.db import migrations
from django.core.serializers import base, python
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # Save the old _get_model() function
    old_get_model = python._get_model

    # Define new _get_model() function here, which utilizes the apps argument to
    # get the historical version of a model. This piece of code is directly stolen
    # from django.core.serializers.python._get_model, unchanged. However, here it
    # has a different context, specifically, the apps variable.
    def _get_model(model_identifier):
        try:
            return apps.get_model(model_identifier)
        except (LookupError, TypeError):
            raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)

    # Replace the _get_model() function on the module, so loaddata can utilize it.
    python._get_model = _get_model

    try:
        # Call loaddata command
        call_command('loaddata', 'your_data.json', app_label='yourapp')
    finally:
        # Restore old _get_model() function
        python._get_model = old_get_model


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]

1
Rockallite, vous faites un point très fort. Votre réponse m'a laissé me demander si la solution 2.1 de la réponse de @ n__o / @ mlissner qui repose sur objects = serializers.deserialize('json', fixture, ignorenonexistent=True)souffrirait du même problème que loaddata? Ou ignorenonexistent=Truecouvre-t-il tous les problèmes possibles?
Dário

7
Si vous regardez la source , vous constaterez que l' ignorenonexistent=Trueargument a deux effets: 1) il ignore les modèles d'un appareil qui ne sont pas dans les définitions de modèle les plus courantes, 2) il ignore les champs d'un modèle d'un appareil qui ne le sont pas dans la définition de modèle correspondante la plus récente. Aucun d'entre eux ne gère la situation de nouveau champ obligatoire dans le modèle . Donc, oui, je pense qu'il souffre du même problème que la plaine loaddata.
Rockallite

Cela a très bien fonctionné une fois que j'ai compris que mon ancien json avait des modèles référencés d'autres modèles en utilisant un natural_key() , ce que cette méthode ne semble pas prendre en charge - je viens de remplacer la valeur natural_key par l'ID réel du modèle référencé.
dsummersl

1
Cette réponse en tant que réponse acceptée serait probablement plus utile, car lors de l'exécution de cas de test, une nouvelle base de données est créée et toutes les migrations sont appliquées à partir de zéro. Cette solution résout les problèmes auxquels un projet avec unittest sera confronté en cas de non remplacement de _get_model lors de la migration de données. Tnx
Mohammad Ali Baghershemirani

Merci pour la mise à jour et les explications, @Rockallite. Ma réponse initiale a été publiée quelques semaines après l'introduction des migrations dans Django 1.7, et la documentation sur la façon de procéder n'était pas claire (et l'est toujours, la dernière fois que j'ai vérifié). Espérons que Django mettra à jour son mécanisme de chargement / migration pour prendre en compte un jour l'historique du modèle.
n__o

6

Inspiré par certains des commentaires (à savoir n__o) et le fait que j'ai beaucoup de initial_data.*fichiers répartis sur plusieurs applications, j'ai décidé de créer une application Django qui faciliterait la création de ces migrations de données.

L' utilisation django-migration-appareil vous pouvez simplement exécuter la commande de gestion suivante et il recherche dans tous vos INSTALLED_APPSpour les initial_data.*fichiers et les transformer en migrations de données.

./manage.py create_initial_data_fixtures
Migrations for 'eggs':
  0002_auto_20150107_0817.py:
Migrations for 'sausage':
  Ignoring 'initial_data.yaml' - migration already exists.
Migrations for 'foo':
  Ignoring 'initial_data.yaml' - not migrated.

Voir django-migration-fixture pour les instructions d'installation / d'utilisation.


2

Afin de donner à votre base de données des données initiales, écrivez une migration de données. Dans la migration de données, utilisez le RunPython fonction pour charger vos données.

N'écrivez aucune commande loaddata car cette méthode est obsolète.

Vos migrations de données ne seront exécutées qu'une seule fois. Les migrations sont une séquence ordonnée de migrations. Lorsque les migrations 003_xxxx.py sont exécutées, les migrations django écrit dans la base de données que cette application est migrée jusqu'à celle-ci (003) et exécutera uniquement les migrations suivantes.


Vous m'encouragez donc à répéter les appels à myModel.create(...)(ou à utiliser une boucle) dans la fonction RunPython?
Mickaël

à peu près ouais. Les bases de données transactionnelles le géreront parfaitement :)
FlogFR

1

Les solutions présentées ci-dessus n'ont malheureusement pas fonctionné pour moi. J'ai constaté que chaque fois que je change de modèle, je dois mettre à jour mes appareils. Idéalement, j'écrirais plutôt des migrations de données pour modifier les données créées et les données chargées par les appareils de la même manière.

Pour faciliter cela, j'ai écrit une fonction rapide qui cherchera dans le fixturesrépertoire de l'application actuelle et chargera un appareil. Mettez cette fonction dans une migration au point de l'historique du modèle qui correspond aux champs de la migration.


Merci pour cela! J'ai écrit une version qui fonctionne avec Python 3 (et passe notre strict Pylint). Vous pouvez l'utiliser comme usine avec RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley

1

À mon avis, les luminaires sont un peu mauvais. Si votre base de données change fréquemment, les maintenir à jour deviendra bientôt un cauchemar. En fait, ce n'est pas seulement mon avis, dans le livre "Two Scoops of Django" c'est bien mieux expliqué.

Au lieu de cela, j'écrirai un fichier Python pour fournir la configuration initiale. Si vous avez besoin de quelque chose de plus, je vous suggère de regarder Factory boy .

Si vous avez besoin de migrer certaines données, vous devez utiliser des migrations de données .

Il y a aussi "Gravez vos appareils, utilisez des usines de modèles" sur l'utilisation des appareils.


1
Je suis d'accord sur votre point "difficile à maintenir si des changements fréquents", mais ici le montage vise uniquement à fournir des données initiales (et minimales) lors de l'installation du projet ...
Mickaël

1
Il s'agit d'un chargement ponctuel de données, ce qui, s'il est effectué dans le contexte de migrations, a du sens. Puisque s'il s'agit d'une migration, il ne devrait pas être nécessaire de modifier les données json. Tout changement de schéma qui nécessite des modifications des données plus tard doit être traité via une autre migration (à ce stade, d'autres données peuvent être dans la base de données qui devront également être modifiées).
mtnpaul

0

Sur Django 2.1, je voulais charger certains modèles (comme les noms de pays par exemple) avec les données initiales.

Mais je voulais que cela se produise automatiquement juste après l'exécution des migrations initiales.

J'ai donc pensé que ce serait formidable d'avoir un sql/dossier dans chaque application qui nécessitait le chargement des données initiales.

Ensuite, dans ce sql/dossier, j'aurais des .sqlfichiers avec les DML requis pour charger les données initiales dans les modèles correspondants, par exemple:

INSERT INTO appName_modelName(fieldName)
VALUES
    ("country 1"),
    ("country 2"),
    ("country 3"),
    ("country 4");

Pour être plus descriptif, voici à quoi sql/ressemblerait une application contenant un dossier: entrez la description de l'image ici

J'ai également trouvé des cas où j'avais besoin du sql scripts soient exécutés dans un ordre spécifique. J'ai donc décidé de préfixer les noms de fichiers avec un numéro consécutif comme le montre l'image ci-dessus.

Ensuite, j'avais besoin d'un moyen de charger SQLsautomatiquement tout disponible dans n'importe quel dossier d'application en faisantpython manage.py migrate .

J'ai donc créé une autre application nommée initial_data_migrationspuis j'ai ajouté cette application à la liste des INSTALLED_APPSdans le settings.pyfichier. Ensuite, j'ai créé un migrationsdossier à l'intérieur et ajouté un fichier appelé run_sql_scripts.py( qui est en fait une migration personnalisée ). Comme le montre l'image ci-dessous:

entrez la description de l'image ici

J'ai créé run_sql_scripts.pypour qu'il s'occupe d'exécuter tous les sqlscripts disponibles dans chaque application. Celui-ci est ensuite renvoyé lorsque quelqu'un court python manage.py migrate. Cette coutume migrationajoute également les applications impliquées en tant que dépendances, de cette façon, elle tente d'exécuter les sqlinstructions uniquement après que les applications requises ont exécuté leurs 0001_initial.pymigrations (nous ne voulons pas tenter d'exécuter une instruction SQL sur une table inexistante).

Voici la source de ce script:

import os
import itertools

from django.db import migrations
from YourDjangoProjectName.settings import BASE_DIR, INSTALLED_APPS

SQL_FOLDER = "/sql/"

APP_SQL_FOLDERS = [
    (os.path.join(BASE_DIR, app + SQL_FOLDER), app) for app in INSTALLED_APPS
    if os.path.isdir(os.path.join(BASE_DIR, app + SQL_FOLDER))
]

SQL_FILES = [
    sorted([path + file for file in os.listdir(path) if file.lower().endswith('.sql')])
    for path, app in APP_SQL_FOLDERS
]


def load_file(path):
    with open(path, 'r') as f:
        return f.read()


class Migration(migrations.Migration):

    dependencies = [
        (app, '__first__') for path, app in APP_SQL_FOLDERS
    ]

    operations = [
        migrations.RunSQL(load_file(f)) for f in list(itertools.chain.from_iterable(SQL_FILES))
    ]

J'espère que quelqu'un trouve cela utile, cela a très bien fonctionné pour moi !. Si vous avez des questions, n'hésitez pas à me le faire savoir.

REMARQUE: Ce n'est peut-être pas la meilleure solution puisque je ne fais que commencer avec django, mais je voulais toujours partager ce "Guide pratique" avec vous tous car je n'ai pas trouvé beaucoup d'informations en cherchant sur Google à ce sujet.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.