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.
BlackJack

@snafu: Ich denke Du bringst da ein bisschen durcheinander. Weder die Session, noch die Engine sind die tatsächlichen Datenbankverbindungsobjekte. Die werden von der Engine verwaltet, und zwar üblicherweise in einem Pool und werden bei Bedarf erstellt. Und da macht das auch nichts wenn die stundenlang offen sind. Wenn einen das stört (oder das DBMS auf der anderen Seite), dann kann man der Engine eine maximale Lebensdauer für Verbindungen als Argument mitgeben. Um das auf- und abbauen der tatsächlichen Verbindungen zur Datenbank kümmert sich SQLAlchemy hinter den Kulissen automatisch. Darum würde ich sagen, dass jedes mal ein neues Engine-Objekt zu erstellen, keine gute Idee ist, denn dann würde man tatsächlich jedes mal neue Verbindungen aufbauen wenn man etwas mit der Datenbank anstellt.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Wie ich schon schrieb, halte ich Geschäftslogik/Programmlogik und Präsentation/View strikt auseinander. Ihr habt es mir damals eingebleut und daran halte ich mich umunstößlich. Das ist mir quasi zum ersten Gebot geworden. Und das Anmelden an die Datenbank kann man leicht als Skript schreiben (Weitere Anmerkungen meinerseits weiter unten nach dem Quelltext.):

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import exc
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker

import pymysql
import sys
from sqlalchemy.exc import SQLAlchemyError

Base = declarative_base()

def connect_to_dbms(dbm_system, dbm_driver, db_user, db_passwd, db_host, db_port, db_name):

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

        connection = engine.connect()

        Base.metadata.create_all(engine)
        Sesstion = sessionmaker(bind=engine)

        sess = Sesstion()

        return "Logged in successfully"
    except SQLAlchemyError as err:
        return err[0]

def main():
    dbm_system = raw_input("Which DBMS? (type for e.g. mysql): ")
    dbm_driver = raw_input("Which db-driver? (type for e.g. pymysql): ")
    db_host = raw_input("Server-Host: ")
    db_user = raw_input("Database-user: ")
    db_passwd = raw_input("User-Password: ")
    db_name = raw_input("Database Name: ")
    db_port = raw_input("Port: ")

    result_login = connect_to_dbms(dbm_system, dbm_driver, db_user, \
                    db_passwd, db_host, db_port, db_name)

    print result_login
    
if __name__ == '__main__':
    main()
Die Anmeldung erfolgt bei korrekter Angabe. Und genau JETZT bleibe ich hängen. Der User hat sich angemeldet. Wohin nun das engine-Objekt speichern? Auf Rat von jerch nehme ich lieber das engine-Objekt, damit auch daraus eine Session() erzeugen kann. Denn BlackJack hat zurecht angemerkt, dass man Sessions getrost nach jeder Interaktion verwerfen kann. Gut, in meinem Beispiel werden nur Zeichenketten zurückgegeben, aber es ist ja kein Hexenwerk mehrere Objekte an einem Return zu hängen. Aber nach der Anmeldung passiert nun was? Ich möchte das Objekt speichern, damit er zum späteren Zeitpunkt verwendet werden kann, sobald der Anwender etwas vornimmt.
Zuletzt geändert von Sophus am Mittwoch 10. Februar 2016, 17:37, insgesamt 1-mal geändert.
Benutzeravatar
snafu
User
Beiträge: 6856
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@BlackJack
Ich habe jetzt beispielhaft eine `main()`-Funktion und einen erklärenden Absatz hinzugefügt, um zu erläutern, was ich meine. Das Scope-Objekt ist wie gesagt so gedacht, dass man es nur einmal erzeugt. Daher steckt dieser Code auch nicht in `__enter__()`, sondern in `__init__()`.

Natürlich könnte man der `SessionScope`-Klasse aber auch ein bereits extern initialisiertes `Engine`-Objekt übergeben, sodass man diese beiden Schritte auch nochmal getrennt hätte und SQLAlchemy nicht so sehr in seiner Pool-Logik herumpfuscht.
Zuletzt geändert von snafu am Mittwoch 10. Februar 2016, 17:39, insgesamt 1-mal geändert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@snafu: Dein Ansatz klingt interessant. Ich werde mir das mal in aller Ruhe anschauen. Ich lerne immer wieder gerne was hinzu. Danke.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Ich würde hier nicht das `engine`-Objekt speichern, sondern den Sessionmaker, also das `Session`, aus welchem Du mittels Aufruf jederzeit eine gültige Session zurückbekommst, grob schematisch:

Code: Alles auswählen

# db.py
<sqlalchemy-imports here>

<import TableModels or declare here>

<some db_actions 1 & 2>

class DatabaseConnection(object):
    def __init__(self, <credentials go here>):
        self.engine = create_engine(<credentials go here>)
        # build tables upon first startup
        Base.metadata.create_all(self.engine)
        # store a sessionmaker for this db connection object
        self.sessionmaker = sessionmaker(bind=engine)
        
    def get_session(self):
        return self.sessionmaker()
 
 # main.py - not event based
 from db import DatabaseConnection, db_action1, db_action2
 
 if __name__ == '__main__':
     # create application wide db connection object
     db_connection = DatabaseConnection(<credentials go here>)
     
     # your application logic with db interaction
     session = db_connection.get_session()
     db_action1(session)
     db_action2(session)
     session.close()
     
     # or with a context manager
     with NotShownHere() as session:
         db_action1(session)
         db_action2(session)
 
  # main.py - GUI/event based
 from db import DatabaseConnection, db_action1, db_action2
 
 class MotherObject:
     def __init__(self)
         self.db_connection = None
         # accociate a real DatabaseConnection object later on by some event action
         
     def db_login_button(self):
         ...
         self.db_connection = DatabaseConnection(<credentials go here>)
         
     def db_logout_button(self):
         # some close magic here, see sqlqlchemy for it
         if self.db_connection:
             self.db_connection.close_magic()
         self.db_connection = None
         
     def insert_button_clicked(self):
         if not self.db_connection:
             # do wotever is apropriate here, e.g. show error message or login popup
             return
         session = self.db_connection.get_session()
         db_action1(session)
         db_action2(session)
         session.close()

 if __name__ == '__main__':
     mother = MotherObject()
     mother.show()  # only if mother itself has a GUI representation
     start_event_loop() # e.g. app.exec_() for Qt
Das letztere Bsp. merkt sich das DB-Verbindungsobjekt im "Mutterobjekt" der Anwendung (kann je nach Komplexität der Anwendung mit oder ohne GUI-Repräsentation sein).

Edit: Was ich noch vergessen habe - Qt macht es Dir sehr leicht, applikationsweite Daten abzulegen. Jede QtGui-Anwendung braucht genau ein QApplication-Object (singleton), in welchem Du sowas ablegen kannst. Qt nutzt das selbst für DB-Verbindungen, QDatabase-Objekte immer global innerhalb einer Anwendung erreichbar. Letzteres nützt Dir leider nichts mit Sqlalchemy.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Besten dank. Aber ich möchte mich so wenig wie es geht an Qt binden bzw. von Qt abhängig machen. Also fällt QDatabase weg. Es mag vielleicht einiges erleichtern, aber der Preis der Abhängigkeit ist mir leider zu hoch. Dann hast du ja QApplication angemerkt, die ja nur einmal angelegt bzw. erzeugt werden kann. Aber der Anwender gibt nicht gleich zu Beginn seine LogIn-Daten an. Das Programm startet ohne sich gleich mit der Datenbank zu verbinden. Das Programm startet in meinem Projekt erst einmal ganz unverbindlich. Über die Menuleiste muss sich der Anwender navigieren und dann anmelden. Also findet die Anmeldung erst nachdem das Programm gestartet ist statt.

Nehmen wir mal dein Beispiel: Die DatabaseConnection()-Klasse liegt im Modul namens db.py. Wenn ich also von woanders aus das Modul importiere ( from db import DatabaseConnection, db_action1, db_action2) wird dadurch nicht jedesmal eine neue (leere) Klasse importiert? Oder verwechsel ich da jetzt was? Ich möchte am Ende nicht jedesmal neue Verbindungen aufbauen.

EDIT: Denn bedenke, dass ich innerhalb meiner MDI-Anwendung mehrere Fenster benutze. Denn bei Filme zum Beispiel werden bestimmte Informationen über mehrere Fenster eingegeben. Oder verstehe ich dich so, dass das Aufbauen der DB-Verbindung in der MDI-Anwendung stattfinden soll und die anderen Informationen von anderen Fenstern dann bei MDI zusammenlaufen und von der MDI aus dann zur Geschäftslogik geschickt wird?
Zuletzt geändert von Sophus am Mittwoch 10. Februar 2016, 18:47, insgesamt 1-mal geändert.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hast Du den schematischen Code überhaupt angeschaut? Weder ist es da nötig, dass die User zu Programmstart sich anmelden müssen noch liegt die DatabaseConnection-Klasse in main.py. Für GUI-Programmierung ist es unabdingbar, sich über den Kontrollfluß im Klaren zu sein. MDI hat damit zunächst gar nichts zu tun.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Habs korrigiert. War unkonzentriert. Hast Recht 8) MDI ist sozusagen mein Hauptprogramm. Daher habe ich es erwähnt. Letztendlich ist es egal.

Was mich ein wenig stutzig macht: Das self.db_connection-Attribut der MotherObject()-Klasse ist intern "global", also man kann intern unkontrolliert auf das Attribut zugreifen. Ist damit nicht auch die Gefahr groß, dass durch bestimmte Seiteneffekte das Objekt verändert werden kann? Ich frage nur, weil BlackJack mich eindringlich gewarnt hat, die Attribute nicht "global" zu machen, sondern sie eher lokal zu erzeugen und die lokal erstellten Variablen über Funktionen rumzureichen.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Sophus hat geschrieben:Nehmen wir mal dein Beispiel: Die DatabaseConnection()-Klasse liegt im Modul namens db.py. Wenn ich also von woanders aus das Modul importiere ( from db import DatabaseConnection, db_action1, db_action2) wird dadurch nicht jedesmal eine neue (leere) Klasse importiert? Oder verwechsel ich da jetzt was? Ich möchte am Ende nicht jedesmal neue Verbindungen aufbauen.
Was soll ich Deiner Meinung nach darauf sagen ausser - beschäftige Dich mit Python Grundlagen! Leere Klasse importiert - was willst Du damit sagen? Geladen werden Module nur einmal (singleton) und ja im Namensraum liegt dann der Name der Klasse rum. Eine Verbindungsobjekt wird erst mit Aufruf der Klasse erstellt, wie man am Code erkennen kann.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

jerch hat geschrieben:
Sophus hat geschrieben:Nehmen wir mal dein Beispiel: Die DatabaseConnection()-Klasse liegt im Modul namens db.py. Wenn ich also von woanders aus das Modul importiere ( from db import DatabaseConnection, db_action1, db_action2) wird dadurch nicht jedesmal eine neue (leere) Klasse importiert? Oder verwechsel ich da jetzt was? Ich möchte am Ende nicht jedesmal neue Verbindungen aufbauen.
Was soll ich Deiner Meinung nach darauf sagen ausser - beschäftige Dich mit Python Grundlagen! Leere Klasse importiert - was willst Du damit sagen? Geladen werden Module nur einmal (singleton) und ja im Namensraum liegt dann der Name der Klasse rum. Eine Verbindungsobjekt wird erst mit Aufruf der Klasse erstellt, wie man am Code erkennen kann.
Vergiss was ich schrieb. War Müll. Einfach ignorieren.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Sophus hat geschrieben:Was mich ein wenig stutzig macht: Das self.db_connection-Attribut der MotherObject()-Klasse ist intern "global", also man kann intern unkontrolliert auf das Attribut zugreifen. Ist damit nicht auch die Gefahr groß, dass durch bestimmte Seiteneffekte das Objekt verändert werden kann? Ich frage nur, weil BlackJack mich eindringlich gewarnt hat, die Attribute nicht "global" zu machen, sondern sie eher lokal zu erzeugen und die lokal erstellten Variablen über Funktionen rumzureichen.
Wo ist das denn global? Was meinst Du mit "intern global"? Einen gewissen Sichtbarkeitsbereich muss es IMMER geben, sonst machen Variablen überhaupt keinen Sinn. `self.db_connection` ist hier objektweit sichtbar, nicht global.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Deswegen habe ich das Wort global in Anführungszeichen geschrieben. Innerhalb der Klasse ist sie "global". Ich kann innerhalb der Klasse darauf zugreifen. Ich nahm an, man solle sowas lokal in Funktionen erstellen und diese dann rumreichen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Wenn ich jerchs Beispiel umsetze, sieht man Quelltext wie folgt aus:

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import exc
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError

import pymysql
import sys

Base = declarative_base()

class DatabaseConnection(object):
    def __init__(self, dbms, dbdriver, dbuser, dbuser_pwd, db_server_host, dbport, db_name):

        self.dbms = dbms
        self.dbdriver = dbdriver
        self.dbuser = dbuser
        self.dbuser_pwd = dbuser_pwd
        self.db_server_host = db_server_host
        self.dbport = dbport
        self.db_name = db_name
        
        self.url = '{dbms}+{dbdriver}://{dbuser}:{dbuser_pwd}@{db_server_host}:{dbport}/{db_name}'.format(
            dbms=self.dbms, dbdriver=self.dbdriver, dbuser=self.dbuser, dbuser_pwd=self.dbuser_pwd, \
            db_server_host=self.db_server_host, dbport=self.dbport, db_name=self.db_name)

        try: 
            self.engine = create_engine(self.url, encoding='utf8', echo=True)
            
            # build tables upon first startup
            Base.metadata.create_all(self.engine)
            # store a sessionmaker for this db connection object
            self.sessionmaker = sessionmaker(bind=self.engine)
            return "Logged in successfully"
        except SQLAlchemyError as err:
            return err[0]
        
    def get_session(self):
        return self.sessionmaker()
  
def main():
    dbm_system = raw_input("Which DBMS? (type for e.g. mysql): ")
    dbm_driver = raw_input("Which db-driver? (type for e.g. pymysql): ")
    db_host = raw_input("Server-Host: ")
    db_user = raw_input("Database-user: ")
    db_passwd = raw_input("User-Password: ")
    db_name = raw_input("Database Name: ")
    db_port = raw_input("Port: ")

    result_login = DatabaseConnection(dbm_system, dbm_driver, db_user, \
                    db_passwd, db_host, db_port, db_name)    <------- ZEILE 55

    print result_login
    
if __name__ == '__main__':
    main()
Und beim Anmelden bekomme ich folgende Fehlermeldung:
Traceback (most recent call last):
File "C:\Users\Sophus\Desktop\db_testing.py", line 60, in <module>
main()
File "C:\Users\Sophus\Desktop\db_testing.py", line 55, in main
db_passwd, db_host, db_port, db_name)
TypeError: __init__() should return None, not 'str'
Im Quelltext habe ich die Stelle mit <------- ZEILE 55 markiert, die die Zeile 55 sein soll.
BlackJack

@Sophus: Der Fehlertext ist ja wohl selbsterklärend. Du darfst aus der `__init__()` nichts anderes als `None` zurückgeben. Hätte sowieso keinen Sinn weil der Rückgabewert nirgends landen würde, denn der Code der das Aufruft gibt das Objekt zurück das mir `__init__()` initialisiert wurde, und das hat der Code ja selbst schon und kann es direkt zurückgeben.

Die Lösung ist was schon gesagt wurde: Nicht versuchen Ausnahmen durch Fehlerwerte zu ersetzen. Denn Ausnahmen wurden erfunden um diese Fehlerwerte loszuwerden.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Ich habe das so gelöst, wie ich beim Speichern des "Session()"-Objektes vorgegangen bin. Ich habe einen weiteren Getter gebastelt, und den Rückgabewert in ein dafür vorgesehenes Attribut gespeichert. Aber kurz zum Fehlerwert. Der Nutzer soll doch darauf hingewiesen werden, dass die Anmeldung fehlgeschlagen wurde. Und der Fehlerwert sagt auch gleichzeitig warum etwas fehlschlug. Zum Beispiel gibt die Except-Methode folgendes zurück; (pymysql.err.OperationalError) (1045, u"Access denied for user 'root'@'localhost' (using password: YES)"). Und diese Zeichenkette nehme ich zum Inhalt der Messagebox. Wieso sollte ich dem Anwender dies Vorenthalten, indem ich die Ausnahme nicht behandel?

Hier ist mein Quelltext, um das Problem mit dem __init__-Initialisator zu lösen:

Code: Alles auswählen

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import exc
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError

import pymysql
import sys

Base = declarative_base()

class DatabaseConnection(object):
    def __init__(self, dbms, dbdriver, dbuser, dbuser_pwd, db_server_host, dbport, db_name):
        
        self.url = '{dbms}+{dbdriver}://{dbuser}:{dbuser_pwd}@{db_server_host}:{dbport}/{db_name}'.format(
            dbms=dbms, dbdriver=dbdriver, dbuser=dbuser, dbuser_pwd=dbuser_pwd, \
            db_server_host=db_server_host, dbport=dbport, db_name=db_name)

        self.status_db = ""
        self.sessionmaker = ""
            
        try: 
            self.engine = create_engine(self.url, encoding='utf8', echo=True)
            # build tables upon first startup
            Base.metadata.create_all(self.engine)
            # store a sessionmaker for this db connection object
            self.sessionmaker = sessionmaker(bind=self.engine)
            self.status_db = "Logged in successfully"
        except SQLAlchemyError as err:
            self.status_db = err
            
    def get_status(self):
        return self.status_db
        
    def get_session(self):
        return self.sessionmaker()

def main():
    dbm_system = raw_input("Which DBMS? (type for e.g. mysql): ")
    dbm_driver = raw_input("Which db-driver? (type for e.g. pymysql): ")
    db_host = raw_input("Server-Host: ")
    db_user = raw_input("Database-user: ")
    db_passwd = raw_input("User-Password: ")
    db_name = raw_input("Database Name: ")
    db_port = raw_input("Port: ")
    
    result_login = DatabaseConnection(dbm_system, dbm_driver, db_user, \
                    db_passwd, db_host, db_port, db_name)

    
    print "Status: ", result_login.get_status()
    
if __name__ == '__main__':
    main()
Sirius3
User
Beiträge: 18260
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: niemand hat geschrieben, dass Du die Ausnahme nicht behandeln sollst. Aber Du sollst sie dort behandeln, wo es Sinn macht. Also dort, wo man dem Nutzer eine Meldung anzeigen kann. Diese unsinnigen Getter kannst Du auch gleich sein lassen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Aber ergibt es keinen Sinn den Fehlerwert dort abzufangen wo ich es mache? In meiner GUI-Anwendung wird dieser Fehlerwert in Form von MessageBox verarbeitet.
BlackJack

@Sophus: Nein es ergibt keinen Sinn.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Nur mal rein gedanklich. Wenn die Verbindung fehlschlägt, dann möchte man es dem Benutzer mitteilen. Als fängt man diese Nachricht gleich dort ab, wo die Verbindung gescheitert ist. Und diesen Wert hole ich mir dann vom View-Bereich aus, da der Wert des Fehler in der DatabaseConnection()-Klasse in einem Attribut gespeichert liegt. Ich kann euch gerade gedanklich nicht verfolgen.
Benutzeravatar
snafu
User
Beiträge: 6856
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@Sophus
Wenn eine Exception geworfen wird, dann wird sie solange den Aufruf-Stack entlang hochgereicht bis sie irgendwo abgefangen wird. Wenn sich kein "Abfänger" findet, dann gibt der Python-Interpreter die Exception auf dem Bildschirm aus.

Es ist durchaus üblich, irgendwo eine Funktion aufzurufen, von der man weiß, dass sie eine bestimmte Exception werfen kann, aber diese Exception dann bewusst nicht abzufangen, damit sie automatisch hochgereicht wird.

Mit anderen Worten: Die korrekte Stelle, um die Exception sinnvoll zu behandeln, ist der Bereich deiner GUI, wo du bei geworfener Exception das Fehler-Fenster anzeigen lässt. Wenn du aus Exceptions irgendwelche Fehlerwerte machst und diese als Text herumreichst, dann läuft das der eigentlichen Idee von Exceptions zuwider und macht mehr Arbeit für dich als notwendig.
Antworten