Session D-BUFF

DataBuffering

Volker Stamme
Wizards & Builders GmbH


Zur Einstimmung

"Früher" gehörte es zum guten Ton zu ermöglichen, Änderungen oder neu erfaßte Daten nicht nur zu sichern, sondern gegebenenfalls auch zu verwerfen. Inzwischen gehört diese Fähigkeit zu den ersten Pflichten einer Anwendung und sollte tunlichst unterstützt werden. Voraussetzung ist natürlich, daß zu jeder Zeit der Eingabe sowohl die aktuellen Feldinhalte als auch der vorhergehende Zustand zur Verfügung stehen. In den Vor-Visual-Zeiten von FoxPro gab es eigentlich nur 2 Möglichkeiten, den ursprünglichen Inhalt zu Rekonstruktionszwecken zu sichern: Kopieren des gesamten Datensatzes z.B. mittels SELECT ... INTO CURSOR ... oder die Verteilung der betreffenden Felder auf Speichervariablen oder in ein Array mittels SCATTER. Bei letzterer Methode konnte man sich aussuchen, ob die Kopie bearbeitet werden sollte oder der Tabelleninhalt und je nachdem, für welche der beiden Möglichkeiten man sich entschieden hatte, war entweder die Sichern- oder die Verwerfen-Funktion im wesentlichen eine GATHER-Anweisung.

Die Zeiten haben sich geändert und seit der Einführung von Visual FoxPro gibt es noch eine, wesentlich elegantere Möglichkeit - DataBuffering. Ein "DataBuffer", oder zu Deutsch "Datenpuffer", ist eigentlich nichts weiter als eine Kopie der Originaldaten, also im Prinzip nichts anderes als das Resultat der oben erwähnten SCATTER / GATHER - Kombination. Warum man trotzdem die Möglichkeiten des DataBuffering nutzen und nicht irgendwelche anderen Hilfskonstrukte verwenden sollte, ist, neben den theoretischen Grundlagen, Inhalt des ersten Teils des folgenden Textes. Der zweite Teil setzt sich etwas mehr mit der Praxis auseinander und behandelt die Verwendung des Buffers und der damit verbundenen Funktionen.

Teil 1: Die Hintergründe

Ein Buffer ist also eine Kopie der Daten, die sich physikalisch auf der Festplatte befinden, und dient dazu, diese gespeicherten Daten wiederherzustellen, wenn die Bearbeitung abgebrochen wird. Soweit, so gut. Aber was ist daran jetzt eleganter oder sogar besser als SCATTER?

Was wird gepuffert?

Einen ersten Anhaltspunkt zur Beantwortung dieser Frage findet man in der Tatsache, daß FoxPro 4 verschiedene "BufferModes" unterstützt. Die Satzweise Pufferung oder Row-Buffering ist das, was man auch mit SCATTER und GATHER realisieren könnte - Von dem Datensatz, der gerade in Bearbeitung ist, wird eine Speicherkopie angefertigt. Aber häufig will man ja auch mehrere Datensätze einer Tabelle bearbeiten oder neu anlegen und trotzdem auch die Möglichkeit haben, diese Änderungen zu stornieren. Hier ist ein Punkt erreicht, an dem SCATTER und GATHER jämmerlich versagen. Mit Buffering ist das allerdings kein Problem, denn neben der zeilenweisen Pufferung kennt VFP auch noch die Tabellenpufferung (Table-Buffering), die eigentlich Multi-Zeilen-Pufferung oder so heißen müßte. Aber dieser Modus erlaubt es tatsächlich, beliebig viele Zeilen einer Tabelle zu bearbeiten und dennoch von jeder einzelnen eine Sicherheitskopie zu besitzen. Die erste Unterscheidung der Buffermodes betrifft also die Anzahl der zu puffernden Datensätze.

Wer puffert?

Die zweite Unterscheidung wird dann interessant, wenn auch die Anzahl der Anwender größer als 1 sein kann, mit anderen Worten im Netzwerk. Sobald mehrere Anwender mit einem zentralen Datenbestand arbeiten, besteht potentiell die Gefahr von Kollisionen. Beispiel: Kunde Meier ruft an und teilt Sachbearbeiter X seine neue Faxnummer mit. Eine Woche später hat sich seine Adresse geändert und diesmal gerät er an Sachbearbeiter Y. X hat es verbummelt, die Änderung gleich einzugeben, und ausgerechnet in dem Moment, als Y, der weniger tranige, die Adressänderung eingibt, ändert X die Faxnummer - des gleichen Kunden, wohlgemerkt.

Solange beide Sachbearbeiter noch fleißig tippen und - dank Buffer - die Änderung nur in der lokalen Speicherkopie passiert, ist alles in Butter. X ist schneller fertig als Y, weil's ja nur eine Faxnummer ist, und speichert ab. Y, der noch die alte Faxnummer aus seinem lokalen Puffer vor Augen hat, ist jetzt auch fertig und speichert seinen Puffer. Resultat: Y überschreibt mit veralteten Daten die eben eingegebene neue Faxnummer, woraufhin sämtliche Faxe an den Kunden im Nirwana landen und X wird verhauen, weil man ihm ob seiner Tranigkeit fahrlässige Verbummelung unterstellt.

Da niemand will, daß arme Sachbearbeiter mit falschen Anschuldigungen oder Prügel konfrontiert werden, gibt es Möglichkeiten, den eben beschriebenen Unfall zu vermeiden, nämlich Satzsperren. Die Pufferung unterscheidet hierbei 2 Möglichkeiten:

  1. Der Satz wird bei der geringsten Änderung gesperrt, woraufhin andere Anwender beim Versuch der geringsten Änderung die Meldung erhalten, daß der Satz bereits in Benutzung ist und deshalb jetzt und hier nicht bearbeitet werden kann. Da diese Form der Sperrung nur dann Sinn macht, wenn die Wahrscheinlichkeit ist sehr groß, daß mehrere Anwender gleiche Daten bearbeiten wollen, und hauptsächlich gestandene Schwarzseher diesem Fall die entsprechende Bedeutung zugestehen, heißt dieses Verfahren "pessimistische Pufferung".
  2. Der Satz wird nicht bei der Bearbeitung gesperrt, sondern erst dann, wenn diese Änderungen auch wirklich zurückgeschrieben werden. Das ist solange völlig problemlos, solange nicht der oben geschilderte Fall eintritt und mehrere Leute die gleichen Daten bearbeiten wollen. Diese Methode rechnet also zunächst einmal nicht mit möglichen Kollisionen, setzt demnach einen herzerfrischenden Optimismus voraus und heißt logischerweise "optimistische Pufferung".

Beides hat Vor- und Nachteile. Der Vorteil der pessimistischen Pufferung ist, daß Änderungen wirklich nur an einer Stelle vorgenommen werden können. Damit ist definitiv ausgeschlossen, daß an zwei oder mehr Arbeitsplätzen geändert und zurückgeschrieben wird und sich die verschiedenen Fassungen gegenseitig überschreiben. Der Nachteil hierbei ist, daß die Sperre erst nach ausdrücklichem Zurückschreiben oder Verwerfen der Änderungen aufgehoben wird. Wenn X also nach seiner Rückkehr aus einer mehrstündigen Pause beschließt, die schon fast halb eingegebene Faxnummer erst am nächsten Tag zu vervollständigen und stattdessen Feierabend macht, kann selbst der fleißigste Nachtarbeiter nichts an diesen Daten ändern.

Der Vorteil der optimistischen Methode ist, daß dieser "Kaffeepausen-Effekt" nicht eintreten kann, denn nur während des tatsächlichen Speichervorgangs wird der Datensatz kurzzeitig gesperrt. Der Nachteil sind die möglichen Kollisionen, die man einzeln berücksichtigen muß. Wie das geht, erläutert der 2. Teil dieses Textes.

Die eingangs erwähnten 4 Buffermodes sind die Kombinationen aus Satz- und Tabellenpufferung und den verschiedenen Sperrmodi und heißen im einzelnen

  • Pessimistische Zeilenpufferung (pessimistic row buffering)
  • Optimistische Zeilenpufferung (optimistic row buffering)
  • Pessimistische Tabellenpufferung (pessimistic table buffering)
  • Optimistische Tabellenpufferung (optimistic table buffering)

Weitere Unterschiede zu anderen Verfahren

Ein anderer Unterschied ist, daß ein Buffer von VFP automatisch unterstützt wird. Ein Buffer muß also nicht vor der Bearbeitung eines Datensatzes immer wieder aufs Neue definiert werden, sondern wird in der Regel pro Formular und Tabelle nur einmal angegeben und steht damit während der gesamten Laufzeit dieses Formulars auch zur Verfügung. Die SCATTER/GATHER Methode dagegen setzt voraus, daß bei jedem Datensatzwechsel die Speicherkopie "manuell" aktualisiert werden muß.

Falls mehrere Datensätze mittels SCATTER und GATHER z.B. in einem Array gepuffert werden sollen, müssen zudem alle Sätze einzeln zurückgeschrieben werden. VFPs Tablebuffering hingegen benötigt zunächst einmal nur einen Befehl, um beliebig viele Datensätze zu aktualisieren.

Welche Pufferung sollte verwendet werden?

Die Antwort auf diese Frage könnte von Radio Eriwan stammen - "das kommt darauf an...". Die Frage, ob satz- oder tabellenweise gepuffert werden muß, klärt sich normalerweise recht schnell. Wird z.B. ein Auftrag erfaßt, besteht dieser normalerweise aus einem Auftragskopf und mehreren Auftragspositionen. Beim Kopf würde also eine Zeilenpufferung völlig ausreichen. Bei den Positionen möchte man natürlich nicht jede einzeln speichern oder verwerfen, hier würde man also tabellenweise puffern.

Wenn an einem Arbeitsplatz ausschließlich neue Aufträge erfaßt werden sollen, ist auch die Gefahr von Kollisionen nicht gegeben, der Sperrmodus wäre also belanglos. Im Normalfall werden einmal erfaßte Daten aber auch nachbearbeitet und damit muß man sich - zumindest im Netz - auch mit der Sperrung auseinandersetzen.

Beiden Sperrmodi gemeinsam ist die Tatsache, daß VFP nicht automatisch kaskadiert sperrt. Es könnten also 2 verschiedene Sachbearbeiter im gleichen Auftrag unterschiedliche Positionen bearbeiten, ohne einander ins Gehege zu kommen. X bearbeitet die Positionen 1 und 3 und Y bearbeitet 2 und 4. Nehmen wir an, diese Änderung soll z.B. mit einem Zeitstempel im Auftragskopf protokolliert werden. Bei pessimistischer Pufferung würde das etwa so aussehen:

  • X liest Auftragskopf und Positionen
  • X bearbeitet Positionen 1 und 3; diese werden daher sofort gesperrt
  • Y liest Auftragskopf und Positionen
  • Y bearbeitet Positionen 2 und 4; auch diese werden sofort gesperrt
  • X ist mit seinen Änderungen fertig; beim Zurückspeichern wird mittels REPLACE der Timestamp im Auftragskopf geändert und damit der Kopf gesperrt.
  • Falls Y genau jetzt das gleiche versucht, wird eine Meldung erscheinen "AT-Kopf in Bearbeitung - bitte warten" oder sowas ähnliches. 2 Sekunden später ist X fertig und Y kann seine Änderungen ebenfalls schreiben. Y hat zuletzt gespeichert, der Zeitstempel ist also aktuell und korrekt. Alle Daten wurden gesichert - alles paletti.

Bei optimistischer Pufferung würde in diesem Beispiel genau das gleiche passieren, außer das X nicht schon mit dem REPLACE eine Satzsperre verursacht, sondern erst beim Zurückschreiben. Die Meldung wäre aber vermutlich dieselbe.

Solange also keine Überschneidungen auftreten, ist der Sperrmodus wurscht. Zweites Beispiel: Der Auftrag hat, der Einfachheit halber, nur eine Position. Der Preis eines Artikels kann kundenabhängig geändert werden und wird deshalb zusammen mit der Stückzahl und der Artikelnummer in den Auftragspositionen gespeichert. Der Kunde hat soeben eine gigantische Bestellung aufgegeben und erhält deshalb auch im Beispielauftrag einen günstigeren Preis. Für diese Rabattanpassung ist Y zuständig. X dagegen war am Telefon, als der Kunde die Stückzahl von 2 auf 3 erhöht hat. Die pessimistische Variante:

  • X nimmt sich den Auftrag vor und ändert in der Stückzahl 2 in 3 um. Der Satz wird sofort gesperrt.
  • X packen plötzliche Zweifel, ob die neue Stückzahl auch stimmt. X ist, wie gesagt, nicht der Hellsten einer, hat sich nichts notiert und muß deshalb den Kunden anrufen. Der ist in einer Besprechung und braucht demnach eine ganze Weile, bis er am Telefon ist.
  • Zwischenzeitlich hat sich auch Y den Auftrag geholt - Lesen funktioniert schließlich immer. Nur bei dem Versuch, aus den 50,- DM Einzelpreis 45,- DM zu machen, geht selbstredend der Sperrversuch in die Hose, weil X ja auch noch drin ist.

Beide ändern völlig unterschiedliche Werte, beide Änderungen führen zu einer Neuberechnung des Auftragswertes, beide Änderungen müßten also gefahrlos auch von zwei verschiedenen Arbeitsstationen aus möglich sein - optimistisch. Praktisch würde hierbei aber Y die falsche Stückzahl und X den falschen Preis abspeichern. So gesehen ist die pessimistische Variante doof und die optimistische liefert Datenschrott.

Den Datenschrott kann man elegant umschiffen, dazu mehr in den Code-Beispielen. Leider bedeutet das eine gewisse Menge zusätzlicher Arbeit, sowohl Theorie als auch Code. Die pessimistische Variante dagegen ist kugelsicher und bedarf nur eines Minimums an Rahmenarbeit - bleibt aber doof, da helfen bekanntlich keine Pillen. Zusammenfassend heißt das - Satz- oder Tabellenpufferung klärt sich von alleine, optimistisch oder pessimistisch hängt davon ab, ob Ihr Kunde ein im Kollisionsfall mitunter dämliches aber dafür preiswertes Produkt will oder ob er bereit ist, die Mehrkosten für eine clevere Anwendung zu tragen.

Teil 2: Praxis

Was ein Buffer grundsätzlich ist, wurde im vorangegangenen Teil weitgehend erläutert. Wichtig ist, daß ein Puffer im Gegensatz zu Selektionen oder Views keine Tabelle ist, keinen Aliasnamen besitzt und auch keinen Arbeitsbereich belegt. Alles spielt sich nur im Arbeitsspeicher ab. Das ist deshalb wichtig, weil man nicht gezielt sagen kann, ob man Informationen aus den ursprünglichen Daten, also von der Platte, haben will, oder ob man die möglicherweise bereits geänderten Daten im Puffer anspricht. Die Befehle und Funktionen von FoxPro lassen sich dahingehend nicht beeinflussen, sondern arbeiten immer nur mit Entweder oder mit Oder. In den meisten Fällen kann man logisch herleiten, welche Befehle wo arbeiten. REPLACE zum Beispiel. Sie arbeiten mit Buffering und ändern mittels REPLACE einen Wert - das soll natürlich im Puffer passieren, denn genau dafür ist er schließlich da. Die folgenden Tabellen listen die wichtigsten Befehle und Funktionen auf.

Funktionen, die sich auf den Buffer beziehen

Befehl / Funktion

Hinweis

APPEND

BLANK

BROWSE / EDIT

COPY TO

COUNT

DELETE

DELETE - SQL

 

DELETED( )

EXPORT

GATHER

IMPORT

INDEXSEEK( )

INSERT - SQL

 

ISBLANK( )

KEYMATCH( )

LIST

LOCATE / CONTINUE

LOOKUP( )

MEMLINES( )

MLINE( )

RECALL

REPLACE

SCATTER

SEEK

SEEK( )

SET RELATION

SUM

 

 

 

 

 

 

Findet neu angehängte Werte nicht, wenn diese in der Where-Klausel verwendet werden.

 

 

 

 

 

"Normales" INSERT liefert Fehler wenn Buffering eingeschaltet ist!

 

Wie man sieht arbeiten u.a. sämtliche Suchfunktionen nicht in den Originaldaten. Bei der Suche mit z.B. Seek oder Locate ist also zu beachten, daß

  1. die ursprünglichen Werte nicht mehr gefunden werden
  2. auch neu eingegebene Werte gefunden werden. Da diese aber noch keine gültige Satznummer besitzen, solange sie nicht zurückgeschrieben sind, werden in dem Fall negative Satznummern geliefert.

Funktionen, die mit Originaldaten arbeiten

Befehl / Funktion

Hinweis

SELECT - SQL

 

Befehle und Funktionen mit Sonderstatus

Befehl / Funktion

Hinweis

GO|GOTO / SKIP

 

Da ein Buffer wie gesagt keinen eigenen Arbeitsbereich darstellt, sind die Satzzeiger immer auf den Puffer bezogen. Wird z.B. ein neuer Datensatz angelegt, liefert RecCount() einen entsprechend erhöhten Wert zurück. Recno() liefert den "korrekten" Wert für Datensätze, die auch in den Originaldaten vorkommen. Falls Datensätze im Puffer neu hinzukommen, erhalten diese von VFP zunächst eine negative Nummer. Erst wenn diese Daten tatsächlich zurückgeschrieben werden, wird der Satzzeiger positiv.

Befehl / Funktion

Hinweis

CURVAL( )

GETFLDSTATE( )

GETNEXTMODIFIED( )

 

 

OLDVAL( )

SETFLDSTATE( )

 

 

liefert zurück, welche ORIGINALDATEN verändert wurden. D.h. im Puffer neu angelegte Datensätze werden nicht erkannt.

Diese Funktionen beziehen sich auf die Unterschiede zwischen Originaldaten, aktuellem Tabelleninhalt und Puffer, also den noch nicht gesicherten Änderungen. Ein Beispiel: In der Tabelle steht der Wert "sieben". An zwei Arbeitsstationen wird diese Tabelle nun gepuffert bearbeitet. Arbeitsstation 1 ändert "sieben" in "sechs" um und speichert die Änderungen. Arbeitsstation 2 ändert ebenfalls "sieben" um, nur diesmal in "fünf". Hier nun die Rückgaben der o.g. Funktionen und des aktuellen Feldinhaltes aus Sicht der beiden Arbeitsstationen:

    Arbeitsstation 1:

  • CurVal( ) liefert den aktuellen Tabelleninhalt. Dieser ist "sechs", da die letzten Änderungen bereits gesichert wurden.
  • OldVal( ) liefert das gleiche wie CurVal( ), da seit der letzten Sicherung nichts geändert wurde
  • Auch der aktuelle Feldinhalt, der den Puffer wiederspiegelt, hat den gleichen Wert wie CurVal( )

    Arbeitsstation 2:

  • CurVal( ) liefert den aktuellen Tabelleninhalt. Dieser ist "sechs", da Arbeitsstation 1 ihre Änderungen bereits gespeichert hat.
  • OldVal( ) liefert "sieben", denn das war der ursprüngliche Wert, genauer gesagt der Wert, der in der Tabelle stand, als der Datensatz geladen wurde
  • Der aktuelle Feldinhalt ist "fünf", da "sieben" abgeändert und noch nicht gesichert wurde

Das heißt, daß auf einer Station, die Ihre Änderungen erfolgreich und problemlos sichern konnte, diese 3 Werte stets identisch und damit praktisch nutzlos sind. Nur bei solchen Arbeitsstationen, die vor irgendwelchen Änderungen ihre Daten erhalten haben und nach diesen Änderungen die eigenen Änderungen sichern wollen, sind die 3 Werte relevant. Was man damit anstellen kann, kommt etwas später nochmal dran.

Bei neu angelegten Sätzen liefern CurVal() und OldVal() übrigens .NULL. zurück, beide Funktionen "kennen" neu angelegte Sätze also erst, nachdem diese erfolgreich gesichert wurden. Das gilt allerdings wieder nur für die Arbeitsstation, auf der die entsprechenden Sätze angelegt wurden. Woher sollte eine Station auch vom Pufferinhalt einer anderen Station wissen.

Ebenso erkennt GetNextModified( ) neu angelegte Datensätze nicht und liefert nur für bereits bestehende Datensätze brauchbare Werte zurück.

GetFldState() kann verwendet werden, um Datensätze und Felder zu finden, die im Puffer verändert wurden. Allerdings ist hier wieder zu beachten, daß GetFldState( ) nur lokale Änderungen berücksichtigt. GetFldState( ) liefert also im Grunde den Unterschied zwischen OldVal( ) und dem aktuellen Feld-(Puffer-) inhalt und kriegt Änderungen von anderen Arbeitsstationen schlicht nicht mit.

Sieht man sich die obigen Tabellen genau an, fällt auf, daß es außer CurVal( ) praktisch nur einen Befehl gibt, der tatsächlich ausschließlich nur Originaldaten, respektive den aktuellen Inhalt der Tabelle, liefert. Das reicht im allgemeinen auch aus, denn dieser Befehl ist außerordentlich vielseitig: SELECT - SQL.

Der Umgang mit Datenpuffern

Bis jetzt haben wir nur überlegt, welche Vor- und Nachteile eine Pufferung hat, was man beachten muß und wo zuweilen das Hämmerchen hängt. Aber wie wird so ein Puffer überhaupt erzeugt? 2 Möglichkeiten (schon wieder):

  1. die interaktive Möglichkeit. Nehmen wir an, Sie haben sich ein Formular erstellt und möchten mit diesem Formular eine gepufferte Tabelle verbinden. Das geht so:
    • Rechter Mausclick ins Formular; wählen Sie aus dem Popup-Menü "Datenumgebung" aus
    • Rechter Mausclick in die Datenumgebung; wählen Sie aus dem Popup-Menü "Hinzufügen" aus
    • Wählen Sie die gewünschte Tabelle aus
    • Rechter Mausclick auf das neue Cursor-Objekt, also die Repräsentation der eben ausgewählten Tabelle in der Datenumgebung; wählen Sie aus dem Popup-Menü "Eigenschaften" aus
    • Unter den "Daten"-Eigenschaften ist gleich der zweite Eintrag der gesuchte. "BufferModeOverride" heißt das Ding und steht normalerweise auf 1 - "Einstellungen der Form verwenden". Die Form hat also auch so eine Einstellung - da heißt sie allerdings "BufferMode" und kann nur die Werte 0 - keine Pufferung, 1 - pessimistische Pufferung und 2 - optimistische Pufferung annehmen. Weitere mögliche Werte, die BufferModeOverride außer den Vorgaben des Formulars annehmen kann, sind 0 - keine Pufferung, 2 - pessimistische Zeilenpufferung, 3 - optimistische Zeilenpufferung, 4 - pessimistische Tabellenpufferung und 5 - optimistische Tabellenpufferung.

Das war's schon. Sobald diese Form gestartet wird, werden Änderungen an dieser Tabelle in einem Puffer vorgenommen anstatt direkt in der Tabelle. Aber manchmal muß man ja auch in einem reinen Programmfile eine Tabelle öffnen und möchte diese trotzdem puffern - auch ohne dazugehöriges Formular. Dieses Verfahren möchte ich natürlich niemandem vorenthalten:

  1. die programmatische Möglichkeit. Sie schreiben also ein Programm für die Metzgereiverwaltung, möchten die Tabelle "WURST.DBF" verwenden und diese soll pessimistisch zeilengepuffert sein. Das geht so:
    • Der übliche Teil zum Öffnen der Tabelle, also
       
      IF NOT used( "wurst" )
      USE wurst IN 0
      ENDIF

      Oder so was ähnliches. Hauptsache, das Ding ist offen.
       

    • Jetzt kommt der Buffermode. Wie wir im interaktiven Beispiel gesehen haben, kann man eine Tabelle auch als Cursor bezeichnen, und BufferModeOverride war eine Eigenschaft desselben, zu gut Englisch "Property". Also brauchen wir die Funktion, mit der man die Properties eines Cursors set(z)en kann, nämlich CursorSetProp( ). Die Syntax lautet
      CursorSetProp( , , ). Im Code:
      CursorSetProp( "Buffering", 2, "Wurst" )

Die 2 steht dabei für pessimistische Zeilenpufferung, und weil man sich sowas eigentlich nicht merkt und außerdem der Code leserlich bleiben sollte, empfehle ich die Verwendung von sprechenden Konstanten. Das ist zwar etwas mehr Schreibarbeit, aber zahlt sich garantiert aus. Das sähe dann so aus:

#define  _CBUFF_NONE                1

#define  _CBUFF_PESSIMISTIC_ROW     2

#define  _CBUFF_OPTIMISTIC_ROW      3

#define  _CBUFF_PESSIMISTIC_TABLE   4

#define  _CBUFF_OPTIMISTIC_TABLE    5

Dieser Teil könnte z.B. in irgendeiner globalen .H - Datei stehen, wird also nur einmal geschrieben. Bei den Begleitdatein befindet übrigens die Include-Datei "d_buff.h", die das schon enthält.

Der CursorSetProp( )-Aufruf lautet demnach dann

CursorSetProp( "Buffering", _CBUFF_PESSIMISTIC_ROW, "Wurst" )

Ok, Tabelle ist offen, Buffermode ist gesetzt, also könnten wir jetzt Änderungen machen und diese wahlweise zurückschreiben oder verwerfen. Dazu fehlen nur noch die entsprechenden Funktionen. Sie heißen "TableUpdate( )" und "TableRevert( )", und weil die etwas umfangreicher sind, kriegen sie eine eigene Überschrift.

TableUpdate( ) und TableRevert( )

Fangen wir mit dem Updaten, also dem Zurückschreiben, an, weil das wohl die häufiger verwendete Funktion sein wird. Die Syntax lautet

TABLEUPDATE([nRows [, lForce]] [, cTableAlias | nWorkArea][, cErrorArray])

Bei genauerer Betrachtung dieses Wusts aus eckigen Klammern stellt sich heraus, daß Alle 4 Parameter optional sind. Sehen wir und diese Parameter genauer an, angefangen mit nRows. Der folgende, kursive Text ist der VFP-Online-Hilfe entnommen (ich konnte nicht widerstehen, gelegentlich etwas dabei zu lästern):

nRows

Legt fest, welche an der Tabelle oder dem Cursor vorgenommenen Änderungen übergeben werden sollen. Wenn nRows gleich 0 (oder .F.) ist und Zeilen- oder Tabellenpufferung aktiv ist, werden nur Änderungen übertragen, die am aktuellen Datensatz der Tabelle oder des Cursors vorgenommen wurden.

    (... und wenn Zeilen- oder Tabellenpufferung nicht aktiv ist, mit anderen Worten wenn überhaupt nicht gepuffert wird, dann kann man diese Funktion garnicht erst aufrufen. Anm. von mir.)

Wenn nRows gleich 1 und Tabellenpufferung aktiv ist, werden alle an den Datensätzen der Tabelle oder des Cursors vorgenommenen Änderungen übergeben. Wenn nRows gleich 1 (oder .T.) ist und Zeilenpufferung aktiv ist, werden nur Änderungen übertragen, die am aktuellen Datensatz der Tabelle oder des Cursors vorgenommen wurden.

    (... Kunststück. Wenn Zeilenpufferung aktiv ist, kann man sowieso nicht mehr als einen zurückschreiben, weil dieser beim Verlassen ohnehinschon zurückgeschrieben wird. Anm. von mir.)

Wenn nRows gleich 2 ist, werden Änderungen der Tabelle oder des Cursors in der gleichen Weise übergeben, wie wenn nRows gleich 1 (oder .T.) ist. Es tritt jedoch kein Fehler auf, wenn eine Änderung nicht übergeben werden kann, und Visual FoxPro verarbeitet weiter alle verbleibenden Datensätze in der Tabelle oder dem Cursor. Bei der Verwendung von cErrorArray wird ein Datenfeld mit Fehlerinformationen erzeugt, sobald ein Fehler auftritt.

    (... die "Fehlerinformationen" sind bloß Satznummern, keine Bange. Anm. von mir.)

Der Standardwert für nRows ist 0.

Also, ich fasse mal in zusammen, was da steht:

  • Zunächsteinmal der Fall, daß kein Buffermode aktiviert wurde. In dem Fall bringt der Aufruf von TableUpdate( ) oder TableRevert( ) nichts weiter als Fehler 1586, der sinngemäß sagt: Kein Buffer - keine Kekse.
  • Im Fall eines egal wie mistischen RowBuffering werden eventuelle Änderungen sofort beim Verlassen eines Satzes kommentarlos und vollautomatisch zurückgeschrieben. Es ist also nicht möglich, mehr als den aktuellen Satz per RowBuffering in Arbeit zu haben und damit ist der Wert von nRows erschreckend bedeutungslos.
  • Bleibt noch Tablebuffering. Warum man einen Parameter, der 3 verschiedene Werte annehmen kann, auch als logischen Wert zuläßt, ist noch nicht zu mir durchgedrungen. Deshalb betrachte ich mal ausschließlich die numerischen Werte.

nRows

Bedeutung bei TableBuffering

0

 

1

 

2

Nur der aktuelle Datensatz wird zurückgeschrieben. Falls dabei ein Problem auftritt, gibt es - angeblich - eine entsprechende Fehlermeldung. Betrifft auch RowBuffering.

Alle geänderten Zeilen werden zurückgeschrieben. Auch hier - angeblich - bedarfsweise eine Fehlermeldung

Alle geänderten Zeilen werden zurückgeschrieben, nur diesmal kann passieren was will, es gibt keine Meldung.

Ich habe mir eine Weile den Kopf zerbrochen, warum die Möglichkeit ausgespart wurde, nur den aktuellen Satz ohne Fehlermeldungen zu speichern. Vermutlich wußte man bei MS nicht, wie man 4 verschiedene Werte mit .T. und .F. abbildet. Das zweite, was mich etwas verwundert hat, war die, von mir bewußt mit "angeblich" kommentierte Fehlermeldung. Sollte bei beliebigem nRows etwas schiefgehen, könnte einer der folgenden Fehler auftauchen, und bei nRows != 2 kommt er - angeblich auch:

  • 1484: Warnung: Die nächsten veränderten Datensätze sind bereits auf dem Server verändert worden
  • 1585: Aktualisierungskonflikt.
  • 1595: Aktualisierungskonflikt: Einige Ihrer Änderungen in den aktuellen Tabellenzeilen sind durchgeführt worden. Verwenden Sie TABLEUPDATE() mit dem Parameter lErzwingen, um die Aktualisierung durchzuführen, oder die manuelle Transaktion, um die Änderungen zurückzunehmen.

Es geht auch ohne lErzwingen, aber egal - Sie ahnen es schon: Keiner dieser Fehler kam. Ich habe mutwillig alle möglichen Katastrophen bei meinen diversen Aktualisierungsversuchen herbeigeführt, aber in keinem Fall einen Fehler erhalten. Hier nun also die aktualisierte Fassung des obigen Kastens:

nRows

Bedeutung bei TableBuffering

0

 

1

 

 

2

Nur der aktuelle Datensatz wird zurückgeschrieben. Falls dabei ein Problem auftritt, gibt TableUpdate() .F. zurück, ansonsten .T.. Betrifft auch RowBuffering.

Alle geänderten Zeilen werden zurückgeschrieben. Auch hier - wenn es schief geht, liefert TableUpdate() .F. zurück, sonst .T. . Wird ein Fehlerarray angegeben (letzter Parameter), enthält dieses ein Feld mit Inhalt -1.

Alle geänderten Zeilen werden zurückgeschrieben, und diesmal gibt es erst recht keine Meldung. Dafür ist der Inhalt des Arrays brauchbar.

 

Fazit: Wenn nicht alle Datensätze zurückgesichert werden konnten, erkennt man diesen Mißstand nur an der Rückgabe. TableUpdate gibt - unabhängig von den Parametern - ein .T. zurück, wenn alles Paletti ist, und analog .F., wenn irgendwas nicht planmäßig verlaufen ist. Reagiert man auf ein .F. nicht, hat man sog. "uncommitted changes", zu Deutsch irgendwas halbfertiges herumliegen. Das merkt man z.B. dann, wenn man gleich nach dem vergeblichen Update-Versuch das Formular schließen will und VFP versucht, die Tabelle ebenfalls zu schließen. Das geht nämlich dann nicht, und jetzt kommt Fehler 1585. Die Logik ist an und für sich völlig korrekt, die Funktionsbeschreibung dagegen ist - naja.

Der zweite Parameter war lForce - auch hier einleitend Microsofts Kundgebungen:

lForce

Legt fest, ob von einem anderen Benutzer im Netzwerk vorgenommene Änderungen an der Tabelle oder dem Cursor überschrieben werden. Wenn lForce gleich Wahr (.T.) ist, werden alle Änderungen an der Tabelle oder dem Cursor, die von einem anderen Netzwerkbenutzer vorgenommen wurden, überschrieben.

Wenn lForce gleich Falsch (.F.) ist, führt Visual FoxPro Änderungen an der Tabelle oder dem Cursor durch, und zwar vom ersten Datensatz bis zum Ende der Tabelle oder des Cursors. Wenn ein Datensatz gefunden wird, der von einem anderen Netzwerkbenutzer geändert wurde, generiert Visual FoxPro einen Fehler.

Wenn Visual FoxPro den Fehler generiert, können Sie den Fehler mit einer ON ERROR-Routine behandeln, und die ON ERROR-Routine kann TABLEUPDATE( ) ausführen, wobei lForce auf Wahr (.T.) gesetzt ist, um Änderungen am Datensatz zu übergeben. Alternativ kann die ON ERROR-Routine für den Fall, daß eine Transaktion ausgeführt wird, den Fehler behandeln und dann ROLLBACK ausführen, um die Tabelle oder den Cursor in den ursprünglichen Zustand zurückzuversetzen.

Der Standardwert für lForce ist Falsch (.F.).

Uff. Ich werde anhand eines Beispiels wieder erläutern, was das heißt. An zwei Arbeitsplätzen wird der gleiche Datensatz geladen, beide Male steht "Meier" drin. Am ersten Arbeitsplatz wird das in "Müller" umgeändert und gespeichert. Am zweiten Arbeitsplatz steht noch immer "Meier" auf dem Schirm und das wird jetzt in "Schmitt" geändert. Das geht durchaus in allen Buffermodi. An Arbeitsplatz 2 wird nun mittels TableUpdate( ) zurückgeschrieben.

Ein lForce = .T. bedeutet dann, daß alle zwischenzeitlich an anderen Arbeitsplätzen vorgenommenen Änderungen kommentarlos übergebügelt werden, man wird also nie erfahren, daß 5 Minuten lang "Müller" drin stand.

lForce = .F. bedeutet sinngemäß: "Sieh mal zu, wie weit Du kommst, und meld Dich, wenn's Probleme gibt."

Der Kommentar zu lForce = .F. im Hilfetext ist nicht ganz vollständig gelungen, da müssen noch ein paar Dinge berücksichtigt werden. Z.B generiert FoxPro nur dann einen Fehler, wenn das eben besprochene nRows nicht 2 ist. Und auch, wenn es die meisten wissen - ROLLBACK funktioniert prima - aber nur dann, wenn vorher an geeigneter Stelle ein BEGIN TRANSACTION ausgeführt wurde. Drittens sichert man normalerweise innerhalb eines Formulars, und da gibt es Error-Events - das ON ERROR ist also auch nicht ganz das Gelbe vom Ei.

cTableAlias bzw nWorkArea

Zu den beiden brauche ich eigentlich nicht viel zu sagen, außer einen gutgemeinten Rat: Mit Arbeitsbereichsnummern zu arbeiten, ist gefährlich, unübersichtlich und war in FoxPro noch nie notwendig. Beide Möglichkeiten sind optional, aber bevor man sie wegläßt, muß sichergestellt sein, daß vor dem TableUpdate( ) die richtige Tabelle selektiert ist. Es ist kaum Arbeit und zahlt sich aus, bei allen Funktionen, die diese Parameter bieten, sie auch zu verwenden, und zwar den Alias-Namen.

Bleibt noch der letzte Parameter:

cErrorArray

Gibt den Namen eines Datenfeldes an, das erzeugt wird, wenn nRows gleich 2 ist und die Änderungen nicht an einen Datensatz übergeben werden können. Das Datenfeld enthält eine einzelne Spalte, die die Nummern der Datensätze enthält, für die Änderungen nicht übergeben werden sollen.

    (... das muß natürlich "nicht übergeben werden können" heißen. Anm. von mir.)

Wenn Sie den Namen eines Datenfeldes angeben, müssen Sie auch entweder ein Tabellen- oder Cursoralias, cTableAlias oder eine Arbeitsbereichszahl nWorkArea angeben.

    (... das stimmt zwar, aber dann sind, wie so oft, die eckigen Klammern in der Syntaxdefinition Quatsch. Anm. von mir.)

Anmerkung Wenn während der Aktualisierung von Datensätzen ein anderer als ein einfacher Übergabefehler auftritt, enthält das erste Element von cErrorArray -1 und Sie können AERROR( ) benutzen, um herauszufinden, warum die Änderungen nicht übergeben werden konnten.

Ich habe in der Hilfe unter "einfache Übergabefehler" nachgesehen und nichts gefunden. Ich weiß, ich bin gehässig.

Mit TableUpdate( ) wären wir durch, zumindest, was die Theorie angeht. Kommen wir damit zu TableRevert().

Die Syntax lautet

TABLEREVERT([lAllRows [, cTableAlias | nWorkArea]])

Der Kenner erkennt's sofort: viel weniger Parameter als bei TableUpdate().

lAllRows

Da es bei dieser Funktion darum geht, Änderungen zu verwerfen, kann nicht viel schiefgehen. Zu Konflikten kann es allenfalls mit dem Arbeitgeber kommen, aber nicht mit den Tabellen im Netzwerk. Deshalb ist hier ein logischer Wert ausreichend, und er besagt nur

    .F. - nur die Änderungen am aktuellen Datensatz oder

    .T. - Änderungen in allen Datensätzen wegschmeißen

Da es bei RowBuffering, wie gesagt, ohnehin nur den aktuellen Datensatz gibt, ist dieser Parameter in dem Fall auch noch völlig egal und VFP überliest ihn einfach. lAllRows betrifft also wieder nur TableBuffering.

Zu Aliasname bzw. Arbeitsbereichsnummer habe ich mich bereits ausgelassen, und damit wäre dieser Befehl schon ausreichend ausführlich betrachtet.

Möglichkeiten der Konfliktbereinigung

Im vorangegangenen Text habe ich bereits angedeutet, daß es durchaus Möglichkeiten gibt, Updatekonflikte zu lösen. Beispiel:

Abb 1.

Diese Abbildung zeigt 3 Datensätze einer Tabelle auf einem Netzwerkserver. Außerdem werden alle 3 Datensätze auf 2 Arbeitsstationen mit optimistischer Datenpufferung in einer Maske angezeigt - sagen wir, es sei ein Grid.

An Arbeitsplatz 2 werden nun in 2 von den 3 Datensätzen das Alter der jeweiligen Person geändert. Nach diesen - mit einem Kreis markierten - Änderungen speichert Arbeitsstation 1 seinen gesamten Tabellenpuffer zurück. Die neuen Werte in der Servertabelle sind mit einem Kästchen versehen. Da auf Arbeitsstation 2 diese Änderungen noch nicht bekannt sind, stehen in den entsprechenden Feldern noch die alten Werte. Nun ändert jemand an Arbeitsstation 2 die Schuhgröße in einem der Datensätze (siehe Abb. 2)

Abb2.

Hier also der Zustand bis zu dem eben beschriebenen Punkt. Die Änderung der Schuhgröße sollte die einzige Änderung auf Arbeitsstation 2 sein, die dementsprechend jetzt sichern möchte. VFP ist so clever und merkt, daß in den beiden ersten Sätzen keine Änderungen vorgenommen wurden, und versucht deshalb nur, den letzten Datensatz zurückzuschreiben.

Was passiert? Würde der letzte Datensatz einfach zurückgeschrieben, würde die Änderung des Alters im Datensatz "Schmitt" mit dem inzwischen veralteten Wert 37 überschrieben. Foxpro merkt das und deshalb liefert

TableUpdate( 2, .F., "tabelle", array )

.F. zurück, da es zu einem Updatekonflikt gekommen ist. Da wir für nRows den Wert 2 gewählt und ein array angegeben haben, steht in diesem Array jetzt die Nummer des einen Satzes, der Probleme gemacht hat. Bevor man jetzt irgendetwas unternimmt, sollte man zunächst versuchen, diesen Satz mittels rlock() zu sperren, weil sonst wieder andere darin herumfummeln und die Konflikte kein Ende mehr nehmen. Jetzt kann man sich mit den Funktionen fcount() und field() durch diesen Datensatz hangeln und für jedes einzelne Feld dessen Lebenslauf durchleuchten. Wie etwas weiter vorne bereits erwähnt, gibt es hierfür die Funktionen OldVal() und CurVal() sowie den aktuellen Feldinhalt aus dem Puffer.

Wenn also OldVal() und CurVal() für ein Feld unterschiedlicheErgebnisse liefern, bedeutet das, daß dieses Feld zwischenzeitlich geändert wurde. Wenn zudem OldVal() und der aktuelle Feldinhalt identisch sind, kann man also ganz einfach das entsprechende Feld im Puffer mit dem jetzt aktuellen Wert überschreiben. Das würde dazu führen, daß der jetzige Inhalt

Schmitt   37   49

zu

Schmitt   36   49

würde. Diesen Satz könnte man jetzt mit etwas mehr Nachdruck, sprich mit lForce = .T. zurückschreiben. Anschließend muß man nur noch die Satzsperre(n) wieder aufheben, und der Konflikt hat sich erledigt, ohne daß irgendjemand auch nur einmal "OK" drücken mußte.

Problematisch wird dieses Verfahren eigentlich nur an zwei Stellen:

  1. Der Satz wurde von jemandem gelöscht. Dann muß geklärt werden, ob man sich dem beugt und seine Änderungen freiwillig Manitu übergibt, oder ob man die Löschung kurzerhand aufhebt und den Satz wiederbelebt.
  2. Ein und dasselbe Feld wurde von 2 oder mehreren Arbeitsstationen "gleichzeitig" geändert. Auch hier muß entschieden werden, wer recht hat.

Buffering und Views

Abschließend noch ein paar Worte zu Ansichten bzw Views. Wie bereits erwähnt kann jede geöffnete Tabelle gepuffert werden. Ein View ist zwar im Prinzip eine Tabelle, aber hier kann das Buffering nicht zugeschaltet werden, sondern es kann nicht abgeschaltet werden. Views werden automatisch immer gepuffert. Erstens. Zweitens hat man auch bei der Auswahl der Puffermodi nicht so viele Möglichkeiten, denn Views werden zudem immer optimistisch gepuffert. Man kann sich also nur noch zwischen Tabellen- und Satzpufferung entscheiden. Dies ist nebenbei ein weiterer Grund, "normale" Tabellen ebenfalls optimistisch zu puffern - man muß nicht ständig umdenken, wenn man außerdem Views verwendet.

Das Verfahren beim Zurückschreiben und/oder Verwerfen ist das gleiche, auch hier kann es logischerweise zu Konflikten kommen. Einziger Unterschied ist, daß ein View immer Arbeitsplatzlokal ist. Wenn man also, wie im vorangegangenen Beispiel, manuell seine Daten abgleicht, kann man auf das RecordLocking verzichten. Anders gesagt ist die gesamte Update-Geschichte mit Views etwas weniger fehleranfällig.

Und noch etwas ist anders: Wenn man mit Views arbeitet, sollte man ausschließlich mit Views arbeiten. Falls es aber aus irgendeinem Grund notwendig wird, daß an Views beteiligte Tabellen auch direkt bearbeitet werden können, sollten diese sinnigerweise auch gepuffert werden. Was passiert jetzt, wenn ein View mittels TableUpdate() zurückgeschrieben wird und eine oder mehrere seiner Basistabellen sind ebenfalls gepuffert? Antwort: es wird in den Puffer der Tabelle geschrieben. Betrachtet man also CurVal() eines der geänderten Felder oder die gesamte Tabelle von einer anderen Arbeitsstation aus, stellt man fest, daß dort noch immer die alten Werte drin stehen. Es braucht also ein zweites TableUpdate(), diesmal auf die Tabelle(n) selbst, damit tatsächlich zurückgeschrieben wird. Aber wie schon gesagt, dieser Fall ist äußerst selten wirklich notwendig, verursacht meist Kopfschmerzen und sollte tunlichst vermieden werden.