The Case of the Unusable Reusable
The Django web framework has a huge following that releases lots of simple reusable apps that can be plugged into your website. However, sometimes they are too simple, and not ”plugin-able“ quite enough.
Take for instance django-favorites. By itself it‘s a great package that does exactly what you want–it allows a user to mark things as being a favorite. Notice that I said ”things.“ ”Things“ here means any model, because it uses the generic relationships framework that Django provides.
This is a huge win on its own because it means that I don’t have to define a new favorites model for blog posts, one for photos and another for music I‘m listening too. But, there’s a downside to this as well.
How do I check to see if an item is a favorite? Well, for each blog post I select back from the database, I have to make another query to see whether or not it was a favorite or not. Alternatively, of course, I could be smarter and do a bulk query using an IN
clause, making the query count only 2. But, if I wasn‘t using an ORM, my SQL would use an OUTER JOIN
, or a sub-query to select back whether or not it was marked a favorite for the current user.
Django can do this using the `extra` method for QuerySet
s.
Ok, so we can use extra
every time we want to select back whether or not an item is a favorite, no big deal.
content_type = ContentType.objects.get_for_model(Photo) SQL = """SELECT 1 FROM favorites_favorite f WHERE f.content_type_id = %(content_type)d and f.object_id = photos_photo.id and f.user_id = %(user_id)d """ attrs = {'content_type': content_type.id, 'user_id': user.id} entries = Photo.objects.extra(select={'is_favorite': SQL % attrs})
Except that you have to do this every time. So, you then create a custom manager for Photo
that includes a check_is_favorite
method, which adds the is_favorite
pseudo-column and everything is good.
That is until you have to do it for Entry
, and Song
, and User
.
The solution however is simple. Reusable apps should include a ”ManagerMixin“, if it makes sense to make reusing the app as painless as possible. The django- favorites application that I’ve been using in my example would be complete in my eyes if it had something like this defined in it:
class FavoritesManagerMixin(object): """ A Mixin to add a
favoritefavorite` column via extra """ def with_favorite_for(self, user, all=True): """ Adds a column favoritefavorite to the returned object, which indicates whether or not this item is a favorite for a user ”“” contenttype = ContentType.objects.getformodel(self.model) pkfield = “%s.%s” % (qn(self.model.meta.dbtable), qn(self.model.meta.pk.column)) favoritesql = “”“(SELECT 1 FROM %(favoritesdbtable)s WHERE %(favoritesdbtable)s.objectid = %(pkfield)s and %(favoritesdbtable)s.contenttypeid = %(contenttype)d and %(favoritesdbtable)s.userid = %(userid)d) """ % {'pkfield‘: pkfield, \ 'dbtable’: qn(self.model.meta.dbtable), \ ‘favoritesdbtable’: qn(Favorite.meta.dbtable), \ ‘userid': user.pk, \ 'contenttype’: contenttype.id, \ } extras = { 'select': {'favoritefavorite': favoritesql}, } if not all: extras[‘where’] = [‘favoritefavorite == 1’] return self.extra(extras) `
I have yet to run a bench mark against this to determine whether or not the sub-query here is less efficient than doing an OUTER JOIN
. My gut says it would be, but for a first go at it, I‘ll keep it like this.
Anyway, then to make use of this you create a custom manager that uses FavoritesManagerMixin
as one of its base classes:
class SongManager(models.Manager, **FavoritesMixinManager**): pass class Song(models.Model): title = models.CharField(max_length=255, null=False, blank=False) artist = models.ForeignKey('Artist') album = models.ForeignKey('Album') **objects = SongManager()**
And then to make use of it, we do:
all_songs_with_favorites_marked = Song.objects.with_favorite_for(user) only_favorite_songs = Song.objects.with_favorite_for(user, all=False)
The intention of this post isn’t to point out a flaw in django-favorites, an otherwise great reusable application, BTW, but instead is meant to give a way that this idea of reusable can actually be usable.
I put my django-favorites on github. Feel free to flame me for going against what this post stands for and creating yet another ”favorites“ reusable app, without submitting a patch to the original, I wanted some practice in Django development.
Code)
(Generic Relations)
Relational Mapper)
methods-that-return-new-querysets
—2009-07-22