CNK's blog

Django's GenericForeignKeys and GenericRelations

I am working on a project that has two separate but interrelated Django web sites (projects in Django’s parlance). In an earlier blog post, I described setting up the second project (mk_ai) to have read-only access to the first project’s database (mk_web_core) in dev but then getting around those access restrictions for testing. The main thing I need for testing is a big, set of hierarchical data to be loaded into the first project’s test database. I can use the manage commands dumpdata and loaddata to preserve date in my development environment, but when I tried to load that same data into the test database, I ran into problems.

We are using GenericForeignKeys and GenericRelations. Django implements GenericForeignKeys by creating a database foreign key into the django_content_type table. In our mixed database setup, my django_content_type table is in the mk_ai schema. So, even if I set up my database router to allow_relation across databases AND the postgres database adapter would even attempt to make that join, the content types in the references in mk_web_core would not be in mk_ai’s django_content_type table. So we can’t use Django’s GenericForeignKeys. What shall we do instead?

Rails implements a similar type of relationship with a feature it calls Polymorphic Associations. Django stores the object’s id + a FK link to row in the content_type table representing the the object’s model. Rails store’s the object’s id + the object’s class name in a field it calls _type. I decided to use the Rails method to set up my database representations. That replaces the GenericForiegnKey aspect. To replace the GenericRelation part, I just created a case statement that allows queries to chain in the approrpriate related model based on the … content type. Perhaps showing an example will make this clearer.

The original way, using Django’s GenericForeignKey:

class PageBlock(models.Model):
    page = models.ForeignKey('Page')
    position = models.PositiveSmallIntegerField()
    allowed_block_types = models.Q(app_label='materials', model='text') | \
            models.Q(app_label='materials', model='video') | \
            models.Q(app_label='course_materials', model='image')
    block_type = models.ForeignKey(ContentType, limit_choices_to=allowed_block_types)
    object_id = models.PositiveSmallIntegerField()
    material = GenericForeignKey(block_type', 'object_id')

The ‘rails’ way, using a block_type name field that can be read directly in the mk_ai schema.

class PageBlock(models.Model):
    """
    This is a mapping table to all us to access collections of
    blocks regardless of their actual type.

    TODO:
    Figure out how to make the object_id options fill a select
    list once the user chooses a block_type in the form on the
    admin interface.
    """
    BLOCK_TYPE_NAMES = [('text', 'TextBlock'),
                        ('video', 'VideoBlock'),
                        ('image', 'ImageBlock'),
                       ]
    page = models.ForeignKey('Page')
    position = models.PositiveSmallIntegerField()
    block_type_name = models.CharField(max_length=100, choices=BLOCK_TYPE_NAMES)
    # The block_id would be a ForeignKey field into a Video, Image... if we were mapping to just one model
    block_id = models.PositiveSmallIntegerField()

    @property
    def block(self):
        if self.block_type == 'TextBlock':
            return TextBlock.objects.get(pk=self.block_id)
        if self.block_type == 'VideoBlock':
            return VideoBlock.objects.get(pk=self.block_id)
        if self.block_type == 'ImageBlock':
            return ImageBlock.objects.get(pk=self.block_id)

GenericForeignKey and GenericRelation are two sides of the coin - they allow you to easily make queries both directions. In our domain, I don’t really have much occaision to go from Block to Page, so I don’t really need to GenericRelation. However, if you need to replace it, you can create a method to do the appropriate query.

# ORIGINALLY
class VideoBlock(models.Model):
    title = models.CharField(max_length=256)
    content = models.FileField(upload_to='videos/')
    page_block = GenericRelation(PageBlock,
                                 object_id_field='object_id',
                                 content_type_field='page_block')
    @property
    def model_name(self):
       return "VideoBlock"

# AFTER REMOVING THE GenericForeignKey
class VideoBlock(models.Model):
    title = models.CharField(max_length=256)
    content = models.FileField(upload_to='videos/')

    @property
    def model_name(self):
        return "VideoBlock"

    @property
    def page_block(self):
        return self.PageBlock.objects.filter(block_type_name='VideoBlock',
                                             object_id=self.id)

Comments