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
which integrates money in the Django ORM, including forms, admin integration, template tags, DRF integration, currency
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
__delete__ accessors to update
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
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.
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
__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.
Migrations: By implementing
@deconstructible, you'll add migration support.
Serialization: To support fixtures, you'll have to define a
Serializer class and a
plus support in
get_prep_value. Register your serialization support via app_config with
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.
- 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!