Memuat data awal dengan Django 1.7 dan migrasi data

96

Saya baru-baru ini beralih dari Django 1.6 ke 1.7, dan saya mulai menggunakan migrasi (saya tidak pernah menggunakan Selatan).

Sebelum 1.7, saya biasa memuat data awal dengan fixture/initial_data.jsonfile, yang dimuat dengan python manage.py syncdbperintah (saat membuat database).

Sekarang, saya mulai menggunakan migrasi, dan perilaku ini tidak digunakan lagi:

Jika aplikasi menggunakan migrasi, tidak ada pemuatan perlengkapan secara otomatis. Sejak migrasi akan dibutuhkan untuk aplikasi di Django 2.0, perilaku ini dianggap ditinggalkan. Jika Anda ingin memuat data awal untuk suatu aplikasi, pertimbangkan untuk melakukannya dalam migrasi data. ( https://docs.djangoproject.com/en/1.7/howto/initial-data/#automatically-loading-initial-data-fixtures )

The dokumentasi resmi tidak memiliki contoh yang jelas tentang bagaimana untuk melakukannya, jadi pertanyaan saya adalah:

Apa cara terbaik untuk mengimpor data awal seperti itu menggunakan migrasi data:

  1. Tulis kode Python dengan banyak panggilan ke mymodel.create(...),
  2. Gunakan atau tulis fungsi Django ( seperti memanggilloaddata ) untuk memuat data dari file perlengkapan JSON.

Saya lebih suka opsi kedua.

Saya tidak ingin menggunakan Selatan, karena Django tampaknya dapat melakukannya secara asli sekarang.

Mickaël
sumber
3
Juga, saya ingin menambahkan pertanyaan lain ke pertanyaan asli OP: Bagaimana kita harus melakukan migrasi data untuk data yang tidak termasuk dalam aplikasi kita. Misalnya, jika seseorang menggunakan kerangka situs, dia perlu memiliki perlengkapan dengan data situs. Karena kerangka situs tidak terkait dengan aplikasi kami, di mana kami harus meletakkan migrasi data itu? Terima kasih!
Serafeim
Poin penting yang belum dibahas oleh siapa pun di sini adalah apa yang terjadi ketika Anda perlu menambahkan data yang ditentukan dalam migrasi data ke database tempat Anda memalsukan migrasi. Karena migrasi dipalsukan, migrasi data Anda tidak akan berjalan dan Anda harus melakukannya secara manual. Pada titik ini Anda mungkin juga memanggil loaddata pada file fixture.
hekevintran
Skenario menarik lainnya adalah apa yang terjadi jika Anda memiliki migrasi data untuk membuat auth. Contoh grup misalnya dan nanti Anda memiliki Grup baru yang ingin Anda buat sebagai data benih. Anda perlu membuat migrasi data baru. Ini bisa mengganggu karena data seed Grup Anda akan ada dalam banyak file. Juga jika Anda ingin mengatur ulang migrasi, Anda harus mencari untuk menemukan migrasi data yang mengatur data seed dan juga memportnya.
hekevintran
@Serafeim Pertanyaan "Di mana harus meletakkan data awal untuk aplikasi pihak ketiga" tidak berubah jika Anda menggunakan migrasi data, bukan perlengkapan, karena Anda hanya mengubah cara data dimuat. Saya menggunakan aplikasi kustom kecil untuk hal-hal seperti ini. Jika aplikasi pihak ketiga disebut "foo", saya menyebut aplikasi sederhana saya yang berisi migrasi data / fixture "foo_integration".
guettli
@guettli ya, mungkin menggunakan aplikasi tambahan adalah cara terbaik untuk melakukannya!
Serafeim

Jawaban:

82

Pembaruan : Lihat komentar @ GwynBleidD di bawah untuk masalah yang dapat ditimbulkan oleh solusi ini, dan lihat jawaban @Rockallite di bawah ini untuk pendekatan yang lebih tahan lama untuk perubahan model di masa mendatang.


Dengan asumsi Anda memiliki file fixture di <yourapp>/fixtures/initial_data.json

  1. Buat migrasi kosong Anda:

    Di Django 1.7:

    python manage.py makemigrations --empty <yourapp>

    Di Django 1.8+, Anda dapat memberikan nama:

    python manage.py makemigrations --empty <yourapp> --name load_intial_data
  2. Edit file migrasi Anda <yourapp>/migrations/0002_auto_xxx.py

    2.1. Implementasi kustom, terinspirasi oleh Django ' loaddata(jawaban awal):

    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. Solusi yang lebih sederhana untuk load_fixture(sesuai saran @ 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) 

    Berguna jika Anda ingin menggunakan direktori kustom.

    2.3. Paling sederhana: memanggil loaddatadengan app_labelakan memuat perlengkapan dari direktori <yourapp>'s fixturessecara otomatis:

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

    Jika Anda tidak menentukan app_label, loaddata akan mencoba memuat fixturenama file dari semua direktori perlengkapan aplikasi (yang mungkin tidak Anda inginkan).

  3. Menjalankannya

    python manage.py migrate <yourapp>
tidak
sumber
1
ok, Anda benar ... Juga menelepon loaddata('loaddata', fixture_filename, app_label='<yourapp>')juga akan langsung menuju ke direktori perlengkapan aplikasi (karenanya tidak perlu membangun jalur lengkap perlengkapan itu)
n__o
15
Dengan menggunakan metode itu, serializer akan bekerja pada status model dari models.pyfile saat ini , yang dapat memiliki beberapa bidang tambahan atau beberapa perubahan lainnya. Jika beberapa perubahan dibuat setelah membuat migrasi, itu akan gagal (jadi kami bahkan tidak dapat membuat migrasi skema setelah migrasi itu). Untuk mengatasinya, kita tidak dapat mengubah registri aplikasi yang sedang dikerjakan serializer ke registri yang disediakan untuk fungsi migrasi pada parameter pertama. Registri ke jalur terletak di django.core.serializers.python.apps.
GwynBleidD
3
Mengapa kita melakukan ini? Mengapa Django menjadi semakin sulit untuk dijalankan dan dipelihara? Saya tidak ingin pergi melalui ini, saya ingin antarmuka baris perintah sederhana yang memecahkan masalah ini untuk saya yaitu seperti dulu dengan perlengkapan. Django seharusnya membuat hal ini lebih mudah, bukan lebih sulit :(
CpILL
1
@GwynBleidD Ini adalah poin yang sangat penting yang Anda buat, dan saya pikir itu akan muncul dalam jawaban yang diterima ini. Ini adalah pernyataan yang sama yang muncul sebagai komentar dalam contoh kode migrasi data dari dokumentasi . Apakah Anda tahu cara lain untuk menggunakan serializers dengan yang disediakan app registry, tanpa mengubah variabel global (yang dapat menyebabkan masalah di masa depan hipotetis dengan migrasi database paralel).
Iklan N
3
Jawaban ini dipilih untuk kazoo bersama dengan penerimaan adalah mengapa saya merekomendasikan kepada orang-orang untuk tidak menggunakan stackoverflow. Bahkan sekarang dengan komentar & anekdot saya masih memiliki orang-orang di #django yang mengacu pada hal ini.
shangxiao
50

Versi pendek

Anda TIDAK boleh menggunakan loaddataperintah manajemen secara langsung dalam migrasi data.

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

Versi panjang

loaddata memanfaatkan django.core.serializers.python.Deserializer yang menggunakan model paling mutakhir untuk deserialisasi data historis dalam migrasi. Itu perilaku yang salah.

Misalnya, ada migrasi data yang memanfaatkan loaddata perintah manajemen untuk memuat data dari fixture, dan itu sudah diterapkan di lingkungan pengembangan Anda.

Kemudian, Anda memutuskan untuk menambahkan bidang baru yang diperlukan ke model yang sesuai, sehingga Anda melakukannya dan melakukan migrasi baru terhadap model yang diperbarui (dan mungkin memberikan nilai satu kali ke bidang baru saat./manage.py makemigrations diminta).

Anda menjalankan migrasi berikutnya, dan semuanya baik-baik saja.

Akhirnya, Anda telah selesai mengembangkan aplikasi Django Anda, dan Anda menyebarkannya pada server produksi. Sekarang saatnya Anda menjalankan seluruh migrasi dari awal di lingkungan produksi.

Namun, migrasi data gagal . Itu karena model deserialized dari loaddataperintah, yang mewakili kode saat ini, tidak dapat disimpan dengan data kosong untuk bidang baru yang diperlukan yang Anda tambahkan. Perlengkapan asli kekurangan data yang diperlukan untuk itu!

Tetapi bahkan jika Anda memperbarui fixture dengan data yang diperlukan untuk bidang baru, migrasi data masih gagal . Saat migrasi data berjalan, migrasi berikutnya yang menambahkan kolom terkait ke database, belum diterapkan. Anda tidak dapat menyimpan data ke kolom yang tidak ada!

Kesimpulan: dalam migrasi data,loaddataperintah tersebut menyebabkan kemungkinan ketidakkonsistenan antara model dan database. Anda sebaiknya TIDAK menggunakannya secara langsung dalam migrasi data.

Solusinya

loaddataperintah bergantung pada django.core.serializers.python._get_modelfungsi untuk mendapatkan model yang sesuai dari perlengkapan, yang akan mengembalikan versi model yang paling mutakhir. Kita perlu menambalnya sehingga mendapatkan model historis.

(Kode berikut bekerja untuk 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
sumber
1
Rockallite, Anda membuat poin yang sangat kuat. Jawaban Anda membuat saya bertanya-tanya, apakah solusi 2.1 dari jawaban @ n__o / @ mlissner yang mengandalkan objects = serializers.deserialize('json', fixture, ignorenonexistent=True)mengalami masalah yang sama seperti loaddata? Atau apakah ignorenonexistent=Truemencakup semua kemungkinan masalah?
Dário
7
Jika Anda melihat sumbernya , Anda akan menemukan bahwa ignorenonexistent=Trueargumen memiliki dua efek: 1) mengabaikan model perlengkapan yang tidak ada dalam definisi model terbaru, 2) mengabaikan bidang model perlengkapan yang tidak dalam definisi model terkait terbaru. Tak satu pun dari mereka menangani situasi baru-bidang-dalam-model . Jadi, ya, saya pikir itu mengalami masalah yang sama seperti biasa loaddata.
Rockallite
Ini berfungsi dengan baik setelah saya mengetahui bahwa json lama saya memiliki model yang mereferensikan model lain menggunakan a natural_key(), yang tampaknya tidak didukung oleh metode ini - Saya baru saja mengganti nilai natural_key dengan id sebenarnya dari model yang direferensikan.
dsummersl
1
Mungkin jawaban ini sebagai jawaban yang diterima akan lebih membantu, karena dalam menjalankan kasus uji, database baru dibuat dan semua migrasi diterapkan dari awal. Solusi ini memperbaiki masalah yang akan dihadapi proyek dengan unittest jika _get_model tidak diganti dalam migrasi data. Tnx
Mohammad ali baghershemirani
Terima kasih atas pembaruan dan penjelasannya, @Rockallite. Jawaban awal saya diposting beberapa minggu setelah migrasi diperkenalkan di Django 1.7, dan dokumentasi tentang bagaimana melanjutkan tidak jelas (dan masih, terakhir kali saya memeriksa). Mudah-mudahan Django akan memperbarui mekanisme loaddata / migrasi mereka untuk memperhitungkan riwayat model suatu hari nanti.
n__o
6

Terinspirasi oleh beberapa komentar (yaitu n__o) dan fakta bahwa saya memiliki banyak initial_data.*file yang tersebar di beberapa aplikasi saya memutuskan untuk membuat aplikasi Django yang akan memfasilitasi pembuatan migrasi data ini.

Menggunakan Django-migrasi-perlengkapan Anda hanya dapat menjalankan perintah manajemen berikut dan akan mencari melalui semua Anda INSTALLED_APPSuntuk initial_data.*file dan mengubahnya menjadi migrasi data.

./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.

Lihat django-migrasi-fixture untuk petunjuk pemasangan / penggunaan.

alexhayes
sumber
2

Untuk memberikan database Anda beberapa data awal, tulis migrasi data. Dalam migrasi data, gunakan fungsi RunPython untuk memuat data Anda.

Jangan menulis perintah loaddata apa pun karena cara ini sudah usang.

Migrasi data Anda hanya akan dijalankan sekali. Migrasi adalah urutan migrasi yang teratur. Ketika 003_xxxx.py migrations dijalankan, django migrations menulis dalam database bahwa aplikasi ini dimigrasi hingga yang ini (003), dan hanya akan menjalankan migrasi berikut.

FlogFR
sumber
Jadi Anda mendorong saya untuk mengulangi panggilan ke myModel.create(...)(atau menggunakan loop) di fungsi RunPython?
Mickaël
cukup banyak ya. Database Transaactionnal akan menanganinya dengan sempurna :)
FlogFR
1

Sayangnya, solusi yang disajikan di atas tidak berhasil untuk saya. Saya menemukan bahwa setiap kali saya mengganti model saya, saya harus memperbarui perlengkapan saya. Idealnya saya akan menulis migrasi data untuk mengubah data yang dibuat dan data yang dimuat fixture dengan cara yang sama.

Untuk memfasilitasi ini saya menulis fungsi cepat yang akan mencari di fixturesdirektori aplikasi saat ini dan memuat fixture. Letakkan fungsi ini ke dalam migrasi di titik riwayat model yang cocok dengan bidang dalam migrasi.

leifdenby
sumber
Terima kasih untuk ini! Saya menulis versi yang bekerja dengan Python 3 (dan melewati Pylint ketat kami). Anda dapat menggunakannya sebagai pabrik dengan RunPython(load_fixture('badger', 'stoat')). gist.github.com/danni/1b2a0078e998ac080111
Danielle Madeley
1

Menurut saya perlengkapannya agak buruk. Jika database Anda sering berubah, memperbaruinya akan segera menjadi mimpi buruk. Sebenarnya bukan hanya pendapat saya saja, di buku "Two Scoops of Django" dijelaskan jauh lebih baik.

Sebagai gantinya saya akan menulis file Python untuk menyediakan pengaturan awal. Jika Anda membutuhkan sesuatu yang lebih saya sarankan Anda melihat Factory boy .

Jika Anda perlu memigrasi beberapa data, Anda harus menggunakan migrasi data .

Ada juga "Bakar Perlengkapan Anda, Gunakan Pabrik Model" tentang penggunaan perlengkapan.

Griffosx
sumber
1
Saya setuju pada poin Anda "sulit dipertahankan jika sering berubah", tetapi di sini perlengkapan hanya bertujuan untuk menyediakan data awal (dan minimal) saat menginstal proyek ...
Mickaël
1
Ini untuk pemuatan data satu kali, yang jika dilakukan dalam konteks migrasi masuk akal. Karena jika berada dalam migrasi, seseorang tidak perlu melakukan perubahan pada data json. Setiap perubahan skema yang memerlukan perubahan data lebih jauh harus ditangani melalui migrasi lain (pada saat itu data lain mungkin dalam database yang juga perlu dimodifikasi).
mtnpaul
0

Pada Django 2.1, saya ingin memuat beberapa model (Seperti nama negara misalnya) dengan data awal.

Tetapi saya ingin ini terjadi secara otomatis tepat setelah eksekusi migrasi awal.

Jadi saya pikir akan sangat bagus untuk memiliki sql/folder di dalam setiap aplikasi yang membutuhkan data awal untuk dimuat.

Kemudian di dalam sql/folder itu saya akan memiliki .sqlfile dengan DML yang diperlukan untuk memuat data awal ke dalam model yang sesuai, misalnya:

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

Untuk lebih deskriptif, seperti inilah tampilan aplikasi yang berisi sql/folder: masukkan deskripsi gambar di sini

Saya juga menemukan beberapa kasus di mana saya memerlukan sqlskrip untuk dieksekusi dalam urutan tertentu. Jadi saya memutuskan untuk mengawali nama file dengan nomor yang berurutan seperti yang terlihat pada gambar di atas.

Lalu saya membutuhkan cara untuk memuat apa pun SQLs tersedia di dalam folder aplikasi apa pun secara otomatis dengan melakukan python manage.py migrate.

Jadi saya membuat aplikasi lain bernama initial_data_migrationsdan kemudian saya menambahkan aplikasi ini ke daftar INSTALLED_APPSdalam settings.pyfile. Kemudian saya membuat migrationsfolder di dalam dan menambahkan file bernama run_sql_scripts.py( Yang sebenarnya adalah migrasi khusus ). Seperti yang terlihat pada gambar di bawah ini:

masukkan deskripsi gambar di sini

Saya membuat run_sql_scripts.pyagar menangani menjalankan semua sqlskrip yang tersedia dalam setiap aplikasi. Yang ini kemudian ditembakkan saat seseorang lari python manage.py migrate. Kustom ini migrationjuga menambahkan aplikasi yang terlibat sebagai dependensi, dengan cara itu ia mencoba menjalankan sqlpernyataan hanya setelah aplikasi yang diperlukan telah mengeksekusi 0001_initial.pymigrasinya (Kami tidak ingin mencoba menjalankan pernyataan SQL terhadap tabel yang tidak ada).

Inilah sumber dari skrip itu:

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

Saya harap seseorang menemukan ini bermanfaat, ini bekerja dengan baik untuk saya !. Jika Anda memiliki pertanyaan, beri tahu saya.

CATATAN: Ini mungkin bukan solusi terbaik karena saya baru saja memulai dengan django, namun masih ingin berbagi "How-to" ini dengan Anda semua karena saya tidak menemukan banyak informasi saat googling tentang ini.

Antony Fuentes Artavia
sumber