Composite Primary Keys in Django 5.2: A Complete Guide

This article explains Django 5.2’s new support for composite primary keys. It covers how to define CompositePrimaryKey, how pk behaves as a tuple, limitations in migrations and relationships, how composite keys interact with forms and database functions, and how to properly introspect primary key fields using _meta.pk_fields.

Django 5.2 composite primary keyCompositePrimaryKey, multi-column primary keyForeignObject, pk_fields, Django models

~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:

  1. Modify the database schema manually according to your backend’s instructions.
  2. Add CompositePrimaryKey to your Django model.
  3. Apply migrations using --fake to avoid errors.
  4. 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

Related Articles

Django Tasks Framework: A Complete Guide to Background Task Execution in Django 6.0

Django 6.0 introduces the Tasks framework, a built‑in system for defining and queuing background work outside the request–response cycle. This article explains how Tasks work, how to configure backends, how to define and enqueue tasks, how context works, and how to integrate third‑party worker systems for production environments.

Continue

Asynchronous Support in Django: A Complete Guide to Async Views, ORM, Middleware, Performance, and Safety

This article explains Django’s asynchronous (async) capabilities, including async views, ASGI support, middleware behavior, async ORM features, performance considerations, handling disconnects, and Django’s async safety protections. It also covers how to use sync_to_async(), async ORM methods, and how to safely run synchronous code in async environments.

Continue

Django System Check Framework: A Complete Guide to Writing, Registering, Running, and Testing System Checks

This article explains Django’s System Check Framework—a powerful mechanism for detecting configuration issues, validating project structure, and ensuring code quality. It covers how checks are executed, how to write custom checks, how messages work, how to register and tag checks, how to extend checks for fields and models, and how to write both unit and integration tests for system checks.

Continue

Django Signals: A Complete Guide to Listening, Connecting, Sending, and Managing Application Events

This article provides a comprehensive explanation of Django’s signal system—an event‑driven mechanism that allows decoupled applications to react to actions occurring elsewhere in the framework. It covers how to define receivers, connect signals, use decorators, handle specific senders, organize signal code, and follow best practices to avoid complexity.

Continue

Understanding Django Settings: Configuration, Environment Management, and Best Practices

This article provides a complete overview of Django’s settings system. It explains how settings files work, how to designate a settings module, how to use settings in your code, how to configure Django manually, how to secure sensitive settings, and how to work with custom default settings. It also covers the role of django.setup() for standalone scripts.

Continue

Serializing and Deserializing Django Objects: A Complete Guide to Django’s Serialization Framework

This article explains Django’s serialization framework, including how to serialize and deserialize model instances, work with subsets of fields, handle inherited models, use different serialization formats (JSON, XML, YAML, JSONL), and understand how relational fields are represented. It also covers DeserializedObject behavior and common pitfalls.

Continue