Filter concepts by levelShowing all levels.

Django · Section 10

Django Models

Level
advanced
Read
24 min
Concepts
3

Primary keys beyond the default id — a UUIDField primary key and why default=uuid.uuid4 must be the callable, not a call — plus the field-level lookup/naming options (db_index, unique, db_column), and the two groups of Model Meta options: db_table/verbose_name/ordering, and the database-level rules in constraints/indexes.

What is true here

  1. Every model gets an automatic AutoField id unless another field sets primary_key=True; a UUIDField primary key needs default=uuid.uuid4, the callable, not uuid.uuid4().
  2. db_index/unique are enforced by the database itself, not just Django; db_column renames only the underlying column, never the Python attribute.
  3. Meta.ordering sets the default sort for every unordered query — an explicit .order_by() overrides it, and it does NOT apply inside annotate()/aggregate() GROUP BY queries.
  4. UniqueConstraint/CheckConstraint enforce rules at the database level and can be conditional (condition=Q(...)); models.Index is purely a performance tool with no rule attached.
  5. Every constraint and index needs a unique name — required, since a migration identifies it by that name.

What you will be able to do

  • Choose between the default AutoField id and a UUID primary key for a given use case
  • Use db_index/unique/db_column correctly, understanding what each actually enforces
  • Set a sensible Meta.ordering, and know when it silently doesn't apply
  • Write a conditional UniqueConstraint for a partial uniqueness rule

Primary keys and field options

Beyond the default id — a UUID primary key, and the field-level options that control lookup speed and naming.

Primary keys and field-level options

coreintermediate

Django adds an auto-incrementing id (AutoField) primary key unless a field is explicitly marked primary_key=True — a UUIDField with default=uuid.uuid4 is a common non-sequential alternative. db_index=True and unique=True add a database-level index/constraint; db_column renames just the underlying column, independent of the Python attribute name.

Think of it as

The default id is a ticket-counter primary key: sequential, predictable, and revealing (row 42 was probably created before row 100). A UUIDField primary key trades that predictability away deliberately — for public-facing IDs where guessing '/orders/43/' should not work, or for merging data from multiple sources where sequential IDs would collide. db_index/unique/db_column are all about the COLUMN, not the Python attribute — a field can be named one thing in Python and a completely different thing in the actual table via db_column.

python
import uuid

class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

What we're doing: Use a UUID primary key for a model whose IDs are exposed in public URLs, so sequential guessing isn't possible.

orders/models.pypython
import uuid
from django.db import models

class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    customer = models.ForeignKey("customers.Customer", on_delete=models.PROTECT)
    total = models.DecimalField(max_digits=10, decimal_places=2)
5
default=uuid.uuid4 passes the FUNCTION itself, not a call to it — Django calls it once per new instance, generating a fresh UUID each time; editable=False keeps it out of any ModelForm/admin edit view.

Why this works: A sequential id would let anyone guess "/orders/44/" exists right after seeing "/orders/43/" — a UUID primary key removes that predictability entirely, at the cost of a less compact, non-sortable-by-creation-order identifier compared to the default AutoField.

Calling uuid.uuid4() instead of passing the function as default

Wrong

python
class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4())   # called once, at class-definition time

Better

python
class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)   # the function itself

What you see: Every Order created gets the exact same UUID as its primary key, causing an IntegrityError on the second insert — a unique constraint violation that seems to make no sense at first glance.

Why: uuid.uuid4() calls the function immediately, once, when the class body runs at import time — that single generated value becomes the default for every future instance. default=uuid.uuid4 (no parentheses) instead passes the callable itself, which Django calls fresh for each new instance, generating a distinct UUID every time.

Sequential id vs UUID primary key

AutoField (default id)

  • +Sequential, predictable
  • +Row 42 was likely created before row 100
  • +Compact, sortable by creation order

UUIDField (default=uuid.uuid4)

  • Non-sequential — not guessable
  • Safe for public-facing URLs
  • Needs the callable, not a call: uuid.uuid4
  • AutoField (default id)
    • Sequential, predictable
    • Row 42 was likely created before row 100
    • Compact, sortable by creation order
  • UUIDField (default=uuid.uuid4)
    • Non-sequential — not guessable
    • Safe for public-facing URLs
    • Needs the callable, not a call: uuid.uuid4

Field-level lookup and naming options

Field-level lookup and naming options
OptionEffect
primary_key=Truethis field IS the primary key — disables the automatic id
default=value (or callable)value used when none is provided at creation
editable=Falseexcluded from ModelForm/admin editing — still settable in code
db_index=Trueadds a plain index on this column
unique=Trueadds a database-level UNIQUE constraint
db_column="name"the real column name, if different from the Python attribute

Together

python
import uuid
from django.db import models

class Order(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    reference = models.CharField(max_length=20, unique=True, db_index=True)
    internal_notes = models.TextField(db_column="notes", blank=True)

Remember: default=uuid.uuid4 (the callable, not a call) for a UUID primary key; db_index/unique are enforced by the database itself; db_column renames only the underlying column, not the Python attribute.

See also: models · meta ordering · constraints and indexes

Advertisement

Model Meta

The model-wide settings panel — naming and default sort order, then the database-level rules a Meta can declare.

Model Meta: db_table, verbose_name, ordering

coreintermediate

class Meta inside a model configures the model itself rather than any one field — db_table renames the table, verbose_name/verbose_name_plural set the human-readable names Django uses (mainly in the admin), and ordering sets the default sort applied whenever no explicit .order_by() is given.

Think of it as

Meta is the model's own settings panel, separate from its fields — nothing in Meta describes a column, everything in it describes how the model as a WHOLE behaves or displays. ordering is the most consequential of the three: it changes what QuerySet.all() returns by default, silently, everywhere that query is used without its own .order_by() — which is exactly why it's worth knowing it's there rather than discovering it by surprise.

python
class Article(models.Model):
    title = models.CharField(max_length=200)

    class Meta:
        ordering = ["-created_at"]

What we're doing: Set a default ordering so the latest articles come first everywhere, without every view needing its own .order_by().

articles/models.pypython
class Article(models.Model):
    title = models.CharField(max_length=200)
    published_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-published_at"]

# Article.objects.all() is now newest-first by default, everywhere
6
The "-" prefix on "published_at" means descending — newest first — the same convention used by QuerySet.order_by().

Why this works: Setting ordering once in Meta means every place in the codebase that queries Article.objects.all() — a view, the admin, a template, a management command — gets a consistent, predictable order without each caller needing to remember to add .order_by("-published_at") itself.

Assuming ordering applies inside an .annotate()/.aggregate() GROUP BY query

Wrong

python
class Article(models.Model):
    class Meta:
        ordering = ["-published_at"]

# assumed: grouped results respect Meta.ordering automatically
Article.objects.values("category").annotate(count=Count("id"))

Better

python
Article.objects.values("category").annotate(count=Count("id")).order_by("category")
# explicit order_by() required — Meta.ordering is not applied here

What you see: A GROUP BY query (via values().annotate()) returns rows in an arbitrary, database-dependent order, even though the model has Meta.ordering set.

Why: Django's docs explicitly call out that Meta.ordering does not apply within aggregate/GROUP BY queries — the grouping changes what a "row" even means, and an explicit .order_by() is required on that specific queryset rather than relying on the model-level default.

What each Meta option controls

class Article(models.Model): title = models.CharField(max_length=200) published_at = models.DateTimeField() class Meta: db_table = "cms_article" verbose_name_plural = "Articles" ordering = ["-published_at"]

db_table = "cms_article"

db_table — overrides the default table name

verbose_name_plural = "Articles"

verbose_name_plural — the admin display name

ordering = ["-published_at"]

ordering — default sort for every unordered query — overridden by an explicit .order_by()

  • Whole: class Article(models.Model): title = models.CharField(max_length=200) published_at = models.DateTimeField() class Meta: db_table = "cms_article" verbose_name_plural = "Articles" ordering = ["-published_at"]
  • db_table = "cms_article" — db_table: overrides the default table name
  • verbose_name_plural = "Articles" — verbose_name_plural: the admin display name
  • ordering = ["-published_at"] — ordering: default sort for every unordered query — overridden by an explicit .order_by()

Common Meta options

Common Meta options
OptionAffects
db_tablethe real database table name
verbose_name / verbose_name_pluralhuman-readable singular/plural, mainly in the admin
orderingdefault sort order for every unordered query on this model

Together

python
class Article(models.Model):
    title = models.CharField(max_length=200)
    published_at = models.DateTimeField()

    class Meta:
        db_table = "cms_article"
        verbose_name_plural = "Articles"
        ordering = ["-published_at"]

Remember: Meta.ordering sets the default sort for every unordered query on the model — an explicit .order_by() always overrides it, and it does NOT apply inside annotate()/aggregate() GROUP BY queries.

See also: models · primary keys and field options · constraints and indexes

Meta.constraints and Meta.indexes

coreadvanced

Meta.constraints (a list of UniqueConstraint/CheckConstraint) enforces data rules at the database level, spanning one or more fields — the modern replacement for the older unique_together. Meta.indexes (a list of models.Index) adds database indexes for query performance, independent of any constraint.

Think of it as

A constraint is a rule the database itself refuses to break, no matter what path the data took to get there — Python code, the admin, a raw SQL script, a bug. An index is purely a performance structure with no rule attached — it makes a query faster without changing what data is allowed to exist. It is entirely possible (and common) to have an index with no matching constraint, or a constraint with no matching index — they solve different problems.

python
class Meta:
    constraints = [
        models.CheckConstraint(condition=models.Q(age__gte=18), name="age_gte_18"),
    ]
    indexes = [
        models.Index(fields=["last_name", "first_name"], name="name_idx"),
    ]

What we're doing: Enforce a conditional (partial) uniqueness rule — one active subscription per customer, but any number of cancelled ones.

billing/models.pypython
class Subscription(models.Model):
    customer = models.ForeignKey("Customer", on_delete=models.CASCADE)
    status = models.CharField(max_length=20)  # "active" or "cancelled"

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["customer"],
                condition=models.Q(status="active"),
                name="one_active_subscription_per_customer",
            ),
        ]
7
condition=models.Q(status="active") is what makes this a PARTIAL constraint — it only applies to rows where status is "active", so any number of cancelled subscriptions for the same customer are still allowed.

Why this works: A plain UniqueConstraint(fields=["customer"]) would allow only ONE subscription per customer ever, active or not — the condition is what narrows the rule to exactly the business requirement (one ACTIVE subscription at a time), which a check purely in application code could never guarantee against a concurrent write the way a database constraint does.

Adding a UniqueConstraint without a migration

Wrong

python
class Meta:
    constraints = [
        models.UniqueConstraint(fields=["email"], name="unique_email"),
    ]
# edited models.py, but makemigrations was never run

Better

bash
python manage.py makemigrations
python manage.py migrate
# the constraint only exists in the real database after this

What you see: Duplicate rows are still accepted by the database, even though the model appears to declare a UniqueConstraint — the Python class and the real table have drifted apart.

Why: A constraint declared in Meta is just Python until a migration turns it into an actual database-level rule, the same way any other model change works — see the "editing a model field and forgetting to migrate" mistake on the base Models concept. The class describes intent; the migration is what makes it real.

A constraint enforces a rule; an index only speeds up reads

UniqueConstraint / CheckConstraint

the database refuses to break this rule

models.Index

purely a performance structure — no rule attached

  1. UniqueConstraint / CheckConstraint — the database refuses to break this rule
  2. models.Index — purely a performance structure — no rule attached

Constraint types vs. an index

Constraint types vs. an index
ToolEnforcesSpeeds up queries?
UniqueConstraintno two rows share these field(s) — optionally, only when condition matchesyes, as a side effect
CheckConstrainta row-level condition (age >= 18)no
models.Indexnothing — no rule at allyes, that's its only job

Together

python
class Reservation(models.Model):
    table = models.ForeignKey("Table", on_delete=models.CASCADE)
    date = models.DateField()

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["table", "date"], name="one_reservation_per_table_per_day"),
        ]

Remember: UniqueConstraint/CheckConstraint enforce rules at the database level (UniqueConstraint can be conditional via condition=Q(...)); models.Index is purely for query speed, with no rule attached — both need makemigrations/migrate to take effect.

See also: meta ordering · models · primary keys and field options

Advertisement