In Django 5.2+, how do you key a line-item table by (order, line_no) with CompositePrimaryKey, and what are its current limits?
answer
- a virtual field named pk
- pk becomes a tuple
- who can point at it
- not for existing tables
basics
~20 sDeclare pk = models.CompositePrimaryKey('order_id', 'line_no'); pk becomes a tuple usable in filters. Limits: ForeignKey and generic relations cannot target the model, the admin cannot register it, and Django cannot migrate a table to or from it.
solid answer
~40 sSince Django 5.2 you set a model attribute named `pk` to `models.CompositePrimaryKey("order_id", "line_no")`, and Django creates `PRIMARY KEY (order_id, line_no)` instead of an `id` column. `line.pk` is a tuple such as `(42, 1)`, you can assign a tuple to it, and `filter(pk=(42, 1))` works. The limits are real: a `ForeignKey` cannot point at the model (use `ForeignObject`, which creates no constraint), generic relations do not support it, the admin cannot register it, and Django cannot migrate an existing table to or from a composite key or change its members. Member fields may not be nullable or generated, `pk` is excluded from `ModelForm`s, `Max("pk")` raises `ValueError`, and code should use `_meta.pk_fields` instead of `field.primary_key`.
code
python · 14 linesfrom django.db import models
class LineItem(models.Model):
pk = models.CompositePrimaryKey("order_id", "line_no")
order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)
line_no = models.PositiveSmallIntegerField()
sku = models.CharField(max_length=40)
quantity = models.PositiveIntegerField()
line = LineItem.objects.get(pk=(42, 1))
line.pk # (42, 1)
LineItem._meta.pk_fields # the order and line_no field objectsgo deeper
Know that Django 5.2 added CompositePrimaryKey and that pk then holds a tuple of the member values.
Declare one correctly, named pk with non-null local members, and use tuple pk values in get() and filter().
Weigh the limits, no ForeignKey targets, no admin, no migration to or from it, against a surrogate id with a unique constraint.
Set a policy for when legacy composite-keyed schemas are mapped as-is and when new tables use surrogate keys, given how young the feature still is.
## Declaring one A **composite primary key** identifies a row by more than one column. Django 5.2 added **`CompositePrimaryKey`** for it. For order lines, where the natural identity is "line 3 of order 42", the declaration is: ```python pk = models.CompositePrimaryKey("order_id", "line_no") ``` Rules Django enforces: - the attribute **must be named `pk`** (system check `fields.E013`); - it needs **at least two distinct** field names, given as strings; - it cannot have `default`, `db_default` or `db_column`, and is never editable; - each member must be a real local column that is **not `null=True`** and **not a `GeneratedField`** (check `models.E042`). Django then creates `PRIMARY KEY (order_id, line_no)` and adds no `id` column. ## Using it The composite key is a **virtual field** — it has no column of its own and represents its members as a `tuple`: | Operation | Result | |---|---| | `line.pk` | `(42, 1)` | | `LineItem(pk=(42, 2))` | sets `order_id=42` and `line_no=2` | | `LineItem.objects.filter(pk=(42, 1))` | a `WHERE` on both columns | | `LineItem.objects.get(pk=(42, 1))` | one row | | `Count("pk")` | allowed | | `Max("pk")` | `ValueError`: the function needs one expression | ## The limits Composite keys are young in Django, and the documented limits decide whether you can use one: 1. **Relations** — a `ForeignKey` cannot reference a model with a composite key, and generic relations do not support it. The workaround is `ForeignObject` with `from_fields`/`to_fields`, which creates **no columns, no foreign key constraint and no index**, and ignores `on_delete`. 2. **Admin** — models with a composite primary key cannot be registered in the Django admin yet. 3. **Migrations** — Django does not migrate a table *to* or *from* a composite key after creation, nor add or remove members. `makemigrations` still detects the change; you apply the database-specific change yourself and record the Django side with `--fake` or `SeparateDatabaseAndState`. 4. **Forms and validation** — `pk` is excluded from `ModelForm`s, and naming it in `fields` raises `FieldError`; `exclude={"pk"}` has no effect in `clean_fields()`, so list member fields individually. 5. **Introspection** — no member field has `primary_key=True`, so generic code must read `_meta.pk_fields`. As with any key, changing a member value on a saved object and calling `save()` creates a second row rather than renaming the first. ## Adopting it on an existing table Because Django will not migrate a table to a composite key, adopting one on a populated table is a manual sequence: 1. Change the key in the database with your backend's own DDL, typically dropping the old primary key constraint and creating the new one over `(order_id, line_no)`. 2. Add `pk = models.CompositePrimaryKey("order_id", "line_no")` to the model and remove the old key field from it. 3. Run `makemigrations`; it detects the change even though it cannot apply it. 4. Apply that migration with `--fake`, or wrap the DDL and the generated operations in `SeparateDatabaseAndState`, so Django's migration state and the real schema agree. Anything that referenced the old `id` — foreign keys, URLs, caches keyed by id — has to be reworked first, which is usually the larger part of the job. ## Composite key or surrogate key? The common alternative keeps a `BigAutoField` `id` and adds `UniqueConstraint(fields=["order", "line_no"], name="uniq_order_line")`. Compare: | | `CompositePrimaryKey` | surrogate `id` + unique constraint | |---|---|---| | Other models reference it | `ForeignObject` only, no constraint | ordinary `ForeignKey` with constraint | | Admin | not supported | supported | | Matches an existing composite-keyed table | yes | needs a schema change | | Extra column and index | none | `id` plus the unique index | Choose the composite key when mapping an existing schema that already uses one, or for a leaf table nothing references. For new tables that other models, the admin or generic code will touch, the surrogate key with a unique constraint remains the lower-friction choice in Django today.
- A Shipment model must reference a LineItem keyed by CompositePrimaryKey; what are the options?A `ForeignKey` cannot target it. Use `ForeignObject` with `from_fields` naming two local columns and `to_fields` naming `order_id` and `line_no`: it gives relation access but creates no database constraint or index and ignores `on_delete`. If integrity matters, give `LineItem` a surrogate `id` and a unique constraint on `(order, line_no)` instead.
- Can you add CompositePrimaryKey to an existing table with an id column through makemigrations?Not directly. Django does not support migrating to or from a composite key. Change the key with your database's own DDL, then add the field to the model and record the migration with `--fake` or `SeparateDatabaseAndState` so state and schema agree.
saying these in an interview costs you the question
- A ForeignKey can point at a model whose key is a CompositePrimaryKey.
- Django migrates an existing id-keyed table to a composite key automatically.
- Members of a composite key may be nullable fields.
- Each member field of a composite key has primary_key=True set.
- The composite key can be given any attribute name, such as line_key.