Dokumentation - Herder-IT-Solutions/HerderBib GitHub Wiki

Hier soll eine technische Dokumentation des Codes folgen.

Übersicht

Das Projekt wurde mithilfe von vier mehr oder weniger unabhängigen Entwicklerteams realisiert. Jedes Team hat sich einem anderen Programmteil gewidmet. Die „Datenbankengruppe“ verwaltet die Unit „DBConnection" und setzt das grundsätzliche Datenbankenmodell um und stellt Funktionen zur Datenabfrage und Manipulation zur Verfügung. Im MVC-Konzept würde man ihr die Funktion des „Modells" zuweisen

Die „GUI-Gruppe“ ist zuständig für die Unit „gui“ und somit für die Lazarus Form, den für den Nutzer sichtbaren Teil. Der Name ergibt sich aus dem englischen Graphical User Interface, was eine geläufige Bezeichnung für Benutzeroberflächen ist. Sie stellt Datensätze aus der Datenbank anschaulich dar, lässt den Nutzer Daten manipulieren und sendet diese Nutzerinteraktionen mehr oder weniger direkt an die „Management“ Gruppe. Der GUI fällt einerseits die traditionelle Aufgabe des „Views“ zu, da sie aber auch schon viele Nutzereingaben interpretiert, übernimmt sie teilweise Aufgaben, die ursprünglich in die Zuständigkeit des s.g. „Controllers“ fallen.

Die Management Gruppe (uManagement Unit) bildet das Bindeglied zwischen View und Management, in dem es die Anfragen der GUI entgegennimmt, an DBConnection weiterleitet und gegebenenfalls von dieser wieder Daten an die GUI gibt, damit sie dargestellt werden können. Management übernimmt außerdem die Erstellung der 8-stelligen Schüler- und Buch-IDs samt Prüfziffer und das einpflegen neuer Schülerdatensätze in die Datenbank per CSV-Dateiformat, beides Aufgaben die somit von der GUI unabhängig gewartet werden können. uManagement lässt sich anhand des MVC-Modells am ehesten als Controller-Gruppe einordnen.

Ebenso als eigenständige Einheit sind die Generierung und der Druck von Schüler- bzw. Buch-Barcodes angelegt. Dies wird in der Unit „uBarcodePrint“ umgesetzt. Sie übernimmt alle Aufgaben, die von dem Nutzerbefehl „Barcodes-Drucken“ und der damit verbundenen Übergabe einer von der GUI aggregierten „Druckwarteschlange“, bis zum Druck mehrerer Barcodes auf ein dafür vorgesehenes Papier mit Klebebeschichtung.

Diese Modularisierung ermöglicht es diese Programmteile möglichst unabhängig voneinander implementieren, warten und auch erweitern zu können. Naturgemäß sind View und Controller immer noch abhängig vom Modell, dessen Änderungen also immer auch Auswirkung auf das restliche Programm haben. Jedoch könnten komplett neue Nutzeroberflächen gestaltet werden, ohne das Modell zu beeinträchtigen. Ebenso ist eine Veränderung der Barcode-Logik denkbar oder sogar eine Umstellung auf QR-Codes, ohne den restlichen Code groß ändern zu müssen.

Um sich in den einzelnen Programmteilen zurechtzufinden, sind die Klassen und Funktionsnahmen so selbsterklärend wie möglich gewählt. Außerdem finden sich besonders in uManagement und DBConnection Kommentare, die z.B: die Auswirkungen und Rückgabewerte von Funktionen erleutern. Zu großen Teilen ist diese Namensgebung und Kommentierung in englischer Sprache erfolgt, da sich die meisten Gruppenmitglieder damit wohler fühlten.

Es war die Absicht des Entwicklerteams den Quellcode nachvollziehbar zu Strukturierung damit sich dritte einfach einarbeiten können. Um diesen Prozess weiter zu erleichtern, stellen im Folgenden die jeweiligen Unterteams ihren Programmteil nocheinmal genauer dar.

Hinweis: Das Projekt lässt sich sowohl auf 32Bit als auch 64Bit Maschinen mit Lazarus kompilieren, jedoch wird erfahrungsgemäß eine 32Bit Version von Lazarus selbst benötigt, um ein problemloses Arbeiten mit dem Projekt zu ermöglichen.

Datenbank

"Model"

ER-Modell

ER-Modell

HerderBib speichert seine Daten innerhalb einer SQLite 3-Datenbank. Vorteile des SQL-Standards sind die weitreichende Verbreitung, eine gute Dokumentation und die uns bereits vorhanden gewesene Vertrautheit mit Struktur und Befehlen. Letztendlich entschieden wir uns für eine SQLite Datenbank an Stelle eines MySQL-Datenbankservers, da erstere Lösung für Laien komfortabler zu warten ist (auf einer einzigen Datei basierend, daher einfach zu sichern etc.). Performancetechnisch wäre ein MySQL-Server weit überlegen, allerdings sollte eine SQLite-Datenbank bei einer Single-User Anwendung die hier anfallenden Datenmengen problemlos verarbeiten können. Für unseren Anwendungsfall, einem einzigen zentralen PC zur Bücherverwaltung überwiegen hier die Vorteile gegenüber den Nachteilen.

Während der Arbeitsphase bemerkten wir, dass sich die Anbindung von Lazarus-Anwendungen an Datenbanken generell problematisch gestaltet; die Komponenten für verschiedenen Datenbanktypen ähneln zwar einander, sind jedoch nicht dokumentiert. Auch ein Blick in den Quelltext half uns nicht weiter, da Bezeichner weder selbsterklärend gewählt, noch dokumentiert sind. Letztendlich kamen wir mit dem Durchstöbern mehrerer Delphi-Foren und Ausprobieren zu einem funktionsfähigen Zustand, wobei uns noch bis heute einige Fragen offen geblieben sind. Die Sicherheit der Daten ist jedoch nicht gefährdet.

Das ER-Modell von HerderBib setzt sich aus drei Entitäten zusammen, die nachfolgend erläutert werden. Nähere Informationen zur Anzeige der Werte lassen sich der GUI-Dokumentation entnehmen..

Dem Schüler (student) ist eine ID (id) zugeordnet, welche für jeden Schüler einmalig ist und diesen identifiziert. Neben dem Vornamen (first_name) und Nachnamen (last_name) wird sein Geburtsdatum (birth) gespeichert, um ihn seitens der GUI schneller identifizieren zu können, anstatt den vollen Namen eintippen zu müssen. Zusätzlich zur Klasse (class_name) wird für spätere Zwecke der Benutzername der Schuldomäne (ldap_user) gespeichert, mit dem sich der Schüler im Schulnetzwerk anmelden kann.

Buchtypen (booktype) repräsentieren einzelne Buchserien (Bsp. “Elemente der Mathematik, Klasse 12, ISBN …”), von der ein oder mehrere Exemplare angeschafft wurden. Neben ISBN-Nummer (isbn), Titel (title) und Fach (subject) wird eine Regalnummer (storage) gespeichert, um das Buch in Bibliothekssituationen schneller ausgeben zu können.

Jedes Buch (book) ist per ISBN (isbn) einem Buchtypen zugeordnet. Neben einer eindeutigen Identifikationsnummer (id), die auch im ins Buch geklebten Barcode enthalten ist, wird der aktuelle Zustand des Buchs (condition) auf einer Skala in Form von einer einstelligen Zahl gespeichert.

Die n-zu-m-Beziehung zwischen Buch und Schüler wird als Ausleihbeziehung (rental) dargestellt. Sie besitzt die Attribute Ausleihdatum (rental_date), Rückgabedatum (return_date) und ID (id). Die ID stellt eine eindeutige Identifikation der Rentals sicher, da eine Identifizierung über die beiden Fremdschlüssel (book_id und student_id) nicht immer eindeutig ist. Dies ist der Fall, wenn dasselbe Buch zwei Jahre hintereinander vom selben Schüler ausgeliehen wird. Per Abfrage nach Buch-ID lässt sich eine komplette Verleihhistorie anzeigen.

Objektorientierung der Datenbankmodellierung

Die Daten werden, außerhalb der Datenbank, in 4 Klassen gespeichert. Diese sind primär zur Zwischenspeicherung und objektorientierten Modellierung von Daten gedacht, die eigentliche Langzeitspeicherung erfolgt in der Datenbank. Für diese Zwischenspeicherung und Modellierung stehen die Klassen TStudent, TRental, TBook und TBooktype zur Verfügung. In der Datei DBConnection.pas befindet sich die Verwaltungsklasse für die Datenbank. Entity-Objekte (book, booktype, student, rental) werden referenziert (als Adresse im Speicher) übergeben, um Dateninkonsiszenzen auszuschließen. Wenn in der GUI ein Objekt gelöscht wird, wird es von der TDBConnection zerstört. Daraufhin können Eigenschaften des Objekts nicht mehr abgefragt werden. Diese Klasse heißt TDBConnection und beinhaltet diverse Funktionen, die auf die Datenbank zugreifen indem sie Daten auslesen, löschen, oder einfügen. Im Konstruktor wird zuerst eine Verbindung zur Datenbank hergestellt. Auf diese wird durch die Funktionen zugegriffen solange wie das Objekt besteht. Daher sollte stets nur ein TDBConnection Objekt existieren um Komplikationen zu vermeiden, welche entstehen könnten wenn mehrere Datenbankverbindungen gleichzeitig bestehen. Um häufig verwendete Variablen einfach/effizient ändern zu können werden in der Datei “DBConstants.pas” einige wichtige Konstanten definiert. Der Vorteil in der Verwendung von Klassen zur Speicherung von Entitätsdaten im Vergleich zu simpleren Arrays liegt in der besseren objektorientierten Modellierbarkeit und Codeeffizienz, da beispielsweise bei der Funktion TDBConnection.getBookById einfach ein TBook-Objekt anstatt mehreren Strings zurückgegeben werden kann. Die Funktionen selbst sind im Code kommentiert um das Verständnis dieser zu garantieren. Der folgende Code ist die komplette Deklaration der TDBConnection Klasse mit Kommentaren zu den einzelnen Funktionen:

type
  ArrayOfStudents = array of TStudent;
  ArrayOfRentals = array of TRental;
  ArrayOfBooks = array of TBook;
  ArrayOfBooktypes = array of TBooktype;

  ///////////////////////////////////////////////////////////
  //         Notes for use of TDBConnection                //
  //                                                       //
  //  You can call every function below, but you need to   //
  //  check if there were any errors by checking that the  //
  //  result of TDBConnection.getError function is NIL.    //
  //                                                       //
  ///////////////////////////////////////////////////////////

  TDBConnection = class
  public
    /////////////////////////////////////////////////////////
    //             STUDENT                                 //
    /////////////////////////////////////////////////////////

    // Returns an array of all students
    // result: array of student objects
    function getStudents: ArrayOfStudents;

    // Returns an array of all students with given first name
    // parameter: first name pattern. "%" can be used as a placeholder
    // result: array of student objects
    function getStudentsByFirstNamePattern(firstName: string): ArrayOfStudents;

    // Returns an array of all students with given last name
    // parameter: last name pattern. "%" can be used as a placeholder
    // result: array of student objects
    function getStudentsByLastNamePattern(lastName: string): ArrayOfStudents;

    // Returns an array of all students with given class name
    // parameter: class name
    // result: array of student objects
    function getStudentsByClassName(classN: string): ArrayOfStudents;

    // Returns student object with given id
    // parameter: student id
    // result: student object
    function getStudentById(id: LargeInt): TStudent;

    // Returns an array of all students object with given birthdate
    // parameter: TDate object (birthdate)
    // result: array of student objects
    function getStudentsByBirthdate(birthdate: TDate): ArrayOfStudents;

    // Returns an array of all students with given ldap username
    // parameter: student's ldap username. "%" can be used as a placeholder
    // result: array of student objects
    function getStudentsByLDAPUserPattern(ldap_user: string): ArrayOfStudents;

    // Returns student who rented a book, nil if the book is not rented
    // parameter: book object
    // result: student object
    function getStudentWhoRentedBook(book: TBook): TStudent;

    // Returns an array of Students matching the given parameters
    // parameter: first name, last name, class name, birthdate
    // result: array of student objects
    function getStudentsByFistLastClassNameBirthdate(fname, lname, cname: string;
      birth: TDate): ArrayOfStudents;

    // updateInserts student object into database. Either updates an existing one or inserts a new one
    // parameter: student object
    // result: TRUE on success
    function updateInsertStudent(var student: TStudent): boolean;

    // Deletes a student and destroys the object
    // parameter: student object
    // result: TRUE on success
    function deleteStudent(var student: TStudent): boolean;


    /////////////////////////////////////////////////////////
    //             RENTAL                                  //
    /////////////////////////////////////////////////////////

    // Returns an array of all rentals
    // result: array of rental objects
    function getRentals: ArrayOfRentals;

    // Returns an array of all rentals with given student and book
    // parameter: student, book objects
    // result: array of rental objects
    function getAllRentalsByBookAndStudent(var student: TStudent;
      var book: TBook): ArrayOfRentals;

    // Checks if there are unreturned rentals for a specific book
    // parameter: book object
    // result: true when rentals exist that match this criteria
    function existsUnreturnedRentalByBook(var book: TBook): boolean;

    // updateInserts rental object into database. Either updates an existing one or inserts a new one
    // parameter: rental object
    // result: TRUE on success
    function updateInsertRental(var rental: TRental): boolean;

    // Deletes a rental and destroys the object
    // parameter: rental object
    // result: TRUE on success
    function deleteRental(var rental: TRental): boolean;

    // Deletes all returned rentals older than a certain date
    // parameter: date
    // result: Amount of deleted rentals on success, -1 on error
    function deleteReturnedRentalOlderThan(date: TDate): integer;

    /////////////////////////////////////////////////////////
    //             BOOK                                    //
    /////////////////////////////////////////////////////////

    // Returns an array of all books
    // result: array of book objects
    function getBooks: ArrayOfBooks;

    // Returns the book object by given book id or NIL if the book does not exist
    // parameter: book id
    // result: book object | nil
    function getBookById(id: LargeInt): TBook;

    // updateInserts book object into database. Either updates an existing one or inserts a new one
    // parameter: book object
    // result: TRUE on success
    function updateInsertBook(var book: TBook): boolean;

    // Deletes a book and destroys the object
    // parameter: book object
    // result: TRUE on success
    function deleteBook(var book: TBook): boolean;

    /////////////////////////////////////////////////////////
    //             BOOKTYPE                                //
    /////////////////////////////////////////////////////////

    // Returns an array of all booktypes
    // result: array of booktype objects
    function getBooktypes: ArrayOfBooktypes;

    // Returns the Booktype of an ISBN Number
    // parameter: isbn
    // result: TBooktype on success, NIL on failure
    function getBooktypeByIsbn(isbn: string): TBooktype;

    // updateInserts booktype object into database. Either updates an existing one or inserts a new one
    // parameter: booktype object
    // result: TRUE on success
    function updateInsertBooktype(var booktype: TBooktype): boolean;

    // Deletes a booktype and destroys the object
    // parameter: booktype object
    // result: TRUE on success
    function deleteBooktype(var booktype: TBooktype): boolean;

    /////////////////////////////////////////////////////////

    // Returns the current Error Object
    // result: Error Object (DBError, Type: Exception)
    function getError: Exception;

    /////////////////////////////////////////////////////////

    // Opens database connection
    // parameter: file path to sqlite file
    constructor Create(databasePath: string);

    // Closes the database connection
    destructor Destroy;

    // Checks if conncetion to database was successful
    // result: TRUE on success
    function isConnected: boolean;

  private
    procedure setStudentFields(var resultVar: ArrayOfStudents; returnOne: boolean);
    procedure setRentalFields(var resultVar: ArrayOfRentals; returnOne: boolean);
    procedure setBookFields(var resultVar: ArrayOfBooks; returnOne: boolean);
    procedure setBooktypeFields(var resultVar: ArrayOfBooktypes; returnOne: boolean);

  private
    SQLite3Connection: TSQLite3Connection;
    SQLQuery: TSQLQuery;
    SQLTransaction: TSQLTransaction;
    DBError: Exception;
  end;

Erweiterung um weitere Datenbankfelder

Bsp. student

  1. Entitätsklasse student bearbeiten, um lokale Variable und Getter/Setter ergänzen, die Bedingungen für die Datenbank (Bsp. not NULL) prüfen
  2. In der TDBConnection.setStudentFields-Funktion analog zu den anderen Datenbankfeldern die Umsetzung zwischen SQL-Abfrage und Modellierung per student-Klasse ergänzen. Diese Funktionen überträgt Werte aus dem Ergebnis der Datenbankabfrage in Delphi-Objekte.
  3. Die TDBConnection.updateInsertStudent-Funktion analog zu den anderen Datenbankfeldern und genau entgegengesetzt zur TDBConnection.setStudentFields-Funktion ergänzen. Falls das Datenbankfeld NULL sein darf, einen Null-Check wie beim Datenbankfeld birth durchführen, um das Feld im SQLQuery mit der Clear-Funktion auf NULL zu setzen. Diese Funktion überträgt Werte aus Delphi-Objekten in die Datenbank. Dabei wird anhand der ID (existiert/existiert nicht) automatisch entschieden, ob ein neuer Datensatz eingefügt oder ein bestehender geändert werden muss.

Falls IDs per Auto-Increment generiert werden, so ist in der updateInsert-Funktion wie bei updateInsertRental folgende Zeile nach dem Ausführen des SQL-Befehls einzufügen:

rental.setId(FieldByName('id').AsLargeInt); //set id generated by auto-increment

Alle Funktionen sind ausreichend kommentiert.

SQL-Injection

Mittels SQL-Injection kann die Kontrolle über Datenbank durch manipulierte Textfeldeingaben übernommen werden. Obwohl sich der anrichtbare Schaden mangels Netzwerkanbindung der Datenbank in Grenzen hält, werden in Zukunft Textfeldeingaben geprüft und SQL-Injections vorgebeugt werden.

Management-Klasse

"Controller"

Grundlegend ist die Management Unit dazu gedacht, dass die Kommunikation zwischen GUI und Datenbank zu ordnen und zu vereinfachen. Wir stellen der GUI alle Funktionen zusammen, die sie braucht um alle geplanten Eigenschaften der Benutzeroberfläche zu realisieren. Zum einen fassen wir Funktionen aus der DBConnection zusammen und machen aus mehreren Funktionen eine, für die GUI gut nutzbare, Funktion. Zum anderen erweitern wir Funktionen der DBConnection. So erstellen wir die IDs für Bücher und Schüler, sowie die dazugehörigen Prüfziffer und kommunizieren diese mit der GUI sowie mit der Datenbank. Des weiteren haben wir daran gearbeitet dass eine Schülerliste als .csv Datei automatisch eingelesen werden kann.

Das Einlesen der .csv Datei verläuft so, dass in dem Dokument alle Informationen systematisch abgelegt sind. Zunächst die Klasse, dann der Nachname, der Vorname und das Geburtsdatum. Dadurch können auch automatisch generierte .csv Dateien, die auch bei dem Eintragen der Schüler in die Schuldatenbank genutzt werden, vom Programm in die Datenbank übernommen werden. Wichtig ist dass die einzelnen Informationen mit einem Semikolon getrennt sind. Damit werden alle nachfolgendne Werte nach dem Geburtsdatum ignoriert. So wird auch das Geschlecht welches in den automatisch generierten Listen enthalten ist aber für unsere Datenbank irrelevant ist ignoriert. So ist ein Eintrag in einer .csv Datei '12;Mustermann;Max;01-01-2000;m' wird er Wert für Wert ausgelesen. Die einzelnen Werte werden in verschiedenen Strings gespeichert und mittels Funktionen aus der DBConnection in die Datenbank eingetragen. So enstehen vier Strings:

cname := '12'

lname := 'Mustermann'

fname := 'Max'

birth := '01-01-2000'

Alles was nach dem Geburtsdatum kommt wird ignoriert. Zudem wird darauf geachtet dass bei bereits in der Datenbank vorhandenen Einträgen nur die Klasse neu eingetragen wird. So muss beim Anlegen der .csv Datei nicht darauf geachtet werden dass nur neue Einträge hinzu kommen oder dass alte Eintäge manuell bearbeitet werden.

Die IDs der Schüler werden gleich den IDs der Bücher generiert. Zunächst wird bei beiden ID-Arten eine zufällige Zahl (für Bücher zwischen 300001 und 800001, für Schüler zwischen 100000 und 300000) erstellt. Da für den verwendeten Strichcode die letzte Ziffer eine Prüfziffer sein muss, wird diese berechnet und hinten an die vorher generierte Zahl angehängt.

Prüfzifferberechnung: Es werden alle Zahlen die sich auf ungeraden Ziffernstellen befinden zusammen addiert und dann mit drei multipliziert, dazu werden alle Zahlen auf den geraden Ziffernpositionen addiert. Von dieser Summe wird der Mod10 genommen. Diese Zahl (0-10) -10 ist dann die Prüfziffer. Ausnahme: Wenn bei der Mod10-Berechnung eine 10 als Ergebnis heraus kommt, dann ist die Prüfziffer 0.

Zurechtfinden in der Unit:

Die Funktionen der Unit sind alphabetisch gegliedert in die Kategorien Book, Booktype, Rental, Student. Im Interface steht jede Funktion mit umfassender Erläuterung zur Funktion, den Parameter und den Rückgabewerte. Auch die Funktionen in der Implementation sind unter den Gliederungspunkten alphabetisch sortiert. An unübersichtlichen oder schwierigen Stellen unterstützen weitere Kommentare das Verständnis.

Die uDBManagement ist in jeder Hinsicht erweiterbar, solange man die Trennung von Datenbankzugriffen in der DBConnection von den verwaltenden Funktionen in der uDBManagement klar beibehält. Variablen sind eindeutig erkennbar oder beschrieben und solange Rücksprache mit der Datenbank Gruppe gehalten wird, ist der Zugriff auf deren Funktionen einfach. Es ist wichtig dass bei einer Erweiterung beide Gruppen gut zusammenarbeiten und kommunizieren.

GUI

Namensgebung von GUI-Elementen

  1. Zwei Buchstaben für den Elementtyp (Lb für TLabel, Ed für TEdit, ...)
  2. Name des Tabs (Info für den Tab "Informationen betrachten und bearbeiten")
  3. Name des Untertabs (Student für den Tab "Schüler")
  4. Bezeichner des Elements (der Bedeutung entsprechend)

LbInfoStudError ist also das Label das Fehler im Tab Info/Student ausgibt

Administration

Im Administrationstab kann sich der Administrator einloggen. Der Administratorzugang wird bis jetzt sehr primitiv gelöst, dadurch, dass die Prozedur die bei Logineingabe ausgeführt wird, alle Administrationsfunktionen freischaltet.

Im Code zurecht finden

Der Zweck von GUI-Elementen lässt sich eindeutig an ihrer Position in der Form sowie an ihrer Benennung erkennen, daher haben wir im Code auf Kommentare verzichtet. Die Funktionsweise verschiedener Tabs kann schnell mit dem Objektinspektor erschlossen werden. Die meisten Buttons greifen direkt auf eine Funktion aus TManagement zu, gelegentlich wird eine lokale Variable zur Durchführung dieser benötigt. Sollte die GUI erweitert werden müssen können jederzeit neue Tabs erstellt werden oder neue Elemente in bestehende Tabs eingefügt werden. Die neuen Aufgaben müssen aber selbstverständlich mit entsprechenden Funktionen aus TManagement unterstützt werden. In allen Aufgabenbereichen werden "Exceptions" abgefangen um zu verhindern, dass das Programm während der Benutzung abstürzt, sollte der Nutzer eine falsche Eingabe tätigen. Stattdessen wird ihm eine Fehlermeldung angezeigt, die ihn auf seinen Fehler hinweist und die er bei Bedarf in der Wiki nachschlagen kann.

Barcode-Generator

Dokumentation hier

⚠️ **GitHub.com Fallback** ⚠️