#Gelöscht in Datenfeldern

28. Dezember 2015

Als Datenbankentwickler mit Access ist man immer wieder mit der Situation konfrontiert, dass Access in den Datenzeilen statt der erwarteten Tabelleninhalte #Gelöscht anzeigt oder dass nach dem Speichern von neuen Datensätzen falsche Daten angezeigt werden. Das sieht dann zum Beispiel so aus (Bild zeigt die englische Variante):

Statt der Daten wird #Gelöscht angezeigt

In den meisten Fällen, in denen Access statt der gewünschten Daten ein #Gelöscht anzeigt, gibt es eine einfache Ursache: Access speichert das Ergebnis einer Datenbankabfrage meist in sogenannten Dynasets. Das bedeutet, dass nicht die kompletten Datenbankinhalte des entsprechenden Recordsets sondern nur die entsprechenden PrimaryKeys gespeichert werden, was natürlich viel weniger Speicherplatz und Zeit bei der Datengenerierung benötigt. Erst wenn man sich die entsprechende Daten zum Beispiel durch Herunterscrollen wirklich anschauen möchte, werden die zu einem PrimaryKey gehörenden Daten aus der Datenbank ausgelesen.

Das funktioniert jedoch nur dann gut, wenn für alle Tabellen "vernünftige" PrimaryKeys definiert sind. Vernünftig bedeutet in dem Zusammenhang, dass für jede Tabelle eine Autowert-Spalte als Zähler mit der PrimaryKey-Eigenschaft eingerichtet wird. Alle anderen PrimaryKeys wie zum Beispiel Fließkommazahlen oder Strings sind eigentlich nicht geeignet. Weitere eindeutige Schlüssel, sogenannte CandidateKeys, können unabhängig davon noch zusätzlich definiert werden. Der für die Autowert-Spalte benötigte zusätzliche Speicherplatz spielt auf heutigen Computersystemen denke ich keine Rolle mehr.

Mit der oben beschriebenen Regel sind die allermeisten Probleme in Bezug auf die #Gelöscht-Datensätze behoben. Es gibt jedoch insbesondere im Zusammenspiel mit SQL-Servern ganz hartnäckige Fälle, in denen nach dem Speichern eines Datensatzes entweder #Gelöscht oder auch ein komplett anderer Datensatz angezeigt wird. Dies zeigt der untenstehende Screenshot:

Ein neu angelegter Datensatz wird als gelöscht angezeigt

Ich habe schon Stunden erfolglos damit zugebracht, eine Systematik und daraus resultierend eine Umgehung für dieses Problem zu finden. Am Ende habe ich akzeptiert, dass beim Anlegen eines neuen Datensatzes die korrekte ID des neuen Datensatzes aus irgendeinem Grund nicht den Weg zurück zu Access-Applikation findet, sondern Access von einem falschen Wert ausgeht. Da diese Beobachtung eigentlich nur bei Tabellen mit verhältnismäßig vielen Datensätzen gemacht wurde, vermute ich irgendeine Form von Überlauf in der Datenbankschnittstelle.

Ich habe in meiner Not dann etwas eigentlich Unschönes gemacht, das in meinen Applikationen das Problem aber zum Verschwinden gebracht hat: Vor Aktualisierung des Datensatzes bestimme ich von der Applikation aus den nächsten freien Autowert. Dies ist eigentlich die primäre Aufgabe der Datenbank, führt aber, wie oben beschreiben, zu einem Fehler. Da es sein kann, dass genau in demselben Augenblick ein anderer Client ebenfalls den nächsten freien Autowert bestimmen will, kopple ich diesen Vorgang noch mit einer Sperrfunktion, so dass immer nur ein Client zur Zeit einen neuen Autowert bestimmen kann. Ist die Funktion gesperrt, warte ich einen Moment und probiere es dann noch einmal. Da die Anzahl der Benutzer in den Applikationen, in denen dieser Workaround zum Einsatz kommt, nicht zu groß ist, hat es hier noch nie Probleme gegeben. Das ist zwar keine wirklich schöne Programmierung, aber manchmal heiligt der Zweck halt die Mittel.

Jetzt aber noch einmal im Detail: Im Event-Handler des Ereignisses Vor Aktualisiertung weise ich dem Feld mit der ID den selbst ermittelten ID-Wert zu. Etwa so:

FTei!AufTei_ID = AutoWert("Auftraege_Teile", "AufTei_ID")

Die Funktion Autowert selbst sieht so aus:

Function AutoWert(Tabelle As String, Autowertspalte As String, Optional Sperr As Boolean = True) As Long
'
' Nach dem Speichern eines Datensatzes wird dieser noch einmal neu geladen. Manchmal passieren dabei Fehler,
' was sich in #gelöscht-Einträgen oder einfach in einem falsch angezeigten Datensatz mainfestiert. Darum wird der
' Wert des Zählerfeldes vor dem Speichern bestimmt.
'
Dim SName As String ' wird für die Funktion Sperren benötigt
Dim MaxID As Variant
Dim I As Integer
  
  GlobVarSetzen
   
  ' Sperrung für das Erzeugen eines Autowertes erzeugen
  If Sperr Then
    For I = 1 To 10
      If (Sperren("Autowert " & Tabelle, SName)) Then
        Exit For
      End If
     
      ' 2 Sekunden warten
      sSleep 2000
      
      If I = 10 Then
        Err.Raise 12345, "AutoWert()", "Es kann kein neuer Autowert für Tabelle " & Tabelle & " erzeugt werden."
        Exit Function
      End If
    Next I
  End If
  
  MaxID = DMax(Autowertspalte, Tabelle)
  
  If IsNull(MaxID) Then
    AutoWert = 1
  Else
    AutoWert = MaxID + 1
  End If
  
  EntSperren "Autowert " & Tabelle
  
End Function

Die Funktion Sperren() setzt dabei eine Semaphore für den Vorgang "Autowert in Tabelle X setzen" und verhindert so, dass zwei Benutzer gleichzeitig versuchen, einen Autowert für dieselbe Tabelle zu erzeugen. Wir haben dazu in der Datenbank eine Tabelle "Sperrungen". Beim Aufruf von Sperren() wird in dieser Tabelle ein Eintrag für den Autowert-Vorgang generiert - sofern es nicht schon einen Eintrag zu diesem Vorgang gibt, weil nämlich ein anderer Benutzer gerade einen Autowert für die Tabelle erzeugt; in diesem Fall bekommt der Benutzer eine Rückmeldung, dass er es gleich noch einmal versuchen soll. Die Funktion Entsperren() hebt nach Durchführung des Vorgangs die Sperrung wieder auf. 

Die Funktion Sperren() kann natürlich nicht nur für die Erstellung von Autowerten, sondern für alle Vorgänge verwendet werden, die immer nur ein Benutzer zur Zeit ausführen darf. 

Für die Umsetzung gibt es diverse Möglichkeiten. Der untenstehende Code ist deshalb nur als Beispiel zu verstehen.

Function Sperren(Art As String, SName As String) As Boolean
' Setzt Sperrung für aktuellen User

Dim SQL As String
Dim R As New ADODB.Recordset

  SQL = "SELECT * FROM Sperrungen WHERE (Art = '" & Art & "')"
  R.Open SQL, SQLCon, adOpenKeyset, adLockOptimistic
  
  If R.EOF Then
    ' Sperrung kann gesetzt werden
    With R
      .AddNew
      !Art = Art
      !Benutzer = Akt_Login
      !Datum = Now()
      .Update
    End With
    Sperren = True
    
  ElseIf (R!Benutzer = Akt_Login) Then
    ' Sperrung vom selben Benutzer -> darf überschrieben werden
    With R
      !Benutzer = Akt_Login
      !Datum = Now()
      .Update
    End With
    Sperren = True
    
  ElseIf DateDiff("d", R!Datum, Date) > 1 Then
    ' Sperrung ist älter als 1 Tag und darf überschrieben werden
    With R
      !Benutzer = Akt_Login
      !Datum = Now()
      .Update
    End With
    Sperren = True

  Else
    ' Sperrung nicht möglich; SName für qualifizierte Rückmeldung an Benutzer
    SName = R!Benutzer
    Sperren = False
  End If
  
  CloseObj R
  Exit Function

End Function