Jak migrować model z jednej aplikacji django do nowej?

126

Mam aplikację django z czterema modelami. Zdaję sobie teraz sprawę, że jeden z tych modeli powinien znajdować się w osobnej aplikacji. Mam południe zainstalowane na potrzeby migracji, ale nie sądzę, aby było to możliwe automatycznie. Jak mogę przenieść jeden z modeli ze starej aplikacji do nowej?

Pamiętaj też, że będę potrzebować tego, aby był to powtarzalny proces, aby móc migrować system produkcyjny i tym podobne.

Apreche
źródło
6
Dla django 1.7 i nowszych patrz stackoverflow.com/questions/25648393/…
Rick Westera

Odpowiedzi:

184

Jak migrować, korzystając z południa.

Powiedzmy, że mamy dwie aplikacje: wspólną i konkretną:

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

Teraz chcemy przenieść model common.models.cat do konkretnej aplikacji (dokładnie do specific.models.cat). Najpierw wprowadź zmiany w kodzie źródłowym, a następnie uruchom:

$ 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

Teraz musimy edytować oba pliki migracji:

#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

Teraz migracje obu aplikacji są świadome zmiany, a życie jest do bani tylko trochę mniej :-) Ustalenie tego związku między migracjami jest kluczem do sukcesu. Teraz, jeśli to zrobisz:

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

zrobi zarówno migrację, jak i

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

przeniesie rzeczy w dół.

Zauważ, że do aktualizacji schematu użyłem zwykłej aplikacji, a do obniżenia wersji użyłem konkretnej aplikacji. To dlatego, że działa tutaj zależność.

Potr Czachur
źródło
1
Wow, dzięki. Nauczyłem się południa na własną rękę, odkąd zadałem to pytanie, ale jestem pewien, że to bardzo pomoże innym.
Apreche
11
Konieczne może być również wykonanie migracji danych w tabeli django_content_type.
spookylukey
1
Naprawdę świetny przewodnik @Potr. Jestem ciekawy, czy nie powinno być również orm['contenttypes.contenttype'].objects.filter linii w tylnej części 0003_create_cat? Chcę również podzielić się wskazówką. Jeśli masz indeksy, również trzeba je zmodyfikować. W moim przypadku były to unikalne indeksy, więc mój napastnik wygląda tak: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher
2
Aby uzyskać dostęp orm['contenttypes.contenttype'], musisz również dodać --freeze contenttypesopcję do swoich schemamigrationpoleceń.
Gary,
1
W moim przypadku (Django 1.5.7 i South 1.0) .. Musiałem wpisać python manage.py schemamigration specific create_cat --auto --freeze commondostęp do modelu kota ze zwykłej aplikacji.
geoom
35

Budować na potr Czachur „s odpowiedzi , sytuacje, które wymagają ForeignKeys są bardziej skomplikowane i powinny być traktowane nieco inaczej.

(Poniższy przykład opiera się na aplikacjach commoni, specificdo których odwołuje się w bieżącej odpowiedzi).

# common/models.py

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

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

zmieniłby się na

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Bieganie

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

wygeneruje następujące migracje (celowo ignoruję zmiany Django ContentType - zobacz poprzednio przywoływaną odpowiedź, jak sobie z tym poradzić):

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

Jak widać, FK musi zostać zmieniony, aby odwoływał się do nowej tabeli. Musimy dodać zależność tak, że wiemy w jakiej kolejności zostaną zastosowane migracje (a więc, że tabela będzie istnieć, zanim spróbujemy dodać FK do niego), ale musimy również upewnić się, walcówka działa wstecz, ponieważ zbyt zależność ma zastosowanie w odwrotnym kierunku .

# 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

Zgodnie z dokumentacją South , depends_onzapewni, że będzie 0004_auto__add_catdziałać wcześniej 0009_auto__del_cat podczas migracji do przodu, ale w odwrotnej kolejności podczas migracji do tyłu . Jeśli wyszliśmy db.rename_table('specific_cat', 'common_cat')w specificwycofywania, commonwycofywania zawiedzie podczas próby migracji ForeignKey ponieważ tabela odwołuje tabela nie istnieje.

Miejmy nadzieję, że jest to bliższe sytuacji „prawdziwego świata” niż istniejące rozwiązania i ktoś uzna to za pomocne. Twoje zdrowie!

Matt Briançon
źródło
Naprawione źródła w tej odpowiedzi pomijają wiersze do aktualizacji typów zawartości, które są obecne w odpowiedzi Potr Czachur. To może być mylące.
Shai Berger,
@ShaiBerger Odniosłem się do tego konkretnie: „Celowo ignoruję zmiany typu ContentType w Django - zobacz wcześniej wymienioną odpowiedź, jak sobie z tym poradzić”.
Matt Briançon,
9

Modele nie są ściśle powiązane z aplikacjami, więc przenoszenie jest dość proste. Django używa nazwy aplikacji w nazwie tabeli bazy danych, więc jeśli chcesz przenieść swoją aplikację, możesz zmienić nazwę tabeli bazy danych za pomocą ALTER TABLEinstrukcji SQL lub - nawet prościej - po prostu użyć db_tableparametru w Metaklasie swojego modelu, aby odwołać się do stara nazwa.

Jeśli do tej pory korzystałeś z ContentTypes lub relacji ogólnych w dowolnym miejscu w kodzie, prawdopodobnie będziesz chciał zmienić nazwę app_labeltypu contenttype wskazującego na model, który się porusza, aby zachować istniejące relacje.

Oczywiście, jeśli w ogóle nie masz żadnych danych do zachowania, najłatwiej jest całkowicie usunąć tabele bazy danych i uruchomić je ./manage.py syncdbponownie.

Daniel Roseman
źródło
2
Jak mam to zrobić z migracją na południe?
Apreche
4

Oto jeszcze jedna poprawka do doskonałego rozwiązania Potra. Dodaj następujące elementy do pliku specific / 0003_create_cat

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

Chyba że zależność ta jest ustawiona Południowa nie zagwarantuje, że common_cattabela istnieje w czasie, gdy specyficzny / 0003_create_cat uruchomieniu rzuca django.db.utils.OperationalError: no such table: common_catbłąd na ciebie.

South uruchamia migracje w porządku leksykograficznym, chyba że zależność jest jawnie ustawiona. Ponieważ commonnastępuje to, zanim specificwszystkie commonmigracje zostaną uruchomione przed zmianą nazwy tabeli, więc prawdopodobnie nie zostanie odtworzony w oryginalnym przykładzie pokazanym przez Potr. Ale jeśli zmiana nazwy commonna app2i specificdo app1trafisz na ten problem.

Ihor Kaharlichenko
źródło
W rzeczywistości nie stanowi to problemu w przypadku przykładu Potra. Używa określonej migracji do zmiany nazwy, a wspólnej migracji zależy od konkretnej. Jeśli konkretny jest uruchamiany jako pierwszy, wszystko jest w porządku. Jeśli common zostanie uruchomione jako pierwsze, zależność spowoduje wykonanie określonego uruchomienia przed nim. To powiedziawszy, zamieniłem kolejność podczas robienia tego, więc zmiana nazwy była wspólna, a zależność w konkretnym, a następnie musisz zmienić zależność, jak opisałeś powyżej.
Emil Stenström
1
Nie mogę się z tobą zgodzić. Z mojego punktu widzenia rozwiązanie powinno być solidne i działać bez prób zadowalania go. Oryginalne rozwiązanie nie działa, jeśli zaczniesz od nowej bazy danych i pliku syncdb / migrate. Moja propozycja to rozwiązuje. Tak czy inaczej, odpowiedź Porta zaoszczędziła mi dużo czasu, uznanie dla niego :)
Ihor Kaharlichenko
Niezrobienie tego może również spowodować niepowodzenie testów (zawsze wydaje się, że podczas tworzenia testowej bazy danych zawsze uruchamiają pełne migracje na południe). Zrobiłem już wcześniej coś podobnego. Dobry chwyt Ihor :)
odinho - Velmont
4

Proces, na którym się obecnie zdecydowałem, ponieważ wróciłem tu kilka razy i postanowiłem go sformalizować.

Ten został wybudowany na odpowiedź potr Czachur za i odpowiedzi Matt Briancon , używając Południowa 0.8.4

Krok 1. Odkryj relacje podrzędnego klucza obcego

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

Tak więc w tym rozszerzonym przypadku odkryliśmy inny powiązany model, taki jak:

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

    ...

Krok 2. Utwórz migracje

# 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

Krok 3. Kontrola źródła: Zatwierdź dotychczasowe zmiany.

Sprawia, że ​​jest to bardziej powtarzalny proces, jeśli napotkasz konflikty scalania, takie jak koledzy z zespołu piszący migracje w zaktualizowanych aplikacjach.

Krok 4. Dodaj zależności między migracjami.

Zasadniczo create_kittycatzależy od aktualnego stanu wszystkiego, a wszystko od tego zależy 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'),
    )
    ...

Krok 5. Zmiana nazwy tabeli, którą chcemy wprowadzić.

# 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

Krok 6. Tylko jeśli potrzebujesz backwards () do pracy ORAZ uzyskać KeyError działający wstecz.

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

Krok 7. Przetestuj - to, co działa u mnie, może nie wystarczyć w Twojej prawdziwej sytuacji :)

python manage.py migrate

# If you need backwards to work
python manage.py migrate specific <the_one_before_create_kittycat>
pzrq
źródło
3

Więc użycie oryginalnej odpowiedzi od @Potr powyżej nie zadziałało dla mnie w South 0.8.1 i Django 1.5.1. Piszę poniżej, co zadziałało dla mnie, mając nadzieję, że będzie to pomocne dla innych.

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
źródło
1

Podam bardziej wyraźną wersję jednej z rzeczy, które Daniel Roseman zasugerował w swojej odpowiedzi ...

Jeśli po prostu zmienisz db_tableatrybut Meta modelu, który przeniosłeś, tak, aby wskazywał na istniejącą nazwę tabeli (zamiast nowej nazwy, którą Django nadałby, gdybyś porzucił i zrobił a syncdb), możesz uniknąć skomplikowanych migracji na południe. na przykład:

Oryginalny:

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

Po przeprowadzce:

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

Teraz wystarczy przeprowadzić migrację danych, aby zaktualizować app_labeldla MyModelw django_content_typetabeli i powinno być dobrze ...

Uruchom, ./manage.py datamigration django update_content_typea następnie edytuj plik utworzony przez South:

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()
Anentropowy
źródło