Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

…erializing

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14994 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b60d5df07266dd4cd4f63afa6986ffb2932facc2 1 parent c2657d8
Chris Beaven authored December 21, 2010
18  django/core/serializers/base.py
@@ -170,3 +170,21 @@ def save(self, save_m2m=True, using=None):
170 170
         # prevent a second (possibly accidental) call to save() from saving
171 171
         # the m2m data twice.
172 172
         self.m2m_data = None
  173
+
  174
+def build_instance(Model, data, db):
  175
+    """
  176
+    Build a model instance.
  177
+
  178
+    If the model instance doesn't have a primary key and the model supports
  179
+    natural keys, try to retrieve it from the database.
  180
+    """
  181
+    obj = Model(**data)
  182
+    if obj.pk is None and hasattr(Model, 'natural_key') and\
  183
+            hasattr(Model._default_manager, 'get_by_natural_key'):
  184
+        pk = obj.natural_key()
  185
+        try:
  186
+            obj.pk = Model._default_manager.db_manager(db)\
  187
+                                           .get_by_natural_key(*pk).pk
  188
+        except Model.DoesNotExist:
  189
+            pass
  190
+    return obj
20  django/core/serializers/python.py
@@ -27,11 +27,13 @@ def start_object(self, obj):
27 27
         self._current = {}
28 28
 
29 29
     def end_object(self, obj):
30  
-        self.objects.append({
31  
-            "model"  : smart_unicode(obj._meta),
32  
-            "pk"     : smart_unicode(obj._get_pk_val(), strings_only=True),
33  
-            "fields" : self._current
34  
-        })
  30
+        data = {
  31
+            "model": smart_unicode(obj._meta),
  32
+            "fields": self._current
  33
+        }
  34
+        if not self.use_natural_keys or not hasattr(obj, 'natural_key'):
  35
+            data['pk'] = smart_unicode(obj._get_pk_val(), strings_only=True)
  36
+        self.objects.append(data)
35 37
         self._current = None
36 38
 
37 39
     def handle_field(self, obj, field):
@@ -82,7 +84,9 @@ def Deserializer(object_list, **options):
82 84
     for d in object_list:
83 85
         # Look up the model and starting build a dict of data for it.
84 86
         Model = _get_model(d["model"])
85  
-        data = {Model._meta.pk.attname : Model._meta.pk.to_python(d["pk"])}
  87
+        data = {}
  88
+        if 'pk' in d:
  89
+            data[Model._meta.pk.attname] = Model._meta.pk.to_python(d['pk'])
86 90
         m2m_data = {}
87 91
 
88 92
         # Handle each field
@@ -127,7 +131,9 @@ def m2m_convert(value):
127 131
             else:
128 132
                 data[field.name] = field.to_python(field_value)
129 133
 
130  
-        yield base.DeserializedObject(Model(**data), m2m_data)
  134
+        obj = base.build_instance(Model, data, db)
  135
+
  136
+        yield base.DeserializedObject(obj, m2m_data)
131 137
 
132 138
 def _get_model(model_identifier):
133 139
     """
31  django/core/serializers/xml_serializer.py
@@ -42,16 +42,12 @@ def start_object(self, obj):
42 42
             raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
43 43
 
44 44
         self.indent(1)
45  
-        obj_pk = obj._get_pk_val()
46  
-        if obj_pk is None:
47  
-            attrs = {"model": smart_unicode(obj._meta),}
48  
-        else:
49  
-            attrs = {
50  
-                "pk": smart_unicode(obj._get_pk_val()),
51  
-                "model": smart_unicode(obj._meta),
52  
-            }
53  
-
54  
-        self.xml.startElement("object", attrs)
  45
+        object_data = {"model": smart_unicode(obj._meta)}
  46
+        if not self.use_natural_keys or not hasattr(obj, 'natural_key'):
  47
+            obj_pk = obj._get_pk_val()
  48
+            if obj_pk is not None:
  49
+                object_data['pk'] = smart_unicode(obj_pk)
  50
+        self.xml.startElement("object", object_data)
55 51
 
56 52
     def end_object(self, obj):
57 53
         """
@@ -173,13 +169,10 @@ def _handle_object(self, node):
173 169
         Model = self._get_model_from_node(node, "model")
174 170
 
175 171
         # Start building a data dictionary from the object.
176  
-        # If the node is missing the pk set it to None
177  
-        if node.hasAttribute("pk"):
178  
-            pk = node.getAttribute("pk")
179  
-        else:
180  
-            pk = None
181  
-
182  
-        data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)}
  172
+        data = {}
  173
+        if node.hasAttribute('pk'):
  174
+            data[Model._meta.pk.attname] = Model._meta.pk.to_python(
  175
+                                                    node.getAttribute('pk'))
183 176
 
184 177
         # Also start building a dict of m2m data (this is saved as
185 178
         # {m2m_accessor_attribute : [list_of_related_objects]})
@@ -210,8 +203,10 @@ def _handle_object(self, node):
210 203
                     value = field.to_python(getInnerText(field_node).strip())
211 204
                 data[field.name] = value
212 205
 
  206
+        obj = base.build_instance(Model, data, self.db)
  207
+
213 208
         # Return a DeserializedObject so that the m2m data has a place to live.
214  
-        return base.DeserializedObject(Model(**data), m2m_data)
  209
+        return base.DeserializedObject(obj, m2m_data)
215 210
 
216 211
     def _handle_fk_field_node(self, node, field):
217 212
         """
23  docs/topics/serialization.txt
@@ -307,6 +307,12 @@ into the primary key of an actual ``Person`` object.
307 307
     fields will be effectively unique, you can still use those fields
308 308
     as a natural key.
309 309
 
  310
+.. versionchanged:: 1.3
  311
+
  312
+Deserialization of objects with no primary key will always check whether the
  313
+model's manager has a ``get_by_natural_key()`` method and if so, use it to
  314
+populate the deserialized object's primary key.
  315
+
310 316
 Serialization of natural keys
311 317
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
312 318
 
@@ -353,6 +359,23 @@ use the `--natural` command line flag to generate natural keys.
353 359
     natural keys during serialization, but *not* be able to load those
354 360
     key values, just don't define the ``get_by_natural_key()`` method.
355 361
 
  362
+.. versionchanged:: 1.3
  363
+
  364
+When ``use_natural_keys=True`` is specified, the primary key is no longer
  365
+provided in the serialized data of this object since it can be calculated
  366
+during deserialization::
  367
+
  368
+    ...
  369
+    {
  370
+        "model": "store.person",
  371
+        "fields": {
  372
+            "first_name": "Douglas",
  373
+            "last_name": "Adams",
  374
+            "birth_date": "1952-03-11",
  375
+        }
  376
+    }
  377
+    ...
  378
+
356 379
 Dependencies during serialization
357 380
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
358 381
 
14  tests/regressiontests/serializers_regress/models.py
@@ -264,3 +264,17 @@ class LengthModel(models.Model):
264 264
 
265 265
     def __len__(self):
266 266
         return self.data
  267
+
  268
+#Tests for natural keys.
  269
+class BookManager(models.Manager):
  270
+    def get_by_natural_key(self, isbn13):
  271
+        return self.get(isbn13=isbn13)
  272
+
  273
+class Book(models.Model):
  274
+    isbn13 = models.CharField(max_length=14)
  275
+    title = models.CharField(max_length=100)
  276
+
  277
+    objects = BookManager()
  278
+
  279
+    def natural_key(self):
  280
+        return (self.isbn13,)
28  tests/regressiontests/serializers_regress/tests.py
@@ -414,8 +414,36 @@ def streamTest(format, self):
414 414
     self.assertEqual(string_data, stream.getvalue())
415 415
     stream.close()
416 416
 
  417
+def naturalKeyTest(format, self):
  418
+    book1 = {'isbn13': '978-1590597255', 'title': 'The Definitive Guide to '
  419
+             'Django: Web Development Done Right'}
  420
+    book2 = {'isbn13':'978-1590599969', 'title': 'Practical Django Projects'}
  421
+
  422
+    # Create the books.
  423
+    adrian = Book.objects.create(**book1)
  424
+    james = Book.objects.create(**book2)
  425
+
  426
+    # Serialize the books.
  427
+    string_data = serializers.serialize(format, Book.objects.all(), indent=2,
  428
+                                        use_natural_keys=True)
  429
+
  430
+    # Delete one book (to prove that the natural key generation will only
  431
+    # restore the primary keys of books found in the database via the
  432
+    # get_natural_key manager method).
  433
+    james.delete()
  434
+
  435
+    # Deserialize and test.
  436
+    books = list(serializers.deserialize(format, string_data))
  437
+    self.assertEqual(len(books), 2)
  438
+    self.assertEqual(books[0].object.title, book1['title'])
  439
+    self.assertEqual(books[0].object.pk, adrian.pk)
  440
+    self.assertEqual(books[1].object.title, book2['title'])
  441
+    self.assertEqual(books[1].object.pk, None)
  442
+
417 443
 for format in serializers.get_serializer_formats():
418 444
     setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format))
419 445
     setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))
  446
+    setattr(SerializerTests, 'test_' + format + '_serializer_natural_key',
  447
+            curry(naturalKeyTest, format))
420 448
     if format != 'python':
421 449
         setattr(SerializerTests, 'test_' + format + '_serializer_stream', curry(streamTest, format))

0 notes on commit b60d5df

Please sign in to comment.
Something went wrong with that request. Please try again.