Home Computers Web Application Development Django UUIDField Problem
Django UUIDField Problem PDF Print E-mail
Written by Gordon Tillman   
Tuesday, 16 December 2008 21:37

I wanted to be able to have UUID primary keys for certain models. I found the implementation done by the django_extensions project (http://code.google.com/p/django-command-extensions/) and it works fine as is.

But I wanted to be able to have a UUIDField that stored its values in an actual 'uuid' column if we were using PostgreSQL. For all other databases it would use the same char(36) field as in the original implementation. This was for performance reasons, by the way.

So listed below was my attempt at creating a UUIDField (based on the django_extensions one) that would do that. It works great in every situation EXCEPT if I am working with a model that inherits from another model.

I've included the complete UUIDField definition, some sample models, and a small test case.

Here is an interesting part. The to_python() method of the UUIDField is supposed to return an instance of uuid.UUID. This is my original implementation of that method:

    def to_python(self, value):
        if not value:
            return None
        if isinstance(value, uuid.UUID):
            return value
        # attempt to parse a UUID
        return uuid.UUID(smart_unicode(value))

This original implementation words great unless I'm working with a model that inherits from a base model that uses the UUIDField as its primary key. If I change the implementation to this then the test case passes, both for the base model and the inherited model. But of course now it is returning a string, not a uuid.UUID value.

    def to_python(self, value):
        if not value:
            return None
        if isinstance(value, uuid.UUID):
            return smart_unicode(value)
        else:
            return value

I was wondering if anyone could suggest an improvement to my UUIDField implementation so that:

  1. to_python() returns a proper uuid.UUID instance
  2. I can still use 'uuid' columns in databases that support it

It's possible there is a bug in the part of the Django code that deals with inherited models, but I'm sure it's way more likely that there is a bug in my UUIDField!

UUIDField definition

import uuid

from django.forms.util import ValidationError
from django import forms
from django.db import models
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy

class UUIDVersionError(Exception):
    pass

class UUIDField(models.Field):
    """Encode and stores a Python uuid.UUID in a manner that is appropriate
    for the given datatabase that we are using.

    For sqlite3 or MySQL we save it as a 36-character string value
    For PostgreSQL we save it as a uuid field

    This class supports type 1, 2, 4, and 5 UUID's.
    """
    __metaclass__ = models.SubfieldBase

    _CREATE_COLUMN_TYPES = {
        'postgresql_psycopg2': 'uuid',
        'postgresql': 'uuid'
    }

    def __init__(self, verbose_name=None, name=None, auto=True, version=1,
        node=None, clock_seq=None, namespace=None, **kwargs):
        """Contruct a UUIDField.

        @param verbose_name: Optional verbose name to use in place of what
            Django would assign.
        @param name: Override Django's name assignment
        @param auto: If True, create a UUID value if one is not specified.
        @param version: By default we create a version 1 UUID.
        @param node: Used for version 1 UUID's.  If not supplied, then the
            uuid.getnode() function is called to obtain it.  This can be slow.
        @param clock_seq: Used for version 1 UUID's.  If not supplied a random
            14-bit sequence number is chosen
        @param namespace: Required for version 3 and version 5 UUID's.
        @param name: Required for version4 and version 5 UUID's.

        See Also:
          - Python Library Reference, section 18.16 for more information.
          - RFC 4122, "A Universally Unique IDentifier (UUID) URN Namespace"

        If you want to use one of these as a primary key for a Django
        model, do this::
            id = UUIDField(primary_key=True)
        This will currently I{not} work with Jython because PostgreSQL support
        in Jython is not working for uuid column types.
        """
        self.max_length = 36
        kwargs['max_length'] = self.max_length
        if auto:
            kwargs['blank'] = True
            kwargs.setdefault('editable', False)

        self.auto = auto
        self.version = version
        if version==1:
            self.node, self.clock_seq = node, clock_seq
        elif version==3 or version==5:
            self.namespace, self.name = namespace, name

        super(UUIDField, self).__init__(verbose_name=verbose_name,
            name=name, **kwargs)

    def create_uuid(self):
        if not self.version or self.version==4:
            return uuid.uuid4()
        elif self.version==1:
            return uuid.uuid1(self.node, self.clock_seq)
        elif self.version==2:
            raise UUIDVersionError("UUID version 2 is not supported.")
        elif self.version==3:
            return uuid.uuid3(self.namespace, self.name)
        elif self.version==5:
            return uuid.uuid5(self.namespace, self.name)
        else:
            raise UUIDVersionError("UUID version %s is not valid." % self.version)

    def db_type(self):
        from django.conf import settings
        return UUIDField._CREATE_COLUMN_TYPES.get(settings.DATABASE_ENGINE,
            "char(%s)" % self.max_length)

    def to_python(self, value):
        """Return a uuid.UUID instance from the value returned by the
        database."""
        #
        # This is the proper way... But this doesn't work correctly when
        # working with an inherited model
        #
        if not value:
            return None
        if isinstance(value, uuid.UUID):
            return value
        # attempt to parse a UUID
        return uuid.UUID(smart_unicode(value))

        #
        # If I do the following (returning a String instead of a UUID
        # instance), everything works.
        #

        #if not value:
        #    return None
        #if isinstance(value, uuid.UUID):
        #    return smart_unicode(value)
        #else:
        #    return value

    def pre_save(self, model_instance, add):
        if self.auto and add:
            value = self.create_uuid()
            setattr(model_instance, self.attname, value)
        else:
            value = super(UUIDField, self).pre_save(model_instance, add)
            if self.auto and not value:
                value = self.create_uuid()
                setattr(model_instance, self.attname, value)
        return value


    def get_db_prep_value(self, value):
        """Casts uuid.UUID values into the format expected by the back end
        for use in queries"""
        if isinstance(value, uuid.UUID):
            return smart_unicode(value)
        return value


    def value_to_string(self, obj):
        val = self._get_val_from_obj(obj)
        if val is None:
            data = ''
        else:
            data = smart_unicode(val)
        return data

    def formfield(self, **kwargs):
        defaults = {
            'form_class': forms.CharField,
            'max_length': self.max_length
            }
        defaults.update(kwargs)
        return super(UUIDField, self).formfield(**defaults)

Sample Models

from django.db import models
from fields import UUIDField

class Customer(models.Model):
    MAX_NAME_LEN = 200
    id = UUIDField(primary_key=True)
    name = models.CharField(max_length=MAX_NAME_LEN)

class User(models.Model):
    MAX_FIRST_NAME = 32
    MAX_LAST_NAME = 32
    MAX_USERNAME = 32

    id = UUIDField(primary_key=True)
    customer = models.ForeignKey(Customer)
    first_name = models.CharField(max_length=MAX_FIRST_NAME)
    last_name = models.CharField(max_length=MAX_LAST_NAME)
    username = models.CharField(max_length=MAX_USERNAME)
    password = models.CharField(max_length=128, blank=True)

class Teacher(User):
    email = models.EmailField()
    admin = models.BooleanField(default=False)

Sample Test

import logging
import random
import unittest
import time
import sys
from models import *

class InheritanceTestCase(unittest.TestCase):
    """
    python manage.py test hb7t.InheritanceTestCase
    """

    def runTest(self):
        c = Customer(name='cust1')
        c.save()
        u = User(customer=c, first_name='f', last_name='l', username='fl',
            password='p')
        u.save()
        self.assertEqual(1, Customer.objects.count())
        self.assertEqual(1, User.objects.count())
        self.assertEqual(0, Teacher.objects.count())

        t = Teacher(customer=c, first_name='g', last_name='t', 
            username='gt', password='p', email='
 This e-mail address is being protected from spambots. You need JavaScript enabled to view it
 ')
        # Everything passes up to this point, whether or not to_python() returns
        # a uuid.UUID value OR a string value
        # But t.save() fails if to_python() returns a uuid.UUID value
        t.save()
        self.assertEqual(1, Customer.objects.count())
        self.assertEqual(2, User.objects.count())
        self.assertEqual(1, Teacher.objects.count())

        c.delete()
        self.assertEqual(0, Customer.objects.count())
        self.assertEqual(0, User.objects.count())
        self.assertEqual(0, Teacher.objects.count())

Error When Running Sample Test

ERROR: runTest (kindle.hb7t.tests.InheritanceTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/gordy/projects/kindle/../kindle/hb7t/tests.py", line 118, in runTest
    t.save()
  File "/usr/local/lib/python2.5/site-packages/django/db/models/base.py", line 328, in save
    self.save_base(force_insert=force_insert, force_update=force_update)
  File "/usr/local/lib/python2.5/site-packages/django/db/models/base.py", line 375, in save_base
    manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())):
  File "/usr/local/lib/python2.5/site-packages/django/db/models/query.py", line 191, in __nonzero__
    iter(self).next()
  File "/usr/local/lib/python2.5/site-packages/django/db/models/query.py", line 185, in _result_iter
    self._fill_cache()
  File "/usr/local/lib/python2.5/site-packages/django/db/models/query.py", line 618, in _fill_cache
    self._result_cache.append(self._iter.next())
  File "/usr/local/lib/python2.5/site-packages/django/db/models/query.py", line 659, in iterator
    for row in self.query.results_iter():
  File "/usr/local/lib/python2.5/site-packages/django/db/models/sql/query.py", line 203, in results_iter
    for rows in self.execute_sql(MULTI):
  File "/usr/local/lib/python2.5/site-packages/django/db/models/sql/query.py", line 1756, in execute_sql
    cursor.execute(sql, params)
ProgrammingError: can't adapt

Last Updated on Tuesday, 16 December 2008 21:50