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.
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.
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é!
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.
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
# 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)
...
# 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
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.
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'),
)
...
# 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
# 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']})
},
...
}
python manage.py migrate
# If you need backwards to work
python manage.py migrate specific <the_one_before_create_kittycat>
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.
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';")
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()