1

I'm using django-polymorphic to model a simple relationship like this:

from django.db import models

from polymorphic.models import PolymorphicModel

class Base(PolymorphicModel):
    attribute_base = models.BooleanField()

class DescendantA(Base):
    attribute_a = models.BooleanField()

class DescendantB(Base):
    attribute_b = models.BooleanField()

I would like to find a way to convert an instance of DescendantA to an instance of DescendantB while keeping the data of its Base part (specifically the primary key). In my scenario, related models exist pointing to the instance. I would like to keep these relationships (and the instance's primary key) intact.

Setting the appropriate __class__ and polymorphic_ctype attributes on the instance (see this answer) works mostly but predictably results in inconsistent data: The database row for DescendantA will not be deleted and continues pointing to the Base row.

Is there a clean(er) way to achieve this conversion? I'm inclined to just create a new instance and copy the original data but I hope there's a better way.


Related questions:

Sören Weber
  • 601
  • 6
  • 25

2 Answers2

0

So for what I understand by "converting" you really want to persist data that way in your database. Then you do have to create and save a new instance of DescendantB copying the data over from your DescendantA and then deleting the former as they are two different tables. If they were proxy models though they would share the same table and then all you would have to do would be changing the polymorphic_ctype.

What you could try to do is copying over the attributes from the DescendantA instance using __dict__ but that relies of course that both models always share the same fields:

from copy import deepcopy

descendant_a = DescendantA.objects.first()  # the one you are copying over
descendant_b_kwargs = deepcopy(descendant_a.__dict__) # creates a dict copying the attributes over from descendant_a
descendant_b_kwargs.pop('_state')
descendant_b_kwargs.pop('base_ptr_id') # remove also the pointer to the parent
descendant_b = DescendantB.objects.create(**descendant_b_kwargs)
descendant_b.delete()
Vitor Diniz
  • 353
  • 2
  • 8
  • Thanks for your answer! I would like to avoid creating a whole new instance (although this is a good backup plan). Optimally, I would like to create a `DescendantB` row that points to the existing `Base` row and then delete the former `DescendantA` row in an idiomatic way. I hope there are some utilities in `django-polymorphic` that could facilitate this but I couldn't find any. – Sören Weber Jul 19 '22 at 17:29
  • Normally in n multi-table inheritance both parent and child are treated as a single entity so it's an odd situation having to change a child pointer (as the child even inherit the primary key from the parent). I doubt `django-polymorphic` would have a native support for such a request, also don't see why such a problem creating a new instance since in the end the ORM will execute the same database operations. – Vitor Diniz Jul 19 '22 at 19:07
  • You could also consider changing your architecture as for what I understand both DescendantA and DescendantB even have the same attributes, using multi-table inheritance would be an overkill. You could consider using proxy models https://docs.djangoproject.com/en/4.0/topics/db/models/#proxy-models. – Vitor Diniz Jul 19 '22 at 19:08
  • Unfortunately, they don't have the same attributes. I just tried to build a minimal example here, sorry for the confusion. The base model has attributes and both descendant models have a separate set of attributes as well. You are completely right that multi-table inheritance (and `django-polymorphic`) would just be a massive overhead otherwise. – Sören Weber Jul 19 '22 at 19:16
  • Sorry, didn't see your first comment. I would like to avoid creating a completely new instance since I'd like to keep the primary key and existing relationships with other models. By creating a new instance, I'd have to replicate both of them manually. I'd rather keep as much information as possible and only replace the necessary parts. – Sören Weber Jul 19 '22 at 19:31
0

I found a way that works pretty well but uses private parts of Django's API and so might break in the future. I wrote some tests that will alert me if this happens.

The gist of my solution is

from django.contrib.contenttypes.models import ContentType

# Setup
a = DescendantA.objects.create()

# Create new child instance
b = DescendantB(base_ptr=a)
b.save_base(raw=True)

# Delete the other child instance
a.delete(keep_parents=True)

# Change class and point to correct content type
a.__class__ = DescendantB
a.polymorphic_ctype = ContentType.objects.get_for_model(DescendantB)
a.save()

Related answers:

Sören Weber
  • 601
  • 6
  • 25