SQLAlchemy: Session richtig verstehen
@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.
@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.):
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.
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()
Zuletzt geändert von Sophus am Mittwoch 10. Februar 2016, 17:37, insgesamt 1-mal geändert.
@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.
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.
@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: 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.
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
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.
@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?
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.
@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.
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.
Habs korrigiert. War unkonzentriert. Hast Recht
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.

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.
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.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.
Vergiss was ich schrieb. War Müll. Einfach ignorieren.jerch hat geschrieben: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.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.
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.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.
@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.
Wenn ich jerchs Beispiel umsetze, sieht man Quelltext wie folgt aus:
Und beim Anmelden bekomme ich folgende Fehlermeldung:
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()
Im Quelltext habe ich die Stelle mit <------- ZEILE 55 markiert, die die Zeile 55 sein soll.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'
@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.
Die Lösung ist was schon gesagt wurde: Nicht versuchen Ausnahmen durch Fehlerwerte zu ersetzen. Denn Ausnahmen wurden erfunden um diese Fehlerwerte loszuwerden.
@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:
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()
@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.
@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.
@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.
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.