Datenbank-Entwicklung mit mehr Komfort – Teil I

Die (sich) selbst-bewusste Datenbank

Kann es denn eigentlich wahr sein, dass ich das Thema Datenbank-Entwicklung so sträflich vernachlässigt habe bisher? Mea culpa! Dies sei nun mal behoben, mit ganz besonderer Würdigung komfortabler spezialisierter Entwicklungssysteme wie FileMaker.

Falls ich damit noch nie angegeben haben sollte: Vor vielen Jahren habe ich einmal eine wirklich, wirklich große Datenbank entwickelt. Für einen Kunden eigentlich in meinem Gestaltungsjob, einen großen Berliner Autorenverlag. Das Ding war mehrbenutzerfähig, mit eigenem Kommunikationsprotokoll (E-Mail via Datensatz) und ermöglichte jederzeitigen Überblick über Interessenten, Autoren, Veröffentlichungen und Produktionsablauf, inkl. Statistiken. Wie so häufig bei mir war es einfach das Vertrauen in das Entwicklungssystem, das mich so ein Projekt anbieten ließ. Bis dato hatte ich nur allerwinzigste Datenbank-Tools damit entwickelt, aber mich dabei in die Dokumentation gelesen und daher gedacht, das könne ich schon schaffen. Konnte ich. Das Entwicklungssystem hieß FileMaker, und es machte einfach Spaß, damit Strukturen zusammenzuschrauben und Steuerelemente via Scripts zum Leben zu erwecken.

Würde die Firma FileMaker nicht durch gesalzene Lizenzpolitik resp. Änderungsandrohungen dieser die Anwenderschaft verunsichern und die Aussichten vage machen, ob man damit überhaupt noch günstig kleine Anwendungen entwickeln und veräußern kann, dann wäre dieser Artikel hier zu Ende. FileMaker macht Spaß. Es hat seine Begrenzungen dort, wo man mehr aus der reinen App herauskitzeln will. FileMaker ist eben ein Datenbank-Entwicklungs- und kein allgemeines App-Entwicklungssystem wie Xojo. Wenngleich hier zahlreiche Erweiterungen von Christian Schmitz, dessen MonkeyBread Software-Plugins jedem Xojo-Anwender vertraut sein dürften, den Funktionsumfang auch kräftig aufbohren helfen.

Wegen obiger Umstände aber sind viele FileMaker-Anwender auf der Suche nach Alternativen, zumindest für kleinere Anwendungen, die sich eher über die Stückzahl als über die Entwicklungskosten rechnen. Xojo bringt ja mindestens SQLite mit, und das ist eine sehr ordentliche Datenbank-Engine, zumal kostenlos und so ziemlich unter jeder denkbaren Konstellation verfügbar. Wagt man sich als FileMaker-Experte aber an die Datenbankentwicklung mit Xojo, dann wage ich auch etwas, nämlich die Behauptung zu treffen, dass die Erstreaktion in vielen Fällen eine sein dürfte, und zwar Ernüchterung.

Statt eines fast schon intuitiven Umgangs mit Datenbank-Objekten und einer sehr klaren Scriptsprache muss man sich auf den Sprachlevel der Datenbank begeben. Es heißt also für viele eine neue Sprache lernen, SQL, und dann recht komplizierte Objekte handlen: Recordsets, die sich anfühlen wie ein Array, aber keines sind, stattdessen eher etwas klobig in der Handhabung.

Mittlerweile gibt es ja schon diverse Lösungen für Datenbank-Entwickler, die einigen Komfort ergänzen. Damit will und kann ich mich nicht messen, ich bin trotz obigen Erfolgs kein Datenbankexperte wie die hinter den Erweiterungen stehenden Entwickler. Aber da ich letztens gebeten wurde, doch einmal Expeditionen in weitere Class Interfaces zu unternehmen, hab ich die Gelegenheit ergriffen und mir einen Teil der für Datenbanken relevanten Schnittstellen angeschaut. Und dabei die Anfänge eines Frameworks für DB-sensitive Steuerelemente zusammengetippt, das ich Ihnen hier in den Grundlagen als Anregung für eigene Experimente schildern möchte.

Das alles wird eigentlich nicht allzu kompliziert, aber komplex! Weshalb der Titel dieses Beitrags auch auf weitere Teile hinweist. Ich würde mich gerade bei diesem Thema über Feedback freuen, um zu wissen, ob (baldige) Fortsetzungen gewünscht sind! Nach dieser langen Einführung also:

Grundsätzliches

Während ja an sich alles in Xojo objektorientiert aufgebaut ist, kann man bei einer Datenbank so nicht davon sprechen: Die ist eine völlig externe Geschichte, die sich herzlich wenig um Objektstrukturen schert. Man schickt grundsätzlich der Datenbank als solchen einen SQL-Befehl, und diese reagiert dann, ggf. indem sie ein Recordset zurückschickt. Aus dem klamüsert man sich die Daten zusammen, die man gerade braucht.

Häufig sieht eine Xojo-Anwendung dann auch so aus, dass Steuerelemente angestupst werden – sie haben z.B. eine Methode, der man eine ID übergibt, basteln sich ihren individuellen Query-String zusammen, starten dann die SQL-Query auf der Suche nach dem Datensatz mit dieser ID

Dim rs As Recordset = DB.SQLSelect ("Select * From TabellenName Where id = ID")

z.B. und geben das Ergebnis irgendwie passend aus.

Das kann man machen und ist völlig legitim. Der Nachteil ist nur, dass sie statt eines Selects nun steuerelementmal viele haben. Das hat Auswirkungen auf die Performance, insbesondere, wenn die Datenbank übers Netz abgefragt wird. Wie ich zu vermeidbaren Redundanzen stehe, hab ich ja schon des öfteren erwähnt.

Nicht zu vergessen: Nach allen möglichen Befehlen sollte man die Datenbank auf Fehlermeldungen überprüfen. Laut Murphys Gesetzen wird man das ausgerechnet an der Stelle vergessen, an der später der Fehler auftritt.

Und dann noch zum Thema Objekte: Eine Datenbank besitzt doch eigentlich eine sehr nachvollziehbare Objektstruktur. In der Grundausstattung die Datenbank selbst als Mutterobjekt mit ihren Kindern Tabellen. Diese wiederum besitzen beliebig viele Felder. Wie wäre es denn, diese Objekte auch wirklich greifbar nachzuvollziehen?

EddiesElectronics.png
Die Beispieldatenbank EddiesElectronics aus den Xojo-Musterprojekten in einem SQLite-Editor – links die Tabellen, im Hauptteil die Felder der Customers-Tabelle

Das ist mein Plan, und damit der erste Bestandteil der Klasse

eaSyQLDataBase

die ich in einem neuen Desktop-Projekt anlege. Dies wird eine Wrapper-Klasse um eine SQLite-Datenbank. Sie bekommt als Klasse ohne Superklasse also zunächst eine Property

DB As SQLiteDatabase,

die ich mittels Rechtsklick zu einer Computed Property mache und den Setter lösche. Die Datenbank soll von außen zwar zugänglich, aber nicht unbeabsichtigt veränderbar sein.

Dazu verpasse ich der Klasse noch eine Public Property

DatabaseName As Text

(ich benutze ja am liebsten das neue Framework und bereite damit meine Klassen auf die nahende Zeit vor, wenn dieses überall völlig verfügbar sein wird) und eine Computed Property

Public Property DatabaseFolderItem as FolderItem
 Get
  if mdb <> nil then return mdb.DatabaseFile
 End Get

 Set
  mdb = new SQLiteDatabase
  mdb.DatabaseFile = value
 End Set
End Property

mDB ist die oben angelegte private Property, die hinter DB steht. Da ich eine externe Datenbank verwenden möchte, halte ich den Code flexibel, um ggf. einen späteren Ortswechsel der Datenbank einfach zu gestalten. Dann müsste man nur das Folderitem anpassen, die Datenbank wird automatisch zugewiesen.

Und jetzt: Die ganzen Funktionen einer Datenbank hinzufügen? Nicht nötig! Ich integriere einfach durch Klick auf „Interfaces/Choose …“ das DatabaseConnectioninterface.

DatabaseConnectionInterface.png

… und habe damit die Methodenköpfe aller Datenbank-Methoden in der Klasse.

Die reichen mir aber nicht. Um gleich mal einen nervigen Umstand der SQL-Handhabung wegfallen zu lassen, lege ich eine neue Methode an:

Public Sub CheckError()
 if db <> nil and db.error then
  if not RaiseEvent error(db.ErrorCode, db.ErrorMessage.ToText) then
   dim err as new xojo.Core.ErrorException
   err.ErrorNumber = db.ErrorCode
   err.Message = db.ErrorMessage
   raise err
  end if
 end if
End Sub

… und definiere einen Event:

Event Error(ErrorCode As Integer, ErrorMessage as Text) As Boolean

Also: Jedesmal, wenn Checkerror aufgerufen wird, wird die Datenbank (so vorhanden) auf einen Fehler überprüft. Gab es einen, feuert die Klasse einen Event, und wird dieser nicht durch Rückgabe von True bestätigt, gibt’s eine Exception obendrauf. Damit ist wahlweise ein allgemeines Fehlerhandling möglich (im Eventhandler) oder auch ein individuelles per Try/Catch im Code.

Weil es bequemer ist, eine Klasse mit Events optisch im Layout zu haben (finde ich), bekommt die EaSyQLDatabase noch einen Constructor (ohne kann eine Klasse nicht aufs Layout gezogen werden):

Public Sub Constructor()
 Xojo.core.timer.CallLater 0, addressof RaiseOpen
End Sub

Dazu passend gehören eine Open-Event-Definition ohne Parameter und diese Methode:

Private Sub RaiseOpen()
 mDatabases.Append xojo.core.weakref.Create(me)
 RaiseEvent open
End Sub

Diese wiederum verweist auf eine Shared Property

Private Shared Property mDatabases() as xojo.core.weakref

für die ich gleich noch eine Shared Method entwerfe:

Public Shared Function Databases() as eaSyQLDataBase()
 dim result() as eaSyQLDataBase
 for q as integer = 0 to mDatabases.Ubound
  dim val as xojo.Core.WeakRef = mDatabases(q)
  if val <> nil and val.Value <> nil then result.Append eaSyQLDataBase(val.Value)
 next
 return result
End Function

In etwa dieser Reihenfolge wird dann auch alles bei der Initialisierung durchlaufen. Der Constructor macht nichts weiter, als via Xojo.core.Timer.Calllater die Methode RaiseOpen aufzurufen. Falls Ihnen der Sinn unklar ist, ist dies ein guter Taschentuchknotenmoment:

Würde man den Open-Event direkt aus dem Constructor aufrufen, würde sein Eventhandler nicht durchlaufen werden. Das wäre zu früh fürs restliche Programm. Über den Calllater-Aufruf wird der Open-Event entkoppelt und feuert erst, wenn das Programm auch Zeit dafür hat.

Die RaiseOpen-Methode macht aber noch eines mehr, nämlich ein WeakRef der Klasseninstanz selbst im Shared Array mDatabases anzulegen. Der Sinn dahinter ist folgender: Ich möchte später folgende Steuerelemente so bequem wie möglich initialisieren. Sie sollen selbsttätig Ihre eaSyQLite-Datenbank suchen und sich bei ihr (bzw. bei der entsprechenden Tabelle bzw. dem Feld) anmelden können. Ich könnte die Datenbanken auch als EaSyQLiteDatabase einem Shared Array of EaSyQLiteDatabase hinzufügen, aber dann würde ich Kreuzreferenzen produzieren bzw. müsste die Instanzen sich auch immer abmelden lassen. Durch das Halten im Array würde ansonsten die Datenbank im Speicher gehalten werden, selbst wenn das Fenster, auf dem sie liegt, schon lange geschlossen wurde. Ein WeakRef hält nichts im Speicher fest; es referenziert ein Objekt nur, solange es auch vorhanden ist. Deshalb die Shared Funktion DataBases(), die die noch existenten Datenbanken als Array zurückgibt.

In Funktion, wie gesagt, wird dieses Feature erst später zu sehen sein.

TabellenKalkulationen

Durch das Class Interface schwirren zurzeit jede Menge unausgefüllter Methoden herum. Kümmern wir uns mal um die erste, die auch die erste im üblichen Programmablauf sein dürfte:

Public Function Connect() as Boolean
 // Part of the DatabaseConnectionInterface interface.
 dim result as boolean
 if db <> nil then
  result = db.connect
  if result then
   enquireTables
  end if
 end if
 return result
End Function

Hier wird also zunächst die Datenbank verbunden. Ist das gelungen, wird eine weitere Methode aufgerufen:

Private Sub enquireTables()
 if tables.Ubound = -1 then
  dim scheme as recordset = db.TableSchema
  if scheme <> nil and not scheme.eof then
   for q as integer = 1 to scheme.RecordCount
    dim fieldname as text = scheme.Field(eaSyQL.Tablename).TextValue
    tables.Append new eaSyQLtable(me, fieldname )
    scheme.MoveNext
   next
  end if
 end if
End Sub

Die passende private Property fehlt noch:

Private Property tables() as eaSyQLTable

Also ein Array für die (noch nicht existente) Klasse eaSyQLTable, der Klasse, die analog zur Database eine Tabelle innerhalb dieser verwalten soll. Ich möchte vermeiden, dass dieses Array mehrfach aufgebaut wird, nachdem etwa die Verbindung zur Datenbank verlorengegangen war und sie neu geöffnet wurde. Deshalb schaut die erste Codezeile nach, ob schon Tabellen existieren. Falls nicht, erfragt sie das TableSchema der Datenbank.

Das TableSchema ist ein Recordset sozusagen mit vorgefertigter Query. Inhalt ist nur ein Feld, das auf den Namen TableName hört. Den habe ich als Textkonstante in ein Modul namens EaSyQL gelegt. In dieses sollen alle Textkonstanten rein, die zu SQL gehören, und noch ein paar Konvertierungsmethoden. Unter anderem

Public Function TextValue(extends d as DatabaseField) as Text
 return d.StringValue.ToText
End Function

Wenn Sie dem Classic-Framework treu bleiben wollen, können Sie auf diese Funktion natürlich verzichten und weiterhin mit String hantieren.

Die Fieldnamen – die Namen der Tabellen – werden also der Reihe nach ausgelesen, es wird jeweils eine eaSyQLTable davon erzeugt und diese ans Array rangehängt. Um diese Klasse kümmern wir uns jetzt.

EaSyQLTable

ist, ähnlich wie die Database als Wrapper einer Datenbank, ein Wrapper für eine Datenbank-Tabelle. Was ist diese aber im Grunde? Ein Recordset mit der Kenntnis über seine Felder. Also braucht die neue Klasse ohne Superklasse zunächst eine Computed Property

Public Property RS as RecordSet
 Get
  return mRS
 End Get

 Set
  mRS = value
  mRSPosition = 0
 End Set
End Property

Mit passender Private Property mRS As RecordSet und einer ähnlichen Kombination

Public Property RSPosition as Integer
 Get
  return mRSPosition
 End Get

 Set
  mRSPosition = value
 End Set
End Property

mit Private Property mRSPosition As Integer.

mRS wird also den jeweils gültigen RecordSet der Tabelle aufnehmen, mRSPosition ist eine Hilfsvariable, die sozusagen den Index des Recordsets aufnehmen wird, wenn man sich den RecordSet als Array of Records vorstellt, das es so ja nicht gibt.

Das ist übrigens auch einer der Gründe, warum dieses Beispiel SQLite benutzt. In einem SQLite-Recordset kann man beliebig in Einerschritten vor- und zurückhüpfen. Einige andere Datenbank-Engines lassen nur Vorwärtsbewegungen via RecordSet.MoveNext zu. Aber auch hier: Das folgt später!

Die Tabelle soll wieder selbst wissen, zu welcher Datenbank sie gehört, aber diese nicht im Speicher festhalten. Deshalb, ähnlich wie bei der Datenbank mit ihrer Shared Property Databases, eine

Private Property mDataBase as xojo.core.weakref

und eine setterlose

Public Property DataBase as eaSyQLDataBase
 Get
  if mDataBase <> nil and mDataBase.value <> nil then
   return eaSyQLDataBase( mDataBase.Value)
  end if
 End Get
End Property

Die Tabellen muss auch noch ihren Namen kennen. Den bekommt sie von der EnquireTables-Methode der Datenbank ja mitgeteilt. Ein Speicher dafür:

Private Property mTableName as Text

und

Public Property TableName as Text
 Get
  return mTableName
 End Get
End Property

Kein Setter – soll ja nicht nachträglich zu Verwirrungen kommen. Stattdessen kümmert sich der Constructor der Klasse um die Benamsung:

Public Sub Constructor(db as eaSyQLDataBase, TableName as text)
 me.mDataBase = xojo.core.WeakRef.Create(db)
 me.mTableName = TableName
 enquireFields
End Sub

Auch hier haben wir wieder eine Erfrage-Methode, in diesem Fall für die Felder. Die ist wieder ganz ähnlich aufgebaut wie EnquireTables in der Datenbank-Klasse:

Private Sub enquireFields()
 dim scheme as recordset = database.db.FieldSchema(me.TableName)
 if scheme <> nil and not scheme.eof then
  for q as integer = 1 to scheme.RecordCount
   dim fieldname as text = scheme.Field(eaSyQL.columnname).TextValue
   dim type as integer = scheme.Field(eaSyQL.fieldType).IntegerValue
   dim primary as Boolean = scheme.Field(eaSyQL.isPrimary).BooleanValue
   dim notnull as Boolean = scheme.Field(eaSyQL.NotNull).BooleanValue
   dim length as integer = scheme.Field(eaSyQL.Length).IntegerValue
   fields.Append new eaSyQLField(me, fieldname,type, primary, notnull, length )
   scheme.MoveNext
  next
 end if
End Sub

Hier wird das Database.FieldSchema abgerufen. Da die Tabelle weiß, wie sie heißt, und da sie Direktzugriff auf ihre Datenbank hat, macht sie das eben selbst und liest dann die Werte ihrer Felder, die im RecordSet scheme enthalten sind, ein. Dazu werden wieder Textkonstanten aus dem Modul EaSyQL benutzt. Sie können aber auch die Werte als Strings direkt eingeben, also „ColumName“, „FieldType“, „isPrimary“, „NotNull“, „Length“.

Mit diesen Werten wird dann wieder der Constructur einer weiteren Klasse aufgerufen. Bevor wir aber zu dem kommen, fehlt noch eine

Private Property Fields() as eaSyQLField

an die diese dann angehängt werden.

Zur letzten Klasse:

EaSyQLField

ist diesmal kein Wrapper, denn eine analoge Klasse zu Feldern existiert nicht. Sie stecken ja im Recordset der Tabelle. Sie bekommt nur zum sanften Verweis auf ihre Tabelle die

Private Property mTable as xojo.core.weakref

mit der

Public Property Table as eaSyQLTable
 Get
  if mTable <> nil and mTable.value <> nil then
   return easyQLTable(mTable.Value)
  end if
 End Get
End Property

Das kennen Sie jetzt schon zur Genüge, oder?

Für die ihr in der EnquireFields-Methode übergebenen Properties lege ich ähnliche Kombinationen aus privaten Properties mit Computed Properties ohne Setter an, nämlich (hier nur die privaten Properties, denken Sie sich die öffentlichen Nur-Getter-Computed Properties bitte dazu):

Private Property mFieldName as Text
Private Property mFieldType as Integer
Private Property mIsIndexField as Boolean
Private Property mLength as Integer
Private Property mnotNull as Boolean

Der Constructor sieht dann so aus:

Public Sub Constructor(table as eaSyQLtable, fieldname as Text, fieldtype as integer, primary as Boolean, notnull as Boolean, length as integer)
 mTable = xojo.core.WeakRef.Create(table)
 mFieldName = fieldname
 mfieldtype = fieldtype
 me.mIsIndexField = primary
 me.mnotNull = notnull
 me.mlength = length
End Sub

Zeit zur Muße

Das war jetzt ungewohnt viel auf einmal, und dabei passiert eigentlich nicht gar nichts, gell? Dann ziehen Sie doch einmal die Klasse eaSyQLDatabase auf Ihr Layout und füllen Sie ihren Open-Eventhandler mit

Sub Open() Handles Open
 dim p as FolderItem = GetFolderItem("EddiesElectronics.sqlite")
 me.DatabaseFolderItem = p
 call me.Connect
 Break
End Sub

Suchen Sie dann EddiesElectronics aus dem Examples-Verzeichnis von Xojo, kopieren die Datei in den Projektordner und starten Sie alles.

Untersuchen Sie dann die Datenbank mit ihren Properties im Debugger. Wenn ich nichts vergessen habe (das Projekt ist schon ein ganzes Stück weiter; ich will aber nicht zu sehr vorgreifen), sollten Sie die komplette Struktur der Datenbank einsehen können.

Die Vorarbeit ist noch nicht beendet. Die würde ich, Ihr Interesse vorausgesetzt, in einem Folgebeitrag beenden. Von dort ist’s dann nur ein kleiner Schritt zu z.B. Textfeldern wie diesen hier, die ohne jedes bisschen individuellen Code eine Field-Inhalt anzeigen. Dann auch mit Beispielprojekt.

Mögen Sie mehr hören? Teil 2 ist hier.

easyqlwindow-minimal

 

 

 

 

2 Gedanken zu “Datenbank-Entwicklung mit mehr Komfort – Teil I

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s