Wie migriere ich ein Modell aus einer Django-App in eine neue?

126

Ich habe eine Django-App mit vier Modellen. Mir ist jetzt klar, dass eines dieser Modelle in einer separaten App sein sollte. Ich habe South für Migrationen installiert, aber ich glaube nicht, dass dies automatisch erledigt werden kann. Wie kann ich eines der Modelle aus der alten App in ein neues migrieren?

Denken Sie auch daran, dass dies ein wiederholbarer Prozess sein muss, damit ich das Produktionssystem und dergleichen migrieren kann.

Apreche
quelle
6
Für Django 1.7 und höher siehe stackoverflow.com/questions/25648393/…
Rick Westera

Antworten:

184

Wie man mit Süden migriert.

Nehmen wir an, wir haben zwei Apps: allgemein und spezifisch:

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

Jetzt möchten wir das Modell common.models.cat in eine bestimmte App verschieben (genau in bestimmte.models.cat). Nehmen Sie zuerst die Änderungen im Quellcode vor und führen Sie dann Folgendes aus:

$ 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

Jetzt müssen wir beide Migrationsdateien bearbeiten:

#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

Jetzt sind sich beide Apps-Migrationen der Veränderung bewusst und das Leben ist nur ein bisschen weniger schlecht :-) Das Festlegen dieser Beziehung zwischen Migrationen ist der Schlüssel zum Erfolg. Wenn Sie dies tun:

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

wird sowohl Migration als auch durchführen

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

wird Dinge nach unten migrieren.

Beachten Sie, dass ich für das Upgrade des Schemas eine allgemeine App und für das Downgrade eine bestimmte App verwendet habe. Das liegt daran, wie die Abhängigkeit hier funktioniert.

Potr Czachur
quelle
1
Wow, danke. Seit ich diese Frage gestellt habe, habe ich alleine nach Süden gelernt, aber ich bin mir sicher, dass dies anderen sehr helfen wird.
Apreche
11
Möglicherweise müssen Sie auch Migrationen von Daten in der Tabelle django_content_type durchführen.
Spookylukey
1
Wirklich tolle Anleitung @Potr. Ich bin neugierig, sollte es nicht auch eine orm['contenttypes.contenttype'].objects.filter Linie im rückwärtigen Teil von geben 0003_create_cat? Auch ich möchte einen Tipp teilen. Wenn Sie Indizes haben, müssen diese ebenfalls geändert werden. In meinem Fall handelte es sich um eindeutige Indizes, daher sieht mein Forward wie folgt aus:db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher,
2
Um darauf zugreifen zu können orm['contenttypes.contenttype'], müssen Sie --freeze contenttypesIhren schemamigrationBefehlen auch die Option hinzufügen .
Gary
1
In meinem Fall (Django 1.5.7 und South 1.0) musste ich tippen python manage.py schemamigration specific create_cat --auto --freeze common, um über eine gemeinsame App auf das Katzenmodell zuzugreifen.
Geoom
35

Um bauen auf Potr Czachur ‚s Antwort , Situationen , die Foreign einzubeziehen sind komplizierter und sollte etwas anders gehandhabt werden.

(Das folgende Beispiel baut auf den commonund specificApps auf, auf die in der aktuellen Antwort verwiesen wird.)

# common/models.py

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

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

würde dann zu ändern

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Laufen

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

würde die folgenden Migrationen generieren (ich ignoriere absichtlich Django ContentType-Änderungen - siehe Antwort, auf die zuvor verwiesen wurde, um damit umzugehen):

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

Wie Sie sehen können, muss der FK geändert werden, um auf die neue Tabelle zu verweisen. Wir müssen eine Abhängigkeit hinzufügen, damit wir die Reihenfolge kennen, in der die Migrationen angewendet werden (und damit die Tabelle vorhanden ist, bevor wir versuchen, eine FK hinzuzufügen), aber wir müssen auch sicherstellen, dass das Zurückrollen auch funktioniert, da die Die Abhängigkeit gilt in umgekehrter Richtung .

# 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

Per der Dokumentation Süd , depends_onwird sichergestellt , dass 0004_auto__add_catläuft , bevor Sie 0009_auto__del_cat bei der Migration nach vorn , aber in der umgekehrten Reihenfolge beim Rückwärts Migration . Wenn wir db.rename_table('specific_cat', 'common_cat')das specificRollback belassen , commonschlägt das Rollback fehl, wenn versucht wird, den ForeignKey zu migrieren, da die Tabelle, auf die die Tabelle verweist, nicht vorhanden wäre.

Hoffentlich ist dies näher an einer "realen" Situation als die vorhandenen Lösungen, und jemand wird dies hilfreich finden. Prost!

Matt Briançon
quelle
In den festen Quellen in dieser Antwort fehlen die Zeilen zum Aktualisieren der Inhaltstypen, die in der Antwort von Potr Czachur enthalten sind. Dies könnte irreführend sein.
Shai Berger
@ShaiBerger Ich habe das speziell angesprochen: "Ich ignoriere absichtlich Änderungen am Django ContentType. Informationen dazu finden Sie in der zuvor genannten Antwort."
Matt Briançon
9

Modelle sind nicht sehr eng mit Apps verbunden, daher ist das Verschieben ziemlich einfach. Django verwendet den App-Namen im Namen der Datenbanktabelle. Wenn Sie also Ihre App verschieben möchten, können Sie die Datenbanktabelle entweder über eine SQL- ALTER TABLEAnweisung umbenennen oder - noch einfacher - einfach den db_tableParameter in der MetaKlasse Ihres Modells verwenden , um auf die zu verweisen alte Bezeichnung.

Wenn Sie bisher irgendwo in Ihrem Code ContentTypes oder generische Relationen verwendet haben, möchten Sie wahrscheinlich app_labelden Content-Typ umbenennen, der auf das sich bewegende Modell verweist, damit vorhandene Relationen erhalten bleiben.

Wenn Sie überhaupt keine Daten zum Speichern haben, ist es natürlich am einfachsten, die Datenbanktabellen vollständig zu löschen und ./manage.py syncdberneut auszuführen .

Daniel Roseman
quelle
2
Wie mache ich das mit einer Südwanderung?
Apreche
4

Hier ist eine weitere Lösung für Potrs hervorragende Lösung. Fügen Sie Folgendes zu bestimmten / 0003_create_cat hinzu

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

Wenn diese Abhängigkeit nicht festgelegt ist, kann South nicht garantieren, dass die common_catTabelle zum Zeitpunkt der Ausführung von spezifischem / 0003_create_cat vorhanden ist, und wirft einen django.db.utils.OperationalError: no such table: common_catFehler auf Sie.

South führt Migrationen in lexikografischer Reihenfolge durch, sofern die Abhängigkeit nicht explizit festgelegt ist. Da commonvor kommt specificall commonMigrationen s‘würde , bevor Tabelle Umbenennung laufen lassen, so wäre es wahrscheinlich nicht im ursprünglichen Beispiel durch Potr gezeigt reproduzieren. Aber wenn Sie umbenennen commonzu app2und specificzu app1werden Sie in dieses Problem.

Ihor Kaharlichenko
quelle
Dies ist eigentlich kein Problem mit dem Beispiel des Potr. Es verwendet die spezifische Migration zum Umbenennen und die allgemeine Migration, um von der spezifischen abhängig zu sein. Wenn bestimmte zuerst ausgeführt werden, ist alles in Ordnung. Wenn common zuerst ausgeführt wird, wird die Abhängigkeit zuvor ausgeführt. Das heißt, ich habe dabei die Reihenfolge vertauscht, sodass die Umbenennung gemeinsam und die Abhängigkeit spezifisch erfolgte. Anschließend müssen Sie die Abhängigkeit wie oben beschrieben ändern.
Emil Stenström
1
Ich kann dir nicht zustimmen. Aus meiner Sicht sollte die Lösung robust sein und funktionieren, ohne zu versuchen, sie zufrieden zu stellen. Die ursprüngliche Lösung funktioniert nicht, wenn Sie von einer neuen Datenbank und der Syncdb / Migration starten. Mein Vorschlag behebt es. So oder so, Port's Antwort hat mir viel Zeit gespart, ein großes Lob an ihn :)
Ihor Kaharlichenko
Wenn Sie dies nicht tun, können auch Tests fehlschlagen (sie scheinen beim Erstellen ihrer Testdatenbank immer vollständige Südmigrationen auszuführen). Ich habe schon etwas Ähnliches gemacht. Guter Fang Ihor :)
Odinho - Velmont
4

Der Prozess, auf den ich mich derzeit festgelegt habe, seit ich ein paar Mal hier war und beschlossen habe, ihn zu formalisieren.

Dies wurde ursprünglich gebaut auf Potr Czachur Antwort und Matt Briançon Antwort , mit Süd 0.8.4

Schritt 1. Ermitteln Sie untergeordnete Fremdschlüsselbeziehungen

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

In diesem erweiterten Fall haben wir ein anderes verwandtes Modell entdeckt, wie:

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

    ...

Schritt 2. Erstellen Sie Migrationen

# 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

Schritt 3. Quellcodeverwaltung: Bisherige Änderungen festschreiben.

Dies macht den Vorgang wiederholbarer, wenn Sie auf Zusammenführungskonflikte stoßen, z. B. wenn Teamkollegen Migrationen auf die aktualisierten Apps schreiben.

Schritt 4. Fügen Sie Abhängigkeiten zwischen den Migrationen hinzu.

Grundsätzlich create_kittycathängt es vom aktuellen Zustand von allem ab, und dann hängt alles davon ab 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'),
    )
    ...

Schritt 5. Die Änderung zum Umbenennen der Tabelle, die wir vornehmen möchten.

# 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

Schritt 6. Nur wenn Sie backwards () benötigen, um zu arbeiten UND einen KeyError rückwärts laufen zu lassen.

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

Schritt 7. Testen Sie es - was für mich funktioniert, reicht möglicherweise nicht für Ihre reale Lebenssituation aus :)

python manage.py migrate

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

Die Verwendung der ursprünglichen Antwort von @Potr oben funktionierte für mich unter South 0.8.1 und Django 1.5.1 nicht. Ich poste unten, was für mich funktioniert hat, in der Hoffnung, dass es für andere hilfreich ist.

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';")
Tim Sutton
quelle
1

Ich werde eine explizitere Version eines der Dinge geben, die Daniel Roseman in seiner Antwort vorgeschlagen hat ...

Wenn Sie nur das db_tableMeta-Attribut des Modells ändern, das Sie verschoben haben, um auf den vorhandenen Tabellennamen zu verweisen (anstelle des neuen Namens, den Django ihm geben würde, wenn Sie ihn fallen lassen und a ausführen syncdb), können Sie komplizierte Südmigrationen vermeiden. z.B:

Original:

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

Nach dem Umzug:

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

Jetzt müssen Sie nur noch eine Datenmigration durchführen, um das app_labelfor MyModelin der django_content_typeTabelle zu aktualisieren, und schon kann es losgehen ...

Führen Sie ./manage.py datamigration django update_content_typedie von South für Sie erstellte Datei aus und bearbeiten Sie sie:

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()
Anentropisch
quelle