SQLAlchemy的多租户

Ser*_*gey 10 python postgresql sqlalchemy multi-tenant

我有一个使用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()
Run Code Online (Sandbox Code Playgroud)

这一切都很简单,但也存在一些缺点

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

所以我认为以某种方式分割每个用户的数据真的很好.我可以想到两种方法:

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

    documents_alice
    documents_bob
    pictures_alice
    pictures_bob
    
    Run Code Online (Sandbox Code Playgroud)

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

    _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
    
    Run Code Online (Sandbox Code Playgroud)
  2. 为每个用户使用单独的数据库:

    - database_alice
       - pictures
       - documents
    - database_bob
       - pictures
       - documents 
    
    Run Code Online (Sandbox Code Playgroud)

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

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

syn*_*tic 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()
Run Code Online (Sandbox Code Playgroud)


Ser*_*gey 4

search_path好吧,我最终使用金字塔的事件在每个请求的开头进行了修改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()
Run Code Online (Sandbox Code Playgroud)

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