Migrace aplikací Django z PostgreSQL do SQL Server

Tento článek je podrobný průvodce migrací pro aplikace Django, které se přesouvají z PostgreSQL (psycopg2nebopsycopg) na SQL Server (mssql-django). Obecný přehled migrace z jakékoli databáze najdete v tématu Migrace aplikací Django z jiných databází do SQL Server.

Předpoklady

  • Python 3.8 nebo novější
  • Microsoft ovladač ODBC 17 nebo 18 pro SQL Server. Vizte Nainstalujte mssql-django.
  • SQL Server 2016 nebo novější nebo Azure SQL Database

Přepnutí back-endu databáze

Nahraďte konfiguraci PostgreSQL v 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",
        },
    },
}

Aktualizace requirements.txt:

# Remove
# psycopg2-binary>=2.9
# or psycopg[binary]>=3.1

# Add
mssql-django>=1.5

Nahrazení funkcí django.contrib.postgres

Modul django.contrib.postgres poskytuje pole, funkce a vyhledávání specifická pro PostgreSQL. Nefungují s SQL Server. Následující části ukazují, jak nahradit jednotlivé funkce.

Pole

PostgreSQL ArrayField ukládá pole nativně. SQL Server nemá datový typ sloupce pro pole.

Možnost 1: JSONField (pracuje s Django 3.2 a novějšími verzemi)

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

Dotazování na změny:

# 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"],
    )
)

Možnost 2: Související tabulka (normalizovaná, lepší pro velká pole nebo časté filtrování)

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

Nahradit s 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 podporuje stejnou syntaxi vyhledávání klíčů:

# Both backends support this
Profile.objects.filter(metadata__theme="dark")

Pole rozsahu hodnot

Typy rozsahů PostgreSQL (IntegerRangeField, BigIntegerRangeField, DateRangeField, DateTimeRangeField, DecimalRangeField) nemají žádný SQL Server ekvivalent. Použijte dvě samostatná pole:

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

Aktualizujte dotazy tak, aby používaly samostatná porovnání polí. Předtím s PostgreSQL DateRangeField:

from django.contrib.postgres.fields import DateRangeField
from psycopg2.extras import DateRange

Event.objects.filter(dates__contains=DateRange(start, end))

Poté se dvěma DateField sloupci v SQL Serveru:

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 a CIEmailField

Typy textu nerozlišující velká a malá písmena PostgreSQL používají příponu citext . Výchozí kolace SQL Serveru (SQL_Latin1_General_CP1_CI_AS) už nerozlišuje malá a velká písmena, takže standardní CharField a EmailField se chovají stejně:

# 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

Fulltextové vyhledávání PostgreSQL je hluboce integrované s Django. SQL Server má vlastní fulltextový vyhledávací modul, ale žádnou integraci Django ORM. Podívejte se na migraci fulltextového vyhledávání dále v tomto článku.

Agregační funkce

Nahraďte agregace specifické pro 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_AGGvyžaduje SQL Server 2017 nebo novější nebo Azure SQL Database.

Migrace fulltextového vyhledávání

Fulltextové vyhledávání PostgreSQL používá tsvectortsquerya GIN indexy. SQL Server má samostatný fulltextový vyhledávací modul.

Povolení fulltextového vyhledávání v 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;

Dotazování fulltextového vyhledávání z Django

K přístupu k funkcím CONTAINS a FREETEXT serveru SQL Server použijte nezpracované příkazy SQL:

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

Pro seřazené výsledky (ekvivalentní k 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],
    )

Provozní příručka pro údržbu fulltextového indexu

Plánování údržby SQL Server fulltextových indexů po migraci:

  • Používá se CHANGE_TRACKING AUTO pro aktualizace téměř v reálném čase.
  • Použijte CHANGE_TRACKING MANUAL pro okna hromadného načítání a pak spusťte celou populaci.
  • Sledujte stav procházení a nevyřízené položky pomocí sys.fulltext_indexes a sys.dm_fts_index_population.

Kontrola stavu:

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;

Po rozsáhlém načtení dat při ručním sledování:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

Znovu sestavte nebo znovu naplňte fulltextové indexy v době nízkého provozu. U velkých tabulek může být jejich úplné naplnění nákladné.

Vytvoření správce vyhledávání

Zabalení nezpracovaného SQL ve správci pro čistý přístup:

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 a prostorová data

mssql-django neobsahuje GIS backend GeoDjango. Pokud vaše aplikace PostgreSQL používá PostGIS prostřednictvím django.contrib.gis, nemůžete migrovat prostorové dotazy přímo do Django ORM na SQL Server.

SQL Server nativně podporuje datové typy geografie a geometrie. Práce s prostorovými daty po migraci:

  • Ukládejte prostorová data pomocí nezpracovaného SQL nebo vlastních polí modelu, která se mapují na sloupce geography nebo geometry v SQL Serveru.
  • Dotazování prostorových dat pomocí nezpracovaného SQL pomocí předdefinovaných prostorových funkcí 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],
    )
  • Zvažte knihovny třetích stran, které přidávají SQL Server prostorovou podporu do Django, nebo zachovat prostorové dotazy jako nezpracované SQL při použití ORM pro všechno ostatní.

Note

Pokud vaše aplikace silně závisí na prostorových vyhledáváních GeoDjango, pečlivě vyhodnoťte náklady na migraci. Přesun prostorových dotazů do nezpracovaného SQL vyžaduje přepsání každého prostorového filtru GeoDjango.

Migrace sdružování připojení

Pokud vaše aplikace PostgreSQL používá pgbouncer pro sdružování připojení, nahraďte ji integrovanou správou připojení Django nebo sdružováním připojení ODBC.

Znovupoužití připojení v 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+
    },
}

Další podrobnosti najdete v tématu Sdružování připojení v mssql-django.

Náhrada za DISTINCT ON

PostgreSQL podporuje DISTINCT ON, které umožňuje získat jeden řádek z každé skupiny. SQL Server tuto syntaxi nepodporuje. Místo toho použijte funkce okna:

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

Dotazy nad JSONB

Typ PostgreSQL jsonb podporuje bohaté operátory dotazů. SQL Server ukládá JSON jako nvarchar(max) s dostupnými funkcemi dotazů od SQL Server 2016.

Syntaxe vyhledávání Django JSONField funguje na obou back-endech pro základní operace:

# Works on both PostgreSQL and SQL Server
Config.objects.filter(data__settings__theme="dark")
Config.objects.filter(data__has_key="settings")

Pro pokročilé dotazy nad JSON, které ORM frameworku Django nepodporuje, použijte funkce SQL Serveru JSON_VALUE a 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")

Odebrání závislostí PostgreSQL

Po migraci odeberte z projektu balíčky PostgreSQL:

pip uninstall psycopg2-binary psycopg2 psycopg

Odebrat django.contrib.postgres z INSTALLED_APPS :settings.py

INSTALLED_APPS = [
    # Remove this line:
    # "django.contrib.postgres",
    "django.contrib.admin",
    "django.contrib.auth",
    # ...
]

Kontrolní seznam pro migraci

Krok Podrobnosti
Přepnutí back-endu Nahraďte django.db.backends.postgresql za mssql v settings.py.
Nahradit contrib.postgres Prohoďte ArrayField, HStoreField, pole rozsahu a pole CI.
Aktualizace fulltextového vyhledávání Migrujte z tsvector/tsquery do SQL Server . CONTAINS/FREETEXT
Aktualizace prostorových dotazů Přepište vyhledávání GeoDjango jako nezpracované SQL pomocí SQL Server prostorových funkcí.
Nahradit DISTINCT ON Používejte ROW_NUMBER() funkce oken.
Aktualizace nezpracovaných SQL Změňte syntaxi PostgreSQL (LIMIT, ||, NOW()) na SQL Server syntaxi. Viz Aktualizace vlastního SQL.
Povolení RCSI Nastaví se READ_COMMITTED_SNAPSHOT ON tak, aby odpovídalo chování MVCC PostgreSQL. Viz Rozdíly v izolaci transakcí.
Kolace testů Ověřte, že chování rozlišování velkých a malých písmen odpovídá vašim očekáváním. Viz rozdíly v řazení.
Odebrat psycopg2 Odinstalujte psycopg2-binary nebo psycopg. Odeberte django.contrib.postgres.
Opětovné generování migrací Odstraňte staré soubory migrace, spusťte makemigrations a migrate znovu.
Migrujte data Použijte dumpdata/loaddata nebo nástroj ETL pro velké datové sady.