2012-11-14 6 views
8

Ho un'applicazione Web che è stata creata con Pyramid/SQLAlchemy/Postgresql e consente agli utenti di gestire alcuni dati e che i dati sono quasi completamente indipendenti per i diversi utenti. Dire, Alice visita alice.domain.com ed è in grado di caricare foto e documenti, e Bob visita bob.domain.com ed è anche in grado di caricare foto e documenti. Alice non vede mai nulla creato da Bob e viceversa (questo è un esempio semplificato, potrebbero esserci molti dati in più tabelle, ma l'idea è la stessa).Multi-tenancy con SQLAlchemy

Ora, l'opzione più semplice per organizzare i dati nel backend DB è quello di utilizzare un unico database, dove ogni tavolo (pictures e documents) ha user_id campo, quindi, in fondo, per ottenere tutte le immagini di Alice, posso fare qualcosa come

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

Questo è tutto facile e semplice, tuttavia ci sono alcuni svantaggi

  • ho bisogno di ricordare di usare sempre condizione di filtro aggiuntivo quando si effettuano le query, altrimenti Alice può vedere pi di Bob ctures;
  • Se ci sono molti utenti le tabelle possono crescere enorme
  • Può essere difficile per dividere l'applicazione web tra più macchine

Così sto pensando che sarebbe davvero bello per dividere in qualche modo i dati per -utente. Mi vengono in mente due approcci:

  1. Avere tavoli per Alice e Bob di immagini e documenti separati all'interno dello stesso database (Postgres' Schemas sembra essere un approccio corretto da utilizzare in questo caso):

    documents_alice 
    documents_bob 
    pictures_alice 
    pictures_bob 
    

    e poi, con un po 'di magia nera, "route" tutte le query per uno o per l'altra tabella in base al dominio del richiesta corrente:

    _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. utilizzare un database separato per ogni utente:

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

    che sembra la soluzione più pulita, ma non sono sicuro se più connessioni al database richiederebbe molto più RAM e altre risorse, limitando il numero di possibili " inquilini".

Quindi, la domanda è: tutto ha senso? In caso affermativo, come posso configurare SQLAlchemy per modificare i nomi delle tabelle in modo dinamico su ogni richiesta HTTP (per l'opzione 1) o per mantenere un pool di connessioni a diversi database e utilizzare la connessione corretta per ogni richiesta (per l'opzione 2)?

+2

Strettamente legato: http://stackoverflow.com/questions/9298296/ sqlalchemy-support-of-postgres-schemi –

+0

@CraigRinger: sì, se il comando "SET search_path TO ..." dalla risposta accettata funziona, questa sarebbe una soluzione per l'opzione n. Grazie. – Sergey

+1

Se vuoi evitare di condividere il tuo database subito, ci sono un paio di ricette su sqlalchemy.org per [Pre-Filtered Queries] (http://www.sqlalchemy.org/trac/wiki/UsageRecipes/PreFilteredQuery) e [Filtri globali] (http://www.sqlalchemy.org/trac/wiki/UsageRecipes/GlobalFilter) che possono aiutarti a evitare di estrarre dati che non vuoi per caso. –

risposta

2

Ok, ho finito con la modifica search_path all'inizio di ogni richiesta, utilizzando NewRequest evento di piramide:

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

funziona davvero beh, se si lascia la gestione delle transazioni su Pyramid (cioè non eseguire il commit/rollback delle transazioni manualmente, lasciando che Pyramid lo faccia alla fine della richiesta) - che è ok dato che commettere manualmente le transazioni non è comunque un buon approccio.

3

Ciò che funziona molto bene per me è impostare il percorso di ricerca a livello di pool di connessioni, piuttosto che nella sessione. Questo esempio usa Flask e i relativi proxy locali del thread per passare il nome dello schema, quindi dovrai modificare schema = current_schema._get_current_object() e il blocco try attorno ad esso.

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

Al motore momento della creazione:

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

Da dove proviene current_schema? – synergetic

+1

'current_schema' è un proxy creato da un'istanza di' werkzeug.local.Local() '. Qualcosa come 'thread_locals = Local(); current_schema = thread_locals ('schema') '. Il valore corrente dello schema è impostato all'inizio di una richiesta. È un modo conveniente per avere un valore accessibile globalmente legato al thread corrente. –

9

Dopo aver riflettuto sulla risposta di JD ero in grado di ottenere lo stesso risultato per PostgreSQL 9.2, SQLAlchemy 0.8 e 0.9 pallone quadro:

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