Bagaimana cara memigrasi model dari satu aplikasi Django ke aplikasi baru?

126

Saya memiliki aplikasi Django dengan empat model di dalamnya. Saya menyadari sekarang bahwa salah satu model ini harus di aplikasi yang terpisah. Saya memang telah menginstal selatan untuk migrasi, tetapi saya tidak berpikir ini adalah sesuatu yang dapat ditangani secara otomatis. Bagaimana saya bisa memigrasi salah satu model dari aplikasi lama ke yang baru?

Juga, perlu diingat bahwa saya akan membutuhkan ini untuk menjadi proses yang berulang, sehingga saya dapat memigrasi sistem produksi dan semacamnya.

Apreche
sumber
6
Untuk django 1.7 ke atas lihat stackoverflow.com/questions/25648393/…
Rick Westera

Jawaban:

184

Cara bermigrasi menggunakan selatan.

Katakanlah kita punya dua aplikasi: umum dan spesifik:

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

Sekarang kami ingin memindahkan model common.models.cat ke aplikasi tertentu (tepatnya ke specific.models.cat). Pertama-tama buat perubahan dalam kode sumber lalu jalankan:

$ 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

Sekarang kita perlu mengedit kedua file migrasi:

#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

Sekarang kedua migrasi aplikasi menyadari perubahan dan hidup sedikit menyebalkan :-) Mengatur hubungan antara migrasi ini adalah kunci keberhasilan. Sekarang jika Anda melakukannya:

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

akan melakukan migrasi keduanya, dan

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

akan memigrasi semuanya.

Perhatikan bahwa untuk meningkatkan skema saya menggunakan aplikasi umum dan untuk menurunkan versi, saya menggunakan aplikasi tertentu. Itu karena cara ketergantungan di sini bekerja.

Potr Czachur
sumber
1
Wow terima kasih. Saya belajar ke selatan sendiri sejak mengajukan pertanyaan ini, tetapi saya yakin ini akan sangat membantu orang lain.
Apreche
11
Anda mungkin juga perlu melakukan migrasi data dalam tabel django_content_type.
spookylukey
1
Panduan @Potr yang benar-benar hebat. Saya ingin tahu, bukankah seharusnya ada orm['contenttypes.contenttype'].objects.filter garis di bagian belakang 0003_create_catjuga? Saya juga ingin berbagi tip. Jika Anda memiliki indeks, mereka perlu dimodifikasi juga. Dalam kasus saya itu adalah indeks unik, sehingga pemain depan saya terlihat seperti ini: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher
2
Untuk mengakses orm['contenttypes.contenttype'], Anda juga perlu menambahkan --freeze contenttypesopsi ke schemamigrationperintah Anda .
Gary
1
Dalam kasus saya (Django 1.5.7 dan South 1.0) .. Saya harus mengetik python manage.py schemamigration specific create_cat --auto --freeze commonuntuk mengakses model kucing dari aplikasi umum.
geoom
35

Untuk membangun di Potr Czachur 's jawaban , situasi yang melibatkan ForeignKeys lebih rumit dan harus ditangani sedikit berbeda.

(Contoh berikut dibangun di atas commondan specificaplikasi yang disebut dalam jawaban saat ini).

# common/models.py

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

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

akan berubah menjadi

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

Lari

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

akan menghasilkan migrasi berikut (saya sengaja mengabaikan perubahan Django ContentType — lihat jawaban yang dirujuk sebelumnya untuk cara mengatasinya):

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

Seperti yang Anda lihat, FK harus diubah untuk referensi tabel baru. Kita perlu menambahkan ketergantungan sehingga kita tahu urutan migrasi akan diterapkan (dan dengan demikian bahwa meja akan ada sebelum kita mencoba untuk menambahkan FK untuk itu) tapi kita juga perlu memastikan bergulir mundur bekerja juga karena yang ketergantungan bergantung pada arah sebaliknya .

# 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

Per dokumentasi Selatan , depends_onakan memastikan bahwa 0004_auto__add_catberjalan sebelum 0009_auto__del_cat ketika bermigrasi ke depan tetapi dalam urutan yang berlawanan ketika bermigrasi ke belakang . Jika kami meninggalkan db.rename_table('specific_cat', 'common_cat')di specificrollback, commonrollback akan gagal ketika mencoba untuk memigrasi ForeignKey karena tabel tabel yang dirujuk tidak ada.

Semoga ini lebih dekat dengan situasi "dunia nyata" daripada solusi yang ada dan seseorang akan menemukan ini bermanfaat. Bersulang!

Matt Briançon
sumber
Sumber tetap dalam jawaban ini menghilangkan baris untuk memperbarui tipe konten, yang ada dalam jawaban Potr Czachur. Ini bisa menyesatkan.
Shai Berger
@ShaiBerger saya membahasnya secara khusus: "Saya sengaja mengabaikan perubahan Django ContentType — lihat jawaban yang dirujuk sebelumnya untuk bagaimana mengatasinya."
Matt Briançon
9

Model tidak terlalu erat dengan aplikasi, jadi pemindahan cukup sederhana. Django menggunakan nama aplikasi dalam nama tabel database, jadi jika Anda ingin memindahkan aplikasi, Anda dapat mengubah nama tabel database melalui ALTER TABLEpernyataan SQL , atau - bahkan lebih sederhana - cukup gunakan db_tableparameter di Metakelas model Anda untuk merujuk ke nama lama.

Jika Anda telah menggunakan ContentTypes atau hubungan umum di mana saja dalam kode Anda sejauh ini, Anda mungkin ingin mengubah nama tipe konten app_labelyang menunjuk pada model yang bergerak, sehingga hubungan yang ada dipertahankan.

Tentu saja, jika Anda tidak memiliki data sama sekali untuk disimpan, hal termudah untuk dilakukan adalah dengan menjatuhkan tabel database sepenuhnya dan berjalan ./manage.py syncdbkembali.

Daniel Roseman
sumber
2
Bagaimana saya melakukannya dengan migrasi selatan?
Apreche
4

Inilah satu lagi perbaikan untuk solusi luar biasa Potr. Tambahkan berikut ini ke / 0003_create_cat tertentu

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

Kecuali jika dependensi ini ditetapkan ke Selatan, tidak akan menjamin bahwa common_cattabel ada pada saat spesifik / 0003_create_cat dijalankan, melemparkan django.db.utils.OperationalError: no such table: common_catkesalahan pada Anda.

South menjalankan migrasi dalam urutan leksikografis kecuali ketergantungan ditetapkan secara eksplisit. Sejak commondatang sebelum specificsemua commonmigrasi akan dijalankan sebelum penggantian nama tabel, jadi mungkin tidak akan mereproduksi dalam contoh asli yang ditunjukkan oleh Potr. Tetapi jika Anda mengubah nama commonuntuk app2dan specificuntuk app1Anda akan mengalami masalah ini.

Ihor Kaharlichenko
sumber
Ini sebenarnya bukan masalah dengan contoh Potr. Ia menggunakan migrasi spesifik untuk mengubah nama, dan migrasi umum bergantung pada yang spesifik. Jika spesifik dijalankan terlebih dahulu, Anda baik-baik saja. Jika common dijalankan terlebih dahulu, dependensi akan membuat jalankan khusus sebelum itu. Yang mengatakan, saya bertukar urutan ketika melakukan ini, sehingga penamaan ulang terjadi secara umum, dan ketergantungan pada spesifik, dan kemudian Anda perlu mengubah ketergantungan seperti yang Anda jelaskan di atas.
Emil Stenström
1
Saya tidak bisa setuju dengan Anda. Dari sudut pandang saya solusinya harus kuat dan berfungsi hanya tanpa berusaha untuk menyenangkannya. Solusi asli tidak berfungsi jika Anda memulai dari db baru dan syncdb / migrasi. Proposal saya memperbaikinya. Apa pun caranya, jawaban Port menghemat banyak waktu, pujian kepadanya :)
Ihor Kaharlichenko
Tidak melakukan hal ini dapat membuat tes gagal juga (mereka tampaknya selalu menjalankan migrasi selatan penuh saat membuat database pengujian mereka). Saya pernah melakukan hal serupa sebelumnya. Tangkapan yang bagus Ihor :)
odinho - Velmont
4

Proses yang saya selesaikan sejak saya kembali ke sini beberapa kali dan memutuskan untuk meresmikannya.

Ini awalnya dibangun di atas jawaban Potr Czachur dan jawaban Matt Briançon , menggunakan South 0.8.4

Langkah 1. Temukan hubungan kunci asing anak

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

Jadi dalam kasus yang diperluas ini, kami telah menemukan model terkait lainnya seperti:

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

    ...

Langkah 2. Buat migrasi

# 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

Langkah 3. Kontrol sumber: Komit perubahan sejauh ini.

Jadikan ini proses yang lebih berulang jika Anda mengalami konflik gabungan seperti rekan satu tim menulis migrasi pada aplikasi yang diperbarui.

Langkah 4. Tambahkan dependensi di antara migrasi.

Pada dasarnya create_kittycattergantung pada keadaan saat ini dari segalanya, dan semuanya kemudian tergantung pada 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'),
    )
    ...

Langkah 5. Tabel ganti nama perubahan yang ingin kita buat.

# 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

Langkah 6. Hanya jika Anda perlu mundur () untuk bekerja DAN mendapatkan KeyError berjalan mundur.

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

Langkah 7. Uji - apa yang berhasil untuk saya mungkin tidak cukup untuk situasi kehidupan nyata Anda :)

python manage.py migrate

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

Jadi menggunakan respons asli dari @Potr di atas tidak berfungsi untuk saya di South 0.8.1 dan Django 1.5.1. Saya memposting apa yang berhasil bagi saya di bawah ini dengan harapan bermanfaat bagi orang lain.

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

Saya akan memberikan versi yang lebih eksplisit dari salah satu hal yang disarankan Daniel Roseman dalam jawabannya ...

Jika Anda hanya mengubah db_tableatribut Meta dari model yang telah Anda pindahkan untuk menunjuk ke nama tabel yang ada (bukan nama baru yang Django akan berikan jika Anda menjatuhkan dan melakukan a syncdb) maka Anda dapat menghindari migrasi Selatan yang rumit. misalnya:

Asli:

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

Setelah pindah:

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

Sekarang Anda hanya perlu melakukan migrasi data untuk memperbarui app_labeluntuk MyModeldi django_content_typemeja dan Anda harus baik untuk pergi ...

Jalankan ./manage.py datamigration django update_content_typekemudian edit file yang dibuat Selatan untuk Anda:

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()
Anentropik
sumber