Estratégia de migração do Django para renomear um modelo e campos de relacionamento

152

Estou planejando renomear vários modelos em um projeto existente do Django, onde existem muitos outros modelos que têm relacionamentos de chave estrangeira com os modelos que gostaria de renomear. Estou bastante certo de que isso exigirá várias migrações, mas não tenho certeza do procedimento exato.

Digamos que eu comece com os seguintes modelos em um aplicativo Django chamado myapp:

class Foo(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_ridonkulous = models.BooleanField()

Quero renomear o Foomodelo porque o nome realmente não faz sentido e está causando confusão no código, e Bartornaria um nome muito mais claro.

Pelo que li na documentação de desenvolvimento do Django, estou assumindo a seguinte estratégia de migração:

Passo 1

Modificar models.py:

class Bar(models.Model):  # <-- changed model name
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    foo = models.ForeignKey(Bar)  # <-- changed relation, but not field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Bar)  # <-- changed relation, but not field name
    is_ridonkulous = models.BooleanField()

Observe que o AnotherModelnome do campo para foonão muda, mas a relação é atualizada para o Barmodelo. Meu raciocínio é que eu não deveria mudar muito de uma só vez e que se eu alterasse esse nome de campo, barcorreria o risco de perder os dados nessa coluna.

Passo 2

Crie uma migração vazia:

python manage.py makemigrations --empty myapp

etapa 3

Edite a Migrationclasse no arquivo de migração criado na etapa 2 para adicionar a RenameModeloperação à lista de operações:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar')
    ]

Passo 4

Aplique a migração:

python manage.py migrate

Etapa 5

Edite os nomes dos campos relacionados em models.py:

class Bar(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_ridonkulous = models.BooleanField()

Etapa 6

Crie outra migração vazia:

python manage.py makemigrations --empty myapp

Etapa 7

Edite a Migrationclasse no arquivo de migração criado na etapa 6 para adicionar as RenameFieldoperações de qualquer nome de campo relacionado à lista de operações:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0002_rename_fields'),  # <-- is this okay?
    ]

    operations = [
        migrations.RenameField('AnotherModel', 'foo', 'bar'),
        migrations.RenameField('YetAnotherModel', 'foo', 'bar')
    ]

Etapa 8

Aplique a 2ª migração:

python manage.py migrate

Além de atualizar o restante do código (visualizações, formulários etc.) para refletir os novos nomes de variáveis, é basicamente assim que a nova funcionalidade de migração funcionaria?

Além disso, isso parece ter muitos passos. As operações de migração podem ser condensadas de alguma maneira?

Obrigado!

Fiver
fonte

Respostas:

125

Então, quando eu tentei isso, parece que você pode condensar as etapas 3 a 7:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'), 
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar'),
        migrations.RenameField('AnotherModel', 'foo', 'bar'),
        migrations.RenameField('YetAnotherModel', 'foo', 'bar')
    ]

Você pode receber alguns erros se não atualizar os nomes nos quais foram importados, por exemplo, admin.py e arquivos de migração ainda mais antigos (!).

Atualização : Como ceasaro menciona, as versões mais recentes do Django geralmente são capazes de detectar e perguntar se um modelo é renomeado. Portanto, tente manage.py makemigrationsprimeiro e verifique o arquivo de migração.

wasabigeek
fonte
Obrigado pela resposta. Desde então, migrei usando as etapas descritas, mas estou curioso para saber se você tentou isso com dados existentes ou apenas com um banco de dados vazio?
Fiver
2
Tentou fazê-lo com os dados existentes, ainda que apenas algumas linhas sobre sqlite em meu env local (quando eu passar para Produção Pretendo limpar tudo para fora incl migração de arquivos.)
wasabigeek
4
Você não precisa alterar o nome do modelo nos arquivos de migração, se usá apps.get_model-los. Levei muito tempo para descobrir isso.
ahmed 12/05
9
No django 2.0, se você alterar o nome do seu modelo, o ./manage.py makemigrations myappcomando perguntará se você renomeou o seu modelo. Por exemplo: você renomeou o modelo myapp.Foo para Bar? [s / N] Se você responder 's', a sua migração conterá as migration.RenameModel('Foo', 'Bar')mesmas contagens para os campos renomeados :-)
ceasaro
1
manage.py makemigrations myappainda pode falhar: "Talvez seja necessário adicionar manualmente isso se você alterar o nome do modelo e alguns de seus campos de uma só vez; para o detector automático, será como se você tivesse excluído um modelo com o nome antigo e adicionado um novo com um nome diferente e a migração criada perderá todos os dados da tabela antiga ". Docs do Django 2.1 Para mim, foi suficiente criar uma migração vazia, adicionar o nome do modelo a ela e executar makemigrationscomo de costume.
hlongmore
36

No começo, pensei que o método de Fiver funcionou para mim porque a migração funcionou bem até a etapa 4. No entanto, as alterações implícitas 'ForeignKeyField (Foo)' em 'ForeignKeyField (Bar)' não estavam relacionadas em nenhuma migração. É por isso que a migração falhou quando eu quis renomear os campos de relacionamento (etapa 5-8). Isso pode ser devido ao fato de que meu 'AnotherModel' e 'YetAnotherModel' são despachados em outros aplicativos no meu caso.

Então, consegui renomear meus modelos e campos de relacionamento, seguindo as etapas abaixo:

Eu adaptei o método a partir disso e, particularmente, o truque do otranzer.

Então, como o Fiver, digamos que temos no myapp :

class Foo(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)

E em myotherapp :

class AnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Foo)
    is_ridonkulous = models.BooleanField()

Passo 1:

Transforme todos os OneToOneField (Foo) ou ForeignKeyField (Foo) em IntegerField (). (Isso manterá o ID do objeto Foo relacionado como valor do campo inteiro).

class AnotherModel(models.Model):
    foo = models.IntegerField()
    is_awesome = models.BooleanField()

class YetAnotherModel(models.Model):
    foo = models.IntegerField()
    is_ridonkulous = models.BooleanField()

Então

python manage.py makemigrations

python manage.py migrate

Etapa 2: (como a etapa 2-4 do Fiver)

Alterar o nome do modelo

class Bar(models.Model):  # <-- changed model name
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)

Crie uma migração vazia:

python manage.py makemigrations --empty myapp

Em seguida, edite-o como:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.RenameModel('Foo', 'Bar')
    ]

Eventualmente

python manage.py migrate

Etapa 3:

Transforme seu IntegerField () de volta no ForeignKeyField ou OneToOneField anterior, mas com o novo Modelo de Barras. (O campo inteiro anterior estava armazenando o ID, então o django entenda isso e restabeleça a conexão, o que é legal.)

class AnotherModel(models.Model):
    foo = models.ForeignKey(Bar)
    is_awesome = models.BooleanField()

class YetAnotherModel(models.Model):
    foo = models.ForeignKey(Bar)
    is_ridonkulous = models.BooleanField()

Então faça:

python manage.py makemigrations 

Muito importante, nesta etapa, você deve modificar todas as novas migrações e adicionar a dependência nas migrações RenameModel Foo-> Bar. Portanto, se o AnotherModel e o YetAnotherModel estiverem no myotherapp, a migração criada no myotherapp deverá ser assim:

class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '00XX_the_migration_of_myapp_with_renamemodel_foo_bar'),
        ('myotherapp', '00xx_the_migration_of_myotherapp_with_integerfield'),
    ]

    operations = [
        migrations.AlterField(
            model_name='anothermodel',
            name='foo',
            field=models.ForeignKey(to='myapp.Bar'),
        ),
        migrations.AlterField(
            model_name='yetanothermodel',
            name='foo',
            field=models.ForeignKey(to='myapp.Bar')
        ),
    ]

Então

python manage.py migrate

Passo 4:

Eventualmente, você pode renomear seus campos

class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar) <------- Renamed fields
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar) <------- Renamed fields
    is_ridonkulous = models.BooleanField()

e depois renomeie automaticamente

python manage.py makemigrations

(o django deve perguntar se você realmente renomeou o nome do modelo, diga sim)

python manage.py migrate

E é isso!

Isso funciona no Django1.8

v.thorey
fonte
3
Obrigado! Isso foi extremamente útil. Mas uma observação - eu também tive que renomear e / ou remover os índices de campo do PostgreSQL manualmente, porque, depois de renomear Foo para Bar, criei um novo modelo chamado Bar.
Anatoly Scherbakov
Obrigado por isso! Eu acho que a parte principal está convertendo todas as chaves estrangeiras, dentro ou fora do modelo a ser renomeado, para IntegerField. Isso funcionou perfeitamente para mim e tem a vantagem adicional de serem recriados com o nome correto. Naturalmente, eu recomendaria revisar todas as migrações antes de executá-las!
Zelanix
Obrigado! Tentei muitas estratégias diferentes para renomear um modelo para o qual outros modelos possuem chaves estrangeiras (etapas 1 a 3), e essa foi a única que funcionou.
MSH
Alterar ForeignKeys para IntegerFields salvou meu dia hoje!
mehmet
8

Eu precisava fazer a mesma coisa e seguir. Mudei o modelo de uma só vez (etapas 1 e 5 juntas da resposta de Fiver). Em seguida, criou uma migração de esquema, mas a editou para ser esta:

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.rename_table('Foo','Bar')

    def backwards(self, orm):
        db.rename_table('Bar','Foo')

Isso funcionou perfeitamente. Todos os meus dados existentes apareceram, todas as outras tabelas mencionadas Bar bem.

daqui: https://hanmir.wordpress.com/2012/08/30/rename-model-django-south-migration/

John Q
fonte
Ótimo, obrigado por compartilhar. Certifique-se de marcar wasibigeek com +1 se essa resposta ajudou.
Fiver
7

No Django 1.10, consegui alterar dois nomes de classe de modelo (incluindo uma ForeignKey e com dados) executando simplesmente Makemigrations e depois Migrate para o aplicativo. Para a etapa Makemigrations, tive que confirmar que desejava alterar os nomes das tabelas. Migrate alterou os nomes das tabelas sem problemas.

Em seguida, alterei o nome do campo ForeignKey para corresponder e novamente fui solicitado pelos Makemigrations para confirmar que queria alterar o nome. Migre do que fez a alteração.

Então, tomei isso em duas etapas, sem nenhuma edição especial de arquivo. No início, eu recebi erros porque esqueci de alterar o arquivo admin.py, conforme mencionado por @wasibigeek.

excyberlabber
fonte
Muito obrigado! Perfeito para Django 1.11 demais
Francisco
5

Também enfrentei o problema como v.thorey descreveu e descobriu que sua abordagem é muito útil, mas pode ser condensada em menos etapas, que são realmente as etapas de 5 a 8, conforme Fiver descreveu sem as etapas de 1 a 4, exceto que a etapa 7 precisa ser alterada, pois abaixo da etapa 3. As etapas gerais são as seguintes:

Etapa 1: edite os nomes dos campos relacionados em models.py

class Bar(models.Model):
    name = models.CharField(unique=True, max_length=32)
    description = models.TextField(null=True, blank=True)


class AnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_awesome = models.BooleanField()


class YetAnotherModel(models.Model):
    bar = models.ForeignKey(Bar)  # <-- changed field name
    is_ridonkulous = models.BooleanField()

Etapa 2: criar uma migração vazia

python manage.py makemigrations --empty myapp

Etapa 3: Edite a classe Migration no arquivo de migração criado na Etapa 2

class Migration(migrations.Migration):

dependencies = [
    ('myapp', '0001_initial'), 
]

operations = [
    migrations.AlterField(
        model_name='AnotherModel',
        name='foo',
        field=models.IntegerField(),
    ),
    migrations.AlterField(
        model_name='YetAnotherModel',
        name='foo',
        field=models.IntegerField(),
    ),
    migrations.RenameModel('Foo', 'Bar'),
    migrations.AlterField(
        model_name='AnotherModel',
        name='foo',
        field=models.ForeignKey(to='myapp.Bar'),
    ),
    migrations.AlterField(
        model_name='YetAnotherModel',
        name='foo',
        field=models.ForeignKey(to='myapp.Bar'),
    ),
    migrations.RenameField('AnotherModel', 'foo', 'bar'),
    migrations.RenameField('YetAnotherModel', 'foo', 'bar')
]

Etapa 4: aplicar a migração

python manage.py migrate

Feito

PS Eu tentei essa abordagem no Django 1.9

Curtis Lo
fonte
5

Estou usando o Django versão 1.9.4

Eu segui os seguintes passos: -

Acabei de renomear o modelo oldName para NewName Run python manage.py makemigrations. Ele solicitará que você Did you rename the appname.oldName model to NewName? [y/N]selecione Y

Corra python manage.py migratee ele solicitará

Os seguintes tipos de conteúdo são antigos e precisam ser excluídos:

appname | oldName
appname | NewName

Quaisquer objetos relacionados a esses tipos de conteúdo por uma chave estrangeira também serão excluídos. Tem certeza de que deseja excluir esses tipos de conteúdo? Se não tiver certeza, responda 'não'.

Type 'yes' to continue, or 'no' to cancel: Select No

Renomeie e migre todos os dados existentes para uma nova tabela nomeada para mim.

Piyush S. Wanare
fonte
Cara de agradecimento, eu estava confuso porque nada aconteceu quando depois de bater "não"
farhawa
3

Infelizmente, encontrei problemas (cada django 1.x) com a renomeação da migração, deixando nomes de tabelas antigos no banco de dados.

O Django nem tenta nada na mesa antiga, apenas renomeia o seu próprio modelo. O mesmo problema com chaves estrangeiras e índices em geral - as alterações não são rastreadas corretamente pelo Django.

A solução mais simples (solução alternativa):

class Foo(models.Model):
     name = models.CharField(unique=True, max_length=32)
     ...
Bar = Foo  # and use Bar only

A solução real (uma maneira fácil de alternar todos os índices, restrições, gatilhos, nomes etc. em 2 confirmações, mas em tabelas menores ):

confirmar A:

  1. crie o mesmo modelo que o antigo
# deprecated - TODO: TO BE REMOVED
class Foo(model.Model):
    ...

class Bar(model.Model):
    ...
  1. alterne o código para funcionar apenas com o novo modelo Bar. (incluindo todas as relações no esquema)

Na migração RunPython, prepare , que copia dados de Foo para Bar (incluindo idFoo)

  1. otimização opcional (se necessário para tabelas maiores)

confirmar B: (sem pressa, faça-o quando uma equipe inteira for migrada)

  1. queda segura do modelo antigo Foo

limpeza adicional:

  • squash em migrações

bug no Django:

Sławomir Lenart
fonte
3

Só queria confirmar e adicionar um comentário ceasaro. O Django 2.0 parece fazer isso automaticamente agora.

Estou no Django 2.2.1, tudo o que tive que fazer para renomear o modelo e executar makemigrations .

Aqui ele pergunta se eu havia renomeado a classe específica de Apara B, eu escolhi yes e executei migrate e tudo parece funcionar.

Nota: eu não renomeei o nome do modelo antigo em nenhum arquivo dentro da pasta projeto / migrações.

Peheje
fonte
1

Eu precisava renomear algumas tabelas. Mas apenas uma renomeação de modelo foi notada pelo Django. Isso aconteceu porque o Django repete os modelos adicionados e removidos. Para cada par, ele verifica se eles são do mesmo aplicativo e têm campos idênticos . Apenas uma tabela não tinha chaves estrangeiras para as tabelas a serem renomeadas (chaves estrangeiras contêm o nome da classe do modelo, como você se lembra). Em outras palavras, apenas uma tabela não teve alterações de campo. Por isso foi notado.

Portanto, a solução é renomear uma tabela por vez, alterando o nome da classe do modelo e models.py, possivelmente views.py, fazendo uma migração. Depois disso, inspecione seu código em busca de outras referências (nomes de classe de modelo, nomes relacionados (consulta), nomes de variáveis). Faça uma migração, se necessário. Em seguida, combine opcionalmente todas essas migrações em uma (certifique-se de copiar também as importações).

x-yuri
fonte
1

Eu faria palavras @ceasaro, minhas no seu comentário sobre esta resposta .

Versões mais recentes do Django podem detectar mudanças e perguntar sobre o que foi feito. Eu também acrescentaria que o Django pode misturar a ordem de execução de alguns comandos de migração.

Seria sensato para aplicar pequenas mudanças e correr makemigrationse migratee se o erro ocorrer o arquivo de migração podem ser editados.

Algumas linhas de ordem de execução podem ser alteradas para evitar erros.

diogosimao
fonte
Bom notar que isso não funciona se você mudar nomes de modelos e há chaves estrangeiras definidas, etc ...
Dean Kayton
Expandindo o comentário anterior: Se tudo o que faço é alterar os nomes dos modelos e executar makemigrations, recebo 'NameError: name' <oldmodel> 'não está definido' nas chaves estrangeiras, etc ... em admin.py ... se eu corrigir isso e executar makemigrations novamente, recebo a solicitação 'Você renomeou o modelo <app.oldmodel> para <newmodel>'? Mas, ao aplicar migrações, recebo 'ValueError: The field <app .newmodel.field1> foi declarado com uma referência lenta a '<app.oldmodel>', mas o aplicativo '<app>' não fornece o modelo '<oldmodel>', etc ... '
Dean Kayton
Este erro parece que você precisa renomear as referências nas suas migrações históricas.
Mhatch
@DeanKayton diria que isso migrations.SeparateDatabaseAndStatepode ajudar?
diogosimao 25/11/19
1

Se você estiver usando um bom IDE como PyCharm, poderá clicar com o botão direito do mouse no nome do modelo e refatorar -> renomear. Isso poupa o trabalho de analisar todo o seu código que faz referência ao modelo. Em seguida, execute makemigrations e migre. O Django 2+ simplesmente confirmará a mudança de nome.

Josh
fonte
-10

Atualizei o Django da versão 10 para a versão 11:

sudo pip install -U Django

( -Upara "atualização") e resolveu o problema.

Muhammad Hafid
fonte