Rob Oakes
Oct 22, 2022

Populating a Django Model Subclass from a Parent Table

One of the most powerful features in the Django web framework is the ability to extend the properties of a model instance through inheritance. (Sometimes unfortunately ...) When you do this, the child model will only register instances created from the child class. What about cases where you need to populate a child class from the parent and preserve attributes and IDs? In this blog post, we'll look at one way to create multi-table child instances from an existing row in the parent table.

One of the most powerful features in the Django web framework is the ability to extend the properties of a model instance (a class inheriting from django.db.model.Model) through Python inheritance. This form of "multi-table inheritance" makes it easy to add additional properties to an already existing model without having to duplicate the code or data properties.

The example below (stolen from the Django documentation) shows how this can work:

from django.db import models


class Place(models.Model):
    ''' Parent model containing attributes such as a "name" and "address"
    '''
    name = models.CharField(max_length=50)
    address = models.CharField(max_length=256)


class Restaurant(Place):
    ''' Child model linked to Place with additional data properties
        describing a restaurant.
    '''
    serves_hot_dogs = models.BooleanField(default=False)
    serves_pizza = models.BooleanField(default=False)
    

class Store(Place):
    ''' Child model linked to Place with properties describing
        a retail store.
    '''
    store_type = models.CharField(max_length=256)
    hours = models.CharField(max_length=256)

Multi-table inheritance works by splitting the properties of each model into its own database table. Child models are linked to their parents via a one-to-one relationship (which uses a OneToOneField). The parent model, Place, will contain entries for every Restaurant and Store, and may contain entries not linked to either of the child tables.

Generally speaking, this form of allowing properties to span multiple tables is invisible. Child models are created with a full set of properties, and Django handles the attribute splitting and routing under the hood. The properties of parent models populate transparently and will be incorporated into child queries as though they were part of the same table. The reverse is not true, however, unless created via a child, entries from the parent table without a corresponding child are invisible.

# Create a restaurant called "Bob's Cafe"
>>> r = Restaurant(name="Bob's Cafe", description='',
...     serves_hot_dogs=True, serves_pizza=True)
>>> r.save()

# Query for the cafe using Restaurant and Place.
# Both queries will return results.
>>> assert Place.objects.filter(name__icontains="Bob's Cafe").exists()
>>> assert Restaruant.objects.filter(name__icontains="Bob's Cafe").exists()

# Create a place called "Famous Hiking Trail" and try to query it
# using the Restaurant model (should throw an assertion error)
>>> p = Place(name='Famous Hiking Trail', description='A good place to get away.')
>>> p.save()
>>> assert Restaruant.objects.filter(name__icontains='Famous Hiking Trail').exsists()

Under nearly all circumstances, this is how you want the models to behave. It doesn't make a lot of sense to create Restaurants for every entry in the Place table. But there are some exceptions. For example, you might need to add properties to an existing User model and extending the existing model class is not an option.

I recently ran into just this sort of problem working on an API for Sonador (an open source medical imaging platform that Oak-Tree helps to develop). I needed to extend the "API token" model to include a description field, and it was not possible to modify the main model instance. So I created a subclass and added the additional field.

from secure.models import APiAccess
from django.contrib import admin


class SonadorApiAccess(ApiAccess):
    ''' Model subclass which allows for the API access (access ID/secret) to appear in the same model
        as groups and auth servers
    '''
    description = models.CharField(blank=True, null=True, max_length=1024)
    
    class Meta:
    	app_label = 'auth'
    	verbose_name = 'API Access'
    	verbose_name_plural = 'Access IDs/Secret Keys'
	
    def __str__(self):
    	return '%s... (user=%s)' % (self.access_id[:15], self.user.username)
		

class UserLabelMixin(object):
    ''' Mixin class for Django admin structures that allows for table display for 
        the username, first name, last name, and email of the account associated
        with the object. User properties are added to the search.
    '''
    search_fields = ('user__username', 'user__first_name', 'user__last_name', 'user__email')
    autocomplete_fields = ('user',)
    
    def user_display(self, obj):
    	return user_displayname(obj.user)
    user_display.short_description = ''
    
    def user_email(self, obj):
    	return obj.user.email
    user_email.short_description = 'Email'


class SonadorApiAccessAdmin(UserLabelMixin, ApiAccessAdmin):
    ''' Admin to manage API access IDs and secrets within Sonador
    '''
    list_display = ('user', 'user_display', 'user_email', 
        'admin_masked_access_id', 'description', 'ctime')

Hurrah, problem solved! Except ... not really. After making this change, it was no longer possible to see or manage any of the existing API access credentials from the admin interface. They still existed and authentication to the API worked as expected, but the administrative interface no longer worked. Not ideal.

So, I needed to find a way to populate the new child table from instances in the parent table. Unfortunately, this was not as straightforward as I hoped it would be. Simply trying to use the one-to-one pointer does not work.

# DOES NOT WORK: Trying to populate a child model from a parent
# instance by populating the one-to-one field fails.
>>> for a in ApiAccess.objects.all():
...   sa = SonadorApiAccess(apiaccess_ptr_id=a.pk, description='Test')
...   sa.save()

Luckily, after casting around on Stack Overflow I found a method that does work.

  1. Create an instance of the child model and populate the pointer ID
  2. Copy all model values from the parent model to the child and populate the additional child attributes.
  3. Save the child model instance.

This method accomplished everything I needed: it preserved the existing API credentials, created entries in the new child model, and allowed for credential descriptions to be added.

# Create entries in a child model from a parent
>>> for a in ApiAccess.objects.all():
...   sa = SonadorApiAccess(apiaccess_ptr_id=a.pk, description='Test') 
...   sa.__dict__.update(a.__dict__)
...   sa.save()

It was so useful, I thought I would add it here for future reference.

Rob Oakes Oct 22, 2022
More Articles by Rob Oakes

Loading

Unable to find related content

Comments

Loading
Unable to retrieve data due to an error
Retry
No results found
Back to All Comments