複合主キー¶
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 はその複合主キーを正しく認識し、適切に扱えるようになります。
主キーフィールドに対するマイグレーション操作(たとえば AddField や AlterField )はサポートされていませんが、 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"),
)
ForeignObject は ForeignKey によく似ていますが、データベース上にカラム (例: item_id)、外部キー制約、インデックスなどを作成しない点、 on_delete 引数が無視される点が異なります。
複合主キーとデータベース関数¶
多くのデータベース関数は、単一の式しか受け付けません。
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.editable を False に設定し、ModelForm から除外することを検討してください。
モデルバリデーションと複合主キー¶
pk は仮想フィールドなので、 Model.clean_fields() の exclude 引数に pk を指定しても効果はありません。複合主キーの各フィールドをバリデーションから除外したい場合は、それぞれのフィールド名を個別に指定してください。一方、 Model.validate_unique() に対しては exclude={"pk"} を指定することで、一意性のチェックをスキップできます。
複合主キーに対応したアプリケーションをつくる¶
Prior to the introduction of composite primary keys, the single field composing
the primary key of a model could be retrieved by introspecting the
primary_key attribute of its fields:
>>> 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>
Now that a primary key can be composed of multiple fields the
primary_key attribute can no longer be relied
upon to identify members of the primary key as it will be set to False to
maintain the invariant that at most one field per model will have this
attribute set to True:
>>> 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>
]