In Django serialization, what are natural_key() and get_by_natural_key(), and why would a fixture use them instead of primary keys?
answer
- ids differ between databases
- a model method and a manager method
- tuple out, tuple in
- two dumpdata flags
- ordering: an attribute on the method
basics
~20 snatural_key() on a model returns a tuple of unique business values; get_by_natural_key() on its default manager finds the row from that tuple. Fixtures then reference rows by stable values like an ISO code instead of database-specific ids.
solid answer
~40 sA **natural key** identifies a row by values that mean something outside the database, such as a currency's ISO code, instead of its auto-generated id, which differs between databases. On the model you define `natural_key()` returning a tuple, and on the model's **default manager** you define `get_by_natural_key(*values)` returning the matching instance. `dumpdata --natural-foreign` then writes foreign keys and many-to-many references as those tuples, and `--natural-primary` omits the object's own `pk`. On load, Django calls `get_by_natural_key()` to resolve references and, for objects without a `pk`, to find an existing row to update. The fields must be unique together, ideally enforced by a constraint, and a key built from another model's key needs `natural_key.dependencies` so that model is dumped first.
code
python · 34 linesfrom django.db import models
class CurrencyManager(models.Manager):
def get_by_natural_key(self, code):
return self.get(code=code)
class Currency(models.Model):
code = models.CharField(max_length=3, unique=True)
name = models.CharField(max_length=64)
objects = CurrencyManager()
def natural_key(self):
return (self.code,)
class CountryManager(models.Manager):
def get_by_natural_key(self, iso_code):
return self.get(iso_code=iso_code)
class Country(models.Model):
iso_code = models.CharField(max_length=2, unique=True)
name = models.CharField(max_length=100)
currency = models.ForeignKey(Currency, on_delete=models.PROTECT)
objects = CountryManager()
def natural_key(self):
return (self.iso_code,)
natural_key.dependencies = ["geo.currency"]go deeper
Remember the pair: natural_key() on the model returns a tuple, get_by_natural_key() on the manager takes it back, and they let fixtures avoid raw ids.
Explain the two dumpdata flags, why the lookup must be on the default manager, and how a pk-less object is matched to an existing row on load.
Show you choose immutable, uniquely constrained fields for the key, declare dependencies for composed keys, and dump contenttypes and permissions only with natural foreign keys.
Discuss whether identifiers exchanged between environments should be natural keys at all, and how that choice interacts with renames and data governance.
## The problem natural keys solve A default Django fixture references every related row by **primary key**. That works while the fixture is loaded into the database it came from, but ids are just counters: the currency with id 3 on a laptop may have id 11 in staging. The worst offenders are rows that `migrate` itself creates, such as `contenttypes.ContentType` and `auth.Permission`, which exist in every database with ids that depend on the order apps were migrated. A fixture that says "permission 42" is wrong somewhere. A **natural key** is a tuple of field values that identify a row in business terms: `("EUR",)` for a currency, `("FR",)` for a country, `(codename, app_label, model)` for a permission. Django's serialization framework can write and read references in that form. ## The two halves | Piece | Where it lives | Used when | |---|---|---| | `natural_key()` | a method on the **model** | serializing: turns an instance into a tuple | | `get_by_natural_key(*key)` | a method on the model's **default manager** | deserializing: turns a tuple back into an instance | They are independent. Define only `get_by_natural_key()` and Django can **load** natural keys but will still **write** ids. Define only `natural_key()` and it can write them but cannot load them back. The lookup goes through the default manager, so if a model has several managers, the one Django treats as default is the one that needs the method. Django's own models show the pattern: `ContentType.natural_key()` returns `(app_label, model)`, `Permission.natural_key()` returns the codename plus its content type's key, and `AbstractBaseUser.natural_key()` returns the username, with `BaseUserManager.get_by_natural_key()` looking it up. ## Writing and loading them 1. `dumpdata --natural-foreign` writes every foreign key and many-to-many reference **to a model that defines `natural_key()`** as that tuple instead of an id. This is the flag the docs recommend whenever permissions or content types are in the dump. 2. `dumpdata --natural-primary` omits `pk` from objects whose model defines `natural_key()`, because the key can be recomputed on load. 3. On `loaddata`, a list value in a relation field is passed to `get_by_natural_key()` of the related model's default manager. 4. For an object with **no `pk`**, the deserializer builds the instance, calls its `natural_key()`, and asks `get_by_natural_key()` for an existing row; if one exists, its `pk` is reused and the row is updated, otherwise a new row is inserted. Step 4 is what makes a natural-primary fixture re-loadable without duplicates, and it is also why the key's fields must genuinely be unique. Uniqueness does not have to be a database constraint for serialization to work, but without one, a second row with the same values makes `get_by_natural_key()` raise `MultipleObjectsReturned`. ## Dependencies and forward references If `Country.natural_key()` is built from a field plus the currency's natural key, a country cannot be resolved until its currency exists. Setting `natural_key.dependencies = ["geo.currency"]` on the method tells `dumpdata --natural-foreign` to serialize currencies before countries. When the order cannot be guaranteed (hand-written files, cycles), `serializers.deserialize(..., handle_forward_references=True)` defers unresolved references and `save_deferred_fields()` fills them in later; that route requires the foreign key to be `null=True`. `loaddata` already uses this mechanism internally. ## Newer detail and pitfalls - **Django 6.1:** a subclass of a model that defines `natural_key()` can return an empty tuple `()` to opt out, and the serializer falls back to the primary key (useful with `--natural-primary`). - A natural key built from mutable data (a display name) breaks as soon as someone renames the row. - `--natural-foreign` affects references only; `--natural-primary` affects the object's own id. Using one without the other is legitimate. ## Choosing the key fields A good natural key is: - **Unique**, preferably enforced by `unique=True` or a `UniqueConstraint`, so `get_by_natural_key()` can only ever return one row. - **Immutable in practice.** ISO codes, slugs that never change, usernames on a system that forbids renames. A display name is a poor key because a harmless edit orphans every fixture that references it. - **Meaningful across environments.** The same currency must have the same key on a laptop, in CI and in production; that is the whole point. - **Small.** It is written into every referencing row in the dump, so a short tuple keeps fixtures readable. For the country and currency scenario, `(iso_code,)` and `(code,)` meet all four.
- In Django, what happens on loaddata when a natural-primary fixture object matches an existing row?The deserializer sees no `pk`, calls the instance's `natural_key()`, and asks the default manager's `get_by_natural_key()` for a row. If it finds one, it copies that row's `pk` onto the instance, so the save updates the row instead of inserting a duplicate. If the lookup raises `DoesNotExist`, the object is inserted as a new row.
- Why does Django's natural_key.dependencies attribute exist?When a natural key includes another model's natural key, that other row must already be in the database when the key is resolved on load. `dependencies` lists the model labels that must be serialized first, and `dumpdata --natural-foreign` sorts its output to respect it. Without it, the dependent rows may come first and fail to resolve.
A primary key is a locker number in one gym: locker 42 in another gym holds someone else's things. A natural key is the owner's name on the bag, which finds the right bag in whichever gym you walk into, provided no two members share that name.
saying these in an interview costs you the question
- natural_key() is enough; Django derives the lookup automatically on load
- get_by_natural_key() can live on any manager, not the default one
- Natural keys can be built from any fields, uniqueness does not matter
- --natural-foreign also removes the object's own primary key from the output
- Natural keys change the table's primary key column in the database