Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion docs/managers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,39 @@ shortcut:
...

For further discussion see `this topic on the Q&A page
<https://github.com/jazzband/django-polymorphic/discussions/696#discussioncomment-15223661>`_.
<https://github.com/jazzband/django-polymorphic/discussions/696#discussioncomment-15223661>`_.


Natural Key Serialization
-------------------------

When using Django's natural key serialization with :django-admin:`dumpdata` and
:django-admin:`loaddata`, polymorphic models require special handling.

.. important::

Always use :meth:`~polymorphic.managers.PolymorphicQuerySet.non_polymorphic` in
``get_by_natural_key()`` for polymorphic models. Without this, deserialization fails
when loading new objects because polymorphic queries try to fetch incomplete objects.

Example implementation:

.. literalinclude:: ../src/polymorphic/tests/examples/managers/models.py
:language: python
:linenos:

Usage:

.. code-block:: bash

# Dump with natural keys
$ python manage.py dumpdata myapp --natural-primary --natural-foreign > fixtures.json

# Load into another database
$ python manage.py loaddata fixtures.json

.. note::

* Child models inherit ``natural_key()`` from the parent - no need to override
* Always use both ``--natural-primary`` and ``--natural-foreign`` flags with polymorphic models
* See `issue #517 <https://github.com/jazzband/django-polymorphic/issues/517>`_ for details
2 changes: 1 addition & 1 deletion src/polymorphic/tests/deletion/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-08 00:17
# Generated by Django 4.2 on 2026-01-08 16:36

from decimal import Decimal
from django.conf import settings
Expand Down
Empty file.
41 changes: 41 additions & 0 deletions src/polymorphic/tests/examples/managers/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 5.2.9 on 2026-01-08 16:33

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]

operations = [
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('title', models.CharField(max_length=200)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='BlogPost',
fields=[
('article_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='managers.article')),
('author', models.CharField(max_length=100)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('managers.article',),
),
]
Empty file.
21 changes: 21 additions & 0 deletions src/polymorphic/tests/examples/managers/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models
from polymorphic.models import PolymorphicModel
from polymorphic.managers import PolymorphicManager


class ArticleManager(PolymorphicManager):
def get_by_natural_key(self, slug):
return self.non_polymorphic().get(slug=slug)


class Article(PolymorphicModel):
slug = models.SlugField(unique=True)
title = models.CharField(max_length=200)
objects = ArticleManager()

def natural_key(self):
return (self.slug,)


class BlogPost(Article):
author = models.CharField(max_length=100)
37 changes: 37 additions & 0 deletions src/polymorphic/tests/examples/managers/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.test import TestCase
from django.core import serializers
from .models import Article, BlogPost


class NaturalKeyTests(TestCase):
def test_natural_key_serialization(self):
# Create objects
Article.objects.create(slug="article-1", title="First Article")
BlogPost.objects.create(slug="blog-post-1", title="First Blog Post", author="John Doe")

# Serialize using natural keys
data = serializers.serialize(
"json",
Article.objects.all(),
use_natural_foreign_keys=True,
use_natural_primary_keys=True,
)

# Verify serialization contains natural keys (slugs)
self.assertIn("article-1", data)
self.assertIn("blog-post-1", data)

# Deserialize
for obj in serializers.deserialize("json", data):
obj.save()

# Verify objects still exist and are correct types
self.assertEqual(Article.objects.count(), 2)
self.assertEqual(BlogPost.objects.count(), 1)

a1 = Article.objects.get(slug="article-1")
self.assertIsInstance(a1, Article)
self.assertNotIsInstance(a1, BlogPost)

b1 = Article.objects.get(slug="blog-post-1")
self.assertIsInstance(b1, BlogPost)
2 changes: 1 addition & 1 deletion src/polymorphic/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.2 on 2026-01-08 00:17
# Generated by Django 4.2 on 2026-01-08 16:36

from django.conf import settings
from django.db import migrations, models
Expand Down
1 change: 1 addition & 0 deletions src/polymorphic/tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"polymorphic.tests.deletion",
"polymorphic.tests.test_migrations",
"polymorphic.tests.examples.views",
"polymorphic.tests.examples.managers",
)

MIDDLEWARE = (
Expand Down
Loading