web-dev-qa-db-fra.com

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

J'ai une application Django avec quatre modèles. Je me rends compte maintenant que l'un de ces modèles devrait être dans une application distincte. J'ai installé South pour les migrations, mais je ne pense pas que cela est quelque chose qu'il peut 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 reproductible, afin de pouvoir migrer le système de production et autres.

123
Apreche

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

Maintenant, nous devons 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 des applications sont conscientes du changement et la vie est un peu moins bonne :-) L'établissement de cette relation entre les migrations est la clé du succès. Maintenant, si vous le faites:

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

fera à la fois la migration et

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

fera 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 la dépendance fonctionne ici.

183
Potr Czachur

Pour s'appuyer sur Potr Czachur s answer , les situations impliquant des ForeignKeys sont plus compliquées et doivent être traitées légèrement différemment.

(L'exemple suivant s'appuie sur les applications common et specific ré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 Django ContentType change — voir la réponse référencée précédemment 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 de connaître 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 parce que 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

D'après la documentation Sud , depends_on S'assurera que 0004_auto__add_cat S'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 laissions db.rename_table('specific_cat', 'common_cat') dans la restauration specific, la restauration common échouerait lors de la migration de ForeignKey car la table référencée par table n'existerait pas.

Espérons que cela soit plus proche d'une situation "réelle" que les solutions existantes et que quelqu'un trouvera cela utile. À votre santé!

35
Matt Briançon

Les modèles ne sont pas très étroitement liés aux applications, donc le déplacement est 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 un SQL ALTER TABLE, ou - encore plus simple - utilisez simplement db_table paramètre dans la classe Meta de votre modèle pour faire référence à l'ancien nom.

Si vous avez utilisé ContentTypes ou des relations génériques n'importe où dans votre code jusqu'à présent, vous souhaiterez probablement renommer le app_label du type de contenu pointant vers 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, la chose la plus simple à faire est de supprimer complètement les tables de la base de données et d'exécuter ./manage.py syncdb encore.

7
Daniel Roseman

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

Cela a été initialement construit sur réponse de Potr Czachur et réponse de Matt Briançon , en utilisant South 0.8.4

Étape 1. Découvrez les relations clés étrangères des enfants

# 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 apparenté 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éez 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 source: validez les modifications jusqu'à présent.

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.

Fondamentalement create_kittycat dépend 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. La table renomme le changement 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 une KeyError fonctionnant en arrière.

# 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 peut ne pas être 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>
4
pzrq

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, South ne garantira pas que le common_cat la table existe au moment où spécifique/0003_create_cat est exécuté, lançant un Django.db.utils.OperationalError: no such table: common_cat erreur sur vous.

South exécute les migrations dans ordre lexicographique sauf si la dépendance est explicitement définie. Puisque common précède specific toutes les migrations de common seraient exécutées avant le changement de nom de la table, donc il ne se reproduirait probablement pas dans l'exemple original montré par Potr. Mais si vous renommez common en app2 et specific à app1 vous rencontrerez ce problème.

4
Ihor Kaharlichenko

Donc, utiliser la réponse originale de @Potr ci-dessus n'a pas fonctionné pour moi sur South 0.8.1 et Django 1.5.1. Je poste ce qui a fonctionné pour moi ci-dessous dans l'espoir que ce 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';")
3
Tim Sutton

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

Si vous modifiez simplement le db_table Attribut méta du modèle que vous avez déplacé pour pointer vers le nom de la table existante (au lieu du nouveau nom Django le donnerait si vous tombiez et faisiez un syncdb) puis vous pouvez éviter les migrations compliquées vers le Sud, 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 ne vous reste plus qu'à effectuer une migration des données pour mettre à jour le app_label pour MyModel dans le Django_content_type table et vous devriez être prêt à partir ...

Courir ./manage.py datamigration Django update_content_type puis éditez 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()
1
Anentropic