DEV Community

Shen
Shen

Posted on • Originally published at shenli.dev

Seamlessly integrate Hashids with Django

Introduction

Hashids is a library that maps an integer to a string with provided salt, alphabet, and min_length. For example, it can turn integer 1 into "r87f" and convert it back when required.

It is a way to obfuscate ids and is particularly useful when you don't want anyone to iterate through everything in your database by going through all the ids.

I have used Hashids a few times with Django. And every time I need to expose the id field, I would convert the id value to a hashids by calling something like this:

from utils import hashids

def to_json(obj):
    exposed_id = hashids.encode(obj.id)
    ...
    return {
        'id': exposed_id,
        ...
    }

And everywhere that exposed_id is used I need to convert it back, which ended up as a lot of code in different places.

Another issue with this approach is that it's hard to use different configurations, such as salt and alphabet, for different models. The exposed_id for different models with the same actual id will be the same.

There are some existing projects that integrate the two, but they are more intrusive than I would like them to be. As they usually actually writes to the database, instead of just encode/decode between obfuscated id and integer ids on the fly.

This leads to this small library I made with less than 100 lines of code, called django-hashids.

django-hashids

django-hashids integrates Django with Hashids by introducing a "virtual" field to Django models. It is "virtual" because it does not have a column in the database but allows people to query it as if there were an actual database column.

Here's a simple example:

class TestModel(Model):
    hashid = HashidsField(real_field_name="id")

instance = TestModel.objects.create()
instance2 = TestModel.objects.create()
instance.id  # 1
instance2.id  # 2

# Allows access to the field
instance.hashid  # '1Z'
instance2.hashid  # '4x'

# Allows querying by the field
TestModel.objects.get(hashid="1Z")
TestModel.objects.filter(hashid="1Z")
TestModel.objects.filter(hashid__in=["1Z", "4x"])
TestModel.objects.filter(hashid__gt="1Z")  # same as id__gt=1, would return instance 2

# Allows usage in queryset.values
TestModel.objects.values_list("hashid", flat=True) # ["1Z", "4x"]
TestModel.objects.filter(hashid__in=TestModel.objects.values("hashid"))

As you can see, it allows you to use TestModel.hashid like a real field with all the sensible lookups but queries are proxies to id field with encoding/decoding happening on the fly providing a seamless experience.

For more usage and configuration options please visit django-hashids on github

Top comments (0)