2012-11-14 31 views
8

Tôi đã có một ứng dụng web được xây dựng với Pyramid/SQLAlchemy/Postgresql và cho phép người dùng quản lý một số dữ liệu và dữ liệu đó gần như hoàn toàn độc lập với những người dùng khác nhau. Nói, Alice truy cập alice.domain.com và có thể tải lên hình ảnh và tài liệu và Bob truy cập bob.domain.com và cũng có thể tải lên hình ảnh và tài liệu. Alice không bao giờ thấy bất cứ điều gì được tạo bởi Bob và ngược lại (đây là một ví dụ đơn giản, có thể có rất nhiều dữ liệu trong nhiều bảng thực sự, nhưng ý tưởng là như nhau).Multi-tenancy với SQLAlchemy

Bây giờ, tùy chọn đơn giản nhất để tổ chức các dữ liệu trong backend DB là sử dụng một cơ sở dữ liệu duy nhất, nơi mỗi bảng (picturesdocuments) có user_id lĩnh vực, vì vậy, về cơ bản, để có được tất cả các hình ảnh của Alice, tôi có thể làm cái gì đó như

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

Đây là tất cả dễ dàng và đơn giản, tuy nhiên có một số nhược điểm

  • tôi cần phải nhớ để luôn luôn sử dụng điều kiện lọc thêm khi thực hiện truy vấn, nếu không có thể thấy Alice pi của Bob ctures;
  • Nếu có nhiều người sử dụng các bảng có thể phát triển lớn
  • Nó có thể là khó khăn để phân chia các ứng dụng web giữa nhiều máy tính

Vì vậy, tôi nghĩ nó sẽ thực sự tốt đẹp để bằng cách nào đó chia dữ liệu mỗi -người dùng. Tôi có thể nghĩ đến hai phương pháp:

  1. Có riêng bảng cho Alice và Bob hình ảnh và tài liệu trong cơ sở dữ liệu tương tự (Postgres' Schemas có vẻ là một cách tiếp cận đúng để sử dụng trong trường hợp này):

    documents_alice 
    documents_bob 
    pictures_alice 
    pictures_bob 
    

    và sau đó, sử dụng một số phép thuật bóng tối, "lộ trình" tất cả các truy vấn đến một hoặc vào bảng khác theo miền yêu cầu hiện tại của:

    _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. Sử dụng một cơ sở dữ liệu riêng biệt cho mỗi người dùng:

    - database_alice 
        - pictures 
        - documents 
    - database_bob 
        - pictures 
        - documents 
    

    mà dường như là giải pháp sạch, nhưng tôi không chắc chắn nếu nhiều kết nối cơ sở dữ liệu sẽ đòi hỏi nhiều hơn nữa RAM và các nguồn lực khác, hạn chế số lượng càng tốt " người thuê nhà ".

Vì vậy, câu hỏi là, tất cả đều có ý nghĩa không? Nếu có, làm thế nào để cấu hình SQLAlchemy để sửa đổi tên bảng động trên mỗi yêu cầu HTTP (cho tùy chọn 1) hoặc để duy trì một nhóm kết nối tới cơ sở dữ liệu khác nhau và sử dụng kết nối đúng cho mỗi yêu cầu (cho tùy chọn 2)?

+2

Liên quan chặt chẽ: http://stackoverflow.com/questions/9298296/ sqlalchemy-support-of-postgres-schemas –

+0

@CraigRinger: vâng, nếu "SET search_path TO ..." thingie từ câu trả lời được chấp nhận hoạt động, đó sẽ là giải pháp cho tùy chọn # 1. Cảm ơn. – Sergey

+1

Nếu bạn muốn tránh sharding cơ sở dữ liệu của bạn ngay lập tức dơi, có một cặp công thức nấu ăn trên sqlalchemy.org cho [Pre-Filtered Queries] (http://www.sqlalchemy.org/trac/wiki/UsageRecipes/PreFilteredQuery) và [Global Filters] (http://www.sqlalchemy.org/trac/wiki/UsageRecipes/GlobalFilter) có thể giúp bạn tránh việc kéo dữ liệu mà bạn không muốn vô tình. –

Trả lời

2

Ok, tôi đã kết thúc với sửa đổi search_path vào đầu mọi yêu cầu, sử dụng NewRequest sự kiện Kim Tự Tháp:

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() 

trình thực sự tốt, miễn là bạn rời khỏi quản lý giao dịch cho Pyramid (tức là không cam kết/roll-back giao dịch bằng tay, cho phép Kim tự tháp để làm điều đó vào cuối yêu cầu) - đó là ok như cam kết giao dịch bằng tay không phải là một cách tiếp cận tốt anyway.

3

Điều gì làm việc rất tốt cho tôi để đặt đường dẫn tìm kiếm ở cấp độ kết nối, thay vì trong phiên. Ví dụ này sử dụng Flask và các proxy địa phương chủ đề của nó để truyền tên lược đồ, do đó bạn sẽ phải thay đổi schema = current_schema._get_current_object() và khối thử xung quanh nó.

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() 

Tại thời điểm tạo động cơ:

engine = create_engine(dsn, listeners=[SearchPathSetter()]) 
+0

Current_schema xuất phát từ đâu? – synergetic

+1

'current_schema' là một proxy được tạo bởi một thể hiện của' werkzeug.local.Local() '. Một cái gì đó như 'thread_locals = Local(); current_schema = thread_locals ('lược đồ') '. Giá trị hiện tại của lược đồ được đặt ở đầu yêu cầu. Đó là một cách thuận tiện để có một giá trị có thể truy cập trên toàn cầu gắn với luồng hiện tại. –

9

Sau khi cân nhắc về câu trả lời jd của tôi đã có thể đạt được kết quả tương tự cho postgresql 9.2, SQLAlchemy 0.8, và bình 0,9 khung:

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() 
Các vấn đề liên quan