複合主キー

New in Django 5.2.

Django では、すべてのモデルに主キーが存在します。デフォルトでは、この主キーは単一のフィールドで構成されます。

ほとんどの場合、単一の主キーで十分です。しかし、データベース設計においては、複数のフィールドからなる主キーが必要になることもあります。

複合主キーを使用するには、モデルを定義する際に pk 属性を CompositePrimaryKey に設定します:

class Product(models.Model):
    name = models.CharField(max_length=100)


class Order(models.Model):
    reference = models.CharField(max_length=20, primary_key=True)


class OrderLineItem(models.Model):
    pk = models.CompositePrimaryKey("product_id", "order_id")
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    quantity = models.IntegerField()

これにより、Django はテーブル作成時に複合主キー( PRIMARY KEY (product_id, order_id) )を作成するようになります。

複合主キーは tuple (タプル)で表現されます:

>>> product = Product.objects.create(name="apple")
>>> order = Order.objects.create(reference="A755H")
>>> item = OrderLineItem.objects.create(product=product, order=order, quantity=1)
>>> item.pk
(1, "A755H")

pk 属性に tuple を代入することで、対応するフィールドの値をまとめて設定できます:

>>> item = OrderLineItem(pk=(2, "B142C"))
>>> item.pk
(2, "B142C")
>>> item.product_id
2
>>> item.order_id
"B142C"

複合主キーは、 tuple を使ってフィルタリングできます:

>>> OrderLineItem.objects.filter(pk=(1, "A755H")).count()
1

リレーションシップフィールド (GenericForeignKey を含む) や Django 管理サイトにおける複合主キーのサポートは、現在も開発中です。現時点では、複合主キーを持つモデルは Django の管理サイトに登録できません。将来のリリースで対応が追加される予定です。

複合主キーへの移行

Django は、テーブル作成後に複合主キーへマイグレーションしたり、それを元に戻したりする操作をサポートしていません。また、複合主キーにフィールドを追加・削除することもサポートされていません。

既存のテーブルを単一の主キーから複合主キーに移行したい場合は、利用中のデータベースバックエンドの手順にしたがって操作してください。

複合主キーを設定できたら、モデルに CompositePrimaryKey フィールドを追加してください。これにより、Django はその複合主キーを正しく認識し、適切に扱えるようになります。

主キーフィールドに対するマイグレーション操作(たとえば AddFieldAlterField )はサポートされていませんが、 makemigrations は変更を検出します。

エラーを避けるために、上記のようなマイグレーションは --fake オプションを付けて適用することを推奨します。

代わりに、 SeparateDatabaseAndState を使うことで、データベース固有のマイグレーションと Django による状態管理用マイグレーションを、1つの操作としてまとめて実行することもできます。

複合主キーとリレーション

リレーションシップフィールド (ジェネリックリレーション を含む) は、複合主キーをサポートしていません。

たとえば先ほどの OrderLineItem モデルにおいて、次のような使い方はサポートされていません:

class Foo(models.Model):
    item = models.ForeignKey(OrderLineItem, on_delete=models.CASCADE)

これは、現在 ForeignKey が複合主キーを持つモデルを参照できないためです。

この制限を回避するには、代わりに ForeignObject が使えます:

class Foo(models.Model):
    item_order_id = models.CharField(max_length=20)
    item_product_id = models.IntegerField()
    item = models.ForeignObject(
        OrderLineItem,
        on_delete=models.CASCADE,
        from_fields=("item_order_id", "item_product_id"),
        to_fields=("order_id", "product_id"),
    )

ForeignObjectForeignKey によく似ていますが、データベース上にカラム (例: item_id)、外部キー制約、インデックスなどを作成しない点、 on_delete 引数が無視される点が異なります。

警告

ForeignObject は内部 API です。つまり、 非推奨ポリシー の対象外です。

複合主キーとデータベース関数

多くのデータベース関数は、単一の式しか受け付けません。

MAX("order_id")  -- OK
MAX("product_id", "order_id")  -- ERROR

複合主キーは複数のカラム表現を含むため、このような関数に渡すと、 ValueError が発生します。 ただし Count だけは例外で、複合主キーでも利用できます。

Max("order_id")  # OK
Max("pk")  # ValueError
Count("pk")  # OK

フォーム内での複合主キー

複合主キーは仮想フィールドであり、単一のデータベースカラムを表さないため、ModelForm には含まれません。

たとえば、次のようなフォームを考えてみましょう:

class OrderLineItemForm(forms.ModelForm):
    class Meta:
        model = OrderLineItem
        fields = "__all__"

このフォームには、複合主キーのための pk フィールドは含まれません:

>>> OrderLineItemForm()
<OrderLineItemForm bound=False, valid=Unknown, fields=(product;order;quantity)>

複合主キーである pk をフォームフィールドとして指定しようとすると、存在しないフィールドとして FieldError が発生します。

主キーフィールドは読み取り専用です

既存のオブジェクトで主キーの値を変更してから保存すると、新しいオブジェクトが元のオブジェクトとは別に作成されます (Field.primary_key を参照)。

これは複合主キーの場合も同様です。そのため、すべての主キーフィールドに対して Field.editableFalse に設定し、ModelForm から除外することを検討してください。

モデルバリデーションと複合主キー

pk は仮想フィールドなので、 Model.clean_fields()exclude 引数に pk を指定しても効果はありません。複合主キーの各フィールドをバリデーションから除外したい場合は、それぞれのフィールド名を個別に指定してください。一方、 Model.validate_unique() に対しては exclude={"pk"} を指定することで、一意性のチェックをスキップできます。

複合主キーに対応したアプリケーションをつくる

複合主キーが導入される前は、モデルの主キーとなる単一フィールドを見つけたいとき、各フィールドの primary_key 属性を確認するだけで済みました:

>>> pk_field = None
>>> for field in Product._meta.get_fields():
...     if field.primary_key:
...         pk_field = field
...         break
...
>>> pk_field
<django.db.models.fields.AutoField: id>

しかし primary key 属性はひとつのモデルにつき最大ひとつのフィールドしか True を取れないという制約があるため、複合主キーのもとではすべて False になってしまい、これでは主キーを構成するフィールドを特定するためには使えません。

>>> pk_fields = []
>>> for field in OrderLineItem._meta.get_fields():
...     if field.primary_key:
...         pk_fields.append(field)
...
>>> pk_fields
[]

複合主キーに正しく対応するコードを書くためには、代わりに _meta.pk_fields 属性を使用してください:

>>> Product._meta.pk_fields
[<django.db.models.fields.AutoField: id>]
>>> OrderLineItem._meta.pk_fields
[
    <django.db.models.fields.ForeignKey: product>,
    <django.db.models.fields.ForeignKey: order>
]