SQLAlchemy: tworzenie a ponowne użycie sesji

99

Tylko krótkie pytanie: SQLAlchemy mówi o wywołaniu sessionmaker()raz, ale wywołaniu Session()klasy wynikowej za każdym razem, gdy musisz porozmawiać z bazą danych. Dla mnie oznacza to, że drugi raz zrobiłbym pierwszy session.add(x)lub coś podobnego, zrobiłbym pierwszy

from project import Session
session = Session()

To, co zrobiłem do tej pory było, aby nawiązać połączenie session = Session()w moim modelu raz , a następnie importować zawsze ten sam nigdzie sesji w mojej aplikacji. Ponieważ jest to aplikacja internetowa, zwykle oznaczałoby to to samo (gdy wykonywany jest jeden widok).

Ale gdzie jest różnica? Jaka jest wada używania jednej sesji przez cały czas w porównaniu z używaniem jej do obsługi mojej bazy danych, dopóki moja funkcja nie zostanie zakończona, a następnie utworzenie nowej, gdy następnym razem będę chciał rozmawiać z moją bazą danych?

Rozumiem, że jeśli używam wielu wątków, każdy powinien mieć własną sesję. Ale używając scoped_session(), już upewniam się, że ten problem nie istnieje, prawda?

Proszę o wyjaśnienie, jeśli którekolwiek z moich założeń jest błędne.

javex
źródło

Odpowiedzi:

227

sessionmaker()jest fabryką, ma zachęcić do umieszczania opcji konfiguracyjnych do tworzenia nowych Sessionobiektów w jednym miejscu. Jest to opcjonalne, ponieważ możesz równie łatwo zadzwonić w Session(bind=engine, expire_on_commit=False)dowolnym momencie, gdy potrzebujesz nowego Session, z wyjątkiem tego, że jest on rozwlekły i zbędny, a ja chciałem powstrzymać rozprzestrzenianie się małych „pomocników”, z których każdy podchodził do problemu tej nadmiarowości w jakimś nowym i bardziej zagmatwany sposób.

sessionmaker()Jest to więc tylko narzędzie, które pomaga tworzyć Sessionobiekty, gdy ich potrzebujesz.

Następna część. Myślę, że pytanie brzmi, jaka jest różnica między robieniem nowego Session()w różnych punktach a zwykłym używaniem go przez cały czas. Odpowiedź nie bardzo. Sessionto pojemnik na wszystkie obiekty, które do niego włożysz, a następnie śledzi otwartą transakcję. W momencie wywołania rollback()lub commit()transakcja jest zakończona i Sessionserwer nie ma połączenia z bazą danych, dopóki nie zostanie wezwany do ponownego wysłania kodu SQL. Łącza, które utrzymuje do mapowanych obiektów, mają słabe odwołania, pod warunkiem, że obiekty są Sessionwolne od oczekujących zmian, więc nawet w tym względzie will opróżnia się z powrotem do zupełnie nowego stanu, gdy aplikacja utraci wszystkie odniesienia do zamapowanych obiektów. Jeśli zostawisz to z domyślną wartością"expire_on_commit"ustawienie, wszystkie obiekty tracą ważność po zatwierdzeniu. Jeśli to się Sessionzawiesi przez pięć lub dwadzieścia minut, a wszystkie rodzaje rzeczy zmieniły się w bazie danych następnym razem, gdy go użyjesz, załaduje cały nowy stan przy następnym dostępie do tych obiektów, nawet jeśli były one w pamięci przez dwadzieścia minut.

W aplikacjach internetowych zwykle mówimy: hej, dlaczego nie tworzysz zupełnie nowego Sessionna każde żądanie, zamiast używać tego samego w kółko. Ta praktyka gwarantuje, że nowe żądanie rozpocznie się „po wyczyszczeniu”. Jeśli niektóre obiekty z poprzedniego żądania nie zostały jeszcze zebrane, a być może wyłączyłeś "expire_on_commit", być może jakiś stan z poprzedniego żądania nadal się kręci, a ten stan może być nawet dość stary. Jeśli uważasz, aby pozostawić expire_on_commitwłączone i zdecydowanie zadzwonić commit()lub rollback()po zakończeniu żądania, to w porządku, ale jeśli zaczniesz od zupełnie nowego Session, nie ma nawet wątpliwości, że zaczynasz od czystego. Więc pomysł, aby każde żądanie rozpoczynać od nowegoSessionjest naprawdę najprostszym sposobem, aby upewnić się, że zaczynasz od nowa, i uczynić użycie expire_on_commitpraktycznie opcjonalnego, ponieważ ta flaga może spowodować dużo dodatkowego kodu SQL dla operacji, która wywołuje commit()w środku serii operacji. Nie jestem pewien, czy to odpowiada na Twoje pytanie.

Następna runda dotyczy gwintowania. Jeśli Twoja aplikacja jest wielowątkowa, zalecamy upewnić się, że Sessionużywana jest lokalnie dla ... czegoś. scoped_session()domyślnie ustawia go lokalnie dla bieżącego wątku. W aplikacji sieciowej, lokalne żądanie jest w rzeczywistości jeszcze lepsze. Flask-SQLAlchemy w rzeczywistości wysyła niestandardową „funkcję zakresu” do scoped_session(), aby uzyskać sesję o zasięgu żądania. Przeciętna aplikacja Pyramid umieszcza sesję w rejestrze „żądań”. Korzystając z takich schematów, idea „utwórz nową sesję na początku żądania” nadal wygląda na najprostszy sposób na prostotę.

zzzeek
źródło
17
Wow, to odpowiada na wszystkie moje pytania dotyczące części SQLAlchemy, a nawet dodaje trochę informacji o Flask i Pyramid! Dodatkowy bonus: odpowiedź deweloperów;) Chciałbym móc głosować więcej niż raz. Dziękuję Ci bardzo!
javex
Jedno wyjaśnienie, jeśli to możliwe: mówisz, że expire_on_commit "może spowodować dużo dodatkowego zapytania SQL" ... czy możesz podać więcej szczegółów? Myślałem, że expire_on_commit dotyczy tylko tego, co dzieje się w pamięci RAM, a nie tego, co dzieje się w bazie danych.
Veky
3
expire_on_commit może skutkować większą liczbą SQL, jeśli ponownie użyjesz tej samej sesji, a niektóre obiekty nadal kręcą się w tej sesji, kiedy uzyskasz do nich dostęp, otrzymasz pojedynczy wiersz SELECT dla każdego z nich, ponieważ każdy z nich indywidualnie odświeża się ich stan w kontekście nowej transakcji.
zzzeek
1
Cześć @zzzeek. Dzięki za doskonałą odpowiedź. Jestem bardzo nowy w Pythonie i kilka rzeczy, które chcę wyjaśnić: 1) Czy rozumiem poprawnie, kiedy tworzę nową "sesję" przez wywołanie metody Session (), to utworzy transakcję SQL, a następnie transakcja zostanie otwarta do momentu zatwierdzenia / wycofania sesji ? 2) Czy session () używa jakiejś puli połączeń lub za każdym razem tworzy nowe połączenie z sql?
Alex Gurskiy,
27

Oprócz doskonałej odpowiedzi zzzeek, ​​oto prosty przepis na szybkie tworzenie jednorazowych, zamkniętych sesji:

from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

@contextmanager
def db_session(db_url):
    """ Creates a context with an open SQLAlchemy session.
    """
    engine = create_engine(db_url, convert_unicode=True)
    connection = engine.connect()
    db_session = scoped_session(sessionmaker(autocommit=False, autoflush=True, bind=engine))
    yield db_session
    db_session.close()
    connection.close()

Stosowanie:

from mymodels import Foo

with db_session("sqlite://") as db:
    foos = db.query(Foo).all()
Berislav Lopac
źródło
3
Czy istnieje powód, dla którego tworzysz nie tylko nową sesję, ale także nowe połączenie?
danqing
Niezupełnie - to szybki przykład pokazujący mechanizm, choć ma sens tworzenie wszystkiego na świeżo w testach, w których najczęściej używam tego podejścia. Powinno być łatwe rozszerzenie tej funkcji o połączenie jako opcjonalny argument.
Berislav Lopac