Z sqlalchemy jak dynamicznie powiązać silnik bazy danych na żądanie

Mam aplikację internetową opartą na pylonach, która łączy się poprzez Sqlalchemy (v0.5) z bazą danych Postgres. Dla bezpieczeństwa, zamiast postępować zgodnie z typowym wzorem prostych aplikacji internetowych (jak widać w prawie wszystkich samouczkach), nie używam ogólnego użytkownika Postgres (np. "webapp"), ale wymagam od użytkowników wprowadzenia własnego identyfikatora użytkownika Postgres i hasła i używam go do nawiązania połączenia. Oznacza to, że w pełni korzystamy z Postgres Ochrona.

Komplikując sprawy jeszcze bardziej, istnieją dwie oddzielne bazy danych do połączenia. Chociaż obecnie znajdują się w tym samym klastrze Postgres, muszą być w stanie przenieść się do oddzielnych hostów w późniejszym terminie.

Używamy pakietu deklaratywnego sqlalchemy, choć nie widzę, żeby to miało jakikolwiek związek z tą sprawą.

Większość przykładów SQLAlchemy pokazuje trywialne podejścia, takie jak konfigurowanie metadanych raz, podczas uruchamiania aplikacji, z ogólnym database userid i hasło, które jest używane przez aplikację webową. Zwykle odbywa się to za pomocą metadanych.bind = create_engine (), czasami nawet na poziomie modułu w plikach modelu bazy danych.

Moje pytanie brzmi, jak możemy odroczyć ustanowienie połączeń do czasu zalogowania się użytkownika, a następnie (oczywiście) ponownie wykorzystać te połączenia, lub ponownie ustanowić je przy użyciu tych samych danych uwierzytelniających, dla każdego kolejnego żądania.

Mamy to działa -- myślimy -- ale nie tylko nie jestem pewien jeśli chodzi o bezpieczeństwo, myślę również, że wygląda niesamowicie ciężko jak na tę sytuację.

Wewnątrz metody __call__ Basecontrollera pobieramy identyfikator użytkownika i hasło z sesji sieci Web, wywołujemy SQLAlchemy create_engine() raz dla każdej bazy danych, a następnie wywołujemy procedurę wywołującą sesję.bind_mapper () wielokrotnie, raz dla każdej tabeli, do której może się odwoływać na każdym z tych połączeń, nawet jeśli każde żądanie zwykle odwołuje się tylko do jednej lub dwóch tabel. Wygląda coś takiego:

# in lib/base.py on the BaseController class
def __call__(self, environ, start_response):

    # note: web session contains {'username': XXX, 'password': YYY}
    url1 = 'postgres://%(username)s:%(password)s@server1/finance' % session
    url2 = 'postgres://%(username)s:%(password)s@server2/staff' % session

    finance = create_engine(url1)
    staff = create_engine(url2)
    db_configure(staff, finance)  # see below
    ... etc

# in another file

Session = scoped_session(sessionmaker())

def db_configure(staff, finance):
    s = Session()

    from db.finance import Employee, Customer, Invoice
    for c in [
        Employee,
        Customer,
        Invoice,
        ]:
        s.bind_mapper(c, finance)

    from db.staff import Project, Hour
    for c in [
        Project,
        Hour,
        ]:
        s.bind_mapper(c, staff)

    s.close()  # prevents leaking connections between sessions?

Więc wywołania create_engine () występują przy każdym żądaniu... Widzę, że jest to potrzebne, a Pula połączeń prawdopodobnie je buforuje i robi rzeczy rozsądnie.

Ale wywołanie sesji.bind_mapper () raz dla każdego tabeli, na każdego żądania? Wygląda na to, że musi być lepszy sposób.

Oczywiście, ponieważ pragnienie silnego bezpieczeństwa leży u podstaw tego wszystkiego, nie chcemy żadnej szansy, że połączenie ustanowione dla wysokiego bezpieczeństwa użytkownik nieumyślnie zostanie użyty w późniejszym żądaniu przez użytkownika o niskim poziomie bezpieczeństwa.

Author: Peter Hansen, 2009-12-07

2 answers

Wiązanie obiektów globalnych (maperów, metadanych) do połączenia specyficznego dla użytkownika nie jest dobrym sposobem. Jak również za pomocą sesji scoped. Proponuję utworzyć nową sesję dla każdego żądania i skonfigurować ją tak, aby korzystała z połączeń specyficznych dla użytkownika. Poniższy przykład zakłada, że dla każdej bazy danych używa się oddzielnych obiektów metadanych:

binds = {}

finance_engine = create_engine(url1)
binds.update(dict.fromkeys(finance_metadata.sorted_tables, finance_engine))
# The following line is required when mappings to joint tables are used (e.g.
# in joint table inheritance) due to bug (or misfeature) in SQLAlchemy 0.5.4.
# This issue might be fixed in newer versions.
binds.update(dict.fromkeys([Employee, Customer, Invoice], finance_engine))

staff_engine = create_engine(url2)
binds.update(dict.fromkeys(staff_metadata.sorted_tables, staff_engine))
# See comment above.
binds.update(dict.fromkeys([Project, Hour], staff_engine))

session = sessionmaker(binds=binds)()
 4
Author: Denis Otkidach,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-12-08 08:51:23

Chciałbym spojrzeć na połączenie pooling i zobaczyć, czy nie można znaleźć sposób, aby mieć jedną pulę na użytkownika. Możesz dispose() po wygaśnięciu sesji użytkownika

 -1
Author: John La Rooy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-12-07 03:02:22