Course outline

Django / Request and data foundations / Lesson 6

Models, schema, and migration history

Separate Python model declarations, database constraints, data validation, and the reviewed migration history that connects them.

About 4 min readContent reviewed
On this page

Lesson goals

  • A model describes more than a Python record
  • Create a complete disposable storage fixture
  • Review schema and data transitions together

A model describes more than a Python record

A Django model declares fields and relationships that map to database storage under the configured backend. Field choices carry domain meaning: text identifiers are not automatically quantities, and a seat count needs a nonnegative policy. Nullability, blank form input, defaults, uniqueness, and deletion behavior are separate choices. Do not use a permissive default merely to make a migration easy if it invents false business data. Keep the intended invariant in ordinary language before encoding it in fields or constraints. Model code, forms, and database enforcement cooperate, but none is a universal substitute for the other boundaries.

Saving does not automatically run full validation

Model full_clean can perform model-level validation, but ordinary save does not automatically call it. ModelForm validation covers its included model fields through its own lifecycle, while bulk operations and direct queryset updates have different behavior. Database constraints remain essential for invariants that must survive concurrent writes and alternate code paths. A unique validation check can improve feedback but cannot reserve a value between the check and insert. Handle the eventual constraint failure according to the operation’s contract. Likewise, choices improve declared validation and presentation but should not be mistaken for every backend’s independently enforced database check.

Create a complete disposable storage fixture

The first program declares one isolated Course model with an explicit lab app label and creates its table only in the program’s in-memory SQLite connection. It inserts two synthetic records and reads their titles in deterministic order. schema_editor is used solely to provision this disposable demonstration, not as a replacement for application migrations. No project settings or working database are imported. The output establishes local ORM storage behavior under the tested backend; it does not prove production-engine locking, collation, or deployment permissions. Keep that distinction when reusing a small example as test evidence.

python Isolated executable example
from django.conf import settings
settings.configure(
    SECRET_KEY="synthetic-course-key",
    USE_I18N=False, USE_TZ=True,
    INSTALLED_APPS=[],
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    DEFAULT_AUTO_FIELD="django.db.models.AutoField",
    ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()

from django.db import connection, models

class Course(models.Model):
    title = models.CharField(max_length=100)
    seats = models.PositiveIntegerField(default=0)
    class Meta:
        app_label = "lab"

with connection.schema_editor() as editor:
    editor.create_model(Course)
Course.objects.create(title="Django", seats=4)
Course.objects.create(title="Python", seats=2)
print(list(Course.objects.order_by("title").values_list("title", flat=True)))
Expected output
['Django', 'Python']

The only database is the program’s in-memory SQLite connection. Direct schema setup is a fixture technique, not a production migration procedure.

Migrations are a versioned schema contract

The second fragment is a hypothetical migration that adds seats to an earlier catalog Course schema that did not yet have that field. Its dependency identifies the prior migration, and its operation describes the state change. It is not applied here. Generated migrations must be reviewed for defaults, data changes, reversibility, and backend-specific locking or rewrite costs. Editing a deployed migration casually can leave different environments with incompatible histories. A model file change alone does not alter an existing table. Plan compatible application and schema rollout steps, especially when old and new application versions may run simultaneously.

python Application-context fragment
from django.db import migrations, models

class Migration(migrations.Migration):
    dependencies = [("catalog", "0001_initial")]
    operations = [
        migrations.AddField(
            model_name="course", name="seats",
            field=models.PositiveIntegerField(default=0),
        ),
    ]
Expected output
If reviewed and applied in the declared app: a seats field is added with the stated default.

This application migration assumes the earlier catalog model lacks seats. Zero is an explicit teaching default that needs domain review for real existing rows. The migration is not applied.

Review schema and data transitions together

Separate a reversible schema operation from an irreversible data transformation. Backfill rules must preserve meaning, and large tables may need staged changes with measured impact. When code runs inside a data migration, use the historical models supplied by the migration app registry rather than importing the current model and assuming its shape matches the old state. Verify backups and rollback limits through the deployment process before destructive changes. This course never executes project migration commands or changes a configured database; isolated fixtures and reasoned migration review support learning without claiming an operational rollout was tested.

Common errors and fixes

A model edit leaves an existing table unchanged

Why it happens
Schema code and applied migration state were confused.
How to fix it
Review and apply the intended migration through the isolated or approved workflow.
Prevent it next time
Track migration history with the model change.

save accepts data the developer expected full_clean to reject

Why it happens
Automatic model validation was assumed.
How to fix it
Validate at the appropriate boundary and keep database constraints.
Prevent it next time
Test alternate and bulk write paths.

A data migration breaks after later model changes

Why it happens
It imported current model code instead of historical state.
How to fix it
Use the migration app registry’s historical model.
Prevent it next time
Review migration replay from an earlier state.

Check your understanding

1. Why is a form uniqueness check insufficient against two concurrent inserts?

Show worked solution

Both requests can pass validation before either commits. A database unique constraint arbitrates the actual writes, and the application must handle its failure cleanly. Validation provides earlier feedback; it does not reserve the value or replace transaction and constraint design.

2. Plan a safer addition of a required field to a populated large table.

Show worked solution

Define a truthful value policy, consider a staged nullable or otherwise compatible schema change, backfill through a reviewed bounded process, then enforce the final requirement when data and application versions support it. Evaluate backend costs and rollback limits rather than inventing one universal migration command.

Key takeaways

  • Model declarations and applied schema are different states.
  • save does not automatically call full_clean.
  • Migrations need historical and operational review.

Official references