[英]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.