web-dev-qa-db-fra.com

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

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

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

Maintenant, j'ai commencé à utiliser les migrations, et ce comportement est déconseillé:

Si une application utilise des migrations, il n'y a pas de chargement automatique des fixtures . Étant donné que les 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/fr/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

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

Quel est le meilleur moyen d'importer de telles 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 appelant loaddata ) pour charger des données à partir d’un fichier d’appareil JSON.

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

Je ne veux pas utiliser le sud, car Django semble être capable de le faire en natif maintenant.

88
Mickaël

Update: Voir le commentaire de @ GwynBleidD ci-dessous pour connaître les problèmes que cette solution peut entraîner, et reportez-vous à la réponse de @ Rockallite pour une approche plus durable pour les modifications futures du 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 donner un nom:

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

    2.1. Implémentation personnalisée, inspirée par 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 (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 loaddata avec app_label chargera automatiquement les projecteurs à partir du répertoire <yourapp> 'fixtures

    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 fixture nom_fichier à partir de tous répertoires des applications (que vous ne voulez probablement pas). 

  3. Exécuter

    python manage.py migrate <yourapp>
    
76
n__o

Version courte

Vous devez PAS utiliser la commande de gestion loaddata 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

loaddata utilise Django.core.serializers.python.Deserializer, qui utilise les modèles les plus récents 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 la commande de gestion loaddata pour charger des données à partir d’une installation, et que celle-ci est déjà appliquée à votre environnement de développement.

Plus tard, vous décidez d’ajouter un nouveau champ required au modèle correspondant. Vous devez donc le faire et effectuer une nouvelle migration par rapport au modèle mis à jour (et éventuellement fournir une valeur unique au nouveau champ lorsque ./manage.py makemigrations vous y invite) .

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

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

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

Mais même si vous mettez à jour le projecteur 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 migration next, qui ajoute la colonne correspondante à la base de données, n'est pas encore appliquée. Vous ne pouvez pas sauvegarder de données dans une colonne qui n'existe pas!

Conclusion: dans une migration de données, la commande loaddata introduit une incohérence potentielle entre le modèle et la base de données. Vous devez absolument utiliser PAS directement dans une migration de données.

La solution

La commande loaddata s'appuie sur la fonction Django.core.serializers.python._get_model pour obtenir le modèle correspondant à partir d'un appareil, qui renverra la version la plus récente d'un modèle. Nous devons le patcher de manière à obtenir 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),
    ]
37
Rockallite

Inspiré par certains des commentaires (notamment ceux de n__o) et par le fait que j'ai beaucoup de fichiers initial_data.* 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.

En utilisant Django-migration-fixture , vous pouvez simplement exécuter la commande de gestion suivante. Tous les fichiers INSTALLED_APPS et initial_data.* seront ensuite recherchés et transformés 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.

5
alexhayes

Le meilleur moyen de charger les données initiales dans les applications migrées consiste à effectuer des migrations de données (comme le recommande également la documentation). L'avantage est que le support est ainsi chargé à la fois pendant les tests et la production.

@n__o a suggéré de réimplémenter la commande loaddata dans la migration. Dans mes tests, cependant, appeler directement la commande loaddata fonctionne également très bien. L'ensemble du processus est donc:

  1. Créer un fichier de fixture dans <yourapp>/fixtures/initial_data.json

  2. Créez votre migration vide:

    python manage.py makemigrations --empty <yourapp>
    
  3. Editez votre fichier de migration /migrations/0002_auto_xxx.py

    from Django.db import migrations
    from Django.core.management import call_command
    
    
    def loadfixture(apps, schema_editor):
        call_command('loaddata', 'initial_data.json')
    
    
    class Migration(migrations.Migration):
    
        dependencies = [
            ('<yourapp>', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(loadfixture),
        ]
    
4
Pratyush

Afin de donner à votre base de données des données initiales, écrivez une migration data. Dans la migration des données, utilisez la fonction RunPython 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 écrivent dans la base de données que cette application est migrée jusqu'à celle-ci (003) et n'exécutera que les migrations suivantes.

2
aRkadeFR

Les solutions présentées ci-dessus ne m'ont malheureusement pas fonctionné. 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 de la même manière les données créées et les données chargées.

Pour faciliter ceci j’ai écrit une fonction rapide qui va chercher dans le répertoire fixtures de l’application actuelle et charger un appareil. Placez cette fonction dans une migration au point de l'historique du modèle qui correspond aux champs de la migration.

1
leifdenby

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é qu'il serait intéressant de disposer d'un dossier sql/ dans chaque application nécessitant le chargement de données initiales.

Ensuite, dans ce dossier sql/, j'aurais des fichiers .sql avec les fichiers 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 ressemblerait une application contenant un dossier sql/:  enter image description here

J'ai aussi trouvé des cas où j'avais besoin que les scripts sql 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 indiqué dans l'image ci-dessus.

Ensuite, il me fallait un moyen de charger toute SQLs disponible dans un dossier d'application automatiquement en faisant python manage.py migrate.

J'ai donc créé une autre application nommée initial_data_migrations, puis j'ai ajouté cette application à la liste de INSTALLED_APPS dans le fichier settings.py. Ensuite, j'ai créé un dossier migrations à l'intérieur et ajouté un fichier appelé run_sql_scripts.py (qui correspond en fait à une migration personnalisée). Comme on le voit dans l'image ci-dessous:

 enter image description here

J'ai créé run_sql_scripts.py pour qu'il se charge d'exécuter tous les scripts sql disponibles dans chaque application. Celui-ci est ensuite déclenché lorsque quelqu'un exécute python manage.py migrate. Cette personnalisation migration ajoute également les applications impliquées en tant que dépendances, de cette façon elle tente d'exécuter les instructions sql uniquement après que les applications requises ont exécuté leur migration 0001_initial.py (nous ne souhaitons pas 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 trouvera cela utile, cela a fonctionné parfaitement pour moi !. Si vous avez des questions s'il vous plaît faites le moi savoir.

NOTE: Ce n'est peut-être pas la meilleure solution car je viens juste de commencer à utiliser Django, mais je voulais tout de même partager ce "Guide pratique" avec vous tous, car je ne trouvais pas beaucoup d'informations à ce sujet .

0

À mon avis, les rencontres sont un peu mauvaises. Si votre base de données change fréquemment, les tenir à jour sera bientôt un cauchemar. En fait, ce n’est pas seulement mon avis, dans le livre "Two Scoops of Django", il est beaucoup mieux expliqué.

Au lieu de cela, je vais écrire 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 des données, vous devez utiliser migrations de données .

Il y a aussi "Brûlez vos appareils, utilisez des usines modèles" à propos de l’utilisation d’appareils.

0
Griffosx