# Creating Executors
As stated earlier, an Executor
is the object that will be responsible for running your queries. Go back to the earlier documentation about why the methods are usually empty and how to link Executor methods to SQL files.
In this section, we will look at some more customized features of Executors
and different ways to instantiate them.
# Loading and instantiating
Mayim provides a few flexible options for registering an Executor
. This should generally be done as early in the application as possible. For example, if you are running a web server, then this should ideally happen before the server starts to accept requests and not inside of a request handler.
# Loading a class
The simplest method is to load the class definition of your executor into your Mayim
definition.
from some.location.my.app import MyExecutor
async def run():
Mayim(
executors=[MyExecutor],
...
)
But, if you need to, you can also instantiate Mayim
without the executors
argument and come back to load your Executor
later.
from some.location.my.app import MyExecutor
async def run():
mayim = Mayim(...)
...
mayim.load(executors=[MyExecutor])
Feel free to mix and match as needed.
from some.location.my.app.users import UserExecutor
from some.location.my.app.products import ProductExecutor
from some.location.my.app.vendors import VendorExecutor
async def run():
mayim = Mayim(executors=[UserExecutor])
...
mayim.load(executors=[ProductExecutor, VendorExecutor])
# Loading an instance
In both the Mayim
constructor and the Mayim.load
method, you can also pass an Executor
instance.
from some.location.my.app.users import UserExecutor
from some.location.my.app.products import ProductExecutor
from some.location.my.app.vendors import VendorExecutor
async def run():
user_executor = UserExecutor()
product_executor = ProductExecutor()
vendor_executor = VendorExecutor()
Mayim(executors=[user_executor], ...)
...
mayim.load(executors=[product_executor, vendor_executor])
# Implied registration
On the frontpage example, we instantiated our executor before calling Mayim
, and never explicitly loaded the class (or instance). This implie registration is fine only if it is instantiated before the Mayim
object. Because of this "gotcha", this option is not the recommended approach.
# Global registration with @register
You also have the option of wrapping your Executor
instance with @register
. This will automatically add the Executor
to the registry without having to manually load it later. This is more explicit (and therefore preferred) than implied registration.
from mayim import PostgresExecutor, register
@register
class MyExecutor(PostgresExecutor):
async def select_something(self) -> Something:
...
Later on in your application, when you create Mayim
, you will not need to explicitly pass the Executor
class or instance.
async def run():
Mayim(...)
WARNING
Be careful with this method. If you use it you may need to pay close attention to your import ordering. That is because you will need to either: (1) instantiate Mayim
, or (2) run mayim.load
sometime after the Executor
has been imported.
If you are using @register
and your queries are not running as expected, a first place to check is to make sure that your imports are properly ordered.
# Fetching an Executor
Anytime after an Executor
has been loaded, you can fetch an executable instance:
from mayim import Mayim
from some.location.my.app import MyExecutor
executor = Mayim.get(MyExecutor)
This is a very helpful pattern to allow you to access Executor
instances in just about any part of your appication that you need to.
WARNING
Be careful about import ordering here. Although the example above places executor
in the global scope, that is not well advised. You are much better off placing it inside of some function that will be called by your application to avoid import and run time errors. For example, inside of a web handler endpoint.
@app.get("/foo")
async def handler(request: Request):
executor = Mayim.get(MyExecutor)
...
# In-line SQL queries
Sometimes you may decide that you do not want to have to load SQL from files. In this case, you can define the SQL in your Python code.
from mayim import PostgresExecutor, query
class ItemExecutor(PostgresExecutor):
@query(
"""
SELECT *
FROM items
WHERE item_id = $item_id;
"""
)
async def select_item(self, item_id: int) -> Item:
...
# Dynamic queries and raw execute
What if you need to generate some SQL and not use a predefined query? Mayim provides access to a lower-level API for this purpose. You should pass your generated SQL query to execute
.
class CityExecutor(PostgresExecutor):
async def select_city(self, ident: int | str, by_id: bool) -> City:
query = """
SELECT *
FROM city
"""
if by_id:
query += "WHERE id = $ident"
else:
query += "WHERE name = $ident"
return await self.execute(
query,
as_list=False,
allow_none=False,
params={"ident": ident}
)
FYI - as_list
defaults to False
. It is shown here just as an example that you may need to be explicit about passing this argument if you expect to return a list
. Once you have dropped down into executing your own code, you are responsible for telling Mayim if it needs to return a list
or a single instance. Similarly, allow_none
also defaults to False
and is shown for demonstration purposes here.
# Query fragments
What if you have some really big query fragments, or some fragment that needs to be used in a lot of places? For example, you might have a large select statement that you want to reuse. Wouldn't it be nice if you could define those in .sql
files and compose them? Of course!
class CityExecutor(PostgresExecutor):
generic_prefix: str = "fragment_"
async def select_city(self, ident: int | str, by_id: bool) -> City:
query = self.get_query("fragment_select_city")
if by_id:
query += self.get_query("fragment_where_id")
else:
query += self.get_query("fragment_where_name")
return await self.execute(query, params={"ident": ident})
# Low level run_sql
What if you need are not sure at run time what model to return? What if you need to dynamically determine what should be hydrated? Mayim also provides a lower-level API for running the SQL, and then hydrating it with a given model.
class CityExecutor(PostgresExecutor):
async def select_city_by_id(self, city_id: int):
query = self.get_query()
results = await self.run_sql(query.text, params={"city_id": city_id})
return self.hydrator.hydrate(results, City)
# Fetching query
In the previous example, you may have noticed query = self.get_query()
. This method allows you to fetch the predefined query that would have been executed. It is helpful in cases where you need to add some more custom logic to your method, but still want to preload your SQL from a .sql
file.
When you call get_query()
with no arguments, it will fetch the query that Mayim thinks should have been run. This is based upon the method name. You can explicitly pass it a name to retrieve that as seen in the query fragments section.
# Custom pools
Sometimes, you may find the need for an executor to be linked to a different database pool than other executors. This might be particularly helpful if you have multiple databases to query.
from mayim.interface.postgres import PostgresPool
from some.location.my.app.users import UserExecutor
from some.location.my.app.products import ProductExecutor
from some.location.my.app.vendors import VendorExecutor
async def run():
vendor_pool = PostgresPool("postgres://user@vendor.db:5432/db")
user_executor = UserExecutor()
product_executor = ProductExecutor()
vendor_executor = VendorExecutor(pool=vendor_pool)
Mayim(executors=[user_executor], dsn="postgres://user@main.db:5432/db")
...
mayim.load(executors=[product_executor, vendor_executor])
# Custom hydrators
There is more information about creating hydrators coming up next. But first, you should know that you can create your own custom hydrators that only operate on a specific Executor
.
from somewhere import CityHydrator
class CityExecutor(Executor):
async def select_all_cities(
self, limit: int = 4, offset: int = 0
) -> List[City]:
...
async def run():
city_executor = CityExecutor(hydrator=CityHydrator())
Mayim(executors=[city_executor], ...)
# Method specific hydrator
You can also define a hydrator that will only be used for a single method. This is done by wrapping the method with the @hydrator
decorator as shown here
from mayim import Mayim, Executor, Hydrator, hydrator
class HydratorA(Hydrator):
...
class HydratorB(Hydrator):
...
class SomeExecuto(Executor):
async def select_a(...) -> Something:
...
@hydrator(HydratorB())
async def select_b(...) -> Something:
...
Mayim(executors=[SomeExecutor(hydrator=HydratorA())])
# Fetching hydrators
Just like you can use self.get_query()
to have access to the SQL that would run for a method, you can use self.get_hydrator()
similarly. Let's rewrite that earlier example with a method-specific hydrator.
from mayim import Mayim, PostgresExecutor, Hydrator, hydrator
class CityHydrator(Hydrator):
...
class CityExecutor(PostgresExecutor):
@hydrator(CityHydrator())
async def select_city_by_id(self, city_id: int) -> List[City]:
query = self.get_query()
hydrator = self.get_hydrator()
results = await self.run_sql(query.text, params={"city_id": city_id})
return [hydrator.hydrate(city, City) for city in results]
# Empty methods in STRICT mode
By default, Mayim will startup in STRICT mode. You can disable this at instantiation, or load
:
mayim = Mayim(..., strict=False)
# OR
mayim.load(..., strict=False)
What does it do, and why would you use it? When enabled, Mayim will raise an exception when it encounters an empty Executor method without a defined query. So, if you have a method without any code inside of it, without a @query
decorator, and without a .sql
file, you will receive an exception.
# Loading this executor would raise an exception in STRICT mode
class CityExecutor(PostgresExecutor):
async def select_query_does_not_exist(self) -> None:
...