Repository navigation
Pattern for Declarative Use of Gino to Connect to Multiple Databases #209
Description
- GINO version: 0.7.0
- Python version: 3.5
- Operating System: macOS
Description
We are building a multi-tenant web application using PostGreSQL, Sanic using asyncio semantics. Essentially each tenant has their own distinct database and so the app, depending on who the request is for, must connect to the appropriate database and perform database calls.
Each Sanic request could then potentially access a completely different database. I'd like to use a declarative type interface with Models.
What is the appropriate pattern for this. I may have N different databases to connect to.
What I Did
Assuming I have a separate engine object for each client, I see how I can use db.bind to change the engine currently being used by the model.
Below is what I have so far. Note I define models separately in distinct files for code organization. But I'm not sure if this is the right approach because:
dbobject for a declared model is set before await, and we are waiting for a result and processing of other requests will continue.- While we are waiting for the result we may have passed and control to another Sanic request for a different customer, and in the class, the global
dbobject may have a different object set, (and thus will use a different database).
# m_company.py
import asyncio
from gino import Gino
db = Gino()
class Company(db.Model):
__tablename__ = 'companys'
id = db.Column(db.Integer(), primary_key=True)
company_name = db.Column(db.Unicode(), default='noname')
@classmethod
def bind_engine(cls, engine):
global db
db.bind
# m_user.py
import asyncio
from gino import Gino
db = Gino()
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.Unicode(), default='noname')
@classmethod
def bind_engine(cls, engine):
global db
db.bind
Something like this must be done to set engine to points to appropriate database, before a query is carried out:
# code which uses declarative model pattern
from m_user import User
class Main:
# assume different engine per customer
engine_customer_db1 = #... engine for connection to customer 1 database
engine_customer_db2 = #... engine for connection to customer 2 database
# this method is called to get the appropriate engine for a particular customer
@classmethod
def get_customer_engine(cls, customer_id):
# ... assume it returns the appropriate engine depending on customer id
# this method is called by various Sanic requets
@classmethod
async def get_customer_users(cls, customer_id):
User.bind_engine(Main.get_customer_engine(customer_id))
users = await User.get(1)
return users
I've written this code for illustration.
Really what I'm looking for is feedback or a suggestion on if this is appropriate - I'm worried that by binding different engines per request, I may have a database query that is affected by the database engine being modified between requests. Is there a declarative approach that is more appropriate for my use case?
It looks like for SQLAlchemy other's have asked similar questions on stack overflow:
https://stackoverflow.com/questions/13372001/multi-tenancy-with-sqlalchemy
It is important to note that in my application tenancy can change dynamically while the server is running. At one moment we may have n customers, and another we may have n+1 or n-1 customers. So a solution for me wouldn't be statically specifying databases
Thanks in advance.
Activity
mashaalmemon commentedon Apr 23, 2018
Some more thoughts here as I investigate. I see that you can bind an engine to crud functions like so:
However it is not clear how to set the bound engine for a query or for other chainable API calls:
fantix commentedon Apr 24, 2018
Before everything, I would suggest use a single
dbinstance overall so that all tables share the sameMetaData(db), instead of severaldbinstances for each table unless you have a reason to do so.To achieve your goal, the first solution is the same as you investigated - to use engines explicitly without binding them to
db. That is to say, implicit execution (chainable.gino.all()) is not an option - instead you should directly useengine.all():This way, different concurrent coroutines won't interfere with each other, because engines are always explicitly specified.
A second solution is a bit more complex and implicit, but cleaner in code. This requires overriding
Ginoto have a dynamic binding:(https://github.com/fantix/aiocontextvars)
Then set and clear the context value in a common place once for all, like Sanic request and response middleware:
With such, you would be able to directly use GINO to access database without having to worry about the actual engine to use in any context of Sanic request (be careful about websockets):
fantix commentedon May 2, 2018
Closing for now, please feel free to reopen for further discussion.
thomas-maschler commentedon Apr 16, 2020
Thanks for posting this pattern @fantix .
I have a similar use case as mashaalmemon, only that I want to use different engines, depending on the Request method. I use AWS Aurora, and for get requests, I would like to point to the read only endpoint (basically a load balancer which forwards requests to the different read replicas). For Put/Post/Patch/Delete requests, I'd like to point to the write endpoint, which forwards requests to the writer node of the cluster.
I use
I tried overriding the Gino class and implement a middleware as suggested below
However, I get an error
Any idea why this doesn't work?
thomas-maschler commentedon Apr 16, 2020
Sorry, I could fix this issue by adding a property setter
However, this still doesn't work for me.
I now get an error
I do not use async/ await to create my engine or bind. Why is bind now a coroutine?
fantix commentedon Apr 16, 2020
https://python-gino.org/docs/en/master/explanation/engine.html#creating-engines
The SQLAlchemy strategy of GINO makes
create_enginereturn a coroutine, you need to await on it to get the engine.thomas-maschler commentedon Apr 16, 2020
Ok, thanks.
I think the issue is, that other packages try to access the bind property synchronously, not as a coroutine. To fix the error message above, I would need to update gino_starlette to use
extra={"color_message": msg + (await self.bind).repr(color=True)}And then also any other function which tries to us
bind. Is there a better way to address this issue?fantix commentedon Apr 16, 2020
No,
self.bindmust not be a coroutine - that means you should await oncreate_engine(). Please see my example below:thomas-maschler commentedon Apr 16, 2020
BOOM!
Thanks, this works.
yaniaular commentedon Jun 22, 2020
@fantix This issue thread it's very helpfully thanks, however, I have a question about create method with a specific engine.
I am using a single DB instance, but, I have a thread in python (in a different loop) where I need to use a specific engine. (I am using a thread to keep open a RabbitMQ consumer)
When I receive a event from rabbit I need to make queries to DB (including create). The solution that you said above works for me, I made the following:
However, I have not been able to use a specific engine when I want to create a new record in my database. I would like to know if Is it possible to make some like following?:
yaniaular commentedon Jun 22, 2020
@fantix I made this and It's works for me,
what do you think?, is there a way more cleaner in code?