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)