Code

Opened 9 months ago

Last modified 6 months ago

#20776 new Cleanup/optimization

Multi-level Multi-table Inheritance - mismatched PKs / explicit db_columns

Reported by: dowstreet@… Owned by: nobody
Component: Database layer (models, ORM) Version: 1.5
Severity: Normal Keywords: Muti-table Inheritance
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

There seems to be a problem with queries that (i) span several levels of multi-table inheritance subclassing when (ii) PKs do not match all the way up the hierarchy. This situation can arise when a higher level object (e.g. parent) is created before a lower level object (e.g. grandchild). The query seems to use the PK of the grandchild's parent instead of walking the whole way up the hierarchy when looking up fields that are stored in the grandparent table.

In the example below there are three levels in the class hierarchy. When accessing the name field (which is stored in the Level1 base class) from an object in Level3, the query returns an unexpected value 'Top 1' instead of the expected 'Bot 1':

# models.py

from django.db import models

class Level1(models.Model):
 """ Top level base class """

 level1_id = models.AutoField(primary_key=True, db_column='Level1_ID')
 name = models.CharField(max_length=32, db_column='Name', blank=True, null=True)

 class Meta:
     db_table = 'Level1'


class Level2(Level1):
 """ Middle level class """

 level2_id = models.AutoField(primary_key=True, db_column='Level2_ID')
 level1 = models.OneToOneField('Level1', db_column='Level1_ID', parent_link=True)  # parent class

 class Meta:
     db_table = 'Level2'


class Level3(Level2):
 """ Bottom level class """

 level3_id = models.AutoField(primary_key=True, db_column='Level3_ID')
 level2 = models.OneToOneField('Level2', db_column='Level2_ID', parent_link=True)  # parent class

 class Meta:
     db_table = 'Level3'

Using the shell to add a Level1 object and then a Level3 object to an otherwise empty database:

from sc_test.models import *

>>> top1 = Level1()
>>> top1.name = 'Top 1'
>>> top1.save()

>>> bot1 = Level3()
>>> bot1.name = 'Bot 1'
>>> bot1.save()

>>> l1 = Level1.objects.all()
>>> l1
[<Level1: Level1 object>, <Level1: Level1 object>]

>>> l2 = Level2.objects.all()
>>> l2
[<Level2: Level2 object>]

>>> l3 = Level3.objects.all()
>>> l3
[<Level3: Level3 object>]

>>> l1[0].name
u'Top 1'

>>> l1[1].name
u'Bot 1'

>>> l2[0].name
u'Bot 1'

>>> l3[0].name
u'Top 1'                               # WRONG!! - expected value is u'Bot 1'

Note: In this example OneToOneField and ID/primary keys are manually added so that specific db_column names can be used. It is unclear whether the problem is related to this combination, or whether it affects auto-generated OneToOneFields as well.

Attachments (0)

Change History (3)

comment:1 Changed 9 months ago by akaariai

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

I believe the problem is having level2_id in Level2, and level2 OneToOneField in Level3. OneToOneFields store the related instance in field.name (level2) and the raw database value in field.attname (level2_id). The level2 field's attname conflicts with level2_id field name in Level2.

I am not 100% sure if this is the case. You could test this by changing the name of level2_id to something else in Level2. If the reason is the naming conflict this looks like something that could be dealt with in the validation GSoC project.

comment:2 Changed 6 months ago by timo

  • Component changed from Uncategorized to Database layer (models, ORM)
  • Triage Stage changed from Unreviewed to Accepted
  • Type changed from Bug to Cleanup/optimization

@dowstreet, can you provide any further info? I'm going to tentatively mark it accepted to get it off the unreviewed queue.

comment:3 Changed 6 months ago by dowstreet@…

It looks like the problem occurs if a separate PK is defined in each child class distinct from the OneToOneField pointing to the parent class. If instead the OneToOneField is used as the PK in the child class then everything seems to work. Maybe this is ok - i.e. one is a valid configuration and one is not? We had initially configured a separate PK in order to match a legacy database (but ended up changing that schema) - not sure if there is a standard schema design for other (non-django) software that uses similar class hierarchy.

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as new
The owner will be changed from nobody to anonymous. Next status will be 'assigned'
as The resolution will be set. Next status will be 'closed'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.