简体   繁体   中英

How to bind different Mongoengine database aliases for Flask-Admin views?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM