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
This commit is contained in:
Russell Keith-Magee 2008-06-09 14:03:35 +00:00
parent 1426c24517
commit 12716794db
9 changed files with 232 additions and 13 deletions

View File

@ -38,7 +38,7 @@ class Serializer(object):
self.start_serialization() self.start_serialization()
for obj in queryset: for obj in queryset:
self.start_object(obj) self.start_object(obj)
for field in obj._meta.fields: for field in obj._meta.local_fields:
if field.serialize: if field.serialize:
if field.rel is None: if field.rel is None:
if self.selected_fields is None or field.attname in self.selected_fields: if self.selected_fields is None or field.attname in self.selected_fields:

View File

@ -290,12 +290,17 @@ class Model(object):
meta = cls._meta meta = cls._meta
signal = False signal = False
for parent, field in meta.parents.items(): # If we are in a raw save, save the object exactly as presented.
self.save_base(raw, parent) # That means that we don't try to be smart about saving attributes
setattr(self, field.attname, self._get_pk_val(parent._meta)) # that might have come from the parent class - we just save the
# attributes we have been given to the class we have been given.
if not raw:
for parent, field in meta.parents.items():
self.save_base(raw, parent)
setattr(self, field.attname, self._get_pk_val(parent._meta))
non_pks = [f for f in meta.local_fields if not f.primary_key] non_pks = [f for f in meta.local_fields if not f.primary_key]
# First, try an UPDATE. If that doesn't update anything, do an INSERT. # First, try an UPDATE. If that doesn't update anything, do an INSERT.
pk_val = self._get_pk_val(meta) pk_val = self._get_pk_val(meta)
# Note: the comparison with '' is required for compatibility with # Note: the comparison with '' is required for compatibility with

View File

@ -102,7 +102,7 @@ class Options(object):
# field. # field.
field = self.parents.value_for_index(0) field = self.parents.value_for_index(0)
field.primary_key = True field.primary_key = True
self.pk = field self.setup_pk(field)
else: else:
auto = AutoField(verbose_name='ID', primary_key=True, auto = AutoField(verbose_name='ID', primary_key=True,
auto_created=True) auto_created=True)

View File

@ -63,6 +63,41 @@ be serialized.
doesn't specify all the fields that are required by a model, the deserializer doesn't specify all the fields that are required by a model, the deserializer
will not be able to save deserialized instances. will not be able to save deserialized instances.
Inherited Models
~~~~~~~~~~~~~~~~
If you have a model that is defined using an `abstract base class`_, you don't
have to do anything special to serialize that model. Just call the serializer
on the object (or objects) that you want to serialize, and the output will be
a complete representation of the serialized object.
However, if you have a model that uses `multi-table inheritance`_, you also
need to serialize all of the base classes for the model. This is because only
the fields that are locally defined on the model will be serialized. For
example, consider the following models::
class Place(models.Model):
name = models.CharField(max_length=50)
class Restaurant(Place):
serves_hot_dogs = models.BooleanField()
If you only serialize the Restaurant model::
data = serializers.serialize('xml', Restaurant.objects.all())
the fields on the serialized output will only contain the `serves_hot_dogs`
attribute. The `name` attribute of the base class will be ignored.
In order to fully serialize your Restaurant instances, you will need to
serialize the Place models as well::
all_objects = list(Restaurant.objects.all()) + list(Place.objects.all())
data = serializers.serialize('xml', all_objects)
.. _abstract base class: http://www.djangoproject.com/documentation/model-api/#abstract-base-classes
.. _multi-table inheritance: http://www.djangoproject.com/documentation/model-api/#multi-table-inheritance
Deserializing data Deserializing data
------------------ ------------------

View File

@ -147,8 +147,13 @@ Test constructor for Restaurant.
>>> c.save() >>> c.save()
>>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c) >>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c)
>>> ir.save() >>> ir.save()
>>> ItalianRestaurant.objects.filter(address='1234 W. Ash')
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
>>> ir.address = '1234 W. Elm' >>> ir.address = '1234 W. Elm'
>>> ir.save() >>> ir.save()
>>> ItalianRestaurant.objects.filter(address='1234 W. Elm')
[<ItalianRestaurant: Ristorante Miron the italian restaurant>]
# Make sure Restaurant and ItalianRestaurant have the right fields in the right # Make sure Restaurant and ItalianRestaurant have the right fields in the right
# order. # order.

View File

@ -0,0 +1,120 @@
"""
Regression tests for Model inheritance behaviour.
"""
from django.db import models
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
class Meta:
ordering = ('name',)
def __unicode__(self):
return u"%s the place" % self.name
class Restaurant(Place):
serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField()
def __unicode__(self):
return u"%s the restaurant" % self.name
class ItalianRestaurant(Restaurant):
serves_gnocchi = models.BooleanField()
def __unicode__(self):
return u"%s the italian restaurant" % self.name
class ParkingLot(Place):
# An explicit link to the parent (we can control the attribute name).
parent = models.OneToOneField(Place, primary_key=True, parent_link=True)
capacity = models.IntegerField()
def __unicode__(self):
return u"%s the parking lot" % self.name
__test__ = {'API_TESTS':"""
# Regression for #7350, #7202
# Check that when you create a Parent object with a specific reference to an existent
# child instance, saving the Parent doesn't duplicate the child.
# This behaviour is only activated during a raw save - it is mostly relevant to
# deserialization, but any sort of CORBA style 'narrow()' API would require a
# similar approach.
# Create a child-parent-grandparent chain
>>> place1 = Place(name="Guido's House of Pasta", address='944 W. Fullerton')
>>> place1.save_base(raw=True)
>>> restaurant = Restaurant(place_ptr=place1, serves_hot_dogs=True, serves_pizza=False)
>>> restaurant.save_base(raw=True)
>>> italian_restaurant = ItalianRestaurant(restaurant_ptr=restaurant, serves_gnocchi=True)
>>> italian_restaurant.save_base(raw=True)
# Create a child-parent chain with an explicit parent link
>>> place2 = Place(name='Main St', address='111 Main St')
>>> place2.save_base(raw=True)
>>> park = ParkingLot(parent=place2, capacity=100)
>>> park.save_base(raw=True)
# Check that no extra parent objects have been created.
>>> Place.objects.all()
[<Place: Guido's House of Pasta the place>, <Place: Main St the place>]
>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's House of Pasta"), ('serves_hot_dogs', True)]]
>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's House of Pasta"), ('serves_gnocchi', True), ('serves_hot_dogs', True)]]
>>> dicts = ParkingLot.objects.values('name','capacity')
>>> [sorted(d.items()) for d in dicts]
[[('capacity', 100), ('name', u'Main St')]]
# You can also update objects when using a raw save.
>>> place1.name = "Guido's All New House of Pasta"
>>> place1.save_base(raw=True)
>>> restaurant.serves_hot_dogs = False
>>> restaurant.save_base(raw=True)
>>> italian_restaurant.serves_gnocchi = False
>>> italian_restaurant.save_base(raw=True)
>>> place2.name='Derelict lot'
>>> place2.save_base(raw=True)
>>> park.capacity = 50
>>> park.save_base(raw=True)
# No extra parent objects after an update, either.
>>> Place.objects.all()
[<Place: Derelict lot the place>, <Place: Guido's All New House of Pasta the place>]
>>> dicts = Restaurant.objects.values('name','serves_hot_dogs')
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's All New House of Pasta"), ('serves_hot_dogs', False)]]
>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
>>> dicts = ParkingLot.objects.values('name','capacity')
>>> [sorted(d.items()) for d in dicts]
[[('capacity', 50), ('name', u'Derelict lot')]]
# If you try to raw_save a parent attribute onto a child object,
# the attribute will be ignored.
>>> italian_restaurant.name = "Lorenzo's Pasta Hut"
>>> italian_restaurant.save_base(raw=True)
# Note that the name has not changed
# - name is an attribute of Place, not ItalianRestaurant
>>> dicts = ItalianRestaurant.objects.values('name','serves_hot_dogs','serves_gnocchi')
>>> [sorted(d.items()) for d in dicts]
[[('name', u"Guido's All New House of Pasta"), ('serves_gnocchi', False), ('serves_hot_dogs', False)]]
"""}

View File

@ -223,3 +223,23 @@ class ModifyingSaveData(models.Model):
"A save method that modifies the data in the object" "A save method that modifies the data in the object"
self.data = 666 self.data = 666
super(ModifyingSaveData, self).save(raw) super(ModifyingSaveData, self).save(raw)
# Tests for serialization of models using inheritance.
# Regression for #7202, #7350
class AbstractBaseModel(models.Model):
parent_data = models.IntegerField()
class Meta:
abstract = True
class InheritAbstractModel(AbstractBaseModel):
child_data = models.IntegerField()
class BaseModel(models.Model):
parent_data = models.IntegerField()
class InheritBaseModel(BaseModel):
child_data = models.IntegerField()
class ExplicitInheritBaseModel(BaseModel):
parent = models.OneToOneField(BaseModel)
child_data = models.IntegerField()

View File

@ -32,7 +32,7 @@ def data_create(pk, klass, data):
instance = klass(id=pk) instance = klass(id=pk)
instance.data = data instance.data = data
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
return instance return [instance]
def generic_create(pk, klass, data): def generic_create(pk, klass, data):
instance = klass(id=pk) instance = klass(id=pk)
@ -40,32 +40,45 @@ def generic_create(pk, klass, data):
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
for tag in data[1:]: for tag in data[1:]:
instance.tags.create(data=tag) instance.tags.create(data=tag)
return instance return [instance]
def fk_create(pk, klass, data): def fk_create(pk, klass, data):
instance = klass(id=pk) instance = klass(id=pk)
setattr(instance, 'data_id', data) setattr(instance, 'data_id', data)
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
return instance return [instance]
def m2m_create(pk, klass, data): def m2m_create(pk, klass, data):
instance = klass(id=pk) instance = klass(id=pk)
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
instance.data = data instance.data = data
return instance return [instance]
def o2o_create(pk, klass, data): def o2o_create(pk, klass, data):
instance = klass() instance = klass()
instance.data_id = data instance.data_id = data
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
return instance return [instance]
def pk_create(pk, klass, data): def pk_create(pk, klass, data):
instance = klass() instance = klass()
instance.data = data instance.data = data
models.Model.save_base(instance, raw=True) models.Model.save_base(instance, raw=True)
return instance return [instance]
def inherited_create(pk, klass, data):
instance = klass(id=pk,**data)
# This isn't a raw save because:
# 1) we're testing inheritance, not field behaviour, so none
# of the field values need to be protected.
# 2) saving the child class and having the parent created
# automatically is easier than manually creating both.
models.Model.save(instance)
created = [instance]
for klass,field in instance._meta.parents.items():
created.append(klass.objects.get(id=pk))
return created
# A set of functions that can be used to compare # A set of functions that can be used to compare
# test data objects of various kinds # test data objects of various kinds
def data_compare(testcase, pk, klass, data): def data_compare(testcase, pk, klass, data):
@ -94,6 +107,11 @@ def pk_compare(testcase, pk, klass, data):
instance = klass.objects.get(data=data) instance = klass.objects.get(data=data)
testcase.assertEqual(data, instance.data) testcase.assertEqual(data, instance.data)
def inherited_compare(testcase, pk, klass, data):
instance = klass.objects.get(id=pk)
for key,value in data.items():
testcase.assertEqual(value, getattr(instance,key))
# Define some data types. Each data type is # Define some data types. Each data type is
# actually a pair of functions; one to create # actually a pair of functions; one to create
# and one to compare objects of that type # and one to compare objects of that type
@ -103,6 +121,7 @@ fk_obj = (fk_create, fk_compare)
m2m_obj = (m2m_create, m2m_compare) m2m_obj = (m2m_create, m2m_compare)
o2o_obj = (o2o_create, o2o_compare) o2o_obj = (o2o_create, o2o_compare)
pk_obj = (pk_create, pk_compare) pk_obj = (pk_create, pk_compare)
inherited_obj = (inherited_create, inherited_compare)
test_data = [ test_data = [
# Format: (data type, PK value, Model Class, data) # Format: (data type, PK value, Model Class, data)
@ -255,6 +274,10 @@ The end."""),
(data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)), (data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)),
(data_obj, 810, ModifyingSaveData, 42), (data_obj, 810, ModifyingSaveData, 42),
(inherited_obj, 900, InheritAbstractModel, {'child_data':37,'parent_data':42}),
(inherited_obj, 910, ExplicitInheritBaseModel, {'child_data':37,'parent_data':42}),
(inherited_obj, 920, InheritBaseModel, {'child_data':37,'parent_data':42}),
] ]
# Because Oracle treats the empty string as NULL, Oracle is expected to fail # Because Oracle treats the empty string as NULL, Oracle is expected to fail
@ -277,13 +300,19 @@ def serializerTest(format, self):
# Create all the objects defined in the test data # Create all the objects defined in the test data
objects = [] objects = []
instance_count = {}
transaction.enter_transaction_management() transaction.enter_transaction_management()
transaction.managed(True) transaction.managed(True)
for (func, pk, klass, datum) in test_data: for (func, pk, klass, datum) in test_data:
objects.append(func[0](pk, klass, datum)) objects.extend(func[0](pk, klass, datum))
instance_count[klass] = 0
transaction.commit() transaction.commit()
transaction.leave_transaction_management() transaction.leave_transaction_management()
# Get a count of the number of objects created for each class
for klass in instance_count:
instance_count[klass] = klass.objects.count()
# Add the generic tagged objects to the object list # Add the generic tagged objects to the object list
objects.extend(Tag.objects.all()) objects.extend(Tag.objects.all())
@ -304,6 +333,11 @@ def serializerTest(format, self):
for (func, pk, klass, datum) in test_data: for (func, pk, klass, datum) in test_data:
func[1](self, pk, klass, datum) func[1](self, pk, klass, datum)
# Assert that the number of objects deserialized is the
# same as the number that was serialized.
for klass, count in instance_count.items():
self.assertEquals(count, klass.objects.count())
def fieldsTest(format, self): def fieldsTest(format, self):
# Clear the database first # Clear the database first
management.call_command('flush', verbosity=0, interactive=False) management.call_command('flush', verbosity=0, interactive=False)