28

Feb

Filed in Code, Django |

So today I went ahead and cleaned up my UUIDField implementation. Originally it was just based off of a snippet. I wanted to swap it over to use a BINARY column in MySQL, after reading on how FriendFeed had been using UUIDs. Due to using Django however, you can’t use binary data or BLOB columns effectively. It tries to coerce everything to unicode so it ends up failing on any kind of binary data.

Instead of using a BINARY(16) column, I just switched over to a CHAR(32) (previously a CHAR(36)). In doing this I also converted the field to keep everything in the UUID format (to better handle passing around the values, and converting to any format you want).

All in all, the new UUIDField works as intended, and is a nice improvement over the simple “automatically insert a UUID here”, which is what it was before.

Here’s the code:

from django.db import models
 
import uuid
 
class UUIDField(models.CharField):
    """
        A field which stores a UUID value in hex format. This may also have
        the Boolean attribute 'auto' which will set the value on initial save to a
        new UUID value (calculated using the UUID1 method). Note that while all
        UUIDs are expected to be unique we enforce this with a DB constraint.
    """
    __metaclass__ = models.SubfieldBase
 
    def __init__(self, version=4, node=None, clock_seq=None, namespace=None, auto=False, name=None, *args, **kwargs):
        assert(version in (1, 3, 4, 5), "UUID version %s is not supported." % (version,))
        self.auto = auto
        self.version = version
        # Set this as a fixed value, we store UUIDs in text.
        kwargs['max_length'] = 32
        if auto:
            # Do not let the user edit UUIDs if they are auto-assigned.
            kwargs['editable'] = False
            kwargs['blank'] = True
            kwargs['unique'] = True
        if version == 1:
            self.node, self.clock_seq = node, clock_seq
        elif version in (3, 5):
            self.namespace, self.name = namespace, name
        super(UUIDField, self).__init__(*args, **kwargs)
 
    def _create_uuid(self):
        if self.version == 1:
            args = (self.node, self.clock_seq)
        elif self.version in (3, 5):
            args = (self.namespace, self.name)
        else:
            args = ()
        return getattr(uuid, 'uuid%s' % (self.version,))(*args)
 
    def db_type(self):
        return 'char'
 
    def pre_save(self, model_instance, add):
        """ see CharField.pre_save
            This is used to ensure that we auto-set values if required.
        """
        value = getattr(model_instance, self.attname, None)
        if not value and self.auto:
            # Assign a new value for this attribute if required.
            value = self._create_uuid()
            setattr(model_instance, self.attname, value)
        return value
 
    def to_python(self, value):
        if not value: return
        if not isinstance(value, uuid.UUID):
            value = uuid.UUID(value)
        return value
 
    def get_db_prep_save(self, value):
        if not value: return
        assert(isinstance(value, uuid.UUID))
        return value.hex
 
    def get_db_prep_value(self, value):
        if not value: return
        assert(isinstance(value, uuid.UUID))
        return unicode(value)
  • Thanks for sharing - this code works well for me! I'm also experimenting with just using a regular CharField and auto-populating the value using a post_init signal handler.
  • I'm using the following for Postgres 8.2.

    def db_type(self):
    return 'character varying(32)'

    def get_db_prep_value(self, value):
    if not value: return
    if not isinstance(value, uuid.UUID):
    value = uuid.UUID(value)
    return value.hex #unicode(value)
  • I originally started out using UUID as primary keys on my old project. I had some trouble with uuid imports in Python 2.5

    I seem to recall that if you for some reason try to import uuid in settings.py python will silently crash.

    Did you have any trouble like that?
  • Joe
    Ah, sorry, I need varchar(length) on Postgres... but it supports uuid anyway, so I'm using ---

    def db_type(self):
    return 'uuid'

    Although it seems (for me, at least) that get_db_prep_value assumes it's dealing with a uuid.UUID, when in the admin change view it's a unicode (and postgresql and other DBs seemingly want a 36 char string anyway...) so I'm using the following,

    def get_db_prep_value(self, value):
    if not value: return
    print type(value)
    if isinstance(value, uuid.UUID):
    return unicode(value)
    return value
  • Joe
    Yeah, for some reason my __init__ doesn't seem to be called in trunk.

    In other words with,

    class MyModel(models.Model):
    uuid = UUIDField(primary_key=True, db_index=True, auto=True)

    I get a character(1) in my DB (postgresql) and thus everything fails... so weird. I can even import the class and UUIDField() with a 1/0 in the init and nothing happens? I assume this has to do with the metaclass or something?
  • David
    Sorry the original code has been fixed.

    However, there seems to be an issue in Django where it's not quite passing in UUID instances everywhere. I'm currently looking into why.
  • Joe
    What is the variable "name" here (undefined)?

    elif version in (3, 5):
    self.namespace, self.name = namespace, name
  • trbs
    Any patched for useful improvements to the UUIDField in django-extensions would be much appreciated :)
  • Very nice. Do you know how this compares to the UUIDField in django extensions?

    http://code.google.com/p/django-command-extensi...
  • Brantley Harris
    Careful, the uuid module is not in 2.4 or below.
  • This one seems useful, thanks!
  • ah, good point. I was thinking in terms of using a UUIDField as a model's PK, without considering FK scenarios. Thanks again!
  • David
    No because it could just be a ForeignKey reference.
  • Interesting snippet!! Thanks for sharing.

    One question: shouldn't "kwargs['unique'] = True" be outside of the "if self.auto:" block?
blog comments powered by Disqus