Come faccio a migrare un modello da un'app django a una nuova?


126

Ho un'app django con quattro modelli. Mi rendo conto ora che uno di questi modelli dovrebbe essere in un'app separata. Ho installato South per le migrazioni, ma non credo sia qualcosa che può gestire automaticamente. Come posso migrare uno dei modelli dalla vecchia app in una nuova?

Inoltre, tieni presente che avrò bisogno che questo sia un processo ripetibile, in modo da poter migrare il sistema di produzione e simili.


6
Per Django 1.7 e soprattutto vedere stackoverflow.com/questions/25648393/...
Rick Westera

Risposte:


184

Come migrare usando il sud.

Diciamo che abbiamo due app: comuni e specifiche:

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   `-- 0002_create_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   `-- 0002_create_dog.py
    `-- models.py

Ora vogliamo spostare il modello common.models.cat in un'app specifica (precisamente in specific.models.cat). Apportare innanzitutto le modifiche al codice sorgente e quindi eseguire:

$ python manage.py schemamigration specific create_cat --auto
 + Added model 'specific.cat'
$ python manage.py schemamigration common drop_cat --auto
 - Deleted model 'common.cat'

myproject/
|-- common
|   |-- migrations
|   |   |-- 0001_initial.py
|   |   |-- 0002_create_cat.py
|   |   `-- 0003_drop_cat.py
|   `-- models.py
`-- specific
    |-- migrations
    |   |-- 0001_initial.py
    |   |-- 0002_create_dog.py
    |   `-- 0003_create_cat.py
    `-- models.py

Ora dobbiamo modificare entrambi i file di migrazione:

#0003_create_cat: replace existing forward and backward code
#to use just one sentence:

def forwards(self, orm):
    db.rename_table('common_cat', 'specific_cat') 

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='common',
            model='cat',
        ).update(app_label='specific')

def backwards(self, orm):
    db.rename_table('specific_cat', 'common_cat')

    if not db.dry_run:
        # For permissions to work properly after migrating
        orm['contenttypes.contenttype'].objects.filter(
            app_label='specific',
            model='cat',
        ).update(app_label='common')

#0003_drop_cat:replace existing forward and backward code
#to use just one sentence; add dependency:

depends_on = (
    ('specific', '0003_create_cat'),
)
def forwards(self, orm):
    pass
def backwards(self, orm):
    pass

Ora entrambe le migrazioni delle app sono consapevoli del cambiamento e la vita fa schifo solo un po 'meno :-) Impostare questa relazione tra le migrazioni è la chiave del successo. Ora se lo fai:

python manage.py migrate common
 > specific: 0003_create_cat
 > common: 0003_drop_cat

farà entrambe le migrazioni, e

python manage.py migrate specific 0002_create_dog
 < common: 0003_drop_cat
 < specific: 0003_create_cat

migrerà le cose verso il basso.

Si noti che per l'aggiornamento dello schema ho usato un'app comune e per il downgrade ho usato un'app specifica. Questo perché la dipendenza qui funziona.


1
Wow grazie. Ho imparato a sud da solo da quando ho posto questa domanda, ma sono sicuro che questo aiuterà molto gli altri.
Apreche,

11
Potrebbe anche essere necessario eseguire migrazioni di dati nella tabella django_content_type.
spookylukey,

1
Davvero un'ottima guida @Potr. Sono curioso, non dovrebbe esserci anche una orm['contenttypes.contenttype'].objects.filter linea nella parte arretrata 0003_create_cat? Anche io voglio condividere un suggerimento. Se si dispone di indici, anche questi dovranno essere modificati. Nel mio caso erano indici univoci, quindi il mio attaccante sembra questo: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher,

2
Per accedere orm['contenttypes.contenttype'], devi anche aggiungere l' --freeze contenttypesopzione ai tuoi schemamigrationcomandi.
Gary,

1
Nel mio caso (Django 1.5.7 e South 1.0) .. Ho dovuto digitare python manage.py schemamigration specific create_cat --auto --freeze commonper accedere al modello di gatto dall'app comune.
geoom,

35

Per costruire il Potr Czachur s' risposta , situazioni che coinvolgono ForeignKeys sono più complicate e devono essere maneggiati in modo leggermente diverso.

(L'esempio seguente si basa sulle app commone specificcui si fa riferimento nella risposta corrente).

# common/models.py

class Cat(models.Model):
    # ...

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

cambierebbe quindi in

# common/models.py

from specific.models import Cat

class Toy(models.Model):
    belongs_to = models.ForeignKey(Cat)
    # ...

# specific/models.py

class Cat(models.Model):
    # ...

In esecuzione

./manage.py schemamigration common --auto
./manage.py schemamigration specific --auto # or --initial

genererebbe le seguenti migrazioni (sto ignorando intenzionalmente le modifiche di Django ContentType — vedi la risposta precedentemente citata per come gestirla):

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.delete_table('common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.create_table('common_cat', (
            # ...
        ))
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.create_table('specific_cat', (
            # ...
        ))

    def backwards(self, orm):
        db.delete_table('specific_cat')

Come puoi vedere, l'FK deve essere modificato per fare riferimento alla nuova tabella. Dobbiamo aggiungere una dipendenza in modo da conoscere l'ordine in cui verranno applicate le migrazioni (e quindi che la tabella esisterà prima di provare ad aggiungere un FK ad essa) ma dobbiamo anche assicurarci che anche il rollback indietro funzioni anche perché il la dipendenza si applica nella direzione opposta .

# common/migrations/0009_auto__del_cat.py

class Migration(SchemaMigration):

    depends_on = (
        ('specific', '0004_auto__add_cat'),
    )

    def forwards(self, orm):
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.Cat']))

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))

# specific/migrations/0004_auto__add_cat.py

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat')

    def backwards(self, orm):
        pass

Secondo la documentazione del Sud , depends_ongarantirà che venga 0004_auto__add_cateseguita prima 0009_auto__del_cat durante la migrazione in avanti ma nell'ordine opposto durante la migrazione all'indietro . Se si lasciasse db.rename_table('specific_cat', 'common_cat')il specificrollback, il commonrollback fallirebbe quando si tenta di migrare ForeignKey perché la tabella a cui fa riferimento la tabella non esisterebbe.

Speriamo che questo sia più vicino a una situazione del "mondo reale" rispetto alle soluzioni esistenti e qualcuno lo troverà utile. Saluti!


Le fonti fisse in questa risposta omettono le linee per l'aggiornamento dei tipi di contenuto, che sono presenti nella risposta di Potr Czachur. Questo potrebbe essere fuorviante.
Shai Berger,

@ShaiBerger L'ho affrontato specificatamente: "Sto ignorando intenzionalmente le modifiche di Django ContentType — vedi la risposta precedentemente citata per come gestirla".
Matt Briançon,

9

I modelli non sono strettamente accoppiati alle app, quindi lo spostamento è abbastanza semplice. Django utilizza il nome dell'app nel nome della tabella del database, quindi se si desidera spostare l'app è possibile rinominare la tabella del database tramite un'istruzione SQL ALTER TABLEo, ancora più semplice, utilizzare il db_tableparametro nella Metaclasse del modello per fare riferimento al vecchio nome.

Se finora hai utilizzato ContentTypes o relazioni generiche in qualsiasi punto del codice, probabilmente vorrai rinominare il tipo app_labeldi contenuto che punta al modello che si sta spostando, in modo da preservare le relazioni esistenti.

Naturalmente, se non si dispone di alcun dato da conservare, la cosa più semplice da fare è eliminare completamente le tabelle del database ed eseguirle ./manage.py syncdbnuovamente.


2
Come posso farlo con una migrazione a sud?
Apreche,

4

Ecco un'altra soluzione all'eccellente soluzione di Potr. Aggiungi quanto segue a specific / 0003_create_cat

depends_on = (
    ('common', '0002_create_cat'),
)

A meno che questa dipendenza non sia impostata, South non garantirà l' common_catesistenza della tabella nel momento in cui viene eseguito specific / 0003_create_cat , generando un django.db.utils.OperationalError: no such table: common_caterrore.

Il sud gestisce le migrazioni in ordine lessicografico, a meno che la dipendenza non sia esplicitamente impostata. Dato che commonviene prima che specifictutte le commonmigrazioni vengano eseguite prima della ridenominazione della tabella, quindi probabilmente non si riprodurrebbe nell'esempio originale mostrato da Potr. Ma se rinominate commonin app2e specificper app1voi si imbatterà in questo problema.


Questo non è in realtà un problema con l'esempio di Potr. Utilizza la migrazione specifica per rinominare e la migrazione comune per dipendere da quella specifica. Se specifico viene eseguito per primo, stai bene. Se common viene eseguito per primo, la dipendenza eseguirà l'esecuzione specifica prima di esso. Detto questo, ho scambiato l'ordine durante questa operazione, quindi la ridenominazione è avvenuta in comune e la dipendenza in modo specifico, quindi è necessario modificare la dipendenza come descritto sopra.
Emil Stenström,

1
Non posso essere d'accordo con te. Dal mio punto di vista, la soluzione dovrebbe essere robusta e funzionare senza cercare di soddisfarla. La soluzione originale non funziona se si parte da un nuovo db e da syncdb / migrate. La mia proposta lo risolve. Ad ogni modo, la risposta di Port mi ha fatto risparmiare un sacco di tempo, complimenti a lui :)
Ihor Kaharlichenko,

In caso contrario, anche i test potrebbero fallire (sembrano sempre eseguire migrazioni a sud complete durante la creazione del database dei test). Ho fatto qualcosa di simile prima. Buona cattura Ihor :)
odinho - Velmont

4

Il processo che ho attualmente avviato da quando sono tornato qui alcune volte e ho deciso di formalizzarlo.

Questo è stato originariamente costruito sulla risposta di Potr Czachur e la risposta di Matt Briançon , utilizzando Sud 0.8.4

Passaggio 1. Scopri le relazioni chiave esterna figlio

# Caution: This finds OneToOneField and ForeignKey.
# I don't know if this finds all the ways of specifying ManyToManyField.
# Hopefully Django or South throw errors if you have a situation like that.
>>> Cat._meta.get_all_related_objects()
[<RelatedObject: common:toy related to cat>,
 <RelatedObject: identity:microchip related to cat>]

Quindi, in questo caso esteso, abbiamo scoperto un altro modello correlato come:

# Inside the "identity" app...
class Microchip(models.Model):

    # In reality we'd probably want a ForeignKey, but to show the OneToOneField
    identifies = models.OneToOneField(Cat)

    ...

Passaggio 2. Creare migrazioni

# Create the "new"-ly renamed model
# Yes I'm changing the model name in my refactoring too.
python manage.py schemamigration specific create_kittycat --auto

# Drop the old model
python manage.py schemamigration common drop_cat --auto

# Update downstream apps, so South thinks their ForeignKey(s) are correct.
# Can skip models like Toy if the app is already covered
python manage.py schemamigration identity update_microchip_fk --auto

Passaggio 3. Controllo del codice sorgente: commit delle modifiche finora.

Rende un processo più ripetibile se si incontrano conflitti di unione come i compagni di squadra che scrivono migrazioni sulle app aggiornate.

Passaggio 4. Aggiungere dipendenze tra le migrazioni.

Fondamentalmente create_kittycatdipende dallo stato attuale di tutto, e quindi tutto dipende da create_kittycat.

# create_kittycat
class Migration(SchemaMigration):

    depends_on = (
        # Original model location
        ('common', 'the_one_before_drop_cat'),

        # Foreign keys to models not in original location
        ('identity', 'the_one_before_update_microchip_fk'),
    )
    ...


# drop_cat
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...


# update_microchip_fk
class Migration(SchemaMigration):

    depends_on = (
        ('specific', 'create_kittycat'),
    )
    ...

Passaggio 5. La tabella rinomina la modifica che vogliamo apportare.

# create_kittycat
class Migration(SchemaMigration):

    ...

    # Hopefully for create_kittycat you only need to change the following
    # 4 strings to go forward cleanly... backwards will need a bit more work.
    old_app = 'common'
    old_model = 'cat'
    new_app = 'specific'
    new_model = 'kittycat'

    # You may also wish to update the ContentType.name,
    # personally, I don't know what its for and
    # haven't seen any side effects from skipping it.

    def forwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.old_app, self.old_model),
            '%s_%s' % (self.new_app, self.new_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.old_app,
                model=self.old_model,
            ).update(
                app_label=self.new_app,
                model=self.new_model,
            )

        # Going forwards, should be no problem just updating child foreign keys
        # with the --auto in the other new South migrations

    def backwards(self, orm):

        db.rename_table(
            '%s_%s' % (self.new_app, self.new_model),
            '%s_%s' % (self.old_app, self.old_model),
        )

        if not db.dry_run:
            # For permissions, GenericForeignKeys, etc to work properly after migrating.
            orm['contenttypes.contenttype'].objects.filter(
                app_label=self.new_app,
                model=self.new_model,
            ).update(
                app_label=self.old_app,
                model=self.old_model,
            )

        # Going backwards, you probably should copy the ForeignKey
        # db.alter_column() changes from the other new migrations in here
        # so they run in the correct order.
        #
        # Test it! See Step 6 for more details if you need to go backwards.
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['common.Cat']))
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['common.Cat']))


# drop_cat
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Remove the db.delete_table(), if you don't at Step 7 you'll likely get
        # "django.db.utils.ProgrammingError: table "common_cat" does not exist"

        # Leave existing db.alter_column() statements here
        db.alter_column('common_toy', 'belongs_to_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass


# update_microchip_fk
class Migration(SchemaMigration):

    ...

    def forwards(self, orm):
        # Leave existing db.alter_column() statements here
        db.alter_column('identity_microchip', 'identifies_id', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['specific.KittyCat']))

    def backwards(self, orm):
        # Copy/paste the auto-generated db.alter_column()
        # into the create_kittycat migration if you need backwards to work.
        pass

Passaggio 6. Solo se hai bisogno di backward () per funzionare E far funzionare un KeyError all'indietro.

# the_one_before_create_kittycat
class Migration(SchemaMigration):

    # You many also need to add more models to South's FakeORM if you run into
    # more KeyErrors, the trade-off chosen was to make going forward as easy as
    # possible, as that's what you'll probably want to do once in QA and once in
    # production, rather than running the following many times:
    #
    # python manage.py migrate specific <the_one_before_create_kittycat>

    models = {
        ...
        # Copied from 'identity' app, 'update_microchip_fk' migration
        u'identity.microchip': {
            'Meta': {'object_name': 'Microchip'},
            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
            'identifies': ('django.db.models.fields.related.OneToOneField', [], {to=orm['specific.KittyCat']})
        },
        ...
    }

Passaggio 7. Provalo - ciò che funziona per me potrebbe non essere sufficiente per la tua situazione di vita reale :)

python manage.py migrate

# If you need backwards to work
python manage.py migrate specific <the_one_before_create_kittycat>

3

Quindi usare la risposta originale di @Potr sopra non ha funzionato per me su South 0.8.1 e Django 1.5.1. Sto pubblicando ciò che ha funzionato per me di seguito nella speranza che sia utile per gli altri.

from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

    def forwards(self, orm):
        db.rename_table('common_cat', 'specific_cat') 

        if not db.dry_run:
             db.execute(
                "update django_content_type set app_label = 'specific' where "
                " app_label = 'common' and model = 'cat';")

    def backwards(self, orm):
        db.rename_table('specific_cat', 'common_cat')
            db.execute(
                "update django_content_type set app_label = 'common' where "
                " app_label = 'specific' and model = 'cat';")

1

Darò una versione più esplicita di una delle cose suggerite da Daniel Roseman nella sua risposta ...

Se modifichi l' db_tableattributo Meta del modello che hai spostato per puntare al nome della tabella esistente (invece del nuovo nome che Django gli darebbe se cadessi e facessi a syncdb) allora puoi evitare complicate migrazioni verso sud. per esempio:

Originale:

# app1/models.py
class MyModel(models.Model):
    ...

Dopo lo spostamento:

# app2/models.py
class MyModel(models.Model):
    class Meta:
        db_table = "app1_mymodel"

Ora devi solo eseguire una migrazione dei dati per aggiornare il app_labelfor MyModelnella django_content_typetabella e dovresti essere pronto per andare ...

Esegui ./manage.py datamigration django update_content_typequindi modifica il file che South crea per te:

def forwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app1', model='mymodel')
    moved.app_label = 'app2'
    moved.save()

def backwards(self, orm):
    moved = orm.ContentType.objects.get(app_label='app2', model='mymodel')
    moved.app_label = 'app1'
    moved.save()
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.