2012-11-14 22 views
8

私はPyramid/SQLAlchemy/Postgresqlで構築され、ユーザがいくつかのデータを管理でき、そのデータはユーザごとにほとんど完全に独立しているWebアプリケーションを持っています。アリスはalice.domain.comにアクセスして画像や文書をアップロードし、ボブはbob.domain.comにアクセスし、画像や文書をアップロードすることもできます。アリスはボブが作成したものは一切見ることができません。逆の場合はです(これは単純な例ですが、実際には複数のテーブルに多数のデータがあるかもしれませんが、アイデアは同じです)SQLAlchemyを使ったマルチテナント

さて、DBのバックエンドでデータを整理する最も簡単なオプションは、すべてのアリスの画像を取得するために、各テーブル(picturesdocumentsは)基本的には、user_idフィールドを持っているので、単一のデータベースを使用することです、私が行うことができます

user_id = _figure_out_user_id_from_domain_name(request) 
pictures = session.query(Picture).filter(Picture.user_id==user_id).all() 

のようなもの。これは、すべての簡単でシンプルです、しかし、いくつかの欠点がある

  • 私がそうでなければ、アリスがボブのパイを見て、クエリを作成するときは、常に追加のフィルタ条件を使用することを覚えておく必要がありますctures;
  • テーブルが
  • 巨大な成長する可能性が多くのユーザーが存在する場合だから私はそれを何とかあたりのデータを分割するために、本当にいいだろうと思っています複数のマシン

の間でWebアプリケーションを分割するのが難しいかもしれ-ユーザー。私は2つのアプローチを考えることができます。

  1. は、同じデータベース内の別ののテーブルアリスのためのとボブの写真や文書を持っている(PostgresのSchemasが、この場合に使用する正しいアプローチのようです)現在のリクエストのドメインに応じて

    documents_alice 
    documents_bob 
    pictures_alice 
    pictures_bob 
    

    、その後、いくつかの黒魔術を使って、「ルート」1や他のテーブルにすべてのクエリ:

    _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や他のリソースを必要とするかどうかわからないんだけど、可能性の数を制限しますテナント "と呼ばれる。

したがって、問題はすべて意味がありますか?はいの場合、各HTTP要求(オプション1)でテーブル名を動的に変更するか、または異なるデータベースへの接続プールを維持し、各要求(オプション2)に対して正しい接続を使用するようにSQLAlchemyを構成するにはどうすればよいですか?

+2

:http://stackoverflow.com/questions/9298296/をsqlalchemy-support-of-postgres-schemas –

+0

@CraigRinger:はい、受け入れられた回答の "SET search_path TO ..."がうまくいけば、それはオプション#1の解決策になります。ありがとう。 – Sergey

+1

バットからデータベースをシャードするのを避けたい場合は、sqlalchemy.orgの[Pre-Filtered Queries](http://www.sqlalchemy.org/trac/wiki/UsageRecipes/PreFilteredQuery)のレシピがあります。および[Global Filters](http://www.sqlalchemy.org/trac/wiki/UsageRecipes/GlobalFilter)を参照してください。 –

答えて

2

[OK]を、私はピラミッドの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() 

作品本当にピラミッドにトランザクション管理を任せている限り(すなわち、トランザクションを手動でコミット/ロールバックしないで、ピラミッドに要求の最後にそれをさせる) - トランザクションを手動でコミットすることは、とにかく良いアプローチではありません。

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( 'スキーマ') '。スキーマの現在の値は、要求の開始時に設定されます。グローバルにアクセス可能な値を現在のスレッドに結び付ける便利な方法です。 –

9

はJDの答えに熟考した後、私は、SQLAlchemyの0.8 PostgreSQLの9.2のための同じ結果を得ることができた、と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() 
関連する問題