Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье описано подробное руководство по миграции приложений Django, перемещающихся из PostgreSQL (psycopg2илиpsycopg) в SQL Server (mssql-django). Общие сведения о миграции из любой базы данных см. в статье "Миграция приложений Django из других баз данных в SQL Server".
Необходимые условия
- Python 3.8 или более поздней версии
- Microsoft драйвер ODBC 17 или 18 для SQL Server. См. раздел "Установка mssql-django".
- SQL Server 2016 или более поздней версии или База данных SQL Azure
Смените серверную часть базы данных
Замените конфигурацию PostgreSQL в settings.py:
# Before (PostgreSQL)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mydb",
"USER": "myuser",
"PASSWORD": "mypassword",
"HOST": "localhost",
"PORT": "5432",
},
}
# After (SQL Server)
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "mydb",
"USER": "myuser",
"PASSWORD": "mypassword",
"HOST": "localhost",
"PORT": "1433",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
},
},
}
Обновить requirements.txt:
# Remove
# psycopg2-binary>=2.9
# or psycopg[binary]>=3.1
# Add
mssql-django>=1.5
Замена функций django.contrib.postgres
Модуль django.contrib.postgres предоставляет специфичные для PostgreSQL поля, функции и операции поиска. Они не работают с SQL Server. В следующих разделах показано, как заменить каждую функцию.
ArrayField
PostgreSQL ArrayField хранит массивы в собственном коде. SQL Server не имеет типа столбца массива.
Вариант 1: JSONField (работает с Django 3.2 и более поздними версиями)
# Before
from django.contrib.postgres.fields import ArrayField
class Product(models.Model):
tags = ArrayField(models.CharField(max_length=50), default=list)
# After
class Product(models.Model):
tags = models.JSONField(default=list)
Запрос изменений:
# Before (PostgreSQL)
Product.objects.filter(tags__contains=["sale"])
Product.objects.filter(tags__overlap=["sale", "new"])
Product.objects.filter(tags__len=3)
# After (SQL Server with JSONField)
# Use __contains for exact list matching
Product.objects.filter(tags__contains=["sale"])
# For overlap-style queries, use raw SQL
from django.db.models.expressions import RawSQL
Product.objects.filter(
pk__in=RawSQL(
"""
SELECT p.id FROM products_product p
CROSS APPLY OPENJSON(p.tags) t
WHERE t.value IN (%s, %s)
""",
["sale", "new"],
)
)
Вариант 2. Связанная таблица (нормализованная, лучше для больших массивов или частой фильтрации)
class Product(models.Model):
name = models.CharField(max_length=200)
class ProductTag(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="tags")
tag = models.CharField(max_length=50, db_index=True)
class Meta:
unique_together = [("product", "tag")]
HStoreField
Замените на JSONField:
# Before
from django.contrib.postgres.fields import HStoreField
class Profile(models.Model):
metadata = HStoreField(default=dict)
# After
class Profile(models.Model):
metadata = models.JSONField(default=dict)
JSONField поддерживает тот же синтаксис поиска ключей:
# Both backends support this
Profile.objects.filter(metadata__theme="dark")
Поля диапазона
Типы диапазонов PostgreSQL (IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField) не имеют эквивалента в SQL Server. Используйте два отдельных поля:
# Before
from django.contrib.postgres.fields import DateRangeField
class Event(models.Model):
dates = DateRangeField()
# After
class Event(models.Model):
start_date = models.DateField()
end_date = models.DateField()
Обновите запросы для использования отдельных сравнений полей. Раньше, с DateRangeField PostgreSQL:
from django.contrib.postgres.fields import DateRangeField
from psycopg2.extras import DateRange
Event.objects.filter(dates__contains=DateRange(start, end))
После этого с двумя DateField столбцами на SQL Server:
from datetime import date
start = date(2026, 1, 1)
end = date(2026, 12, 31)
Event.objects.filter(start_date__lte=start, end_date__gte=end)
CITextField и CIEmailField
Текстовые типы PostgreSQL без учета регистра используют расширение citext. Правило сортировки SQL Server по умолчанию (SQL_Latin1_General_CP1_CI_AS) уже нечувствительно к регистру, поэтому стандартные CharField и EmailField ведут себя одинаково:
# Before
from django.contrib.postgres.fields import CITextField
class Tag(models.Model):
name = CITextField(max_length=100)
# After - already case-insensitive with default SQL Server collation
class Tag(models.Model):
name = models.CharField(max_length=100)
SearchVector, SearchQuery, SearchRank
Полнотекстовый поиск PostgreSQL глубоко интегрирован с Django. SQL Server имеет собственную полнотекстовую поисковую систему, но не интеграцию Django ORM. См. полнотекстовую миграцию поиска далее в этой статье.
Агрегатные функции
Замените агрегатные функции, специфичные для PostgreSQL:
# Before
from django.contrib.postgres.aggregates import ArrayAgg, StringAgg
Product.objects.values("category").annotate(
all_names=ArrayAgg("name"),
name_list=StringAgg("name", delimiter=", "),
)
# After - use SQL Server equivalents via RawSQL
from django.db.models.expressions import RawSQL
Product.objects.values("category").annotate(
name_list=RawSQL(
"STRING_AGG(name, ', ') WITHIN GROUP (ORDER BY name)",
[],
),
)
Note
STRING_AGGтребуется SQL Server 2017 или более поздней версии или База данных SQL Azure.
Миграция полнотекстового поиска
Полнотекстовый поиск PostgreSQL использует tsvectorи tsqueryGIN индексы. SQL Server имеет отдельную полнотекстовую поисковую систему.
Включение полнотекстового поиска в SQL Server
-- Create a full-text catalog
CREATE FULLTEXT CATALOG [MyAppCatalog] AS DEFAULT;
-- Create a full-text index (table must have a unique index)
CREATE FULLTEXT INDEX ON [products_product]([name], [description])
KEY INDEX [PK_products_product]
WITH CHANGE_TRACKING AUTO;
Запрос полнотекстового поиска из Django
Используйте необработанный SQL для доступа к функциям SQL Server CONTAINS и FREETEXT:
from django.db.models.expressions import RawSQL
# Equivalent of PostgreSQL SearchVector + SearchQuery
def search_products(query):
return Product.objects.filter(
pk__in=RawSQL(
"""
SELECT p.id FROM products_product p
WHERE CONTAINS((p.name, p.description), %s)
""",
[query],
)
)
Для ранжированных результатов (эквивалентно SearchRank):
def search_products_ranked(query):
return Product.objects.raw(
"""
SELECT p.*, ft.[RANK]
FROM products_product p
INNER JOIN CONTAINSTABLE(products_product, (name, description), %s) ft
ON p.id = ft.[KEY]
ORDER BY ft.[RANK] DESC
""",
[query],
)
Регламент обслуживания полнотекстового индекса
Планирование обслуживания SQL Server полнотекстовых индексов после миграции:
- Используется
CHANGE_TRACKING AUTOдля обновлений почти в режиме реального времени. - Используйте
CHANGE_TRACKING MANUALдля окон массовой загрузки, а затем запустите полную популяцию. - Отслеживайте состояние обхода и очередь через
sys.fulltext_indexesиsys.dm_fts_index_population.
Проверьте состояние:
SELECT
OBJECT_NAME(i.object_id) AS table_name,
i.change_tracking_state_desc,
i.has_crawl_completed,
i.crawl_type_desc
FROM sys.fulltext_indexes AS i;
После загрузки больших данных с отслеживанием вручную:
ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;
Tip
Перестраивайте или заново заполняйте полнотекстовые индексы в периоды низкой нагрузки. Полная популяция может быть дорогой на больших таблицах.
Создание диспетчера поиска
Оберните сырой SQL в менеджер для удобного доступа:
class ProductSearchManager(models.Manager):
def search(self, query):
if not query:
return self.none()
return self.filter(
pk__in=RawSQL(
"""
SELECT p.id FROM products_product p
WHERE CONTAINS((p.name, p.description), %s)
""",
[query],
)
)
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
objects = ProductSearchManager()
# Usage: Product.objects.search("mountain bike")
PostGIS и пространственные данные
mssql-django не включает серверную часть GeoDjango GIS. Если приложение PostgreSQL использует PostGIS черезdjango.contrib.gis, вы не можете перенести пространственные запросы непосредственно в Django ORM на SQL Server.
SQL Server поддерживает типы данных geography и geometry в собственном коде. Чтобы работать с пространственными данными после миграции:
- Хранение пространственных данных с помощью необработанных полей SQL или настраиваемых моделей, которые сопоставляют с географическими или геометрическими столбцами SQL Server.
- Запрос пространственных данных с помощью необработанных функций SQL с встроенными пространственными функциями SQL Server:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT id, name
FROM stores
WHERE location.STDistance(geography::Point(%s, %s, 4326)) <= %s
""",
[latitude, longitude, radius_meters],
)
- Рассмотрите сторонние библиотеки, которые добавляют поддержку пространственных данных SQL Server в Django, или используйте пространственные запросы в виде raw SQL, используя ORM для всего остального.
Note
Если ваше приложение в значительной степени зависит от пространственных запросов GeoDjango, тщательно оцените затраты на миграцию. Для перемещения пространственных запросов в необработанный SQL требуется перезаписать каждый пространственный фильтр GeoDjango.
Миграция пула подключений
Если ваше приложение PostgreSQL использует pgbouncer для организации пула подключений, замените его на встроенное в Django управление подключениями или пул подключений ODBC.
Повторное использование подключения Django
DATABASES = {
"default": {
"ENGINE": "mssql",
"NAME": "<your-database>",
"HOST": "<your-server>",
"OPTIONS": {
"driver": "ODBC Driver 18 for SQL Server",
},
"CONN_MAX_AGE": 600, # Reuse connections for 10 minutes
"CONN_HEALTH_CHECKS": True, # Django 4.1+
},
}
Дополнительные сведения см. в разделе "Пул подключений" в mssql-django.
Замена DISTINCT ON
PostgreSQL поддерживает DISTINCT ON, позволяющий получать по одной строке на каждую группу. SQL Server не поддерживает этот синтаксис. Вместо этого используйте функции окна:
# Before (PostgreSQL)
Entry.objects.order_by("blog_id", "-pub_date").distinct("blog_id")
# After (SQL Server) - use raw SQL with ROW_NUMBER
Entry.objects.raw(
"""
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY blog_id ORDER BY pub_date DESC) AS rn
FROM blog_entry
) sub
WHERE rn = 1
"""
)
Запросы JSONB
Тип PostgreSQL jsonb поддерживает расширенные операторы запросов. SQL Server сохраняет JSON как nvarchar(max) с функциями запросов, доступными с SQL Server 2016 года.
Синтаксис поиска Django JSONField работает в обоих бэкендах при выполнении базовых операций:
# Works on both PostgreSQL and SQL Server
Config.objects.filter(data__settings__theme="dark")
Config.objects.filter(data__has_key="settings")
Для сложных запросов JSON, не поддерживаемых ORM Django, используйте функции SQL Server JSON_VALUE и OPENJSON:
from django.db.models.expressions import RawSQL
# Query nested JSON values
Config.objects.annotate(
theme=RawSQL("JSON_VALUE(data, '$.settings.theme')", [])
).filter(theme="dark")
Удаление зависимостей PostgreSQL
После миграции удалите пакеты PostgreSQL из проекта:
pip uninstall psycopg2-binary psycopg2 psycopg
Удалить django.contrib.postgres из INSTALLED_APPS в settings.py:
INSTALLED_APPS = [
# Remove this line:
# "django.contrib.postgres",
"django.contrib.admin",
"django.contrib.auth",
# ...
]
Контрольный список миграции
| Step | Details |
|---|---|
| Переключить бэкенд | Замените django.db.backends.postgresql на mssql в settings.py. |
| Заменить contrib.postgres | Поменяйте местами ArrayField, HStoreField, поля диапазона и поля CI. |
| Обновление полнотекстового поиска | Переход с tsvector/tsquery на SQL Server CONTAINS/FREETEXT. |
| Обновление пространственных запросов | Перепишите запросы GeoDjango в raw SQL с использованием пространственных функций SQL Server. |
Заменить DISTINCT ON |
Используйте ROW_NUMBER() функции окна. |
| Обновление необработанного SQL | Измените синтаксис PostgreSQL (LIMIT, ||, NOW()) на SQL Server синтаксис. См. раздел "Обновление пользовательского SQL". |
| Включение RCSI | Задайте READ_COMMITTED_SNAPSHOT ON для соответствия поведению PostgreSQL MVCC. См. различия изоляции транзакций. |
| Проверка сортировки | Убедитесь, что поведение чувствительности к регистру соответствует вашим ожиданиям. См. различия в сортировке. |
| Удалить psycopg2 | Удалите psycopg2-binary или psycopg. Удалите django.contrib.postgres. |
| Пересоздать миграции | Удалите старые файлы миграции и заново выполните makemigrations и migrate. |
| Перенос данных | Используйте dumpdata/loaddata или средство ETL для больших наборов данных. |