Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #7350, #7202 -- Fixed serialization for multi-model inheritance…

…, which had multiple problems:

 * Serializers were including all superclass fields in their output. Now only local fields are included.
 * Implicit OneToOne primary keys were not correctly added to the metamodel, so they were always marked to be serialized, even though they were primary
 * Model saving was too aggressive about creating new parent class instances during deserialization. Raw save on a model now skips saving of the parent class.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@7600 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 12716794dbcf2e1bf7a07bd18dfc0ae24be4e6f8 1 parent 1426c24
Russell Keith-Magee authored
2  django/core/serializers/base.py
@@ -38,7 +38,7 @@ def serialize(self, queryset, **options):
38 38
         self.start_serialization()
39 39
         for obj in queryset:
40 40
             self.start_object(obj)
41  
-            for field in obj._meta.fields:
  41
+            for field in obj._meta.local_fields:
42 42
                 if field.serialize:
43 43
                     if field.rel is None:
44 44
                         if self.selected_fields is None or field.attname in self.selected_fields:
13  django/db/models/base.py
@@ -290,12 +290,17 @@ def save_base(self, raw=False, cls=None):
290 290
             meta = cls._meta
291 291
             signal = False
292 292
 
293  
-        for parent, field in meta.parents.items():
294  
-            self.save_base(raw, parent)
295  
-            setattr(self, field.attname, self._get_pk_val(parent._meta))
  293
+        # If we are in a raw save, save the object exactly as presented.
  294
+        # That means that we don't try to be smart about saving attributes
  295
+        # that might have come from the parent class - we just save the 
  296
+        # attributes we have been given to the class we have been given.
  297
+        if not raw:
  298
+            for parent, field in meta.parents.items():
  299
+                self.save_base(raw, parent)
  300
+                setattr(self, field.attname, self._get_pk_val(parent._meta))
296 301
 
297 302
         non_pks = [f for f in meta.local_fields if not f.primary_key]
298  
-
  303
+            
299 304
         # First, try an UPDATE. If that doesn't update anything, do an INSERT.
300 305
         pk_val = self._get_pk_val(meta)
301 306
         # Note: the comparison with '' is required for compatibility with
2  django/db/models/options.py
@@ -102,7 +102,7 @@ def _prepare(self, model):
102 102
                 # field.
103 103
                 field = self.parents.value_for_index(0)
104 104
                 field.primary_key = True
105  
-                self.pk = field
  105
+                self.setup_pk(field)
106 106
             else:
107 107
                 auto = AutoField(verbose_name='ID', primary_key=True,
108 108
                         auto_created=True)
35  docs/serialization.txt
@@ -63,6 +63,41 @@ be serialized.
63 63
     doesn't specify all the fields that are required by a model, the deserializer
64 64
     will not be able to save deserialized instances.
65 65
 
  66
+Inherited Models
  67
+~~~~~~~~~~~~~~~~
  68
+
  69
+If you have a model that is defined using an `abstract base class`_, you don't
  70
+have to do anything special to serialize that model. Just call the serializer
  71
+on the object (or objects) that you want to serialize, and the output will be
  72
+a complete representation of the serialized object.
  73
+
  74
+However, if you have a model that uses `multi-table inheritance`_, you also
  75
+need to serialize all of the base classes for the model. This is because only
  76
+the fields that are locally defined on the model will be serialized. For
  77
+example, consider the following models::
  78
+	
  79
+	class Place(models.Model):
  80
+		name = models.CharField(max_length=50)
  81
+		
  82
+	class Restaurant(Place):
  83
+		serves_hot_dogs = models.BooleanField()
  84
+		
  85
+If you only serialize the Restaurant model::
  86
+
  87
+	data = serializers.serialize('xml', Restaurant.objects.all())
  88
+
  89
+the fields on the serialized output will only contain the `serves_hot_dogs`
  90
+attribute. The `name` attribute of the base class will be ignored.
  91
+
  92
+In order to fully serialize your Restaurant instances, you will need to
  93
+serialize the Place models as well::
  94
+
  95
+	all_objects = list(Restaurant.objects.all()) + list(Place.objects.all())
  96
+	data = serializers.serialize('xml', all_objects)
  97
+
  98
+.. _abstract base class: http://www.djangoproject.com/documentation/model-api/#abstract-base-classes
  99
+.. _multi-table inheritance: http://www.djangoproject.com/documentation/model-api/#multi-table-inheritance
  100
+
66 101
 Deserializing data
67 102
 ------------------
68 103
 
5  tests/modeltests/model_inheritance/models.py
@@ -147,8 +147,13 @@ def __unicode__(self):
147 147
 >>> c.save()
148 148
 >>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c)
149 149
 >>> ir.save()
  150
+>>> ItalianRestaurant.objects.filter(address='1234 W. Ash')
  151
+[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
  152
+
150 153
 >>> ir.address = '1234 W. Elm'
151 154
 >>> ir.save()
  155
+>>> ItalianRestaurant.objects.filter(address='1234 W. Elm')
  156
+[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
152 157
 
153 158
 # Make sure Restaurant and ItalianRestaurant have the right fields in the right
154 159
 # order.
0  tests/regressiontests/model_inheritance_regress/__init__.py
No changes.
120  tests/regressiontests/model_inheritance_regress/models.py
... ...
@@ -0,0 +1,120 @@
  1
+"""
  2
+Regression tests for Model inheritance behaviour.
  3
+"""
  4
+
  5
+from django.db import models
  6
+
  7
+class Place(models.Model):
  8
+    name = models.CharField(max_length=50)
  9
+    address = models.CharField(max_length=80)
  10
+
  11
+    class Meta:
  12
+        ordering = ('name',)
  13
+        
  14
+    def __unicode__(self):
  15
+        return u"%s the place" % self.name
  16
+
  17
+class Restaurant(Place):
  18
+    serves_hot_dogs = models.BooleanField()
  19
+    serves_pizza = models.BooleanField()
  20
+
  21
+    def __unicode__(self):
  22
+        return u"%s the restaurant" % self.name
  23
+
  24
+class ItalianRestaurant(Restaurant):
  25
+    serves_gnocchi = models.BooleanField()
  26
+
  27
+    def __unicode__(self):
  28
+        return u"%s the italian restaurant" % self.name
  29
+
  30
+class ParkingLot(Place):
  31
+    # An explicit link to the parent (we can control the attribute name).
  32
+    parent = models.OneToOneField(Place, primary_key=True, parent_link=True)
  33
+    capacity = models.IntegerField()
  34
+
  35
+    def __unicode__(self):
  36
+        return u"%s the parking lot" % self.name
  37
+
  38
+__test__ = {'API_TESTS':"""
  39
+# Regression for #7350, #7202
  40
+# Check that when you create a Parent object with a specific reference to an existent
  41
+# child instance, saving the Parent doesn't duplicate the child. 
  42
+# This behaviour is only activated during a raw save - it is mostly relevant to 
  43
+# deserialization, but any sort of CORBA style 'narrow()' API would require a
  44
+# similar approach.
  45
+
  46
+# Create a child-parent-grandparent chain
  47
+>>> place1 = Place(name="Guido's House of Pasta", address='944 W. Fullerton')
  48
+>>> place1.save_base(raw=True)
  49
+>>> restaurant = Restaurant(place_ptr=place1, serves_hot_dogs=True, serves_pizza=False)
  50
+>>> restaurant.save_base(raw=True)
  51
+>>> italian_restaurant = ItalianRestaurant(restaurant_ptr=restaurant, serves_gnocchi=True)
  52
+>>> italian_restaurant.save_base(raw=True)
  53
+
  54
+# Create a child-parent chain with an explicit parent link
  55
+>>> place2 = Place(name='Main St', address='111 Main St')
  56
+>>> place2.save_base(raw=True)
  57
+>>> park = ParkingLot(parent=place2, capacity=100)
  58
+>>> park.save_base(raw=True)
  59
+
  60
+# Check that no extra parent objects have been created.
  61
+>>> Place.objects.all()
  62
+[<Place: Guido's House of Pasta the place>, <Place: Main St the place>]
  63
+
  64
+>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
  65
+>>> [sorted(d.items()) for d in dicts]
  66
+[[('name', u"Guido's House of Pasta"), ('serves_hot_dogs', True)]]
  67
+
  68
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
  69
+>>> [sorted(d.items()) for d in dicts]
  70
+[[('name', u"Guido's House of Pasta"), ('serves_gnocchi', True), ('serves_hot_dogs', True)]]
  71
+
  72
+>>> dicts = ParkingLot.objects.values('name','capacity')
  73
+>>> [sorted(d.items()) for d in dicts]
  74
+[[('capacity', 100), ('name', u'Main St')]]
  75
+
  76
+# You can also update objects when using a raw save.
  77
+>>> place1.name = "Guido's All New House of Pasta"
  78
+>>> place1.save_base(raw=True)
  79
+
  80
+>>> restaurant.serves_hot_dogs = False
  81
+>>> restaurant.save_base(raw=True)
  82
+
  83
+>>> italian_restaurant.serves_gnocchi = False
  84
+>>> italian_restaurant.save_base(raw=True)
  85
+
  86
+>>> place2.name='Derelict lot'
  87
+>>> place2.save_base(raw=True)
  88
+
  89
+>>> park.capacity = 50
  90
+>>> park.save_base(raw=True)
  91
+
  92
+# No extra parent objects after an update, either.
  93
+>>> Place.objects.all()
  94
+[<Place: Derelict lot the place>, <Place: Guido's All New House of Pasta the place>]
  95
+
  96
+>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
  97
+>>> [sorted(d.items()) for d in dicts]
  98
+[[('name', u"Guido's All New House of Pasta"), ('serves_hot_dogs', False)]]
  99
+
  100
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
  101
+>>> [sorted(d.items()) for d in dicts]
  102
+[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
  103
+
  104
+>>> dicts = ParkingLot.objects.values('name','capacity')
  105
+>>> [sorted(d.items()) for d in dicts]
  106
+[[('capacity', 50), ('name', u'Derelict lot')]]
  107
+
  108
+# If you try to raw_save a parent attribute onto a child object,
  109
+# the attribute will be ignored.
  110
+
  111
+>>> italian_restaurant.name = "Lorenzo's Pasta Hut"
  112
+>>> italian_restaurant.save_base(raw=True)
  113
+
  114
+# Note that the name has not changed
  115
+# - name is an attribute of Place, not ItalianRestaurant
  116
+>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
  117
+>>> [sorted(d.items()) for d in dicts]
  118
+[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
  119
+
  120
+"""}
20  tests/regressiontests/serializers_regress/models.py
@@ -223,3 +223,23 @@ def save(self):
223 223
         "A save method that modifies the data in the object"
224 224
         self.data = 666
225 225
         super(ModifyingSaveData, self).save(raw)
  226
+
  227
+# Tests for serialization of models using inheritance.
  228
+# Regression for #7202, #7350
  229
+class AbstractBaseModel(models.Model):
  230
+    parent_data = models.IntegerField()
  231
+    class Meta:
  232
+        abstract = True
  233
+
  234
+class InheritAbstractModel(AbstractBaseModel):
  235
+    child_data = models.IntegerField()
  236
+    
  237
+class BaseModel(models.Model):
  238
+    parent_data = models.IntegerField()
  239
+
  240
+class InheritBaseModel(BaseModel):
  241
+    child_data = models.IntegerField()
  242
+
  243
+class ExplicitInheritBaseModel(BaseModel):
  244
+    parent = models.OneToOneField(BaseModel)
  245
+    child_data = models.IntegerField()
50  tests/regressiontests/serializers_regress/tests.py
@@ -32,7 +32,7 @@ def data_create(pk, klass, data):
32 32
     instance = klass(id=pk)
33 33
     instance.data = data
34 34
     models.Model.save_base(instance, raw=True)
35  
-    return instance
  35
+    return [instance]
36 36
 
37 37
 def generic_create(pk, klass, data):
38 38
     instance = klass(id=pk)
@@ -40,32 +40,45 @@ def generic_create(pk, klass, data):
40 40
     models.Model.save_base(instance, raw=True)
41 41
     for tag in data[1:]:
42 42
         instance.tags.create(data=tag)
43  
-    return instance
  43
+    return [instance]
44 44
 
45 45
 def fk_create(pk, klass, data):
46 46
     instance = klass(id=pk)
47 47
     setattr(instance, 'data_id', data)
48 48
     models.Model.save_base(instance, raw=True)
49  
-    return instance
  49
+    return [instance]
50 50
 
51 51
 def m2m_create(pk, klass, data):
52 52
     instance = klass(id=pk)
53 53
     models.Model.save_base(instance, raw=True)
54 54
     instance.data = data
55  
-    return instance
  55
+    return [instance]
56 56
 
57 57
 def o2o_create(pk, klass, data):
58 58
     instance = klass()
59 59
     instance.data_id = data
60 60
     models.Model.save_base(instance, raw=True)
61  
-    return instance
  61
+    return [instance]
62 62
 
63 63
 def pk_create(pk, klass, data):
64 64
     instance = klass()
65 65
     instance.data = data
66 66
     models.Model.save_base(instance, raw=True)
67  
-    return instance
68  
-
  67
+    return [instance]
  68
+
  69
+def inherited_create(pk, klass, data):
  70
+    instance = klass(id=pk,**data)
  71
+    # This isn't a raw save because:
  72
+    #  1) we're testing inheritance, not field behaviour, so none
  73
+    #     of the field values need to be protected.
  74
+    #  2) saving the child class and having the parent created
  75
+    #     automatically is easier than manually creating both. 
  76
+    models.Model.save(instance)
  77
+    created = [instance]
  78
+    for klass,field in instance._meta.parents.items():
  79
+        created.append(klass.objects.get(id=pk))
  80
+    return created
  81
+    
69 82
 # A set of functions that can be used to compare
70 83
 # test data objects of various kinds
71 84
 def data_compare(testcase, pk, klass, data):
@@ -94,6 +107,11 @@ def pk_compare(testcase, pk, klass, data):
94 107
     instance = klass.objects.get(data=data)
95 108
     testcase.assertEqual(data, instance.data)
96 109
 
  110
+def inherited_compare(testcase, pk, klass, data):
  111
+    instance = klass.objects.get(id=pk)
  112
+    for key,value in data.items():
  113
+        testcase.assertEqual(value, getattr(instance,key))
  114
+    
97 115
 # Define some data types. Each data type is
98 116
 # actually a pair of functions; one to create
99 117
 # and one to compare objects of that type
@@ -103,6 +121,7 @@ def pk_compare(testcase, pk, klass, data):
103 121
 m2m_obj = (m2m_create, m2m_compare)
104 122
 o2o_obj = (o2o_create, o2o_compare)
105 123
 pk_obj = (pk_create, pk_compare)
  124
+inherited_obj = (inherited_create, inherited_compare)
106 125
 
107 126
 test_data = [
108 127
     # Format: (data type, PK value, Model Class, data)
@@ -255,6 +274,10 @@ def pk_compare(testcase, pk, klass, data):
255 274
 
256 275
     (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)),
257 276
     (data_obj, 810, ModifyingSaveData, 42),
  277
+    
  278
+    (inherited_obj, 900, InheritAbstractModel, {'child_data':37,'parent_data':42}),
  279
+    (inherited_obj, 910, ExplicitInheritBaseModel, {'child_data':37,'parent_data':42}),
  280
+    (inherited_obj, 920, InheritBaseModel, {'child_data':37,'parent_data':42}),
258 281
 ]
259 282
 
260 283
 # Because Oracle treats the empty string as NULL, Oracle is expected to fail
@@ -277,13 +300,19 @@ def serializerTest(format, self):
277 300
 
278 301
     # Create all the objects defined in the test data
279 302
     objects = []
  303
+    instance_count = {}
280 304
     transaction.enter_transaction_management()
281 305
     transaction.managed(True)
282 306
     for (func, pk, klass, datum) in test_data:
283  
-        objects.append(func[0](pk, klass, datum))
  307
+        objects.extend(func[0](pk, klass, datum))
  308
+        instance_count[klass] = 0
284 309
     transaction.commit()
285 310
     transaction.leave_transaction_management()
286 311
 
  312
+    # Get a count of the number of objects created for each class
  313
+    for klass in instance_count:
  314
+        instance_count[klass] = klass.objects.count()
  315
+        
287 316
     # Add the generic tagged objects to the object list
288 317
     objects.extend(Tag.objects.all())
289 318
 
@@ -304,6 +333,11 @@ def serializerTest(format, self):
304 333
     for (func, pk, klass, datum) in test_data:
305 334
         func[1](self, pk, klass, datum)
306 335
 
  336
+    # Assert that the number of objects deserialized is the
  337
+    # same as the number that was serialized.
  338
+    for klass, count in instance_count.items():
  339
+        self.assertEquals(count, klass.objects.count())
  340
+
307 341
 def fieldsTest(format, self):
308 342
     # Clear the database first
309 343
     management.call_command('flush', verbosity=0, interactive=False)

0 notes on commit 1271679

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