13

私は、Pyramid/SQLAlchemy/Postgresql で構築され、ユーザーがいくつかのデータを管理できるようにする Web アプリケーションを持っています。そのデータは、さまざまなユーザーに対してほぼ完全に独立しています。たとえば、Alice が訪問alice.domain.comして写真とドキュメントをアップロードでき、Bobbob.domain.comも訪問して写真とドキュメントをアップロードできるとします。Alice は Bob によって作成されたものを見ることはなく、その逆も同様です (これは単純化された例であり、実際には複数のテーブルに多くのデータが存在する可能性がありますが、考え方は同じです)

さて、DB バックエンドでデータを整理する最も簡単なオプションは、各テーブル (picturesおよびdocuments) にuser_idフィールドがある単一のデータベースを使用することです。したがって、基本的に、アリスのすべての写真を取得するには、次のようなことができます。

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

これはすべて簡単でシンプルですが、いくつかの欠点があります

  • クエリを作成するときは常に追加のフィルター条件を使用することを覚えておく必要があります。そうしないと、アリスがボブの写真を見る可能性があります。
  • 多くのユーザーがいる場合、テーブルが巨大になる可能性があります
  • Web アプリケーションを複数のマシンに分割するのは難しい場合があります

だから、どういうわけかユーザーごとにデータを分割するのは本当にいいと思います。私は2つのアプローチを考えることができます:

  1. 同じデータベース内にアリスとボブの写真とドキュメント用に別々のテーブルを用意します (この場合、Postgres のスキーマを使用するのが正しいアプローチのようです)。

    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 やその他のリソースを必要とし、可能な「テナント」の数が制限されるかどうかはわかりません。

それで、問題は、それはすべて理にかなっていますか? はいの場合、HTTP リクエストごとに動的にテーブル名を変更する (オプション 1 の場合)、または異なるデータベースへの接続のプールを維持し、各リクエストに正しい接続を使用する (オプション 2 の場合) ように SQLAlchemy を構成するにはどうすればよいですか?

4

3 に答える 3

9

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()
于 2013-09-12T06:36:22.033 に答える
4

OK、search_pathPyramid の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()

トランザクション管理を Pyramid に任せている限り (つまり、トランザクションを手動でコミット/ロールバックせず、要求の最後に Pyramid にそれを行わせる) 限り、非常にうまく機能します - いずれにせよトランザクションを手動でコミットすることは良いアプローチではないので、これは問題ありません.

于 2013-01-05T00:07:40.437 に答える
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()])
于 2012-12-15T09:39:07.053 に答える