Customising query generation¶

For both flat and nested data, Mantle will automatically generate the ORM queries you need. It leverages django-readers in order to do this.

Mantle’s role is to generate the required readers spec for your attrs shape classes, and then map the returned data back to attrs instances using cattrs.

Make sure you’ve read How Mantle Works to understand that flow completely.

This page explains how to customise query generation.

In summary:

  • In the majority of cases you use the @overrides decorator to customise individual fields.

  • For more complex cases you can use the @spec decorator to provide the readers spec directly.

  • By combining a manual @spec with a custom cattrs converter, passed to Query, you can enable advanced data reshaping if (or when) that need arrises.

Examples¶

The following examples range from simple customisations to a much more complex data reshaping case. They assume you’re familiar, in particular, with django-readers pairs.

You can likely work out the simpler examples as you go. The final example might require a little more.

Again, see How Mantle Works if you’re unsure.

Mapping a field value¶

Probably the most basic use of @overrides is to adjust the value produced from the ORM to apply a custom method or transform.

A Django FileField is a good example. Here we want to populate our data shape with the filename for an attachement’s file field:

from attrs import define
from django_readers import qs
from mantle import overrides

@overrides(
    {"file": (qs.include_fields("file"), lambda a: a.file.name)}
)
@define
class Attachment:
    id: int
    name: str
    file: str  # Django file.name relative to media root

We override the file prepare-produce pair to include the file field and then extract the field name from the ORM instance.

Annotations¶

Another example is providing ORM annotations, either for aggregations or to include a field value from a parent model:

from attrs import define
from django_readers import producers, qs
from mantle import overrides, Query

@overrides({
    "username": (
        qs.annotate(username=F("user__username")),
        producers.attr("username"),
    )
})
@define
class BookmarkData:
    id: int
    title: str
    favourite: bool
    username: str

Here we want to include the username for the parent user for our Bookmark. We annotate the QuerySet at the prepare phase, and then use that value for our final data.

Disable a field¶

Sometimes we might have an attrs field that is set in application code, and not via the data from the ORM. In this case we can exclude the field from the readers spec by setting the override to None:

@overrides({
    "related_bookmarks": None,
})
@define
class BookmarkData:
    ...

    related_bookmarks: List[BookmarkData] = field(default=list)

Here related_bookmarks will initially be an empty list, provided by the field default, and would be set subsequently by your application.

Reshaping data¶

Finally, for a much more complex example, we may wish to reshape our data significantly, providing a manual spec and code to map that to our data shape.

Imagine an Django User model, with a related Profile, that has an avatar_image ImageField.

We want to map that to a flat UserInfo shape with only name, email, and avatar_url attributes.

Our problem is that readers specs naturally follow the model relationships, so the raw projected data is still nested. Our task is to flatten it.

1. Define a model-shaped spec on your target class¶

Here we pass a readers spec manually using the @spec decorator:

from attrs import define
from django_readers import qs
from mantle.readers import spec


@spec([
    {
        "name": (
            qs.include_fields("first_name", "last_name"),
            lambda user: user.get_full_name(),
        )
    },
    "email",
    {
        "profile": [
            {
                "avatar_image": [
                    "url",
                ]
            }
        ]
    },
])
@define
class UserInfo:
    name: str
    email: str
    avatar_url: str

The name field is using the lambda technique again to call the User model’s get_full_name() method.

The email field is just a plain string, which is the readers shortcut for “include this field, and then select it”.

The profile field is a nested spec for the profile reaching in the the avatar_image and the url property of that. Coming out of django-readers, the data for the profile will be a nested dictionaries down to the url field we’re interested in.

2. Add a classmethod to reshape spec output¶

In order to flatten the data we add a classmethod to create our UserInfo instance from the expected data:

@spec(...)  # Not shown again here
@define
class UserInfo:
    name: str
    email: str
    avatar_url: str

    @classmethod
    def from_spec_data(cls, data: dict) -> "UserInfo":
        profile = data.get("profile") or {}
        avatar_image = profile.get("avatar_image") or {}
        return cls(
            name=data["name"],
            email=data["email"],
            avatar_url=avatar_image.get("url", ""),
        )

Here from_spec_data defensively extracts the avatar_url we need.

3. Use a custom cattrs converter to structure the data¶

Now we need to register our new method with cattrs so it knows to use the classmethod for structuring UserInfo instances.

This is a technique described in the cattrs docs here: Using class-specific structure and unstructure methods.

from mantle.db import make_default_converter


user_info_converter = make_default_converter()


@user_info_converter.register_structure_hook
def structure_user_info(data: dict, _) -> UserInfo:
    return UserInfo.from_spec_data(data)

We start from Mantle’s make_default_converter, which defines structure hooks for common Django value types, including date, datetime, time, timedelta, UUID, and Decimal.

4. Fetch with Query¶

Finally, we fetch with Query, making sure use our custom converter:

from mantle import Query
from django.contrib.auth.models import User


user_info = Query(
    User.objects.filter(pk=user_id),
    UserInfo,
    converter=user_info_converter,
).get()

By using @overrides or by defining the spec manually with @spec, and by customising the cattrs converter, you have full control over the query generation and structuring process.