Multi-tenancy with SQLAlchemy

后端 未结 3 419
被撕碎了的回忆
被撕碎了的回忆 2020-12-28 09:36

I\'ve got a web-application which is built with Pyramid/SQLAlchemy/Postgresql and allows users to manage some data, and that data is almost completely independent for differ

相关标签:
3条回答
  • 2020-12-28 10:06

    What works very well for me it to set the search path at the connection pool level, rather than in the session. This example uses Flask and its thread local proxies to pass the schema name so you'll have to change schema = current_schema._get_current_object() and the try block around it.

    from sqlalchemy.interfaces import PoolListener
    class SearchPathSetter(PoolListener):
        '''
        Dynamically sets the search path on connections checked out from a pool.
        '''
        def __init__(self, search_path_tail='shared, public'):
            self.search_path_tail = search_path_tail
    
        @staticmethod
        def quote_schema(dialect, schema):
            return dialect.identifier_preparer.quote_schema(schema, False)
    
        def checkout(self, dbapi_con, con_record, con_proxy):
            try:
                schema = current_schema._get_current_object()
            except RuntimeError:
                search_path = self.search_path_tail
            else:
                if schema:
                    search_path = self.quote_schema(con_proxy._pool._dialect, schema) + ', ' + self.search_path_tail
                else:
                    search_path = self.search_path_tail
            cursor = dbapi_con.cursor()
            cursor.execute("SET search_path TO %s;" % search_path)
            dbapi_con.commit()
            cursor.close()
    

    At engine creation time:

    engine = create_engine(dsn, listeners=[SearchPathSetter()])
    
    0 讨论(0)
  • 2020-12-28 10:11

    After pondering on jd's answer I was able to achieve the same result for postgresql 9.2, sqlalchemy 0.8, and flask 0.9 framework:

    from sqlalchemy import event
    from sqlalchemy.pool import Pool
    @event.listens_for(Pool, 'checkout')
    def on_pool_checkout(dbapi_conn, connection_rec, connection_proxy):
        tenant_id = session.get('tenant_id')
        cursor = dbapi_conn.cursor()
        if tenant_id is None:
            cursor.execute("SET search_path TO public, shared;")
        else:
            cursor.execute("SET search_path TO t" + str(tenant_id) + ", shared;")
        dbapi_conn.commit()
        cursor.close()
    
    0 讨论(0)
  • 2020-12-28 10:16

    Ok, I've ended up with modifying search_path in the beginning of every request, using Pyramid's NewRequest event:

    from pyramid import events
    
    def on_new_request(event):
    
        schema_name = _figire_out_schema_name_from_request(event.request)
        DBSession.execute("SET search_path TO %s" % schema_name)
    
    
    def app(global_config, **settings):
        """ This function returns a WSGI application.
    
        It is usually called by the PasteDeploy framework during
        ``paster serve``.
        """
    
        ....
    
        config.add_subscriber(on_new_request, events.NewRequest)
        return config.make_wsgi_app()
    

    Works really well, as long as you leave transaction management to Pyramid (i.e. do not commit/roll-back transactions manually, letting Pyramid to do that at the end of request) - which is ok as committing transactions manually is not a good approach anyway.

    0 讨论(0)
提交回复
热议问题