Session D-CTRA

Transaktionale Client/Server Entwicklung mit Remote Views und SQL Pass Through

Arturo Devigus
Devigus Engineering


Zusammenfassung

In dieser Session wird aufgezeigt, wie eine Client/Server Lösung gleichzeitig mit Remote Views und SQL Pass Through (kurz: SPT) transaktional aufgebaut werden kann. Es werden zunächst die Grundlagen von SQL Server Transaktionen erläutert. Insbesondere werden die im Zusammenhang mit Transaktionen bekannte "ACID" Rule, sowie die SQL Server "Isolation Levels" erläutert und anschaulich erklärt, welche Auswirkungen das Auslösen von Transaktionen auf dem SQL Server hat. Es wird aufgezeigt, wie mehrere updatable Remote Views in ein und denselben Transaktionsprozess zu integrieren sind. Am Beispiel einer Visual Extend Client/Server Anwendung wird aufgezeigt, wie Remote Views und SQL Pass Through in der Praxis koexistieren können und wie jeweils das Beste aus beiden Ansätzen für die eigene Client/Server Anwendung verwendet werden kann. Diese Session eignet sich auch für diejenigen, welche in die n-Tier Entwicklung unter Microsoft Transaction Server einsteigen wollen und sich hierzu das nötige Grundverständnis bzgl. Transaktionen aneignen wollen.

Voraussetzungen

Sie haben bereits erste Client/Server Anwendungen entwickelt oder haben dies in der nächsten Zeit vor. Sie wollen sich einen Überblick über transaktionale Aspekte in der Client/Server Entwicklung verschaffen, welche Sie in Ihren eigenen Projekten sofort umsetzen möchten. Es wird vorausgesetzt, dass der Teilnehmer weiss, was eine Datenbank Transaktion ist, und dass in einer 2-Tier Client/Server Anwendung der Client immer mit einer Connection auf den Datenbank Server zugreift.

Einführung in Datenbank Transaktionen

Bei allen Transaktionen geht es immer wieder um dasselbe Grundproblem: Wie stelle ich sicher, dass eine Abfolge von einzelnen Operationen als ganzes, untrennbares Paket betrachtet wird. Hierbei muss sichergestellt werden, dass nicht Teile meines Gesamtpaketes erfolgreich abgeschlossen werden und andere hingegen nicht. Nur wenn alle im Paket enthaltenen Operationen erfolgreich waren, dürfen alle darin enthaltenen Operationen bestehen bleiben. Oder umgekehr formuliert: Sofern auch nur eine einzige Operation innerhalb einer gesamten Transaktion fehlgeschlagen hat, müssen alle Änderungen, welche durch irgendeine andere Operation in derselben Transaktion ausgelöst wurden, wieder rückgängig gemacht werden.

Um sicher zu gehen, dass meine Transaktion möglichst sicher und erfolgreich abgeschlossen werden kann, würde es an und für sich genügen, alle Tabellen, welche innerhalb der Transaktion angesprochen werden, vor Zugriffen anderer Transaktionen und oder Datenbank Benutzer zu schützen. Wir alle wissen, wie so etwas am einfachsten zu bewerkstelligen ist: Mit einem möglichst "grobkörnigen" Locking Mechanismus, z.B. auf Stufe Tabelle mit einem "Table" Lock. Es ist klar, dass bei weniger grobkörnigen Locking Mechanismen, wie z.B. "Page" Locking oder "Record" Locking die Gefahren zunehmen, dass v.a. bei Transaktionen mit mehreren Einzeltransaktionen Konflikte entstehen, da die Daten, auf welche in der Transaktion zugegriffen werden soll, evtl. nicht verfügbar sind. In der Praxis haben wir es hier mit einem ausgewachsenen Zielkonflikt zu tun und wir haben abzuwägen, wie fein wir unsere Datensätze wirklich sperren wollen um unsere eigene Transaktion zu bevorzugen, ohne jedoch andere Transaktionen und Datenbank Benutzer zu stark einzuschränken. Dieser Zielkonflikt ist unvermeidlich. Würde er nicht existieren, bräuchte man sich in der Welt der Datenbank Transaktionen um einiges keine Gedanken zu machen. In Tat und Wahrheit ist es aber so, dass alle Datensätze, welche im Zusammenhang mit einer Transaktion verändert werden, für andere Benutzer oder Transaktionen nicht mehr, oder nur mit Einschränkungen (wie wir weiter unten noch sehen werden), zur Verfügung stehen. Deshalb ist es unausweichlich, sich mit den Mechanismen von Datenbank Transaktionen seriös auseinanderzusetzen.

Soll Eigenschaften von Transaktionen: ACID

Transaktionen müssen von sich aus schon so konzipiert sein, dass möglichst wenig Konfliktpotential entsteht, wenn Sie abgearbeitet werden. Wichtig ist in diesem Zusammenhang, dass sich Transaktionen an einige Grundregeln halten. Werden diese eingehalten, so haben wir mit einer gewissen Wahrscheinlichkeit ein erfolgreiches Betreiben der Transaktionen ermöglicht.

Die Abkürzung ACID steht hierbei für folgendes:

Atomic

Atomar: Eine Transaktion soll als unzertrennbares Atom betrachtet werden, entweder als ganzes erfolgreich, oder als ganzes nicht erfolgreich. Das Atom darf quasi nicht gespalten werden.

Consistent

Konsistent: Einzelne Operationen innerhalb einer Transaktion müssen bereits konsistent sein. Sie dürfen nicht bestehende Datenbank oder Business Regeln weder explizit noch implizit verletzen.

Isolated

Isoliert: Das System muss noch nicht bestätigte Veränderungen einer Transaktion vor allen anderen Transaktionen schützen. Dieses Schützen erfolgt i.d.R. durch Locking der in der Transaktion beteiligten Datensätze.

Durable

Beständig: Wenn eine Transaktion bestätigt wurde, dann muss die Datenbank in der Lage sein, alle vorgenommenen Veränderungen permanent zu speichern. Im Falle eines Systemausfalles müssen diese wieder hergestellt werden können.

Die Locking Mechanismen des SQL Servers

Der SQL Server besitzt seit der verseion 7.0 neu auch die Möglichkeit des Record Lockings. In SQL Server 6.5 gab es dies nur bedingt, d.h. nur beim Einfügen von neuen Datensätzen am Ende einer Tabelle in der letzen Daten Seite.

Das Vorhandensein des Record Locking Features sollte jedoch nicht darüber hinwegtäuschen, dass es trotzdem durchaus Sinn machen kann, wenn man seitenweise lockt. Auf einer Seite sind mehrere Datensätze untergebracht und durch eine Page Lock Operation können mehrere Datensätze in Serie rascher upgedated werden als wenn dies mit einzelnen Record Locks zu erfolgen hätte.

Bei einem SQL Statement kann man mit sogenannten "Locking Hints" steuern, wie granular gelockt werden soll:

UPDATE customer WITH (ROWLOCK) SET item = item * 1.1 WHERE type = 1

obiges SQL Statement würde alle Datensätze einzeln mir Record Locks abwickeln.

Folgende SQL Server Locking Hints gibt es:

Hint

Beschreibung

PAGLOCK

Sperre die gesamte Seite (meherer Datensätze betroffen)

ROWLOCK

Record Locking auf Stufe Einzeldatensatz

TABLOCK

Table Lock. SQL Server hält diesen Lock nur solange, bis das Statement abgearbeitet ist. Nur wenn HOLDLOCK zusätzlich angegeben wird, wird der Table Lock bis ans Ende der aktuellen Transaktion beibehalten

TABLOCKX

Exclusiver Lock auf die Tabelle. Dieser Lock verhindert, dass andere von der Tabelle irgendwelche Daten auch nur lesen können. Kann, wie TABLOCK auch, auf das Statement beschränkt bleiben oder bis zum Ende der Transaktion ausgedehnt werden.

Der SQL Server unterscheidet zwischen "Write" Locks und "Read" Locks. Die "Write" Locks werden auch als "Exclusive" Locks bezeichnet und die "Read" Locks auch als sogenannte "Shared" Locks. Ein "Write" Lock ist mit einem anderen "Write" Lock im Konflikt, und auch mit anderen "Read" Locks. "Read" Locks hingegen sind grundsätzlich mit anderen "Read" Locks nicht im Konflikt. Es ist aber wichtig zu verstehen, dass eine Transaktion keinen "Write" Lock erhalten kann, wenn gerade ein "Read" Lock offen ist. Diese fundamentale Eigenschaft des SQL Servers stellt sicher, dass Daten, welche gerade in einer Transaktion gelesen werden, nicht gleichzeitig verändert werden können.

SQL Server Isolation Levels

Die alles entscheidende Frage ist nun: "Wenn ein Lesevorgang innerhalb einer Transaktion einen "Read" Lock absetzt, wie lange soll dieser Lock bestehen bleiben?" Je länger ein "Read" Lock bestehen bleibt, desto eher ist meine eigene Transaktion "isoliert" von anderen. Als Nachteil erweist sich natürlich einmal mehr die Verfügbarkeit des Systems für andere Benutzer.

Genau aus dem Grund ist es möglich, selber zu beeinflussen, welchen "Isolation" Level man bzgl. der Locks wünscht:

Isolation Level

Description

Read Uncommitted

Die Transaktion kann alle Daten lesen, unabhängig davon, ob nun ausstehende “Write” Locks vorhanden sind oder nicht. Zusätzlich wird die Transaktion in diesem Modus keine eigenen “Read” Locks absetzen. Dieser Isolation Level bringt natürlich die beste Performance ist aber anfälliger auf Dateninkonsistenz denn dieser Isolation Level ist der am wenigsten Restriktive und kann zu sogenannten “Dirty Reads” führen, d.h. dass Daten, welche eigentlich “nicht stimmig sind” und von anderen Transaktionen noch nicht bestätigt wurden (und deshalb evtl wieder zurückgestellt werden!) trotzdem gelesen werden können!

Read Committed

Hier kann die Transaktion nur Daten lesen, welche bereits “committed“ wurden, d.h. es muss gewartet werden, bis offene “Write” Locks abgeschlossen sind. Für das Lesen von Daten werden neue “Read” Locks abgesetzt, welche aber bereits nach Abschluss des SQL Statements (d.h. vor Ende der Transaktion!) wieder freigegeben werden. Es ist nicht sichergestellt, dass ein erneutes Lesen derselben Daten innerhalb der Transaktion zu demselben Resultat führt. Dieser Isolation Level ist die Standardeinstellung für ADO und via VFP(!) wenn NICHT aus MTS heraus betrieben!

Repeatable Read

Analog zu “Read Committed” mit der Ausnahme, dass die “Read” Locks bis ans Ende der Transaktion aufrecht erhalten werden. SQL Server 6.5 unterstützt diesen Level nicht, er stellt in einem solchen Fall auf “Serializable” um. Beim Isolation Level “Repeatable Read” werden alle in einer Query bezogenen Daten gelockt (“Read” bzw. “Write” Locks) aber andere Benutzer und Transaktionen können neue Datensätze in die betroffenen Tabellen weiterhin einfügen, weshalb ein erneutes Lesen innerhalb der Transaktion diese neuen Datensätze sehen würde.

Serializable

Analog zu “Repeatable Read” wobei zusätzlich sichergestellt wird, dass selbst bei mehrmaligem Ausführen von ein und demselben SQL Statement immer dieselben Resultate gesehen werden. Es wird verhindert, dass neue Datensätze angelegt werden (dass bestehende nicht entfernt werden können, wird ja bereits durch die Locks sichergestellt), solange die Transaktion läuft. Dies wird durch “Table” oder “Index” Locks sichergestellt. Dieser Isolation Level ist der Standard bei allen MTS Komponenten!

Das Setzen des Transaction Isolation Levels erfolgt mit folgender TSQL Anweisung, welche pro Connection via SQL Pass Through gesetzt werden kann:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

SQL Server Transaktionen aus MTS

Für diejenigen, welche in die n-Tier Entwicklung unter MTS einsteigen, ist es wichtig zu verstehen, dass für das Sicherstellen von Transaktionen aus dem MTS heraus der DTS (Distributed Transaction Coordinator) verwendet wird. DTS wird auf den Maschinen, auf welchen der entsprechende SQL Server läuft, als Service betrieben. Zusammen mit der oben gewonnenen Erkenntnis, dass der Isolation Level "Serializable" der Standard für alle Transaktionen aus MTS heraus ist, ist sofort klar, dass Performance Aspekte im MTS Umfeld sehr wohl ein Thema sind, ist doch der Isolation Level "Serializable" derjenige der am "verschwenderischsten" mit der Verfügbarkeit umgeht oder anders gesagt, auf der vorsichtigen Seite operiert, was die eigene Transaktionssicherheit angeht, den anderen Benutzern und Transaktionen aber entsprechend weniger Spielraum lässt.

SQL Server Transaktionen aus VFP heraus steuern

Nun haben Sie bereits ein sehr solides Fundament erhalten, um wagemutig Ihre ersten SQL Server basierten Datenbank Transaktionen aus VFP heraus ansteuern zu können. Sie werden sehen, das geht ganz einfach. Bevor wir beginnen, halten wir uns aber nochmals folgendes vor Augen:

Um eine Verbindung zum SQL Server herzustellen, können wir aus VFP heraus folgendermassen vorgehen:

lncon = sqlconnect("fibu")

Wenn die Verbindung erfolgreich war, dann erhalten wir eine gültige Connection Nummer zurück. Diese müssen wir uns natürlich abspeichern, damit wir bequem damit arbeiten können. Dies erfolgt in obigem Beispiel in der Variablen lncon. Der Paramerer "fibu" ist der Name des ODBC Data Source Names (DSN).

Um nun eine Transaction zu beginnen, muss folgende Anweisung eingegeben werden

sqlsetprop(lncon, "Transactions", 2)

Diese Anweisung stellt sicher, dass die Transaktionsverarbeitung manuell gesteuert werden kann. Erst wenn mit der Anweisung SQLCOMMIT(nConnectionHandle) die Transaktion bestätigt, bzw. mit SQLROLLBACK(nConnectionHandle) verworfen wird, ist die Transaktion beendet. In unserem Beispiel können wir das folgendermassen zeigen:

?sqlexec(lncon, "update hinweis set bez = 'aaa' where hinweis = 1")

Wenn nun ein anderer Benutzer denselben Datensatz lesen möchte, dann kann er das, entsprechend dem, was wir eingangs dieser Session gelernt haben, NICHT, da unsere Transaktion den betroffenen Datensatz gesperrt hat. Man kann das selber ganz einfach nachvollziehen, indem man eine neue Connection eröffnet (nicht vergessen: alles läuft pro connection!) und versucht, auf diesen Datensatz zuzugreifen. Natürlich ohne Erfolg! Erst wenn die erste Connection die Transaktion beendet, entweder durch SQLCOMMIT oder SQLROLLBACK, ist die Tabelle wieder frei und kann von einer anderen Connection bezogen werden.

Quizfrage: Was passiert, wenn ich oben statt einer "update" Anweisung ein "select from" abgesetzt hätte? Wenn Sie diese Frage nicht beantworten können, habe ich meine Aufgabe nicht erfüllt oder Sie waren mit Ihren Gedanken anderswo. Ich hoffe natürlich das zweite. Ganz im Ernst: Der Isolation Level "Read Committed" stellt sicher, dass die "Read" locks (auch "shared" Locks genannt) gleich nach Beendigung des SQL Statements wieder frei gegeben werden. Zudem würden sich zwei "Read" Locks sowieso nicht gegenseitig behindern. Nur die "Write" Locks, (auch "exclusive" Locks genannt) behindern sich gegenseitig. Ich betone hier diese Begriffe ganz bewusst nochmals, insbesondere auch die Begriffe "Shared" Locks und "Exclusive" Locks, da diese sehr häufig verwendet werden und man sehr gut daran tut, sich deren Bedeutung immer wieder vor Augen zu führen.

Die alles steuernde Connection

Wir haben nun gelernt, dass die Connection das Mass aller Dinge ist. Dieser Sachverhalt kann gar nicht genug hervorgehoben werden. In der Praxis bedeutet dies, dass man sich über jede Connection, unter welcher man Transaktionen betreiben will, klar werden muss und man das Verwenden von "irgendwelchen" Connection Nummern auf gar keinen Fall dem Zufall überlassen kann.

Wenn man nun alle Connctions von Hand durch explizite SQLCONNECT() Anweisungen herstellen würde, könnte man selbst steuern, welche Connections verwendet werden und was damit gemacht wird. In VFX haben wir für diesen Sachverhalt eigens einen Connection Manager geschrieben, der das Erstellen und Verwalten von Connections auf Applikationsebene vereinfacht.

Will man nun aber mit den sehr praktischen VFP remote views operieren, so muss man auf folgendes achten:

Die Connection Definition im DBC

Wird eine remote view erstellt, so muss man zunächst eine Connection aus dem DBC auswählen. Doch Achtung: Diese DBC Connection ist nur die Definition, wie auf den SQL Server zugegriffen werden muss und hat mit den effektiven Connections, welche später durch das Öffnen von verschiedenen remote views verwendet werden, nichts zu tun. Die Connection Definition wird im DBC abgespeichert.

Die im DBC zu definierende Connection beinhaltet folgende Informationen:

Um zur Laufzeit die Connection den aktuellen Bedingungen beim Kunden am besten anpassen zu können empfiehlt es sich, mit der Option "Connections string" zu arbeiten. Ein ausgewachsener Connection String beinhaltet folgendes:

DSN=Fibu;SERVER=(local);UID=;APP=Microsoft® Visual FoxPro®;WSID=HINOTE2000;DATABASE=Fibu;AutoTranslate=No;
Trusted_Connection=Yes;UseProcForPrepare=0;QuotedId=No;
AnsiNPW=No

Wobei die einzelnen Optionen im Wesentlichen denjenigen Einstellungen entsprechen, welche auch beim Definieren eines ODBC-DSN Eintrages vorkommen.

TIP:Um obigen Connection String zu erhalten, kann man auf "Verify Connection" klicken.

WICHTIG: Um beim Einsatz Ihres Programmes beim Kunden keine Probleme zu haben, müssen Sie unbedingt sicherstellen, dass keine umgebungsspezifischen Einstellungen im Connection String vorkommen. Am sichersten ist es, wenn Sie hierzu nur den DSN=DsnName Eintrag, wie in obigem Bild dargestellt, verwenden. Das gibt Ihnen die Möglichkeit, beim Kunden vor Ort die Einstellungen in der DSN Definition, d.h. beim Definieren der ODBC Verbindung, vorzunehmen. Um zur Laufzeit den Connection String zu manipulieren, z.B. weil der Kunde mit einem anderen DSN Eintrag arbeiten muss, oder diesen zur Laufzeit ändern will, bietet VFP folgende Möglichkeit:

dbsetprop("conFibu", "CONNECTION", "ConnectString", "dsn=" + alltrim(m.gs_dsn))

 

In VFX bietet sich z.B. die Methode onpostlogin() an, um die gewünschten Einstellungen vorzunehmen. Hier ein Beispiel:

PROCEDURE onpostlogin

   dodefault()

   *--dsn ggfl. setzen

   if !empty(m.gs_dsn)

      dbsetprop("conFibu", "CONNECTION", "ConnectString", ;

                "dsn=" + alltrim(m.gs_dsn))

   else

      dbsetprop("conFibu", "CONNECTION", "ConnectString", "dsn=fibu")

   endif

 

 

endproc

In obigem Beispiel wird der Connection String entsprechend dem Eintrag in der System Variablem m.gs_dsn gesetzt sowie weitere, applikationsspezifische Prüfungen vorgenommen.

Nachdem wir die Connection in unserem DBC richtig eingestellt haben und auch wissen, wie wir diese zur Laufzeit manipulieren können, sind wir bereit für den nächsten Schritt:

Connections und Remote Views

Beim Erstellen einer Remote View muss immer zuerst eine im DBC vorhandene Connection ausgewählt werden.

Wird die Remote View anschliessend erstellt, ist in der View Definition die in obigem Dialog ausgewählte Connection Definition fest abgespeichert. Wird die Remote View erstellt, muss im Menu "Query"

die Option "Advanced Options…" angewählt werden, um folgende Einstellungen im "Advanced Options" Dialog vornehmen zu können:

Die Checkbox "Share Connection" ist mitunter eine der wichtigsten überhaupt. Nur wenn diese angeklickt ist, kann sichergestellt werden, dass eine neu geöffnete Remote View versucht, eine bereits vorhandene Connection einer zuvor durch eine andere Remote View geöffnete Connection, zu nutzen.

ACHTUNG: Eine manuell zuvor geöffnete Connection wird nie hierfür in Betracht gezogen! Das ist auch gut so, denn VFP kann ja nicht wissen, was mit dieser "fremden" Connection alles bewerkstelligt wurde oder noch wird. Vielleicht wurde damit ja bereits ein SQL Prepare String an den Server gesendet oder es läuft bereits eine Transaktion darauf.

Was VFP aber kann, ist das Verwenden einer Connection, welche VFP selbst für das Erstellen einer anderen Remote View bereits eröffnet hat. Das und nur das bedeutet "Share Connection".

Transaktion mit verschiedenen Remote Views und SPT

Will ich nun eine Transaktionale Verarbeitung mit verschiedenen vorhandenen, updatable Remote Views, sowie evtl. eigener SQL Pass Through (SPT) Routinen verwenden, dann muss als Voraussetzung folgendes erfüllt sein: Alle Remote Views müssen mit ein und derselben Connection arbeiten. Andernfalls ist es unmöglich, diese in ein und denselben Transaktionsprozess einzubinden.

Diese wichtige Voraussetzung ist nur dann erfüllbar, wenn alle beteiligten Remote Views die Option "Share connection", so wie weiter oben beschrieben, angewählt haben. Um mit der weiter oben beschriebenen Methode SQLSETPROP(nConnectionhandle, "TRANSACTIONS", 2) Anweisung eine manuelle Transaktion zu starten, ist es erforderlich, die aktuelle Connection der beteiligten Remote Views zu kennen. VFP bietet uns hierzu folgende Möglichkeit an:

lnconnection = CURSORGETPROP("ConnectHandle")

Durch obige Anweisung wird vom aktuellen Arbeitsbereich (optional könnte auch noch der Alias übergeben werden) die Connection Nummer ermittelt und zurückgegeben.

Ist dies einmal geschehen, habe ich eigentlich Tür und Tor offen, um selbst zu bestimmen wie es weiter gehen soll. Zunächst einmal kann ich die Transaktion starten:

sqlsetprop(lnconnection, "Transactions", 2)

Und meine benötigte transaktionale Business Logik durchlaufen um am Schluss wieder mit

                sqlcommit(lnconnection)

die Transaktion im Erfolgsfall permanent zu beenden, oder aber mit

sqlrollback(lnconnection)

die Transaktion zurückzufahren, und alle in der Transaktion vollzogenen Änderungen auf der Datenbank wieder rückgängig zu machen.

In VFX kann dies z.B. durch das Überschreiben der onsave() Methode in einem Form folgendermassen aussehen:

local laError[7], lViewMessage, lSaved, lValid, lCanSave, oMatEingang

local lcoldpoint

lcoldpoint = SET("POINT")

set point to "."

 

if thisForm.lUseHook and !thisForm.OnEventHook("OnSave",this,thisForm)

   set point to (lcoldpoint)

   return .t.

endif

 

select (thisform.cWorkAlias)

 

if !empty(tlFromChild)

   tlAllTable = .f.

else

   tlAllTable = .t.

endif

 

lSaved   = .f.

lValid   = .f.

lCanSave = .t.

 

if thisform.nFormStatus == ID_EDIT_MODE

   lCanSave = thisForm.OnPostEdit()

endif

 

this.lcanedit = this.lactcanedit

 

lValid = thisForm.Valid()

 

this.SetObjectFocus()

 

this.OnWriteBuffer()

 

if lCanSAve and lValid

   select (thisform.cWorkAlias)

 

   if type("goProgram")=="O"

      goProgram.ClearAllIdx()

   else

      this.ClearIDX()

   endif

 

   do case

   case thisform.nformstatus = 2

      replace erf    with gu_user, ;

            erfdat   with datetime() in rvEingang

   case thisform.nformstatus = 1

      replace mut    with gu_user, ;

            mutdat   with datetime() in rvEingang

   endcase

 

   * Transaction

   LOCAL lnConnection, lntransok

   lnconnection = CURSORGETPROP("ConnectHandle")

   sqlsetprop(lnconnection, "Transactions", 2)

 

   * Applikationsspezifische Business Logik…

   * Prozessstatus setzen

 

   local lcUpdateSQL, lcTempCursor

   lcTempCursor = sys(2015)

   ** Bei neuer Prozessid muss auch ein neuer Record erzeugt werden

   lcUpdateSQL = "SELECT Prozessid FROM Prozess"+;

              " WHERE Prozessid = "+ str(nvl(rveingang.prozessid,0))

   if vfxsqlexec(lnconnection,lcUpdateSQL,(lcTempCursor)) <= 0

      aerror(laerror)

      =messagebox(“Vom Datenbank Server ist folgender Fehler” + ;

                  “ zurückgemeldet worden:" + CHR(13) + CHR(13) + ;

                  laerror[1,3] + CHR(13) + CHR(13) + ;

                  "Falls Sie durch Anpassen Ihrer Eingabe den “ + ;

                  “Fehler nicht verhindern können, " + ;

                  "nehmen Sie bitte mit Ihrem Systemadministrator “ +;

                  “Kontakt auf!",0+16,"Fehler bei Eingang/Prozess!")

      lntransok = .f.

   else

      select(lcTempCursor)

      if reccount() < 1

         lcUpdateSQL = "INSERT INTO Prozess (Prozessid, Stufe,” +;

                        “erf,erfdat,"+;

                        "einstieg,prozessstatus,prozessdatum) "+;

                    " VALUES ("+ str(nvl(rveingang.prozessid,0))+",'"+;

                        rveingang.stufe+"','"+;

                        rveingang.erf+"','"+;

                        dtos(datetime())+" "+ttoc(datetime(),2)+"',"+;

                        str(5)+","+;

                        str(5)+",'"+;

                        dtos(datetime())+" "+ttoc(datetime(),2)+"')"

      else

         lcUpdateSQL = "UPDATE Prozess SET Prozessstatus = 5 "+;

                    " WHERE Prozessid = "+ str(nvl(“ + ;

                    “ rveingang.prozessid ,0))+" and"+;

                    " (prozessstatus is null or Prozessstatus < 5)"

      endif

      select (thisform.cWorkAlias)

      if vfxsqlexec(lnconnection,lcUpdateSQL,(lcTempCursor)) <= 0

         aerror(laerror)

         =messagebox("Vom Datenbank Server ist folgender Fehler “ + ;

                     “zurückgemeldet worden:" + CHR(13) + CHR(13) + ;

                     laerror[1,3] + CHR(13) + CHR(13) + ;

                     "Falls Sie durch Anpassen Ihrer Eingabe den “ + ;

                     “ Fehler nicht verhindern können, " + ;

                     "nehmen Sie bitte mit Ihrem Systemadministrator“ +;

                     “ Kontakt auf!",0+16,"Fehler bei Eingang/Prozess!")

         lntransok = .f.

      else

         lntransok = tableupdate(.t., .t., "rveingang")

         if !lntransok

            aerror(laerror)

            =messagebox("Vom Datenbank Server ist folgender Fehler “ +;

                        “zurückgemeldet worden:" + CHR(13) + CHR(13) + ;

                        laerror[1,3] + CHR(13) + CHR(13) + ;

                        "Falls Sie durch Anpassen Ihrer Eingabe “ +;

                        “den Fehler nicht verhindern können, " + ;

                        "nehmen Sie bitte mit Ihrem “ +;

                        “Systemadministrator Kontakt auf!", ;

                        0+16,"Fehler bei Eingang/rvEingang!")

         else

            * Busineslogik

            oMatEingang = createobject("cMatEingang")

            lntransok = oMatEingang.Eintragen(lnconnection,;

                        "rveingang","rveingangpos")

            oMatEingang.Release()

            oMatEingang = .NULL.

            if !lntransok

               aerror(laerror)

               =messagebox("Vom Datenbank Server ist folgender “ +;

                           “Fehler zurückgemeldet worden:" + ;

                           CHR(13) + CHR(13) + ;

                        laerror[1,3] + CHR(13) + CHR(13) + ;

                        "Falls Sie durch Anpassen Ihrer Eingabe “ +;

                        “den Fehler nicht verhindern können, " + ;

                        "nehmen Sie bitte mit Ihrem “ + ;

                        “Systemadministrator Kontakt auf!",0+16,;

                        "Fehler bei Eingang/busineslogic!")

            endif

         endif

      endif

   endif

     

   if lntransok

      sqlcommit(lnconnection)

      lsaved   = .t.

   else

      sqlrollback(lnconnection)

     

      lsaved   = .f.

   endif

     

   sqlsetprop(lnconnection, "Transactions", 1)

   * end Transaction

endif

 

if !lSaved

   lViewMessage = .T.

   laError[1] = 0

  

   =aerror(laError)

  

   if laError[1] != 0

      lViewMessage = !this.ErrorHandler(laError[1])

   endif

 

   if lViewMessage and lValid

      = messagebox(MSG_NOT_SAVED,MB_ICONINFORMATION,MSG_ATTENTION)

   endif   

else

   if thisForm.nPageList >0

      this.pgfPageFrame.pages[thisForm.nPageList].enabled = .t.

      **

      this.pgfPageFrame.pages[thisForm.nPageList].grdgrid.enabled = .t.

   endif

 

   if tlAllTable

  

      thisForm.Caption = thisForm.cOldTitle

      this.nFormStatus = ID_NORMAL_MODE

      this.lEmpty      = .f.

   endif

 

   select (thisform.cWorkAlias)

 

   if !eof()

      go recno()

   else

      thisForm.oRecord.GoTop()

   endif

 

   this.RefreshToolBar(.t.)

   this.OnFormStatusChange()

 

   if !tlNotRefresh

      this.refresh()

   endif

  

   unlock all

endif

*!* End of code from cdataform.onsave.

set point to (lcoldpoint)

 

return lSaved

In obigem Beispiel wird schön ersichtlich, dass in der gesamten Speicherlogik, sowohl SPT Anweisungen, welche UPDATE oder INSERT Befehle absetzen neben ganz normalen TableUpdate Anweisungen verwendet werden.

Der Vorteil eines solchen Ansatzes liegt auf der Hand: Man kann die sehr praktischen Remote Views ohne weiters in eine Transaktionsverarbeitung integrieren, selbst wenn sich erst nach Fertigstellung eines Forms der Bedarf ergeben sollte, dass das ganze Abspeichern transaktional zu erfolgen hat.

Aufgepasst mit busy connections

Auf ein Risiko beim Arbeiten mit ein und derselben Connection bei mehreren Remote Views sei noch hingewiesen. Nämlich das Problem, dass eine Connection auch busy sein kann. Die häufigste Ursache liegt darin, dass nicht alle selektierten Datensätze bezogen wurden, da die Fetchsize nicht auf -1 eingestellt wurde. Es ist sehr wichtig, dass man die Views so parametrisiert, dass es immer Sinn macht, alle selektierten Datensätze sofort zu beziehen, und nicht mit dem Datenbezug zuzuwarten. Nur so kann verhindert werden, dass eine View "busy" bleibt.

Weitere Anwendungsbeispiele

Als weitere Anwendungsbeispiele zeige ich den Buchungsmechanismus der VFX Finanzbuchaltung. Dort werden alle Daten mit SPT transaktional abgearbeitet.

Ein weiteres Beispiel für die Verwendung von Remote Views für das Anzeigen und das Erfassen, jedoch SPT für das Abspeichern kann ebenfalls aus der VFX Finanzbuchhaltung gezeigt werden: Die Eingabe der Eröffnungssaldi.

Schlussbemerkung

Das Verständnis, wie Transaktionale Prozesse unter Verwendung bestehender Remote Views als auch SPT Anweisungen kombiniert werden können, eröffnet für das Erstellen von robusten, transaktionalen VFP Client/Server Anwendungen Tür und Tor. Es ist klar, dass spätestens jetzt, wo es MSDE als kostenlose SQL Server 7.0 kompatible Datenbank für das Verteilen und Betreiben von Client/Server Anwendungen gibt, die Frage, ob man eine neue Anwendung als File Server Anwendung mit "dbf" Dateien, oder als eine Client/Server Anwendung erstellen soll, immer häufiger zu Gunsten der Client/Server Architektur entschieden wird. VFP ist ein ausgezeichnetes Tool um hervorragende Client/Server Anwendungen zu erstellen und mit VFX steht dem Client/Server Entwickler ein ausgewachsenes und reifes Framework zur Verfügung.