eugene
eugene

Reputation: 41735

Convert a subclass model instance to another subclass model instance in django?

I have a ModelBase, and ModelA, ModelB.

I want to change ModelA instance to ModelB instance. (I can handle the difference of attributes they have)

I've seen related questions but doesn't quite work for me.

How can I create an inherited django model instance from an existing base model instance?
Change class of child on django models

When you have Place - Restaurant/Bar relationship,
I think it's quite reasonable to be able to switch a restaurant to a bar.

Upvotes: 4

Views: 3120

Answers (4)

pierreben
pierreben

Reputation: 396

Maybe the Django models API has evolved since this question was asked, but here's how I easily handled it, with a solution that has the advantage of keeping the parent database entry unchanged during the conversion process, ensuring that all related objects in M2M or O2M are retained :

Considering the following models :

class Parent(Model):
    pass

class Child(Parent):
   child_field = models.CharField()


class Child2(Parent):
   child2_field = models.CharField()

In this configuration, Django creates 3 tables parent, child and child2 with a parent_ptr_id OneToOne from child to parent and a parent_ptr_id OneToOne from child2 to parent.

We can represent it such as :

class Parent(Model):
    pass

class Child(Model):
   parent_ptr = models.OneToOneField(Parent)
   child_field = models.CharField()

class Child2(Model):
   parent_ptr = models.OneToOneField(Parent)
   child2_field = models.CharField()

If we consider a Child model instance that we want to convert into a Child2 model instance, the best option is to :

  • Keep the record in the parent table unchanged
  • Delete the record from the child table (the child_field value will obviously be lost)
  • Create a new record in the child2 table, referencing the record in the parent table

This can be done with the following steps:

object_id = 123 # Child object id
child_object = Child.objects.get(id=object_id)

# Delete the record in the child table, keeping the record in the parent table :
child_object.delete(keep_parents=True)

# Create the record in the child2 table :
child2_object = Child2(parent_ptr_id=object_id)
child2_object.save_base(raw=True)

Upvotes: 0

Salman Aljabri
Salman Aljabri

Reputation: 217

I had to deal with the same problem, both yuvi and arctelix answers did not work for me. yuvi solution gives an error and arctelix solution creates new object with new pk.

The goal here is to change the subclass model while keeping the original superclass as it is with the old pk.

First: Delete the old subclass and keep the superclass.Check Django documents.

Second: Add the new subclass with its fields and pass the superclass to it. Check this q

Example: A place could be a restaurant or a caffe, and you want to change a restaurant place to a caffee; as follow:

class Place(models.Model):
            name = models.CharField(max_length=50)
            address = models.CharField(max_length=80)

class Caffe(Place):
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)

class Restaurant(Place):
    serves_tea = models.BooleanField(default=False)
    serves_coffee = models.BooleanField(default=False)

# get the objecte to be changed
rest = Restaurant.objects.get(pk=1) #arbitrary number
#delete the subclass while keeping the parent
rest.delete(keep_parents=True)

place = Place.objects.get(pk=1) # the primary key must be the same as the deleted restaurant

# Create a caffe and pass the original place
caffee = Caffe(place_ptr_id=place.pk) #this will empty the parent field

#update parent fields
caffee.__dict__.update(place.__dict__)

#add other field
........

#save the caffe
caffee.save()

Upvotes: 4

Arctelix
Arctelix

Reputation: 4576

In yuvi's answer manually assigning modelbase_ptr and saving fails since instance.modelbase_ptr is deleted prior to save.

Building on yuvi's answer here a more explicit example and works generically for abstract and non-abstract conversions of:

  • ModelBase -> ModelChild
  • ModelChild -> ModelBase
  • ModelChild -> ModelChild

Optionally preserves the original id and this follows the django docs recomended methodology.

ex_model = ModelA
new_model = ModelB

ex_instance = ex_model.objects.get(pk=1) #arbitrary

# find fields required for new_model:
new_fields = [f.name for f in new_model._meta.fields]

# make new dict of existing field : value
new_fields_dict = dict( [(x, getattr(ex_instance, x, None)) for x in new_fields] )

# Save temp copy as new_model with new id
# modelbase_ptr will be created automatically as required
new_fields_dict.pop('project_ptr', None)
temp_instance = new_model(**new_fields_dict) 
temp_instance.pk = None
temp_instance.id = None
temp_instance.save()
# you must set all your related fields here
temp_instance.copy_related(ex_instance)

ex_instance.delete() 

# (optional) Save final copy as new_model with original id
final_instance = new_model(**new_fields_dict)
final_instance.save()
final_instance.copy_related(temp_instance)
temp_instance.delete()

# here are the removed fields, handle as required
removed_fields = [f.name for f in ex_model._meta.fields if f.name not in new_fields_dict.keys()]
removed_fields_dict = dict( [(x, getattr(ex_instance, x, None)) for x in removed_fields] )

In Class ModelBase:

def copy_related(self, from):
    # include all your related fields here
    self.related_field = from.related_field.all()
    self.related_field_a = from.related_field_a.all()

Upvotes: 0

yuvi
yuvi

Reputation: 18447

I would create an entirely new instance of the second model with the same values of their shared attributes, then delete the old one. Seems like the cleanest way to me.

If ModelBase is abstract:

instance = ModelA.objects.get(pk=1) #arbitrary        

# find parent class fields:
fields = [f.name for f in ModelBase._meta.fields]

# get the values from the modelA instance
values = dict( [(x, getattr(instance, x)) for x in fields] )

#assign same values to new instance of second model
new_instance = ModelB(**values) 

#add any additional information to new instance here

new_instance.save() #save new one
instance.delete() # remove the old one

If ModelBase is not abstract, however, you'll have to do an extra workaround:

fields = [f.name for f in ModelBase._meta.fields if f.name != 'id']
#... other parts are the same...

new_instance.modelbase_ptr = instance.modelbase_ptr #re-assign related parent
instance.delete() #delete this first!
new_instance.save()

Upvotes: 3

Related Questions