複合主キー¶
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
引数が無視される点が異なります。
警告
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.editable
を False
に設定し、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>
]