簡體   English   中英

SQLAlchemy的多租戶

[英]Multi-tenancy with SQLAlchemy

我有一個使用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的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 

    這似乎是最干凈的解決方案,但我不確定多個數據庫連接是否需要更多的RAM和其他資源,限制了可能的“租戶”數量。

所以,問題是,這一切都有意義嗎? 如果是,我如何配置SQLAlchemy以在每個HTTP請求上動態修改表名(對於選項1)或者維護到不同數據庫的連接池並為每個請求使用正確的連接(對於選項2)?

在思考了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()

對我來說,在連接池級別而不是在會話中設置搜索路徑非常有效。 此示例使用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()])

好的,我最終使用Pyramid的NewRequest事件在每個請求的開頭修改了search_path

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在請求結束時這樣做) - 這是好的,因為手動提交事務不是一個好方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM