Fixed #26291 -- Allowed loaddata to handle forward references in natural_key fixtures.
This commit is contained in:
parent
8f75d21a2e
commit
312eb5cb11
|
@ -109,8 +109,11 @@ class Command(BaseCommand):
|
||||||
return
|
return
|
||||||
|
|
||||||
with connection.constraint_checks_disabled():
|
with connection.constraint_checks_disabled():
|
||||||
|
self.objs_with_deferred_fields = []
|
||||||
for fixture_label in fixture_labels:
|
for fixture_label in fixture_labels:
|
||||||
self.load_label(fixture_label)
|
self.load_label(fixture_label)
|
||||||
|
for obj in self.objs_with_deferred_fields:
|
||||||
|
obj.save_deferred_fields(using=self.using)
|
||||||
|
|
||||||
# Since we disabled constraint checks, we must manually check for
|
# Since we disabled constraint checks, we must manually check for
|
||||||
# any invalid keys that might have been added
|
# any invalid keys that might have been added
|
||||||
|
@ -163,6 +166,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
objects = serializers.deserialize(
|
objects = serializers.deserialize(
|
||||||
ser_fmt, fixture, using=self.using, ignorenonexistent=self.ignore,
|
ser_fmt, fixture, using=self.using, ignorenonexistent=self.ignore,
|
||||||
|
handle_forward_references=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
for obj in objects:
|
for obj in objects:
|
||||||
|
@ -189,6 +193,8 @@ class Command(BaseCommand):
|
||||||
'error_msg': e,
|
'error_msg': e,
|
||||||
},)
|
},)
|
||||||
raise
|
raise
|
||||||
|
if obj.deferred_fields:
|
||||||
|
self.objs_with_deferred_fields.append(obj)
|
||||||
if objects and show_progress:
|
if objects and show_progress:
|
||||||
self.stdout.write('') # add a newline after progress indicator
|
self.stdout.write('') # add a newline after progress indicator
|
||||||
self.loaded_object_count += loaded_objects_in_fixture
|
self.loaded_object_count += loaded_objects_in_fixture
|
||||||
|
|
|
@ -3,8 +3,11 @@ Module for abstract serializer/unserializer base classes.
|
||||||
"""
|
"""
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
DEFER_FIELD = object()
|
||||||
|
|
||||||
|
|
||||||
class SerializerDoesNotExist(KeyError):
|
class SerializerDoesNotExist(KeyError):
|
||||||
"""The requested serializer was not found."""
|
"""The requested serializer was not found."""
|
||||||
|
@ -201,9 +204,10 @@ class DeserializedObject:
|
||||||
(and not touch the many-to-many stuff.)
|
(and not touch the many-to-many stuff.)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, obj, m2m_data=None):
|
def __init__(self, obj, m2m_data=None, deferred_fields=None):
|
||||||
self.object = obj
|
self.object = obj
|
||||||
self.m2m_data = m2m_data
|
self.m2m_data = m2m_data
|
||||||
|
self.deferred_fields = deferred_fields
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: %s(pk=%s)>" % (
|
return "<%s: %s(pk=%s)>" % (
|
||||||
|
@ -225,6 +229,25 @@ class DeserializedObject:
|
||||||
# the m2m data twice.
|
# the m2m data twice.
|
||||||
self.m2m_data = None
|
self.m2m_data = None
|
||||||
|
|
||||||
|
def save_deferred_fields(self, using=None):
|
||||||
|
self.m2m_data = {}
|
||||||
|
for field, field_value in self.deferred_fields.items():
|
||||||
|
opts = self.object._meta
|
||||||
|
label = opts.app_label + '.' + opts.model_name
|
||||||
|
if isinstance(field.remote_field, models.ManyToManyRel):
|
||||||
|
try:
|
||||||
|
values = deserialize_m2m_values(field, field_value, using, handle_forward_references=False)
|
||||||
|
except M2MDeserializationError as e:
|
||||||
|
raise DeserializationError.WithData(e.original_exc, label, self.object.pk, e.pk)
|
||||||
|
self.m2m_data[field.name] = values
|
||||||
|
elif isinstance(field.remote_field, models.ManyToOneRel):
|
||||||
|
try:
|
||||||
|
value = deserialize_fk_value(field, field_value, using, handle_forward_references=False)
|
||||||
|
except Exception as e:
|
||||||
|
raise DeserializationError.WithData(e, label, self.object.pk, field_value)
|
||||||
|
setattr(self.object, field.attname, value)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
def build_instance(Model, data, db):
|
def build_instance(Model, data, db):
|
||||||
"""
|
"""
|
||||||
|
@ -244,7 +267,7 @@ def build_instance(Model, data, db):
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def deserialize_m2m_values(field, field_value, using):
|
def deserialize_m2m_values(field, field_value, using, handle_forward_references):
|
||||||
model = field.remote_field.model
|
model = field.remote_field.model
|
||||||
if hasattr(model._default_manager, 'get_by_natural_key'):
|
if hasattr(model._default_manager, 'get_by_natural_key'):
|
||||||
def m2m_convert(value):
|
def m2m_convert(value):
|
||||||
|
@ -262,10 +285,13 @@ def deserialize_m2m_values(field, field_value, using):
|
||||||
values.append(m2m_convert(pk))
|
values.append(m2m_convert(pk))
|
||||||
return values
|
return values
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise M2MDeserializationError(e, pk)
|
if isinstance(e, ObjectDoesNotExist) and handle_forward_references:
|
||||||
|
return DEFER_FIELD
|
||||||
|
else:
|
||||||
|
raise M2MDeserializationError(e, pk)
|
||||||
|
|
||||||
|
|
||||||
def deserialize_fk_value(field, field_value, using):
|
def deserialize_fk_value(field, field_value, using, handle_forward_references):
|
||||||
if field_value is None:
|
if field_value is None:
|
||||||
return None
|
return None
|
||||||
model = field.remote_field.model
|
model = field.remote_field.model
|
||||||
|
@ -273,7 +299,13 @@ def deserialize_fk_value(field, field_value, using):
|
||||||
field_name = field.remote_field.field_name
|
field_name = field.remote_field.field_name
|
||||||
if (hasattr(default_manager, 'get_by_natural_key') and
|
if (hasattr(default_manager, 'get_by_natural_key') and
|
||||||
hasattr(field_value, '__iter__') and not isinstance(field_value, str)):
|
hasattr(field_value, '__iter__') and not isinstance(field_value, str)):
|
||||||
obj = default_manager.db_manager(using).get_by_natural_key(*field_value)
|
try:
|
||||||
|
obj = default_manager.db_manager(using).get_by_natural_key(*field_value)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
if handle_forward_references:
|
||||||
|
return DEFER_FIELD
|
||||||
|
else:
|
||||||
|
raise
|
||||||
value = getattr(obj, field_name)
|
value = getattr(obj, field_name)
|
||||||
# If this is a natural foreign key to an object that has a FK/O2O as
|
# If this is a natural foreign key to an object that has a FK/O2O as
|
||||||
# the foreign key, use the FK value.
|
# the foreign key, use the FK value.
|
||||||
|
|
|
@ -83,6 +83,7 @@ def Deserializer(object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False
|
||||||
It's expected that you pass the Python objects themselves (instead of a
|
It's expected that you pass the Python objects themselves (instead of a
|
||||||
stream or a string) to the constructor
|
stream or a string) to the constructor
|
||||||
"""
|
"""
|
||||||
|
handle_forward_references = options.pop('handle_forward_references', False)
|
||||||
field_names_cache = {} # Model: <list of field_names>
|
field_names_cache = {} # Model: <list of field_names>
|
||||||
|
|
||||||
for d in object_list:
|
for d in object_list:
|
||||||
|
@ -101,6 +102,7 @@ def Deserializer(object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), None)
|
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), None)
|
||||||
m2m_data = {}
|
m2m_data = {}
|
||||||
|
deferred_fields = {}
|
||||||
|
|
||||||
if Model not in field_names_cache:
|
if Model not in field_names_cache:
|
||||||
field_names_cache[Model] = {f.name for f in Model._meta.get_fields()}
|
field_names_cache[Model] = {f.name for f in Model._meta.get_fields()}
|
||||||
|
@ -118,17 +120,23 @@ def Deserializer(object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False
|
||||||
# Handle M2M relations
|
# Handle M2M relations
|
||||||
if field.remote_field and isinstance(field.remote_field, models.ManyToManyRel):
|
if field.remote_field and isinstance(field.remote_field, models.ManyToManyRel):
|
||||||
try:
|
try:
|
||||||
values = base.deserialize_m2m_values(field, field_value, using)
|
values = base.deserialize_m2m_values(field, field_value, using, handle_forward_references)
|
||||||
except base.M2MDeserializationError as e:
|
except base.M2MDeserializationError as e:
|
||||||
raise base.DeserializationError.WithData(e.original_exc, d['model'], d.get('pk'), e.pk)
|
raise base.DeserializationError.WithData(e.original_exc, d['model'], d.get('pk'), e.pk)
|
||||||
m2m_data[field.name] = values
|
if values == base.DEFER_FIELD:
|
||||||
|
deferred_fields[field] = field_value
|
||||||
|
else:
|
||||||
|
m2m_data[field.name] = values
|
||||||
# Handle FK fields
|
# Handle FK fields
|
||||||
elif field.remote_field and isinstance(field.remote_field, models.ManyToOneRel):
|
elif field.remote_field and isinstance(field.remote_field, models.ManyToOneRel):
|
||||||
try:
|
try:
|
||||||
value = base.deserialize_fk_value(field, field_value, using)
|
value = base.deserialize_fk_value(field, field_value, using, handle_forward_references)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), field_value)
|
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), field_value)
|
||||||
data[field.attname] = value
|
if value == base.DEFER_FIELD:
|
||||||
|
deferred_fields[field] = field_value
|
||||||
|
else:
|
||||||
|
data[field.attname] = value
|
||||||
# Handle all other fields
|
# Handle all other fields
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
@ -137,7 +145,7 @@ def Deserializer(object_list, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False
|
||||||
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), field_value)
|
raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), field_value)
|
||||||
|
|
||||||
obj = base.build_instance(Model, data, using)
|
obj = base.build_instance(Model, data, using)
|
||||||
yield base.DeserializedObject(obj, m2m_data)
|
yield base.DeserializedObject(obj, m2m_data, deferred_fields)
|
||||||
|
|
||||||
|
|
||||||
def _get_model(model_identifier):
|
def _get_model(model_identifier):
|
||||||
|
|
|
@ -8,6 +8,7 @@ from xml.sax.expatreader import ExpatParser as _ExpatParser
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.serializers import base
|
from django.core.serializers import base
|
||||||
from django.db import DEFAULT_DB_ALIAS, models
|
from django.db import DEFAULT_DB_ALIAS, models
|
||||||
from django.utils.xmlutils import (
|
from django.utils.xmlutils import (
|
||||||
|
@ -151,6 +152,7 @@ class Deserializer(base.Deserializer):
|
||||||
|
|
||||||
def __init__(self, stream_or_string, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False, **options):
|
def __init__(self, stream_or_string, *, using=DEFAULT_DB_ALIAS, ignorenonexistent=False, **options):
|
||||||
super().__init__(stream_or_string, **options)
|
super().__init__(stream_or_string, **options)
|
||||||
|
self.handle_forward_references = options.pop('handle_forward_references', False)
|
||||||
self.event_stream = pulldom.parse(self.stream, self._make_parser())
|
self.event_stream = pulldom.parse(self.stream, self._make_parser())
|
||||||
self.db = using
|
self.db = using
|
||||||
self.ignore = ignorenonexistent
|
self.ignore = ignorenonexistent
|
||||||
|
@ -181,6 +183,7 @@ class Deserializer(base.Deserializer):
|
||||||
# Also start building a dict of m2m data (this is saved as
|
# Also start building a dict of m2m data (this is saved as
|
||||||
# {m2m_accessor_attribute : [list_of_related_objects]})
|
# {m2m_accessor_attribute : [list_of_related_objects]})
|
||||||
m2m_data = {}
|
m2m_data = {}
|
||||||
|
deferred_fields = {}
|
||||||
|
|
||||||
field_names = {f.name for f in Model._meta.get_fields()}
|
field_names = {f.name for f in Model._meta.get_fields()}
|
||||||
# Deserialize each field.
|
# Deserialize each field.
|
||||||
|
@ -200,9 +203,26 @@ class Deserializer(base.Deserializer):
|
||||||
|
|
||||||
# As is usually the case, relation fields get the special treatment.
|
# As is usually the case, relation fields get the special treatment.
|
||||||
if field.remote_field and isinstance(field.remote_field, models.ManyToManyRel):
|
if field.remote_field and isinstance(field.remote_field, models.ManyToManyRel):
|
||||||
m2m_data[field.name] = self._handle_m2m_field_node(field_node, field)
|
value = self._handle_m2m_field_node(field_node, field)
|
||||||
|
if value == base.DEFER_FIELD:
|
||||||
|
deferred_fields[field] = [
|
||||||
|
[
|
||||||
|
getInnerText(nat_node).strip()
|
||||||
|
for nat_node in obj_node.getElementsByTagName('natural')
|
||||||
|
]
|
||||||
|
for obj_node in field_node.getElementsByTagName('object')
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
m2m_data[field.name] = value
|
||||||
elif field.remote_field and isinstance(field.remote_field, models.ManyToOneRel):
|
elif field.remote_field and isinstance(field.remote_field, models.ManyToOneRel):
|
||||||
data[field.attname] = self._handle_fk_field_node(field_node, field)
|
value = self._handle_fk_field_node(field_node, field)
|
||||||
|
if value == base.DEFER_FIELD:
|
||||||
|
deferred_fields[field] = [
|
||||||
|
getInnerText(k).strip()
|
||||||
|
for k in field_node.getElementsByTagName('natural')
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
data[field.attname] = value
|
||||||
else:
|
else:
|
||||||
if field_node.getElementsByTagName('None'):
|
if field_node.getElementsByTagName('None'):
|
||||||
value = None
|
value = None
|
||||||
|
@ -213,7 +233,7 @@ class Deserializer(base.Deserializer):
|
||||||
obj = base.build_instance(Model, data, self.db)
|
obj = base.build_instance(Model, data, self.db)
|
||||||
|
|
||||||
# Return a DeserializedObject so that the m2m data has a place to live.
|
# Return a DeserializedObject so that the m2m data has a place to live.
|
||||||
return base.DeserializedObject(obj, m2m_data)
|
return base.DeserializedObject(obj, m2m_data, deferred_fields)
|
||||||
|
|
||||||
def _handle_fk_field_node(self, node, field):
|
def _handle_fk_field_node(self, node, field):
|
||||||
"""
|
"""
|
||||||
|
@ -229,7 +249,13 @@ class Deserializer(base.Deserializer):
|
||||||
if keys:
|
if keys:
|
||||||
# If there are 'natural' subelements, it must be a natural key
|
# If there are 'natural' subelements, it must be a natural key
|
||||||
field_value = [getInnerText(k).strip() for k in keys]
|
field_value = [getInnerText(k).strip() for k in keys]
|
||||||
obj = model._default_manager.db_manager(self.db).get_by_natural_key(*field_value)
|
try:
|
||||||
|
obj = model._default_manager.db_manager(self.db).get_by_natural_key(*field_value)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
if self.handle_forward_references:
|
||||||
|
return base.DEFER_FIELD
|
||||||
|
else:
|
||||||
|
raise
|
||||||
obj_pk = getattr(obj, field.remote_field.field_name)
|
obj_pk = getattr(obj, field.remote_field.field_name)
|
||||||
# If this is a natural foreign key to an object that
|
# If this is a natural foreign key to an object that
|
||||||
# has a FK/O2O as the foreign key, use the FK value
|
# has a FK/O2O as the foreign key, use the FK value
|
||||||
|
@ -264,7 +290,17 @@ class Deserializer(base.Deserializer):
|
||||||
else:
|
else:
|
||||||
def m2m_convert(n):
|
def m2m_convert(n):
|
||||||
return model._meta.pk.to_python(n.getAttribute('pk'))
|
return model._meta.pk.to_python(n.getAttribute('pk'))
|
||||||
return [m2m_convert(c) for c in node.getElementsByTagName("object")]
|
values = []
|
||||||
|
try:
|
||||||
|
for c in node.getElementsByTagName('object'):
|
||||||
|
values.append(m2m_convert(c))
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, ObjectDoesNotExist) and self.handle_forward_references:
|
||||||
|
return base.DEFER_FIELD
|
||||||
|
else:
|
||||||
|
raise base.M2MDeserializationError(e, c)
|
||||||
|
else:
|
||||||
|
return values
|
||||||
|
|
||||||
def _get_model_from_node(self, node, attr):
|
def _get_model_from_node(self, node, attr):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -184,7 +184,10 @@ Requests and Responses
|
||||||
Serialization
|
Serialization
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* You can now deserialize data using natural keys containing :ref:`forward
|
||||||
|
references <natural-keys-and-forward-references>` by passing
|
||||||
|
``handle_forward_references=True`` to ``serializers.deserialize()``.
|
||||||
|
Additionally, :djadmin:`loaddata` handles forward references automatically.
|
||||||
|
|
||||||
Signals
|
Signals
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
|
@ -514,17 +514,68 @@ command line flags to generate natural keys.
|
||||||
natural keys during serialization, but *not* be able to load those
|
natural keys during serialization, but *not* be able to load those
|
||||||
key values, just don't define the ``get_by_natural_key()`` method.
|
key values, just don't define the ``get_by_natural_key()`` method.
|
||||||
|
|
||||||
|
.. _natural-keys-and-forward-references:
|
||||||
|
|
||||||
|
Natural keys and forward references
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
|
||||||
|
Sometimes when you use :ref:`natural foreign keys
|
||||||
|
<topics-serialization-natural-keys>` you'll need to deserialize data where
|
||||||
|
an object has a foreign key referencing another object that hasn't yet been
|
||||||
|
deserialized. This is called a "forward reference".
|
||||||
|
|
||||||
|
For instance, suppose you have the following objects in your fixture::
|
||||||
|
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"model": "store.book",
|
||||||
|
"fields": {
|
||||||
|
"name": "Mostly Harmless",
|
||||||
|
"author": ["Douglas", "Adams"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"model": "store.person",
|
||||||
|
"fields": {
|
||||||
|
"first_name": "Douglas",
|
||||||
|
"last_name": "Adams"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
|
||||||
|
In order to handle this situation, you need to pass
|
||||||
|
``handle_forward_references=True`` to ``serializers.deserialize()``. This will
|
||||||
|
set the ``deferred_fields`` attribute on the ``DeserializedObject`` instances.
|
||||||
|
You'll need to keep track of ``DeserializedObject`` instances where this
|
||||||
|
attribute isn't ``None`` and later call ``save_deferred_fields()`` on them.
|
||||||
|
|
||||||
|
Typical usage looks like this::
|
||||||
|
|
||||||
|
objs_with_deferred_fields = []
|
||||||
|
|
||||||
|
for obj in serializers.deserialize('xml', data, handle_forward_references=True):
|
||||||
|
obj.save()
|
||||||
|
if obj.deferred_fields is not None:
|
||||||
|
objs_with_deferred_fields.append(obj)
|
||||||
|
|
||||||
|
for obj in objs_with_deferred_fields:
|
||||||
|
obj.save_deferred_fields()
|
||||||
|
|
||||||
|
For this to work, the ``ForeignKey`` on the referencing model must have
|
||||||
|
``null=True``.
|
||||||
|
|
||||||
Dependencies during serialization
|
Dependencies during serialization
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
Since natural keys rely on database lookups to resolve references, it
|
It's often possible to avoid explicitly having to handle forward references by
|
||||||
is important that the data exists before it is referenced. You can't make
|
taking care with the ordering of objects within a fixture.
|
||||||
a "forward reference" with natural keys -- the data you're referencing
|
|
||||||
must exist before you include a natural key reference to that data.
|
|
||||||
|
|
||||||
To accommodate this limitation, calls to :djadmin:`dumpdata` that use
|
To help with this, calls to :djadmin:`dumpdata` that use the :option:`dumpdata
|
||||||
the :option:`dumpdata --natural-foreign` option will serialize any model with a
|
--natural-foreign` option will serialize any model with a ``natural_key()``
|
||||||
``natural_key()`` method before serializing standard primary key objects.
|
method before serializing standard primary key objects.
|
||||||
|
|
||||||
However, this may not always be enough. If your natural key refers to
|
However, this may not always be enough. If your natural key refers to
|
||||||
another object (by using a foreign key or natural key to another object
|
another object (by using a foreign key or natural key to another object
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "fixtures.naturalkeything",
|
||||||
|
"fields": {
|
||||||
|
"key": "t1",
|
||||||
|
"other_thing": [
|
||||||
|
"t2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "fixtures.naturalkeything",
|
||||||
|
"fields": {
|
||||||
|
"key": "t2",
|
||||||
|
"other_thing": [
|
||||||
|
"t1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,23 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "fixtures.naturalkeything",
|
||||||
|
"fields": {
|
||||||
|
"key": "t1",
|
||||||
|
"other_things": [
|
||||||
|
["t2"], ["t3"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "fixtures.naturalkeything",
|
||||||
|
"fields": {
|
||||||
|
"key": "t2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "fixtures.naturalkeything",
|
||||||
|
"fields": {
|
||||||
|
"key": "t3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -116,3 +116,21 @@ class Book(models.Model):
|
||||||
|
|
||||||
class PrimaryKeyUUIDModel(models.Model):
|
class PrimaryKeyUUIDModel(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||||
|
|
||||||
|
|
||||||
|
class NaturalKeyThing(models.Model):
|
||||||
|
key = models.CharField(max_length=100)
|
||||||
|
other_thing = models.ForeignKey('NaturalKeyThing', on_delete=models.CASCADE, null=True)
|
||||||
|
other_things = models.ManyToManyField('NaturalKeyThing', related_name='thing_m2m_set')
|
||||||
|
|
||||||
|
class Manager(models.Manager):
|
||||||
|
def get_by_natural_key(self, key):
|
||||||
|
return self.get(key=key)
|
||||||
|
|
||||||
|
objects = Manager()
|
||||||
|
|
||||||
|
def natural_key(self):
|
||||||
|
return (self.key,)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.key
|
||||||
|
|
|
@ -17,7 +17,8 @@ from django.db import IntegrityError, connection
|
||||||
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
|
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Article, Category, PrimaryKeyUUIDModel, ProxySpy, Spy, Tag, Visa,
|
Article, Category, NaturalKeyThing, PrimaryKeyUUIDModel, ProxySpy, Spy,
|
||||||
|
Tag, Visa,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -780,3 +781,21 @@ class FixtureTransactionTests(DumpDataAssertMixin, TransactionTestCase):
|
||||||
'<Article: Time to reform copyright>',
|
'<Article: Time to reform copyright>',
|
||||||
'<Article: Poker has no place on ESPN>',
|
'<Article: Poker has no place on ESPN>',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardReferenceTests(TestCase):
|
||||||
|
def test_forward_reference_fk(self):
|
||||||
|
management.call_command('loaddata', 'forward_reference_fk.json', verbosity=0)
|
||||||
|
self.assertEqual(NaturalKeyThing.objects.count(), 2)
|
||||||
|
t1, t2 = NaturalKeyThing.objects.all()
|
||||||
|
self.assertEqual(t1.other_thing, t2)
|
||||||
|
self.assertEqual(t2.other_thing, t1)
|
||||||
|
|
||||||
|
def test_forward_reference_m2m(self):
|
||||||
|
management.call_command('loaddata', 'forward_reference_m2m.json', verbosity=0)
|
||||||
|
self.assertEqual(NaturalKeyThing.objects.count(), 3)
|
||||||
|
t1 = NaturalKeyThing.objects.get_by_natural_key('t1')
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
t1.other_things.order_by('key'),
|
||||||
|
['<NaturalKeyThing: t2>', '<NaturalKeyThing: t3>']
|
||||||
|
)
|
||||||
|
|
|
@ -19,3 +19,21 @@ class NaturalKeyAnchor(models.Model):
|
||||||
|
|
||||||
class FKDataNaturalKey(models.Model):
|
class FKDataNaturalKey(models.Model):
|
||||||
data = models.ForeignKey(NaturalKeyAnchor, models.SET_NULL, null=True)
|
data = models.ForeignKey(NaturalKeyAnchor, models.SET_NULL, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class NaturalKeyThing(models.Model):
|
||||||
|
key = models.CharField(max_length=100)
|
||||||
|
other_thing = models.ForeignKey('NaturalKeyThing', on_delete=models.CASCADE, null=True)
|
||||||
|
other_things = models.ManyToManyField('NaturalKeyThing', related_name='thing_m2m_set')
|
||||||
|
|
||||||
|
class Manager(models.Manager):
|
||||||
|
def get_by_natural_key(self, key):
|
||||||
|
return self.get(key=key)
|
||||||
|
|
||||||
|
objects = Manager()
|
||||||
|
|
||||||
|
def natural_key(self):
|
||||||
|
return (self.key,)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.key
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.core import serializers
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import Child, FKDataNaturalKey, NaturalKeyAnchor
|
from .models import Child, FKDataNaturalKey, NaturalKeyAnchor, NaturalKeyThing
|
||||||
from .tests import register_tests
|
from .tests import register_tests
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,7 +93,100 @@ def natural_pk_mti_test(self, format):
|
||||||
self.assertEqual(child.child_data, child.parent_data)
|
self.assertEqual(child.child_data, child.parent_data)
|
||||||
|
|
||||||
|
|
||||||
|
def forward_ref_fk_test(self, format):
|
||||||
|
t1 = NaturalKeyThing.objects.create(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.create(key='t2', other_thing=t1)
|
||||||
|
t1.other_thing = t2
|
||||||
|
t1.save()
|
||||||
|
string_data = serializers.serialize(
|
||||||
|
format, [t1, t2], use_natural_primary_keys=True,
|
||||||
|
use_natural_foreign_keys=True,
|
||||||
|
)
|
||||||
|
NaturalKeyThing.objects.all().delete()
|
||||||
|
objs_with_deferred_fields = []
|
||||||
|
for obj in serializers.deserialize(format, string_data, handle_forward_references=True):
|
||||||
|
obj.save()
|
||||||
|
if obj.deferred_fields:
|
||||||
|
objs_with_deferred_fields.append(obj)
|
||||||
|
for obj in objs_with_deferred_fields:
|
||||||
|
obj.save_deferred_fields()
|
||||||
|
t1 = NaturalKeyThing.objects.get(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.get(key='t2')
|
||||||
|
self.assertEqual(t1.other_thing, t2)
|
||||||
|
self.assertEqual(t2.other_thing, t1)
|
||||||
|
|
||||||
|
|
||||||
|
def forward_ref_fk_with_error_test(self, format):
|
||||||
|
t1 = NaturalKeyThing.objects.create(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.create(key='t2', other_thing=t1)
|
||||||
|
t1.other_thing = t2
|
||||||
|
t1.save()
|
||||||
|
string_data = serializers.serialize(
|
||||||
|
format, [t1], use_natural_primary_keys=True,
|
||||||
|
use_natural_foreign_keys=True,
|
||||||
|
)
|
||||||
|
NaturalKeyThing.objects.all().delete()
|
||||||
|
objs_with_deferred_fields = []
|
||||||
|
for obj in serializers.deserialize(format, string_data, handle_forward_references=True):
|
||||||
|
obj.save()
|
||||||
|
if obj.deferred_fields:
|
||||||
|
objs_with_deferred_fields.append(obj)
|
||||||
|
obj = objs_with_deferred_fields[0]
|
||||||
|
msg = 'NaturalKeyThing matching query does not exist'
|
||||||
|
with self.assertRaisesMessage(serializers.base.DeserializationError, msg):
|
||||||
|
obj.save_deferred_fields()
|
||||||
|
|
||||||
|
|
||||||
|
def forward_ref_m2m_test(self, format):
|
||||||
|
t1 = NaturalKeyThing.objects.create(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.create(key='t2')
|
||||||
|
t3 = NaturalKeyThing.objects.create(key='t3')
|
||||||
|
t1.other_things.set([t2, t3])
|
||||||
|
string_data = serializers.serialize(
|
||||||
|
format, [t1, t2, t3], use_natural_primary_keys=True,
|
||||||
|
use_natural_foreign_keys=True,
|
||||||
|
)
|
||||||
|
NaturalKeyThing.objects.all().delete()
|
||||||
|
objs_with_deferred_fields = []
|
||||||
|
for obj in serializers.deserialize(format, string_data, handle_forward_references=True):
|
||||||
|
obj.save()
|
||||||
|
if obj.deferred_fields:
|
||||||
|
objs_with_deferred_fields.append(obj)
|
||||||
|
for obj in objs_with_deferred_fields:
|
||||||
|
obj.save_deferred_fields()
|
||||||
|
t1 = NaturalKeyThing.objects.get(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.get(key='t2')
|
||||||
|
t3 = NaturalKeyThing.objects.get(key='t3')
|
||||||
|
self.assertCountEqual(t1.other_things.all(), [t2, t3])
|
||||||
|
|
||||||
|
|
||||||
|
def forward_ref_m2m_with_error_test(self, format):
|
||||||
|
t1 = NaturalKeyThing.objects.create(key='t1')
|
||||||
|
t2 = NaturalKeyThing.objects.create(key='t2')
|
||||||
|
t3 = NaturalKeyThing.objects.create(key='t3')
|
||||||
|
t1.other_things.set([t2, t3])
|
||||||
|
t1.save()
|
||||||
|
string_data = serializers.serialize(
|
||||||
|
format, [t1, t2], use_natural_primary_keys=True,
|
||||||
|
use_natural_foreign_keys=True,
|
||||||
|
)
|
||||||
|
NaturalKeyThing.objects.all().delete()
|
||||||
|
objs_with_deferred_fields = []
|
||||||
|
for obj in serializers.deserialize(format, string_data, handle_forward_references=True):
|
||||||
|
obj.save()
|
||||||
|
if obj.deferred_fields:
|
||||||
|
objs_with_deferred_fields.append(obj)
|
||||||
|
obj = objs_with_deferred_fields[0]
|
||||||
|
msg = 'NaturalKeyThing matching query does not exist'
|
||||||
|
with self.assertRaisesMessage(serializers.base.DeserializationError, msg):
|
||||||
|
obj.save_deferred_fields()
|
||||||
|
|
||||||
|
|
||||||
# Dynamically register tests for each serializer
|
# Dynamically register tests for each serializer
|
||||||
register_tests(NaturalKeySerializerTests, 'test_%s_natural_key_serializer', natural_key_serializer_test)
|
register_tests(NaturalKeySerializerTests, 'test_%s_natural_key_serializer', natural_key_serializer_test)
|
||||||
register_tests(NaturalKeySerializerTests, 'test_%s_serializer_natural_keys', natural_key_test)
|
register_tests(NaturalKeySerializerTests, 'test_%s_serializer_natural_keys', natural_key_test)
|
||||||
register_tests(NaturalKeySerializerTests, 'test_%s_serializer_natural_pks_mti', natural_pk_mti_test)
|
register_tests(NaturalKeySerializerTests, 'test_%s_serializer_natural_pks_mti', natural_pk_mti_test)
|
||||||
|
register_tests(NaturalKeySerializerTests, 'test_%s_forward_references_fks', forward_ref_fk_test)
|
||||||
|
register_tests(NaturalKeySerializerTests, 'test_%s_forward_references_fk_errors', forward_ref_fk_with_error_test)
|
||||||
|
register_tests(NaturalKeySerializerTests, 'test_%s_forward_references_m2ms', forward_ref_m2m_test)
|
||||||
|
register_tests(NaturalKeySerializerTests, 'test_%s_forward_references_m2m_errors', forward_ref_m2m_with_error_test)
|
||||||
|
|
Loading…
Reference in New Issue