RizTech Academy logo
RizTech Academy
Models and the ORMLesson 2 of 630 min

Migrations, and how to not get stuck

You have models in Python, but the database knows nothing about them yet. Migrations are how Django turns a change in your models into a change in the database schema — and they are a version-controlled history of that schema, one of Django's genuinely excellent features. Used well, they make schema changes routine and safe; misunderstood, they are where beginners get most thoroughly stuck. This lesson makes them routine.

The two-command rhythm

Every schema change is two commands, and understanding the split between them is the whole game:

python manage.py makemigrations      # 1. detect model changes, write a migration file
python manage.py migrate             # 2. apply pending migrations to the database
  • makemigrations compares your current models to the last recorded state, works out what changed, and writes a new migration file in the app's migrations/ directory. It does not touch the database — it only records intent.
  • migrate takes the migration files that have not yet been applied and runs them against the database, executing the actual CREATE TABLE, ALTER TABLE, and so on.

So makemigrations describes the change and migrate performs it. When you first ran migrate in the setup lesson with no migration files of your own, it applied Django's built-in apps' migrations (auth, sessions). When you add a model, you run both.

What a migration actually is

A migration is a plain Python file describing operations. After defining Nidaan's first models, makemigrations produced:

Migrations for 'patients':
  patients/migrations/0001_initial.py
    + Create model Doctor
    + Create model Patient
    + Create model Appointment

Open 0001_initial.py and you will find a Migration class listing operations like migrations.CreateModel(...). It is readable, and it is code you commit to git. That is the key insight: your database schema's history lives in version control alongside your code, so every developer and every server can build the exact same schema by running the same migrations in the same order. The migration files are numbered (0001, 0002, …) and each records which migration it depends on, so Django always knows the order.

Why this is better than editing the database by hand

Without migrations, a schema change means someone runs SQL against each database — dev, every teammate's machine, staging, production — and hopes they all ran the same thing. They will not. Migrations replace that with: change the model, makemigrations, commit the file, and everyone (and every server) runs migrate to arrive at the identical schema. The migration files are the single source of truth for the schema, and migrate is idempotent — running it when nothing is pending does nothing, so it is safe to run on every deploy.

The commands that get you unstuck

Three read-only commands are your instruments when something is confusing — and per good practice, reach for them before changing anything:

python manage.py showmigrations       # list every migration and whether it is applied ([X] or [ ])
python manage.py migrate --plan       # show what `migrate` WOULD do, without doing it
python manage.py sqlmigrate patients 0001   # print the actual SQL a migration will run

showmigrations answers "what state is this database in?" — an unapplied migration shows [ ]. sqlmigrate prints the SQL a migration generates, which is both a great learning tool (see the CREATE TABLE your model produced) and a way to review a risky change before it runs. Use these to understand the situation rather than guessing.

The mistakes that get people stuck, and how to avoid them

Migrations trip beginners in a few predictable ways. Named plainly:

  • Editing the database by hand, then wondering why migrations conflict. Once you use migrations, let them own the schema. A manual ALTER TABLE puts the database out of step with the migration history, and Django cannot reconcile it.
  • Adding a non-nullable field to a table with existing rows. If Patient already has rows and you add email = models.CharField(max_length=200) with no default and no null=True, makemigrations asks: what value should the existing rows get? You must either provide a default=, allow null=True, or supply a one-off value. This prompt is not a bug — the database genuinely cannot invent a value for existing rows, and Django is making you decide.
  • Forgetting to makemigrations after changing a model. The model changed but no migration file was written, so migrate has nothing to apply and the database stays old. Symptom: "no such column". The fix is to run makemigrations.
  • Not committing migration files. They are code. A teammate who pulls your model change but not your migration file cannot reproduce your schema. Commit migrations/*.py always.

None of these are mysterious once you hold the model: makemigrations writes files from model changes, migrate applies files to the database, and the files are committed source of truth. Almost every "stuck" situation is one of these four.

Rolling back, and starting over in development

You can migrate backwards by naming an earlier migration — python manage.py migrate patients 0001 reverses everything after 0001. This is useful in development to undo a change. In production, treat reversals with great care (a reversal that drops a column destroys its data), which is why production migrations are planned, not improvised. In early development, if your migrations get into a tangle you cannot reason about, it is acceptable to delete the app's migration files and the dev db.sqlite3 and start fresh — but only in development, never once real data exists. Knowing the difference between "throwaway dev database" and "database with data you cannot recreate" is the line that matters.

Check your work

The two commands. makemigrations inspects model changes and writes a migration file (no DB change); migrate applies pending files to the database (the actual schema change).

What a migration is. A committed, numbered Python file of operations (CreateModel, etc.) describing a schema change — your schema's version-controlled history.

Why migrations beat hand-editing. Everyone and every server runs the same files in the same order to reach the identical schema; migrate is idempotent and safe to run on every deploy.

The three read-only helpers. showmigrations (what is applied), migrate --plan (what would run), sqlmigrate (the SQL a migration produces) — use them to understand before acting.

The four common ways to get stuck. Hand-editing the DB, adding a non-nullable field to populated tables (provide a default/allow null), forgetting makemigrations ("no such column"), and not committing migration files.

Reversing and resetting. You can migrate backwards to an earlier migration; deleting migrations + the dev DB to start fresh is fine only in development, never once real data exists.

Practice

  1. Add the Patient model, run makemigrations, and open the generated 0001_initial.py — read the CreateModel operation.
  2. Run sqlmigrate patients 0001 and read the actual CREATE TABLE SQL your model produced.
  3. Run showmigrations before and after migrate and watch the [ ] become [X].
  4. Add a new non-nullable field to Patient (with existing rows) and run makemigrations; read the prompt Django gives you and choose a one-off default. Understand why it asked.
  5. Change a model but do not run makemigrations; try to use the new field and read the "no such column" error. Fix it with makemigrations + migrate.
  6. In development, migrate patients backward to 0001, then forward again with migrate — a reversible change, observed.

Official documentation

Next: ForeignKey, ManyToMany and OneToOne.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship