# Custom Hydrators

A Hydrator is an object that is responsible for turning raw data into a single instance of some model. Out of the box, Mayim works with dataclasses and other packages (like Pydantic) where the following pattern is acceptable:

instance = model(**data)

If you need to support other types of models, or add other business logic to the hydration process, then you can create custom hydrators.

# For an Executor

In the last section, we showed that it is possible to attach a Hydrator to an Executor.

city_executor = CityExecutor(hydrator=CityHydrator())

But, what could the CityHydrator look like?

from typing import Any, Dict, Type
from dataclasses import dataclass

@dataclass
class City:
    id: int
    name: str
    countrycode: str
    district: str
    population: int


class CityHydrator(Hydrator):
    def hydrate(
        self, data: Dict[str, Any], model: Type[object] = City
    ) -> City:
        data["population"] = round(data["population"] / 1_000_000, 2)
        return super().hydrate(data, model)

In this simple example, rather than returning the actual population in the database, we will return it in units of 1 million people.

As you can see, the hydrate method could return ANYTHING you want it to. So if you need to support some other kind of model, all you need to do is create a hydrator that knows how to turn a Dict[str, Any] into the object type of your choice.

# As an async method

Sometimes you may decide that you need hydrate to be an async method. That is acceptable. Notice how we use async def hydate in the below example.

class CityExecutor(PostgresExecutor):
    async def select_city_by_id(self, city_id: int) -> City:
        ...

class CountryExecutor(PostgresExecutor):
    async def select_country_by_country_code(
        self, country_code: str
    ) -> Country:
        ...

class CountryHydrator(Hydrator):
    def __init__(self, city_executor: CityExecutor):
        self.city_executor = city_executor

    async def hydrate(
        self, data: Dict[str, Any], model: Type[Country] = Country
    ) -> Country:
        capital = data.pop("capital")
        data["capital"] = await self.city_executor.select_city_by_id(capital)
        return super().hydrate(data, Country)

async def run():
    city_executor = CityExecutor()
    country_executor = CountryExecutor(hydrator=CountryHydrator(city_executor))
    Mayim.load(
        executors=[
            city_executor,
            country_executor,
        ]
    )
    ...
    print(await country_executor.select_country_by_country_code("ISR"))

WARNING

This example is illustrative, but potentially dangerous. By running another Executor inside of hydrate we are running two queries for every one. If you intend to do this, make sure you know about N+1 operations.

Honestly, this is actually somewhat illustrative of exactly why this library exists. You can create more efficient queries than what traditional ORMs can handle if you write raw SQL. Mayim then makes it easy to take all that hard work and hydrate them into objects. Go nuts and create as complicated a query as you need.

# Fallback hydrator

What if you need to support some type of model that does not take keyword arguments in the constructor? Or, what if you need to perform some logic for every model? Mayim will also allow you to create a custom Hydrator on the Mayim instance instead of having to pass it to every Executor individually.

class MyHydrator(Hydrator):
    ...

async def run()
    Mayim(hydrator=MyHydrator(), ...)