Django 5.2で新しく追加された複合プライマリーキー(Composite Primary Key)機能について解説します。2025年4月2日にリリースされたばかりのDjango 5.2で、長年待ち望まれていたこの機能がいよいよ正式に利用可能になりました。この記事では、複合プライマリーキーの基本概念から実装方法、そして注意点までをわかりやすく解説します。
複合プライマリーキーは、特に関連テーブルや多対多関係の設計において非常に便利です。Djangoがこの機能を公式にサポートしたことで、より自然なデータモデリングが可能になりました。
はじめに:複合プライマリーキーとは何か?
プライマリーキーの基本
データベースにおいてプライマリーキー(Primary Key)は、テーブル内の各行(レコード)を一意に識別するためのフィールドまたはフィールドの組み合わせです。プライマリーキーには以下の特徴があります:
- 一意性:同じ値を持つ行は存在できない
- NOT NULL:NULL値を持つことができない
- 不変性:一度設定したら変更しにくい(変更すると参照整合性に影響する)
複合プライマリーキーとは
複合プライマリーキー(Composite Primary Key)は、2つ以上のフィールドの組み合わせで構成されるプライマリーキーのことです。従来のデータベース設計では、単一のフィールドをプライマリーキーとして使用することが一般的でしたが、複数のフィールドを組み合わせて一意性を確保する必要があるケースも多くあります。
わかりやすい例
注文システムを例に考えてみましょう:
各注文(Order)には複数の商品(Product)が含まれ、各商品は複数の注文に含まれることがあります。この多対多の関係を表現するための中間テーブル(OrderLineItem)では、以下のようなデータ構造が必要です:
注文ID | 商品ID | 数量 |
---|---|---|
1 | 101 | 2 |
1 | 102 | 1 |
2 | 101 | 3 |
2 | 103 | 1 |
このテーブルでは、「注文ID」または「商品ID」のどちらか単独では行を一意に識別できません。「注文ID=1」だけでは2つの行が該当してしまいます。そこで「注文ID」と「商品ID」の組み合わせをプライマリーキーとして使用することで、各行を一意に識別できるようになります。これが複合プライマリーキーの基本的な考え方です。
Django 5.2以前の対応方法
Django 5.2より前は、Djangoは複合プライマリーキーを直接サポートしていませんでしたが、以下のような回避策がありました:
1. サードパーティパッケージの利用
# django-composite-foreignkey パッケージを使用した例
from django.db import models
from compositefk.fields import CompositeForeignKey
class Order(models.Model):
order_id = models.CharField(max_length=20)
class Product(models.Model):
product_id = models.IntegerField()
class OrderLineItem(models.Model):
order_id = models.CharField(max_length=20)
product_id = models.IntegerField()
quantity = models.IntegerField()
# 複合外部キーの定義
order = CompositeForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items',
to_fields={
'order_id': 'order_id'
}
)
class Meta:
# 複合プライマリーキーの代わりに一意性制約を設定
unique_together = ('order_id', 'product_id')
2. Meta.unique_together
また、DjangoのMeta
クラスにunique_together
オプションを設定するも一般的でした:
class OrderLineItem(models.Model):
# 通常は自動生成されるプライマリーキー (id) を持つ
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.IntegerField()
class Meta:
# 複数フィールドの組み合わせに一意性制約を設定
unique_together = ('order', 'product')
この方法で、データベースレベルで一意性は保証され問題なく使えそうです。しかし内部的には依然として自動生成のid
フィールドがプライマリーキーとして使用されていました。
3. カスタムSQL
より直接的なアプローチとして、マイグレーションでカスタムSQLを実行する方法もあるようです:
# migrations/0002_custom_primary_key.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.RunSQL(
"ALTER TABLE myapp_orderlineitem DROP CONSTRAINT myapp_orderlineitem_pkey;",
"ALTER TABLE myapp_orderlineitem ADD PRIMARY KEY (order_id, product_id);"
),
]
しかし、ちょっと無理やりな気がしますね
従来の方法の問題点
これらの方法は以下のような問題を抱えていました:
- 実装が複雑で直感的でない
- コード内の表現と実際のデータベース構造に不一致が生じやすい
- マイグレーションやモデル変更時にエラーが発生しやすい
- ORMの機能が制限される場合がある
- 保守が難しく、開発効率が下がる
これらの課題が、Django 5.2で解決されます!
Django 5.2での新機能:CompositePrimaryKey
Django 5.2では、待望のCompositePrimaryKey
クラスが導入され、複合プライマリーキーを簡単かつ直感的に定義できるようになりました。
基本的な使用方法
複合プライマリーキーを定義するには、モデルのpk
属性にCompositePrimaryKey
インスタンスを設定します:
# LinkedInのユーザー名: djv-mo の例をベースに解説
from django.db import models
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()
動作原理
この例では、OrderLineItem
モデルのプライマリーキーがproduct_id
とorder_id
の組み合わせとして定義されます。具体的には以下のことが起こります:
- Djangoはテーブル作成時に複合プライマリーキー(
PRIMARY KEY (product_id, order_id)
)を作成します - 自動生成される
id
フィールドは作成されません -
product
とorder
の外部キーフィールドが、複合プライマリーキーの一部として機能します
マイグレーションファイルでの表現
マイグレーションファイルでは、複合プライマリーキーは以下のように表現されます:
# migrations/0001_initial.py(自動生成されるファイル)
migrations.CreateModel(
name='OrderLineItem',
fields=[
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='myapp.product')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='myapp.order')),
('quantity', models.IntegerField()),
],
options={
'db_table': 'myapp_orderlineitem',
},
),
migrations.AddConstraint(
model_name='orderlineitem',
constraint=models.UniqueConstraint(fields=('product', 'order'), name='orderlineitem_pk', primary_key=True),
),
生成されるSQLは以下のようになります(PostgreSQLの例):
CREATE TABLE "myapp_orderlineitem" (
"product_id" integer NOT NULL,
"order_id" varchar(20) NOT NULL,
"quantity" integer NOT NULL,
PRIMARY KEY ("product_id", "order_id"),
FOREIGN KEY ("product_id") REFERENCES "myapp_product" ("id") DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY ("order_id") REFERENCES "myapp_order" ("reference") DEFERRABLE INITIALLY DEFERRED
);
このように、複合プライマリーキーはデータベースレベルで、本物の複合プライマリーキーとして機能します。
複合プライマリーキーの利用方法
複合プライマリーキーを実際に使用するにあたって、いくつかの重要な操作パターンを見ていきましょう。
オブジェクトの作成と取得
複合プライマリーキーを持つオブジェクトの作成は通常のDjangoモデルと同じように行えます:
# モデルインスタンスの作成
>>> product = Product.objects.create(name="apple")
>>> order = Order.objects.create(reference="A955H")
>>> item = OrderLineItem.objects.create(product=product, order=order, quantity=1)
作成されたオブジェクトのプライマリーキー(pk)は、タプル(tuple)として表現されます:
>>> item.pk
(1, "A955H") # (product_id, order_id) のタプル
プライマリーキーを直接指定したインスタンス作成
複合プライマリーキーの値がわかっている場合は、pk
属性にタプルを直接代入してインスタンスを作成できます:
# プライマリーキー値を直接指定
>>> item = OrderLineItem(pk=(2, "B142C"))
# 関連するフィールド値が自動的に設定される
>>> item.pk
(2, "B142C")
>>> item.product_id # 外部キーの値が設定される
2
>>> item.order_id
"B142C"
# 残りの属性を設定して保存
>>> item.quantity = 3
>>> item.save()
クエリでの活用
複合プライマリーキーを使ったフィルタリングも行えます:
# プライマリーキーでの検索
>>> OrderLineItem.objects.filter(pk=(1, "A955H")).count()
1
# または個別のフィールドでフィルタリング
>>> OrderLineItem.objects.filter(product_id=1, order_id="A955H").first()
<OrderLineItem: OrderLineItem object (1, 'A955H')>
# 複数のプライマリーキー値でフィルタリング
>>> OrderLineItem.objects.filter(pk__in=[(1, "A955H"), (2, "B142C")])
<QuerySet [<OrderLineItem: (1, 'A955H')>, <OrderLineItem: (2, 'B142C')>]>
オブジェクトの更新
複合プライマリーキーを持つオブジェクトの更新は、プライマリーキー以外のフィールドに対して通常通り行えます:
# オブジェクトの取得と更新
>>> item = OrderLineItem.objects.get(pk=(1, "A955H"))
>>> item.quantity = 5
>>> item.save()
# 更新後の確認
>>> OrderLineItem.objects.get(pk=(1, "A955H")).quantity
5
注意: プライマリーキーの値自体を変更すると、新しいオブジェクトが作成されます(既存の行は更新されません)。これは単一のプライマリーキーの場合と同じです。
現在の制限事項と注意点
Django 5.2の複合プライマリーキーサポートは画期的な機能ですが、まだ初期段階であり、いくつかの注意点があります。
1. リレーションシップフィールドの制限
複合プライマリーキーを持つモデルは、現状では通常のForeignKey
から参照できません:
# これはサポートされていない
class Foo(models.Model):
item = models.ForeignKey(OrderLineItem, on_delete=models.CASCADE)
代わりに、ForeignObject
を使用します:
class Foo(models.Model):
# 複合プライマリーキーの各要素を個別のフィールドとして保持
item_product_id = models.IntegerField()
item_order_id = models.CharField(max_length=20)
# ForeignObjectを使って関連付け
item = models.ForeignObject(
OrderLineItem,
on_delete=models.CASCADE,
from_fields=("item_product_id", "item_order_id"),
to_fields=("product_id", "order_id"),
)
注意:ForeignObject
は非推奨になっており、プロダクション環境では使用しないほうがいいかもれしれません。
2. マイグレーションの制限
以下のマイグレーションはサポートされていません:
- 既存テーブルを単一プライマリーキーから複合プライマリーキーへ変更する
- 複合プライマリーキーから単一プライマリーキーへ変更する
- 複合プライマリーキーに含まれるフィールドを追加・削除する
既存のテーブルを複合プライマリーキーに移行するには、データベースバックエンドを直接変更し、その後モデルにCompositePrimaryKey
フィールドを追加する必要があるとのことです。
3. Django管理画面で使えない
現時点では、複合プライマリーキーを持つモデルをDjangoアドミンに登録することはできません:
# これはエラーになる
admin.site.register(OrderLineItem)
この機能は将来のリリースで追加される見込みです。
4. データベース関数の制限
多くのデータベース関数は単一の式しか受け付けないため、複合プライマリーキーとの組み合わせには制限があります:
# これはエラーになる
Max("pk") # ValueError: pk は複数のカラム式で構成されているため
# 代わりに個別のフィールドを使用する
Max("product_id") # OK
# Countは例外的に使用可能
Count("pk") # OK
5. JSONシリアライゼーション
複合プライマリーキーをJSONにシリアライズする場合は、タプルをリスト形式に変換する必要があります:
import json
from django.core.serializers.json import DjangoJSONEncoder
class CompositePKJSONEncoder(DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, tuple):
return list(obj)
return super().default(obj)
# 使用例
item = OrderLineItem.objects.first()
json.dumps({"pk": item.pk}, cls=CompositePKJSONEncoder)
# 結果: {"pk": [1, "A955H"]}
フォームでの扱い
複合プライマリーキーは仮想フィールド(単一のデータベースカラムに対応しないフィールド)なので、Djangoのフォーム処理においていくつかの特殊な考慮が必要です。
ModelFormsでの自動除外
複合プライマリーキーフィールド(pk
)は、ModelFormsから自動的に除外されます:
class OrderLineItemForm(forms.ModelForm):
class Meta:
model = OrderLineItem
fields = "__all__" # すべてのフィールドを含める
# フォームインスタンスを作成
>>> form = OrderLineItemForm()
>>> print(form)
<OrderLineItemForm bound=False, valid=Unknown, fields=(product;order;quantity)>
フォームにpk
フィールドを明示的に含めようとすると、エラーが発生します:
class OrderLineItemForm(forms.ModelForm):
class Meta:
model = OrderLineItem
fields = ['pk', 'quantity'] # pkを含めようとする
# 結果
django.core.exceptions.FieldError: Unknown field(s) (pk) specified for OrderLineItem
プライマリーキーフィールドの編集可能性
Djangoモデルのプライマリーキーフィールドは読み取り専用として扱うことがほとんどでしょう。既存のオブジェクトのプライマリーキー値を変更して保存すると、新しいオブジェクトが作成されることになります(古いオブジェクトはそのまま残ります)。
この特性は複合プライマリーキーでも同様です。そのため、プライマリーキーを構成するフィールドにはeditable=False
を設定しておくと意図せず新しいレコードが作成されることを防げます:
class OrderLineItem(models.Model):
pk = models.CompositePrimaryKey("product_id", "order_id")
# 編集不可に設定
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
editable=False # フォームから除外
)
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
editable=False # フォームから除外
)
quantity = models.IntegerField()
カスタムフォームの作成
複合プライマリーキーを持つモデルのフォームを作成する場合、プライマリーキーフィールドの代わりに、他の識別子を使用するのがよいプラクティスです:
class OrderLineItemForm(forms.ModelForm):
# 読み取り専用の表示フィールドを追加
order_reference = forms.CharField(
disabled=True,
required=False,
label="注文番号"
)
product_name = forms.CharField(
disabled=True,
required=False,
label="商品名"
)
class Meta:
model = OrderLineItem
fields = ['order_reference', 'product_name', 'quantity']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk:
self.fields['order_reference'].initial = self.instance.order.reference
self.fields['product_name'].initial = self.instance.product.name
アプリケーション構築のためのヒント
複合プライマリーキーをサポートするアプリケーションを開発する際には、いくつかの重要な設計パターンを知っておくと役立ちます。
プライマリーキーフィールドの取得方法
従来のDjangoアプリケーションでは、モデルのプライマリーキーフィールドを次のように取得していました:
# Django 5.2より前の方法
pk_field = None
for field in Product._meta.get_fields():
if field.primary_key:
pk_field = field
break
print(pk_field) # <django.db.models.fields.AutoField: id>
しかし、複合プライマリーキーの場合、この方法は機能しません。なぜなら、複合プライマリーキーを構成する個々のフィールドのprimary_key
属性はFalse
に設定されるためです:
# 複合プライマリーキーモデルで試すと...
pk_fields = []
for field in OrderLineItem._meta.get_fields():
if field.primary_key:
pk_fields.append(field)
print(pk_fields) # [](空のリスト)
if field.primary_key: はfalse
になります
代わりに、Django 5.2では新しい_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>
]
この属性を使用することで、単一プライマリーキーと複合プライマリーキーの両方に対応したコードを書くことができます。
_meta.pk_fields
を使う習慣をつけておけばよさそうです
URLパターンの設計
複合プライマリーキーを持つモデルのURLパターンを設計する場合、複数のパラメータを使用するアプローチが一般的です:
# urls.py
urlpatterns = [
path(
'order-items/<int:product_id>/<str:order_id>/',
views.order_item_detail,
name='order_item_detail'
),
]
# views.py
def order_item_detail(request, product_id, order_id):
item = get_object_or_404(OrderLineItem, pk=(product_id, order_id))
return render(request, 'order_item_detail.html', {'item': item})
モデルメソッドのカスタマイズ
複合プライマリーキーを持つモデルでは、下記の様なメソッドを追加しておくと便利です:
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()
def __str__(self):
# 表示用の文字列表現
return f"Order {self.order.reference} - {self.product.name} (x{self.quantity})"
def get_absolute_url(self):
# 詳細ページへのURL
from django.urls import reverse
return reverse('order_item_detail', kwargs={
'product_id': self.product_id,
'order_id': self.order_id
})
@classmethod
def get_by_pk(cls, product_id, order_id):
# プライマリーキーで検索するヘルパーメソッド
return cls.objects.get(pk=(product_id, order_id))
モデル検証の調整
複合プライマリーキーを持つモデルの検証を行う場合、次の点に注意する必要があります:
# clean_fields()メソッドのexcludeに'pk'を指定しても効果がない
item.clean_fields(exclude={'pk'}) # pkフィールドは除外されない
# 代わりに、プライマリーキーを構成する個々のフィールドを指定する
item.clean_fields(exclude={'product', 'order'}) # これは有効
# validate_unique()メソッドでは'pk'を使用できる
item.validate_unique(exclude={'pk'}) # これは有効
実践的なユースケース
複合プライマリーキーが役立つシナリオを見ていきましょう。
1. 多対多関係の中間テーブル
最も一般的なユースケースは、多対多(Many-to-Many)関係を表現する中間テーブルです。
例:注文システム
ECサイトの注文処理を考えてみましょう:
class Order(models.Model):
order_number = models.CharField(max_length=20, primary_key=True)
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
order_date = models.DateField()
class Product(models.Model):
code = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
class OrderItem(models.Model):
pk = models.CompositePrimaryKey("order_id", "product_id")
order = models.ForeignKey(Order, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField()
price_at_order = models.DecimalField(max_digits=10, decimal_places=2)
この設計では、各注文アイテムが「注文番号」と「商品コード」の組み合わせで一意に識別されます。
例:学生の講座登録
大学の履修登録システム:
class Student(models.Model):
student_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=100)
class Course(models.Model):
course_code = models.CharField(max_length=10, primary_key=True)
title = models.CharField(max_length=100)
class Enrollment(models.Model):
pk = models.CompositePrimaryKey("student_id", "course_id")
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
enrollment_date = models.DateField(auto_now_add=True)
grade = models.CharField(max_length=2, blank=True, null=True)
2. 時系列データとエンティティの組み合わせ
同じエンティティの時系列データを追跡する場合にも複合プライマリーキーが有効です。
例:日次の在庫記録
class Product(models.Model):
code = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=100)
class InventoryRecord(models.Model):
pk = models.CompositePrimaryKey("product_id", "date")
product = models.ForeignKey(Product, on_delete=models.CASCADE)
date = models.DateField()
quantity = models.IntegerField()
warehouse = models.CharField(max_length=50)
この設計では、各商品の特定の日付における在庫状況が記録されます。
例:従業員の勤務記録
class Employee(models.Model):
employee_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=100)
class WorkRecord(models.Model):
pk = models.CompositePrimaryKey("employee_id", "work_date")
employee = models.ForeignKey(Employee, on_delete=models.CASCADE)
work_date = models.DateField()
hours_worked = models.DecimalField(max_digits=4, decimal_places=2)
project = models.CharField(max_length=50)
3. 地理的データと測定値
位置情報と測定値を組み合わせるケースにも適しています。
例:地理的な測定ポイント
class MeasurementPoint(models.Model):
pk = models.CompositePrimaryKey("latitude", "longitude")
latitude = models.DecimalField(max_digits=9, decimal_places=6)
longitude = models.DecimalField(max_digits=9, decimal_places=6)
elevation = models.DecimalField(max_digits=6, decimal_places=2)
description = models.CharField(max_length=100)
例:気象観測データ
class WeatherStation(models.Model):
station_id = models.CharField(max_length=10, primary_key=True)
name = models.CharField(max_length=100)
class WeatherObservation(models.Model):
pk = models.CompositePrimaryKey("station_id", "observation_time")
station = models.ForeignKey(WeatherStation, on_delete=models.CASCADE)
observation_time = models.DateTimeField()
temperature = models.DecimalField(max_digits=5, decimal_places=2)
humidity = models.IntegerField()
4. 多言語コンテンツ
多言語対応のコンテンツ管理にも複合プライマリーキーが有効です。
class Article(models.Model):
article_id = models.CharField(max_length=20, primary_key=True)
publication_date = models.DateField()
class ArticleTranslation(models.Model):
pk = models.CompositePrimaryKey("article_id", "language_code")
article = models.ForeignKey(Article, on_delete=models.CASCADE)
language_code = models.CharField(max_length=5) # 'en', 'ja', 'fr' など
title = models.CharField(max_length=200)
content = models.TextField()
このようなユースケースでは、複合プライマリーキーを使用することで、データベース設計がより自然になり、データの整合性が向上します。
パフォーマンスと最適化
複合プライマリーキーを使用する際のパフォーマンスについても考慮すべき点があります。
インデックスの効率
複合プライマリーキーはデータベースによって自動的にインデックスされますが、その効率はフィールドの順序に大きく依存します:
# フィールドの順序が重要
pk = models.CompositePrimaryKey("product_id", "order_id")
この例では、クエリは最初の列(product_id
)に基づいてインデックスされます。そのため、以下のクエリは効率的です:
# 効率的なクエリ
OrderLineItem.objects.filter(product_id=1)
しかし、次のクエリはインデックスを効率的に利用できません:
# 非効率的なクエリ(最初の列を含まない)
OrderLineItem.objects.filter(order_id="A955H")
フィールドの順序は、最も頻繁にフィルタリングするフィールドを最初に配置するよう検討すべきです。
複合インデックスの追加
追加のインデックスを作成することで特定のクエリパターンを最適化できます:
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()
class Meta:
# order_idだけでのフィルタリングを高速化するインデックス
indexes = [
models.Index(fields=['order_id']),
]
まとめ
Django 5.2(2025年4月2日リリース)で導入された複合プライマリーキー機能は、実に2005年から要望されていた待望の機能です。ようやく実現した複合プライマリーキーにより、以下のメリットが得られます:
- 自然なデータモデリング: 単一フィールドでは表現できない複雑なデータ関係をより自然に、データベース設計の原則に忠実に実装できます。
- データ整合性の向上: データベースレベルでの一意性制約として機能し、アプリケーションの堅牢性を高めます。
- コードの簡素化: 従来は回避策やワークアラウンドが必要だった実装が、よりシンプルで直感的になります。
現時点ではDjango管理画面やリレーションシップフィールドのサポート、マイグレーションなどの面でいくつかの制限がありますが、これらは将来のリリースでの改善を期待します。