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
@overridesdecorator to customise individual fields.For more complex cases you can use the
@specdecorator to provide the readers spec directly.By combining a manual
@specwith a customcattrsconverter, passed toQuery, 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.