Memigrasikan aplikasi Django dari PostgreSQL ke SQL Server

Artikel ini adalah panduan migrasi terperinci untuk aplikasi Django yang berpindah dari PostgreSQL (psycopg2 atau psycopg) ke SQL Server (mssql-django). Untuk gambaran umum migrasi dari database apa pun, lihat Memigrasikan aplikasi Django dari database lain ke SQL Server.

Prasyarat

  • Python 3.8 atau yang lebih baru
  • Microsoft ODBC Driver 17 atau 18 untuk SQL Server. Lihat Menginstal mssql-django.
  • SQL Server 2016 atau yang lebih baru, atau Azure SQL Database

Mengalihkan backend database

Ganti konfigurasi PostgreSQL Anda di 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",
        },
    },
}

Pembaruan requirements.txt:

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

# Add
mssql-django>=1.5

Ganti fitur django.contrib.postgres

Modul ini django.contrib.postgres menyediakan bidang, fungsi, dan pencarian khusus PostgreSQL. Ini tidak bekerja dengan SQL Server. Bagian berikut menunjukkan cara mengganti setiap fitur.

ArrayField

PostgreSQL ArrayField menyimpan array secara asli. SQL Server tidak memiliki tipe kolom array.

Opsi 1: JSONField (berfungsi dengan Django 3.2 dan versi yang lebih baru)

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

Mengkueri perubahan:

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

Opsi 2: Tabel terkait (dinormalisasi, lebih baik untuk array besar atau pemfilteran yang sering)

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

Ganti dengan 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 mendukung sintaks pencarian kunci yang sama:

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

Bidang rentang

Jenis rentang PostgreSQL (IntegerRangeField, , BigIntegerRangeField, DateRangeFieldDateTimeRangeField, DecimalRangeField) tidak memiliki SQL Server setara. Gunakan dua bidang terpisah:

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

Perbarui kueri untuk menggunakan perbandingan bidang terpisah. Sebelumnya, dengan PostgreSQL:DateRangeField

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

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

Setelah itu, dengan dua DateField kolom pada 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 dan CIEmailField

Tipe teks PostgreSQL yang tidak membedakan huruf besar dan kecil menggunakan ekstensi citext. Kolasi default SQL Server (SQL_Latin1_General_CP1_CI_AS) sudah tidak membedakan huruf besar dan huruf kecil, sehingga CharField dan EmailField standar berperilaku sama:

# 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

Pencarian teks lengkap PostgreSQL sangat terintegrasi dengan Django. SQL Server memiliki mesin pencari teks lengkapnya sendiri tetapi tidak ada integrasi ORM Django. Lihat Migrasi pencarian teks lengkap nanti di artikel ini.

Fungsi agregat

Ganti agregat khusus 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_AGGmemerlukan SQL Server 2017 atau yang lebih baru atau Azure SQL Database.

Migrasi pencarian teks lengkap

PostgreSQL menggunakan indeks tsvector, tsquery, dan GIN untuk pencarian teks lengkap. SQL Server memiliki mesin pencari teks lengkap terpisah.

Mengaktifkan pencarian teks lengkap di 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;

Kueri pencarian teks lengkap dari Django

Gunakan SQL mentah untuk mengakses fungsi CONTAINS dan FREETEXT milik SQL Server:

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

Untuk hasil peringkat (setara dengan 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],
    )

Panduan operasional pemeliharaan indeks teks lengkap

Rencanakan pemeliharaan untuk SQL Server indeks teks lengkap setelah migrasi:

  • Gunakan CHANGE_TRACKING AUTO untuk pembaruan hampir real-time.
  • Gunakan CHANGE_TRACKING MANUAL untuk jendela beban massal, lalu jalankan populasi penuh.
  • Lacak status perayapan dan tunggakan melalui sys.fulltext_indexes dan sys.dm_fts_index_population.

Periksa status:

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;

Setelah pemuatan data dalam jumlah besar dengan pelacakan manual:

ALTER FULLTEXT INDEX ON [products_product] START FULL POPULATION;

Tip

Membangun kembali atau mengisi ulang indeks teks lengkap selama jendela lalu lintas rendah. Populasi penuh bisa mahal di tabel besar.

Membuat manajer pencarian

Bungkus SQL mentah di manajer untuk akses bersih:

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 dan data spasial

mssql-django tidak menyertakan backend GeoDjango GIS. Jika aplikasi PostgreSQL Anda menggunakan PostGIS melalui django.contrib.gis, Anda tidak dapat memigrasikan kueri spasial langsung ke Django ORM pada SQL Server.

SQL Server mendukung jenis data geografi dan geometri secara asli. Untuk bekerja dengan data spasial setelah migrasi:

  • Simpan data spasial menggunakan bidang SQL mentah atau model kustom yang memetakan ke kolom geografi atau geometri SQL Server.
  • Kueri data spasial menggunakan SQL mentah dengan fungsi spasial bawaan 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],
    )
  • Pertimbangkan pustaka pihak ketiga yang menambahkan dukungan spasial SQL Server ke Django, atau menyimpan kueri spasial sebagai SQL mentah saat menggunakan ORM untuk yang lainnya.

Note

Jika aplikasi Anda sangat bergantung pada pencarian spasial GeoDjango, evaluasi biaya migrasi dengan hati-hati. Memindahkan kueri spasial ke SQL mentah memerlukan penulisan ulang setiap filter spasial GeoDjango.

Migrasi pengumpulan koneksi

Jika aplikasi PostgreSQL Anda menggunakan pgbouncer untuk pengumpulan koneksi, ganti dengan manajemen koneksi bawaan Django atau pengumpulan koneksi ODBC.

Penggunaan kembali koneksi 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+
    },
}

Untuk detail selengkapnya, lihat Pengumpulan koneksi di mssql-django.

Pengganti DISTINCT ON

PostgreSQL mendukung DISTINCT ON untuk mendapatkan satu baris per grup. SQL Server tidak mendukung sintaks ini. Gunakan fungsi jendela sebagai gantinya:

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

Kueri JSONB

Jenis PostgreSQL jsonb mendukung operator kueri yang kaya. SQL Server menyimpan JSON sebagai nvarchar(max) dengan fungsi kueri yang tersedia sejak SQL Server 2016.

Sintaks pencarian Django JSONField bekerja pada kedua backend untuk operasi dasar:

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

Untuk kueri JSON tingkat lanjut yang tidak didukung oleh ORM Django, gunakan fungsi JSON_VALUE dan OPENJSON milik SQL Server:

from django.db.models.expressions import RawSQL

# Query nested JSON values
Config.objects.annotate(
    theme=RawSQL("JSON_VALUE(data, '$.settings.theme')", [])
).filter(theme="dark")

Menghapus dependensi PostgreSQL

Setelah migrasi, hapus paket PostgreSQL dari proyek Anda:

pip uninstall psycopg2-binary psycopg2 psycopg

Hapus django.contrib.postgres dari INSTALLED_APPS dalam settings.py:

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

Daftar periksa migrasi

Step Detail lebih lanjut
Beralih backend Ganti django.db.backends.postgresql dengan mssql di settings.py.
Ganti contrib.postgres Tukar ArrayField, HStoreField, kolom rentang, dan kolom CI.
Memperbarui pencarian teks lengkap Migrasi dari tsvector/tsquery ke SQL Server . CONTAINS/FREETEXT
Memperbarui kueri spasial Tulis ulang pencarian GeoDjango sebagai SQL mentah menggunakan fungsi spasial SQL Server.
Menggantikan DISTINCT ON Gunakan ROW_NUMBER() fungsi jendela.
Memperbarui SQL mentah Ubah sintaks PostgreSQL (LIMIT, , ||NOW()) ke sintaks SQL Server. Lihat Memperbarui SQL kustom.
Mengaktifkan RCSI Atur READ_COMMITTED_SNAPSHOT ON agar sesuai dengan perilaku PostgreSQL MVCC. Lihat Perbedaan isolasi transaksi.
Kolatasi pengujian Verifikasi perilaku sensitivitas huruf besar/kecil sesuai dengan harapan Anda. Lihat Perbedaan kolasi.
Hapus psycopg2 Hapus instalan psycopg2-binary atau psycopg. Hapus django.contrib.postgres.
Buat ulang migrasi Hapus file migrasi lama, jalankan makemigrations dan migrate segar.
Migrasikan data Gunakan dumpdata/loaddata atau alat ETL untuk himpunan data besar.