Carregando dados iniciais com Django 1.7 e migrações de dados

95

Recentemente, mudei do Django 1.6 para o 1.7 e comecei a usar migrações (nunca usei o South).

Antes de 1.7, eu costumava carregar os dados iniciais com um fixture/initial_data.jsonarquivo, que era carregado com o python manage.py syncdbcomando (ao criar o banco de dados).

Agora, comecei a usar migrações e este comportamento está obsoleto:

Se um aplicativo usa migrações, não há carregamento automático de acessórios. Como as migrações serão necessárias para aplicativos no Django 2.0, este comportamento é considerado obsoleto. Se você deseja carregar os dados iniciais de um aplicativo, considere fazer isso em uma migração de dados. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

A documentação oficial não tem um exemplo claro de como fazer isso, então minha pergunta é:

Qual é a melhor maneira de importar esses dados iniciais usando migrações de dados:

  1. Escreva o código Python com várias chamadas para mymodel.create(...),
  2. Use ou escreva uma função Django ( como chamarloaddata ) para carregar dados de um arquivo de fixação JSON.

Prefiro a segunda opção.

Eu não quero usar o South, já que Django parece ser capaz de fazer isso nativamente agora.

Mickaël
fonte
3
Além disso, quero adicionar outra pergunta à pergunta original do OP: Como devemos fazer migrações de dados para dados que não pertencem aos nossos aplicativos. Por exemplo, se alguém está usando o framework de sites, ele precisa ter uma fixação com os dados dos sites. Visto que a estrutura de sites não está relacionada aos nossos aplicativos, onde devemos colocar essa migração de dados? Obrigado !
Serafeim
Um ponto importante que não foi abordado por ninguém aqui ainda é o que acontece quando você precisa adicionar dados definidos em uma migração de dados para um banco de dados no qual você falsificou migrações. Como as migrações foram falsas, sua migração de dados não será executada e você deve fazer isso manualmente. Neste ponto, você também pode chamar loaddata em um arquivo de fixação.
hekevintran
Outro cenário interessante é o que acontece se você tiver uma migração de dados para criar instâncias auth.Group, por exemplo, e mais tarde você tiver um novo Grupo que deseja criar como dados iniciais. Você precisará criar uma nova migração de dados. Isso pode ser irritante porque os dados de seed do Grupo estarão em vários arquivos. Além disso, no caso de você desejar redefinir as migrações, você terá que pesquisar para encontrar as migrações de dados que configuram os dados iniciais e também os portam.
hekevintran
@Serafeim A questão "Onde colocar os dados iniciais de um aplicativo de terceiros" não muda se você usar uma migração de dados em vez de acessórios, uma vez que você apenas altera a forma como os dados são carregados. Eu uso um pequeno aplicativo personalizado para coisas assim. Se o aplicativo de terceiros for chamado de "foo", eu chamo meu aplicativo simples contendo a migração / fixação de dados de "foo_integration".
guettli
@guettli sim, provavelmente usar um aplicativo extra é a melhor maneira de fazer isso!
Serafeim

Respostas:

81

Atualização : Veja o comentário de @GwynBleidD abaixo para os problemas que esta solução pode causar, e veja a resposta de @ Rockallite abaixo para uma abordagem que é mais durável para futuras mudanças de modelo.


Supondo que você tenha um arquivo de fixação em <yourapp>/fixtures/initial_data.json

  1. Crie sua migração vazia:

    No Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    No Django 1.8+, você pode fornecer um nome:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Edite seu arquivo de migração <yourapp>/migrations/0002_auto_xxx.py

    2.1. Implementação personalizada, inspirada em Django ' loaddata(resposta inicial):

    import os
    from sys import path
    from django.core import serializers
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
    
        fixture = open(fixture_file, 'rb')
        objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
        for obj in objects:
            obj.save()
        fixture.close()
    
    def unload_fixture(apps, schema_editor):
        "Brutally deleting all entries for this model..."
    
        MyModel = apps.get_model("yourapp", "ModelName")
        MyModel.objects.all().delete()
    
    class Migration(migrations.Migration):  
    
        dependencies = [
            ('yourapp', '0001_initial'),
        ]
    
        operations = [
            migrations.RunPython(load_fixture, reverse_code=unload_fixture),
        ]

    2.2. Uma solução mais simples para load_fixture(por sugestão de @juliocesar):

    from django.core.management import call_command
    
    fixture_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../fixtures'))
    fixture_filename = 'initial_data.json'
    
    def load_fixture(apps, schema_editor):
        fixture_file = os.path.join(fixture_dir, fixture_filename)
        call_command('loaddata', fixture_file) 

    Útil se você deseja usar um diretório personalizado.

    2.3. Mais simples: chamar loaddatacom app_labelluminárias de carga vontade do <yourapp>'s fixturesdir automaticamente:

    from django.core.management import call_command
    
    fixture = 'initial_data'
    
    def load_fixture(apps, schema_editor):
        call_command('loaddata', fixture, app_label='yourapp') 

    Se você não especificar app_label, loaddata tentará carregar o fixturenome do arquivo de todos os diretórios de fixtures do aplicativo (o que você provavelmente não deseja).

  3. Executá-lo

    python manage.py migrate <yourapp>
não
fonte
1
ok, você está certo ... Chamar loaddata('loaddata', fixture_filename, app_label='<yourapp>')também irá diretamente para o diretório do fixture do aplicativo (portanto, não há necessidade de construir o caminho completo do fixture)
n__o
15
Usando esse método, o serializador funcionará no estado dos modelos dos models.pyarquivos atuais , que podem ter alguns campos extras ou algumas outras alterações. Se algumas alterações foram feitas após a criação da migração, ela falhará (portanto, não podemos nem mesmo criar migrações de esquema após essa migração). Para consertar isso, podemos alterar teporalmente o registro de aplicativos em que o serializador está trabalhando para o registro fornecido para a função de migração no primeiro parâmetro. O registro para o caminho está localizado em django.core.serializers.python.apps.
GwynBleidD
3
Por que estamos fazendo isso? Por que o Django se torna cada vez mais difícil de executar e manter? Eu não quero ir embora, eu quero uma interface de linha de comando simples que resolva esse problema para mim, ou seja, como costumava ser com acessórios. O Django deve tornar isso mais fácil, não mais difícil :(
CpILL
1
@GwynBleidD Este é um ponto muito importante que você está fazendo, e acho que deveria aparecer nesta resposta aceita. É a mesma observação que aparece como comentário no exemplo de código de migração de dados da documentação . Você conhece outra maneira de usar serializadores com o fornecido app registry, sem alterar uma variável global (o que poderia causar problemas em um futuro hipotético com migrações de banco de dados paralelas).
Anúncio N de
3
Esta resposta sendo votada positivamente para kazoo junto com a aceitação é exatamente por que eu recomendo às pessoas que não usem stackoverflow. Mesmo agora, com os comentários e anedotas, ainda tenho pessoas no #django se referindo a isso.
shangxiao
50

Versão curta

Você NÃO deve usar o loaddatacomando de gerenciamento diretamente em uma migração de dados.

# Bad example for a data migration
from django.db import migrations
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # No, it's wrong. DON'T DO THIS!
    call_command('loaddata', 'your_data.json', app_label='yourapp')


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]

Versão longa

loaddatautiliza o django.core.serializers.python.Deserializerque usa os modelos mais atualizados para desserializar dados históricos em uma migração. Esse é um comportamento incorreto.

Por exemplo, suponha que haja uma migração de dados que utiliza o loaddatacomando de gerenciamento para carregar dados de um aparelho, e já está aplicado em seu ambiente de desenvolvimento.

Posteriormente, você decide adicionar um novo campo obrigatório ao modelo correspondente, então você o faz e faz uma nova migração em relação ao seu modelo atualizado (e possivelmente fornece um valor único para o novo campo quando ./manage.py makemigrationssolicitado).

Você executa a próxima migração e está tudo bem.

Finalmente, você concluiu o desenvolvimento de seu aplicativo Django e o implementou no servidor de produção. Agora é a hora de você executar todas as migrações do zero no ambiente de produção.

No entanto, a migração de dados falha . Isso ocorre porque o modelo desserializado do loaddatacomando, que representa o código atual, não pode ser salvo com dados vazios para o novo campo obrigatório adicionado. O acessório original não possui os dados necessários para isso!

Mas mesmo se você atualizar o aparelho com os dados necessários para o novo campo, a migração de dados ainda falhará . Quando a migração de dados está em execução, a próxima migração que adiciona a coluna correspondente ao banco de dados, ainda não é aplicada. Você não pode salvar dados em uma coluna que não existe!

Conclusão: em uma migração de dados, oloaddatacomando introduz uma possível inconsistência entre o modelo e o banco de dados. Definitivamente, você NÃO deveusá-lo diretamente em uma migração de dados.

A solução

loaddatacomando depende da django.core.serializers.python._get_modelfunção para obter o modelo correspondente de um acessório, que retornará a versão mais atualizada de um modelo. Precisamos fazer um patch de macacos para obter o modelo histórico.

(O código a seguir funciona para Django 1.8.x)

# Good example for a data migration
from django.db import migrations
from django.core.serializers import base, python
from django.core.management import call_command


def load_fixture(apps, schema_editor):
    # Save the old _get_model() function
    old_get_model = python._get_model

    # Define new _get_model() function here, which utilizes the apps argument to
    # get the historical version of a model. This piece of code is directly stolen
    # from django.core.serializers.python._get_model, unchanged. However, here it
    # has a different context, specifically, the apps variable.
    def _get_model(model_identifier):
        try:
            return apps.get_model(model_identifier)
        except (LookupError, TypeError):
            raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)

    # Replace the _get_model() function on the module, so loaddata can utilize it.
    python._get_model = _get_model

    try:
        # Call loaddata command
        call_command('loaddata', 'your_data.json', app_label='yourapp')
    finally:
        # Restore old _get_model() function
        python._get_model = old_get_model


class Migration(migrations.Migration):
    dependencies = [
        # Dependencies to other migrations
    ]

    operations = [
        migrations.RunPython(load_fixture),
    ]
Rockallite
fonte
1
Rockallite, você fez um ponto muito forte. Sua resposta me deixou pensando: a solução 2.1 da resposta de @ n__o / @ mlissner, que depende de, teria objects = serializers.deserialize('json', fixture, ignorenonexistent=True)o mesmo problema que loaddata? Ou ignorenonexistent=Truecobre todos os problemas possíveis?
Dário
7
Se você olhar para a fonte , você verá que o ignorenonexistent=Trueargumento tem dois efeitos: 1) ignora os modelos de um acessório que não estão nas definições de modelo mais atuais, 2) ignora os campos de um modelo de um acessório que não estão na definição de modelo correspondente mais atual. Nenhum deles lida com a situação do novo campo obrigatório no modelo . Então, sim, acho que sofre do mesmo problema que o normal loaddata.
Rockallite
Isso funcionou muito bem, uma vez que descobri que meu antigo json tinha modelos referenciados a outros modelos usando um natural_key(), que esse método não parece suportar - eu apenas substituí o valor natural_key pelo id real do modelo referenciado.
dsummersl
1
Provavelmente, esta resposta como resposta aceita seria mais útil, porque ao executar os casos de teste, um novo banco de dados é criado e todas as migrações são aplicadas do zero. Esta solução corrige problemas que um projeto com unittest enfrentará caso não substitua _get_model na migração de dados. Tnx
Mohammad ali baghershemirani
Obrigado pela atualização e explicações, @Rockallite. Minha resposta inicial foi postada algumas semanas depois que as migrações foram introduzidas no Django 1.7, e a documentação sobre como proceder não estava clara (e ainda é, da última vez que verifiquei). Esperançosamente, o Django atualizará seu mecanismo de loaddata / migração para levar em consideração o histórico do modelo algum dia.
n__o 02 de
6

Inspirado por alguns dos comentários (nomeadamente n__o's) e pelo fato de ter muitos initial_data.*arquivos espalhados por vários aplicativos, decidi criar um aplicativo Django que facilitaria a criação dessas migrações de dados.

Usando Django-migration-dispositivo elétrico você pode simplesmente executar o seguinte comando de gerenciamento e irá procurar através de todo o seu INSTALLED_APPSpara initial_data.*arquivos e transformá-los em migrações de dados.

./manage.py create_initial_data_fixtures
Migrations for 'eggs':
  0002_auto_20150107_0817.py:
Migrations for 'sausage':
  Ignoring 'initial_data.yaml' - migration already exists.
Migrations for 'foo':
  Ignoring 'initial_data.yaml' - not migrated.

Veja django-migration-fixture para instruções de instalação / uso.

alexhayes
fonte
2

Para dar ao seu banco de dados alguns dados iniciais, escreva uma migração de dados. Na migração de dados, use a função RunPython para carregar seus dados.

Não escreva nenhum comando loaddata, pois essa forma está obsoleta.

Suas migrações de dados serão executadas apenas uma vez. As migrações são uma sequência ordenada de migrações. Quando as migrações 003_xxxx.py são executadas, django migrações grava no banco de dados que este aplicativo é migrado até este (003), e executará apenas as seguintes migrações.

FlogFR
fonte
Então, você me incentiva a repetir chamadas myModel.create(...)(ou usando um loop) na função RunPython?
Mickaël
praticamente sim. Bancos de dados transacionais irão lidar com isso perfeitamente :)
FlogFR
1

As soluções apresentadas acima não funcionaram para mim, infelizmente. Descobri que toda vez que mudo meus modelos, tenho que atualizar meus acessórios. Idealmente, em vez disso, eu escreveria migrações de dados para modificar os dados criados e os dados carregados pelo aparelho de maneira semelhante.

Para facilitar isso , escrevi uma função rápida que irá procurar no fixturesdiretório do aplicativo atual e carregar um fixture. Coloque esta função em uma migração no ponto do histórico do modelo que corresponde aos campos na migração.

Leifdenby
fonte
Obrigado por isso! Eu escrevi uma versão que funciona com Python 3 (e passa nosso Pylint estrito). Você pode usá-lo como uma fábrica com RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley
1

Na minha opinião, os jogos estão um pouco ruins. Se o seu banco de dados muda com frequência, mantê-los atualizados logo será um pesadelo. Na verdade, não é só minha opinião, no livro "Two Scoops of Django" é explicado muito melhor.

Em vez disso, escreverei um arquivo Python para fornecer a configuração inicial. Se você precisar de algo mais, sugiro que dê uma olhada no Factory boy .

Se você precisa migrar alguns dados, você deve usar migrações de dados .

Também há "Queime seus acessórios , use fábricas de modelos" sobre o uso de acessórios.

Griffosx
fonte
1
Eu concordo com seu ponto "difícil de manter se houver mudanças frequentes", mas aqui o dispositivo visa apenas fornecer dados iniciais (e mínimos) ao instalar o projeto ...
Mickaël
1
Isso é para um carregamento de dados único, o que, se feito no contexto de migrações, faz sentido. Já que, se estiver em uma migração, não será necessário fazer alterações nos dados json. Quaisquer alterações de esquema que exijam alterações nos dados mais adiante devem ser tratadas por meio de outra migração (nesse ponto, outros dados podem estar no banco de dados que também precisarão ser modificados).
mtnpaul
0

No Django 2.1, eu queria carregar alguns modelos (como nomes de países, por exemplo) com dados iniciais.

Mas eu queria que isso acontecesse automaticamente logo após a execução das migrações iniciais.

Portanto, pensei que seria ótimo ter uma sql/pasta dentro de cada aplicativo que exigisse o carregamento dos dados iniciais.

Então, dentro dessa sql/pasta, eu teria .sqlarquivos com os DMLs necessários para carregar os dados iniciais nos modelos correspondentes, por exemplo:

INSERT INTO appName_modelName(fieldName)
VALUES
    ("country 1"),
    ("country 2"),
    ("country 3"),
    ("country 4");

Para ser mais descritivo, esta é a aparência de um aplicativo contendo uma sql/pasta: insira a descrição da imagem aqui

Também encontrei alguns casos em que precisava que os sqlscripts fossem executados em uma ordem específica. Portanto, decidi prefixar os nomes dos arquivos com um número consecutivo, como pode ser visto na imagem acima.

Então eu precisava de uma maneira de carregar qualquer SQLsdisponível dentro de qualquer pasta de aplicativo automaticamente fazendo python manage.py migrate.

Portanto, criei outro aplicativo chamado initial_data_migrationse o adicionei à lista de INSTALLED_APPSno settings.pyarquivo. Em seguida, criei uma migrationspasta dentro e adicionei um arquivo chamado run_sql_scripts.py( que na verdade é uma migração personalizada ). Como pode ser visto na imagem abaixo:

insira a descrição da imagem aqui

Criei run_sql_scripts.pypara que ele cuide da execução de todos os sqlscripts disponíveis dentro de cada aplicativo. Este é então disparado quando alguém corre python manage.py migrate. Este costume migrationtambém adiciona os aplicativos envolvidos como dependências, dessa forma, ele tenta executar as sqlinstruções somente depois que os aplicativos necessários executaram suas 0001_initial.pymigrações (não queremos tentar executar uma instrução SQL em uma tabela inexistente).

Aqui está a fonte desse script:

import os
import itertools

from django.db import migrations
from YourDjangoProjectName.settings import BASE_DIR, INSTALLED_APPS

SQL_FOLDER = "/sql/"

APP_SQL_FOLDERS = [
    (os.path.join(BASE_DIR, app + SQL_FOLDER), app) for app in INSTALLED_APPS
    if os.path.isdir(os.path.join(BASE_DIR, app + SQL_FOLDER))
]

SQL_FILES = [
    sorted([path + file for file in os.listdir(path) if file.lower().endswith('.sql')])
    for path, app in APP_SQL_FOLDERS
]


def load_file(path):
    with open(path, 'r') as f:
        return f.read()


class Migration(migrations.Migration):

    dependencies = [
        (app, '__first__') for path, app in APP_SQL_FOLDERS
    ]

    operations = [
        migrations.RunSQL(load_file(f)) for f in list(itertools.chain.from_iterable(SQL_FILES))
    ]

Espero que alguém ache isso útil, funcionou muito bem para mim !. Se você tiver alguma dúvida, por favor me avise.

NOTA: Esta pode não ser a melhor solução, uma vez que estou apenas começando com o django, no entanto, ainda quero compartilhar este "Como fazer" com todos vocês, já que não encontrei muitas informações enquanto pesquisava sobre isso no Google.

Antony Fuentes Artavia
fonte