Como migrar um modelo de um aplicativo django para um novo?

126

Eu tenho um aplicativo django com quatro modelos. Percebo agora que um desses modelos deve estar em um aplicativo separado. Eu tenho o sul instalado para migrações, mas não acho que isso possa ser feito automaticamente. Como posso migrar um dos modelos do aplicativo antigo para um novo?

Além disso, lembre-se de que vou precisar que este seja um processo repetitivo, para que eu possa migrar o sistema de produção e tal.

Apreche
fonte
6
Para o django 1.7 e superior, consulte stackoverflow.com/questions/25648393/…
Rick Westera

Respostas:

184

Como migrar usando o sul.

Digamos que temos dois aplicativos: comuns e específicos:

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

Agora, queremos mover o modelo common.models.cat para um aplicativo específico (precisamente para specific.models.cat). Primeiro faça as alterações no código fonte e depois execute:

$ 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

Agora precisamos editar os dois arquivos de migração:

#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

Agora, as duas migrações de aplicativos estão cientes da mudança e a vida é um pouco menos :-) Definir esse relacionamento entre migrações é a chave do sucesso. Agora, se você fizer:

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

fará a migração e

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

migrará as coisas para baixo.

Observe que, para a atualização do esquema, usei o aplicativo comum e, para o downgrade, usei o aplicativo específico. Isso ocorre porque a dependência aqui funciona.

Potr Czachur
fonte
1
Uau, obrigada. Aprendi o sul sozinho desde que fiz essa pergunta, mas tenho certeza de que isso ajudará muito os outros.
Apreche 20/11/2009
11
Você também pode precisar fazer migrações de dados na tabela django_content_type.
Spookylukey
1
Realmente ótimo guia @Potr. Estou curioso, não deveria haver uma orm['contenttypes.contenttype'].objects.filter linha na parte de trás 0003_create_cattambém? Também quero compartilhar uma dica. Se você tiver índices, eles também precisarão ser modificados. No meu caso eram índices únicos, então meus Aguarda assim: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher
2
Para acessar orm['contenttypes.contenttype'], você também precisa adicionar a --freeze contenttypesopção aos seus schemamigrationcomandos.
5603 Gary
1
No meu caso (Django 1.5.7 e South 1.0) .. Eu tive que digitar python manage.py schemamigration specific create_cat --auto --freeze commonpara acessar o modelo de gato no aplicativo comum.
Geoom 21/04/2015
35

Para construir sobre potr Czachur 's resposta , situações que envolvem ForeignKeys são mais complicado e deve ser tratado de forma ligeiramente diferente.

(O exemplo a seguir se baseia nos aplicativos commone specificmencionados na resposta atual).

# common/models.py

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

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

mudaria para

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Corrida

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

geraria as seguintes migrações (estou intencionalmente ignorando as alterações do Django ContentType - consulte a resposta mencionada anteriormente para saber como lidar com isso):

# 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 você pode ver, o FK deve ser alterado para fazer referência à nova tabela. Precisamos adicionar uma dependência para que saibamos a ordem em que as migrações serão aplicadas (e, assim, que a tabela existirá antes de tentarmos adicionar um FK a ela), mas também precisamos garantir que a rolagem para trás funcione também porque o dependência se aplica na direção 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

De acordo com a documentação do Sul , depends_ongarantirá que seja 0004_auto__add_catexecutado antes 0009_auto__del_cat ao migrar para a frente, mas na ordem oposta ao migrar para trás . Se deixássemos db.rename_table('specific_cat', 'common_cat')na specificreversão, a commonreversão falharia ao tentar migrar a ForeignKey porque a tabela mencionada na tabela não existiria.

Espero que isso esteja mais próximo de uma situação do "mundo real" do que as soluções existentes e alguém ache isso útil. Felicidades!

Matt Briançon
fonte
As fontes fixas nesta resposta omitem as linhas para atualizar os tipos de conteúdo, presentes na resposta de Potr Czachur. Isso pode ser enganador.
Shai Berger
@ShaiBerger Abordei isso especificamente: "Estou intencionalmente ignorando as alterações do Django ContentType - veja a resposta anteriormente mencionada para saber como lidar com isso".
Matt Briançon
9

Os modelos não são muito acoplados aos aplicativos, portanto, a movimentação é bastante simples. O Django usa o nome do aplicativo no nome da tabela do banco de dados, portanto, se você deseja mover o aplicativo, pode renomear a tabela do banco de dados por meio de uma ALTER TABLEinstrução SQL ou, ainda mais simples, basta usar o db_tableparâmetro na Metaclasse do seu modelo para consultar a Antigo nome.

Se você já usou ContentTypes ou relações genéricas em qualquer lugar do seu código até agora, provavelmente desejará renomear o app_label do contenttype apontando para o modelo que está sendo movido, para que as relações existentes sejam preservadas.

Obviamente, se você não tem nenhum dado a preservar, a coisa mais fácil a fazer é eliminar completamente as tabelas do banco de dados e executar ./manage.py syncdbnovamente.

Daniel Roseman
fonte
2
Como faço isso com uma migração para o sul?
Apreche
4

Aqui está mais uma correção para a excelente solução da Potr. Adicione o seguinte a specific / 0003_create_cat

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

A menos que essa dependência seja definida, o Sul não garantirá que a common_cattabela exista no momento em que específico / 0003_create_cat for executado, lançando um django.db.utils.OperationalError: no such table: common_caterro em você.

O sul executa migrações em ordem lexicográfica, a menos que a dependência seja explicitamente definida. Como commonvem antes de specifictodas as commonmigrações serem executadas antes da renomeação da tabela, provavelmente não seria reproduzido no exemplo original mostrado pelo Potr. Mas se você renomear commonpara app2e specificpara app1você, encontrará esse problema.

Ihor Kaharlichenko
fonte
Na verdade, isso não é um problema com o exemplo do Potr. Ele usa a migração específica para renomear e a migração comum para depender da migração específica. Se específico for executado primeiro, você estará bem. Se comum for executado primeiro, a dependência fará uma execução específica antes dele. Dito isso, troquei a ordem ao fazer isso, para que a renomeação acontecesse em comum e a dependência específica, e então você precise alterar a dependência como descrito acima.
Emil Stenström
1
Eu não concordo com você. Do meu ponto de vista, a solução deve ser robusta e funcionar sem tentar agradá-la. A solução original não funcionará se você iniciar do db fresco e do syncdb / migrate. Minha proposta corrige isso. De qualquer forma ou de outra, a resposta de Porto me salvou um monte de tempo, parabéns para ele :)
Ihor Kaharlichenko
Não fazer isso também pode fazer com que os testes falhem (eles sempre parecem executar migrações completas para o sul ao criar seu banco de dados de teste). Eu fiz algo semelhante antes. Boa captura Ihor :)
odinho - Velmont 8/08
4

O processo em que eu decidi desde que voltei aqui algumas vezes e decidi formalizá-lo.

Esta foi originalmente construída sobre a resposta de potr Czachur e resposta de Matt Briançon , usando 0.8.4 do Sul

Etapa 1. Descubra relacionamentos de chave estrangeira filho

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

Portanto, neste caso estendido, descobrimos outro 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)

    ...

Etapa 2. Crie migrações

# 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

Etapa 3. Controle de origem: confirme as alterações até o momento.

Torna o processo mais repetitivo se você tiver conflitos de mesclagem, como colegas de equipe que escrevem migrações nos aplicativos atualizados.

Etapa 4. Adicione dependências entre as migrações.

Basicamente, create_kittycatdepende do estado atual de tudo, e tudo depende 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'),
    )
    ...

Etapa 5. A tabela renomeia a alteração que queremos fazer.

# 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

Etapa 6. Somente se você precisar que o backwards () funcione E faça com que o KeyError seja executado ao contrário.

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

Etapa 7. Teste - o que funciona para mim pode não ser suficiente para a sua situação na vida real :)

python manage.py migrate

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

Portanto, usar a resposta original do @Potr acima não funcionou para mim no Sul 0.8.1 e no Django 1.5.1. Estou publicando o que funcionou para mim abaixo, na esperança de que seja útil para outras pessoas.

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

Vou dar uma versão mais explícita de uma das coisas que Daniel Roseman sugeriu em sua resposta ...

Se você acabou de alterar o db_tableatributo Meta do modelo, você mudou para apontar para o nome da tabela existente (em vez do novo nome que o Django daria se você soltasse e fizesse umsyncdb ), então você pode evitar migrações sul complicadas. por exemplo:

Original:

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

Depois de se mudar:

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

Agora você só precisa fazer uma migração de dados para atualizar o app_labelparaMyModel na django_content_typetabela e deve estar pronto ...

Execute ./manage.py datamigration django update_content_typee edite o arquivo que South cria para você:

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()
Anentrópico
fonte