Caricamento dei dati iniziali con Django 1.7 e migrazioni dei dati


95

Di recente sono passato da Django 1.6 a 1.7 e ho iniziato a utilizzare le migrazioni (non ho mai usato South).

Prima della 1.7, caricavo i dati iniziali con un fixture/initial_data.jsonfile, che veniva caricato con il python manage.py syncdbcomando (durante la creazione del database).

Ora ho iniziato a utilizzare le migrazioni e questo comportamento è deprecato:

Se un'applicazione utilizza le migrazioni, non vi è alcun caricamento automatico dei dispositivi. Poiché le migrazioni saranno necessarie per le applicazioni in Django 2.0, questo comportamento è considerato deprecato. Se desideri caricare i dati iniziali per un'app, considera di farlo in una migrazione dei dati. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

La documentazione ufficiale non ha un chiaro esempio su come farlo, quindi la mia domanda è:

Qual è il modo migliore per importare tali dati iniziali utilizzando le migrazioni dei dati:

  1. Scrivi codice Python con più chiamate a mymodel.create(...),
  2. Usa o scrivi una funzione Django ( come la chiamataloaddata ) per caricare i dati da un file fixture JSON.

Preferisco la seconda opzione.

Non voglio usare South, dato che Django sembra essere in grado di farlo in modo nativo ora.


3
Inoltre, voglio aggiungere un'altra domanda alla domanda originale dell'OP: come dovremmo eseguire le migrazioni dei dati per i dati che non appartengono alle nostre applicazioni. Ad esempio, se qualcuno utilizza il framework dei siti, deve avere un collegamento con i dati dei siti. Dato che il framework dei siti non è correlato alle nostre applicazioni, dove dovremmo collocare la migrazione dei dati? Grazie !
Serafeim

Un punto importante che non è stato ancora affrontato da nessuno qui è cosa succede quando è necessario aggiungere dati definiti in una migrazione di dati a un database su cui sono state simulate migrazioni. Poiché le migrazioni sono state simulate, la migrazione dei dati non verrà eseguita e sarà necessario eseguirla manualmente. A questo punto puoi anche chiamare solo loaddata su un file fixture.
hekevintran

Un altro scenario interessante è quello che accade se si dispone di una migrazione dei dati per creare istanze di auth.Group, ad esempio e successivamente si dispone di un nuovo gruppo che si desidera creare come dati seed. Dovrai creare una nuova migrazione dei dati. Questo può essere fastidioso perché i dati seed del tuo gruppo saranno in più file. Inoltre, nel caso in cui desideri reimpostare le migrazioni, dovrai guardare attraverso per trovare le migrazioni di dati che configurano i dati di inizializzazione e anche portarli.
hekevintran

@Serafeim La domanda "Dove mettere i dati iniziali per un'app di terze parti" non cambia se si utilizza una migrazione dei dati invece dei dispositivi, poiché si cambia solo il modo in cui i dati vengono caricati. Uso una piccola app personalizzata per cose come questa. Se l'app di terze parti si chiama "foo", chiamo la mia app semplice contenente la migrazione / fixture dei dati "foo_integration".
guettli

@ guettli sì, probabilmente usare un'applicazione extra è il modo migliore per farlo!
Serafeim

Risposte:


81

Aggiornamento : vedere il commento di @ GwynBleidD di seguito per i problemi che questa soluzione può causare e vedere la risposta di @ Rockallite di seguito per un approccio più duraturo per i futuri cambiamenti del modello.


Supponendo che tu abbia un file fixture in <yourapp>/fixtures/initial_data.json

  1. Crea la tua migrazione vuota:

    In Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    In Django 1.8+, puoi fornire un nome:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Modifica il tuo file di migrazione <yourapp>/migrations/0002_auto_xxx.py

    2.1. Implementazione personalizzata, ispirata a Django ' loaddata(risposta iniziale):

    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. Una soluzione più semplice per load_fixture(secondo il suggerimento di @ 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 se vuoi usare una directory personalizzata.

    2.3. Semplice: chiamando loaddatacon app_labelinfissi carico viene dalla <yourapp>'s fixturesdir automaticamente:

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

    Se non specifichi app_label, loaddata proverà a caricare il fixturenome del file da tutte le directory dei dispositivi delle app (cosa che probabilmente non vuoi).

  3. Eseguirlo

    python manage.py migrate <yourapp>

1
ok, hai ragione ... Anche la chiamata loaddata('loaddata', fixture_filename, app_label='<yourapp>')andrà direttamente alla
directory

15
Usando questo metodo, il serializzatore funzionerà sullo stato dei modelli dai models.pyfile correnti , che possono avere alcuni campi extra o altre modifiche. Se alcune modifiche sono state apportate dopo la creazione della migrazione, non riuscirà (quindi non possiamo nemmeno creare migrazioni dello schema dopo quella migrazione). Per risolvere questo problema, possiamo cambiare temporaneamente il registro delle app su cui sta lavorando il serializzatore al registro fornito alla funzione di migrazione sul primo parametro. Il registro del percorso si trova in django.core.serializers.python.apps.
GwynBleidD

3
Perché stiamo facendo questo? Perché Django diventa sempre più difficile da gestire e mantenere? Non voglio fare questo, voglio una semplice interfaccia a riga di comando che risolva questo problema per me, cioè come era con i dispositivi. Django dovrebbe rendere questa roba più facile, non più difficile :(
CpILL

1
@GwynBleidD Questo è un punto molto importante che stai facendo e penso che dovrebbe apparire in questa risposta accettata. È la stessa osservazione che appare come commento nell'esempio di codice di migrazione dei dati della documentazione . Conosci un altro modo di utilizzare i serializzatori con i dati forniti app registry, senza modificare una variabile globale (che potrebbe causare problemi in un ipotetico futuro con migrazioni parallele di database).
Annuncio N

3
Questa risposta è stata votata come kazoo insieme all'accettazione è esattamente il motivo per cui consiglio alle persone di non usare stackoverflow. Anche ora con i commenti e gli aneddoti ho ancora persone in #django che si riferiscono a questo.
shangxiao

50

Versione breve

Si dovrebbe NON utilizzare loaddatail comando gestione direttamente in una migrazione di dati.

# 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),
    ]

Versione lunga

loaddatautilizza django.core.serializers.python.Deserializerquale utilizza i modelli più aggiornati per deserializzare i dati storici in una migrazione. Questo è un comportamento scorretto.

Ad esempio, si suppone che esista una migrazione dei dati che utilizza il loaddatacomando di gestione per caricare i dati da un dispositivo ed è già applicato al proprio ambiente di sviluppo.

Successivamente, decidi di aggiungere un nuovo campo obbligatorio al modello corrispondente, quindi lo fai ed esegui una nuova migrazione rispetto al tuo modello aggiornato (ed eventualmente fornisci un valore una tantum al nuovo campo quando ./manage.py makemigrationsti viene richiesto).

Esegui la prossima migrazione e tutto va bene.

Infine, hai finito di sviluppare la tua applicazione Django e la distribuisci sul server di produzione. Ora è il momento di eseguire tutte le migrazioni da zero nell'ambiente di produzione.

Tuttavia, la migrazione dei dati non riesce . Questo perché il modello deserializzato dal loaddatacomando, che rappresenta il codice corrente, non può essere salvato con dati vuoti per il nuovo campo obbligatorio aggiunto. L'apparecchio originale non dispone dei dati necessari per farlo!

Ma anche se aggiorni il dispositivo con i dati richiesti per il nuovo campo, la migrazione dei dati non riesce comunque . Quando la migrazione dei dati è in esecuzione, la migrazione successiva che aggiunge la colonna corrispondente al database non viene ancora applicata. Non puoi salvare i dati in una colonna che non esiste!

Conclusione: in una migrazione di dati, illoaddatacomando introduce potenziali incongruenze tra il modello e il database. NON dovresti assolutamenteusarlo direttamente in una migrazione dei dati.

La soluzione

loaddataIl comando si basa sulla django.core.serializers.python._get_modelfunzione per ottenere il modello corrispondente da un dispositivo, che restituirà la versione più aggiornata di un modello. Dobbiamo rattopparlo in modo che ottenga il modello storico.

(Il codice seguente funziona per 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, fai un punto molto forte. La tua risposta mi ha fatto pensare, tuttavia, la soluzione 2.1 dalla risposta di @ n__o / @ mlissner su cui si basa objects = serializers.deserialize('json', fixture, ignorenonexistent=True)soffrirebbe dello stesso problema di loaddata? O ignorenonexistent=Truecopre tutti i possibili problemi?
Dário

7
Se guardi la fonte , scoprirai che l' ignorenonexistent=Trueargomento ha due effetti: 1) ignora i modelli di un dispositivo che non sono nelle definizioni di modello più attuali, 2) ignora i campi di un modello di un dispositivo che non lo sono nella definizione del modello corrispondente più attuale. Nessuno di loro gestisce la nuova situazione del campo obbligatorio nel modello . Quindi, sì, penso che soffra lo stesso problema di pianura loaddata.
Rockallite

Ha funzionato alla grande una volta che ho capito che il mio vecchio json aveva modelli che facevano riferimento ad altri modelli utilizzando a natural_key(), che questo metodo non sembra supportare: ho appena sostituito il valore natural_key con l'ID effettivo del modello di riferimento.
dsummersl

1
Probabilmente questa risposta come risposta accettata sarebbe più utile, perché nell'esecuzione di testcase viene creato un nuovo database e tutte le migrazioni vengono applicate da zero. Questa soluzione risolve i problemi che un progetto con unittest dovrà affrontare in caso di non sostituzione di _get_model nella migrazione dei dati. Tnx
Mohammad ali baghershemirani

Grazie per l'aggiornamento e le spiegazioni, @Rockallite. La mia risposta iniziale è stata pubblicata poche settimane dopo che le migrazioni sono state introdotte in Django 1.7 e la documentazione su come procedere non era chiara (e lo è ancora, l'ultima volta che ho controllato). Si spera che Django aggiorni il meccanismo di caricamento / migrazione dei dati per tenere conto della cronologia del modello un giorno.
n__o

6

Ispirato da alcuni commenti (vale a dire i n__o) e dal fatto che ho molti initial_data.*file distribuiti su più app, ho deciso di creare un'app Django che facilitasse la creazione di queste migrazioni di dati.

Utilizzando django-migrazione-dispositivo che si può semplicemente eseguire il seguente comando di gestione e sarà la ricerca in tutto il vostro INSTALLED_APPSper initial_data.*i file e li trasformano in migrazione dei dati.

./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.

Vedi django-migration-fixture per le istruzioni di installazione / utilizzo.


2

Per fornire al tuo database alcuni dati iniziali, scrivi una migrazione dei dati. Nella migrazione dei dati, utilizza la funzione RunPython per caricare i tuoi dati.

Non scrivere alcun comando loaddata poiché questo metodo è deprecato.

Le migrazioni dei dati verranno eseguite solo una volta. Le migrazioni sono una sequenza ordinata di migrazioni. Quando vengono eseguite le migrazioni 003_xxxx.py, django migrations scrive nel database che questa app viene migrata fino a questa (003) ed eseguirà solo le seguenti migrazioni.


Quindi mi incoraggi a ripetere le chiamate a myModel.create(...)(o usando un ciclo) nella funzione RunPython?
Mickaël

praticamente sì. I database transazionali lo gestiranno perfettamente :)
FlogFR

1

Le soluzioni presentate sopra non hanno funzionato per me sfortunatamente. Ho scoperto che ogni volta che cambio i miei modelli devo aggiornare i miei proiettori. Idealmente, scriverei invece migrazioni di dati per modificare i dati creati e i dati caricati dal dispositivo in modo simile.

Per facilitare questo ho scritto una funzione rapida che cercherà nella fixturesdirectory dell'app corrente e caricherà un dispositivo. Metti questa funzione in una migrazione nel punto della cronologia del modello che corrisponde ai campi della migrazione.


Grazie per questo! Ho scritto una versione che funziona con Python 3 (e supera il nostro rigoroso Pylint). Puoi usarlo come fabbrica con RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley

1

Secondo me gli infissi sono un po 'brutti. Se il tuo database cambia frequentemente, tenerli aggiornati diventerà presto un incubo. In realtà, non è solo la mia opinione, nel libro "Two Scoops of Django" è spiegato molto meglio.

Invece scriverò un file Python per fornire la configurazione iniziale. Se hai bisogno di qualcosa in più ti suggerisco di guardare Factory boy .

Se è necessario migrare alcuni dati, utilizzare le migrazioni dei dati .

C'è anche "Burn Your Fixtures, Use Model Factories" sull'uso dei dispositivi.


1
Sono d'accordo sul tuo punto "difficile da mantenere se cambi frequenti", ma qui l'apparecchiatura mira solo a fornire dati iniziali (e minimi) durante l'installazione del progetto ...
Mickaël

1
Questo è per un carico di dati una tantum, che se fatto nel contesto delle migrazioni ha senso. Poiché se è all'interno di una migrazione, non è necessario apportare modifiche ai dati JSON. Eventuali modifiche allo schema che richiedono modifiche ai dati più avanti dovrebbero essere gestite tramite un'altra migrazione (a quel punto potrebbero essere altri dati nel database che dovranno essere modificati).
mtnpaul

0

Su Django 2.1, volevo caricare alcuni modelli (come i nomi dei paesi per esempio) con i dati iniziali.

Ma volevo che ciò accadesse automaticamente subito dopo l'esecuzione delle migrazioni iniziali.

Quindi ho pensato che sarebbe stato fantastico avere una sql/cartella all'interno di ciascuna applicazione che richiedesse il caricamento dei dati iniziali.

Quindi all'interno di quella sql/cartella avrei i .sqlfile con i DML richiesti per caricare i dati iniziali nei modelli corrispondenti, ad esempio:

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

Per essere più descrittivi, ecco come sql/apparirebbe un'app contenente una cartella: inserisci qui la descrizione dell'immagine

Inoltre ho trovato alcuni casi in cui avevo bisogno che gli sqlscript fossero eseguiti in un ordine specifico. Quindi ho deciso di anteporre ai nomi dei file un numero progressivo come mostrato nell'immagine sopra.

Quindi avevo bisogno di un modo per caricare automaticamente qualsiasi SQLsdisponibile all'interno di qualsiasi cartella dell'applicazione python manage.py migrate.

Così ho creato un'altra applicazione di nome initial_data_migrationse poi ho aggiunto questa applicazione per la lista dei INSTALLED_APPSnel settings.pyfile. Quindi ho creato una migrationscartella all'interno e ho aggiunto un file chiamato run_sql_scripts.py( che in realtà è una migrazione personalizzata ). Come si vede nell'immagine qui sotto:

inserisci qui la descrizione dell'immagine

Ho creato in run_sql_scripts.pymodo che si occupi di eseguire tutti gli sqlscript disponibili all'interno di ciascuna applicazione. Questo viene quindi licenziato quando qualcuno corre python manage.py migrate. Questa usanza migrationaggiunge anche le applicazioni coinvolte come dipendenze, in questo modo tenta di eseguire le sqlistruzioni solo dopo che le applicazioni richieste hanno eseguito le loro 0001_initial.pymigrazioni (non vogliamo tentare di eseguire un'istruzione SQL su una tabella inesistente).

Ecco la fonte di quello 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))
    ]

Spero che qualcuno lo trovi utile, ha funzionato perfettamente per me !. In caso di domande, non esitare a contattarmi.

NOTA: questa potrebbe non essere la soluzione migliore dato che sto appena iniziando con django, tuttavia volevo comunque condividere questo "How-to" con tutti voi poiché non ho trovato molte informazioni mentre cercavo su Google.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.