2012-11-14 126 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()