Fixes #13252 -- Use the natural key instead of the primary key when serializing

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14994 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Chris Beaven 2010-12-21 04:40:08 +00:00
parent c2657d8923
commit b60d5df072
6 changed files with 109 additions and 25 deletions

View File

@ -170,3 +170,21 @@ class DeserializedObject(object):
# prevent a second (possibly accidental) call to save() from saving # prevent a second (possibly accidental) call to save() from saving
# the m2m data twice. # the m2m data twice.
self.m2m_data = None self.m2m_data = None
def build_instance(Model, data, db):
"""
Build a model instance.
If the model instance doesn't have a primary key and the model supports
natural keys, try to retrieve it from the database.
"""
obj = Model(**data)
if obj.pk is None and hasattr(Model, 'natural_key') and\
hasattr(Model._default_manager, 'get_by_natural_key'):
pk = obj.natural_key()
try:
obj.pk = Model._default_manager.db_manager(db)\
.get_by_natural_key(*pk).pk
except Model.DoesNotExist:
pass
return obj

View File

@ -27,11 +27,13 @@ class Serializer(base.Serializer):
self._current = {} self._current = {}
def end_object(self, obj): def end_object(self, obj):
self.objects.append({ data = {
"model" : smart_unicode(obj._meta), "model": smart_unicode(obj._meta),
"pk" : smart_unicode(obj._get_pk_val(), strings_only=True), "fields": self._current
"fields" : self._current }
}) if not self.use_natural_keys or not hasattr(obj, 'natural_key'):
data['pk'] = smart_unicode(obj._get_pk_val(), strings_only=True)
self.objects.append(data)
self._current = None self._current = None
def handle_field(self, obj, field): def handle_field(self, obj, field):
@ -82,7 +84,9 @@ def Deserializer(object_list, **options):
for d in object_list: for d in object_list:
# Look up the model and starting build a dict of data for it. # Look up the model and starting build a dict of data for it.
Model = _get_model(d["model"]) Model = _get_model(d["model"])
data = {Model._meta.pk.attname : Model._meta.pk.to_python(d["pk"])} data = {}
if 'pk' in d:
data[Model._meta.pk.attname] = Model._meta.pk.to_python(d['pk'])
m2m_data = {} m2m_data = {}
# Handle each field # Handle each field
@ -127,7 +131,9 @@ def Deserializer(object_list, **options):
else: else:
data[field.name] = field.to_python(field_value) data[field.name] = field.to_python(field_value)
yield base.DeserializedObject(Model(**data), m2m_data) obj = base.build_instance(Model, data, db)
yield base.DeserializedObject(obj, m2m_data)
def _get_model(model_identifier): def _get_model(model_identifier):
""" """

View File

@ -42,16 +42,12 @@ class Serializer(base.Serializer):
raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
self.indent(1) self.indent(1)
obj_pk = obj._get_pk_val() object_data = {"model": smart_unicode(obj._meta)}
if obj_pk is None: if not self.use_natural_keys or not hasattr(obj, 'natural_key'):
attrs = {"model": smart_unicode(obj._meta),} obj_pk = obj._get_pk_val()
else: if obj_pk is not None:
attrs = { object_data['pk'] = smart_unicode(obj_pk)
"pk": smart_unicode(obj._get_pk_val()), self.xml.startElement("object", object_data)
"model": smart_unicode(obj._meta),
}
self.xml.startElement("object", attrs)
def end_object(self, obj): def end_object(self, obj):
""" """
@ -173,13 +169,10 @@ class Deserializer(base.Deserializer):
Model = self._get_model_from_node(node, "model") Model = self._get_model_from_node(node, "model")
# Start building a data dictionary from the object. # Start building a data dictionary from the object.
# If the node is missing the pk set it to None data = {}
if node.hasAttribute("pk"): if node.hasAttribute('pk'):
pk = node.getAttribute("pk") data[Model._meta.pk.attname] = Model._meta.pk.to_python(
else: node.getAttribute('pk'))
pk = None
data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)}
# 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]})
@ -210,8 +203,10 @@ class Deserializer(base.Deserializer):
value = field.to_python(getInnerText(field_node).strip()) value = field.to_python(getInnerText(field_node).strip())
data[field.name] = value data[field.name] = value
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(Model(**data), m2m_data) return base.DeserializedObject(obj, m2m_data)
def _handle_fk_field_node(self, node, field): def _handle_fk_field_node(self, node, field):
""" """

View File

@ -307,6 +307,12 @@ into the primary key of an actual ``Person`` object.
fields will be effectively unique, you can still use those fields fields will be effectively unique, you can still use those fields
as a natural key. as a natural key.
.. versionchanged:: 1.3
Deserialization of objects with no primary key will always check whether the
model's manager has a ``get_by_natural_key()`` method and if so, use it to
populate the deserialized object's primary key.
Serialization of natural keys Serialization of natural keys
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -353,6 +359,23 @@ use the `--natural` command line flag 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.
.. versionchanged:: 1.3
When ``use_natural_keys=True`` is specified, the primary key is no longer
provided in the serialized data of this object since it can be calculated
during deserialization::
...
{
"model": "store.person",
"fields": {
"first_name": "Douglas",
"last_name": "Adams",
"birth_date": "1952-03-11",
}
}
...
Dependencies during serialization Dependencies during serialization
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -264,3 +264,17 @@ class LengthModel(models.Model):
def __len__(self): def __len__(self):
return self.data return self.data
#Tests for natural keys.
class BookManager(models.Manager):
def get_by_natural_key(self, isbn13):
return self.get(isbn13=isbn13)
class Book(models.Model):
isbn13 = models.CharField(max_length=14)
title = models.CharField(max_length=100)
objects = BookManager()
def natural_key(self):
return (self.isbn13,)

View File

@ -414,8 +414,36 @@ def streamTest(format, self):
self.assertEqual(string_data, stream.getvalue()) self.assertEqual(string_data, stream.getvalue())
stream.close() stream.close()
def naturalKeyTest(format, self):
book1 = {'isbn13': '978-1590597255', 'title': 'The Definitive Guide to '
'Django: Web Development Done Right'}
book2 = {'isbn13':'978-1590599969', 'title': 'Practical Django Projects'}
# Create the books.
adrian = Book.objects.create(**book1)
james = Book.objects.create(**book2)
# Serialize the books.
string_data = serializers.serialize(format, Book.objects.all(), indent=2,
use_natural_keys=True)
# Delete one book (to prove that the natural key generation will only
# restore the primary keys of books found in the database via the
# get_natural_key manager method).
james.delete()
# Deserialize and test.
books = list(serializers.deserialize(format, string_data))
self.assertEqual(len(books), 2)
self.assertEqual(books[0].object.title, book1['title'])
self.assertEqual(books[0].object.pk, adrian.pk)
self.assertEqual(books[1].object.title, book2['title'])
self.assertEqual(books[1].object.pk, None)
for format in serializers.get_serializer_formats(): for format in serializers.get_serializer_formats():
setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format))
setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))
setattr(SerializerTests, 'test_' + format + '_serializer_natural_key',
curry(naturalKeyTest, format))
if format != 'python': if format != 'python':
setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))