skip to content

When Django's loaddata saves fixture objects, why should post_save receivers check raw=True, and what else does a raw save skip?

level: middleimportance: should knowfreq 38%

answer

  1. saved exactly as presented
  2. the fixture already has the side effects
  3. database may be half-loaded
  4. model save() override bypassed
  5. m2m_changed joined in 6.1

basics

~20 s

loaddata saves each object with raw=True: the model's save() override is bypassed and pre_save/post_save receivers get raw=True, because related rows may not be loaded yet and derived rows are usually already in the fixture. Receivers should return early.

solid answer

~40 s

`loaddata` saves every object through `DeserializedObject.save()`, which calls `Model.save_base(raw=True)` directly. That skips any custom `save()` override, so logic like slug generation does not run, and it sends `pre_save` and `post_save` with `raw=True`. The docs warn that during a raw save you should not query or modify other records, because the database may not be consistent yet: foreign-key checks are suspended and later objects in the fixture are not loaded. A receiver that creates a related row on `post_save` also duplicates rows the fixture already contains, typically ending in an `IntegrityError` on a unique field. So a receiver starts with `if raw: return`. Since Django 6.1 `loaddata` also passes `raw=True` to `m2m_changed`.

code

python · 14 lines
python
from django.db.models.signals import post_save
from django.dispatch import receiver

from geo.models import Country, CountryStats


@receiver(post_save, sender=Country)
def create_country_stats(sender, instance, created, raw, **kwargs):
    if raw:
        # loaddata: the fixture already carries CountryStats rows,
        # and other rows may not be loaded yet.
        return
    if created:
        CountryStats.objects.create(country=instance)

go deeper

for a junior

Recall that loaddata still fires pre_save and post_save, with raw=True, and that receivers should return early when raw is set.

for a middle

Explain that fixture rows go through save_base(raw=True), skipping save() overrides, and why a half-loaded database makes related queries unsafe.

for a senior

Show you audit receivers for raw guards before relying on fixtures, and know that m2m_changed only gained raw in 6.1, so older code could not guard it.

for a principal

Weigh how much behaviour a codebase hides in signals and save() overrides, since every such hook is something fixture loading silently skips or must guard.

## What a raw save is When `loaddata` (or your own code calling `serializers.deserialize()`) writes an object, it goes through `DeserializedObject.save()`. That method deliberately calls `models.Model.save_base(instance, raw=True)` instead of `instance.save()`. **Raw** means "store this object exactly as presented": the values in the fixture are the final values, and nothing should be derived, adjusted or cascaded. Two consequences follow: - The model's **`save()` override is not called**. Anything you do there, such as filling a slug, normalising a code to upper case or stamping a `modified_by`, does not happen for fixture rows. The fixture must already contain the finished values. - **Signals still fire, but they are told**. `pre_save` and `post_save` receive `raw=True`. Since **Django 6.1**, `loaddata` also sends `m2m_changed` with `raw=True` when it sets many-to-many data; before 6.1 that argument did not exist on `m2m_changed`. ## Why receivers must check it Suppose a `post_save` receiver on `Country` creates a `CountryStats` row whenever a country is created. In normal code that is fine. During `loaddata`, three things go wrong: 1. **Duplicates.** A fixture dumped from a real database already contains the `CountryStats` rows. The receiver creates one when the country loads, then the fixture's own row arrives with the same one-to-one target and violates the unique constraint, or the table ends up with two rows per country. 2. **An inconsistent database.** `loaddata` suspends foreign-key checking where the backend supports it and checks integrity only at the end, and later objects in the same file have not been written yet. The signal documentation says a receiver handling a raw save should not query or modify other records because the database might not be in a consistent state. A receiver that reads a related row may get `DoesNotExist`. 3. **Side effects outside the database.** A receiver that sends an email, enqueues work or calls an API fires once per fixture row, which is rarely what anyone wants when seeding a development database. The fix is a guard at the top of the receiver: `if raw: return`. Everything the receiver would have produced is either already in the fixture or should be produced by a deliberate step afterwards. ## What raw does and does not change | Behaviour | Normal `save()` | Raw save from a fixture | |---|---|---| | Model `save()` override | runs | skipped | | `pre_save` / `post_save` | sent, `raw=False` | sent, `raw=True` | | `m2m_changed` | sent, `raw=False` (6.1+) | sent, `raw=True` (6.1+) | | `created` flag in `post_save` | reflects insert vs update | still reflects insert vs update | | Model validation (`full_clean()`) | not called by `save()` either | not called | Note the last row: raw saves are not the reason fixtures skip validation. `save()` never validates; that is the job of forms and explicit `full_clean()` calls. ## Where the guard belongs - In every receiver that **writes** other rows or reads related rows. - In receivers with **external side effects**. - Not needed in receivers that only touch the instance's own already-set attributes, but adding it is cheap and communicates intent. Business logic you want to run even for fixture data has no good home in a raw save; run it explicitly after loading, for example from a management command. ## A trace of one load Take a fixture that holds `geo.country` FR followed by `geo.countrystats` for FR, and a receiver without the guard: 1. `loaddata` opens its transaction and suspends foreign-key checks where the backend allows it. 2. The country row is written with `save_base(raw=True)`; `post_save` fires with `created=True`. 3. The unguarded receiver creates a `CountryStats` row for FR. 4. The fixture's own `CountryStats` row for FR is written next. If the table has a one-to-one or unique constraint on the country, the load fails with an `IntegrityError` and the whole transaction rolls back; if it does not, FR now has two stats rows. With `if raw: return` at step 3, the load writes exactly what the fixture contains, which is the contract `loaddata` promises.

  • A Django model fills its slug in an overridden save(); after loaddata the slugs are empty. Why?
    Fixture objects are written with `Model.save_base(raw=True)`, which bypasses the model's own `save()`. Any value computed there is never computed for fixture rows. Either put the finished slug in the fixture (dumping from a database where save() ran does this automatically) or fill it in a separate step after loading.
  • In Django, does a raw fixture save still report created correctly to post_save?
    Yes. `save_base()` still decides between update and insert, so `created` is `True` for a new row and `False` when the fixture's `pk` overwrote an existing one. That is exactly why `created` alone is not a safe guard: fixture rows are usually `created=True`, and only `raw` tells the receiver it is being loaded.

saying these in an interview costs you the question

  • loaddata turns signals off, so receivers never run during fixture loads
  • loaddata calls the model's overridden save() for every object
  • Checking created is enough; fixture rows are never reported as created
  • A raw save is what stops fixtures being validated with full_clean()
  • m2m_changed has always received a raw argument during loaddata