Comment migrer un modèle d'une application django vers une nouvelle?

126

J'ai une application django avec quatre modèles. Je réalise maintenant que l'un de ces modèles devrait être dans une application distincte. J'ai installé le sud pour les migrations, mais je ne pense pas que ce soit quelque chose qu'il puisse gérer automatiquement. Comment puis-je migrer l'un des modèles de l'ancienne application vers une nouvelle?

Aussi, gardez à l'esprit que je vais avoir besoin que ce soit un processus répétable, afin que je puisse migrer le système de production et autres.

Apreche
la source
6
Pour django 1.7 et supérieur, voir stackoverflow.com/questions/25648393/…
Rick Westera

Réponses:

184

Comment migrer en utilisant le sud.

Disons que nous avons deux applications: communes et spécifiques:

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

Maintenant, nous voulons déplacer le modèle common.models.cat vers une application spécifique (précisément vers specific.models.cat). Effectuez d'abord les modifications dans le code source, puis exécutez:

$ 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

Nous devons maintenant modifier les deux fichiers de migration:

#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

Maintenant, les deux migrations d'applications sont conscientes du changement et la vie est un peu moins pénible :-) L'établissement de cette relation entre les migrations est la clé du succès. Maintenant si vous faites:

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

effectuera à la fois la migration et

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

va migrer les choses vers le bas.

Notez que pour la mise à niveau du schéma, j'ai utilisé une application commune et pour la rétrogradation, j'ai utilisé une application spécifique. C'est parce que comment fonctionne la dépendance ici.

Potr Czachur
la source
1
Ouah merci. J'ai appris le sud par moi-même depuis que j'ai posé cette question, mais je suis sûr que cela aidera grandement les autres.
Apreche
11
Vous devrez peut-être également effectuer des migrations de données dans la table django_content_type.
spookylukey
1
Vraiment super guide @Potr. Je suis curieux, ne devrait-il pas y avoir une orm['contenttypes.contenttype'].objects.filter ligne dans la partie arrière de 0003_create_cataussi? Je veux aussi partager une astuce. Si vous avez des index, ils devront également être modifiés. Dans mon cas, c'étaient des index uniques, donc mon attaquant ressemble à ça: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher
2
Pour y accéder orm['contenttypes.contenttype'], vous devez également ajouter l' --freeze contenttypesoption à vos schemamigrationcommandes.
Gary
1
Dans mon cas (Django 1.5.7 et South 1.0) .. J'ai dû taper python manage.py schemamigration specific create_cat --auto --freeze commonpour accéder au modèle de chat depuis l'application commune.
geoom
35

Misera sur la Potr Czachur de réponse , les situations qui impliquent ForeignKeys sont plus complexes et doivent être traitées de façon légèrement différente.

(L'exemple suivant s'appuie sur les applications commonet specificréférencées dans la réponse actuelle).

# common/models.py

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

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

changerait alors en

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Fonctionnement

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

générerait les migrations suivantes (j'ignore intentionnellement les modifications de Django ContentType - voir la réponse précédemment référencée pour savoir comment gérer cela):

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

Comme vous pouvez le voir, le FK doit être modifié pour référencer la nouvelle table. Nous devons ajouter une dépendance afin que nous sachions l'ordre dans lequel les migrations seront appliquées (et donc que la table existera avant d'essayer d'y ajouter un FK) mais nous devons également nous assurer que le retour en arrière fonctionne aussi car le la dépendance s'applique dans le sens inverse .

# 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

Selon la documentation du sud , depends_ongarantira que 0004_auto__add_cats'exécute avant 0009_auto__del_cat lors de la migration vers l'avant, mais dans l' ordre inverse lors de la migration vers l'arrière . Si nous restions db.rename_table('specific_cat', 'common_cat')dans la specificrestauration, la commonrestauration échouerait lors de la tentative de migration de la clé étrangère car la table référencée n'existerait pas.

Espérons que ce soit plus proche d'une situation «du monde réel» que les solutions existantes et que quelqu'un trouvera cela utile. À votre santé!

Matt Briançon
la source
Les sources fixes de cette réponse omettent les lignes de mise à jour des types de contenu, qui sont présentes dans la réponse de Potr Czachur. Cela pourrait être trompeur.
Shai Berger
@ShaiBerger J'ai abordé cela spécifiquement: "J'ignore intentionnellement les modifications de Django ContentType - voir la réponse précédemment référencée pour savoir comment gérer cela."
Matt Briançon
9

Les modèles ne sont pas très étroitement liés aux applications, le déplacement est donc assez simple. Django utilise le nom de l'application dans le nom de la table de base de données, donc si vous souhaitez déplacer votre application, vous pouvez soit renommer la table de base de données via une ALTER TABLEinstruction SQL , soit - encore plus simple - simplement utiliser le db_tableparamètre dans la Metaclasse de votre modèle pour faire référence au ancien nom.

Si vous avez utilisé des ContentTypes ou des relations génériques n'importe où dans votre code jusqu'à présent, vous souhaiterez probablement renommer le type app_labelde contenu pointant sur le modèle en mouvement, afin que les relations existantes soient préservées.

Bien sûr, si vous n'avez aucune donnée à conserver, le plus simple est de supprimer complètement les tables de la base de données et de les relancer ./manage.py syncdb.

Daniel Roseman
la source
2
Comment faire cela avec une migration vers le sud?
Apreche
4

Voici une autre solution à l'excellente solution de Potr. Ajoutez ce qui suit à spécifique / 0003_create_cat

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

À moins que cette dépendance ne soit définie, Sud ne garantira pas que la common_cattable existe au moment de l' exécution de / 0003_create_cat spécifique, ce qui vous enverra une django.db.utils.OperationalError: no such table: common_caterreur.

South exécute les migrations dans l' ordre lexicographique à moins que la dépendance ne soit explicitement définie. Puisque commonvient avant que specifictoutes les commonmigrations de soient exécutées avant le changement de nom de table, il ne se reproduirait probablement pas dans l'exemple original montré par Potr. Mais si vous renommez commonen app2et specificen, app1vous rencontrerez ce problème.

Ihor Kaharlichenko
la source
Ce n'est en fait pas un problème avec l'exemple de Potr. Il utilise la migration spécifique pour renommer et la migration commune pour dépendre de la migration spécifique. Si spécifique est exécuté en premier, tout va bien. Si common est exécuté en premier, la dépendance effectuera une exécution spécifique avant elle. Cela dit, j'ai inversé l'ordre en faisant cela, donc le changement de nom s'est produit en commun, et la dépendance en particulier, puis vous devez changer la dépendance comme vous le décrivez ci-dessus.
Emil Stenström
1
Je ne peux pas être d’accord avec vous. De mon point de vue, la solution doit être robuste et fonctionner sans essayer de lui plaire. La solution d'origine ne fonctionne pas si vous commencez à partir de la nouvelle base de données et du syncdb / migrate. Ma proposition le corrige. Quoi qu'il en soit, la réponse de Port m'a fait gagner beaucoup de temps, bravo à lui :)
Ihor Kaharlichenko
Si vous ne le faites pas, les tests peuvent également échouer (ils semblent toujours exécuter des migrations vers le sud lors de la création de leur base de données de test). J'ai déjà fait quelque chose de similaire. Bonne prise Ihor :)
odinho - Velmont
4

Le processus sur lequel je me suis actuellement arrêté depuis que je suis revenu ici plusieurs fois et que j'ai décidé de le formaliser.

Cela a été construit à l' origine sur la réponse de Potr Czachur et la réponse de Matt Briançon , en utilisant 0.8.4 du Sud

Étape 1. Découvrez les relations de clé étrangère enfant

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

Donc, dans ce cas étendu, nous avons découvert un autre modèle connexe comme:

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

    ...

Étape 2. Créer des migrations

# 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

Étape 3. Contrôle de la source: validez les modifications jusqu'à présent.

Cela en fait un processus plus répétable si vous rencontrez des conflits de fusion comme des coéquipiers écrivant des migrations sur les applications mises à jour.

Étape 4. Ajoutez des dépendances entre les migrations.

Cela create_kittycatdépend essentiellement de l'état actuel de tout, et tout dépend alors de 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'),
    )
    ...

Étape 5. Le changement de nom de table que nous voulons apporter.

# 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

Étape 6. Seulement si vous avez besoin de backwards () pour travailler ET obtenir un KeyError à l'envers.

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

Étape 7. Testez-le - ce qui fonctionne pour moi n'est peut-être pas suffisant pour votre situation réelle :)

python manage.py migrate

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

Donc, utiliser la réponse originale de @Potr ci-dessus ne fonctionnait pas pour moi sur South 0.8.1 et Django 1.5.1. Je publie ci-dessous ce qui a fonctionné pour moi dans l'espoir que cela soit utile aux autres.

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
la source
1

Je vais donner une version plus explicite de l'une des choses suggérées par Daniel Roseman dans sa réponse ...

Si vous modifiez simplement l' db_tableattribut Meta du modèle que vous avez déplacé pour qu'il pointe vers le nom de la table existante (au lieu du nouveau nom que Django lui donnerait si vous supprimiez et exécutiez a syncdb), vous pouvez éviter des migrations Sud compliquées. par exemple:

Original:

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

Après avoir déménagé:

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

Il vous suffit maintenant de faire une migration de données pour mettre à jour le app_labelfor MyModeldans le django_content_typetableau et vous devriez être prêt à partir ...

Exécutez ./manage.py datamigration django update_content_typepuis modifiez le fichier que South crée pour vous:

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()
Anentropique
la source