关于概念模型的相关介绍(概念模型的基本概念有哪些)
732
2022-05-30
目录
示例案例:将 django 模型移动到另一个应用程序
设置:准备您的环境
设置 Python 虚拟环境
创建一个 Django 项目
创建 Django 应用程序
生成并应用初始迁移
生成样本数据
漫长的道路:将数据复制到新的 Django 模型
创建新模型
将数据复制到新模型
更新新模型的外键
删除旧模型
奖励:逆转迁移
处理特殊情况
总结:复制数据的利弊
捷径:将新的 Django 模型引用到旧表
创建新模型
消除对数据库的更改
奖励:对新模型进行更改
总结:更改模型参考的利弊
Django 方式:重命名表
创建新模型
重命名旧表
奖励:了解内省
总结:重命名表的利弊
指南:选择最佳方法
结论
如果您曾经考虑过重构Django 应用程序,那么您可能会发现自己需要移动 Django 模型。有几种方法可以使用 Django migrations将 Django 模型从一个应用程序移动到另一个应用程序,但不幸的是,它们都不是直接的。
在Django 应用程序之间移动模型通常是一项非常复杂的任务,涉及复制数据、更改约束和重命名对象。由于这些复杂性,Django对象关系映射器 (ORM)不提供可以检测和自动化整个过程的内置迁移操作。相反,ORM 提供了一组低级迁移操作,允许 Django 开发人员在迁移框架中自己实现流程。
在本教程中,您将学习:
如何将 Django 模型从一个应用程序移动到另一个应用程序
如何使用高级功能Django的迁移命令行界面(CLI),例如sqlmigrate,showmigrations和sqlsequencereset
如何制作和检查迁移计划
如何使迁移可逆以及如何逆向迁移
什么是自省以及 Django 如何在迁移中使用它
完成本教程后,您将能够根据您的特定用例选择将 Django 模型从一个应用程序移动到另一个应用程序的最佳方法。
示例案例:将 Django 模型移动到另一个应用程序
在本教程中,您将在商店应用程序上工作。您的商店将从两个 Django 应用程序开始:
catalog:此应用程序用于存储有关产品和产品类别的数据。
sale:此应用程序用于记录和跟踪产品销售。
完成这两个应用程序的设置后,您要将一个名为 Django 的模型移动Product到一个名为的新应用程序中product。在此过程中,您将面临以下挑战:
被移动的模型与其他模型具有外键关系。
其他模型与被移动的模型具有外键关系。
被移动的模型在其中一个字段上有一个索引(除了主键)。
这些挑战的灵感来自现实生活中的重构过程。克服这些问题后,您就可以为您的特定用例规划类似的迁移过程了。
设置:准备您的环境
在开始移动之前,您需要设置项目的初始状态。本教程使用在 Python 3.8 上运行的 Django 3,但您可以在其他版本中使用类似的技术。
设置 Python 虚拟环境
首先,在新目录中创建您的虚拟环境:
$ mkdir django-move-model-experiment $ cd django-move-model-experiment $ python -m venv venv
有关创建虚拟环境的分步说明,请查看Python 虚拟环境:入门。
创建一个 Django 项目
在您的终端中,激活虚拟环境并安装 Django:
$ source venv/bin/activate $ pip install django Collecting django Collecting pytz (from django) Collecting asgiref~=3.2 (from django) Collecting sqlparse>=0.2.2 (from django) Installing collected packages: pytz, asgiref, sqlparse, django Successfully installed asgiref-3.2.3 django-3.0.4 pytz-2019.3 sqlparse-0.3.1
您现在已准备好创建 Django 项目。使用django-admin startproject创建了一个名为django-move-model-experiment:
$ django-admin startproject django-move-model-experiment $ cd django-move-model-experiment
运行此命令后,您将看到 Django 创建了新文件和目录。有关如何启动新 Django 项目的更多信息,请查看启动 Django 项目。
创建 Django 应用程序
现在你有了一个新的 Django 项目,用你商店的产品目录创建一个应用程序:
$ python manage.py startapp catalog
接下来,将以下模型添加到新catalog应用程序:
# catalog/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=100) class Product(models.Model): name = models.CharField(max_length=100, db_index=True) category = models.ForeignKey(Category, on_delete=models.CASCADE)
您已成功在您的应用程序中创建Category和Product建模catalog。现在您有了目录,您想开始销售您的产品。创建另一个用于销售的应用程序:
$ python manage.py startapp sale
将以下Sale模型添加到新sale应用程序:
# sale/models.py from django.db import models from catalog.models import Product class Sale(models.Model): created = models.DateTimeField() product = models.ForeignKey(Product, on_delete=models.PROTECT)
请注意,Sale模型引用的Product模型使用ForeignKey。
生成并应用初始迁移
要完成设置,请生成迁移并应用它们:
$ python manage.py makemigrations catalog sale Migrations for 'catalog': catalog/migrations/0001_initial.py - Create model Category - Create model Product Migrations for 'sale': sale/migrations/0001_initial.py - Create model Sale $ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, catalog, contenttypes, sale, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying catalog.0001_initial... OK Applying sale.0001_initial... OK Applying sessions.0001_initial... OK
有关 Django 迁移的更多信息,请查看Django 迁移:入门。迁移完成后,您现在可以创建一些示例数据了!
生成样本数据
为了使迁移场景尽可能真实,请从终端窗口激活Django shell:
$ python manage.py shell
接下来,创建以下对象:
>>>
>>> from catalog.models import Category, Product >>> clothes = Category.objects.create(name='Clothes') >>> shoes = Category.objects.create(name='Shoes') >>> Product.objects.create(name='Pants', category=clothes) >>> Product.objects.create(name='Shirt', category=clothes) >>> Product.objects.create(name='Boots', category=shoes)
您创建了两个类别,'Shoes'并且'Clothes'. 接下来,您将两种产品'Pants'和'Shirt',添加到'Clothes'类别中,将一种产品'Boots', 添加到'Shoes'类别中。
恭喜!您已完成项目初始状态的设置。在现实生活中,这是您开始计划重构的地方。本教程中介绍的三种方法中的每一种都将从这一点开始。
漫长的道路:将数据复制到新的 Django 模型
要开始工作,您将走很长的路:
创建新模型
将数据复制到它
放下旧桌子
这种方法有一些你应该注意的陷阱。您将在以下部分详细探索它们。
创建新模型
首先创建一个新的product应用程序。从终端执行以下命令:
$ python manage.py startapp product
运行此命令后,您会注意到一个名为的新目录product已添加到项目中。
要将新应用程序注册到现有的 Django 项目,请将其添加到INSTALLED_APPSDjango 的列表中settings.py:
--- a/store/store/settings.py +++ b/store/store/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'catalog', 'sale', + 'product', ] MIDDLEWARE = [
您的新product应用现已注册到 Django。接下来,Product在新product应用中创建一个模型。您可以从catalog应用程序复制代码:
# product/models.py from django.db import models from catalog.models import Category class Product(models.Model): name = models.CharField(max_length=100, db_index=True) category = models.ForeignKey(Category, on_delete=models.CASCADE)
现在您已经定义了模型,尝试为它生成迁移:
$ python manage.py makemigrations product SystemCheckError: System check identified some issues: ERRORS: catalog.Product.category: (fields.E304) Reverse accessor for 'Product.category' clashes with reverse accessor for 'Product.category'. HINT: Add or change a related_name argument to the definition for 'Product.category' or 'Product.category'. product.Product.category: (fields.E304) Reverse accessor for 'Product.category' clashes with reverse accessor for 'Product.category'. HINT: Add or change a related_name argument to the definition for 'Product.category' or 'Product.category'.
该错误表示 Django 发现两个模型具有相同的字段反向访问器category。这是因为有两个命名Product的Category模型引用了该模型,从而产生了冲突。
当您向模型添加外键时,Django 会在相关模型中创建一个反向访问器。在这种情况下,反向访问器是products。反向访问,您可以访问相关的对象是这样的:category.products。
新模型是您想要保留的模型,因此要解决此冲突,请从 中的旧模型中删除反向访问器catalog/models.py:
--- a/store/catalog/models.py +++ b/store/catalog/models.py @@ -7,4 +7,4 @@ class Category(models.Model): class Product(models.Model): name = models.CharField(max_length=100, db_index=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE) + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='+')
该属性related_name可用于显式设置反向访问器的相关名称。在这里,您使用特殊值+,它指示 Django 不要创建反向访问器。
现在为catalog应用程序生成迁移:
$ python manage.py makemigrations catalog Migrations for 'catalog': catalog/migrations/0002_auto_20200124_1250.py - Alter field category on product
暂时不要应用此迁移!一旦发生这种变化,使用反向访问器的代码可能会中断。
现在反向访问器之间没有冲突,尝试为新product应用程序生成迁移:
$ python manage.py makemigrations product Migrations for 'product': product/migrations/0001_initial.py - Create model Product
伟大的!您已准备好进入下一步。
将数据复制到新模型
在上一步中,您创建了一个新product应用程序,其Product模型与您要移动的模型相同。下一步是将数据从旧模型移动到新模型。
要创建数据迁移,请从终端执行以下命令:
$ python manage.py makemigrations product --empty Migrations for 'product': product/migrations/0002_auto_20200124_1300.py
编辑新的迁移文件,并添加从旧表复制数据的操作:
from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('product', '0001_initial'), ] operations = [ migrations.RunSQL(""" INSERT INTO product_product ( id, name, category_id ) SELECT id, name, category_id FROM catalog_product; """, reverse_sql=""" INSERT INTO catalog_product ( id, name, category_id ) SELECT id, name, category_id FROM product_product; """) ]
要在迁移中执行 SQL,请使用特殊的RunSQL迁移命令。第一个参数是要应用的 SQL。您还可以使用reverse_sql参数提供一个操作来逆转迁移。
当您发现错误并想要回滚更改时,反向迁移会派上用场。大多数内置迁移操作都可以撤消。例如,添加字段的反向操作是删除该字段。创建新表的相反操作是删除该表。通常最好提供reverse_SQL给,RunSQL以便在出现问题时可以回溯。
在这种情况下,正向迁移操作会将数据从product_product插入到 中catalog_product。向后操作将执行完全相反的操作,将数据从catalog_productinto 插入product_product。通过向 Django 提供反向操作,您将能够在发生灾难时反向迁移。
此时,您仍处于迁移过程的一半。但是这里有一个教训要学习,所以继续应用迁移:
$ python manage.py migrate product Operations to perform: Apply all migrations: product Running migrations: Applying product.0001_initial... OK Applying product.0002_auto_20200124_1300... OK
在继续下一步之前,请尝试创建一个新产品:
>>>
>>> from product.models import Product >>> Product.objects.create(name='Fancy Boots', category_id=2) Traceback (most recent call last): File "/venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute return self.cursor.execute(sql, params) psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "product_product_pkey" DETAIL: Key (id)=(1) already exists.
当您使用自动递增的主键时,Django在数据库中创建一个序列来为新对象分配唯一标识符。请注意,例如,您没有提供新产品的 ID。您通常不想提供 ID,因为您希望数据库使用序列为您分配主键。但是,在这种情况下,新表为新产品提供了 ID,1即使该 ID 已存在于表中。
那么,出了什么问题?当您将数据复制到新表时,您没有同步序列。要同步序列,您可以使用另一个名为sqlsequencereset. 该命令生成一个脚本,用于根据表中的现有数据设置序列的当前值。此命令通常用于使用预先存在的数据填充新模型。
使用sqlsequencereset产生脚本同步序列:
$ python manage.py sqlsequencereset product BEGIN; SELECT setval(pg_get_serial_sequence('"product_product"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "product_product"; COMMIT;
该命令生成的脚本是特定于数据库的。在这种情况下,数据库是 PostgreSQL。该脚本将序列的当前值设置为序列应产生的下一个值,即表中的最大 ID 加一。
最后,将代码片段添加到数据迁移中:
--- a/store/product/migrations/0002_auto_20200124_1300.py +++ b/store/product/migrations/0002_auto_20200124_1300.py @@ -22,6 +22,8 @@ class Migration(migrations.Migration): category_id FROM catalog_product; + + SELECT setval(pg_get_serial_sequence('"product_product"','id'), coalesce(max("id"), 1), max("id") IS NOT null) FROM "product_product"; """, reverse_sql=""" INSERT INTO catalog_product ( id,
该代码段将在您应用迁移时同步序列,从而解决您在上面遇到的序列问题。
这种了解同步序列的绕道在您的代码中造成了一些混乱。要清理它,请从 Django shell 中删除新模型中的数据:
>>>
>>> from product.models import Product >>> Product.objects.all().delete() (3, {'product.Product': 3})
现在您复制的数据已被删除,您可以反向迁移。要撤消迁移,请迁移到以前的迁移:
$ python manage.py showmigrations product product [X] 0001_initial [X] 0002_auto_20200124_1300 $ python manage.py migrate product 0001_initial Operations to perform: Target specific migration: 0001_initial, from product Running migrations: Rendering model states... DONE Unapplying product.0002_auto_20200124_1300... OK
您首先使用该命令showmigrations列出应用到应用程序的迁移product。输出显示两个迁移都已应用。然后,您0002_auto_20200124_1300通过迁移到先前的迁移来逆转迁移0001_initial。
如果showmigrations再次执行,您将看到第二次迁移不再标记为已应用:
$ python manage.py showmigrations product product [X] 0001_initial [ ] 0002_auto_20200124_1300
空框确认第二次迁移已被撤消。现在你有了一个干净的石板,用新代码运行迁移:
$ python manage.py migrate product Operations to perform: Apply all migrations: product Running migrations: Applying product.0002_auto_20200124_1300... OK
迁移已成功应用。确保您现在可以Product在 Django shell 中创建一个新的:
>>>
>>> from product.models import Product >>> Product.objects.create(name='Fancy Boots', category_id=2)
惊人的!您的辛勤工作得到了回报,您已准备好进行下一步。
更新新模型的外键
旧表当前有其他表使用ForeignKey字段引用它。在删除旧模型之前,您需要更改引用旧模型的模型,以便它们引用新模型。
一个仍然引用旧模型的模型Sale在sale应用程序中。更改Sale模型中的外键以引用新Product模型:
--- a/store/sale/models.py +++ b/store/sale/models.py @@ -1,6 +1,6 @@ from django.db import models -from catalog.models import Product +from product.models import Product class Sale(models.Model): created = models.DateTimeField()
生成迁移并应用它:
$ python manage.py makemigrations sale Migrations for 'sale': sale/migrations/0002_auto_20200124_1343.py - Alter field product on sale $ python manage.py migrate sale Operations to perform: Apply all migrations: sale Running migrations: Applying sale.0002_auto_20200124_1343... OK
该Sale模型现在引用Product了product应用程序中的新模型。因为您已经将所有数据复制到新模型中,所以没有违反约束。
删除旧模型
上一步消除了对旧Product模型的所有引用。现在可以安全地从catalog应用程序中删除旧模型:
--- a/store/catalog/models.py +++ b/store/catalog/models.py @@ -3,8 +3,3 @@ from django.db import models class Category(models.Model): name = models.CharField(max_length=100) - - -class Product(models.Model): - name = models.CharField(max_length=100, db_index=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='+')
生成迁移但尚未应用它:
$ python manage.py makemigrations Migrations for 'catalog': catalog/migrations/0003_delete_product.py - Delete model Product
为了确保只有在复制数据后才删除旧模型,请添加以下依赖项:
--- a/store/catalog/migrations/0003_delete_product.py +++ b/store/catalog/migrations/0003_delete_product.py @@ -7,6 +7,7 @@ class Migration(migrations.Migration): dependencies = [ ('catalog', '0002_auto_20200124_1250'), + ('sale', '0002_auto_20200124_1343'), ] operations = [
添加此依赖项非常重要。跳过这一步可能会产生可怕的后果,包括丢失数据。有关迁移文件和迁移之间依赖关系的更多信息,请查看深入挖掘 Django 迁移。
注意:迁移的名称包括它的生成日期和时间。如果您遵循自己的代码,则名称的这些部分将有所不同。
现在您已经添加了依赖项,请应用迁移:
$ python manage.py migrate catalog Operations to perform: Apply all migrations: catalog Running migrations: Applying catalog.0003_delete_product... OK
转移现已完成!通过创建新模型并将数据处理到其中,您已成功地将Product模型从catalog应用程序移至新product应用程序。
奖励:逆转迁移
Django 迁移的好处之一是它们是可逆的。迁移可逆是什么意思?如果您犯了错误,那么您可以撤消迁移,数据库将恢复到应用迁移之前的状态。
还记得你之前是如何提供reverse_sql给的RunSQL吗?嗯,这就是值得的地方。
在新数据库上应用所有迁移:
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, catalog, contenttypes, product, sale, sessions Running migrations: Applying product.0001_initial... OK Applying product.0002_auto_20200124_1300... OK Applying sale.0002_auto_20200124_1343... OK Applying catalog.0003_delete_product... OK
现在,使用特殊关键字zero反转它们:
$ python manage.py migrate product zero Operations to perform: Unapply all migrations: product Running migrations: Rendering model states... DONE Unapplying catalog.0003_delete_product... OK Unapplying sale.0002_auto_20200124_1343... OK Unapplying product.0002_auto_20200124_1300... OK Unapplying product.0001_initial... OK
数据库现在恢复到其原始状态。如果你部署了这个版本,发现有错误,那么就可以逆向了!
处理特殊情况
当您将模型从一个应用程序移动到另一个应用程序时,某些 Django 功能可能需要特别注意。特别是,添加或修改数据库约束以及使用泛型关系都需要格外小心。
向带有数据的表添加约束可能是在实时系统上执行的危险操作。要添加约束,数据库必须首先验证它。在验证期间,数据库获取表上的锁,这可能会阻止其他操作,直到进程完成。
某些约束(例如NOT NULL和CHECK)可能需要对表进行全面扫描以验证新数据是否有效。其他约束(例如FOREIGN KEY)需要使用另一个表进行验证,这可能需要一些时间,具体取决于引用表的大小。
如果您使用的是通用关系,那么您可能需要一个额外的步骤。通用关系使用模型的主键和内容类型 ID 来引用任何模型表中的行。旧模型和新模型没有相同的内容类型 ID,因此通用连接可能会中断。这有时会被忽视,因为通用外键的完整性不是由数据库强制执行的。
有两种处理通用外键的方法:
将新模型的内容类型 ID 更新为旧模型的内容类型 ID。
将任何引用表的内容类型 ID 更新为新模型的内容类型 ID。
无论您选择哪种方式,请确保在部署到生产之前对其进行正确测试。
总结:复制数据的利弊
通过复制数据将 Django 模型移动到另一个应用程序有其优点和缺点。以下是与此方法相关的一些优点:
它由 ORM 支持:使用内置迁移操作执行此转换可确保正确的数据库支持。
它是可逆的:如有必要,可以逆转此迁移。
以下是与此方法相关的一些缺点:
它很慢:复制大量数据可能需要时间。
它需要停机:在将旧表复制到新表时更改旧表中的数据将导致转换期间数据丢失。为了防止这种情况发生,停机是必要的。
它需要手动工作来同步数据库:将数据加载到现有表需要同步序列和通用外键。
正如您将在以下部分中看到的,使用这种方法将 Django 模型移动到另一个应用程序比其他方法花费的时间要长得多。
捷径:将新的 Django 模型引用到旧表
在前面的方法中,您将所有数据复制到新表中。迁移需要停机,并且可能需要很长时间才能完成,具体取决于要复制的数据量。
如果不是复制数据,而是更改新模型以引用旧表会怎样?
创建新模型
这一次,您将一次对模型进行所有更改,然后让 Django 生成所有迁移。
首先,Product从catalog应用程序中删除模型:
--- a/store/catalog/models.py +++ b/store/catalog/models.py @@ -3,8 +3,3 @@ from django.db import models class Category(models.Model): name = models.CharField(max_length=100) - - -class Product(models.Model): - name = models.CharField(max_length=100, db_index=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE)
您已从应用程序中删除Product模型catalog。现在将Product模型移动到新product应用程序:
# store/product/models.py from django.db import models from catalog.models import Category class Product(models.Model): name = models.CharField(max_length=100, db_index=True) category = models.ForeignKey(Category, on_delete=models.CASCADE)
现在该Product模型存在于product应用程序中,您可以更改对旧Product模型的任何引用以引用新Product模型。在这种情况下,您需要将外键更改sale为引用product.Product:
--- a/store/sale/models.py +++ b/store/sale/models.py @@ -1,6 +1,6 @@ from django.db import models -from catalog.models import Product +from product.models import Product class Sale(models.Model): created = models.DateTimeField()
在继续生成迁移之前,您需要对新Product模型再做一个小的更改:
--- a/store/product/models.py +++ b/store/product/models.py @@ -5,3 +5,6 @@ from catalog.models import Category class Product(models.Model): name = models.CharField(max_length=100, db_index=True) category = models.ForeignKey(Category, on_delete=models.CASCADE) + + class Meta: + db_table = 'catalog_product'
Django 模型有一个Meta名为db_table. 使用此选项,您可以提供一个表名来代替 Django 生成的表名。在表名与 Django 的命名约定不匹配的现有数据库模式上设置 ORM 时,最常使用此选项。
在这种情况下,您在product应用程序中设置表的名称以引用应用程序中的现有表catalog。
要完成设置,请生成迁移:
$ python manage.py makemigrations sale product catalog Migrations for 'catalog': catalog/migrations/0002_remove_product_category.py - Remove field category from product catalog/migrations/0003_delete_product.py - Delete model Product Migrations for 'product': product/migrations/0001_initial.py - Create model Product Migrations for 'sale': sale/migrations/0002_auto_20200104_0724.py - Alter field product on sale
在继续之前,请使用标志生成迁移计划:--plan
$ python manage.py migrate --plan Planned operations: catalog.0002_remove_product_category Remove field category from product product.0001_initial Create model Product sale.0002_auto_20200104_0724 Alter field product on sale catalog.0003_delete_product Delete model Product
命令的输出列出了 Django 将应用迁移的顺序。
消除对数据库的更改
这种方法的主要好处是您实际上不对数据库进行任何更改,只对代码进行了更改。要消除对数据库的更改,您可以使用特殊的迁移操作SeparateDatabaseAndState。
SeparateDatabaseAndState可用于修改 Django 在迁移期间执行的操作。有关如何使用的更多信息SeparateDatabaseAndState,请查看如何在不停机的情况下在 Django 中创建索引。
如果您查看 Django 生成的迁移内容,您会看到 Django 创建了一个新模型并删除了旧模型。如果执行这些迁移,那么数据将丢失,并且表将创建为空。为避免这种情况,您需要确保 Django 在迁移期间不会对数据库进行任何更改。
您可以通过将每个迁移操作包装在一个SeparateDatabaseAndState操作中来消除对数据库的更改。要告诉 Django 不要对数据库应用任何更改,您可以设置db_operations为空列表。
您计划重用旧表,因此您需要防止 Django 丢弃它。在删除模型之前,Django 将删除引用模型的字段。因此,首先,防止 Django 将外键从saleto 中删除product:
--- a/store/catalog/migrations/0002_remove_product_category.py +++ b/store/catalog/migrations/0002_remove_product_category.py @@ -10,8 +10,14 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='product', - name='category', + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name='product', + name='category', + ), + ], + # You're reusing the table, so don't drop it + database_operations=[], ), ]
现在 Django 已经处理了相关对象,它可以删除模型。你想保留Product表,所以防止 Django 删除它:
--- a/store/catalog/migrations/0003_delete_product.py +++ b/store/catalog/migrations/0003_delete_product.py @@ -11,7 +11,13 @@ class Migration(migrations.Migration): ] operations = [ - migrations.DeleteModel( - name='Product', - ), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.DeleteModel( + name='Product', + ), + ], + # You want to reuse the table, so don't drop it + database_operations=[], + ) ]
您曾经database_operations=[]阻止 Django 删除表。接下来,阻止 Django 创建新表:
--- a/store/product/migrations/0001_initial.py +++ b/store/product/migrations/0001_initial.py @@ -13,15 +13,21 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=100)), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Category')), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=100)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Category')), + ], + options={ + 'db_table': 'catalog_product', + }, + ), ], - options={ - 'db_table': 'catalog_product', - }, - ), + # You reference an existing table + database_operations=[], + ) ]
在这里,您曾经database_operations=[]阻止 Django 创建新表。最后,您希望防止 DjangoSale从新Product模型重新创建外键约束。由于您正在重用旧表,因此约束仍然存在:
--- a/store/sale/migrations/0002_auto_20200104_0724.py +++ b/store/sale/migrations/0002_auto_20200104_0724.py @@ -12,9 +12,14 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='sale', - name='product', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.Product'), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name='sale', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.Product'), + ), + ], + database_operations=[], ), ]
现在您已完成对迁移文件的编辑,请应用迁移:
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, catalog, contenttypes, product, sale, sessions Running migrations: Applying catalog.0002_remove_product_category... OK Applying product.0001_initial... OK Applying sale.0002_auto_20200104_0724... OK Applying catalog.0003_delete_product... OK
此时,您的新模型指向旧表。Django没有对数据库做任何改动,所有改动都是在代码中对Django的模型状态进行的。但是在您称其为成功并继续前进之前,值得确认新模型的状态与数据库的状态相匹配。
奖励:对新模型进行更改
为了确保模型的状态与数据库的状态一致,尝试对新模型进行更改,并确保 Django 正确检测到它。
该Product模型在该name字段上定义了一个索引。删除该索引:
--- a/store/product/models.py +++ b/store/product/models.py @@ -3,7 +3,7 @@ from django.db import models from catalog.models import Category class Product(models.Model): - name = models.CharField(max_length=100, db_index=True) + name = models.CharField(max_length=100) category = models.ForeignKey(Category, on_delete=models.CASCADE) class Meta:
您通过消除db_index=True. 接下来,生成迁移:
$ python manage.py makemigrations Migrations for 'product': product/migrations/0002_auto_20200104_0856.py - Alter field name on product
在继续之前,检查 Django 为这次迁移生成的 SQL:
$ python manage.py sqlmigrate product 0002 BEGIN; -- -- Alter field name on product -- DROP INDEX IF EXISTS "catalog_product_name_924af5bc"; DROP INDEX IF EXISTS "catalog_product_name_924af5bc_like"; COMMIT;
伟大的!Django 检测到旧索引,如"catalog_*"前缀所示。现在您可以执行迁移:
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, catalog, contenttypes, product, sale, sessions Running migrations: Applying product.0002_auto_20200104_0856... OK
确保您在数据库中获得了预期的结果:
django_migration_test=# \d catalog_product Table "public.catalog_product" Column | Type | Nullable | Default -------------+------------------------+----------+--------------------------------------------- id | integer | not null | nextval('catalog_product_id_seq'::regclass) name | character varying(100) | not null | category_id | integer | not null | Indexes: "catalog_product_pkey" PRIMARY KEY, btree (id) "catalog_product_category_id_35bf920b" btree (category_id) Foreign-key constraints: "catalog_product_category_id_35bf920b_fk_catalog_category_id" FOREIGN KEY (category_id) REFERENCES catalog_category(id) DEFERRABLE INITIALLY DEFERRED Referenced by: TABLE "sale_sale" CONSTRAINT "sale_sale_product_id_18508f6f_fk_catalog_product_id" FOREIGN KEY (product_id) REFERENCES catalog_product(id) DEFERRABLE INITIALLY DEFERRED
成功!name列上的索引已删除。
总结:更改模型参考的利弊
更改模型以引用另一个模型有其优点和缺点。以下是与此方法相关的一些优点:
速度很快:这种方法不会对数据库进行任何更改,因此速度非常快。
不需要停机:这种方法不需要复制数据,因此可以在实时系统上执行而无需停机。
它是可逆的:如有必要,可以逆转此迁移。
它由 ORM 支持:使用内置迁移操作执行此转换可确保正确的数据库支持。
它不需要与数据库进行任何同步:使用这种方法,相关对象(例如索引和序列)保持不变。
这种方法的唯一主要缺点是它打破了命名约定。使用现有表意味着该表仍将使用旧应用程序的名称。
请注意,这种方法比复制数据要简单得多。
Django 方式:重命名表
在前面的示例中,您使新模型引用了数据库中的旧表。结果,您打破了 Django 使用的命名约定。在这种方法中,您执行相反的操作:您使旧表引用新模型。
更具体地说,您创建新模型并为其生成迁移。然后,您从 Django 创建的迁移中获取新表的名称,而不是为新模型创建表,而是使用特殊迁移操作将旧表重命名为新表的名称AlterModelTable。
创建新模型
就像以前一样,您首先要创建一个新product应用程序来一次性完成所有更改。首先,Product从catalog应用程序中删除模型:
--- a/store/catalog/models.py +++ b/store/catalog/models.py @@ -3,8 +3,3 @@ from django.db import models class Category(models.Model): name = models.CharField(max_length=100) - - -class Product(models.Model): - name = models.CharField(max_length=100, db_index=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE)
你已经Product从catalog. 接下来,将Product模型移动到一个新的product应用程序:
# store/product/models.py from django.db import models from catalog.models import Category class Product(models.Model): name = models.CharField(max_length=100, db_index=True) category = models.ForeignKey(Category, on_delete=models.CASCADE)
该Product模型现在存在于您的product应用程序中。现在将外键更改Sale为引用product.Product:
--- a/store/sale/models.py +++ b/store/sale/models.py @@ -1,6 +1,6 @@ from django.db import models -from catalog.models import Product +from product.models import Product class Sale(models.Model): created = models.DateTimeField() --- a/store/store/settings.py +++ b/store/store/settings.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'catalog', 'sale', + 'product', ]
接下来,让 Django 为您生成迁移:
$ python manage.py makemigrations sale catalog product Migrations for 'catalog': catalog/migrations/0002_remove_product_category.py - Remove field category from product catalog/migrations/0003_delete_product.py - Delete model Product Migrations for 'product': product/migrations/0001_initial.py - Create model Product Migrations for 'sale': sale/migrations/0002_auto_20200110_1304.py - Alter field product on sale
您想阻止 Django 删除该表,因为您打算重命名它。
要获取应用程序中Product模型的名称,请product为创建的迁移生成 SQL Product:
$ python manage.py sqlmigrate product 0001 BEGIN; -- -- Create model Product -- CREATE TABLE "product_product" ("id" serial NOT NULL PRIMARY KEY, "name" varchar(100) NOT NULL, "category_id" integer NOT NULL); ALTER TABLE "product_product" ADD CONSTRAINT "product_product_category_id_0c725779_fk_catalog_category_id" FOREIGN KEY ("category_id") REFERENCES "catalog_category" ("id") DEFERRABLE INITIALLY DEFERRED; CREATE INDEX "product_product_name_04ac86ce" ON "product_product" ("name"); CREATE INDEX "product_product_name_04ac86ce_like" ON "product_product" ("name" varchar_pattern_ops); CREATE INDEX "product_product_category_id_0c725779" ON "product_product" ("category_id"); COMMIT;
Django 为应用程序中的Product模型生成的表的名称product是product_product.
重命名旧表
既然您已经为模型生成了名称 Django,您就可以重命名旧表了。为了Product从catalog应用程序中删除模型,Django 创建了两个迁移:
catalog/migrations/0002_remove_product_category 从表中删除外键。
catalog/migrations/0003_delete_product 删除模型。
在重命名表之前,您希望防止 Django 将外键删除为Category:
--- a/store/catalog/migrations/0002_remove_product_category.py +++ b/store/catalog/migrations/0002_remove_product_category.py @@ -10,8 +10,13 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name='product', - name='category', + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name='product', + name='category', + ), + ], + database_operations=[], ), ]
使用SeparateDatabaseAndStatewith database_operationsset 为空列表可防止 Django 删除该列。
Django 提供了一个特殊的迁移操作 ,AlterModelTable来重命名模型的表。编辑删除旧表的迁移,并将表重命名为product_product:
--- a/store/catalog/migrations/0003_delete_product.py +++ b/store/catalog/migrations/0003_delete_product.py @@ -11,7 +11,17 @@ class Migration(migrations.Migration): ] operations = [ - migrations.DeleteModel( - name='Product', - ), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.DeleteModel( + name='Product', + ), + ], + database_operations=[ + migrations.AlterModelTable( + name='Product', + table='product_product', + ), + ], + ) ]
您使用SeparateDatabaseAndStatewithAlterModelTable为 Django 提供不同的迁移操作以在数据库中执行。
接下来,您需要阻止 Django 为新Product模型创建表。相反,您希望它使用您重命名的表。对product应用程序中的初始迁移进行以下更改:
--- a/store/product/migrations/0001_initial.py +++ b/store/product/migrations/0001_initial.py @@ -13,12 +13,18 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=100)), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Category')), - ], + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=100)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalog.Category')), + ], + ), + ], + # Table already exists. See catalog/migrations/0003_delete_product.py + database_operations=[], ), ]
迁移会在 Django 的状态下创建模型,但由于database_operations=[]. 还记得您将旧表重命名为product_product吗?通过将旧表重命名为 Django 为新模型生成的名称,您可以强制 Django 使用旧表。
最后,您要防止 Django 在Sale模型中重新创建外键约束:
--- a/store/sale/migrations/0002_auto_20200110_1304.py +++ b/store/sale/migrations/0002_auto_20200110_1304.py @@ -12,9 +12,15 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( - model_name='sale', - name='product', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.Product'), - ), + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterField( + model_name='sale', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.Product'), + ), + ], + # You're reusing an existing table, so do nothing + database_operations=[], + ) ]
您现在已准备好运行迁移:
$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, catalog, contenttypes, product, sale, sessions Running migrations: Applying catalog.0002_remove_product_category... OK Applying product.0001_initial... OK Applying sale.0002_auto_20200110_1304... OK Applying catalog.0003_delete_product... OK
伟大的!迁移成功。但在继续之前,请确保它可以逆转:
$ python manage.py migrate catalog 0001 Operations to perform: Target specific migration: 0001_initial, from catalog Running migrations: Rendering model states... DONE Unapplying catalog.0003_delete_product... OK Unapplying sale.0002_auto_20200110_1304... OK Unapplying product.0001_initial... OK Unapplying catalog.0002_remove_product_category... OK
惊人的!迁移是完全可逆的。
注意: 由于一些原因,AlterModelTable通常最好使用RunSQL。
首先,AlterModelTable可以处理名称基于模型名称的字段之间的多对多关系。使用RunSQL重命名表可能需要一些额外的工作。
此外,内置迁移操作(例如AlterModelTable数据库不可知)而RunSQL并非如此。例如,如果您的应用程序需要在多个数据库引擎上运行,那么您在编写与所有数据库引擎兼容的 SQL 时可能会遇到一些麻烦。
奖励:了解内省
Django ORM 是一个抽象层,它将 Python 类型转换为数据库表,反之亦然。例如,当您Product在product应用程序中创建模型时,Django 创建了一个名为product_product. 除了表之外,ORM 还创建其他数据库对象,例如索引、约束、序列等。Django 根据应用程序和模型的名称为所有这些对象制定了命名约定。
要更好地了解它的外观,请检查catalog_category数据库中的表:
django_migration_test=# \d catalog_category Table "public.catalog_category" Column | Type | Nullable | Default --------+------------------------+----------+---------------------------------------------- id | integer | not null | nextval('catalog_category_id_seq'::regclass) name | character varying(100) | not null | Indexes: "catalog_category_pkey" PRIMARY KEY, btree (id)
该表是由 Django 为Category应用程序中的模型生成的catalog,因此名称为catalog_category. 您还可以注意到其他数据库对象的类似命名约定。
catalog_category_pkey 指主键索引。
catalog_category_id_seq指为主键字段生成值的序列id。
接下来,检查Product您从中移动catalog到的模型的表product:
django_migration_test=# \d product_product Table "public.product_product" Column | Type | Nullable | Default -------------+------------------------+----------+--------------------------------------------- id | integer | not null | nextval('catalog_product_id_seq'::regclass) name | character varying(100) | not null | category_id | integer | not null | Indexes: "catalog_product_pkey" PRIMARY KEY, btree (id) "catalog_product_category_id_35bf920b" btree (category_id) "catalog_product_name_924af5bc" btree (name) "catalog_product_name_924af5bc_like" btree (name varchar_pattern_ops) Foreign-key constraints: "catalog_product_category_id_35bf920b_fk_catalog_category_id" FOREIGN KEY (category_id) REFERENCES catalog_category(id) DEFERRABLE INITIALLY DEFERRED
乍一看,相关的对象比较多。然而,仔细观察会发现相关对象的名称与表的名称不一致。例如,表的名称是product_product,但主键约束的名称是catalog_product_pkey。您从名为 的应用程序复制了模型catalog,这意味着迁移操作AlterModelTable不会更改所有相关数据库对象的名称。
为了更好地了解AlterModelTable工作原理,请检查此迁移操作生成的 SQL:
$ python manage.py sqlmigrate catalog 0003 BEGIN; -- -- Custom state/database change combination -- ALTER TABLE "catalog_product" RENAME TO "product_product"; COMMIT;
这表明AlterModelTable只重命名表。如果是这种情况,那么如果您尝试更改与这些对象的表相关的数据库对象之一,会发生什么情况?Django 能够处理这些变化吗?
为了找到答案,尝试删除赛场上的指数name在Product模型:
--- a/store/product/models.py +++ b/store/product/models.py @@ -3,5 +3,5 @@ from django.db import models from catalog.models import Category class Product(models.Model): - name = models.CharField(max_length=100, db_index=True) + name = models.CharField(max_length=100, db_index=False) category = models.ForeignKey(Category, on_delete=models.CASCADE)
接下来,生成迁移:
$ python manage.py makemigrations Migrations for 'product': product/migrations/0002_auto_20200110_1426.py - Alter field name on product
命令成功——这是一个好兆头。现在检查生成的 SQL:
$ python manage.py sqlmigrate product 0002 BEGIN; -- -- Alter field name on product -- DROP INDEX IF EXISTS "catalog_product_name_924af5bc"; DROP INDEX IF EXISTS "catalog_product_name_924af5bc_like"; COMMIT;
生成的 SQL 命令删除索引catalog_product_name_924af5bc。Django 能够检测到现有索引,即使它与表名不一致。这称为内省。
自省在 ORM 内部使用,因此您不会找到太多关于它的文档。每个数据库后端都包含一个内省模块,可以根据其属性识别数据库对象。自省模块通常会使用数据库提供的元数据表。使用自省,ORM 可以在不依赖命名约定的情况下操作对象。这就是 Django 能够检测到要删除的索引名称的方式。
总结:重命名表的利弊
重命名表有其优点和缺点。以下是与此方法相关的一些优点:
速度很快:这种方法只重命名数据库对象,所以速度非常快。
不需要停机:使用这种方法,数据库对象在重命名时只会被锁定一小段时间,因此可以在实时系统上执行而无需停机。
它是可逆的:如有必要,可以逆转此迁移。
它由 ORM 支持:使用内置迁移操作执行此转换可确保正确的数据库支持。
与这种方法相关的唯一潜在缺点是它打破了命名约定。仅重命名表意味着其他数据库对象的名称将与 Django 的命名约定不一致。当直接使用数据库时,这可能会引起一些混乱。但是,Django 仍然可以使用自省来识别和管理这些对象,因此这不是主要问题。
指南:选择最佳方法
在本教程中,您学习了如何以三种不同的方式将 Django 模型从一个应用程序移动到另一个应用程序。以下是本教程中描述的方法的比较:
注意:上表建议重命名表保留 Django 的命名约定。虽然严格来说这不是真的,但您之前了解到 Django 可以使用自省来克服与此方法相关的命名问题。
上述每种方法都有其自身的优点和缺点。那么,您应该使用哪种方法?
作为一般经验法则,您应该在处理小表时复制数据,并且您可以承受一些停机时间。否则,最好的办法是重命名表并将新模型引用到它。
也就是说,每个项目都有自己独特的要求。您应该选择对您和您的团队最有意义的方法。
结论
阅读本教程后,您可以更好地根据您的特定用例、限制和要求做出关于如何将 Django 模型移动到另一个应用程序的正确决定。
在本教程中,您学习了:
如何将 Django 模型从一个应用程序移动到另一个应用程序
如何利用先进的功能Django的迁移CLI,如sqlmigrate,showmigrations和sqlsequencereset
如何制作和检查迁移计划
如何使迁移可逆,以及如何逆向迁移
什么是自省以及 Django 如何在迁移中使用它
要深入了解,请查看完整的数据库教程和Django 教程。
Django 数据复制服务 DRS
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。