[Fixed]-Clone an inherited django model instance

1👍

I was puzzled by Django’s behavior in the scenario you posited, so I put in a bunch of print statements down in Django’s models\base.py file to figure out what was going on. It turns out that, in the inheritance example you set up, ModelC effectively has more than one primary key. There’s even a comment down in the code itself that references this subtle behavior (see related Django ticket 17615).

The crux of the matter is that non-abstract model inheritance works through OneToOneField relationships, which are a special variant of ForeignKey. So ModelC in your example ends up with two OneToOneField relationships: one concrete key (modelb_ptr_id) and one inherited key via ModelB (modela_ptr_id). The id attribute may also be considered a primary key, but I think it’s conceptually just an alias for modela_ptr_id under the hood.

When you set the ModelC instance pk value to 0, you are effectively clearing the "first" (closest binding) primary key. But there’s another primary key in play! The Django save() routine calls an internal _save_parents() method which does a sync between the keys if they don’t match. It does this by recursively walking up the parent relationships, syncing items as it goes. This routine is where the ModelC instance primary key gets "reset" to 1 in your initial example. The routine walks up ModelC to ModelB, sees that it has a parent (and that the parent value does not match the current value), syncs the two, and continues.

If I understand the code correctly, this routine is in place to ensure that all parents have been written to the database as necessary, so that there is no data loss as items get saved. This is subtle behavior, but something worth paying attention to in inheritance usage.

This was a very interesting question!

0👍

c=ModelC(info1="aaa",info2="bbb",info3="ccc")
# creates an instance

c.save()
# writes instance to db

c.pk=None
# I doubt u can nullify the auto-generated pk of an existing object, because a pk is not nullable
c.save()
# if I'm right nothing will happen here.

So c will always be the same object. If you want to clone it you need to generate a new object. Either with a constructor within ModelC:

def __init__(another_modelC_obj=null, self):
   if another_modelC_obj:
      # for every field in another_modelC_obj: do self.field = another_modelC_obj.field
   super().__init__()

so you can go

c2=ModelC(c)

Or call it directly with:

c2=ModelC(c.info1, c.info2, c.info3)

Then c2 and c will be identical despite of their pk

👤Max M

0👍

  1. You need to save() it again immediately after setting c.pk = None.

Why?
Because the ModelC.id field is an auto-generated and auto-incremented field.

When you call save(), Django treats the instance as new, and the database will assign a unique primary key value.

# old id / pk
print(c.id)

# set it null
c.pk = None

# ask Django to save it again
c.save()

#get new id
print(c.id)
  1. Be mindful of other many-to-many or foreign keys in your model, as these relationships will also be copied.

  2. If this is a common requirement for your models, consider creating an abstract class for this purpose.

from django.db import models

class CloneableModel(models.Model):
    class Meta:
        abstract = True

    def clone(self):
        new_instance = self.__class__().get(id=self.id)
        new_instance.id = None
        new_instance.save()

        return new_instance

class ModelC(CloneableModel):
...

Leave a comment