skip to content

Your Django project must ship country and currency reference rows to every environment, production included: should they come from a fixture or a data migration, and why?

level: seniorimportance: should knowfreq 40%

answer

  1. what runs on its own at deploy
  2. which model shape gets used
  3. current models versus historical ones
  4. idempotent by ISO code

basics

~20 s

Use a data migration: migrate applies it automatically, once, in order, against the historical model state, including test databases. A fixture only loads when someone runs loaddata, uses today's models, and overwrites rows by primary key.

solid answer

~40 s

Reference rows that every environment needs belong in a **data migration**. `migrate` applies it automatically on every database, exactly once, in dependency order, and the migration test runner builds the test database the same way. A `RunPython` step that uses `apps.get_model()` sees the **historical** model at that point in the graph, so it keeps working after the models change. A fixture has none of that: nothing loads it unless someone runs `loaddata`, it is deserialized against **today's** models, so a renamed field breaks an old file, and it overwrites existing rows by `pk`, wiping production edits. Calling `loaddata` inside a migration looks like a shortcut but inherits the current-models problem. Fixtures remain right for development and demo data, test data, and copying rows between databases.

code

python · 21 lines
python
from django.db import migrations

CURRENCIES = [("EUR", "Euro", 2), ("JPY", "Yen", 0), ("USD", "US Dollar", 2)]


def seed(apps, schema_editor):
    Currency = apps.get_model("geo", "Currency")
    for code, name, minor_units in CURRENCIES:
        Currency.objects.update_or_create(
            code=code, defaults={"name": name, "minor_units": minor_units}
        )


def unseed(apps, schema_editor):
    Currency = apps.get_model("geo", "Currency")
    Currency.objects.filter(code__in=[c for c, _, _ in CURRENCIES]).delete()


class Migration(migrations.Migration):
    dependencies = [("geo", "0002_currency")]
    operations = [migrations.RunPython(seed, unseed)]

go deeper

for a junior

Know that migrate runs data migrations automatically, while fixtures only load when someone runs loaddata.

for a middle

Explain historical models from apps.get_model() versus the current models loaddata uses, and why that makes loaddata inside a migration fragile.

for a senior

Show the production judgement: reference rows go in idempotent, reversible data migrations, new rows in new migrations, and fixtures never overwrite production edits.

for a principal

Frame the policy: which data is owned by the schema and shipped with it, and which is disposable environment data, and who is allowed to change each.

## The scenario A payments app needs every environment, from a developer laptop to production, to contain the ISO 4217 currencies it supports and the ISO 3166 countries it operates in. New currencies are added a few times a year. Rows are referenced by foreign keys from orders and accounts, so they must exist before any business data. The question is which Django mechanism delivers them. ## What each mechanism guarantees | Property | Fixture + `loaddata` | Data migration (`RunPython`) | |---|---|---| | Runs without anyone remembering | no, only when `loaddata` is run (or a test case lists it) | yes, as part of `migrate` | | Runs once per database | no, every run re-applies it | yes, recorded in `django_migrations` | | Ordered with schema changes | no | yes, via the migration graph | | Model shape used | the **current** models | the **historical** models from `apps.get_model()` | | Behaviour on existing rows | overwrites by `pk` | whatever the code does (e.g. `update_or_create`) | | Present in test databases | only if a test loads it | yes, migrations build the test database | The Django docs describe both routes for initial data and draw the same line: data provided by migrations is loaded automatically (including when the test database is set up), while fixture data is not loaded automatically except through a test case's `fixtures` attribute. ## Why the fixture route fails for production reference data 1. **It depends on a person.** A new environment is only correct if someone remembers to run `loaddata` after `migrate`, in the right release, every time. Deploy scripts can do it, but then it runs on every deploy. 2. **It overwrites.** Fixture objects carrying `pk` values are saved over existing rows. If support staff corrected a country name in production, the next load silently reverts it. 3. **It is bound to the current models.** The serializer resolves `geo.country` through the live app registry. Rename `iso_code` to `alpha2` and every old fixture breaks; `--ignorenonexistent` only drops unknown fields, it does not map them. 4. **Primary keys collide across environments.** Without natural keys, a fixture's hard-coded ids can clash with rows that already exist. ## The trap: loaddata inside a migration Calling `call_command("loaddata", "currencies")` from `RunPython` looks like the best of both worlds. It is not: the command deserializes with the **current** models, not the historical ones the migration framework hands to `RunPython`. The migration works on the day it is written and breaks months later, when a fresh database replays it against a model that has since gained a non-null column or lost a field. A migration's data step should use `apps.get_model()` and literal values. ## A shape that works - Put the rows in a data migration in the `geo` app, keyed on the ISO code, using `update_or_create` so re-running the logic is harmless. - Give it a reverse function (or declare it irreversible deliberately). - When new currencies arrive, add a **new** migration; do not edit the old one, because it has already run in production and will not run again. - Keep the values inside the migration file rather than reading a shared data file that later edits would silently change. ## Where fixtures still win - **Development and demo data**: a few hundred sample merchants and orders, loaded on demand and thrown away. - **Test data**: fixtures listed on a test case (their mechanics belong to testing). - **Moving rows between databases**: `dumpdata --natural-foreign` piped into `loaddata --database`. ## Review checklist - Does every environment get the rows through `migrate`, with no manual step? - Does the data step use `apps.get_model()` and never import from `geo.models`? - Is it idempotent, keyed on a natural identifier rather than an id? - Is there a reverse function, or a deliberate decision that it is irreversible? - Are later additions new migrations rather than edits to one that has already run? The judgement an interviewer wants to hear is simple: data that the schema depends on travels with the schema; data you might want to reset travels as a fixture.

  • Why is call_command('loaddata', ...) inside a Django RunPython migration a trap?
    `loaddata` deserializes through the live app registry, so it uses today's model classes rather than the historical models `RunPython` receives. When a later migration adds a required field or renames one, replaying the old migration on a fresh database fails, because the fixture no longer matches the current model.
  • A support engineer fixed a currency name in production; which Django seeding route would revert it on the next deploy?
    A fixture reloaded with `loaddata` on every deploy: objects with a `pk` are saved over the existing row, so the fixture's old name comes back. A data migration runs once per database, and a new migration that uses `update_or_create` only touches the rows and fields it names.

saying these in an interview costs you the question

  • Fixtures are loaded automatically by migrate, so they are equivalent to data migrations
  • loaddata inside RunPython uses the historical models, so it is safe forever
  • Reloading the fixture on each deploy is harmless because it only inserts missing rows
  • To add a new currency, edit the original data migration in place
  • Data migrations cannot run in the test database, so fixtures are required there