¿Cómo migro un modelo de una aplicación de django a una nueva?

126

Tengo una aplicación django con cuatro modelos. Ahora me doy cuenta de que uno de estos modelos debería estar en una aplicación separada. Tengo instalado el sur para migraciones, pero no creo que esto sea algo que pueda manejar automáticamente. ¿Cómo puedo migrar uno de los modelos de la aplicación anterior a uno nuevo?

Además, tenga en cuenta que voy a necesitar que este sea un proceso repetible, para poder migrar el sistema de producción y demás.

Apreche
fuente
66
Para django 1.7 y superior, consulte stackoverflow.com/questions/25648393/…
Rick Westera,

Respuestas:

184

Cómo migrar usando el sur.

Digamos que tenemos dos aplicaciones: común y específica:

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

Ahora queremos mover el modelo common.models.cat a una aplicación específica (precisamente a specific.models.cat). Primero realice los cambios en el código fuente y luego ejecute:

$ 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

Ahora necesitamos editar ambos archivos de migración:

#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

Ahora ambas migraciones de aplicaciones son conscientes del cambio y la vida apesta un poco menos :-) Establecer esta relación entre las migraciones es la clave del éxito. Ahora si lo haces:

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

hará tanto la migración, y

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

migrará las cosas hacia abajo.

Tenga en cuenta que para actualizar el esquema usé una aplicación común y para degradar, usé una aplicación específica. Eso es porque la dependencia aquí funciona.

Potr Czachur
fuente
1
Wow gracias. Aprendí el sur por mi cuenta desde que hice esta pregunta, pero estoy seguro de que esto ayudará mucho a otros.
Apreche
11
También es posible que deba realizar migraciones de datos en la tabla django_content_type.
spookylukey
1
Muy buena guía @Potr. Tengo curiosidad, ¿no debería haber una orm['contenttypes.contenttype'].objects.filter línea en la parte posterior 0003_create_cattambién? También quiero compartir un consejo. Si tiene índices, también deberán modificarse. En mi caso, eran índices únicos, así que mi delantero se ve así: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher
2
Para acceder orm['contenttypes.contenttype'], también debe agregar la --freeze contenttypesopción a sus schemamigrationcomandos.
Gary
1
En mi caso (Django 1.5.7 y South 1.0). Tuve que escribir python manage.py schemamigration specific create_cat --auto --freeze commonpara acceder al modelo de gato desde una aplicación común.
geoom
35

Para construir sobre Potr Czachur 's respuesta , las situaciones que involucran ForeignKeys son más complicadas y deben ser manejados de forma ligeramente diferente.

(El siguiente ejemplo se basa en las aplicaciones commony specificreferidas en la respuesta actual).

# common/models.py

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

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

luego cambiaría a

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Corriendo

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

generaría las siguientes migraciones (estoy ignorando intencionalmente los cambios de Django ContentType; consulte la respuesta mencionada anteriormente para saber cómo manejar eso):

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

Como puede ver, el FK debe modificarse para hacer referencia a la nueva tabla. Tenemos que añadir una dependencia por lo que sabemos que el orden en el que se aplicarán las migraciones (y por lo tanto que existirá la mesa antes de tratar de añadir un FK a ella), pero también hay que asegurarse de que se vaya hacia atrás también funciona porque la La dependencia se aplica en la dirección inversa .

# 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

Según la documentación del Sur , depends_onse asegurará de que se 0004_auto__add_catejecute antes 0009_auto__del_cat al migrar hacia adelante, pero en el orden opuesto al migrar hacia atrás . Si nos fuimos db.rename_table('specific_cat', 'common_cat')en la specificreversión, la commonreversión fallaría al intentar migrar ForeignKey porque la tabla a la que se hace referencia no existiría.

Esperemos que esto esté más cerca de una situación del "mundo real" que las soluciones existentes y alguien lo encuentre útil. ¡Salud!

Matt Briançon
fuente
Las fuentes fijas en esta respuesta omiten las líneas para actualizar los tipos de contenido, que están presentes en la respuesta de Potr Czachur. Esto podría ser engañoso.
Shai Berger
@ShaiBerger Abordé eso específicamente: "Estoy ignorando intencionalmente los cambios de Django ContentType; vea la respuesta a la que se hizo referencia anteriormente para saber cómo manejar eso".
Matt Briançon
9

Los modelos no están muy unidos a las aplicaciones, por lo que moverse es bastante simple. Django usa el nombre de la aplicación en el nombre de la tabla de la base de datos, por lo que si desea mover su aplicación, puede cambiar el nombre de la tabla de la base de datos a través de una ALTER TABLEdeclaración SQL o, incluso más simple, simplemente use el db_tableparámetro en la Metaclase de su modelo para referirse a viejo nombre.

Si hasta ahora ha utilizado ContentTypes o relaciones genéricas en cualquier parte de su código, probablemente desee cambiar el nombre del app_label de contenido que apunta al modelo que se está moviendo, para preservar las relaciones existentes.

Por supuesto, si no tiene ningún dato para preservar, lo más fácil es eliminar las tablas de la base de datos por completo y ./manage.py syncdbvolver a ejecutarlas .

Daniel Roseman
fuente
2
¿Cómo hago eso con una migración hacia el sur?
Apreche
4

Aquí hay una solución más a la excelente solución de Potr. Agregue lo siguiente a específico / 0003_create_cat

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

A menos que se establezca esta dependencia, South no garantizará que la common_cattabla exista en el momento en que se ejecute / 0003_create_cat específico , arrojando undjango.db.utils.OperationalError: no such table: common_cat error.

South realiza las migraciones en orden lexicográfico a menos que la dependencia se establezca explícitamente. Como se commonproduce antes de que specifictodas las commonmigraciones se ejecuten antes del cambio de nombre de la tabla, es probable que no se reproduzca en el ejemplo original que muestra Potr. Pero si cambia el nombre commona app2y specifichacia app1, se encontrará con este problema.

Ihor Kaharlichenko
fuente
Esto en realidad no es un problema con el ejemplo de Potr. Utiliza la migración específica para cambiar el nombre y la migración común para depender de la específica. Si lo específico se ejecuta primero, está bien. Si common se ejecuta primero, la dependencia realizará una ejecución específica antes. Dicho esto, cambié el orden al hacer esto, por lo que el cambio de nombre sucedió en común y la dependencia en específico, y luego debe cambiar la dependencia como se describe anteriormente.
Emil Stenström
1
No puedo estar de acuerdo contigo. Desde mi punto de vista, la solución debe ser robusta y funcionar sin tratar de complacerla. La solución original no funciona si comienza desde db reciente y syncdb / migrate. Mi propuesta lo arregla. De cualquier manera u otra, la respuesta de Port me ahorró mucho tiempo, felicitaciones a él :)
Ihor Kaharlichenko
No hacerlo puede hacer que las pruebas también fallen (siempre parecen ejecutar migraciones completas al sur al crear su base de datos de prueba) He hecho algo similar antes. Buena captura Ihor :)
odinho - Velmont
4

El proceso en el que me he asentado actualmente desde que regresé aquí varias veces y decidí formalizarlo.

Este fue construido originalmente en la respuesta de Potr Czachur y la respuesta de Matt Briançon , utilizando 0.8.4 del Sur

Paso 1. Descubra las relaciones de claves externas infantiles

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

Entonces, en este caso extendido, hemos descubierto otro modelo relacionado como:

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

    ...

Paso 2. Crear migraciones

# 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

Paso 3. Control de origen: Confirmar cambios hasta ahora.

Lo convierte en un proceso más repetible si se encuentra con conflictos de fusión como compañeros de equipo que escriben migraciones en las aplicaciones actualizadas.

Paso 4. Agregar dependencias entre las migraciones.

Básicamente create_kittycatdepende del estado actual de todo, y todo depende 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'),
    )
    ...

Paso 5. El cambio de nombre de tabla que queremos hacer.

# 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

Paso 6. Solo si necesita al revés () para trabajar Y hacer que un KeyError se ejecute al revés.

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

Paso 7. Pruébalo: lo que funciona para mí puede no ser suficiente para tu situación de la vida real :)

python manage.py migrate

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

Entonces, usar la respuesta original de @Potr anterior no funcionó para mí en South 0.8.1 y Django 1.5.1. Estoy publicando lo que funcionó para mí a continuación con la esperanza de que sea útil para otros.

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
fuente
1

Voy a dar una versión más explícita de una de las cosas que Daniel Roseman sugirió en su respuesta ...

Si solo cambia el db_tableatributo Meta del modelo que ha movido para señalar el nombre de la tabla existente (en lugar del nuevo nombre, Django le daría si lo dejara caer e hiciera unsyncdb ), entonces puede evitar migraciones complicadas del Sur. p.ej:

Original:

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

Después de mudarse:

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

Ahora solo necesita hacer una migración de datos para actualizar el app_labelpara MyModelen la django_content_typetabla y debería estar listo ...

Ejecute y ./manage.py datamigration django update_content_typeluego edite el archivo que South crea para usted:

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()
Anéntrópico
fuente