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