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 では、短いハッシュを含む制約名が生成されるため、スキーマ内の名前が、ここに示すプレースホルダーと一致しません。

スカッシュ移行

多くの移行が蓄積された後、それらを少ないファイルにスカッシュすることができます。

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 のdb_comment機能 (Django 4.2 以降) がサポートされています。 コメントは、SQL Server オブジェクトMS_Description拡張プロパティとして格納されます。

テーブルのコメント

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 を持つことができるテーブルは 1 つだけです。 1 つの atomic() ブロック内の複数のテーブルに明示的な ID を挿入すると、バックエンドはステートメントごとのトグルを処理します。 ただし、同じテーブルで IDENTITY_INSERT も使用する同時実行セッションが競合する可能性があります。