DjangoCon Europe 2019: Building a custom model field from the ground up

Writeup of the DjangoCon Europe 2019 talk »Building a custom model field from the ground up« by Dmitry Dygalo

Dmitry Dygalo: Dmitry is a Tech Lead at kiwi.com in Prague. Started with Python in 2010 as a hobby during the university, he switched to a fulltime developer job after the graduation. He loves writing tests and cares about code maintainability. Hobbies: OpenSource and traveling.

Custom model fields: We want to map custom types to DB fields and interact with them! Le's look at django-money, which integrates money in the Django ORM, including forms, admin integration, template tags, DRF integration, currency rates.

Storage

Money consists of an amount as a decimal and a currency as a string. It includes arithmetic feeatures and localization for formatting currency output. We provide a MoneyField to do that. When creating a custom field, start with test cases (or use built-in test cases) for create/update/delete actions.

We have different storage options: separate fields (numeric and VARCHAR(3)) for amounts and strings are SQL compliant, but hard to manage. On the Python side, we'd use __get__, __set__ and __delete__ accessors to update attributes in __dict__.

An alternative are structured types, which is SQL compliant, and is a usual and good way of implementing custom fields in Django. This is not supported in MySQL, and has some overhead when accessing attributes. We'll need to implement from_db_value and get_prep_value to split up strings, and put them together again.

Summing up, this stage involves designing interfaces, discovering db queries and dbms support. Note: one field is simpler than multiple fields. Go for structured types if you can.

Queries

In terms of queries, we will need to support lookups, transformations, and expressions.

Reuse the code from existing lookups!

@MoneyField.register_lookup
class MoneyGreaterThan(models.lookups.GreaterThan)

    def as_sql(self, compiler, connection):
        return (
            # Insert SQL
        )

Add lookups for your field parts (money__currency='EUR'), for example. Basic expressions will work out of the box. For everything else, e.g. F expressions support, implement _add__, __sub__, __mul__, etc. on your model. And then implement custom expressions.

Summing up, this stage involves defining lookups and transformation behaviour unambiguously, mapping them to DB queries, and using the Lookup API for fun and profit (to build the desired queries). Extend magic methods on your entities for F support, and create custom expressions for your structured types, if you need them.

Extras

Migrations: By implementing deconstruct or @deconstructible, you'll add migration support.

Serialization: To support fixtures, you'll have to define a Serializer class and a Deserializer callable, plus support in get_prep_value. Register your serialization support via app_config with serializers.register_serializer in ready().

Validation: Subclass existing django.core.validator classes to add Max/min/whatever validators.

Admin: Structured types will pretty much just work out of the box – for other options, you'll have to implement some tricky hacks.

Summing up: Extend existing tools from Django. Use AppConfig to register your extensions, and think about which use cases are actually required for your field.

Summary

  • Always start with the interface design and use it for tests.
  • Explore underlying DB queries in different engines.
  • Experiment with your implementation, choose the most simple and unambiguous.
  • Try to evaluate possible consequences of your chosen approach.
  • Django provides a lot of support for your modifications – make use of them!