使用 mssql-django 进行数据库迁移

本文介绍 Django 的迁移系统如何通过mssql-django后端处理SQL Server,并记录已知的边缘情况。

创建和应用迁移

Django 的迁移工作流的工作方式与其他数据库SQL Server相同:

  1. 根据模型变更生成迁移:

    python manage.py makemigrations myapp
    
  2. 查看 <app>/migrations/ 中生成的迁移文件。

  3. 将迁移应用到数据库:

    python manage.py migrate myapp
    
  4. 检查迁移状态:

    python manage.py showmigrations myapp
    

初始项目设置

使用SQL Server设置新的 Django 项目时,请运行迁移以创建 Django 的内置表(身份验证、会话、管理员):

python manage.py migrate

此命令创建列出的 INSTALLED_APPS应用所需的所有表。

迁移中的自定义 SQL

使用 migrations.RunSQL 在迁移过程中执行原始 SQL 语句。 此方法可用于创建存储过程、触发器或其他特定于SQL Server的对象:

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ("myapp", "0001_initial"),
    ]

    operations = [
        migrations.RunSQL(
            sql="CREATE INDEX IX_myapp_product_name ON myapp_product (name);",
            reverse_sql="DROP INDEX IX_myapp_product_name ON myapp_product;",
        ),
    ]

已知迁移边缘案例

以下迁移操作在以 SQL Server 为目标时需要采用变通方法。

AutoField 修改

不支持在迁移期间将模型字段从 AutoField 更改为其他值,或从其他值更改为 AutoField。 SQL Server不允许从现有列添加或删除IDENTITY属性。

解决方法:使用所需的字段类型创建新模型。 将数据从旧表迁移到新表,然后删除旧表。

重命名带有外键约束的字段或模型

重命名具有外键约束的字段或模型可能会失败。 SQL Server需要在重命名操作期间删除和重新创建 FK 约束。

解决方法:用于 migrations.SeparateDatabaseAndState 删除 FK 约束、重命名列并重新创建约束,同时告知 Django 更新其模型状态。 以下示例将 product 模型上的 Order 外键重命名为 item

from django.db import migrations

class Migration(migrations.Migration):

    dependencies = [
        ("myapp", "0002_previous"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_order DROP CONSTRAINT FK_order_product;",
                    reverse_sql="ALTER TABLE myapp_order ADD CONSTRAINT FK_order_product FOREIGN KEY (product_id) REFERENCES myapp_product(id);",
                ),
                migrations.RunSQL(
                    sql="EXECUTE sp_rename 'myapp_order.product_id', 'item_id', 'COLUMN';",
                    reverse_sql="EXECUTE sp_rename 'myapp_order.item_id', 'product_id', 'COLUMN';",
                ),
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_order ADD CONSTRAINT FK_order_item FOREIGN KEY (item_id) REFERENCES myapp_product(id);",
                    reverse_sql="ALTER TABLE myapp_order DROP CONSTRAINT FK_order_item;",
                ),
            ],
            state_operations=[
                migrations.RenameField(
                    model_name="order",
                    old_name="product",
                    new_name="item",
                ),
            ],
        ),
    ]

运行此 T-SQL 代码之前,请在数据库中查找实际约束名称。 Django 生成包含短哈希的约束名称,因此架构中的名称与此处所示的占位符不匹配。

Squash 迁移

当迁移文件积累到很多时,你可以将它们合并为更少的文件:

python manage.py squashmigrations myapp 0001 0010

Tip

始终要针对全新的数据库测试压缩后的迁移,以确保其生成正确的数据库架构。

生成的列(计算列)

mssql-django 后端支持 Django 的 GeneratedField(Django 5.0 及更高版本),它对应于 SQL Server 的计算列。

存储的 (PERSISTED) 生成的列

存储的生成列在物理上写入磁盘,并在源列更改时更新:

from django.db import models
from django.db.models import F

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
    total_price = models.GeneratedField(
        expression=F("price") * (1 + F("tax_rate")),
        output_field=models.DecimalField(max_digits=10, decimal_places=2),
        db_persist=True,
    )

这将生成: total_price AS ([price] * (1 + [tax_rate])) PERSISTED.

虚拟生成的列

虚拟生成的列在查询时计算,不使用存储:

from django.db import models
from django.db.models import F, Value
from django.db.models.functions import Concat

class Employee(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    full_name = models.GeneratedField(
        expression=Concat(F("first_name"), Value(" "), F("last_name")),
        output_field=models.CharField(max_length=101),
        db_persist=False,
    )

Note

SQL Server限制非持久化计算列的索引。 如果需要为生成的列编制索引,请使用 db_persist=True

表和列注释

后端 mssql-django 支持 Django 的功能(Django db_comment 4.2 及更高版本)。 注释作为扩展属性存储在 MS_Description SQL Server 对象上。

表注释

class AuditLog(models.Model):
    action = models.CharField(max_length=50)
    timestamp = models.DateTimeField(auto_now_add=True)

    class Meta:
        db_table_comment = "Tracks user actions for compliance auditing."

列注释

class Measurement(models.Model):
    value = models.FloatField(db_comment="Sensor reading in Celsius")
    recorded_at = models.DateTimeField(db_comment="UTC timestamp from the data logger")

注释在 SQL Server Management Studio 的列/表属性中可见,也可通过 sys.extended_properties 查看。

复合主键

Django 5.2 引入了 CompositePrimaryKey. mssql-django 后端对复合主键提供了部分支持,但某些 Django 测试用例仍被排除在外。 在将复合键迁移和查询投入生产环境之前,请先结合你的应用程序对其进行验证。

  • inspectdb 无法正确生成复合主键。 检查后手动定义它们。
  • 不支持元组查找。 后端将复合键比较分解为单个列条件。
  • 针对子查询的元组比较需要 Django 5.2.4 及以上版本。
  • 某些迁移操作仍存在已知的排除项。 有关当前状态 ,请参阅 mssql-django 中的限制和不支持的功能
from django.db import models
from django.db.models import CompositePrimaryKey

class OrderItem(models.Model):
    pk = CompositePrimaryKey("order_id", "product_id")
    order = models.ForeignKey("Order", on_delete=models.CASCADE)
    product = models.ForeignKey("Product", on_delete=models.CASCADE)
    quantity = models.IntegerField()

IDENTITY_INSERT处理

当您将显式值插入到一个 AutoField 中时(例如,使用特定 ID 从备份中还原数据),后端会自动将该插入操作封装在 SET IDENTITY_INSERT ON / SET IDENTITY_INSERT OFF 中。 无需手动 SQL。

# The backend handles IDENTITY_INSERT automatically
Product.objects.create(id=42, name="Restored Widget", price=9.99)

Note

SQL Server 只允许每个会话一次有一个表具有 IDENTITY_INSERT ON。 如果在单个 atomic() 块中向多个表插入显式 ID,后端会按每条语句分别处理该切换。 但是,在同一个表上也使用 IDENTITY_INSERT 的并发会话可能会发生冲突。