如何将 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_APPS
Django 的列表中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_product
into 插入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)
<Product: Product object (4)>
惊人的!您的辛勤工作得到了回报,您已准备好进行下一步。
更新新模型的外键
旧表当前有其他表使用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
$ 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 将外键从sale
to 中删除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=[],
),
]
使用SeparateDatabaseAndState
with database_operations
set 为空列表可防止 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',
+ ),
+ ],
+ )
]
您使用SeparateDatabaseAndState
withAlterModelTable
为 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 模型从一个应用程序移动到另一个应用程序。以下是本教程中描述的方法的比较:
Metric | Copy Data | Change Table | Rename Table |
---|---|---|---|
Fast | ✗ | ✔️ | ✔️ |
No downtime | ✗ | ✔️ | ✔️ |
Sync related objects | ✗ | ✔️ | ✔️ |
Preserve naming convention | ✔️ | ✗ | ✔️ |
Built-in ORM support | ✔️ | ✔️ | ✔️ |
Reversible | ✔️ | ✔️ | ✔️ |
注意:上表建议重命名表保留 Django 的命名约定。虽然严格来说这不是真的,但您之前了解到 Django 可以使用自省来克服与此方法相关的命名问题。
上述每种方法都有其自身的优点和缺点。那么,您应该使用哪种方法?
作为一般经验法则,您应该在处理小表时复制数据,并且您可以承受一些停机时间。否则,最好的办法是重命名表并将新模型引用到它。
也就是说,每个项目都有自己独特的要求。您应该选择对您和您的团队最有意义的方法。
结论
阅读本教程后,您可以更好地根据您的特定用例、限制和要求做出关于如何将 Django 模型移动到另一个应用程序的正确决定。
在本教程中,您学习了:
- 如何将 Django 模型从一个应用程序移动到另一个应用程序
- 如何利用先进的功能Django的迁移CLI,如
sqlmigrate
,showmigrations
和sqlsequencereset
- 如何制作和检查迁移计划
- 如何使迁移可逆,以及如何逆向迁移
- 什么是自省以及 Django 如何在迁移中使用它
- 点赞
- 收藏
- 关注作者
评论(0)