Although @rockallite's answer is excellent, it does not explain how to handle fixtures that rely on natural keys instead of integer pk
values.
Simplified version
First, note that @rockallite's solution can be simplified by using unittest.mock.patch
as a context manager, and by patching apps
instead of _get_model
:
...
from unittest.mock import patch
...
def load_fixture(apps, schema_editor):
with patch('django.core.serializers.python.apps', apps):
call_command('loaddata', 'your_data.json', ...)
...
This works well, as long as your fixtures do not rely on natural keys.
If they do, you're likely to see a DeserializationError: ... value must be an integer...
.
The problem with natural keys
Under the hood, loaddata
uses django.core.serializers.deserialize()
to load your fixture objects.
The deserialization of fixtures based on natural keys relies on two things:
- the presence of a get_by_natural_key() method on the model's default manager
- the presence of a natural_key() method on the model itself
The get_by_natural_key()
method is necessary for the deserializer to know how to interpret the natural key, instead of an integer pk
value.
Both methods are necessary for the deserializer to get
existing objects from the database by natural key, as also explained here.
However, the apps
registry which is available in your migrations uses historical models, and these do not have access to custom managers or custom methods such as natural_key()
.
Possible solution: step 1
The problem of the missing get_by_natural_key()
method from our custom model manager is relatively easy to solve:
Just set use_in_migrations=True
on your custom manager, as described in the documentation.
This ensures that your historical models can access the current get_by_natural_key()
during migrations, and fixture loading should now succeed.
However, your historical models still don't have a natural_key()
method. As a result, your fixtures will be treated as new objects, even if they are already present in the database.
This may lead to a variety of errors if the data-migration is ever re-applied, such as:
- unique-constraint violations (if your models have unique-constraints)
- duplicate fixture objects (if your models do not have unique-constraints)
- "get returned multiple objects" errors (due to duplicate fixture objects created previously)
So, effectively, you're still missing out on a kind of get_or_create-like behavior during deserialization.
To experience this, just apply a data-migration as described above (in a test environment), then roll back the same data-migration (without removing the data), then re-apply the data-migration.
Possible solution: step 2
The problem of the missing natural_key()
method from the model itself is a bit more difficult to solve.
One solution would be to assign the natural_key()
method from the current model to the historical model, for example:
...
from unittest.mock import patch
from django.apps import apps as current_apps
from django.core.management import call_command
...
def load_fixture(apps, schema_editor):
def _get_model_patch(app_label):
""" add natural_key method from current model to historical model """
historical_model = apps.get_model(app_label=app_label)
current_model = current_apps.get_model(app_label=app_label)
historical_model.natural_key = current_model.natural_key
return historical_model
with patch('django.core.serializers.python._get_model', _get_model_patch):
call_command('loaddata', 'your_data.json', ...)
...
Notes:
- For clarity, I omitted things like error handling and attribute checking from the example. You should implement those where necessary.
- This solution uses the current model's
natural_key
method, which may still lead to trouble in certain scenarios, but the same goes for Django's use_in_migrations
option for model managers.