SQLAlchemy: Arbeiten mit mehreren QThreads?

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hier, die Repository, just called snafus Version. Mit snafus Version meine ich. wie weit ich bei deinem Ansatz gekommen bin. Im Modul worker.py findest du in Zeile 20 meine Frage in einem Kommentar, ob ich dort die Session anfassen soll.

Spaßeshalber habe ich im Modul managed_data_manipulation.py in Zeile 16 eine print-Anweisung eingebaut, um die ID der jeweiligen Session und die dazugehörige ID des Workers darzustellen. Das Ergebnis sieht wie folgt aus:
ID Session 88079408 - ID Worker 87950224
ID Session 88277488 - ID Worker 88030352
ID Session 88080272 - ID Worker 88029704
ID Session 88079952 - ID Worker 87968192
ID Session 88080112 - ID Worker 87994416
ID Session 88078352 - ID Worker 88030208
ID Session 88078064 - ID Worker 87837984
ID Session 88077936 - ID Worker 88029920
Wenn ich das richtig deute, arbeiten meine 8 verschiedene Threads mit 8 verschiedenen Sessions, korrekt?
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Es sollte normalerweise immer die selbe Session sein. Der Sinn ist ja, dass auf die selbe Session aus verschiedenen Threads zugegriffen werden kann. Dabei müssen die Zugriffe aber atomar sein, d.h. wenn Thread 1 irgendwas an der Session macht, dann muss Thread 2 warten bis der erste Thread fertig mit der Operation ist. Also Thread 1 muss nicht komplett beendet sein, aber die DB-bezogene Operation (d.h. die Ausführung des Statements) darf nicht gestört werden. Ich schau mir den hochgeladenen Code später mal an.

Übrigens wird es bei potenziell gleichzeitig laufenden Threads dann interessant, wenn etwas abhängig von einer Rückgabe gemacht werden soll. Denn bei der Reaktion auf die Rückgabe ist nicht garantiert, dass ein anderer Thread nicht zwischendurch etwas so geändert hat, dass der erste Thread hierdurch eine andere Rückgabe hätte. Das atomare ist ja nur für einzelne Operationen garantiert, nicht jedoch für den Zustand der Datenbank insgesamt.

Falls das nicht ganz klar geworden ist: Das ist so als wenn du eine Liste in Python mit einer for-Schleife durchläufst und dir garantiert wird, dass während des Durchlaufs keine Veränderungen an der Liste auftreten (Vgl.: Select-Statement).
Zuletzt geändert von snafu am Dienstag 15. August 2017, 00:29, insgesamt 1-mal geändert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@snafu: Ich danke dir. Hast du BitBucket, damit du forken kannst? Ansonsten muss ich es dir als .*rar-Datei hochladen, damit du das Programm direkt benutzen kannst.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich kann gerade nicht schlafen, zu viele Gedanken schießen durch meinen Kopf. Deshalb habe ich nochmals __deets_s Version aufgeräumt und auf meinen BitBucket zur Verfügung gestellt. Sie ist so ähnlich strukturiert, wie snafus Version, nur dass bei __deets__s Version die scoped_session() auf Modulebene ansiedelt. Im Modul defined_session.py werden session_scoped() und sessionmaker() auf Modulebene definiert. In Modul my_custom_dialog.py wird die engine an die Session.configure() gebunden. Da jetzt fortan die Session an die engine gebunden ist, werden in Modul managed_data_manipulation.py die scoped_session() und sessionmaker() importiert. Es scheint zu klappen. Jetzt habe ich zwei Lösungen, einmal die snafus Version, und einmal die __deets__s Version.

Ich bin auf das Ergebnis von snafu gespannt und gehe dann mal schlafen.
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Habe mir das Repo jetzt mal näher angesehen: Ich finde deine Strukturierung total unübersichtlich. Außerdem benutzt du oft Klassen in Situationen wo es auch eine Funktion getan hätte. Dass du für jede Klasse ein eigenes Modul erstellst, erinnert eher an Java und ist entsprechend nervig, weil man dadurch viel zwischen den Dateien springen muss. Aber ist ja dein Code und du musst dich darin zurecht finden...

Bezüglich der scoped_session scheinst du jetzt alles richtig zu machen.
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Übrigens kannst du die select_all() aus der MasterDataManipulation stark kürzen. Wenn man das try mal außen vor lässt, dann landet man in etwa hier, wenn man es als Funktion schreibt:

Code: Alles auswählen

def select_all(session, category):
    model = getattr(models, category.upper())
    category = category.lstrip('person_')
    for record in session.query(model):
        yield record.id, getattr(record, category)
Wie du siehst, erspart dir getattr() bei richtiger Anwendung so einiges an Schreibarbeit. :)

EDIT:
Und was gibt es da eigentlich zu commiten? Du fragst doch nur Daten ab und führst an der Stelle gar keine Änderung durch...
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@snafu: ein fettes Dankeschön, dass du dir die Mühe gemacht hast, die Repo anzusehen. __deets__ hat ja stark kritisiert, dass ich die Rückgabe von scoped_session() an die Threads verteile. Deshalb habe ich auch eine Repo mit __deets__s Version eingerichtet. Aber wenn ich von dir den Segen bekomme, ist das schon mal ein gutes Zeichen. Zu deinem Vorschlag bezüglich der select_all()-Methode werde ich mir mal genauer ansehen. Denn im Grunde habe ich noch gefühlte weitere Hunderte Tabellen mit anderen Präfixen außer 'person_' ;)

Zu deiner Anmerkung meiner Ordnung. Jeder hinterlässt im Quelltext seine Handschrift. Vielleicht bin ich einfach zu pingelig und bringe alles fein getrennt in Modulen unter. Ich weiß, man kann mit der Atomatisierung übertreiben.

EDIT
Du kannst gern mal die commit()s auskommentieten und deine Version starten. Und dann lade per Re-populate-Schaltfläche die Daten erneut. Mache das mehrmals hinter einander - ohne das Programm neuzustarten. Bei mir fängt sich das Programm an komisch zu verhalten. Es werden dann auf einmal nicht alle QCombox() gefüllt. Beim ersten Mal Laden werden alle QComboBox ordnungsgemäß gefüllt, beim zweiten Re-populate werden ein oder zwei QComboBox() nicht gefüllt und beim dritten Male geht gar nichts mehr. Daher die commit()s. Durch die commit()s werden die Verbindungen wieder freigegeben?
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Das hat normalerweise nichts mit commit() zu tun. Das COMMIT-Statement ist nur für schreibende Operationen gedacht, nicht für lesende. Wichtig ist halt, zeitnah die close()-Methode für eine Session aufzurufen. Das Thema, dass Sessions kurzlebige Objekte sind und keinesfalls über den gesamten Programmlauf offen gehalten werden sollten, hatten wir ja schon.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@snafu: Dann rufe ich am Ende der select_all()-Methode die close() auf, damit nach getaner Arbeit die Verbindungen zum Pool zurückgegeben werden.
Zuletzt geändert von Sophus am Mittwoch 16. August 2017, 23:51, insgesamt 1-mal geändert.
Benutzeravatar
snafu
User
Beiträge: 6732
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ja genau, das Schema ist:

Code: Alles auswählen

try:
    for record in session.query(model):
        # ...
finally:
    session.close()
Oder du benutzt halt dein selbst gebasteltes ScopedSession()-Gedöns mit dem with-Statement. Ob du dort einen commit-Parameter einbaust für einen Wahrheitswert oder ob du es dabei belässt, dass in __exit__() halt immer ein COMMIT ausgeführt wird - selbst in Fällen wo es unnötig ist - überlasse ich dir... :)
BlackJack

@snafu: COMMIT beendet eine Transaktion und kann auch bei lesendem Zugriff Sinn machen, nämlich dann wenn das DBMS „repeatable read“ garantiert. Dann kann auch eine lesende Transaktion Ressourcen binden, weil das DBMS ja auch von anderen Transaktionen gelöschte oder veränderte Datensätze für diese lesende Transaktion immer noch im alten Zustand vorhalten muss.
Antworten