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
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)