ฉันจะย้ายโมเดลออกจากแอพ django หนึ่งและไปยังแอพใหม่ได้อย่างไร


126

ฉันมีแอพ django ที่มีสี่รุ่นอยู่ในนั้น ตอนนี้ฉันรู้แล้วว่าหนึ่งในโมเดลเหล่านี้ควรอยู่ในแอพแยกต่างหาก ฉันติดตั้งทางทิศใต้สำหรับการย้ายข้อมูล แต่ฉันไม่คิดว่านี่เป็นสิ่งที่สามารถจัดการได้โดยอัตโนมัติ ฉันจะย้ายรุ่นใดรุ่นหนึ่งจากแอปเก่าไปเป็นรุ่นใหม่ได้อย่างไร

นอกจากนี้โปรดทราบว่าฉันจะต้องให้สิ่งนี้เป็นกระบวนการที่ทำซ้ำได้เพื่อที่ฉันจะได้ย้ายระบบการผลิตและอื่น ๆ


6
สำหรับ django 1.7 ขึ้นไปโปรดดูstackoverflow.com/questions/25648393/…
Rick Westera

คำตอบ:


184

วิธีการอพยพโดยใช้ทางใต้

สมมติว่าเรามีสองแอพ: ทั่วไปและเฉพาะ:

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

ตอนนี้เราต้องการย้ายโมเดล common.models.cat ไปยังแอพเฉพาะ (เจาะจงไปที่ specific.models.cat) ขั้นแรกให้ทำการเปลี่ยนแปลงในซอร์สโค้ดจากนั้นเรียกใช้:

$ 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

ตอนนี้เราต้องแก้ไขไฟล์การย้ายข้อมูลทั้งสอง:

#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

ตอนนี้การย้ายแอพทั้งสองตระหนักถึงการเปลี่ยนแปลงและชีวิตก็ลดน้อยลงเล็กน้อย :-) การตั้งค่าความสัมพันธ์ระหว่างการย้ายข้อมูลนี้เป็นกุญแจแห่งความสำเร็จ ตอนนี้ถ้าคุณทำ:

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

จะทำการโยกย้ายและ

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

จะโยกย้ายสิ่งต่างๆลง

โปรดสังเกตว่าสำหรับการอัปเกรดสคีมาฉันใช้แอพทั่วไปและสำหรับการดาวน์เกรดฉันใช้แอพเฉพาะ นั่นเป็นเพราะการพึ่งพาที่นี่ทำงานอย่างไร


1
ว้าวขอบคุณ. ฉันเรียนรู้ทางใต้ด้วยตัวเองตั้งแต่ถามคำถามนี้ แต่ฉันมั่นใจว่าสิ่งนี้จะช่วยคนอื่นได้มาก
Apreche

11
คุณอาจต้องทำการย้ายข้อมูลในตาราง django_content_type
spookylukey

1
คู่มือที่ยอดเยี่ยมจริงๆ @Potr ฉันสงสัยไม่ควรมี orm['contenttypes.contenttype'].objects.filter เส้นอยู่ด้านหลัง0003_create_catด้วยหรือไม่? ฉันต้องการแบ่งปันเคล็ดลับด้วย หากคุณมีดัชนีก็จะต้องได้รับการแก้ไขเช่นกัน ในกรณีของฉันมันเป็นดัชนีที่ไม่เหมือนใครดังนั้นการไปข้างหน้าของฉันจึงดูเหมือนว่า: db.delete_unique('common_cat', ['col1']) db.rename_table('common_cat', 'specific_cat') db.delete_unique('specific_cat', ['col1'])
Brad Pitcher

2
ในการเข้าถึงorm['contenttypes.contenttype']คุณต้องเพิ่ม--freeze contenttypesตัวเลือกให้กับschemamigrationคำสั่งของคุณด้วย
Gary

1
ในกรณีของฉัน (Django 1.5.7 และ South 1.0) .. ฉันต้องพิมพ์python manage.py schemamigration specific create_cat --auto --freeze commonเพื่อเข้าถึง cat model จากแอปทั่วไป
geoom

35

เพื่อสร้างคำตอบของPotr Czachurสถานการณ์ที่เกี่ยวข้องกับ ForeignKeys นั้นซับซ้อนกว่าและควรได้รับการจัดการที่แตกต่างกันเล็กน้อย

(ตัวอย่างต่อไปนี้สร้างจากcommonและspecificแอปที่อ้างถึงในคำตอบปัจจุบัน)

# common/models.py

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

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

จากนั้นจะเปลี่ยนเป็น

# common/models.py

from specific.models import Cat

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

# specific/models.py

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

วิ่ง

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

จะสร้างการย้ายข้อมูลต่อไปนี้ (ฉันตั้งใจละเว้นการเปลี่ยนแปลง Django ContentType - ดูคำตอบที่อ้างถึงก่อนหน้านี้สำหรับวิธีจัดการสิ่งนั้น):

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

อย่างที่คุณเห็นต้องเปลี่ยนแปลง FK เพื่ออ้างอิงตารางใหม่ เราจำเป็นต้องเพิ่มการพึ่งพาเพื่อให้เรารู้ว่าลำดับที่การโยกย้ายจะถูกนำไปใช้ (และที่โต๊ะจะมีอยู่ก่อนที่เราจะพยายามที่จะเพิ่ม FK ไป) แต่เรายังต้องให้แน่ใจกลิ้งถอยหลังทำงานเกินไปเพราะการพึ่งพานำไปใช้ในทิศทางตรงกันข้าม

# 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

ตามเอกสารใต้ , depends_onจะให้แน่ใจว่า0004_auto__add_catก่อนจะวิ่ง0009_auto__del_cat เมื่อมีการย้ายไปข้างหน้า แต่ในการสั่งซื้อตรงข้ามเมื่อมีการย้ายไปข้างหลัง หากเราปล่อยไว้db.rename_table('specific_cat', 'common_cat')ในการspecificย้อนกลับการcommonย้อนกลับจะล้มเหลวเมื่อพยายามโยกย้าย ForeignKey เนื่องจากตารางที่อ้างอิงไม่มีอยู่

หวังว่านี่จะใกล้เคียงกับสถานการณ์ "โลกแห่งความจริง" มากกว่าวิธีแก้ปัญหาที่มีอยู่และใครบางคนจะพบว่าสิ่งนี้เป็นประโยชน์ ไชโย!


แหล่งข้อมูลคงที่ในคำตอบนี้ละเว้นบรรทัดสำหรับการอัปเดตประเภทเนื้อหาซึ่งมีอยู่ในคำตอบของ Potr Czachur ซึ่งอาจทำให้เข้าใจผิด
Shai Berger

@ShaiBerger ฉันพูดถึงเรื่องนี้โดยเฉพาะ: "ฉันจงใจเพิกเฉยต่อการเปลี่ยนแปลงของ Django ContentType - ดูคำตอบที่อ้างถึงก่อนหน้านี้สำหรับวิธีจัดการกับสิ่งนั้น"
Matt Briançon

9

โมเดลไม่ได้เชื่อมต่อกับแอพอย่างแน่นหนาดังนั้นการเคลื่อนย้ายจึงค่อนข้างง่าย Django ใช้ชื่อแอพในชื่อของตารางฐานข้อมูลดังนั้นหากคุณต้องการย้ายแอพของคุณคุณสามารถเปลี่ยนชื่อตารางฐานข้อมูลผ่านALTER TABLEคำสั่งSQL หรือ - ง่ายกว่านั้นเพียงแค่ใช้db_tableพารามิเตอร์ในMetaคลาสของโมเดลของคุณเพื่ออ้างถึง ชื่อเก่า.

หากคุณเคยใช้ ContentTypes หรือความสัมพันธ์ทั่วไปที่ใดก็ได้ในโค้ดของคุณคุณอาจต้องการเปลี่ยนชื่อapp_labelประเภทเนื้อหาที่ชี้ไปที่โมเดลที่กำลังเคลื่อนที่เพื่อรักษาความสัมพันธ์ที่มีอยู่

แน่นอนว่าหากคุณไม่มีข้อมูลที่จะเก็บรักษาสิ่งที่ง่ายที่สุดที่ต้องทำคือวางตารางฐานข้อมูลทั้งหมดแล้วเรียกใช้./manage.py syncdbอีกครั้ง


2
ฉันจะทำอย่างไรกับการอพยพทางใต้
เมษายน

4

นี่เป็นอีกหนึ่งวิธีแก้ปัญหาที่ยอดเยี่ยมของ Potr เพิ่มสิ่งต่อไปนี้ในเฉพาะ / 0003_create_cat

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

เว้นแต่จะตั้งค่าการอ้างอิงนี้เป็นทิศใต้จะไม่รับประกันว่าcommon_catตารางจะมีอยู่ในเวลาที่เรียกใช้/ 0003_create_cat เฉพาะซึ่งทำให้เกิดdjango.db.utils.OperationalError: no such table: common_catข้อผิดพลาดที่คุณ

ทางใต้เรียกใช้การย้ายถิ่นตามลำดับศัพท์เว้นแต่จะตั้งค่าการอ้างอิงไว้อย่างชัดเจน เนื่องจากมีcommonมาก่อนที่specificการcommonย้ายข้อมูลทั้งหมดจะทำงานก่อนที่จะเปลี่ยนชื่อตารางดังนั้นจึงอาจไม่เกิดซ้ำในตัวอย่างดั้งเดิมที่แสดงโดย Potr แต่ถ้าคุณเปลี่ยนชื่อcommonไปapp2และspecificเพื่อapp1คุณจะทำงานในปัญหานี้


นี่ไม่ใช่ปัญหากับตัวอย่างของ Potr ใช้การย้ายข้อมูลเฉพาะเพื่อเปลี่ยนชื่อและการย้ายข้อมูลทั่วไปจะขึ้นอยู่กับการย้ายข้อมูลเฉพาะ หากมีการเรียกใช้เฉพาะก่อนคุณก็สบายดี หากมีการรัน common ก่อนการอ้างอิงจะทำการรันเฉพาะก่อนหน้านั้น ที่กล่าวว่าฉันสลับลำดับเมื่อทำสิ่งนี้ดังนั้นการเปลี่ยนชื่อจึงเกิดขึ้นเหมือนกันและการพึ่งพาเฉพาะจากนั้นคุณต้องเปลี่ยนการอ้างอิงตามที่คุณอธิบายไว้ข้างต้น
Emil Stenström

1
ฉันไม่เห็นด้วยกับคุณ จากมุมมองของฉันการแก้ปัญหาควรมีประสิทธิภาพและใช้งานได้โดยไม่ต้องพยายามทำให้พอใจ โซลูชันดั้งเดิมใช้ไม่ได้หากคุณเริ่มจากฐานข้อมูลใหม่และ syncdb / migrate ข้อเสนอของฉันแก้ไขได้ ไม่ว่าจะด้วยวิธีใดคำตอบของ Port ช่วยให้ฉันประหยัดเวลาได้มากขอชื่นชมเขา :)
Ihor Kaharlichenko

การไม่ทำเช่นนี้อาจทำให้การทดสอบล้มเหลวเช่นกัน (ดูเหมือนว่าจะเรียกใช้การย้ายข้อมูลทางทิศใต้อย่างสมบูรณ์เมื่อสร้างฐานข้อมูลการทดสอบ) ฉันเคยทำอะไรคล้าย ๆ กันมาก่อน Ihor จับดี :)
odinho - Velmont

4

กระบวนการนี้ฉันได้ตัดสินใจแล้วเนื่องจากฉันกลับมาที่นี่สองสามครั้งและตัดสินใจที่จะทำให้มันเป็นทางการ

เดิมสร้างขึ้นจาก คำตอบของ Potr Czachur และคำตอบของMatt Briançonโดยใช้ South 0.8.4

ขั้นตอนที่ 1. ค้นหาความสัมพันธ์ของคีย์ต่างประเทศของเด็ก

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

ดังนั้นในกรณีขยายนี้เราได้ค้นพบรูปแบบอื่นที่เกี่ยวข้องเช่น:

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

    ...

ขั้นตอนที่ 2. สร้างการย้ายข้อมูล

# 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

ขั้นตอนที่ 3 การควบคุมแหล่งที่มา: ยอมรับการเปลี่ยนแปลงจนถึงตอนนี้

ทำให้เป็นกระบวนการที่ทำซ้ำได้มากขึ้นหากคุณพบความขัดแย้งในการผสานเช่นเพื่อนในทีมที่เขียนการย้ายข้อมูลในแอปที่อัปเดต

ขั้นตอนที่ 4. เพิ่มการอ้างอิงระหว่างการย้ายข้อมูล

โดยทั่วไปขึ้นอยู่กับสถานะปัจจุบันของทุกอย่างและทุกอย่างนั้นขึ้นอยู่กับcreate_kittycatcreate_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'),
    )
    ...

ขั้นตอนที่ 5. ตารางเปลี่ยนชื่อการเปลี่ยนแปลงที่เราต้องการทำ

# 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

ขั้นตอนที่ 6. เฉพาะในกรณีที่คุณต้องการย้อนกลับ () เพื่อทำงานและรับ KeyError ทำงานย้อนกลับ

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

ขั้นตอนที่ 7. ทดสอบ - สิ่งที่ได้ผลสำหรับฉันอาจไม่เพียงพอสำหรับสถานการณ์ในชีวิตจริงของคุณ :)

python manage.py migrate

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

3

ดังนั้นการใช้คำตอบเดิมจาก @Potr ด้านบนจึงไม่ได้ผลสำหรับฉันใน South 0.8.1 และ Django 1.5.1 ฉันโพสต์สิ่งที่ได้ผลสำหรับฉันด้านล่างโดยหวังว่าจะเป็นประโยชน์ต่อผู้อื่น

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';")

1

ฉันจะให้คำตอบที่ชัดเจนมากขึ้นของหนึ่งในสิ่งที่ Daniel Roseman แนะนำในคำตอบของเขา ...

หากคุณเพียงแค่เปลี่ยนdb_tableแอตทริบิวต์ Meta ของโมเดลที่คุณย้ายให้ชี้ไปที่ชื่อตารางที่มีอยู่ (แทนที่จะเป็นชื่อใหม่ Django จะให้ถ้าคุณทิ้งและทำ a syncdb) คุณจะสามารถหลีกเลี่ยงการย้ายถิ่นใต้ที่ซับซ้อนได้ เช่น:

เดิม:

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

หลังจากย้าย:

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

ตอนนี้คุณต้องทำการย้ายข้อมูลเพื่ออัปเดตapp_labelสำหรับMyModelใน django_content_typeตารางและคุณควรจะไป ...

เรียกใช้./manage.py datamigration django update_content_typeจากนั้นแก้ไขไฟล์ที่ South สร้างให้คุณ:

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()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.