Automatische Primärschlüssel in Multiuser-DB

Hallo geschätzte “Base-Gemeinde”! :wink:

In einer Auftragsverwaltung habe ich einen ganz ähnlichen Anwendungsfall wie “Primärschlüssel aus Nummerierung und Jahreszahl” im Base-Handbuch. Allerdings ist die DB eine MariaDB mit mehreren Anwendern. D.h. die INSERT-Transaktion muss mutliuser-tauglich sein (atomar, ACID, critical section). Zwei Ansätze habe ich verfolgt und stoße auf verschiedene Probleme:

  1. Primärschlüsselgenerierung per DB-Trigger: Das erscheint mit die sauberste Lösung aber leider sieht das Base-Formular nach Speichern den neuen Primärschlüsselwert nicht (bzw. wird nicht angezeigt). Base verliert vermutlich den Datensatzkontext, weil es den generierten Primärschlüsselwert nicht mitbekommt?! (Dabei gibt es doch diese schöne RETURNING-Klausel bei INSERT. :wink:)
  2. Primärschlüsselgenerierung per Makro: Grundsätzlich besteht hier die Gefahr, dass zwei Anwender “gleichzeitig” das Makro auslösen und der gleiche Primärschlüsselwert generiert wird. Also sollte die Makro-Ausführung zeitlich sehr nah am INSERT liegen. (Dafür suche ich noch das passende Ereignis/Trigger in Base.) Bei meinen Tests habe ich das entsprechende Makro zunächst manuell aufgerufen. Der generierte Wert wird ähnlich wie im Base-Handbuch “Beispiel_Fortlaufende_Nummer_Jahr_Hsqldb.odb” schön in das Formular eingetragen aber beim Speichern geschieht folgendes:
    SQL-Status: HYC00
    OPreparedStatement::setObjectWithInfo: invalid arguments.
    Was ist da denn los? Bug?

Hat jemand Ideen/Vorschläge/Anregungen?

Zeige doch einmal die Lösung, wie Du sie per DB-Trigger versucht hast. Das ist allemal besser als über Makro eine Konstruktion zu erstellen.

Ich habe so etwas bei der Serverdatenbank für XRechnungen anders gelöst:
Der Primärschlüssel wird ganz normal über ein Increment gesteuert.
Sobald eine neue Rechnung das erste Mal als Writer-Datei erstellt wird, wird die Rechnungsnummer festgeschrieben. Der Zähler für das Jahr wird als gesondertes Feld in der tbl_Rechnung aufgeführt. Das Löschen des Datensatzes wird anschließend unterbunden, so dass nicht bei einem Löschen die gleiche Rechnungsnummer noch einmal vergeben werden kann. Das Löschen unterbinde ich, indem ich die “tbl_Rechnung”.“ID” in eine separate Tabelle schreibe, die die ID als Fremdschlüssel enthält und in der Verbindung kein Update und kein Löschen erlaubt.

CREATE DEFINER=`xxxxxx`@`%` TRIGGER `aufträge_before_insert` 
BEFORE INSERT ON `aufträge` 
FOR EACH ROW BEGIN
	IF NEW.Auftragsnummer IS NULL THEN 
		SET NEW.Auftragsnummer = (
			SELECT MAX(Auftragsnummer) + 1
			FROM aufträge
			WHERE SUBSTR(Auftragsnummer, 5, 1) = NEW.Kategorie);
	END IF;
	IF NEW.Bearbeiter IS NULL THEN 
		SET NEW.Bearbeiter = 
			(SELECT SUBSTRING( SESSION_USER(), 1, LOCATE('@', SESSION_USER())-1));
	END IF;
END

Die Jahreszahl ist noch nicht drin aber dafür eine Auftragskategorie. Mir gefällt das auch nicht aber so ist der Bestand. Ich hätte dafür ebenfalls eigene Spalten eingerichtet. Primärschlüssel sollten keine Information enthalten!
Zusätzlich wird noch der Loginname in eine weitere Spalte gesetzt.
(Die ganze Anwendung möchte ich nicht hier hochladen aber ich kann bei Gelegenheit gern eine odb erstellen, in der das Problem isoliert wird.)

Kannst Du noch einmal genauer beschreiben, was da bei Base passiert?
Du erstellst einen enuen Datensatz, der Schlüsselwert wird aber über den Trigger srstellt. Siehst Du jetzt im Formular gar nichts mehr oder nur den Schlüsselwert nicht?

Wir hatten bei einem Versuch mit dem Firebird-Server ähnliche Probleme in Unterformularen. Die habe ich mit folgendem Makro “bekämpft”:

SUB FormAutoValueNachher(oEvent AS OBJECT)
  IF oEvent.Action = 1 THEN
	oForm = oEvent.Source
	loRow = oForm.getRow
	oForm.Reload
	IF loRow = 0 THEN
		oForm.MoveToInsertRow
	ELSE
		oForm.Absolute(loRow)
	END IF
  END IF
END SUB

Das wird an das Formularereignis “Nach der Datensatzaktion” eingefügt. Beim Insert (Action = 1) wird die aktuelle Zeile im Formular ausgelesen, das Formular neu geladen und die entsprechende Zeile wieder angesteuert.

Nach dem Speichern erscheint “0” in Auftragsnummer und “NULL” in anderen Feldern.

Zum Testen und Experimentieren:
Automatisch_Primärschlüssel_in_MariaDB.odb (11.7 KB)
(Datenbankverbindung an eigenen Server anpassen.)

SQL zum Erstellen von Tabelle, Trigger und ein paar Daten:
Automatisch_Primärschlüssel_in_MariaDB.zip (803 Bytes)

Das vorgeschlagene Makro werde ich noch ausprobieren.
Vielen Dank soweit!

Speicher doch einfach den Benutzernamen per Default in einer separaten Spalte. Warum sollte eine Auftragsnr. vom Bearbeiter abhängen?

Ich habe das einmal installiert und ausprobiert:
Die automatisch erstellten Felder werden nicht neu eingelesen, wie das bei einem Autoincrementwert der Fall ist. Das obige Makro hilft auch nicht direkt, weil da die entsprechende Eigenschaft nicht vorhanden ist. Die folgende Konstruktion hilt - in der Hoffnung, dass der neu erstellte Datensatz beim neuen Einlesen der letzte Datensatz ist:

GLOBAL inAction AS INTEGER

REM Formular → Ereignis → Vor der Datensatzaktion
SUB Action(oEvent AS OBJECT)
	inAction = oEvent.Action
END SUB
	
REM Formular → Ereignis → Nach der Datensatzaktion
SUB FormAutoValueNachher(oEvent AS OBJECT)
	IF inAction = 1 THEN
		oForm.MoveToLastRow
	END IF
	inAction = 0
END SUB

Du musst also 2 Prozeduren einbinden. Grundsätzlich müsste dann eigentlich nur das in den Formularen angezeit werden, was dem aktuelle SESSION_USER() zugänglich ist. Das kannst Du am besten darüber erledigen, dass Du den SESSION_USER über eine Ansicht abfragst:

SELECT SUBSTRING( SESSION_USER(), 1, LOCATE('@', SESSION_USER())-1) AS `Bearbeiter`

Die Ansicht gibt dann den Bearbeiter aus Deiner Tabelle aus, der zu dem aktuell angemeldeten Bearbeiter passt. So eine Ansicht läuft nur in direktem SQL.

Jetzt erstellst Du eine Abfrage

SELECT `testaufträge`.* FROM `testaufträge` 
WHERE `Bearbeiter` = (SELECT `Bearbeiter` FROM `viw_Bearbeiter`)

wobei die Ansicht als “viw_Bearbeiter” abgespeichert wurde. Achtung, funktioniert nur, wenn die Standardeinstellungen zur Collation halbwegs zu denen des Felds Bearbeiter passen. Bei dem Beispiel, das Du liefertest, wurden bei mir die Felder mit latin1… codiert, und das lässt sich nicht mit dem Standard des Servers (utf8…) vergleichen.

Die oben genannte Abfrage wäre dann die Datenquelle für das Formular. Die Abfrage kann zur Eingabe neuer Datensätze genutzt werden.

Ohne die Abfrage kann es passieren, dass durch das Makro nicht der gerade eingegebene Datensatz nach dem Reload angezeigt wird, weil eine weitere Person zwischendurch auch eine Abspeicherung gemacht hat und die dann die letzte in dem Formular sein würde.

1 Like

Kann man nicht einfach:

ALTER TABLE `testaufträge` ADD COLUMN `Bearbeiter` VARCHAR(20) DEFAULT SESSION_USER()

Der Bearbeiter steht ja schon drin. Und den SESSION_USER() bekommst Du bei der Abfrage-GUI nur über direkte SQL-Ausführung. Da das ja bai jedem Client danach abgefragt werden müsste so der Klimmzug mit dem entsprechenden View.

So isses! Bearbeiter und der neue Primärschlüsselwert werden ja per Trigger automatisch in der Tabelle gespeichert, nur bekommt das Formular in der Base-Anwendung davon nichts mit.
Ich konnte es mir noch einfacher machen mit einem reload-Makro, weil die Spalte “Auftragsdatum” mit CURRENT_TIMESTAMP automatisch besetzt wird und ich das Formular einfach nach “Auftragsdatum” absteigend sortiere und nur ein sehr kurzes Makro benötige, um den jüngsten Datensatz zu holen (Formularereignis: “Nach der Datensatzaktion”).

SUB FormAutoValueNachher(oEvent AS OBJECT)
	oForm = oEvent.Source
	oForm.Reload
END SUB

Jetzt muss ich nur noch verhindern, dass das Makro bei UPDATE oder DELETE ausgeführt wird. Wie erkenne ich den INSERT-Fall?
Nachtrag: Ich sehe im Base-Handbuch (V2408) auf Seite 123 einen ganz ähnlich gelagerten Fall, wo auch nur der INSERT-Fall behandelt wird (Bugs und Workarounds bei verschiedenen Datenbankverbindungen / PostgreSQL).
Das scheint mir genau die gesuchte Fallunterscheidung zu sein “oForm.getInt(1) = 0” aber was hat es mit der unmittelbar vorausgehenden Abfrage “hasUnoInterfaces(oForm, “com.sun.star.form.XForm” )” auf sich?