Developing a web admin interface for an information system using Flask-Admin and Mongoengine, I need Flask-Admin's ModelView
s for all my entities. The system uses several MongoDB databases. Let's assume there's two of them for the sake of clarity.
Normally, people manage such behavior using Mongoengine's database aliases. During initialization, we define several aliases for our Flask app using Flask-Mongoengine's configuration:
from mongoengine import DEFAULT_CONNECTION_NAME
# Local packages
from config import CurrentConfig
SECOND_DB_ALIAS = "second_db"
app.config['MONGODB_SETTINGS'] = [
{
"ALIAS": DEFAULT_CONNECTION_NAME,
"DB": CurrentConfig.DATABASE_NAME,
},
{
"ALIAS": SECOND_DB_ALIAS,
"DB": CurrentConfig.SECOND_DATABASE_NAME,
},
]
Now we can use Document
's meta
field, which binds the database (represented by its alias) to a particular entity:
class Entity(Document):
field = StringField()
meta = {'db_alias': SECOND_DB_ALIAS}
Unfortunately, it doesn't suit my needs here, since the same entities (represented by the same Document
class) can be present in both databases. I want to set the database I query against depending on the logic of the app.
Well, whatever. We still can switch databases dynamically using Mongoengine's context managers:
with switch_db(Entity, SECOND_DB_ALIAS):
Entity(field="value").save()
(Notice: unfortunately, it's not thread-safe at the moment of writing this question)
That's what I do in the rest of the application. The problem is that I can't find a way to do the same in my Flask-Admin's ModelView
s. How to set the alias of the database to query against in this situation?
class EntityView(ModelView):
can_delete = True
can_edit = True
can_view_details = True
can_create = True
can_export = True
# No such or similar attribute!
database_alias = SECOND_DB_ALIAS
def __init__(self):
super().__init__(Entity, name="Entities")
admin = Admin(app, name='Admin Panel', template_mode='bootstrap3')
admin.add_view(EntityView())
Solved it.
Spent some time examining ModelView
's source code, and, indeed, nothing like this was implemented. Well, had to roll up the sleeves.
We have to wrap all queries to the database with switch_db
context manager. The Flask-Admin documentation contains a list of methods needed to implement a model backend. Thus, if any database query takes place, it's there.
By examining these methods' implementation in ModelView
, we can find out that Mongoengine queries can be executed only in get_list
, get_one
, create_model
, update_model
, and delete_model
methods.
Now we derive from ModelView
and wrap those methods with needed context manager:
class SwitchableModelView(ModelView):
database_alias = DEFAULT_CONNECTION_NAME
# Override query methods to add database switchers
def get_list(self, *args, **kwargs):
with switch_db(self.model, self.database_alias):
# It's crucial that the query gets executed immediately,
# while in the switch_db context,
# so we need to override the `execute` argument.
kwargs['execute'] = True
return super().get_list(*args, **kwargs)
def get_one(self, *args, **kwargs):
with switch_db(self.model, self.database_alias):
return super().get_one(*args, **kwargs)
def create_model(self, *args, **kwargs):
with switch_db(self.model, self.database_alias):
return super().create_model(*args, **kwargs)
def update_model(self, *args, **kwargs):
with switch_db(self.model, self.database_alias):
return super().update_model(*args, **kwargs)
def delete_model(self, *args, **kwargs):
with switch_db(self.model, self.database_alias):
return super().delete_model(*args, **kwargs)
Then we can switch the database in our views like this:
class EntityView(SwitchableModelView):
can_delete = True
can_edit = True
can_view_details = True
can_create = True
can_export = True
# Now it works!
database_alias = SECOND_DB_ALIAS
def __init__(self):
super().__init__(Entity, name="Entities")
If database_alias
is omitted, default connection still will be used, resulting in vanilla ModelView
's behaviour.
I tested it. Works.
Although, I have some concerns about this code's efficiency and reliability. As I mentioned, switch_db
is not thread-safe at the moment. The database is switched on the whole Entity
class when entering and leaving the context. So, I'm not sure how it will behave under high load in multi-threaded Flask application and if there's going to be race condition problems.
If anyone comes up with a better approach to the problem or with any improvements to this code, I'd be happy to hear.