SQLAlchemy: Session richtig verstehen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ich kann dir nicht ganz folgen. Stellen wir uns ein Anmelde-Fenster vor. Dort trägt der Anwender die benötigen Daten ein. Mit einem Klick oder Enter übergebe ich all die Daten an die Funktion, die für das Verbinden zuständig ist. Bisher alles kein Problem. Wenn alle Daten stimmen ist der Benutzer nun angemeldet. Und ab diesem Punkt komme ich und frage mich "Und weiter?". Wir gehen auch davon aus, dass der Benutzer vorher nicht versucht hat einen Datensatz abzuspeichern und erst DANN dazu aufgefordert wird seine Zugangsdaten einzugeben, sondern, er meldet sich direkt an. Und wohin nun das Session()-Objekt? Irgendwo muss das Objekt hin. Wenn ich weiß, dass er einen Datensatz einfügen will, kann ich das Objekt an die entspreche Funktion übergeben. Aber im Zeitraum zwischen Anmeldung und Datensatz einfügen befinde ich mich in einer Spannung, dass ich nicht weiß was ich mit dem erzeugten Objekt anstellen soll.
Sirius3
User
Beiträge: 18260
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: Du redest hier von einer GUI? Dann ist der Ort, wo man sich einen SessionMaker speichert, das Objekt, das alles steuert, gemeinhin MainWindow genannt. Und wenn Du wissen willst, was bei Deinen Beispielen mit __ passiert, kannst Du ja mal die Attribute Deiner Objekte mit dir anzeigen lassen.
BlackJack

@Sophus: Der Code, der nach dem Anmeldedialog die Sitzung erstellt, wird doch auch von irgend woher aufgerufen. Und dort hin gibt er die Sitzung zurück. Der Code der den Anmeldedialog initiiert hat, wird danach ja das weitere Programm irgendwie anstossen. Und da wird irgendwann die Sitzung benötigt werden, also übergibt man sie dort hin weiter. Das alles natürlich nicht so direkt, weil man GUI und Programmlogik ja hoffentlich sauber getrennt hat. Aber wie genau Du Dein Programm nun strukturierst wissen wir nicht, also kann man da auch nicht wirklich viel zu sagen. Das sind eben keine 5 bis 10 Zeilen Code der immer gleich aussieht, sondern deutlich mehr und es hängt davon ab wie das Programm aufgebaut ist, und das ist halt so variabel und ungewiss, das man da schlecht *das* allgemeingültige Beispiel bringen kann.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3 und BlackJack:

Vielleicht sollte ich noch detaillierter vorgehen. (01) Ich habe ein sogenanntes "MainWindows" mit dem MDI-Widget. (02) Auf diesem MainWindows ist dann (wie üblich eigentlich) eine Menuleiste. (03) In dieser Menuleiste gibt es ein Menu mit der Beschriftung "Anmelden". (04) Dort klickt der Anwender auf das entsprechende MenuItem und (05) ihm erscheint ein GUI-Fenster, wo er all sein Zugangsdaten in die QLineEdits einträgst und (06) anschließend auf den QPushButton drückt - er kann auch Enter drücken. (07) Durch den QPushButton werden die Zugangsdaten in Form von Argumente zur Programmlogik geschickt - zur Anmelde-Funktion.(08) Dort wird dann eine Anmeldung vorgenommen. (09) War die Anmeldung korrekt, schließt sich das Anmelde-Fenster und der Anwender ist angemeldet. Fazit: Hier wurde die Anmeldung durch einen QPushButton-Klick angestoßen. Hier wird dann eine Funktion aufgerufen, die dann all Inhalte der QLineEdits schnappt, und sie dann zur Programmlogik schickt. Und diese Funktion kann ja nicht das Session()-Objekt behalten.

Das heißt, die Anmeldung wird vom Anwender allein aus initiiert. Der Anwender kann auch versuchen ohne Anmeldung einen Datensatz einzufügen, aber dann erhält der Anwender eine Fehlermeldung (QMessageBox), mit dem Hinweis, dass er nicht angemeldet ist und wird gleichzeitig gefragt ob er sich anzumelden möchte, und dann öffnet sich das Anmelde-Fenster.

Aber mein Problem besteht erst einmal darin, dass der Anwender über die Menuleiste die Anmeldung initiieren kann, ohne vorher versucht zu haben einen Datensatz zu manipulieren oder Anfragen an den Server abzusetzen. Und wenn er sich entschließt "Oh, ich melde mich erst einmal an, und sehen dann weiter, was ich mache - vielleicht gehe ich nach der Anmeldung erst einmal Kaffee trinken".
BlackJack

@Sophus: Die Anmeldedaten werden ja der Programmlogik übergeben, damit die dann die Anmeldung durchführen kann. Und an der Stelle kann sich die Programmlogik dann ja auch an geeigneter Stelle die Sitzung merken. Sie merkt ja anscheinend auch schon ob eine Sitzung existiert, denn sonst könnte man ja keine Fehlermeldung präsentieren wenn man sich noch nicht angemeldet hat. Dazu muss man die Programmlogik ja nach der Sitzung fragen können, oder eben zumindest danach ob es die gibt oder nicht.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Nur weil sich der User über das Popup anmelden kann, heisst das noch lange nicht, dass Du an dieser Stelle in der Programmlogik überhaupt eine DB-Session brauchst. Diese Trennung von Präsentations- und Programmlogik musst Du selber vornehmen und an dieser Stelle kann man nur allegemeine Hinweise geben, da keiner aus Du weiss, wie Du das getrennt/abgebildet hast.

Zu den Session - ich zitier mal die Doku:
A Session is typically constructed at the beginning of a logical operation where database access is potentially anticipated.

...

It’s usually not very hard to determine the best points at which to begin and end the scope of a Session, though the wide variety of application architectures possible can introduce challenging situations.

A common choice is to tear down the Session at the same time the transaction ends, meaning the transaction and session scopes are the same. This is a great choice to start out with as it removes the need to consider session scope as separate from transaction scope.

...

As mentioned before, for non-web applications there is no one clear pattern, as applications themselves don’t have just one pattern of architecture. The best strategy is to attempt to demarcate “operations”, points at which a particular thread begins to perform a series of operations for some period of time, which can be committed at the end. Some examples:

- A background daemon which spawns off child forks would want to create a Session local to each child process, work with that Session through the life of the “job” that the fork is handling, then tear it down when the job is completed.
- For a command-line script, the application would create a single, global Session that is established when the program begins to do its work, and commits it right as the program is completing its task.
- For a GUI interface-driven application, the scope of the Session may best be within the scope of a user-generated event, such as a button push. Or, the scope may correspond to explicit user interaction, such as the user “opening” a series of records, then “saving” them.

As a general rule, the application should manage the lifecycle of the session externally to functions that deal with specific data. This is a fundamental separation of concerns which keeps data-specific operations agnostic of the context in which they access and manipulate that data.
Dann folgt eine Reihe von Beispielen, wie es gehen kann. Bitte bitte die Doku lesen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Es ist zwar unüblich, aber ich zeige dir mal in einem Auschnitt-Format die Funktion, die für die Anmeldung zuständig ist.

Code: Alles auswählen

def connect_to_mysql(dbm_system, dbm_driver, db_user, db_passwd, db_host, db_port, db_name):
    from sqlalchemy.exc import SQLAlchemyError

    try:

        engine = create_engine(dbm_system+'+'+dbm_driver+'://' + db_user + ':' + db_passwd + '@' + db_host + ':' + db_port + '/' + db_name,
             encoding='utf8', echo=True)

        Base.metadata.create_all(engine)
        Session = sessionmaker(bind=engine)
        sess = Sesstion()
        
        return True
    except SQLAlchemyError as err:
        return err[0]
        
Wenn alles richtig war, wird der Boolean-Wert True zurückgegeben, damit sich im View-Teil das Fenster geschlossen werden kann. Und hier sitze ich gedanklich in einer Sackgasse. Der Anwender hat nun seine Anmeldung vorgenommen, bekommt True, sein Fenster schließt sich. Und wohin mit dem Session()-Objekt? Auch dahin, wo True geschickt wird? Ergibt für mich keinen Sinn.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Die Funktion heisst connect_to_mysql kann sich aber zu beliebigen Datenbanken verbinden und stellt keine Verbindungen her sondern prüft letztendlich nur ob man einer herstellen kann. Du merkst das Problem?

Die Funktion sollte die Session zurückgeben und ansonsten eine Exception auslösen.
BlackJack

@Sophus: Die API ist ja schon mal unschön weil die Funktion entweder `True` oder eine Zeichenkette zurück gibt. Das ist hässlich asymmetrisch. Eine Funktion die `True` zurückgibt, sollte als einzige andere Alternative `False` zurück geben. Sinnvoller wäre es die Verbindung zurück zu geben, also entweder die Engine oder die Sitzung und wenn etwas schief läuft einfach die Ausnahme *nicht* behandeln, damit der Aufrufer mitbekommt das etwas schief gelaufen ist.

Die Funktion wird ja (hoffentlich) nicht aus der GUI aufgerufen, sondern aus einem Objekt das die Geschäftslogik modelliert, oder zumindest einen Teil davon der sich mit der Datenhaltung beschäftigt. Und dort kann man sich dann auch die Engine oder die Sitzung merken.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@DasIch: Du hast Recht, der Name der Funktion ist unglücklich gewählt worden.

Ich habe auf Rat von jerch die Dokumentation von SQLAlchemy gelesen (den Abschnit: When do I make s sessionmaker?), und da steht folgendes:
When do I make a sessionmaker?

Just one time, somewhere in your application’s global scope. It should be looked upon as part of your application’s configuration. If your application has three .py files in a package, you could, for example, place the sessionmaker line in your __init__.py file; from that point on your other modules say “from mypackage import Session”. That way, everyone else just uses Session(), and the configuration of that session is controlled by that central point.

If your application starts up, does imports, but does not know what database it’s going to be connecting to, you can bind the Session at the “class” level to the engine later on, using sessionmaker.configure().

In the examples in this section, we will frequently show the sessionmaker being created right above the line where we actually invoke Session. But that’s just for example’s sake! In reality, the sessionmaker would be somewhere at the module level. The calls to instantiate Session would then be placed at the point in the application where database conversations begin.
Hier wird sogar vorgeschlagen, dass die Sessions einmal "global" erstellt werde soll, von mir aus in einem __init__-Modul.
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Da steht dass man SessionMaker nur einmal erstellt, eine Session() sollte man aber nicht global haben.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Im View-Bereich wird ein QPushButton angeklickt:

Code: Alles auswählen

self.ui_login.pushButton_connect.clicked.connect(self.log_in)
Immernoch im View-Bereich wird dann die vom QPushButton ausgelöste Funktion in die Gänge gesetzt:

Code: Alles auswählen

    def log_in(self):
        server_host = unicode(self.ui_login.lineEdit_sevrer_host.text())
        username = unicode(self.ui_login.lineEdit_username.text())
        password = unicode(self.ui_login.lineEdit_password.text())
        database_name = unicode(self.ui_login.lineEdit_database_name.text())
        port = unicode(self.ui_login.lineEdit_port.text())

        # Now its important to find out which database was selected, because
        # each database has own module.
        if self.set_get_settings.dict_set_settings["Database"] == "MySQL":
            dbms_driver = u'pymysql'
            dbm_system = u'mysql'
            
            mysql_connection_result = manage_mysql.connect_to_mysql(dbm_system,
                                                                    dbms_driver,
                                                                    username,
                                                                    password,
                                                                    server_host,
                                                                    port,                                                                   
                                                                    database_name)
Und die Programmlogik liegt nun im Modul namens manage_mysql und dort wird dann die Funktion connect_to_mysql() aufgerufen. Das ist die Funktion, die ich euhc eben vorgestellt habe.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:

Die Erstellung einer Datenbanksitzung verläuft mit SqlAlchemy 2-stufig:

1. Erstellung der Verbindung - das `engine`-Objekt in Deinem Code
2. Erstellung einer Sitzung für Datentransfer und Transaktionslogik - die `session`

Für Ersteres brauchst Du die Verbindungs- und Logindaten. Für Zweiteres brauchst Du Ersteres. Diese Trennung kannst Du wunderbar auf Dein Programm übertragen:

1. Sammle Verbindungsdaten (z.B. mit Deinem Popup), am besten mit Verbindungsprüfung (Dein True/False Ergebnis)
2. Speichere die `engine` bzw. den Sessionmaker so, dass Du jederzeit hieraus Sessions bei Bedarf erzeugen kannst (applikationsweit vermutlich in Deinem Falle)
3. Interaktionen mit der DB stehen an - jetzt brauchst Du die Session (z.B. app.get_session()), mit dieser Session behandlest Du alle logisch zusammengehörigen DB-Aktionen
4. Session verwerfen - session.close()

Edit: Ich finde übrigens das Bsp. mit dem Contextmanager ganz chic - damit ist sichergestellt, dass Deine Anwendung die DB nicht in einem Kraut-und-Rüben-Zustand zurücklässt, falls die Anwendung mittendrin abschmiert. Zusätzlich zwingt Dich der Contextmanager dazu, die DB-Aktionen in "Sinneinheiten" zusammenzufassen und nicht wild über den Code verteilt die DB (mit einem globalen Session-Objekt) aufzusuchen
Zuletzt geändert von jerch am Mittwoch 10. Februar 2016, 15:57, insgesamt 1-mal geändert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Und die Frage ist, wohin das Session()-Objekt speichern? Ich brauche die Session applikationsweit. Nach dem Login habe ich das engine-Objekt, und dieses Objekt binde ich an Sessionmaker, und im Anschluss erzeuge ein "sess"-Objekt, in der das Session()-Objekt gespeichert ist. Und jetzt muss das sess-Objekt erst einmal irgendwo gespeichert werden, damit ich applikationsweit darauf zugreifen kann. Meine Idee war ja, das Session()-Objekt in eine Klasse zu speichern, und sie mir bei Bedarf per Getter geben zu lassen. Und die Session wird es dann verworfen, wenn der Anwender sich selbst über den "Abmelde"-QPushbutton abmeldet oder das Programm schließt. Es wäre ja unsinnig nach jeder Aktion die Session zu verwerfen - denke ich mal.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Nein Du brauchst kein globales Sessionobjekt.

Wie sieht denn Dein Datenmodelling in der Anwendung aus? Wie kommen die Daten aus der DB zur Ansicht und wie Änderungen zurück?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Soweit bin ich noch nicht. Natürlich werden die Interaktionen mit der Datenbank in die Programmlogik verschoben. Aber erst einmal beschäftigt mich das Session()-Objekt. Denn mithilfe dieses Objektes kann ich ja auch erst mit der Datenbank kommunizieren - zumindest indirekt, da ja das Session()-Objekt mit der Datenbank kommuniziert.
BlackJack

@Sophus: Wenn Du die Geschäftslogik als Modul mit Funktionen hast, dann hast Du natürlich ein Problem Dir in der Geschäftslogik irgend etwas zu *merken*. Geht halt nicht (sauber). Daran hängst Du anscheinend jetzt auch gerade. Man würde sich das in dem Objekt merken das die Geschäftslogik implementiert, oder in einem Objekt das den Datenhaltungsteil beinhaltet. Das schrieb ich ja schon mal. Dazu muss man solche Objekte auch haben. Und nicht nur Module mit Funktionen. Einen Getter brauchst Du nicht, da kann man auch auf das Attribut zugreifen. Wichtig dabei nur: Nicht global, also jetzt nicht eine Klasse schreiben und davon ein Exemplar global irgendwo auf Modulebene, denn dann könntest Du auch gleich ``global`` verwenden.

Die Datenbankverbindung gehört zur Programmlogik, die Interaktionen mit der Datenbank auch, denn die brauchen das ja. Ich glaube ich hatte ganz am Anfang mal gesagt das Du am falschen Ende der Anwendung anfängst, nämlich bei der GUI. Wenn Du erst die Programmlogik geschrieben hättest, und da dann die GUI da draus gesetzt hättest, würden wir dieses Thema hier jetzt nicht haben. :-)

Pro Aktion eine neue `Session` wäre übrigens nicht unsinnvoll. Steht so ja auch in der Dokumentation dazu, eben weil man dann keinen Unterschied zwischen der Lebensdauer der Sitzung und von Transaktionen machen muss, wenn beides immer gleich lang ist.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Zunächst zu deiner vorherigen Anmerkung bezüglich der Behandlung von Try/Except. Im View-Bereich bekommt der Anwender die Messagebox mit dem Inhalt, was der Fehler. Würde ich die Fehlerbehandlung nicht berücksichtigen, würde der Anwender auf der GUI-Seite gar nicht merken, dass etwas schief gelaufen ist. Daher schicke ich bei Fehlschlag auch die Zeichenkette zurück, die zum Inhalt der Messagebox wird. Und True wird zurückgeschickt, damit sich das Anmeldefenster schließt, wenn alles in Ordnung war.

Zu deiner Anmerkung bezüglich der Trennung zwischen GUI und Programmlogik. Ich habe eure Anmerkung weit am Anfang ernst genommen und halte sie stetst getrennt. Die Logik befindet sich niemals nie im View-Bereich. In diesem Bereich wird nur eine Funktion in den Gang gesetzt, die dann der Funktion in der Programmlogik etwas übergibt oder die Funktion in der Programmlogik in die Gänge setzt. Also eine Trennung ist vorhanden. Ich will ja das OOP-Konzept lernen, ansonsten hätte ich auch gleich bei VB6 bleiben können.

Aber wenn ich dich richtig verstanden habe, ist, dass ich nicht mit Funktionen arbeiten soll, sondern mit einer Klasse?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hmm also so wie Du es jetzt beschreibst, wirst Du Wohl oder Übel große Teile Deiner GUI-Logik nochmal überarbeiten und an die noch bevorstehende Datenmodellierung anpassen müssen. Normalerweise würde ich bei einer Anwendung von der Datenhaltung ausgehend die Geschäftslogik entwickeln, erst ganz am Ende steht die Frage der Präsentationslogik. Erst wenn man die eingesetzten Frameworks besser kennt, kann man das teilweise parallel entwickeln.

Da Du Qt nutzt - für alles bringt Qt mehr oder weniger für Python brauchbare Abstraktionen mit (z.B. die MVC-Klassen). Damit kannst Du nämlich im Frontendbereich massiv abkürzen (nur sehr spezielle Ausgaben müsstest Du selbst erstellen), allerdings dürfte die "Heirat" zwischen SqlAlchemy und den Modelklassen in der Geschäftslogik recht aufwändig werden.

Und wenn Du nicht zig Datenwrapper bauen willst, wird das alles entscheidenden Einfluß darauf haben, wie und wo Du letztendlich die Sessions brauchst bzw. vorhalten musst. Da Du das noch nicht hast, ist die Diskussion sophistisch ;)
Benutzeravatar
snafu
User
Beiträge: 6854
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Sophus
Ich habe vieles nur überflogen und hoffe, dass ich nichts übersehen habe, aber wenn du dem Benutzer eine einmalige Anmeldung ermöglichen willst, die unter Umständen für mehrere Stunden lang gültig ist, dann solltest du IMHO die tatsächlich dahinterliegende Session *nicht* stundenlang offenhalten. Eine Datenbank-Session würde ich nämlich nicht mit der Dauer eines User-Logins gleichsetzen.

Man könnte sich sein eigenes Session-Objekt bauen, welches die Anmeldung an die Datenbank im Hintergrund durchführt:

Code: Alles auswählen

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

class SessionScope(object):
    def __init__(self, dbms, driver, user, password, host, port, db_name):
        url = '{}+{}://{}:{}@{}:{}/{}'.format(
            dbms, driver, user, password, host, port, db_name
        )
        engine = create_engine(url, encoding='utf8', echo=True)
        self._Session = sessionmaker(bind=engine)
        self.session = None

    def __enter__(self):
        self.session = self._Session()
        return self.session
    
    def __exit__(self, exception, exc_value, traceback):
        try:
            if exception:
                self.session.rollback()
            else:
                self.session.commit()
        finally:
            self.session.close()
            self.session = None


def main():
    session_scope = SessionScope(bla, bla, bla, ...)
    with session_scope as session:
        # ...
Dies ist angelehnt an deinem Beispielcode und an der `session_scope()`-Funktion aus der Doku.

Die Anwendung ist halt so gedacht, dass man einmalig eine Scope-Instanz erzeugt und diese dann an den benötigten Stellen mit dem `with`-Statement benutzt, wodurch jeweils die eigentliche Session geöffnet und nach Verlassen des Scopes wieder geschlossen wird.
Zuletzt geändert von snafu am Mittwoch 10. Februar 2016, 17:31, insgesamt 1-mal geändert.
Antworten