将 Django 应用从其他数据库迁移到SQL Server

本文提供了将 Django 应用程序从 PostgreSQL、MySQL 或 SQLite 迁移到使用 mssql-django 后端的 SQL Server 的指导。

Overview

Django 的 ORM 提取了大多数数据库差异,但某些行为和 SQL 方言在后端之间有所不同。 本指南介绍迁移到SQL Server时遇到的主要差异。

步骤 1:安装 mssql-django

mssql-django安装包及其依赖项:

pip install mssql-django

确保已安装用于SQL Server的 Microsoft ODBC 驱动程序。 有关特定于平台的说明,请参阅 安装 mssql-django

步骤 2:更新 DATABASE 配置

settings.py 中现有的数据库配置替换为:

# Example: From PostgreSQL
# DATABASES = {
#     "default": {
#         "ENGINE": "django.db.backends.postgresql",
#         "NAME": "mydb",
#     },
# }

# To SQL Server
DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "USER": "<your-username>",
        "PASSWORD": "<your-password>",
        "HOST": "<your-server>",
        "PORT": "1433",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
        },
    },
}

步骤 3:创建新的迁移

从SQL Server的干净迁移历史记录开始:

# Remove existing migration files (keep __init__.py)
# Then regenerate
python manage.py makemigrations
python manage.py migrate

Important

使用数据迁移或单独的 ETL 过程传输数据。 不要尝试针对SQL Server运行 PostgreSQL 或 MySQL 迁移文件。

与 PostgreSQL 的主要区别

功能 / 特点 PostgreSQL SQL Server (mssql-django)
自动递增 SERIAL / BIGSERIAL IDENTITY(1,1)
布尔类型 原生 boolean bit01
文本字段 text (无限制) nvarchar(max)
JSON 支持 原生 jsonb 具有 JSON 函数的 nvarchar(max)(SQL Server 2016+)
数组字段 ArrayField 不支持。 使用相关的表或 JSON。
HStore 字段 HStoreField 不支持。 改用 JSONField
范围字段 IntegerRangeFieldBigIntegerRangeFieldDateRangeFieldDateTimeRangeField 不支持。 使用两个单独的字段。
全文搜索 SearchVectorSearchRank 将原始 SQL 与SQL Server全文搜索配合使用。
DISTINCT ON 支持 不支持。 使用 GROUP BY 或子查询。
DateTimeField 带时区 timestamp with time zone datetimeoffset(当为 USE_TZ=True 时)或 datetime2

待替换的 PostgreSQL 特有功能

如果您的代码使用了来自 django.contrib.postgres 的 PostgreSQL 特有功能,请将其替换掉:

# PostgreSQL ArrayField - replace with JSONField or related table
# Before
from django.contrib.postgres.fields import ArrayField
tags = ArrayField(models.CharField(max_length=50))

# After (using JSONField)
tags = models.JSONField(default=list)

# PostgreSQL HStoreField - replace with JSONField
# Before
from django.contrib.postgres.fields import HStoreField
metadata = HStoreField()

# After
metadata = models.JSONField(default=dict)

与 MySQL 的主要区别

功能 / 特点 MySQL SQL Server (mssql-django)
自动递增 AUTO_INCREMENT IDENTITY(1,1)
布尔类型 tinyint(1) bit
文本字段 longtext nvarchar(max)
JSON 支持 原生 JSON (5.7 及更高版本) 带有 JSON 函数的 nvarchar(max)
Collation 可按列配置 实例或数据库级别(使用 COLLATE 选项替代)
DateTimeField datetime(6) datetimeoffsetdatetime2

与 SQLite 的主要区别

功能 / 特点 SQLite SQL Server (mssql-django)
类型强制执行 灵活的键入 严格类型强制执行
并发写入 有限 完全并发支持
最大连接数 有效 1 个编写器 支持大量并发连接的连接池
DateTimeField 存储为文本 datetimeoffsetdatetime2

排序规则差异

排序规则控制 SQL Server 如何比较和排序文本。 这是从 PostgreSQL 或 MySQL 迁移时最常见的意外行为源之一。

事例敏感性

SQL Server的默认排序规则 (SQL_Latin1_General_CP1_CI_AS不区分大小写。 PostgreSQL 默认区分大小写。

这种行为意味着,迁移后,先前区分 "Smith""smith" 的查询会将它们视为相等:

# On PostgreSQL: returns only exact case matches
# On SQL Server (default collation): returns both "Smith" and "smith"
User.objects.filter(last_name="Smith")

如果应用程序依赖于区分大小写的比较,有两个选项:

  • 将数据库或列排序规则更改为区分大小写的变体

    -- Database-level (affects all new columns)
    ALTER DATABASE [<your-database>] COLLATE Latin1_General_CS_AS;
    
    -- Column-level (for specific columns)
    ALTER TABLE [<your-table>]
    ALTER COLUMN [<column-name>] NVARCHAR (150) COLLATE Latin1_General_CS_AS;
    
  • 在原始 SQL 中对特定查询使用 Django 的 __exact 查找功能,并覆盖排序规则

区分重音

SQL Server 的默认排序规则区分重音符(AS),这与 PostgreSQL 的行为一致。 像 ée 这样的字符会被视为不同的字符。 如果需要进行不区分重音符号的比较,请使用以 _AI 结尾的排序规则。

在 mssql-django 中配置排序规则

替代数据库配置中文本字段查找的默认排序规则:

DATABASES = {
    "default": {
        "ENGINE": "mssql",
        "NAME": "<your-database>",
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "collation": "Latin1_General_CS_AS",  # Case-sensitive
        },
    },
}

Note

mssql-django 中的 collation 选项控制 LIKE 中使用的排序规则,以及 Django 的 ORM 查找生成的比较操作中使用的排序规则。 它不会更改数据库中现有列的排序规则。 若要更改存储的列排序规则,请使用 ALTER TABLE / ALTER COLUMN 语句。 有关详细信息,请参阅SQL Server排序规则文档

步骤 4:更新自定义 SQL

如果代码包含原始 SQL,请更新它以获取SQL Server语法:

# PostgreSQL syntax
# cursor.execute("SELECT * FROM products LIMIT 10 OFFSET 20")

# SQL Server syntax
cursor.execute("SELECT * FROM products ORDER BY id OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY")

常见的 SQL 语法差异:

运算 PostgreSQL/MySQL SQL Server
限制结果 LIMIT 10 TOP 10OFFSET ... FETCH NEXT ...
字符串拼接 \|\| (PG) / CONCAT() +CONCAT()
布尔文本 TRUE / FALSE 1 / 0
当前时间戳 NOW() 用于时区感知值的 GETDATE()SYSDATETIME()SYSDATETIMEOFFSET()
如果不存在 CREATE TABLE IF NOT EXISTS 检查 sys.objects 或使用 IF NOT EXISTS

事务隔离差异

PostgreSQL 的 READ COMMITTED 隔离级别使用 MVCC(多版本并发控制)机制。 读取器永远不会阻塞写入器,写入器也永远不会阻塞读取器。

SQL Server 默认的READ COMMITTED采用锁定机制,这意味着读取查询在等待写入事务完成时可能会被阻塞。 如果应用程序在迁移后遇到阻止增加,请考虑在数据库上启用 READ COMMITTED SNAPSHOT

ALTER DATABASE [<your-database>]
SET READ_COMMITTED_SNAPSHOT ON;

这会将 SQL Server 的 READ COMMITTED 更改为使用行版本控制(类似于 PostgreSQL 的 MVCC),而不是锁机制。 读者无需等待活动编写器即可看到行的最后一个提交版本。

Note

READ COMMITTED SNAPSHOT 需要额外的 tempdb 空间来存储行版本。 在生产环境中启用之前,在实际负载下进行测试。 有关详细信息,请参阅 mssql-django 中的事务管理

步骤 5:迁移数据

数据迁移策略取决于数据集大小:

小型数据集(<500 MB)

使用 Django 的 dumpdata/loaddata

# On the source database
python manage.py dumpdata --natural-foreign --natural-primary -o data.json

# Switch settings.py to SQL Server, then:
python manage.py migrate
python manage.py loaddata data.json

大型数据集(>500 MB)

对于大型迁移,请使用专用工具避免内存耗尽和超时问题。 Django 的 ORM 不是适合这种规模的大容量加载工具。 在数据迁移过程中绕过它,之后让 Django 管理数据库模式和应用程序逻辑。

工具 最适用于
SQL Server 导入和导出向导 使用 GUI 进行本地到本地迁移
Azure 数据工厂 任何源到 Azure SQL,包括混合场景
Azure 数据库迁移服务 具备内置验证和回滚的大规模迁移
使用 Apache Arrow 进行 mssql-python 大容量复制 要求在 SQL Server、Azure SQL 数据库和 Fabric 中的 SQL 数据库之间实现最大吞吐量的自定义 Python 管道

迁移后验证

迁移后,验证自动递增列的标识种子一致性:

-- Check identity seed and current value for all tables
SELECT 
    TABLE_NAME,
    IDENT_SEED(TABLE_SCHEMA + '.' + TABLE_NAME) AS IdentitySeed,
    IDENT_INCR(TABLE_SCHEMA + '.' + TABLE_NAME) AS IdentityIncrement,
    IDENT_CURRENT(TABLE_SCHEMA + '.' + TABLE_NAME) AS CurrentIdentity
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = 'BASE TABLE'
    AND OBJECTPROPERTY(OBJECT_ID(TABLE_SCHEMA + '.' + TABLE_NAME), 'TableHasIdentity') = 1
ORDER BY TABLE_NAME;

如果 CurrentIdentity 超过 IdentitySeed + record_count,则重新分配:

DBCC CHECKIDENT ('your_table', RESEED, new_seed);