Skip to content

Polymorphism in your ORM

July 20, 2013

Whenever you try to interface two incompatible technologies, say a relational database and an object-oriented typing system, you are going to run into difficulties. One problem I ran into recently was dealing with polymorphic classes in an ORM. The syntax here will be specific to Django, but the ideas should be applicable to many ORMs.

We have two django models, which do basically the same thing:

from django.db import models

class Foo(models.Model):
  foo_field = models.CharField()

  def run(self):
    ...implementation based on foo_field...

class Bar(models.Model):
  bar_field = models.CommaSeparatedIntegerField()

  def run(self):
    ...implementation based on bar_field...

Let’s say we have another Django model which attaches to our foo and baz models. What we really want is to write:

class Baz(models.Model):
  ...stuff...
  foo_bar = models.ForeignKey(Foo or Bar)

  ...methods...

In particular, I want to make sure that baz, and all of it’s baz-like cousins, get deleted whenever I call delete() on the relevant foo or bar instance. Also, if I were to write a new bazzy class, I shouldn’t have to manipulate foo and bar to make everyone place nice. That is what the ORM is for. So, in my first GoF mention in this space, we nee an adaptor pattern, or a wrapper as the Pythonic part of the engineering universe calls it.

Here is what my foo_bar_wrapper looks like:

class FooBarWrapper(models.Model):
  foo_bar_type = models.CharField(max_length=32)
  foo_bar_id = models.IntegerField()

  unique_together = ('foo_bar_name', 'foo_bar_id')

Now, we only have one class to override our delete() and save() functions for:

class Foo(models.Model):
  ...foo-y stufff...

  def save(self, *args, **kwargs):
    create_wrapper = False
    if not self.id:
      create_wrapper = True
    super(foo, self).save(*args, **kwargs)
    if create_wrapper:
      foo_wrapper = foo_bar_wrapper(
        foo_bar_type=self.__class__.__name__
        foo_bar_id=self.id)
      foo_wrapper.save()

The delete() method would be pretty similar. I implemented the delete() overloading on the wrapper class. We could implement that function just to be safe.

Outside of Django model forms, I attempted to not use the original foo and bar classes as much as possible. To do that, we need to access our foo or bar object:

from django.db.models.loading import get_model

class FooBarWrapper(models.Model):
  ...stuff...

  @propery
  def foo_bar(self):
    return get_model(self.foo_bar_type).objects.get(
      id=self.foo_bar_id)

An initial draft used a __getattr__ trick, but I believe this implementation makes it very clear you are accessing the foo or bar object indirectly through the wrapper. At this point, you can either write your own getter to obtain a foo_bar_wrapper from a foo or bar object, or you can hack foobarwrapper_set to get the object.

We’re even able to safely manipulate the universe attached to FooBarWrapper:

class FooBarWrapper(models.Model):
  ...

  def manipulatae_unverse(self):
    for uni in self.universe_set.all():
      uni.manipulate()

We have to be concerned about grep -r safety here, so I would love any input about universe_set.all() versus Universe.objects.filter(foo_bar_wrapper=self). My bias is towards the latter because of the aforementioned grep issues, but the latter seems to be more Django idiomatic.

We’ve ended up with an ugly one-to-one database relationship, which is often a very bad call. However, by putting FooBarWrapper in the middle, we have one class which is connected to the rest of the universe. Any and all new dependencies can ForeignKey off of FooBarWrapper with impunity, and a new Foo-ish class can be easily hooked into FooBarWrapper without modifying the rest of the universe.

From → Uncategorized

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: