When to leave the ORM

The ORM covers most work, and annotate, Func, Q and F go a long way. Reach for a custom field or raw SQL when you convert the same value over and over, need a database-specific feature, or have measured that the generated query is too slow.

NeedTool
Store a custom typeField subclass
Add a filter such as tags__hascustom Lookup
One SQL expression inside a querysetRawSQL, Func
Complex query that returns modelsModel.objects.raw()
Report, DDL, bulk writeconnection.cursor()

💡 Raw SQL is not portable across SQLite, PostgreSQL and MySQL. Cover it with tests and keep it in one place.

A custom Field

A field translates between a Python object and a database type. Inherit from the closest built-in field so the column type is ready, then override the conversions.

The value round trip
The value round trip
python
from django.db import models

class TagListField(models.TextField):
    description = "A list of tags stored as comma-separated text"

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return [t for t in value.split(",") if t]

    def to_python(self, value):
        if value is None or isinstance(value, list):
            return value
        return [t for t in value.split(",") if t]

    def get_prep_value(self, value):
        if value is None or isinstance(value, str):
            return value
        return ",".join(value)

class Post(models.Model):
    title = models.CharField(max_length=100)
    tags = TagListField(default=list)