~3 min read • Updated Mar 15, 2026
Introduction
In Django, every model has a primary key. Traditionally, this primary key is a single field. However, some database designs require a primary key composed of multiple fields. Starting with Django 5.2, Django officially supports composite primary keys.
Defining a Composite Primary Key
To define a composite primary key, set the model’s pk attribute to CompositePrimaryKey:
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()
This generates a database-level primary key like:
PRIMARY KEY (product_id, order_id)
How pk Works with Composite Keys
For composite primary keys, pk becomes a tuple:
item.pk
# (1, "A755H")
You can assign a tuple directly:
item = OrderLineItem(pk=(2, "B142C"))
item.product_id # 2
item.order_id # "B142C"
Filtering also works with tuples:
OrderLineItem.objects.filter(pk=(1, "A755H")).count()
Current Limitations
Composite primary key support is still evolving. The following features are not supported yet:
- ForeignKey to a model with a composite primary key
- GenericForeignKey
- Registering such models in Django Admin
These capabilities are expected in future releases.
Migrating to a Composite Primary Key
Django cannot migrate an existing table from a single primary key to a composite one. It also cannot add or remove fields from a composite primary key.
To migrate an existing table:
- Modify the database schema manually according to your backend’s instructions.
- Add
CompositePrimaryKeyto your Django model. - Apply migrations using --fake to avoid errors.
- Or use SeparateDatabaseAndState to combine manual and Django migrations.
Composite Primary Keys and Relationships
Relationship fields like ForeignKey do not support composite primary keys:
class Foo(models.Model):
item = models.ForeignKey(OrderLineItem) # ❌ Not supported
Workaround: ForeignObject
You can use ForeignObject instead:
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"),
)
Note: ForeignObject is an internal API and not covered by Django’s deprecation policy.
Composite Primary Keys and Database Functions
Many database functions accept only a single column:
Max("order_id") # OK
Max("product_id") # OK
Max("pk") # ❌ ValueError
Count("pk") # OK
This is because pk expands to multiple columns.
Composite Primary Keys in Forms
Since a composite primary key is a virtual field, it does not appear in ModelForms:
Adding pk to the form raises a FieldError.
Changing a primary key creates a new object, so it’s recommended to set editable=False on all primary key fields.
Model Validation with Composite Keys
Because pk is virtual, excluding it in clean_fields() has no effect.
You must exclude each primary key field individually.
However, validate_unique() can still use exclude={"pk"}.
Introspecting Composite Primary Keys
Previously, you could detect the primary key field using field.primary_key.
With composite keys, no field has primary_key=True.
Instead, use _meta.pk_fields:
Product._meta.pk_fields
# []
OrderLineItem._meta.pk_fields
# [, ]
Conclusion
Composite primary keys in Django 5.2 introduce powerful new capabilities for modeling complex database schemas. Although some limitations remain—especially around relationships, admin integration, and migrations—this feature marks a major step forward for Django’s ORM.
Written & researched by Dr. Shahin Siami