13

我有一个使用 Pyramid/SQLAlchemy/Postgresql 构建的 Web 应用程序,允许用户管理一些数据,并且这些数据对于不同的用户几乎是完全独立的。比如说,Alice 访问alice.domain.com并能够上传图片和文档,Bob 访问bob.domain.com并且也能够上传图片和文档。Alice 永远不会看到 Bob 创建的任何东西,反之亦然(这是一个简化的示例,多个表中可能确实有很多数据,但想法是一样的)

现在,在 DB 后端组织数据最直接的选择是使用单个数据库,其中每个表 (picturesdocuments) 都有user_id字段,因此,基本上,要获取 Alice 的所有图片,我可以执行类似的操作

user_id = _figure_out_user_id_from_domain_name(request)
pictures = session.query(Picture).filter(Picture.user_id==user_id).all()

这一切都很容易和简单,但是有一些缺点

  • 我需要记住在进行查询时始终使用附加过滤条件,否则 Alice 可能会看到 Bob 的图片;
  • 如果有很多用户,表格可能会变得很大
  • 在多台机器之间拆分 Web 应用程序可能会很棘手

所以我认为以某种方式拆分每个用户的数据会非常好。我可以想到两种方法:

  1. 在同一个数据库中为 Alice 和 Bob 的图片和文档设置单独的表(在这种情况下,Postgres 的模式似乎是一种正确的方法):

    documents_alice
    documents_bob
    pictures_alice
    pictures_bob
    

    然后,使用一些黑魔法,根据当前请求的域将所有查询“路由”到一个或另一个表:

    _use_dark_magic_to_configure_sqlalchemy('alice.domain.com')
    pictures = session.query(Picture).all()  # selects all Alice's pictures from "pictures_alice" table
    ...
    _use_dark_magic_to_configure_sqlalchemy('bob.domain.com')
    pictures = session.query(Picture).all()  # selects all Bob's pictures from "pictures_bob" table
    
  2. 为每个用户使用单独的数据库:

    - database_alice
       - pictures
       - documents
    - database_bob
       - pictures
       - documents 
    

    这似乎是最干净的解决方案,但我不确定多个数据库连接是否需要更多的 RAM 和其他资源,从而限制可能的“租户”数量。

所以,问题是,这一切都有意义吗?如果是,如何配置 SQLAlchemy 以在每个 HTTP 请求上动态修改表名(对于选项 1)或维护到不同数据库的连接池并为每个请求使用正确的连接(对于选项 2)?

4

3 回答 3

9

在思考 jd 的回答后,我能够为 postgresql 9.2、sqlalchemy 0.8 和 flask 0.9 框架实现相同的结果:

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()
于 2013-09-12T06:36:22.033 回答
4

好的,我最终search_path使用 Pyramid 的NewRequest事件在每个请求的开头进行了修改:

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()

效果非常好,只要您将事务管理留给 Pyramid(即不要手动提交/回滚事务,让 Pyramid 在请求结束时执行此操作) - 这是可以的,因为手动提交事务无论如何都不是一个好方法.

于 2013-01-05T00:07:40.437 回答
3

在连接池级别而不是在会话中设置搜索路径对我来说非常有效。此示例使用 Flask 及其线程本地代理来传递模式名称,因此您必须更改schema = current_schema._get_current_object()它并围绕它的 try 块。

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()

在引擎创建时:

engine = create_engine(dsn, listeners=[SearchPathSetter()])
于 2012-12-15T09:39:07.053 回答