SQLAlchemy: Arbeiten mit mehreren QThreads?

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Benutzeravatar
snafu
User
Beiträge: 5450
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Montag 14. August 2017, 08:36

Ich denke, du machst es immer noch nicht so wie vorgesehen. Dieses ManagedSessionScope() ist schon komisch gemacht, weil es anscheinend die Original-Session und die ScopedSession zusammen ausliefert (wovon du später die "falsche" verwendest). Du packst quasi etwas in Schutzfolie, holst es aber sofort wieder aus der Folie raus und glaubst, dass der Versand jetzt sicher sei. Lass die ScopedSession doch einfach komplett weg. Das hätte bei deinem Vorgehen den selben Effekt.
shcol (Repo | Doc | PyPi)
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 08:51

@snafu: Wo wird die Originale Session ausgeliefert? ManagedSessionScope() liefert beim Aufruf der __init__() einen Tuple. Einmal die Rückgabe von scoped_session() und dann die Klasse selbst. Aber die Klasse selbst verwende ich erst mal nicht. Ich habe keine Ahmung, wie du es sonst meinst. Vielleicht denke ich auch nur verquert.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 09:36

@snafu: Ich glaube, du bist von der falschen Namensgebung meinerseits irritiert. Also mein ManagedSessionScope()-Objekt liefert folgendes zurück: (extrem verkürzt)

Code: Alles auswählen

class ManagedSessionScope(object):
    def __init__(self, url=None,
                 echo_verbose=True,):
        [...]
        self._Engine = create_engine(self.url, echo=self.echo_verbose) 

        self._Session = scoped_session(sessionmaker(bind=self._Engine))

    def __enter__(self):
        self.session = self._Session     # this is now a scoped session
                                                      # sqlalchemy.orm.scoping.scoped_session
        return self.session, self

    [...]
Du siehst, ich bekomme nicht die originale Session zurück. Warum ich mir dann auch noch self mit zurückschicken lasse? Eigentlich könnte ich sie auch weglassen. Aber verwenden tue ich die self nicht. Ich glaube, das hat dich ein wenig verwirrt. War mein Fehler.

Gedankenproblem:
Aber ich habe ein kleines Gedankenproblem. In meiner zurvor vorgestellten Skizze wird ManagedSessionScope() durch die with-Anweisung direkt am Anfang des Programmstart geöffnet. Dadurch habe ich dann eine session, die ich applikationsweit benutzen könnte. Aber das war ja nur ein Beispiel. In meinem richtigen Projekt ist es so, dass sich der Benutzer später in die Datenbank anmelden kann - wann immer er/sie will. Folgende Situation, der Benutzer startet das Programm, geht zu einem späteren Zeitpunkt in die Menüleiste, und geht auf "Anmelden", ein neues Fenster, geerbt von QDialog(), wird erzeugt. Benutzer gibt seine Anmelde-Daten ein, klickt dann auf die "Ok"-Schaltfläche. Anmeldung läuft durch. Wenn alles gut ging, bekommt der Benutzer eine MessageBox angezeigt, mit der Information, dass alles wunderbar verlaufen ist, ansonsten werden entsprechende Fehlermeldungen über die MessageBox ausgegeben. Sobald der Benutzer diese MessageBox, mit der erfolgreichen Meldung, gesehen hat, schließt sich das Anmelde-Fenster. Und genau JETZT habe ich ein Problem. Wo halte ich die Session während dessen am "leben"? Anders gefragt: wie/wo soll ich die with-Anweisung die ganze Zeit geöffnet lassen, damit die Applikation auch nur mit einer gültigen Session arbeitet? Ich meine, in meiner vorherigen Skizze war die with-Anweisung durch sys.exit(app.exec_()) offen geblieben, denn dies wirkte fast wie eine while-Schleife. Meine erste Überlegung ist, dass ich einen weiteren Thread konstruiere, der nur dafür da ist, die with-Anweisung offen zu halten, damit wir eine gültige Session applikationsweis haben? In diesem besagten Thread müsste ich dann einen QTimer() einbauen, diesen Timer dann in den Korpus der with-Anweisung packen. Ansonsten fällt mir keine Idee ein, wie ich eine with-Anweisung zum späteren Zeitpunkt ununterbrochen(solange wie die Applikation läuft) geöffnet halten soll. Sonst hätte ich ja den gleichen Effekt, die ich bisher gemacht habe, dass jedesmal, bei Gebrauch erneut die with-Anweisung durchgeführt wurde. Würde ich dies tun, hätte ich jedesmal eine andere Session, und keine einzig gültige Session. Ich stecke gerade gedanklich fest.
BlackJack

Montag 14. August 2017, 09:44

@Sophus: Du gibst bei `__enter__()` ein Tupel zurück? Aber das hat doch dann gar keine `__exit__()`-Methode!
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 09:57

Ich werde den Tuple wieder entfernen. Ihr habt Recht, es war eine Dumme Idee :) Ich gebe nur noch die scoped_session zurück.
Benutzeravatar
snafu
User
Beiträge: 5450
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Montag 14. August 2017, 10:56

Die Frage ist, warum du überhaupt die scoped_session nochmals wrappst, denn das ist ja schon ein Wrapper. Wäre es nicht einfacher, eine "normale" Funktion zu programmieren, die eine vorkonfigurierte Sitzung als scoped_session liefert?

Im Übrigen ist es keine so gute Idee, eine Session während des gesamten Programmlaufs offen zu halten. Du solltest besser die Nutzereingaben aus der GUI sammeln und wenn er auf "Übermitteln" oder sowas klickt, dann wird die Session geöffnet und mit den Daten gefüttert, die commited werden sollen. Danach schließt man sie wieder. Und während dieses Ablaufs kann die offene Session (ScopedSession) dann halt auch - abhängig von deiner Programmstruktur - herumgereicht werden.
shcol (Repo | Doc | PyPi)
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 11:14

@snafu: Aber dann lande ich ja bei dem Ausgangspunkt, den du kritisiert hast. Ursprünglich sah die Struktur wie folgt aus, in der GUI-Klasse (MyCustomDialog()) gab es eine start_all_selection()-Methode, die so konstruiert war, dass bei jedem Klick auf die Schaltfläche die ManagedSessionScope() geöffnet wurde, die Rückgabe von scoped_session() an die Trheads weiterreichte, und nach getaner Arbeit wurde die ManagedSessionScope() automatisch wieder beendet. Und das hast du unter anderem doch kritisiert?

Code: Alles auswählen

class MyCustomDialog(QDialog):
 
    finish = pyqtSignal()
 
    def __init__(self, scoped_session=None, parent=None):
        QDialog.__init__(self, parent)

        [...]

    def start_all_selection(self):
    [...]
        with ManagedSessionScope(url=self._url) as session_that_scoped:
            
            for category, combobox in list_tuple:
                combobox.clear()
                
                self.start_thread(combo_box=combobox,session=session_that_scoped, category=category)
Daher war meine Idee, einen Thread zu konstruieren, der die Session solange offen hält, ehe der Benutzer in der Menüleiste auf "Abmelden" klickt, und der laufende Thread beendet wird. Im Thread wäre dann eine while-Schleife, die dann vor dem Beenden des Threads unterbrochen wird, so dass man den Korpus der with-Anweisung verlässt. Klickt also der Anwender auf "Abmelden", wird die while-Schleife im Thread beendet und der Thread wird beendet, und die Session ist geschlossen. Beim nächsten Mal, wenn er sich entschließt, sich anzumelden, wird wieder ein neuer Thread erzeugt, eine neue Session eröffnet und so weiter und so fort.

Ungefähr so: Die Signale habe ich jetzt mal der Übersichthalber ausgelassen, wollte jetzt nicht den vollständigen Quelltext kopieren. Aber so dachte ich mir, könnte das aussehen?

Code: Alles auswählen

class SessionTask(QObject):
 
    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.stop_loop = False
 
    def run(self):
        with ManagedSessionScope(url=self._url) as session:
            # do stuff with given scoped_session()

            while not self.stop_loop:  pass           
 
    def stop(self):
        self.stop_loop = True
__deets__
User
Beiträge: 2858
Registriert: Mittwoch 14. Oktober 2015, 14:29

Montag 14. August 2017, 11:36

Was hat denn eine Session bei *Bedarf* zu oeffnen damit zu tun, das die dann ploetzlich (und fehlerhafterweise) von allen moeglichen Threads geteilt werden muss? Dieser Schluss ist Unsinn.

Jeder Thread braucht seine eigene Session, aber die muss er bei Bedarf oeffnen, und dann wieder schliessen, wenn er sie nicht braucht. Und das ist nicht ueber seine ganze Lebenszeit, sondern waehrend einer bestimmten semantisch sinnvollen Aktion, zB dem auslesen von Werten fuer die Dropdowns, oder dem eintragen eines neuen Datensatzes. Kann also durchaus eine handvoll Statements beinhalten, aber eben nicht ueber die ganze Lebenszeit des Programms.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 12:12

@__deets__: Wenn ich dich richtig verstehe, für jeden Thread eine with-Anweisung konstruieren, und nicht so wie ich es bisher gemacht habe, eine with-Anweisung für alle Threads, die dann am Ende wieder geschlossen wird, wenn der letzte Thread fertig ist? Das heißt in meinem Beispiel, dass ich ganze 8 Twith-Anweisungen anwende?
__deets__
User
Beiträge: 2858
Registriert: Mittwoch 14. Oktober 2015, 14:29

Montag 14. August 2017, 12:44

Ja, wobei du das komisch ausdrueckst. Je nach Programmablauf und Code-Struktur kann das auch nur *eine* with-Anweisung sein, aber die wird halt oft aufgerufen. Oder es sind 2000, weil du eine um jeden SQL-code-block schreibst. Aber das entscheidende ist: kein Thread teilt seine Session mit einem anderen, das fuehrt zur Katastrophe.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 13:31

@__deets__: Das ich mich "komisch" ausdrücke, ist wohl meine Schwäche. Wobei ich es mir eher komisch vorstelle. Sagen wir mal, ich habe 100 Threads (nur rein hypothetisch), dann habe ich auch 100 with-Anweisungen. Der Quelltext bläht sich dadurch auf? Und wenn die session als scoped_session() vorbereitet wurde, dann kann man doch diese Session unter den Threads teilen, also die scoped_session() sich darum kümmern, dass die Threads miteiander nicht in Berührung kommen. Daher nahm ich an, dass man eine with-Anweisung anbringen kann, und im Korpus der with-Anweisung die ganzen Threads abarbeiten und die scoped_session() an die jeweilige Threads verteilen?
__deets__
User
Beiträge: 2858
Registriert: Mittwoch 14. Oktober 2015, 14:29

Montag 14. August 2017, 13:57

Wieso blähen denn 100 threads den Quellcode auf? Hier starte ich nicht nur hypothetisch 1000 Threads, und das in weniger als 1000 Zeilen, womit deine These ja ad absurdum gefuehrt wird:

Code: Alles auswählen

import threading

def tuwas():
      with scoped_session() as session:
             session.execute("etwas")

threads = [threading.Thread(target=tuwas) for _ in xrange(1000)]
[t.start() for t in threads]
[t.join() for t in threads]
Und zum gefuehlt 100sten mal: Sessions duerfen NICHT zwischen Threads geteilt werden. Was glaubst du denn, was eine Session eigentlich *ist*? Du kannst auch keine einzelne Datenbankverbindung nicht zwischen 1000 Threads teilen (normalerweise), weil eine solche Verbindung sich dann in jedem Thread gegenseitig auf die Fuesse tritt. Offene Cursor, Position auf der die stehen etc.

Also, noch einmal: EINE SESSION PRO THREAD.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Montag 14. August 2017, 15:16

@__deets__: Zwei Dinge. Erstens: Das sich dein Quelltext nicht aufbläht ist doch klar. Du erstellst zwar 1000 Threads, aber all die Threads führen nur ein und die selbe Aufgabe aus. In meinem Fall ist es so, dass jeder Thread jedes Eingabefeld (QComboBox(), QLineEdit()... etc.) befüllen. Wesen Aussage führt ad absurdum? Zweitens: Aus deinen Ausführungen werde ich auch nicht schlau, daher zum 101. Male. Du redest die ganze Zeit von Session, demzufolge weiß ich nicht ob du von scoped_session() redest oder von den normalen Session? Denn scoped_session(), die als Rückgabe <class 'sqlalchemy.orm.scoping.scoped_session'> liefert, ist thread-sicher, als im Gegensatz einer normalen Session. So meinem Verständnis: Solange ich NUR die Rückgabe von scoped_session() nicht aufrufe, habe ich keine normale Session, demzufolge frage ich mich, wieso man an dieser Stelle nicht die Rückgabe der scoped_session() an die Threads verteilen kann? Ist nicht gerade der Vorteil der scoped_session(), dass sie sich darum kümmert, dass sich die Threads nicht gegenseitig in die Knie schießen?
__deets__
User
Beiträge: 2858
Registriert: Mittwoch 14. Oktober 2015, 14:29

Montag 14. August 2017, 15:42

Wenn man 100 verschiedene Funktionalitaeten erstellt, dann muss man 100 verschiedene Dinge programmieren, mit diversene Statements, Funktionen, Klassen, Modulen, Paketen sogar. Nichts davon ist besonders. Warum ist dann ein solches with-Statement etwas, das dir solche Sorgen bereitet, und "den Code aufbläht"?

Und die scoped_session ist thread-sicher, weil sie einen thread-local storage verwendet um eine Session anzulegen.

Du redest die ganze Zeit davon, dass du den Wert, der dir durch den Aufruf von eben dieser scoped_session zurueckgegeben wird *verteilen* willst. DAS ist FALSCH.

Du kannst problemlos das hier machen:

Code: Alles auswählen

from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

session_factory = sessionmaker(bind=some_engine)
Session = scoped_session(session_factory)

def tuwas():
      # RICHTIG WEIL IMPLIZIT EIN SESSION-OBJEKT PRO THREAD
      print(Session.query(MyClass).all())

threads = [threading.Thread(target=tuwas) for _ in xrange(1000)]
[t.start() for t in threads]
[t.join() for t in threads]
Das hier ist, wovon du die ganze Zeit redest (zumindest klingt es so, wenn du "die Rückgabe von scoped_session() an die Trheads weiterreichte"

Code: Alles auswählen

from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

session_factory = sessionmaker(bind=some_engine)
Session = scoped_session(session_factory)

def tuwas(session):
      # GANZ DOLLE FALSCH, WEIL GLEICHES OBJEKT IN ALLEN THREADS
      print(session.query(MyClass).all())

my_session = Session()
threads = [threading.Thread(target=tuwas, args=(my_session,)) for _ in xrange(1000)]
[t.start() for t in threads]
[t.join() for t in threads]
BlackJack

Montag 14. August 2017, 16:00

Als Ergänzung: Wenn man das zweite Beispiel von __deets__ in Zeile 12 folgerndermassen ändert…

Code: Alles auswählen

threads = [threading.Thread(target=tuwas, args=(Session(),)) for _ in xrange(1000)]
…also im Hauptthread 1000 mal `Session` aufruft und das Ergebnis an die Threads verteilt, hat man genau das *gleiche* Problem, denn `Session()` liefert in ein und dem selben Thread immer das *selbe* `Session`-Exemplar, nämlich das für den Hauptthread.
Antworten