如何将 Django 模型移动到另一个应用程序

举报
Yuchuan 发表于 2021/09/22 11:23:20 2021/09/22
【摘要】 在Django 应用程序之间移动模型通常是一项非常复杂的任务,涉及复制数据、更改约束和重命名对象。由于这些复杂性,Django对象关系映射器 (ORM)不提供可以检测和自动化整个过程的内置迁移操作。相反,ORM 提供了一组低级迁移操作,允许 Django 开发人员在迁移框架中自己实现流程。

目录

如果您曾经考虑过重构Django 应用程序,那么您可能会发现自己需要移动 Django 模型。有几种方法可以使用 Django migrations将 Django 模型从一个应用程序移动到另一个应用程序,但不幸的是,它们都不是直接的。

Django 应用程序之间移动模型通常是一项非常复杂的任务,涉及复制数据、更改约束和重命名对象。由于这些复杂性,Django对象关系映射器 (ORM)不提供可以检测和自动化整个过程的内置迁移操作。相反,ORM 提供了一组低级迁移操作,允许 Django 开发人员在迁移框架中自己实现流程。

在本教程中,您将学习:

  • 如何将 Django 模型从一个应用程序移动到另一个应用程序
  • 如何使用高级功能Django的迁移命令行界面(CLI),例如sqlmigrateshowmigrationssqlsequencereset
  • 如何制作和检查迁移计划
  • 如何使迁移可逆以及如何逆向迁移
  • 什么是自省以及 Django 如何在迁移中使用它

完成本教程后,您将能够根据您的特定用例选择将 Django 模型从一个应用程序移动到另一个应用程序的最佳方法。

示例案例:将 Django 模型移动到另一个应用程序

在本教程中,您将在商店应用程序上工作。您的商店将从两个 Django 应用程序开始:

  1. catalog:此应用程序用于存储有关产品和产品类别的数据。
  2. 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)

您已成功在您的应用程序中创建CategoryProduct建模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 模型

要开始工作,您将走很长的路:

  1. 创建新模型
  2. 将数据复制到它
  3. 放下旧桌子

这种方法有一些你应该注意的陷阱。您将在以下部分详细探索它们。

创建新模型

首先创建一个新的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。这是因为有两个命名ProductCategory模型引用了该模型,从而产生了冲突。

当您向模型添加外键时,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)
<Product: Product object (4)>

惊人的!您的辛勤工作得到了回报,您已准备好进行下一步。

更新新模型的外键

旧表当前有其他表使用ForeignKey字段引用它。在删除旧模型之前,您需要更改引用旧模型的模型,以便它们引用新模型。

一个仍然引用旧模型的模型Salesale应用程序中。更改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模型现在引用Productproduct应用程序中的新模型。因为您已经将所有数据复制到新模型中,所以没有违反约束。

删除旧模型

上一步消除了对旧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 NULLCHECK)可能需要对表进行全面扫描以验证新数据是否有效。其他约束(例如FOREIGN KEY)需要使用另一个表进行验证,这可能需要一些时间,具体取决于引用表的大小。

处理通用关系

如果您使用的是通用关系,那么您可能需要一个额外的步骤。通用关系使用模型的主键和内容类型 ID 来引用任何模型表中的行。旧模型和新模型没有相同的内容类型 ID,因此通用连接可能会中断。这有时会被忽视,因为通用外键的完整性不是由数据库强制执行的。

有两种处理通用外键的方法:

  1. 将新模型的内容类型 ID 更新为旧模型的内容类型 ID。
  2. 将任何引用表的内容类型 ID 更新为新模型的内容类型 ID。

无论您选择哪种方式,请确保在部署到生产之前对其进行正确测试。

总结:复制数据的利弊

通过复制数据将 Django 模型移动到另一个应用程序有其优点和缺点。以下是与此方法相关的一些优点

  • 它由 ORM 支持:使用内置迁移操作执行此转换可确保正确的数据库支持。
  • 它是可逆的:如有必要,可以逆转此迁移。

以下是与此方法相关的一些缺点

  • 它很慢:复制大量数据可能需要时间。
  • 它需要停机:在将旧表复制到新表时更改旧表中的数据将导致转换期间数据丢失。为了防止这种情况发生,停机是必要的。
  • 它需要手动工作来同步数据库:将数据加载到现有表需要同步序列和通用外键。

正如您将在以下部分中看到的,使用这种方法将 Django 模型移动到另一个应用程序比其他方法花费的时间要长得多。

捷径:将新的 Django 模型引用到旧表

在前面的方法中,您将所有数据复制到新表中。迁移需要停机,并且可能需要很长时间才能完成,具体取决于要复制的数据量。

如果不是复制数据,而是更改新模型以引用旧表会怎样?

创建新模型

这一次,您将一次对模型进行所有更改,然后让 Django 生成所有迁移。

首先,Productcatalog应用程序中删除模型:

--- 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应用程序来一次性完成所有更改。首先,Productcatalog应用程序中删除模型:

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

你已经Productcatalog. 接下来,将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模型生成的表的名称productproduct_product.

重命名旧表

既然您已经为模型生成了名称 Django,您就可以重命名旧表了。为了Productcatalog应用程序中删除模型,Django 创建了两个迁移:

  1. catalog/migrations/0002_remove_product_category 从表中删除外键。
  2. 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 类型转换为数据库表,反之亦然。例如,当您Productproduct应用程序中创建模型时,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 能够处理这些变化吗?

为了找到答案,尝试删除赛场上的指数nameProduct模型:

--- 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,如sqlmigrateshowmigrationssqlsequencereset
  • 如何制作和检查迁移计划
  • 如何使迁移可逆,以及如何逆向迁移
  • 什么是自省以及 Django 如何在迁移中使用它

要深入了解,请查看完整的数据库教程Django 教程

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。