[英]Multi-tenancy with SQLAlchemy
我有一個使用Pyramid / SQLAlchemy / Postgresql構建的Web應用程序,允許用戶管理一些數據,並且該數據幾乎完全獨立於不同的用戶。 說,Alice訪問alice.domain.com
並且能夠上傳圖片和文檔,Bob訪問bob.domain.com
並且還能夠上傳圖片和文檔。 Alice永遠不會看到Bob創建的任何內容,反之亦然(這是一個簡化的例子,真的可能在多個表中有很多數據,但想法是一樣的) 。
現在,在DB后端組織數據最直接的選擇是使用單個數據庫,其中每個表( pictures
和documents
)都有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的圖片和文檔設置單獨的表 (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
為每個用戶使用單獨的數據庫:
- 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.