Projekt Zeitmanagement

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Liebes Forum, wie in diesem Thread (viewtopic.php?f=5&t=39404&start=15) vorgeschlagen poste ich hier den GitHub Link zu meinem kleinen Lernprojekt. Eine Beschreibung steht in der dortigen Readme. Für ein Codereview, gerne auch in Teilen, wäre ich sehr dankbar. Ich bin Anfänger: mir geht es vor allem um Punkte wie Design, Umsetzung der Objektorientierung, durch Denkfehler umständliche Funktionen usw.
Vielen Dank im Voraus an alle Helfer!

GitHub: https://github.com/BeHo86/Stundenmanagement

PS: die Datenbank- und Config-Datei hab ich erstmal nicht beigelegt. Wenn jemand die Funktionalität tatsächlich testen will, kann ich das gerne nachholen.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Viel Schatten, aber auch Licht ;)

Eine recht unzusammenhaengende Liste von Dingen, die mir aufgefallen sind:

- viel zu grosse Funktionen/Methoden. Alles was ueber 10 Zeilen ist, wird schwierig nachzuvollziehen. Ich persoenlich finde auch die Strukturierung durch lokale Funktionen ok, mein Gehirn ist darauf gepolt die als gegeben zu nehme, und mich auf deren Verwendung zu konzentrieren - und erst anzuschauen, wenn ich muss. Andere sehen das anders, und wuerden das in eigene freie Funktionen oder Methoden packen.
- keine Trennung von Datenlogik und GUI. Das ist DER grosse Klassiker, und zB etwas, das durch TDD Entwicklung (bei der du nicht die GUI testen, sondern "nur" die verschiedenen Operationen auf deinem Rechenkern) vermieden worden waere.
- aehnliche Kritik: alles eine Datei. Stattdessen ein vernuenftiges Package mit Submodulen/packages (core, gui, test).
- unnoetiges verwenden von Exceptions als Kontrollstruktur:

Code: Alles auswählen

try:
			self.db_loc = self.conf['LOCATIONS']['db']
			if self.db_loc == '':
				raise Exception
			else:
				self.load_key()
except Exception as e:
schreibt sich simpler als

Code: Alles auswählen

db_loc = self.conf.get('LOCATIONS', {}).get('db')
if db_loc: # deckt sowohl None als auch "" ab
    self.load_key(db_loc)
else:
    ....
- die Database-Klasse, die ja ein netter Anfang einer Trennung von Logik und GUI ist, holt sich ihren Dateinamen ueber eine globale Variable! Die auch noch eine GUI Instanz ist! Das ist so voellig verdreht. Mach db_loc ein Konstruktor-Argument, und du benutzt Database auch in einer zukuenftigen Webanwendung etc.

- die Funktion load_jobs ist ziemlich grauslig. Ein *-Select, von dem dann nachtraeglich das erste Element entfernt wird? Warum nicht einfach gleich die explizite Liste der Spalten angeben? Sollte man eh immer machen, sonst ist man auf Defnitionsreihenfolge angewiesen und ggf. wenig robust, wenn sich das Schema aendert.

- generell *VIEL* zu viele "bare excepts". Die sind so gut wie immer ein Design-Smell, weil sie alles moegliche wegfangen. So etwas gehoert nur an ganz wenige, spezielle Stellen, wie zB ein Mainloop in dem alle Fehler einem logging zugefuehrt werden sollen, oder so etwas.

- das formatieren und DANN vergleichen von Datumswerten ist auch nicht gut. Damit machst du dich von einer Formatierung abhaenging, welche die Ordnung erhaelt. Besser ist es, gleich auf den Datumswerten zu arbeiten, und die *nur* fuer die eigentliche Praesentation zu formatieren.

So. Muss weiter machen, mal als Anfang.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Zeile 5: cp als Abkürzung von configparser ist gewöhnungsbedürftig, da cp so ziemlich alles heißen kann (meist copy). Das Modul wird auch nur ein einziges Mal verwendet, eine Abkürzung ist also auch noch überflüssig.
Zeile 8: Jeder Import sollte in einer eigenen Zeile stehen
Zeile 16: ein Frame sollte sich nicht selbst positionierne
Zeil 30ff: wenn die Lokale anders eingestellt ist, dann wohl aus gutem Grund. Sowas sollte nicht in einem Programm geändert werden.
Zeile 38ff: in der Funktion load_db_loc wird ein komplettes Fenster erzeugt. Das erwartet man von einer Funktion innerhalb eines Frames nicht. Das Konfigurationsfenster sollte eine eigene Klasse geben.
Zeile 44: keine inneren Funktionen, das sollte eine Methode sein
Zeile 45: loc kommt aus dem Nichts. Sollte wohl auch ein Attribut sein.
Zeile 53ff: try-Blöcke sollten so kurz wie möglich sein, also nur den KeyError abfangen und nicht generell alle Exceptions.
Zeile 100ff: für die Funktion load_key gilt das selbe wie für load_db_loc. Die ganzen command-Lambdas wären in einer eigenen Klasse auch schöne Methoden.
Zeile 140: dieser else-Block ist überflüssig
Zeile 157ff: im Prinzip das selbe wie für load_db_loc schon beschrieben.
Zeile 183: ein Frame sollte sich nicht selbst positionieren.
Zeile 211: time_formatted ist schon ein String
Zeile 212: return ist keine Funktion, also Klammern weg
Zeile 222: hatte ich schon erwähnt, dass verschachtelte Funktionen zu vermeiden sind?
Zeile 223: fehleranfällig, falls einer der Strings nicht die erwartete Länge hat.
Zeile 226: für Fenster eigene Klassen erzeugen
oh, das kommt ja noch ein paar Mal.

Zeile 483: x ist ein schlechter Name für eine Datum
Zeile 485: Du solltest Datums-Objekte vergleichen und nicht Strings, also start_from und end_from in datetime umwandeln, statt umgekehrt.
Zeile 487: y ist ein schlechter Name für ein Datum
Zeile 489: z ist ein schlechter Name für einen Job. Was soll dieses Inkrement überhaupt?
Zeile 495: x ist ein schlechter Name für einen Titel. .format benutzen
Zeile 496: das macht list(range()), sowieso überflüssig, weil das der Default von plot ist.
Zeile 498: Schreibfehler.
Zeile 516: was soll diese Liste?
Zeile 668: else weg
Zeile 759: niemals nackte excepts verwenden. Wenn irgendwo ein Fehler auftreten kann, diesen sinnvoll verarbeiten
Zeile 799: Cursors sind etwas kurzlebiges, sollte nicht an ein Attribut gebunden werden. c ist sowieso ein schlechter Name
Zeile 807: es gibt dict.items(). x ist ein schlechter Name für eine
Zeile 815: hier wird ein Datenbankdesignfehler deutlich. Spaltennamen sollten nicht parametrisiert sein.
Zeile 827: hier wird der Designfehler noch deutlicher
Zeile 835: keine *-SELECTS, Spaltennamen explizit angeben
Zeile 837: titles_new ist schon eine Liste. return ist keine Funktion, Klammern weg
Zeile 858: dass execute ein Cursor-Objekt zurückliefert ist nicht Standard, und sollte nicht benutzt werden. Keine *-SELECTS, dann braucht man auch nicht so umständlich die erste Spalte zu entfernen. Und schon wieder zeigt sich ein Problem mit Deinem Datenbankdesign.
Zeile 859: warum gibst Du kein Tuple zurück?
Zeile 862: keine nackten excepts. Was für Fehler erwartest Du hier.
Zeile 863: Eine Funktion sollte immer einen gleichartigen Rückgabewert haben. Wenn Du hier mit dem Fehler, der auftreten könnte nichts anfangen kannst, dann gibt die Excpetion einfach weiter.
Zeile 874: PrintPDF hört sich nicht wie ein Klassennamen an, weil er keine Objekt beschreibt, das sollte eigentlich eine Funktion sein.

Insgesamt ist der Code zu verschachtelt. Ein paar Gott-Klassen tun alles, auch Dinge, die man nicht von ihnen erwartet (z.B. Fenster erzeugen). Die Datenbankanbindung ist nicht gekapselt. Datenbankanfragen sollten nur einer Klasse, die keine GUI-Komponente ist, erfolgen. Das Datenbankobjekt sollte nicht an vielen Stellen erzeugt werden, sondern nur einmal und dann herumgereicht werden, wo es gebraucht wird. Spalte Deinen Code weiter auf. Eigene Klassen für jedes Fenster. Eine Funktion/Klasse sollte eine Aufgabe haben. Die Fehlerbehandlung muß vom Kopf auf die Füße gestellt werden. Exceptions nicht als Flags mißbrauchen und magische Rückgabewerte (None) nicht als Fehlerbehandlung.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Bild

Vielen Dank euch, so in etwa hatte ich mir das vorgestellt... WIe gesagt: ich hab ohne Coaching einfach mal drauflosgeschrieben und versucht, das, was ich über OOP gelesen hatte, umzusetzen. In der ersten Fassung war es sogar noch schlimmer, da gab es nur 3 Klassen: GUI, pdf und Datenbank; nun immerhin 2 mehr. Mein Gedanke war, dass ich Klassen und Funktionen nicht zu kleinteilig machen sollte: eine "ein-Funktion-Klasse" ist überflüssig... Einige andere Sachen, die ihr vorschlagt, sind mir ebenfalls durch den Kopf gegangen. Z.B.: soll ich lieber mehere Objekte der Datenbank Klasse erzeugen oder lieber eines und das dann als Argument rumreichen? Habe mich für ersteres entschieden, aber aus einem reinen Bauchgefühl heraus - dachte, das wäre so schöner.

Werde mir eure Kritik später in Ruhe ansehen und darauf eingehen. Vermutlich werde ich sowieso alles nochmal neu schreiben, aber meine Frau muss es bald nutzen können, deswegen sind einige Reparaturen an dieser Fassung vielleicht sinnvoll.

EDIT: generell bin ichmir unsicher, wann sich eine eigene Klasse lohnt. :K Teilweise kann ich mir eine vorstellen, nur um dann festzustellen, dass sie im Endeffekt aus einer einzigen Funktion bestehen würde. Also packt man diese Funktion zu einer bestehenden Klasse und so entstehen dann die Gottklassen... In den Lehrbüchern ist das (sicher zurecht) immer recht anschaulich beschrieben und auch nachvollziehbar. DIese Beispiele sind gut, um das Prinzip zu verstehen, taugen aber nicht viel für den Alltag. Dieses Programm ist eben etwas abstrakter als einen Haufen Tiere als Klassen zu modellieren.
BlackJack

@T.T. Kreischwurst: Wenn eine Klasse im Endeffekt nur eine Methode hat, dann ist es in der Regel eine Funktion. Die packt man dann nicht in eine andere Klasse sondern eben in eine Funktion. Man muss ja nicht alles zwanghaft in Klassen stecken.

Eine eigene Klasse lohnt sich, wenn sie Sinn macht. Also wenn man mehrere Werte sinnvoll zu einem Objekt zusammen fassen kann, könnte es schon Sinn machen. Noch mehr, wenn es dann noch Operationen auf diesen Werten gibt, die man als Methoden hinzufügen kann. Wenn es nur einen Wert im Objekt gibt, dann braucht man mehrere Operationen darauf damit es Sinn macht eine Klasse daraus zu machen. Eine Datenbank kann dafür ein Beispiel sein, wo man nur die Datenbankverbindung als Zustand hat, und Methoden die dann Operationen zur Verfügung stellen die von der konkreten Datenbank zur Anwendung hin abstrahieren.

Du scheinst das aber ein bisschen von der anderen Seite angegangen zu sein. Also nicht welche Daten habe ich und welche davon fasse ich wie in eigenen Klassen zusammen, sondern eher ich habe eine Klasse da kommt erst mal alles rein, und später schaue ich was davon eine eigene Klasse sein könnte. Das ist eher ungünstig.

Ich würde das umgekehrt angehen. Ohne Klassen anfangen, und dann sieht man recht schnell wo man anfängt zu viele Argumente an Funktionen zu übergeben, und welche davon sinnvollerweise eigentlich eine eigene Einheit bilden würden. Wobei der GUI-Teil einen recht schnell zu eigenen Klassen ”zwingt”, weil man dort für die Rückrufe „Funktionen mit Zustand über Aufrufe hinweg“ benötigt, also Methoden. Und damit dann Klassen.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

OK, der Reihe nach, erst @__deets__:
Ein *-Select, von dem dann nachtraeglich das erste Element entfernt wird? Warum nicht einfach gleich die explizite Liste der Spalten angeben?
Diese Kritik kam ja auch von Sirius. Es sind insgesamt rund 50 Spalten, die hier geladen werden sollen – fast der gesamte Inhalt der Datenbank. Jede einzelne im Select zu nennen, ist etwas...unhandlich. Daher dachte ich, ein * mit Abzug der ersten Spalte wäre zielführender.

Auch die bare excepts wurden mehrfach genannt. Danke für den Hinweis, werde hier stattdessen if/else verwenden, außer es gibt einen guten Grund.

Danke für den Hinweis zu den formatierten Datumswerten. Ich hatte schon vor dem Review vor, hier mal einzugreifen, weil ständig irgendwas hin- und herformatiert wird und so einfach nur Chaos erzeugt. Der Salat ist mit dem Programm angewachsen und irgendwann war es so eingerbeitet, dass es ohne großen Aufwand nicht mehr rauszunehmen war. Garstig.

Jetzt @Sirius:
Zeile 16: ein Frame sollte sich nicht selbst positionieren
OK. Das direkte Erben von der Frame-Klasse war ein Experiment, weil ich damit vorher noch nicht gearbeitet hatte. Mir ist nicht klar, wo ich den den Frame sonst positionieren sollte, außer ich breche die Struktur auf und erzeuge ihn „normal“, also nicht als Erbe der Superklasse.
Zeil 30ff: wenn die Lokale anders eingestellt ist, dann wohl aus gutem Grund
Auf meinem PC müsste ich auch nix ändern. Meine Frau benutzt einen Mac, und dort sind die TKInter Schaltflächen (z.B. Messagebox) ohne ein Ändern der Lokale standardmäßig Englisch. Obwohl im Prinzip kein Problem, entsteht so Sprachmischmasch und das sieht das nat. unschön aus.
Zeile 157ff: im Prinzip das selbe wie für load_db_loc schon beschrieben.
Mir ist es absolut recht, mehr Klassen einzufügen. Ich gliedere lieber einmal zu viel als zu wenig, aber ich hab echt Skrupel, eine Klasse aus zwei Funktionen (die wir z.B. bei den hier erwähnten Passwort Dialogen haben) zu schreiben. Ist das nicht etwas wenig?
Zeile 223: fehleranfällig, falls einer der Strings nicht die erwartete Länge hat.
Ich würde hier dann tatsächlich mit einem try/except arbeiten. Hat einer der Strings nicht die erforderliche Länge (z.B. 0, weil der Nutzer ein Dropdown Menü leer gelassen hat), wird ein ValueError geworfen. Das ist a) relativ denkbar und b) relativ präzise vorhersagbar und handhabbar, deswegen halte ich ein except ValueError für vertretbar. Oder?
Zeile 483: x ist ein schlechter Name für eine Datum [u.a.]
Ja. Verzeih mir, Meister, denn ich war unkreativ – werde es ausbessern. :oops:
Zeile 485: Du solltest Datums-Objekte vergleichen und nicht Strings, also start_from und end_from in datetime umwandeln, statt umgekehrt.

Siehe oben. Die Datumsgeschichten sind ziemlich außer Kontrolle und müssen dringend neu geschrieben werden.
Zeile 489: z ist ein schlechter Name für einen Job. Was soll dieses Inkrement überhaupt?
Das Inkrement fischt alle None-Werte aus der Liste raus. Dann weiß man, wieviele Viertelstunden gearbeitet wurden, kann das durch vier Teilen und bekommt die Stundenanzahl.
Zeile 496: das macht list(range()), sowieso überflüssig, weil das der Default von plot ist.
Das verstehe ich nicht ganz...
Zeile 498: Schreibfehler.
Zeile 516: was soll diese Liste?
Falls du das suptitle() meinst: ne, das soll eine Überschrift werden. Die Liste ist überflüssig, ein Relikt, das ich vergessenhabe zu entfernen. Ist raus.

Dann das Thema Datenbank. Da scheint ja das meiste im Argen zu liegen...daher zunächst mal allgemein: wie könnte man das Design verbessern? Die zahlreichen Spaltennamen habe ich ja schon angesprochen; ich nehme an, dass auf die suboptimale Konstruktion dieser Spaltennamen auch die Kritik an der Parametrisierung abzielt.
Ich muss in der Datenbank für jeden Tag verschiedene Tätigkeiten speichern, die zu unterschiedlichen Zeiten ausgeführt wurden. Dazwischen können „Pausen“ liegen, in denen nicht dokumentationspflichtige Arbeiten erledigt werden. Das zeitliche Raster reicht von 7:00 morgens bis 21:00 abends und muss leider im Abstand von 15 Minuten angelegt werden, um die Kleinteiligkeit korrekt abbilden zu können. Egal, wie ich es gedreht und gewendet habe: das Design der Datenbank war bei mit immer beschissen.
Viele der von euch erwähnten Fehler sind auf Workarounds zurückzuführen, die aufgrund dieses Designs nötig werden.

Konkret noch eine Frage:
Zeile 799: Cursors sind etwas kurzlebiges, sollte nicht an ein Attribut gebunden werden.
Naja, ich brauche den Cursor ja immer wieder für Tätigkeiten in der Datenbank. Demnach müsste ich in fast jeder Methode dieser Klasse erstmal einen neuen Cursor erzeugen – ist das nicht doppelter Code?

Insgesamt schlage ich vor, dass ich in GitHub den Code von Grund auf neu schreibe und einstelle. Bevor ich das mache: erstmal einen soliden Plan machen, wie das nun aussehen soll. Wieviele Module/Packages? Wo muss die bisherige Codestruktur besonders in weitere Klassen unterteilt werden, wo muss der ganze "Algorithmus" überarbeitet werden (z.B. Datenbank)? Usw.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@T.T. Kreischwurst: ich sagte nicht, Du sollst nicht von Frame erben. Aber Deine »Wrapper« soll nicht in »__init__« »self.pack« aufrufen, sondern das passiert im Hauptprogramm. Auf diese Weise kann man Wrapper flexibler einsetzen, wobei »main_window« dafür auch der falsche Name ist. Das Hauptfenster heißt bei Dir »root«. Bei GUI-Programmen hat man sehr schnell die notwendigkeit einer eigenen Klasse. Dein Passwortfenster erbt ja von TopLevel und ist damit auch schon eine vollwertige Klasse, auch wenn da nur ein __init__ und noch eine Methode für den Knopf dazukommt.

Das schwierigste zu ändern ist das Datenbankdesign, da damit Deine gesamte Logik neu geschrieben werden muß. Du hast Jobs, die haben einen Start und ein Ende oder bestehen aus vielen Zeitabschnitten mit Pausen dazwischen? Im zweiten Fall brauchst Du zwei Tabellen, eine, in der alle zum Job gehörenden Informationen stehen und eine zweite, in der Start- und End-Zeiten zusammen mit einer Job-ID stehen. Dann ist das Viertelstundenraster kein Zwang der Datenbank mehr, sondern nur noch eine Konvention, um die Eingabe zu erleichtern. Das Finden von Job- und freien Zeiten besteht dann aus dem Suchen aller Jobs an einem bestimmten Tag und dem Sortieren nach den Startzeiten.
BlackJack

@T.T. Kreischwurst: Die 50 Spalten sind zu viel. Das führt ja auch dazu das Du für Abfragen Spaltennamen dynamisch generierst und in das SQL einfügst. Also letztendlich das Daten in den Spaltennamen kodiert sind, die eigentlich als Werte in eine Tabelle gehören.

Ich habe hier ein bisschen das Gefühl das Du bei der Datenbank zu sehr von einer Tabelle ausgehst wie Du sie manuell auf Papier oder in einer Tabellenkalkulation strukturieren würdest.

Als Locale würde ich '' (die leere Zeichenkette) setzen, denn dann wird die Locale gesetzt die im System eingestellt ist. Dann muss man nicht zwischen den verschiedenen Formaten der Betriebssysteme für diese Zeichenkette unterscheiden und man ist auch nicht auf Deutsch beschränkt wenn der Benutzer eine andere Sprache eingestellt hat.

Eine Klasse aus zwei Methoden ist völlig okay wenn man es nicht mit Funktionen ausdrücken kann. Also übersichtlich, denn die verschachtelten Funktionen die Du da hast finde ich alles andere als übersichtlich. Bei `load_db_loc()` zum Beispiel die innere Funktion die auf `loc` zugreift das in der äusseren Methode erst weiter unten und dann auch noch in einer Ausnahmebehandlung erzeugt wird. Wenn das eine Klasse wäre, dann hätte man eine `__init__()` in der man deutlich sehen würde wo `loc`, also in dem Fall `self.loc` (und vielleicht mit einem besseren Namen) definiert wurde.

Bei den Dialogfenstern würde ich zudem nicht selbst was basteln sondern auf die Dialogklassen aus der Standardbibliothek zurückgreifen. Die regeln dann auch gleich so Sachen, dass die Fenster modal sind. Weil ich sehe da ein `master.widthdraw()` aber es ist doch recht umständlich von dort aus im Quelltext dann heraus zu finden wo das wieder angezeigt wird, und ob das auch korrekt bei allen möglichen Programmabläufen von dort ausgehend passiert.

Ad Zeile 223: Hier geht es nicht nur darum ob was leer bleibt, sondern dass es nicht eindeutig ist. Was ist die Eingabe '1112016'? Ist das der 11.1.2016 oder der 1.11.2016? Da wird keine Ausnahme ausgelöst! Es ist einfach nur unmöglich eines der beiden Daten einzugeben. Und wenn der Tag leer gelassen wird und als Monat der 12. genommen wird, dann bekommt man auch keine Ausnahme sondern den 1.2.2016.

Auf der anderen Seite gibt es einen `ValueError` wenn man zum Beispiel den 31.02.2016 versucht, denn das ist kein gültiges Datum.

Hier könnte man beispielsweise mit einem eigenen Dialog dafür sorgen das der Benutzer gar nicht erst Ok drücken kann solange die Eingabe nicht gültig ist. Die Dialogbasisklasse aus der Standardbibliothek sieht bereits eine Validierungsmethode vor die man überschreiben kann.

Zur Planung: Ich würde mit dem Datenbankentwurf anfangen. Welche Daten gibt es und welche Operationen müssen darauf ausgeführt werden, also wie sehen die Zugriffsmuster aus.

Darauf kann man dann Code aufbauen der die Daten und Operationen auf der Datenbank im Programm abbildet.

Wieviele Module/Packages ist eine Frage die man nicht irgendwie am Anfang beantwortet. Das ergibt sich ja im Laufe der Zeit. Ich fange meist mit einem einzigen Modul an, es sei denn es ist klar, dass es mehr werden. Dann mit einem Package mit einer ``__init__.py`` und wenn ich schon weitere Module sicher weiss, dann lege ich die schon an. Dabei immer im Kopf behalten, dass man ein Modul sehr einfach in ein Package umwandeln kann wenn es zu gross wird und man es weiter unterteilen möchte. Also man muss nicht am Anfang bereits die komplette Package-Struktur festlegen.

Sich Gedanken über die bisherige Codestruktur und wie man die in weitere Klassen unterteilen kann zu machen, halte ich für einen suboptimalen Ansatz. Ich würde da eher von unten her rangehen und das wirklich neu aufbauen. Wie Sirius3 schon geschrieben hat, durch die Änderung des Datenbankentwurfs ändern sich zu viele Dinge im Code.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Ich gehe auf die anderen genannten Punkte später nochmal ein, zunächst mal (weil es wirklich die Grundlage fürs Neuschreiben ist) will ich kurz das Datenbankdesign aufgreifen: ich glaube nach reiflicher Überlegung, dass ich sogar noch mehr Tabellen brauche. Grund: ich will für ein Datum Jobs und zugehörige Zeiten speichern. Da derselbe Job mehrfach zu unterschiedlichen Zeiten vorkommen kann, gibt es zwei Möglichkeiten: entweder sehr umfangreiche von-bis Felder, die mehrere Werte enthalten (z.B. 08:15 bis 09:00; 14:00:15:00). Das würde ich ablehnen: sehr aufwnedig zu parsen, außerdem sollte wohl jedes Datenbankfeld eindeutig sein. Möglichkeit zwei: eine Job ID mit mehreren von-bis Werten verknüpfen, was seinerseits mit mehreren Datumsangaben verküpft werden kann. Letztlich sind das also n:m Beziehungen und beim Datum eine 1:n. Jeder Job kann zu mehreren Zeiten ausgeführt werden und beides wird in dieser Kombination an exakt einem Datum gespeichert.
Also:

1) Jobs:
ID | Bezeichnung
1 | Nasenbohren
2 | Nase putzen
usw

2) Zeiten:
ID | von | bis
1 | 07:00 | 07:15
2 | 07:15 | 07:30
usw.

4) Hilfstabelle 1:
ID | ZeitID | ArbeitsID
1 | 2 | 1


5) Hilfstabelle 2 (die Mastertabelle, Primärschlüssel kann Datum sein):
Datum/ID | Hilfstabelle1ID (=die Arbeitskombi für diesen Tag)
2016-11-1 | 1


Wird von der Implementierung im Code zuerst aufwendiger, aber im Endeffekt wohl sauberer. Was meint ihr?
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@T.T. Kreischwurst: das siehst Du richtig. Ein kompliziertes von-bis-Feld ist keine gute Idee, weil man es nicht innerhalb der Datenbank verarbeiten kann. Aber auch Dein jetziges Design ist noch zu kompliziert. Warum trennst Du Zeit vom Datum? Die zusätzliche Datums-Tabelle macht es doch nur umständlicher. Es reichen zwei Tabellen mit einer 1:n-Beziehung zwischen JobId und Zeiten.

1) Jobs:
Job_ID | Bezeichnung
1 | Nasenbohren
2 | Nase putzen

2) Zeiten:
Job_ID | von | bis
1 | 2016-11-1 07:00 | 2016-11-1 07:15
2 | 2016-11-1 07:15 | 2016-11-1 07:45
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Stimmt. In meinem Entwurf bin ich noch fest davon ausgegangen, dass das Datum ein Wert ist, der nur einmal vorkommen sollte und von den anderen Werten getrennt behandelt wird. Das stammt noch aus dem vorherigen Entwurf, der wohl tatsächlich zu sehr an einer Exceltabelle orientiert war. Packt man ihn zur Uhrzeit (wogegen nichts spricht, da die Behandlung im Code dann durch Datetime Objekte recht unkompliziert möglich ist), wird das ganze schlanker.
Darauf werde ich nun aufbauen und mit dem Neuschreiben der Datenbanklogik beginnen.
Als Locale würde ich '' (die leere Zeichenkette) setzen, denn dann wird die Locale gesetzt die im System eingestellt ist
Das hatte ich ursprünglich auch so. Aus irgendeinem Grund hat das Mac meiner Frau ignoriert... Werde es aber nochmal mit den leeren Anführungsstrichen testen.
Bei den Dialogfenstern würde ich zudem nicht selbst was basteln sondern auf die Dialogklassen aus der Standardbibliothek zurückgreifen
Meinst du die Messagebox-Sachen? Alles andere, was schnelles Googeln mir zu dem Thema ausgespuckt hat, war auch ein Konstrukt aus Toplevel+x
Hier geht es nicht nur darum ob was leer bleibt, sondern dass es nicht eindeutig ist. Was ist die Eingabe '1112016'?
Erwischt. Hätte ich nie dran gedacht, guter Punkt. Noch ein Argument für die grundlegende Umstrukturierung der Datumsangaben. Ich bilde mir sogar ein, dass datetime Objekte hashable und damit als Schlüssel für Datenbanken nutzbar sind, aber das prüfe ich npchmal.
BlackJack

Ich meine `tkinter.simpledialog.Dialog`, das als Basisklasse für eigene, modale Dialoge gedacht ist.
BlackJack

Zum Thema Klasse mit wenig Methoden: Ich bin ja Verfechter davon nicht alles in Klassen zu stopfen sondern auch Funktionen zu schreiben wenn man keine Klassen braucht, also wenn es nur eine `__init__()` und eine weitere Methode gibt ist das ein Zeichen das man über die Klasse noch mal nachdenken sollte. Heute morgen habe ich dann aber mal drei Klassen geschrieben die *gar keine* eigene Methode haben. Für Tag 8 vom Advent of Code habe ich die Operationen auf dem Display so modelliert:

Code: Alles auswählen

class Operation(object):
    
    REGEX = None
    DISPLAY_METHOD_NAME = None

    def __init__(self, operand_a, operand_b):
        self.operand_a = operand_a
        self.operand_b = operand_b

    def __call__(self, display):
        getattr(display, self.DISPLAY_METHOD_NAME)(
            self.operand_a, self.operand_b
        )

    @classmethod
    def try_parse(cls, string):
        match = cls.REGEX.match(string)
        return cls(int(match.group(1)), int(match.group(2))) if match else None


class RectangleSwitch(Operation):

    REGEX = re.compile(r'rect (\d+)x(\d+)')
    DISPLAY_METHOD_NAME = 'switch_rectangle_on'


class RowRotate(Operation):

    REGEX = re.compile(r'rotate row y=(\d+) by (\d+)')
    DISPLAY_METHOD_NAME = 'rotate_row'


class ColumnRotate(Operation):

    REGEX = re.compile(r'rotate column x=(\d+) by (\d+)')
    DISPLAY_METHOD_NAME = 'rotate_column'
Das ist während des Schreibens entstanden. Geplant hatte ich die Operationen als Klassen zu modellieren um Argumente, Anwendung der Argumente, und das Parsen zusammenfassen zu können. Und als ich dann nach `RectangleSwitch` mit `RowRotate` angefangen hatte, fiel mir auf, dass ich bei allen drei Methoden im Grund das selbe schreibe, nur mit anderen Namen für die Methode die auf dem Display aufgerufen wird und einem anderen Muster für `try_parse()`. Also habe ich alles aus `RectangleSwitch` in eine Basisklasse `Operation` verschoben und etwas generischer gemacht.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

So, ich hab mir auf Basis des Feedbacks einen neuen Entwurf für die Datenbank überlegt. Der sollte theoretisch durch ein externes Modull testbar sein (das ich allerdings noch nicht geschrieben habe, wollte das Design erstmal reviewen lassen).
So richtig zufrieden bin ich noch nicht:
  • Verwendung der Cursor richtig? Lt. Blackjack ja kurzlebig, daher nicht als Attribut
  • Die Tabelle Jobs hat m.E. nicht so richtig viel Sinn, außer zur erzwungenen Vereinheitlichung der Daten (vgl. Kommentar im Code)
  • Die Klasse ist schon wieder recht lang, aber ich konnte keine sinnvollen Trennlinien finden
  • Habe noch nie ein Testmodul geschrieben. Blöse Frage: ist das einfach ein Modul, das das Datenbankmodul importiert und die Methoden unter verschiedenen Kondition aufruft, dann prüft, ob der Rückgabewert stimmt?
Hier der Code mit Dank im Voraus fürs Feedback! Ich hoffe, er ist besser als mein alter:

Code: Alles auswählen

import sqlite3

class Database():
	
	def __init__(self, filepath)
		self.connection = sqlite3.connect(filepath)
		self.create_tables()
		
	def create_tables(self):
		cursor = self.connection.cursor()
		cursor.execute(''' CREATE TABLE IF NOT EXISTS jobs (
				jobId INTEGER PRIMARY KEY NOT NULL,
				jobtitle TEXT'''
				)
		cursor.execute(''' CREATE TABLE OF NOT EXISTS times (
				timesId INTEGER PRIMARY KEY NOT NULL,
				time TEXT NOT NULL,
				jobsDone INTEGER,
				FOREIGN KEY(jobsDone) REFERENCES jobs(jobsId)''')
		self.connection.commit()
		
	def insert_job(self, job_name):
		cursor = self.connection.cursor()
		cursor.execute(''' INSERT INTO jobs VALUES (?)''', (job_name,))
		self.connection.commit()
	
	def map_job_to_id(self, job):
		# Um z.B. neue Einträge in der Tabelle times machen zu können, muss die ID des Jobs für den Fremdschlüssel bekannt sein
		cursor = self.connection.cursor()
		jobID = cursor.execute(''' SELECT jobId FROM jobs WHERE jobtitle = ?''', (job,))
		return jobID	
		
	def check_if_time_exists(self, time):
		cursor = self.connection.cursor()
		exisiting_time = cursor.execute(''' SELECT timesId FROM times WHERE time=(?)''', (time,))
		if existing_time:
			return exisiting_time
		
	def insert_time(self, job, date_time):
		cursor = self.connection.cursor()
		jobID = map_job_to_id(job)
		exisiting_time = check_if_time_exists()
		if existing_time:
			cursor.execute(''' UPDATE times SET jobsDone = ? WHERE time = ?''', (jobID, exisiting_time))
		else:
			cursor.execute(''' INSERT INTO times VALUES (?, ?)''', (date_time, job)
	
	def get_jobs(self, time):
		cursor = self.connection.cursor()
		jobs = cursor.execute(''' SELECT jobsDone WHERE time = ?''', (time,))
		return jobs
		
	def remove_job(self, job):
		# Das wird Probleme machen, wenn der Nutzer einen Job löschen will, den er nicht mehr braucht - auf den aber noch Einträge im "times"-table verweisen.
		# Man könnte natürlich einfach reinen Tisch machen und erst alle Einträge aus times mit einer Referenz auf die betroffene jobID löschen, aber das ist faktisch nicht erwünscht.
		# Lösung: Tabelle "jobs" einsparen und die Jobnamen direkt setzen. Nachteil: keine klare Strukturierung der Daten mehr; vertippt sich der Nutzer bei einem Job, existiert der für das System zweimal...
		cursor = self.connection.cursor()
		cursor.execute(''' DELETE FROM jobs WHERE jobtitle = ?''', (job,))
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@T.T. Kreischwurst: Vergleiche mit Strings will man eigentlich vermeiden, daher ist die Job-Tabelle inklusive Job-ID schon ok. Und es gibt auch kein absolut zu lang. Wenn eine Klasse halt viele Methoden hat, dann sollte die einzelne Methode nicht zu lang sein, aber für die Klasse selbst ist das auch ok.
Testmodul ist ungefähr so, wie Du es beschreibst. Es gibt einige Test-Frameworks die das Aufrufen der Tests vereinfachen, z.B. pytest.

Irgendwie habe ich das Gefühl, dass Du immer noch an Deinem Zeitraster hängst, das mit Jobs gefüllt wird. Halte ich für nicht so geeignet, da Du dann ja immer alle Zeiten vorhalten mußt, auch wenn grad Urlaubszeit ist. Und falls das Programm dann von zwei oder mehr Leuten benutzt wird, geht's dann gar nicht mehr.
BlackJack

@T.T. Kreischwurst: Bei den Tabellennamen würde ich die Einzahl wählen, denn letztendlich ist so eine Beschreibung wie bei Klassen die Beschreibung *eines* Eintrags. Spätestens wenn man ein ORM verwendet fällt einem diese Parallele sehr deutlich auf.

Bei den IDs bist Du bei den Namen inkonsistent was Ein- und Mehrzahl angeht. Ich würde Präfixe dort weglassen wenn sie den eigenen Tabellennamen wiederholen, denn dann hat der Präfix keinen Mehrwert. Also in beiden Tabellen einfach nur `id` für den Primärschlüssel.

Gross-/Kleinschreibung würde ich bei SQL-Datenbanken vermeiden. Manche unterscheiden das, manche nicht, bei manchen hängt es von der Konfiguration ab. Kleinbuchstaben mit Unterstrichen funktionieren bei allen gleich.

`times` würde ich `slot` oder `time_slot` nennen damit man nicht für Tabelle, also ein Objekt, den gleichen Namen hat wie für ein Attribut des Objekts. Das führt dann gerne mal zu Verwirrungen ob an den Namen `time` jetzt so ein komplettes Objekt oder nur der Wert des `time`-Attributs gebunden ist.

Generell ist es eine gute Idee die Namen zwischen Programm und Datenbank zu ”synchronisieren”. Wenn das in der Datenbank `job_title` heisst, sollte es im Programm nicht `job` oder `name` heissen, sondern auch `job_title`. Oder eben auch in der Datenbank `name` wenn man es im Programm `name` nennt. Oder nur `title`, denn `job.job_title` ist auch wieder redundant und enthält nicht mehr Informationen wie `job.title`. Bei Fremdschlüsseln macht so ein Präfix vor `_id` Sinn, damit man sieht in welche Tabelle der Schlüssel verweist. Also eher `slot.job_id` als `slot.job_done`. Von dem Namensmuster würde ich erst abweichen wenn es mehr als einen Fremdschlüssel in einer Tabelle auf eine andere gibt.

`time` sollte auch in der Datenbank den entsprechenden Typ haben, also TIMESTAMP oder DATETIME. Und man sollte `sqlite3` dann auch sagen das solche Typen zu Python's `datetime.datetime` abgebildet werden sollen. Ich vergesse immer wieder wie man das macht, weil SQLAlchemy schon automatisch das ”richtige” tut. :-)

Da die Jobtitel/-namen nicht eindeutig sind, geht das mit `map_job_to_id()` und `remove_job()` so nicht. Beim Abfragen nennst Du den Wert jobID obwohl a) bei der Abfrage mehr als eine ID herum kommen kann und b) der Rückgabewert von `cursor.execute()` nicht definiert ist. Du willst da vielleicht vom Cursor das Ergebnis abfragen und ihn auch wieder schliessen bevor die Methode verlassen wird.

Bei anderen Methoden machst Du diesen Fehler auch.

Du verwendest Name oder Zeit als Identifizierung von Datensätzen. Dafür sind aber die IDs da! `remove_job()` löscht *alle* Jobs mit dem gleichen Namen/Titel. Die DB-API 2.0 beschreibt als optionale Erweiterung ein `lastrowid` auf `Cursor`-Objekten das die ID des zuletzt mit `execute()` eingefügten Datensatzes von diesem `Cursor` enthält. `sqlite3` hat das implementiert. Das sollte der Rückgabewert sein wenn man einen Datensatz anlegt. Beziehungsweise sollte das Bestandteil des Rückgabewertes sein wenn man anfängt mit Klassen für die Werte im Programm zu arbeiten. Und mit der Zeit bastelt man sich so sein eigenes ORM. Erwähnte ich SQLAlchemy schon mal? ;-)

Bei `get_jobs()` fehlt der Tabellenname in der Abfrage.

Wenn das löschen der Zeiten nicht erwünscht ist wenn man einen kompletten Job löscht, dann sollte auch das löschen von `job`-Einträgen nicht erwünscht sein. Denn die Zeiten verweisen dann auf einen nicht mehr existierenden Eintrag in einer anderen Tabelle. Das löst man üblicherweise mit einem Flag das `job`-Einträge als ”gelöscht” oder nicht mehr ”aktiv” markiert. Eine weitere Möglichkeit wäre ein Datumsfeld welches den Zeitpunkt der Deaktivierung oder NULL enthält wenn das für die Verarbeitung der Daten wichtig ist. Dann kann man sich auch rückwirkend für beliebige Zeitpunkte die aktiven Jobs filtern. Falls Jobs auch ein Anfangsdatum haben, könnte man das natürlich auch vermerken.

Apropos Flags: Hier hat SQLAlchemy den Vorteil einen BOOLEAN-Typ zu unterstützen auch wenn die dahinterliegende Datenbank den Typ nicht kennt.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Ne Menge Holz...
@BlackJack: ich verstehe dein Feedback so, dass du im ersten Teil eher Design-/Stilfehler ansprichst und danach wirkliche Bugs, also semantische oder Syntaxfehler. Auf erstgenannte gehe ich nicht weiter ein, sondern versuche sie einfach umzusetzen.
Gross-/Kleinschreibung würde ich bei SQL-Datenbanken vermeiden. Manche unterscheiden das, manche nicht, bei manchen hängt es von der Konfiguration ab. Kleinbuchstaben mit Unterstrichen funktionieren bei allen gleich.
Frage: meinst du die Bezeichner der Tabellen und Spalten oder die eigentlichen SQL Statements (z.B. SELECT FROM WHERE usw.)? Die Großbuchstaben bei der SQL Syntax finde ich nämlich z.B. sehr angenehm, weil man so eigenes von SQL-spezifischem leichter/schneller trennen kann.
`time` sollte auch in der Datenbank den entsprechenden Typ haben, also TIMESTAMP oder DATETIME. Und man sollte `sqlite3` dann auch sagen das solche Typen zu Python's `datetime.datetime` abgebildet werden sollen. Ich vergesse immer wieder wie man das macht
http://pythoncentral.io/advanced-sqlite ... in-python/ Anleitung, wie's gemacht wird. Das erfordert demnach sowieso einige Anpassungen am Design, weil der Colname angepasst werden muss.
Da die Jobtitel/-namen nicht eindeutig sind, geht das mit `map_job_to_id()` und `remove_job()` so nicht. Beim Abfragen nennst Du den Wert jobID obwohl a) bei der Abfrage mehr als eine ID herum kommen kann und b) der Rückgabewert von `cursor.execute()` nicht definiert ist. Du willst da vielleicht vom Cursor das Ergebnis abfragen und ihn auch wieder schliessen bevor die Methode verlassen wird.
Hier bin ich mir nicht ganz sicher, ob ich dich richtig verstanden habe. Dass mehr als eine ID ausgegeben werden kann, habe ich gerafft: z.B. wenn der User einen Job mit gleichem Namen zweimal anlegt. Meinst du aber mit dem zweiten Teil, dass ich das fetchone()/fetchall() vergessen habe? Das stimmt in der Tat... :oops:
Du verwendest Name oder Zeit als Identifizierung von Datensätzen. Dafür sind aber die IDs da! `remove_job()` löscht *alle* Jobs mit dem gleichen Namen/Titel. Die DB-API 2.0 beschreibt als optionale Erweiterung ein `lastrowid` auf `Cursor`-Objekten das die ID des zuletzt mit `execute()` eingefügten Datensatzes von diesem `Cursor` enthält. `sqlite3` hat das implementiert. Das sollte der Rückgabewert sein wenn man einen Datensatz anlegt. Beziehungsweise sollte das Bestandteil des Rückgabewertes sein wenn man anfängt mit Klassen für die Werte im Programm zu arbeiten. Und mit der Zeit bastelt man sich so sein eigenes ORM.
Das verstehe ich auch nicht ganz, sorry... Das Programm soll ja alle Namen mit gleichem Titel löschen, denn identische Jobs können nur durch Nutzerfehler entstehen. Ansonsten soll jeder Job nur einmal vorhanden sein. Bzgl. des Rückgabewertes beim Anlegen eines Datensatzes: :arrow: :K
Wenn das löschen der Zeiten nicht erwünscht ist wenn man einen kompletten Job löscht, dann sollte auch das löschen von `job`-Einträgen nicht erwünscht sein. Denn die Zeiten verweisen dann auf einen nicht mehr existierenden Eintrag in einer anderen Tabelle. Das löst man üblicherweise mit einem Flag das `job`-Einträge als ”gelöscht” oder nicht mehr ”aktiv” markiert.
Das klingt interessant. Ich würde das so lösen, dass in remove_jobs() erstmal geprüft wird, ob es in "times" (werde ich noch umbennen) Einträge gibt, die diese JobID haben. Wenn ja, würde ich eine neue Methode aufrufen, die die Flags entsprechend setzt (weiß allerdings nicht, wie genau das aussehen soll!) und dann den Job aus der Tabelle streicht. Wenn nicht, wird der Job "normal" gelöscht.
BlackJack

@T.T. Kreischwurst: Bezeichner sind Namen die der Benutzer frei vergeben kann um ”Dinge” zu bennenen. SELECT, FROM, WHERE, und so weiter sind Schlüsselworte. Und die schränken bei vielen Sprachen die Bezeichner ein, weil sie nicht als solche verwendet werden dürfen. Wenn man das nicht macht, dann kann man so schöne Sachen wie ``if if then else else then`` schreiben und der Benutzer darf dann herausfinden was Schlüsselwort ist und was eine Variable oder ähnliches bezeichnet. :-)

Ah, genau `PARSE_DECLTYPES` & Co war das. Wie gesagt, ich verwende SQLAlchemy, da braucht man sich um so etwas nicht kümmern. Da ”deklariert” man eine Spalte als DATE oder BOOLEAN und bekommt bei jedem DBMS `datetime.date`- und `bool`-Werte, egal ob die Datenbank das tatsächlich als eigenen Datentyp unterstützt oder nicht.

Richtig im zweiten Teil meinte ich `fetch*()` und vor allem mindestens bei `fetchone()` auch das schliessen des Cursors, denn sonst kann einem da eine Ausnahme um die Ohren fliegen. Es gibt DBMS die es nicht mögen wenn man einen Cursor nicht schliesst, weil das Ressourcen belegt die sonst nicht freigegeben werden. Wenn die Datenbank nicht weiss wann man mit einem Cursor fertig ist, kann es auch sein das eine Transaktion nicht beendet wird bis die Sitzung geschlossen wird.

Wenn jeder Titel nur einmal vorkommen darf, dann sollte man gar nicht erst mehrfach den gleichen Speichern können. Da gehört dann ein UNIQUE-Constraint auf die Spalte mit dem Titel. Und auch dann würde ich im Programm mit der ID des Titels für Abfragen arbeiten und nicht mit der Zeichenkette. Und NOT NULL ist der dann auch.

Der Rückgabewert beim Anlegen eines Datensatzen sollte dessen ID sein. Denn das ist das was ihn eindeutig IDentifiziert und das in der Datenbank auch schnell und Effizient. Man kann auch mehr als die ID zurückgeben, zum Beispiel ein Objekt das die Werte des Datensatzen enthält. Und vielleicht auch eine Referenz auf das `db`-Objekt, denn dann kann man dem Objekt Methoden verpassen um sich selbst zu speichern oder um vom Objekt andere Objekte aus der Datenbank zu laden die mit diesem Objekt in Beziehung stehen. Wie gesagt, man bastelt sich so ziemlich schnell ein eigenes ORM. Was es mit SQLAlchemy schon fertig gibt. :-)

Welche Flag willst Du bei den `times` setzen? Sollen die einzeln (de)aktivierbar sein oder nur der Job mit allen seinen Zeiten? Im letzteren Fall braucht man so ein Flag nur beim Job. Denn ob eine Zeit aktiv ist oder nicht kann man ja dann ermitteln in dem man den zugehörigen Job danach fragt.
T.T. Kreischwurst
User
Beiträge: 52
Registriert: Dienstag 2. Februar 2016, 10:56

Ah, Antwort von Blackjack *grabs keyboard, opens editor* :D
Bevor ich eine angepasste Version des Codes poste: unklar sind mir aktuell eigtl. nur die Punkte "ID als Rückgabewert" und Flags.
Ich verstehe nicht ganz, was ich mit der zurückgegebenen ID nach dem Einfügen eines Datensatzes machen soll. Irgendwie validieren? :K Letztlich brauche ich die ID zwar, aber nicht unmittelbar nach dem Eintragen eines Datensatzes, sondern viel später. Z.B. wenn ich irgendwann ein Datum mit den zugehörigen Jobs anfragen will...
Bei den Flags würde ich mir das so vorstellen, dass ich den Job mit allen zugehörigen Zeiten deaktiviere, nicht einzeln. Dass also dann bei einer Zeit steht: "inaktiver Job" oder sowas in der Art. Die Flag wäre in meiner VOrstellung dann ein weiteres Feld in der Job-Tabelle, das standardmäßig den Wert 1 (aktiv) hat und bei Deaktivierung des Jobs auf 0 gesetzt wird. Beim Anfragen der Zeiten/Jobs muss dann zusätzlich der Wert der Flag geprüft und bei inaktiven Jobs die Ausgabe des "Jobtitels" entsprechend geändert werden. Keine Ahnung, ob das im Sinne des Erfinders ist - wäre mein Ansatz.
BlackJack

@T.T. Kreischwurst: Die ID identifiziert den Datensatz in der Datenbank. Also muss man sich die merken wenn man wieder etwas mit dem Datensatz machen möchte. Du scheinst an der Stelle immer wieder über den Jobtitel gehen zu wollen. Das geht hier in diesem speziellen Fall aber auch nur weil der Jobtitel ebenfalls ein Schlüsselkandidat ist. Das ist ja nicht immer der Fall, und muss auch nicht so bleiben, und ist auch nicht so effizient nachschlagbar wie die ID.

Flags: In Deinem vorletzen Beitrag klang das noch so als wenn Du den Job-Datensatz tatsächlich löschen wolltest und bei den Zeiten zu dem Job ein Flag setzen wolltest.
Antworten