InterBase: Pessimistisches Locking

Posted by on in Programming

Hin und wieder fragen Kunden danach, wie man ein pessimistisches Locking mit InterBase erreichen kann. Es finden sich zwar Hinweise, diese sind aber sehr nebulös oder wenig konkret in Verbindung mit FireDAC (hier, hier und hier). InterBase unterstützt ein "SELECT ... FOR UPDATE" nicht, so daß man etwas programmieren muss.

Warum pessimistisches Locking?

Ich möchte jetzt hier nicht über die Vor- und Nachteile(!) des pessimistischen Lockings philosophieren... Nur soviel: Manchmal möchte man das einfach (obschon ich es nicht empfehlen kann). InterBase hat da aber einen entscheidenden Vorteil: Es wird auch tatsächlich nur der Datensatz und nicht etwa ein Page (die mehrere Datensätze beinhalten kann!) gelockt. Ja. InterBase unterstützt locking. Da irren sich einige (hier, hier; aber diese Einträge sind auch schon etwas älter...)

Es gibt auch Fremdherstellerkomponenten, die eine (published) Property für Datasets besitzen (erscheint im Objektinspektor in Delphi), die einem gleich das pessimistische Locking einschalten lassen können: IBObjects kann das (Doc)... aber das funktioniert natürlich auch mit FireDAC.

Zuerst eine einfachste, simple FireDAC Anwendung mit Delphi:

- FDConnection, FDTable, DataSource (alles korrekt und normal verbunden und aktiviert) und eine Listbox:

Die ListBox soll einfach nur eine kleine Ausgabe machen (Log) und bekommt daher eine kleine Methode:

interface
Type TForm1..
procedure log(aString : String);
......

implementation

procedure TForm1.log(aString: String); begin ListBox1.Items.Add(DateTimeToStr(Now())+': '+aString); end;

Jetzt sollte man sich darüber einigen, wenn ein Datensatz gelockt werden soll. Am besten dann, wenn die Bearbeitung beginnt (also noch nicht, wenn nur "geBROWSEd" wird. Dafür eignet sich das "BeforeEdit" des DataSets.

Hier muss man nun den Datensatz sperren. Dazu folgendes Kochrezept:

  • Transaktion öffnen
  • Datensatz aktualisieren (UPDATE) durch das Lesen/Schreiben eines vorhandenen Wertes
  • Transaktion offen lassen!

Also liesst man einen Wert (Achtung: (Primär-) Index erforderlich; um die Zuordnung eindeutig zu gewährleisten) und schreibt diesen wieder. Man könnte den Primärindex selbst nehmen, oder einen anderen Wert. Ich nehme lieber einen anderen Wert:

procedure TForm1.CustomerTableBeforeEdit(DataSet: TDataSet);
var
  s: String;
begin
    log('Before Edit');
  s := 'UPDATE CUSTOMER   ' +
       'SET CUSTOMER.CUST_NO = CUSTOMER.CUST_NO ' +
       'WHERE CUSTOMER.CUST_NO = ' +
       CustomerTable.FieldByName('CUST_NO').AsInteger.ToString;
  log(s);
  EmployeeConnection.Transaction.StartTransaction;
EmployeeConnection.ExecSQL(s) end;

Damit erreicht man schonmal eine (hässliche) Exception, wenn der gleiche Datensatz geändert wird (No-Wait, die Standardeinstellung: Es wird nicht auf das Entsperren gewartet). Hier zweimal die Anwendung parallel gestartet:

Wenn man das noch in einen Try Except Block einbaut

procedure TForm1.CustomerTableBeforeEdit(DataSet: TDataSet);
var
  s: string;
begin
  log('Before Edit');
  s := 'UPDATE CUSTOMER   ' +
       'SET CUSTOMER.CUST_NO = CUSTOMER.CUST_NO ' +
       'WHERE CUSTOMER.CUST_NO = ' +
       CustomerTable.FieldByName('CUST_NO').AsInteger.ToString;
  log(s);
  EmployeeConnection.Transaction.StartTransaction;

  try
    EmployeeConnection.ExecSQL(s)
  except
    MessageDlg('Ein anderer Benutzer hat hier seine Finger im Spiel', mtWarning,
      [mbOk], 0);
    CustomerTable.Cancel;
    CustomerTable.Refresh;
    EmployeeConnection.Transaction.Rollback;
  end;
end;

Zum Schluss noch die Transaktion schliessen (im AfterEdit des DataSets)

procedure TForm1.CustomerTableAfterEdit(DataSet: TDataSet);
begin
  EmployeeConnection.Commit;
end;


Comments

Check out more tips and tricks in this development video: