Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clone an inherited django model instance

When I clone a django model instance I used to clean the 'pk' field. This seems not to work with an inherited model :

Take this :

class ModelA(models.Model):
    info1 = models.CharField(max_length=64)

class ModelB(ModelA):
    info2 = models.CharField(max_length=64)

class ModelC(ModelB):
    info3 = models.CharField(max_length=64)

Now let's create an instance and clone it by the 'usual' way ( I am using a django shell ):

In [1]: c=ModelC(info1="aaa",info2="bbb",info3="ccc")

In [2]: c.save()

In [3]: c.pk
Out[3]: 1L

In [4]: c.pk=None  <------ to clone

In [5]: c.save()   <------ should generate a new instance with a new index key

In [6]: c.pk       
Out[6]: 1L         <------ but don't

In [7]: ModelC.objects.all()
Out[7]: [<ModelC: ModelC object>]   (only one instance !)

The only way I found was to do :

In [16]: c.pk =None

In [17]: c.id=None

In [21]: c.modela_ptr_id=None

In [22]: c.modelb_ptr_id=None

In [23]: c.save()

In [24]: c.pk
Out[24]: 2L    <---- successful clone containing info1,info2,info3 from original instance

In [25]: ModelC.objects.all()
Out[25]: [<ModelC: ModelC object>, <ModelC: ModelC object>]

I find that very ugly, is there a more nice way to clone an instance from an inherited model ?

like image 473
Eric Avatar asked Dec 09 '25 03:12

Eric


1 Answers

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!

like image 144
Jonah Bishop Avatar answered Dec 11 '25 16:12

Jonah Bishop