2012-11-14 124 views
8

我有一个使用Pyramid/SQLAlchemy/Postgresql构建的Web应用程序,它允许用户管理一些数据,而且这些数据几乎完全独立于不同的用户。说,爱丽丝访问alice.domain.com,并能够上传图片和文件,鲍勃访问bob.domain.com,也能够上传图片和文件。 Alice从来没有看到任何由Bob创建的东西,反之亦然(这是一个简化的例子,真的可能有很多数据在多个表中,但想法是相同的)SQLAlchemy的多租户

现在,最直接的选择安排在数据库后端的数据是使用一个单一的数据库,其中每个表(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() 

这是所有容易和简单,但也有一些缺点

  • 我需要记住进行查询时,总是使用额外的过滤条件,否则爱丽丝可能会看到Bob的PI ctures;
  • 如果有许多用户表可能增长巨大
  • 它可能很难拆分所以我想这将是非常好的每莫名其妙地分割数据多台机器

之间的Web应用程序-用户。我能想到的两种方法:

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

    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 
    

    这似乎是最干净的解决方案,但我不知道如果有多个数据库连接,将需要更多的内存和其他资源,限制可能的数字“租户”。

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

+2

密切相关:http://stackoverflow.com/questions/9298296/ sqlalchemy-support-postgres-schemas –

+0

@CraigRinger:是的,如果从接受的答案中找到“SET search_path TO ...”thingie,那么这就是选项#1的解决方案。谢谢。 – Sergey

+1

如果你想避免将数据库分割,那么sqlalchemy.org上有一对关于[Pre-Filtered Queries](预过滤查询)的食谱(http://www.sqlalchemy.org/trac/wiki/UsageRecipes/PreFilteredQuery)和[全局过滤器](http://www.sqlalchemy.org/trac/wiki/UsageRecipes/GlobalFilter),可以帮助您避免不必要地拉取不希望的数据。 –

回答

2

好吧,我已经结束了在每一个请求的开始修改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() 

作品真的好吧,只要您将交易管理留给金字塔(即不要手动提交/回滚交易,让金字塔在请求结束时这样做) - 这是好,因为手动提交交易不是一个好方法。

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()]) 
+0

current_schema从哪里来? – synergetic

+1

'current_schema'是由'werkzeug.local.Local()'实例创建的代理。像'thread_locals = Local(); current_schema = thread_locals('schema')'。模式的当前值在请求开始时设置。这是一种将当前线程绑定到全局可访问值的便捷方式。 –

9

琢磨JD的回答后,我能达到相同的结果对PostgreSQL 9.2,SQLAlchemy的0.8和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()