Sometimes you might want to query a different database or schema because might be convenient to get data from two different sources.
Querying two different databases or schemas its not so uncommon as you might think and to this we can sometimes refer to multi-tenancy or simply two different data sources.
When implementing multi tenancy architecture there are many factors to consider and there are three common ways:
- Shared schemas - The data of all users are shared within the same schema and filtered by common IDs or whatever that is unique to the platform. This is not so great for GDPR (Europe) or similar in different countries.
- Shared database, different Schemas - The user's data is split by different schemas but live on the same database.
- Different databases - The user's data or any data live on different databases.
Edgy has three different ways of achieving this in a simple and clean fashion.
- Using the using in the queryset.
- Using the using_with_db in the queryset.
- Using the with_tenant as global.
You can also use the Edgy helpers for schemas if you need to use it.
Edgy by default provides a functionality that allows the creation of schemas in an easy way by
calling create_schema
in the tenancy module.
from edgy.core.tenancy.utils import create_schema
This function is very powerful and yet simple to use and allows the creation of schemas based on a given registry.
This means, if you have different registries pointing to different databases, you only need to
provide the registry
instance and some extra parameters to generate a brand new schema.
- registry - An instance of a Registry.
- schema_name - The name for the new schema.
- models - Optional dictionary containing the name of the Edgy Model as key and the model
class as value. The reason for being optional its because if not provided, it will generate the
tables from the
models
of theregistry
object. This allows the flexibility of you manipulating what tables should be passed into the new schema. - if_not_exists - If True, the schema will be created only if it does not already exist. Defaults to False.
- should_create_tables - If True, tables will be created within the new schema. Defaults to False.
Example
import edgy
from edgy.core.tenancy.utils import create_schema
database = edgy.Database("sqlite:///db.sqlite")
registry = edgy.Registry(database=database)
# Create the schema
await create_schema(
registry=registry,
schema_name="edgy",
if_not_exists=True,
should_create_tables=True
)
This is probably the one that is more commonly used and probably the one you will be using more often when querying different schemas or databases.
The using is simply an instruction telling to use this schema to query the data instead of the default set in the registry.
Parameters:
- schema - A string parameter with the name of the schema to query.
The syntax is quite simple.
<Model>.query.using(schema=<SCHEMA-NAME>).all()
This is not limited to the all()
at all, you can use any of the available query types
as well.
Let us assume we have two different schemas inside the same database and those schemas have a table
called User
.
- The schema
default
- The one that is automatically used if no schema is specified in the registry. - The schema
other
- The one we also want to query.
{!> ../docs_src/tenancy/using/schemas.py !}
Now we want to query the users from each schema.
Querying the default
As per normal approach, the query looks like this.
User.query.all()
Querying the main
Query the users table from the main
schema.
User.query.using(schema='main').all()
And that is it, really. Using its a simple shortcut that allows querying different schemas without a lot of boilerplate.
Now here it is where the things get interesting. What if you need/want to query a schema but from
a different database instead? Well, that is possible with the use of the using_with_db
.
{!> ../docs_src/shared/extra.md !}
This is another way to create a global tenant
for your application. Instead if using or
using_with_db you simply want to make sure that in your application you
want every request for a specific tenant
to always hit their corresponding tenant data.
This is specially useful for multi-tenant applications where your tenant users will only see their own data.
To use the with_tenant
you can import it via:
from edgy.core.db import with_tenant
!!! Tip
Use the with_tenant
in things like application middlewares or interceptors, right before
reaching the API.
!!! Warning
There is a function named set_tenant
. Because it doesn't set the scope correctly its use is not recommended and it is deprecated.
The with_tenant
can be somehow confusing without a proper example so let us run one 😁.
As usual, for this example Esmerald will be used. This can be applied to any framework of your choice of course.
What are we building:
- Models - Some models that will help us out mapping a user with a tenant.
- Middleware - Intercept the request and set the corresponding tenant.
- API - The API that returns the data for a given tenant.
Let us start with some models where we have a Tenant
, a User
model as well as a Product
where we will be adding some data for different tenants.
The TenantUser
model will serve as the link between a database schema (tenant) and the User
.
We will want to exclude some models from being created in every schema. The Tenant
on save it will
generate the schema
for a user in the database and it will automatically generate the database
models.
!!! Warning This is for explanation purposes, just do in the way you see fit.
{!> ../docs_src/tenancy/example/models.py !}
This is a lot to unwrap is it? Well, that was explained before at the top and this is just the declaration of the models for some general purposes.
Now it is time to generate some example data and populate the tables previously created.
{!> ../docs_src/tenancy/example/data.py !}
We now have models
and mock data for those. You will realise that we created a user
inside the
shared
database (no schema associated) and one specifically inside the newly edgy
schema.
It is time to create a middleware that will take advantage of our new models and tenants and set the tenant automatically.
The middleware will receive some headers with the tenant information and it will lookup if the tenant exist.
!!! Danger Do not use this example in production, the way it is done it is not safe. A real lookup example would need more validations besides a direct headers check.
{!> ../docs_src/tenancy/example/middleware.py !}
Now this is getting somewhere! As you could now see, this is where we take advantage of the with_tenant.
In the middleware, the tenant is intercepted and all the calls in the API will now query only
the tenant data, which means that there is no need for using
or using_with_db
anymore.
Now it is time to simply create the API that will read the created products from the database and assemble everything.
This will create an Esmerald application, assemble the routes
and add the
middleware created in the previous step.
{!> ../docs_src/tenancy/example/api.py !}
If you query the API, you should have similar results to this:
{!> ../docs_src/tenancy/example/query.py !}
The data generated for each schema (shared
and edgy
) should match
the response total returned.
By default a table will be generated for the non-tenancy db schema. If you don't want this,
you can set the flag in the meta of the tenant model register_default
to False.
As you could see in the previous step-by=step example, using the with_tenant can be extremely useful mostrly for those large scale applications where multi-tenancy is a must so you can actually take advantage of this.