Datenbank-Entwicklung mit mehr Komfort – Teil II

Das Datenbankfeld-Textfeld – die Vorbereitungen

Dies ist der schnell hinterhergeschobene zweite Teil der Artikelreihe über komfortablere Datenbank-Anbindungen. Ohne den ersten Teil zu kennen, bleibt er wenig verständlich.

Der Vorartikel blieb im Fazit ziemlich theoretisch. Im typischen Entwicklerfall möchte man es nur seltenst bei einem Ergebnis im Debugger bewenden lassen. Viel häufiger will man damit auch irgendwas anfangen, z.B. auf dem Bildschirm. Das soll heute auch so sein. Fangen wir mit etwas Grundlegendem an: dem TextField. Und falls Sie ungeduldig sind: Am Ende des Artikels finden Sie einen Link zum Beispielprojekt!

Die Beispieldatenbank EddiesElectronics ist ein typischer Anwendungsfall für eine Geschäftslösung: Kunden, Rechnungen und Artikel tummeln sich darin. Machen wir heute doch mal einen Teil der Kundendaten sichtbar; erst einmal Vor- und Nachnamen. Das ganze zwar nicht FileMaker-like (für den Komfort müsste man eine eigenständige Anwendung entwerfen, die die graphische Unterstützung FileMakers nachempfindet und daraus Xojo-Quellcode schreibt), aber doch ein bisschen daran angelehnt: Das TextField soll sich nur bei einem Datenbankfeld anmelden, der Rest (die Anzeige des Feldinhalts) soll dann bitte von alleine laufen.

Dazu müssen die zuletzt entwickelten Klassen weiter aufgebohrt werden. Also auch heute viel Schreibarbeit, aber als Trostpflaster: Nur einmal! Steht das ganze Framework, kann es gerne noch erweitertet werden. Die Grundfunktionen müssen aber nicht jedesmal neu entwickelt werden. Ganz anders als bei einem Textfield, das individuell einen SQLSelect erdichtet.

Als zweites Trostpflaster: Wieder einmal haben die Xojo-Ingenieure uns jede Menge Arbeit abgenommen und ein Interface zur Nutzung hinterlegt. Also denn:

eaSyQLTable

(die Klasse um einen Recordset, der eine Tabelle darstellt) bekommt das passende Interface spendiert: RecordSetInterface.

recordsetinterfae

Damit tauchen jede Menge leerer Methoden in der Klasse auf, die wir auf den Recordset leiten:

Public Function BOF() as Boolean
 // Part of the RecordSetInterface interface.
 return rs.BOF
End Function

Klarer Fall, oder? Der BeginOfFile-Boolean der Property rs wird weitergegeben. Es liegt nach wie vor am Programmierer, vorher zu prüfen, ob rs <> Nil ist. Ähnlich (aber teils unkommentiert) geht es weiter:

Public Sub Close()
 // Part of the RecordSetInterface interface.
 rs.close
End Sub
Public Function ColumnType(zeroBasedIndex As UInt32) as Integer
 // Part of the RecordSetInterface interface.
 return fields(zeroBasedIndex).FieldType
End Function

Die Property Fields() enthält ja die Felder der Tabelle in ihrer Reihenfolge. ColumnType(1) etwa würde uns den Integer-Wert nennen, der den Feldtyp (FieldType) des zweiten Felds angibt. Ein paar Überlegungen zum FieldType weiter unten bei den Feldern!

Public Sub DeleteRecord()
 // Part of the RecordSetInterface interface.
 rs.DeleteRecord
 DataBase.CheckError
 DataChanged
End Sub

Hier kommt das erste Mal CheckError ins Spiel, die Methode der Datenbank, die bei einem Datenbankfehler den Error-Event auslöst und, sollte dieser nicht beantwortet werden, eine ErrorException ins Leben ruft. CheckError werden wir jetzt oft begegnen!

Public Sub Edit()
 // Part of the RecordSetInterface interface.
 rs.Edit
 DataBase.CheckError
End Sub

Die Fehlerüberprüfung bei Edit ist vor allem sinnvoll, wenn Sie die Datenbank multiuser-tauglich gestalten. Bearbeitet ein Anwender gerade einen Datensatz, kann dieser nicht zeitgleich von einem anderen Anwender in die Mangel genommen werden.

Public Function EOF() as Boolean
 // Part of the RecordSetInterface interface.
 if rs <> nil then return rs.eof else return true
End Function

Hier eine kleine Erweiterung, denn: Obige Bedingung – gibt es überhaupt einen Datensatz, und falls ja, enthält er womöglich keine Daten? – tippt man üblicherweise andauernd. Kann man sich hiermit in Zukunft sparen und stattdessen diese Methode nutzen.

Public Function Field(name As String) as DatabaseFieldInterface
 // Part of the RecordSetInterface interface.
 for q as integer = 0 to me.Fields.Ubound
  if fields(q).Name.ToText = name then return fields(q)
 next 
End Function

Die Methode Field liefert das Feld als Interfaceklasse zurück. Ich mag aber noch eine Funktion, die mir gleich alle Erweiterungen mitliefert, die eaSyQLField noch bekommt. Daher eine Zusatzmethode mit annähernd identischer Funktion:

Public Function FieldWithName(FieldName as Text) as eaSyQLField
 for q as integer = 0 to me.Fields.Ubound
  if fields(q).Name.ToText = fieldname then return fields(q)
 next 
End Function
Public Function FieldCount() as UInt32
 // Part of the RecordSetInterface interface.
 return fields.Ubound +1
End Function
Public Function IdxField(zeroBasedIndex As UInt32) as DatabaseFieldInterface
 // Part of the RecordSetInterface interface.
 return fields(zeroBasedIndex)
End Function
Public Sub MoveFirst()
 // Part of the RecordSetInterface interface.
 rs.MoveFirst
 RSPosition = 0
 dataChanged
End Sub

RSPosition, als Erinnerung, ist eine Integer-Hilfsvariable, die die aktuelle Position des Records innerhalb des Recordsets angibt. Damit soll später eine Bewegung auch per Indexeingabe wie in einem Array möglich werden.

Public Sub MoveLast()
 // Part of the RecordSetInterface interface.
 rs.MoveLast
 RSPosition = rs.RecordCount -1
 dataChanged
End Sub
Public Sub MoveNext()
 // Part of the RecordSetInterface interface.
 if RSPosition < rs.RecordCount -1 then 
  RSPosition = RSPosition +1
  rs.MoveNext
  dataChanged
 end if
End Sub
Public Sub MovePrevious()
 // Part of the RecordSetInterface interface.
 if RSPosition > 0 then 
  RSPosition = RSPosition -1
  rs.MovePrevious
  dataChanged
 end if
End Sub
Public Function RecordCount() as UInt64
 // Part of the RecordSetInterface interface.
 if rs <> nil then return rs.RecordCount
End Function

Hier wieder ähnlich erweitert wie EOF – Statt

If table.rs <> nil and not table.eof then …

kann ein Feld also einfach per RecordCount prüfen, ob es sich lohnt, nach einem Wert zu fahnden.

Public Sub Update()
 // Part of the RecordSetInterface interface.
 rs.update
 DataBase.CheckError
End Sub

Eine hier schon mehrfach erwähnte Methode habe ich außer acht gelassen: DataChanged. Das ist wieder einmal eine Interface-Methode, und zwar die einzige des DataNotificationReceiver-Interfaces. Dieses Interface wird der eaSyQLTable– und bei der Gelegenheit auch gleich der eaSyQLField-Klasse zugeordnet!

datanotificationreceiverinterface

Ich hab’s ja angedroht: Dieses Projekt wird nicht kompliziert, aber ganz schön komplex. Wir sind noch lange nicht fertig mit den Vorbereitungen!

DataChanged in der Table-Klasse bekommt nun diesen Code:

Public Sub dataChanged()
 // Part of the dataNotificationReceiver interface.
 for each f as eaSyQLField in fields
  f.dataChanged
 next
End Sub

Wird also durch einen Wechsel des Recordsets rs – entweder weil es neu erzeugt wurde oder weil ein anderer Record angesteuert wurde – die DataChanged-Methode aufgerufen, teilt sie das den angeschlossenen Feldern mit. Damit das sollständig funktioniert, bekommt der Setter der Computed Property RS noch eine weitere Zeile am Ende. Deren Code sollten Sie sich schon denken können:

DataChanged

Damit sind wir beinahe durch mit den Table-Erweiterungen für dieses Mal. Eine Überlegung aber noch:

Häufig wird es mal nötig sein, einen Recordset mit bestehender Query erneut zu laden. Und eigentlich wäre es für unveränderliche Queries – etwa im Fall von Tabellen, die immer den kompletten Datenbestand anzeigen sollen – praktisch, die Query als String (bzw., Sie wissen ja, ich und Xojo-Framework) als Text zu hinterlassen. Also noch eine Property dazu:

Public Property Query as Text

Und um dies auch bald ausprobieren zu können, bekommt eaSyQLTable noch eine Abkürzungsmethode:

Public Sub SelectAll()
 Query = eaSyQL.SelectAll+eaSyQL.from+me.TableName
 me.rs = me.DataBase.SQLSelect(Query)
End Sub

 

eaSyQL, das Modul, wird um ein paar Konstanten erweitert:

Protected Const SelectAll as Text = "Select * "

Protected Const from as Text = "from "

 

Und nun (können Sie noch?) weiter zu

eaSyQLField

Auch hier: Viel Tipparbeit, aber auch viel schon erledigte Vorarbeit! Das DataNotificationReceiverInterface hat die Klasse oben bereits erhalten, aber es fehlen noch zwei weitere:

dbfieldinterface

Das DatabaseFieldInterface bringt der Klasse eine ganze Reihe an Methoden-Pärchen, jeweils zum Lesen und Setzen der typischen Werte eines Datenbankfelds – BooleanValue, CurrencyValue etc. bis zu Value, das den Wert als Variant wiedergibt. Ich denke, Sie haben es verdient, jetzt bald mal Ergebnisse auf dem Bildschirm zu sehen. Wir kümmern uns daher zunächst um die Ausgabemethoden, lassen die „Setter“ – also die mit Assigns beginnenden Methoden – erst mal leer. Als Beispiel hier

Public Function BooleanValue() as Boolean
 // Part of the DatabaseFieldInterface interface.
 if me.table.RecordCount > 0 then return me.table.RS.Field(me.FieldName).BooleanValue
End Function

Und die restlichen so durchdeklinieren, also

 if me.table.RecordCount > 0 then return me.table.RS.Field(me.FieldName).CurrencyValue

für CurrencyValue, DateValue für DateValue, DoubleValue, Int64Value, IntegerValue, StringValue und Value für die jeweils gleichnamige Methode.

Puh! Viel Tipparbeit gespart. Also ich 😉

Fehlt nur noch

Public Function Name() as String
 // Part of the DatabaseFieldInterface interface.
 return me.FieldName
End Function

 

An dieser Stelle dann auch die angekündigte kleine

Grundsatzüberlegung zu FeldTypen

Der FieldType eines Datenbankfelds kommt als Integerwert daher. Ich finde das umständlich und außerdem für den SQLite-Anwendungsfall viel zu weit gezielt: SQLite unterstützt nur so knapp zwei Handvoll Datentypen, und diese sind auch eher Empfehlungen als verpflichtende Angaben. Ich möchte lieber eine Enumeration einführen, die sich auf die wirklich unterstützten Datentypen beschränkt. Nundenn, weitergetippt – ich lasse stattdessen ein Bild sprechen, das die Enumeration eaSyQLFieldType zeigt, die ich der Field-Klasse gebe:

fieldtype

Konsequenterweise erhält die Klasse obendrauf eine

Public Property Type as eaSyQLFieldType
 Get
  return mType
 End Get
 Set
  mType = value
 End Set
End Property

mit passender

Private Property mType as eaSyQLFieldType

Und diese setze ich im Constructor der Klasse, der damit wie folgt aussieht:

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
 select case fieldtype
 case 1, 2, 3
  me.Type = eaSyQLFieldType.IntegerType
 case 4, 5, 18
  me.Type = eaSyQLFieldType.TextType
 case 6, 7, 13
  me.Type = eaSyQLFieldType.DoubleType
 case 8,9, 10
  me.Type = eaSyQLFieldType.DateType
 case 11
  me.Type = eaSyQLFieldType.CurrencyType
 case 12
  me.Type = eaSyQLFieldType.BooleanType
 case 14, 15, 16, 17
  me.type = eaSyQLFieldType.BlobType
 case 19
  me.Type = eaSyQLFieldType.Int64Type
 else
  me.Type = eaSyQLFieldType.unknown
 end select
 me.mIsIndexField = primary
 me.mnotNull = notnull
 me.mlength = length
End Sub

Es ist also empfehlenswert, den Datentyp eines Datenbankfelds bei der Erstellung gleich richtig zu setzen 8man muss das ja nicht wirklich in SQLite). Ist das aber nicht passiert, auch kein Beinbruch, denn Type kann ja verändert werden.

Sinnvolle Anwendung gefällig? Sehr gerne! Die Textausgabe eines EaSyQLFields erfolgt via StringValue, und Sie wissen ja, das neue Framework … Also eine Methode hinzu, die wir später noch erweitern werden:

Public Function TextValue() as Text
 select case me.Type
 case eaSyQLFieldType.BooleanType
  return me.BooleanValue.ToText
 case eaSyQLFieldType.CurrencyType
  return me.CurrencyValue.ToText(xojo.core.Locale.Current)
 case eaSyQLFieldType.DateType
  if me.DateValue <> nil then
   return me.DateValue.tocoredate.ToText(xojo.core.Locale.Current)
  end if
 case eaSyQLFieldType.DoubleType
  return me.DoubleValue.ToText(xojo.core.locale.Current)
 case eaSyQLFieldType.Int64Type
  return me.Int64Value.ToText(xojo.core.locale.Current)
 case eaSyQLFieldType.IntegerType
  return me.IntegerValue.ToText(xojo.core.locale.Current)
 case eaSyQLFieldType.TextType
  return me.StringValue.ToText()
 end select
End Function

Was dem Modul eaSyQL drei neue Methoden anheimstellt:

Public Function toCoreDate(extends aDate As Date) as xojo.Core.Date
 dim tz as new xojo.core.timezone (adate.GMTOffset*3600)
 return new xojo.core.date (aDate.Year, aDate.Month, aDate.Day, aDate.Hour, aDate.Minute, aDate.Second, 0, tz)
End Function
Public Function toDate(extends aDate As xojo.Core.Date) as Date
 return new date (aDate.Year, aDate.Month, aDate.Day, aDate.Hour, aDate.Minute, aDate.Second, aDate.TimeZone.SecondsFromGMT / 3600)
End Function
Public Function ToText(extends b as Boolean) as Text
 return if (b, kTrue, kFalse)
End Function

nebst zwei Textkonstanten

Protected Const kFalse as Text = "False"

Protected Const kTrue as Text = "True"

Fehlt uns eigentlich nur noch eine Möglichkeit, angeschlossenen Steuerelementen mitzuteilen, dass sich der Feldinhalt geändert hat. Die Grundlagen dafür sind bereits durch Import des DataNotifier-Interfaces passiert. Die Klasse eaSyQLField braucht nur noch eine

Private Property Controls() as dataNotificationReceiver

und folgenden Code in den schon vorhandenen Methoden:

Public Sub addDataNotificationReceiver(receiver As dataNotificationReceiver)
 // Part of the dataNotifier interface.
 me.Controls.Append receiver
End Sub
Public Sub removeDataNotificationReceiver(receiver As dataNotificationReceiver)
 // Part of the dataNotifier interface.
 dim id as integer = controls.IndexOf(receiver)
 if id > -1 then me.Controls.Remove id
End Sub

Jetzt nur noch eine

Private Sub informControls()
 for each receiver as dataNotificationReceiver in controls
  receiver.dataChanged
 next
End Sub

und diese Zeile in der schon vorhandenen Interface-Methode

Public Sub dataChanged()
 // Part of the dataNotificationReceiver interface.
 informControls
End Sub

Wenn Sie mich fragen würden, was an der Programmierung mir besonders am Herzen liegt, dann ist das, glaube ich, der philosophische Aspekt. Doch, ernsthaft! Wir finden hier das 3. Prinzip des Hermes Trismegistos am Wirken:

Wie oben, so unten; wie unten, so oben. Wie innen, so außen; wie außen, so innen. Wie im Großen, so im Kleinen.

So wie die Datenbank ihre Tabellen zur Selbsterforschung anregen kann, so können die Tabellen ihre Felder aktivieren, so können die Felder ihre Controls aktualisieren. Später wird’s auch in umgekehrter Reihenfolge gehen, von unten nach oben. Wenn das nicht hübsch ist!

Und nun – tatatatata! – wird’s mal Zeit für ein solches Steuerelement! Das wird eine neue Klasse,

eaSyQLTextField

und sie basiert auf dem Super TextField. Das soll wieder bescheid wissen, zu wem es gehört. Ergo

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

und

Private Property mTable as xojo.core.weakref

sowie

Public Property Field as eaSyQLField
 Get
  if mField <> nil and mField.Value <> nil then return eaSyQLField(mField.Value)
 End Get
End Property

und

Private Property mField as xojo.core.weakref

Dazu noch

Private Property ignoreTextChange as Boolean

nebst

Private Property LastValue as Text

und

Public Property dataTable as Text

IgnoreTextChange wird ein Flag, das verhindert, dass der TextChange-Event während des Setzens per Recordset-Änderung feuert. LastValue puffert den dort gesetzten Textwert, um später entscheiden zu können, ob die Datenbank mit einem neuen Wert aktualisiert werden muss. Und DataTable schließlich nimmt den Namen der Tabelle für die automatische Anmeldung auf. Automatische Anmeldung?

Control sucht Anschluss

Wenn Sie sich ein stinknormales TextField im Inspector anschauen – und zwar den erweiterten Teil, den Sie finden, wenn Sie auf das kleine Zahnradsymbol oberhalb des Inspectors klicken –, dann kennt dieses zwei String-Properties, nämlich DataSource und DataField. Diese kapere ich einfach und ergänze sie mit dataTable, das ich per Rechtsklick auf die Klasse eaSyQLTextField und Wahl von „Inspector Behavior“ im Inspector zugänglich mache.

inspecbehavior-easytextfield

Ein eaSyQLTextField, auf ein Fenster gezogen, sieht dann so aus:

inspec-easytextfield

Ich habe hier Konstanten benutzt, die für den Dateinamen („EddiesElectronics.sqlite“), die Tabelle („Customers“) und das Feld stehen („FirstName“). Würde aber ebenso mit den Namen selbst funktionieren. Mein Tipp wäre, immer Konstanten zu verwenden – nicht nur, dass damit Zeit gespart wird, Vertipper werden ausgeschlossen. Und eine hierarchische Benamsung (wie hier: Tabellen-Konstanten beginnen mit „Tab“, Feldnamen mit „Field“) hilft dann auch noch, Übersicht zu bewahren.

Um das Control nun aber auch wirklich anzumelden, füge ich einen Eventhandler hinzu:

Sub Open() Handles Open
 RaiseEvent open
 xojo.core.timer.CallLater 50, Addressof Lateinit
End Sub

und dupliziere den Open-Event, damit er immer noch individuell belegt werden kann.

Die leichte Verzögerung, mit der die Methode LateInit aufgerufen wird, soll dazu führen, dass grundsätzlich die Datenbank mit ihrem Delay von 0 sich vorher initialisieren. Das ist, wohlgemerkt, experimentell! Sollte sich diese Lösung als unzuverlässig herausstellen, muss hier nich nachgebessert werden. Momentan aber funktioniert sie, und sie sieht so aus:

Public Sub Lateinit()
 dim databases() as eaSyQLDataBase = eaSyQLDataBase.Databases
 dim count as integer = databases.Ubound
 for q as integer = 0 to count
  dim dsource as eaSyQLDataBase = databases(q)
  if dsource.Databasename = me.DataSource.ToText then
   dim tab as easyQLTable = dsource.TableWithName(me.dataTable)
   if tab <> nil then
    mTable = xojo.core.WeakRef.Create(tab)
    dim field as eaSyQLField = tab.fieldwithname(me.DataField.ToText)
    field.addDataNotificationReceiver me
    mField = xojo.core.WeakRef.Create(field)
   end if
   exit for
  end if
 next
 if mTable = nil then
  dim err as new xojo.Core.ErrorException
  err.Message = me.Name+easyql.space+eaSyql.kCouldnotFindTable+eol+me.dataTable
  raise err
 elseif not (me.DataField.ToText.Empty) and field = nil then
  dim err as new xojo.Core.ErrorException
  err.Message = me.Name+easyql.space+eaSyql.kCouldnotFindField+eol+me.DataField
  raise err
 end if
End Sub

Das Control schaut also in der gesharten Property Databases der Klasse eaSyQLiteDatabase nach einer Datenbank, die auf den hinterlegten Namen hört. Gibt es diese, sucht es nach der gewünschten Tabelle, speichert diese als weakRef und wiederholt dasselbe mit dem Feld. Bei Erfolg registriert es sich bei seinem Feld. Hat es am Ende keine Tabelle gefunden (theoretisch könnte Bedarf entstehen, ein TextField auch nur einer Tabelle und keinem definierten Field zuzuordnen, etwa um eine Berechnung basierend auf dem Datensatz auszugeben – deshalb prüfe ich zunächst nur die Existenz der Tabelle, auch wenn die Tabellen noch nicht auf direkte Control-Interaktion ausgelegt sind), wird eine Exeption ausgespuckt, und falls ein Field definiert war, aber nicht gefunden wurde, wiederholt sich das entsprechend. Die Textkonstanten des EasyQL-Moduls sind dabei

Protected Const space as Text =  " "

und

Protected Const kCouldnotFindTable as Text = "could not find the specified table."
Protected Const kCouldnotFindField as Text = "could not find the specified field."

Außerdem die

Public Property EOL as Text
 Get
  return (&u0A)
 End Get
End Property

Die den Unitext-Wert für ein EndOfLine liefert.

Was fehlt noch? Ach ja, die

Public Sub dataChanged()
 // Part of the dataNotificationReceiver interface.
 me.ignoreTextChange = true
 if not raiseevent DataChanged then me.text = Field.TextValue
 me.LastValue = me.text.ToText
 me.ignoreTextChange = false
End Sub

Zusammen mit dem

Event DataChanged() As Boolean

Wird also der DataChanged-Event angestupst, feuert der gleichnamige Event. Darin ist es dann möglich, einen individuellen Wert auszugeben. Das sollte man dann mit True bestätigen, da ansonsten der TextValue des Fields benutzt wird.

Noch ein (vorerst) letzter Eventhandler:

Sub TextChange() Handles TextChange
 if not ignoreTextChange then RaiseEvent TextChange
End Sub

Nach dem der Event selbst wieder einmal dupliziert werden muss.

Damit das alles nun funktioniert, benötigt EasyQLDatabase eine weitere Methode – deren Zweck kennen Sie von der Tabellen-Klasse:

Public Function TableWithName(TableName As Text) as eaSyQLTable
 For q as integer = 0 to tables.Ubound
  if tables(q).TableName = TableName then Return tables(q)
 Next
End Function

Fertig?

Insgesamt noch lange nicht. Um etwas anzusehen, reicht dies aber. wenn Sie bis hierher durchgehalten haben, will ich Ihnen die restliche Arbeit ersparen – bzw. Ihnen eine funktionsfähige Vorlage liefern.

Dies hier ist das Projekt minimal erweitert:: Starten Sie es, klicken Sie auf „OK“, um die Datenbank zu laden, und dann können Sie einige der Feldinhalte der Customers-Tabelle einsehen – allerdings noch nicht verändern. Das wäre dann der nächste Teil …

easyqlwindow2

 

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

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