GraphQL Entwickler Dokumentation - Mutationen (Mutations)
Sie befinden sich auf der Seite: Grundlagen der Mutations und ihr Verhältnis zu Queries.
-
Diese Seite knüpft direkt an die GraphQL Entwickler Doku (1) und GraphQL Entwickler-Doku (2) an und beschreibt als dritter Teil der Dokumentation die in GraphQL nutzbaren "Mutations".
-
Beispiele zu GraphQL-Abfragen finden auf der Seite: GraphQL Beispiel Query's
11. Grundlagen der Mutationen
11.1 Einführung in GraphQL-Mutationen
In der GraphQL-Spezifikation existieren zwei primäre Operationstypen: query
für Leseoperationen und mutation
für Datenmanipulation. Die microtech GraphQL-Implementierung folgt diesem Standard, erweitert ihn jedoch um eine besondere Eigenschaft: Mutationen sind hier als vollständiges Superset von Abfragen realisiert.
Dies bedeutet konkret: Jede gültige query
-Operation kann durch einfaches Ersetzen des Schlüsselworts in eine mutation
-Operation umgewandelt werden. Der fundamentale Unterschied liegt nicht in der verfügbaren Syntax, sondern im zugrundeliegenden Transaktionsmodell und den zusätzlich verfügbaren Schreiboperationen.
# Diese Abfrage...
query {
tblProducts {
rowsRead {
fldArtNr
fldSuchBeg
}
}
}
# ...funktioniert identisch als Mutation
mutation {
tblProducts {
rowsRead {
fldArtNr
fldSuchBeg
}
}
}
11.2 Das Transaktionsmodell im Detail
11.2.1 Snapshot-Transaktionen bei Abfragen
query
-Operationen werden in speziellen Snapshot-Transaktionen ausgeführt. Diese bieten eine konsistente Sicht auf die Datenbank zum Zeitpunkt des Transaktionsbeginns, ohne dass Sperren gesetzt werden müssen. Alle Lesezugriffe innerhalb einer Abfrage sehen denselben, unveränderlichen Datenstand - selbst wenn parallel andere Transaktionen Änderungen vornehmen.
Diese Sperrfreiheit ermöglicht höchste Performance bei reinen Leseoperationen und verhindert, dass Abfragen andere Datenbankoperationen blockieren.
11.2.2 Normale Transaktionen bei Mutationen
mutation
-Operationen arbeiten mit regulären Datenbanktransaktionen. Jeder Zugriff auf eine Tabelle setzt automatisch die entsprechenden Sperren:
- Lesezugriffe setzen Lesesperren (Shared Locks) auf Tabellenebene
- Schreibzugriffe setzen Schreibsperren (Exclusive Locks) auf Tabellenebene
Diese Sperren bleiben bis zum Transaktionsende bestehen. Das gewährleistet die Isolation der Transaktion, kann aber zu Konflikten mit anderen parallelen Transaktionen führen. Die detaillierte Behandlung von Sperren und deren Optimierung erfolgt in Kapitel 15.
11.2.3 Atomizität als Grundprinzip
Eine Mutation wird nur dann festgeschrieben, wenn sie vollständig und fehlerfrei durchläuft. Jeder Laufzeitfehler führt zum sofortigen Abbruch und automatischen Zurückrollen aller bereits durchgeführten Änderungen. Dieses "Alles-oder-Nichts"-Prinzip gewährleistet, dass niemals inkonsistente Zwischenzustände in der Datenbank entstehen können.
11.3 Unterschiede in der Fehlerbehandlung
Die Fehlerbehandlung unterscheidet sich fundamental zwischen Abfragen und Mutationen:
Bei Abfragen führen Laufzeitfehler lediglich dazu, dass das betroffene Feld null
zurückgibt. Die Ausführung der übrigen Felder wird fortgesetzt. Dies ermöglicht partielle Ergebnisse auch bei einzelnen fehlerhaften Feldern.
Bei Mutationen werden Laufzeitfehler standardmäßig nach außen propagiert. Sie führen zum Abbruch der gesamten Operation und zum Zurückrollen der Transaktion. Dies stellt sicher, dass eine Mutation entweder vollständig erfolgreich ist oder gar keine Änderungen hinterlässt.
11.4 Erweiterte Funktionalität in Mutationen
11.4.1 Schema-Erweiterungen
In mutation
-Operationen stehen erweiterte Objekttypen zur Verfügung. Eine Tabelle, die in Abfragen den Typ TableNameTableQueryRead
zurückgibt, liefert in Mutationen TableNameTableMutationRead
. Diese erweiterten Typen enthalten alle Felder des TableQueryRead
-Objektes plus zusätzliche Schreiboperationen.
11.4.2 Verfügbare Schreiboperationen
Zusätzlich zu den aus Abfragen bekannten Leseoperationen stehen in Mutationen folgende Schreiboperationen zur Verfügung:
Operation | Zweck | Rückgabetyp |
---|---|---|
rowNew |
Neuen Datensatz erstellen | RowMutationNew |
rowCopy |
Datensatz kopieren | RowMutationCopy |
rowModify |
Datensatz ändern | RowMutationModify |
rowDelete |
Datensatz löschen | RowMutationDelete |
rowsCopy |
Mehrere Datensätze kopieren | [RowMutationCopy] |
rowsModify |
Mehrere Datensätze ändern | [RowMutationModify] |
rowsDelete |
Mehrere Datensätze löschen | [RowMutationDelete] |
11.4.3 Row-Kontexte in Mutationen
Die verschiedenen RowMutation
-Typen repräsentieren unterschiedliche Row-Kontexte eines Datensatzes:
RowMutationNew
: Ein neu angelegter Datensatz vor dem ersten SpeichernRowMutationCopy
: Eine Kopie eines bestehenden Datensatzes vor dem SpeichernRowMutationModify
: Ein zur Bearbeitung geöffneter bestehender DatensatzRowMutationDelete
: Ein zur Löschung markierter Datensatz (nur Lesezugriff)RowMutationRead
: Ein Datensatz im erweiterten Lesemodus mit Zugriff auf Mutation-spezifische Funktionen
Von diesen fünf Row-Kontexten sind nur drei schreibende Row-Kontexte: RowMutationNew
, RowMutationCopy
und RowMutationModify
. In diesen Kontexten können Feldwerte gesetzt werden.
11.5 Praktische Implikationen
11.5.1 Wann Abfragen, wann Mutationen?
Die Wahl zwischen Abfrage und Mutation sollte sich nach dem beabsichtigten Zweck richten:
- Abfragen für alle reinen Leseoperationen, bei denen keine Datenänderungen erfolgen
- Mutationen für alle Operationen, die Daten erstellen, ändern oder löschen
Auch wenn technisch eine Mutation für reine Leseoperationen verwendet werden könnte, ist dies nicht empfehlenswert, da unnötige Sperren die Performance aller mit dem Datenserver verbundenen Anwendungen und Benutzer beeinträchtigen können. Dies betrifft interaktive Benutzer der microtech Software, andere GraphQL-Anfragen, COM-Clients, Automatisierungsdienste und alle weiteren Anwendungen, die auf denselben Datenserver zugreifen.
11.5.2 Transaktionsplanung
Bei der Entwicklung von Mutationen sollte die transaktionale Natur berücksichtigt werden. Alle logisch zusammengehörigen Änderungen sollten in einer einzigen Mutation erfolgen, um Konsistenz zu gewährleisten. Gleichzeitig sollten Mutationen so kurz wie möglich gehalten werden, um Sperrenkonflikte zu minimieren.
12. Datensatzoperationen
Dieses Kapitel behandelt die fundamentalen Operationen zum Erstellen, Kopieren, Ändern und Löschen von Datensätzen. Diese Operationen bilden das Kernstück der Mutation-Funktionalität und sind essentiell für jede Datenmanipulation.
12.1 Das Row-Zustandsmodell
Jede Datensatzoperation gibt ein Row
-Objekt eines zustandspezifischen Typs zurück, das die verfügbaren Operationen definiert:
- Schreibende Row-Objekte (
RowMutationNew
,RowMutationCopy
,RowMutationModify
): Erlauben das Setzen von Feldwerten - Nur-Lese-Row-Objekte (
RowMutationDelete
,RowMutationRead
): Erlauben nur Lesezugriff auf Felder
Wichtige Unterscheidung: Der Typ des Row
-Objekts und der tatsächliche Zustand des dahinterliegenden Datensatzes können divergieren. Beispiel: Nach einem rowSave
innerhalb eines RowMutationModify
-Objekts ist der Datensatz bereits gespeichert und kann nur noch gelesen werden (Zustand: Read
), aber das Row
-Objekt bleibt vom Typ RowMutationModify
. Weitere Schreibversuche im ursprünglichen Kontext führen dann zu Laufzeitfehlern, da der Datensatz-Zustand nicht mehr zum Kontext passt (siehe Kapitel 12.6 für Details).
12.2 Erstellen neuer Datensätze mit rowNew
Die rowNew
-Operation erstellt einen neuen Datensatz und versetzt ihn in den RowMutationNew
-Kontext.
12.2.1 Grundlegende Verwendung
mutation {
tblProducts {
rowNew {
# Datenfelder setzen
fldArtNr(set: { string: "PROD-NEW-001" } )
fldBez1(set: { text: "Neuer Artikel" } as: DISPLAY_TEXT )
fldVk0_Preis(set: { float: 99.95 } )
} # Werte werden am Ende automatisch gespeichert
}
}
Beim Erstellen neuer Datensätze können automatisch Feldwerte zugewiesen werden. Diese automatischen Zuweisungen erfolgen vor der Ausführung der expliziten Feldzugriffe und sind tabellenspezifisch.
12.2.2 Bedingte Erstellung mit ifNotExists
Der optionale ifNotExists
-Parameter verhindert die Erstellung eines Datensatzes, wenn bereits ein Datensatz mit den angegebenen Kriterien existiert:
mutation {
tblProducts {
rowNew(
ifNotExists: {
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-NEW-001" } } }
}
) {
# Datenfelder setzen
fldArtNr(set: { string: "PROD-NEW-001" } )
fldBez1(set: { text: "Neuer Artikel" } as: DISPLAY_TEXT )
fldVk0_Preis(set: { float: 99.95 } )
} # Neuer Datensatz wird am Ende automatisch gespeichert
}
}
Bei bereits vorhandenem Datensatz gibt rowNew
in diesem Fall null
zurück, anstatt einen Fehler auszulösen.
12.3 Kopieren bestehender Datensätze mit rowCopy
Die rowCopy
-Operation erstellt eine Kopie eines existierenden Datensatzes und versetzt die Kopie in den RowMutationCopy
-Kontext.
mutation {
tblProducts {
rowCopy(
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-NEW-001" } } }
) {
# Neue Artikelnummer für die Kopie vergeben
fldArtNr(set: { string: "PROD-NEW-001-COPY" })
# Andere Felder können beibehalten oder geändert werden
fldBez1(set: { text: "Kopie von Original" } as: DISPLAY_TEXT )
# Nicht explizit gesetzte Felder werden vom Original übernommen
fldVk0_Preis # Behält den Wert des Originals
} # Neuer (kopierter) Datensatz wird am Ende automatisch gespeichert
}
}
Welche Felder beim Kopieren übernommen werden und welche neu gesetzt werden müssen (wie eindeutige Schlüssel), ist tabellenspezifisch. Wird der Quelldatensatz nicht gefunden, gibt rowCopy
null
zurück.
Wie bei rowRead
kann auch bei rowCopy
der optionale modifyLSN
-Parameter verwendet werden (siehe Teil 1, Kapitel 3.5), um sicherzustellen, dass der zu kopierende Datensatz eine bestimmte Version hat.
12.4 Ändern bestehender Datensätze mit rowModify
Die rowModify
-Operation öffnet einen bestehenden Datensatz zur Bearbeitung im RowMutationModify
-Kontext:
mutation {
tblProducts {
rowModify(
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-NEW-001" } } }
) {
# Felder ändern
fldHistKz(set: { boolean: true } )
fldVk0_Rab0_Sz(set: { float: 15.0 } as: DISPLAY_TEXT )
# Unveränderte Felder können weiterhin gelesen werden
fldBez1(as: DISPLAY_TEXT )
fldVk0_Preis # Behält den Wert des Originals
} # Änderungen am Datensatz werden am Ende automatisch gespeichert
}
}
Der zu ändernde Datensatz wird über dieselben Parameter identifiziert wie bei Leseoperationen. Nicht gefundene Datensätze führen zu einem null
-Rückgabewert.
Der optionale modifyLSN
-Parameter (siehe Teil 1, Kapitel 3.5) kann auch bei rowModify
verwendet werden, um Optimistic Locking zu implementieren. Ein praktisches Beispiel dazu findet sich in Abschnitt 12.6.5.
12.5 Löschen von Datensätzen mit rowDelete
Die rowDelete
-Operation markiert einen Datensatz zur Löschung. Der Datensatz befindet sich im RowMutationDelete
-Kontext, der nur Lesezugriffe erlaubt:
mutation {
tblProducts {
copy: rowDelete(
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-NEW-001-COPY" } } }
) {
# Vor der Löschung können Felder noch gelesen werden
fldArtNr
fldBez1(as: DISPLAY_TEXT)
# Schreibzugriffe sind nicht möglich - dies ist kein schreibender Row-Kontext
} # Nach diesem Block ist der Datensatz gelöscht
new: rowDelete(
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-NEW-001" } } }
) {
# GraphQL erfordert, dass mindestens ein Feld zurückgegeben wird
fldArtNr
} # Nach diesem Block ist der Datensatz gelöscht
}
}
Die eigentliche Löschung erfolgt am Ende des rowDelete
-Blocks. Das Verhalten bei abhängigen Datensätzen ist tabellenspezifisch. Je nach interner Konfiguration werden abhängige Datensätze automatisch mit gelöscht, ihre Referenzen auf null gesetzt oder die Löschung wird verhindert.
Wie bei allen Operationen mit exactMatch
unterstützt auch rowDelete
den optionalen modifyLSN
-Parameter (siehe Teil 1, Kapitel 3.5), um sicherzustellen, dass nur eine bestimmte Version des Datensatzes gelöscht wird.
12.6 Speicheroperationen
12.6.1 Implizite und explizite Speicherung
Standardmäßig erfolgt die Speicherung implizit am Ende eines Row-Blocks. Für spezielle Anforderungen stehen explizite Speicheroperationen zur Verfügung:
rowSave
: Speichert den Datensatz und wechselt in denRowMutationRead
-KontextrowSaveAndModify
: Speichert den Datensatz und wechselt/verbleibt imRowMutationModify
-Kontext
12.6.2 Zugriff auf generierte Werte
Der Hauptgrund für explizite Speicheroperationen ist der Zugriff auf automatisch generierte Werte:
mutation {
tblProducts {
rowNew {
# Datenfelder setzen
fldArtNr(set: { string: "PROD-NEW-001" } )
fldBez1(set: { text: "Neuer Artikel" } as: DISPLAY_TEXT )
fldVk0_Preis(set: { float: 99.95 } )
# Explizit speichern
rowSave {
# Jetzt ist der Datensatz schon gespeichert und generierte Werte sind verfügbar
fldModifyLSN # Versionsnummer
fldID # Automatisch generierte ID
}
} # Da der Datensatz schon gespeichert ist, passiert hier nichts mehr
}
}
12.6.3 Zustandsübergänge bei Speicheroperationen
Die Speicheroperationen führen zu definierten Zustandsübergängen:
Nach rowSave
:
- Aus allen schreibenden Row-Kontexten → RowMutationRead
- Keine weiteren Schreibzugriffe möglich
- Zugriff auf verlinkte Tabellen möglich
Nach rowSaveAndModify
:
- Aus RowMutationNew
oder RowMutationCopy
→ RowMutationModify
- Aus RowMutationModify
→ verbleibt in RowMutationModify
- Weitere Schreibzugriffe möglich
- Kein Zugriff auf verlinkte Tabellen (lnk...
)
12.6.4 Wichtige Einschränkung
Nach dem Aufruf von rowSave
oder rowSaveAndModify
führen weitere Schreiboperationen im ursprünglichen Row-Kontext zu einem Laufzeitfehler:
mutation {
tblProducts {
rowNew {
# Datenfelder setzen
fldArtNr(set: { string: "PROD-003" } )
# Explizit speichern und weiter modifizieren
rowSaveAndModify {
# Jetzt ist der Datensatz gespeichert, aber weiterhin schreibar
fldBez1(set: { text: "Neuer Artikel" } as: DISPLAY_TEXT )
fldModifyLSN
} # Änderungen am Datensatz werden am Ende automatisch gespeichert
# lesen erlaubt
fldModifyLSN
# SCHREIBEN VERBOTEN: Weitere Schreiboperationen hier schlagen fehl
fldSuchBeg(set: { text: "Test" }) # Laufzeitfehler
}
}
} # Laufzeitfehler rollt alle Änderungen zurück
{
"data": {
"tblProducts": {
"rowNew": {
"fldArtNr": "PROD-003",
"rowSaveAndModify": {
"fldBez1": "Neuer Artikel",
"fldModifyLSN": "9161168561880169771"
},
"fldModifyLSN": "9161168561880169774",
"fldSuchBeg": null
}
}
},
"errors": [
{
"message": "Can't set field on a row that is not in New, Copy, or Modify state",
"locations": [
{
"line": 17,
"column": 7
}
],
...
12.6.5 Optimistic Locking mit modifyLSN
Der modifyLSN
-Parameter (siehe Teil 1, Kapitel 3.5) kann auch bei rowModify
verwendet werden, um Optimistic Locking zu implementieren. Dies ermöglicht die Erkennung von Konflikten bei gleichzeitigen Änderungen:
mutation ModifyAddressMutationOptimisticLocking (
$adrNr: String = "10000"
$modifyLSN: BigInt = 9161168561880151768
$notFound: Boolean! = false
) {
tblAddresses {
modified:rowModify (
kf1AdrNr: { text: $adrNr }
modifyLSN: $modifyLSN
)
@onNull(
returnValue: skip
set:{ var: $notFound }
)
{
fldID
fldAdrNr
fldInsertLSN
fldModifyLSN
fldInfo( set: {text: "Modified"} as: TEXT )
rowSave {
fldModifyLSN
}
}
mismatchedLSN:rowRead (
kf1AdrNr:{ text: $adrNr }
)
@include( if: $notFound )
{
fldID
fldAdrNr
fldInsertLSN
fldModifyLSN
fldInfo(as:TEXT)
}
}
}
{
"data": {
"tblAddresses": {
"mismatchedLSN": {
"fldID": 1501,
"fldAdrNr": "10000",
"fldInsertLSN": "9161168561880064521",
"fldModifyLSN": "9161168561880151777",
"fldInfo": "Unmodified"
}
}
}
}
mutation ModifyAddressMutationOptimisticLocking (
$adrNr: String = "10000"
$modifyLSN: BigInt = 9161168561880151777
$notFound: Boolean! = false
) {
...
}
{
"data": {
"tblAddresses": {
"modified": {
"fldID": 1501,
"fldAdrNr": "10000",
"fldInsertLSN": "9161168561880064521",
"fldModifyLSN": "9161168561880151777",
"fldInfo": "Modified",
"rowSave": {
"fldModifyLSN": "9161168561880169332"
}
}
}
}
}
12.7 Massenoperationen mit Listen-Feldern
Die Listen-Varianten (rowsCopy
, rowsModify
, rowsDelete
) führen die entsprechende Operation für jeden Datensatz der Ergebnismenge aus:
mutation {
tblProducts {
rowsModify(
allBetween: {
byNr: {
kf1ArtNr: {
from: { string: "PROD-001" },
to: { string: "PROD-199" }
}
}
}
) {
# Wert vor Änderungen auslesen
before: fldHistKz
# Änderungen werden auf alle gefundenen Artikel angewendet
fldHistKz(set: { boolean: true })
# Individuelle Werte jedes Datensatzes können abgefragt werden
fldArtNr
}
}
}
Ein Fehler bei der Bearbeitung eines einzelnen Datensatzes führt zum Abbruch der gesamten Operation. Alle Änderungen werden zurückgerollt - eine Teilausführung ist nicht möglich.
13. Feldmanipulation und Parameter
Nach den grundlegenden Datensatzoperationen behandelt dieses Kapitel die detaillierte Manipulation von Feldwerten und die verschiedenen Parameter-Typen, die dabei zur Verfügung stehen.
13.1 Feldmanipulation mit dem set
-Parameter
In schreibenden Row-Kontexten (RowMutationNew
, RowMutationCopy
, RowMutationModify
) können Feldwerte über den set
-Parameter gesetzt werden.
13.1.1 Typspezifische Eingabe
Jeder Feldtyp hat typenspezifische Eingabemöglichkeiten:
mutation {
tblProducts {
rowNew {
# String-Felder
fldArtNrString: fldArtNr(set: { string: "PROD-005" }) # Native Eingabe
fldArtNrText: fldArtNr(set: { text: "PROD-005" }) # Text-Eingabe
# Numerische Felder
fldVk0_PreisFloat: fldVk0_Preis(set: { float: 99.95 }) # Native Eingabe
fldVk0_PreisText: fldVk0_Preis(set: { text: "99,95" }) # Lokalisierte Text-Eingabe
# Datumsfelder
fldGspAbDatLocalDate: fldGspAbDat(set: { localdate: "2023-12-24" }) # ISO-Format
fldGspAbDatText: fldGspAbDat(set: { text: "24.12.2023" }) # Lokalisiertes Format
# NULL-Werte
fldGspAbDatNULL: fldGspAbDat(set: {}) # Setzt Feld auf NULL
}
}
}
Die verfügbaren Möglichkeiten zur Werteangabe sind dem Schema zu entnehmen.
13.1.2 Rückgabewerte nach dem Setzen
Der as
-Parameter bestimmt das Format des zurückgegebenen Wertes nach dem Setzen:
mutation {
tblProducts {
rowModify(exactMatch: { byNr: { kf1ArtNr: { string: "PROD-001" } } }) {
# Wert setzen und formatiert zurückgeben
Text: fldVk0_Preis(set: { float: 129.95 }, as: TEXT) # Gibt "129,95" zurück
Float: fldVk0_Preis(set: { float: 129.95 }, as: FLOAT) # Gibt 129.95 zurück
}
}
}
13.1.3 Verfügbarkeit von set
-Parametern
Nicht alle Felder sind in allen schreibenden Row-Kontexten schreibbar. Die Verfügbarkeit ist:
- Tabellenspezifisch
- Kontextabhängig (kann sich zwischen New
, Copy
und Modify
unterscheiden)
- Im Schema durch das Vorhandensein des set
-Parameters erkennbar
13.2 Row-Assign-Parameter
Manche Tabellen bieten row...
-assign...
-Parameter, die eine automatische Befüllung mehrerer Felder basierend auf Daten aus anderen Datensätzen ermöglichen.
13.2.1 Verwendung bei der Neuanlage
mutation {
tblContacts {
rowNew(assignAddress: { kf1AdrNr: { text: "10000" } }) {
# Felder wurden automatisch aus der Adresse befüllt
fldNr # "10000" - übernommen
fldAnsInfo # Weitere Informationen aus der Adresse
# Zusätzliche Felder können manuell gesetzt werden
fldInfo(set: { text: "Neuer Kontakt" })
}
}
}
13.2.2 Verfügbarkeit und Syntax
row...
-assign...
-Parameter:
- Sind bei rowNew
, rowCopy
und rowModify
verfügbar
- Verwenden dieselbe Identifikationssyntax wie rowRead
- Können verkürzt ohne exactMatch
geschrieben werden
13.2.3 Mehrere Assign-Parameter
Wenn eine Tabelle mehrere row...
-assign...
-Parameter unterstützt, werden diese in der durch das Schema definierten Reihenfolge ausgeführt, nicht in der Reihenfolge ihrer Angabe:
mutation {
tblContacts {
rowNew(
assignUser: { exactMatch: { byAnmNa: { kf1AnmNa: { text: "Supervisor" } } } }, # Reihenfolge der Angabe
assignAddress: { kf1AdrNr: { text: "10000" } } # ist nicht relevant
) {
# Die Ausführung erfolgt in Schema-definierter Reihenfolge:
# assignAddress, assignBank, assignUser, assignAgent
}
}
}
13.2.4 Technische Aspekte
row...
-assign...
-Parameter führen intern einen lesenden Zugriff auf die referenzierte Tabelle aus. Dies hat folgende Auswirkungen:
- Eine Lesesperre wird auf die Zieltabelle gesetzt (falls noch nicht vorhanden)
- Der gefundene Datensatz wird gelesen und die relevanten Werte extrahiert
- Diese Werte werden dann auf den aktuellen Datensatz übertragen
Bei der Verwendung von @acquireLocks
sollten Tabellen, die über row...
-assign...
-Parameter gelesen werden, im forReading
-Parameter berücksichtigt werden.
13.3 Feld-Assign-Parameter
Zusätzlich zu row...
-assign...
-Parametern bieten manche Datenfelder eigene fld...
-assign...
-Parameter als Alternative zum set
-Parameter.
13.3.1 Konzept und Verwendung
fld...
-assign...
-Parameter ermöglichen die automatische Wertzuweisung basierend auf der Suche in einer verknüpften Tabelle:
mutation {
tblProducts {
rowModify(
exactMatch: { byNr: { kf1ArtNr: { string: "PROD-001" } } }
) {
# Statt den genauen Wert zu kennen:
# fldWgrNr(set: { string: "WGR-123" })
# Automatische Zuweisung basierend auf Suche:
fldWgrNr(assignProductGroup: {
exactMatch: { byBez: { kf1Bez: { text: "Elektronik" } } }
})
}
}
}
13.3.2 Eigenschaften
- Exklusivität: Bei einem Feld kann entweder
set
oder einfld...
-assign...
-Parameter verwendet werden, niemals beide gleichzeitig - Verfügbarkeit: Typischerweise bei Feldern, die auf andere Datensätze verweisen (Fremdschlüssel)
- Syntax: Identisch mit der Syntax von
rowRead
für die Datensatzsuche - Lesezugriff: Wie bei
row...
-assign...
-Parametern wird eine Lesesperre auf die referenzierte Tabelle gesetzt
13.3.3 Praktischer Nutzen
fld...
-assign...
-Parameter sind besonders nützlich wenn:
- Der exakte Wert eines Referenzfeldes nicht bekannt ist
- Die Zuweisung basierend auf Geschäftslogik erfolgen soll
- Konsistenz durch automatische Verknüpfung sichergestellt werden soll
13.4 Automatische Feldwertzuweisungen
Neben den expliziten Parametern können automatische Feldwertzuweisungen in verschiedenen Situationen erfolgen:
13.4.1 Zeitpunkte automatischer Zuweisungen
Automatische Zuweisungen können erfolgen:
- Bei rowNew
oder rowCopy
vor der Ausführung der Feldzugriffe
- Bei Verwendung von Row-Assign-Parametern
- Beim Setzen eines Feldwertes (Auswirkungen auf andere Felder)
- Beim Speichern eines Datensatzes
13.4.2 Charakteristika
- Die genauen Regeln sind tabellenspezifisch
- Können Standardwerte, berechnete Werte oder abhängige Aktualisierungen sein
- Sind nicht immer aus dem Schema ersichtlich
- Können durch explizite Wertzuweisungen überschrieben werden, sofern die automatische Zuweisung vor der expliziten Zuweisung erfolgt
13.4.3 Beispielhafte Szenarien
Typische automatische Zuweisungen (tabellenspezifisch): - Generierung von IDs oder laufenden Nummern - Setzen von Zeitstempeln (Erstellungs-/Änderungsdatum) - Berechnung abhängiger Felder (z.B. Gesamtpreise) - Übernahme von Standardwerten aus Stammdaten
13.5 Reihenfolge von Feldzugriffen
Die Reihenfolge von Feldzugriffen kann relevant sein, da Felder aufeinander Bezug nehmen können:
mutation {
tblProducts {
rowNew {
fldArtNr(set: { text: "PROD-001" } )
fldStSchl(set: {text: "2"} )
fldVk0_IklStKz(set: {boolean: true} )
fldVk0_Preis(set: {float: 100} )
fldVk0_PreisNt
fldVk0_PreisBt
fldVk1_IklStKz(set: {boolean: false} )
fldVk1_Preis(set: {float: 100} )
fldVk1_PreisNt
fldVk1_PreisBt
}
}
}
Diese sequenzielle Verarbeitung ermöglicht es, dass: - Automatische Berechnungen auf bereits gesetzte Werte zugreifen können - Validierungen den aktuellen Zustand des Datensatzes berücksichtigen - Abhängige Felder korrekt aktualisiert werden
14. Beziehungen und komplexe Datenstrukturen
Nach den Grundlagen der Datensatz- und Feldoperationen widmet sich dieses Kapitel der Arbeit mit Beziehungen zwischen Datensätzen. Die drei aus Teil 1 bekannten Beziehungstypen zeigen in Mutationen spezifische Verhaltensweisen und erweiterte Möglichkeiten.
14.1 Verfügbarkeit von Beziehungsstrukturen
Die Verfügbarkeit und Funktionalität der verschiedenen Beziehungstypen hängt vom aktuellen Row-Kontext ab:
Beziehungstyp | RowMutationRead |
Schreibende Row-Kontexte (New /Copy /Modify ) |
---|---|---|
Verlinkungen (lnk... ) |
Vollständige Schreiboperationen | Nicht verfügbar |
Verweise (row... ) |
Gibt RowMutationRead -Typen zurück |
Gibt RowQueryRead -Typen zurück |
Verschachtelte Tabellen (tbl... ) |
Nur Leseoperationen | Vollständige Schreiboperationen |
Diese Verfügbarkeitsregeln basieren auf technischen Notwendigkeiten zur Vermeidung von Sperrkonflikten und Gewährleistung der Datenkonsistenz.
14.2 Arbeiten mit Verlinkungen (lnk...
)
14.2.1 Schreiboperationen über Verlinkungen
In RowMutationRead
-Kontexten bieten Verlinkungen erweiterte Möglichkeiten zur Manipulation verknüpfter Datensätze:
mutation {
tblAddresses {
rowRead(exactMatch: { byNr: { kf1AdrNr: { string: "10000" } } }) {
# Im RowMutationRead-Kontext
lnkPostalAddresses {
# Bestehende Anschriften auflisten
rowsReadBefore: rowsRead(
byAdrNrAnsNr: { usingAdrNr: {} }
) {
fldAnsNr
fldNamen
fldStdReKz
fldStdLiKz
fldStdInKz
fldInfo(as: TEXT)
}
# Neue verknüpfte Anschrift erstellen
rowNew(setAdrNrAnsNr: usingAdrNr) {
fldNa2(set: { text: "Neue Lieferanschrift" })
fldStdLiKz(set: { boolean: true })
rowSave {
fldAnsNr
}
}
# Bestehende Anschriften Info-Feld leeren
rowsModifyClearInfo: rowsModify(
byAdrNrAnsNr: { usingAdrNr: {} }
slowFilter: { isNotNull: {field: fldInfo} }
) {
fldAnsNr
fldInfo(set: {})
}
# Vorher erstellte Lieferanschrift ändern
rowsModifyLi: rowsModify(
byAdrNrAnsNr: { usingAdrNrLiAnsNr: {} }
) {
fldAnsNr
fldInfo(set: { text: "Lieferanschrift" } as: TEXT )
}
# Bestehende Infoanschriften ändern
rowsModifyIn: rowsModify(
byAdrNrAnsNr: { usingAdrNrInAnsNr: {} }
) {
fldAnsNr
fldInfo(set: { text: "Infoanschrift" } as: TEXT )
}
# Endzustand der Anschriften auflisten
rowsReadAfter: rowsRead(
byAdrNrAnsNr: { usingAdrNr: {} }
) {
fldAnsNr
fldNamen
fldStdReKz
fldStdLiKz
fldStdInKz
fldInfo(as: TEXT)
}
}
}
}
}
14.2.2 Der spezielle set...
-Parameter bei rowNew
Anders als bei Leseoperationen verwendet rowNew
in Verlinkungen set...
-Parameter statt by...
-Parameter. Diese definieren, welche Schlüsselfelder des neuen Datensatzes automatisch befüllt werden:
lnkPostalAddresses {
rowNew(setAdrNrAnsNr: usingAdrNr) {
# Das Feld fldAdrNr wird automatisch mit dem Wert aus dem äußeren Kontext befüllt
fldAdrNr # Zeigt "10000" aus dem Beispiel oben
}
}
14.2.3 Warum keine Verlinkungen in schreibenden Row-Kontexten?
Verlinkungen sind in schreibenden Row-Kontexten nicht verfügbar. Ein Datensatz, der sich in Bearbeitung befindet, ist in einem instabilen Zustand. Die Erstellung verlinkter Datensätze erfordert jedoch einen stabilen, gespeicherten Ausgangsdatensatz, um die referenzielle Integrität zu gewährleisten.
14.3 Verweise auf einzelne Datensätze (row...
)
14.3.1 Unterschiedliche Rückgabetypen
Das Verhalten von row...
-Verweisen unterscheidet sich je nach Row-Kontext:
In RowMutationRead
-Kontexten:
mutation {
tblProducts {
rowRead(exactMatch: { byNr: { kf1ArtNr: { string: "PROD-001" } } }) {
rowWgrNr { # Gibt ProductGroupRowMutationRead zurück
fldBez
# Weitere Mutation-Operationen möglich
lnkProducts {
rowsModify (
byWgrNr: {usingWgrNr: {} }
) {
fldMemo(set: {text: "Test" } as: TEXT)
}
}
}
}
}
}
In schreibenden Row-Kontexten:
mutation {
tblProducts {
rowModify(exactMatch: { byNr: { kf1ArtNr: { string: "PROD-001" } } }) {
rowWgrNr { # Gibt ProductGroupRowQueryRead zurück
fldBez
# Nur Abfrage-Operationen möglich
lnkProducts {
rowsRead (
byWgrNr: {usingWgrNr: {} }
) {
fldMemo(as:TEXT)
}
}
}
}
}
}
14.3.2 Technischer Hintergrund
Die Rückgabe von RowQueryRead
-Typen in schreibenden Row-Kontexten verhindert zyklische Zugriffe. Würde ein row...
-Verweis RowMutationRead
-Typen zurückgeben, könnte man über weitere lnk...
-Felder theoretisch zum ursprünglichen, gerade in Bearbeitung befindlichen Datensatz zurücknavigieren. Ein solcher Versuch würde zu einem Sperrkonflikt führen.
14.3.3 Praktische Anwendung
Verweise eignen sich besonders zur Informationsbeschaffung während Schreiboperationen:
mutation (
$wgrBez: String = null
) {
tblProducts {
rowsModify {
fldArtNr
fldWgrNr
# Variable auf null setzen
clearVariable: _any(value: null) @store(in: $wgrBez)
# Information aus verknüpften Daten abrufen
rowWgrNr {
fldBez @store(in: $wgrBez)
}
# Basierend auf diesen Informationen Entscheidungen treffen
updateIfWgrBezExists: _if(expr:{
and: [
{ isNotNull: { value: $wgrBez } }
{ ne: [ { value: $wgrBez } { value: "" } ] }
]
}) {
# Text zusammensetzen...
buildString: _any(expr:{
add: [
{ value: "Aktualisiert basierend auf Warengruppe: " }
{ value: $wgrBez }
]
}) @store(in: $wgrBez)
# ... und ins Memo schreiben
fldMemo(set: { text: $wgrBez } as:TEXT )
}
}
}
}
14.4 Verschachtelte Tabellen (tbl...
)
14.4.1 Umgekehrtes Verfügbarkeitsmuster
Verschachtelte Tabellen zeigen ein zu Verlinkungen genau umgekehrtes Verfügbarkeitsmuster:
- In
RowMutationRead
: Nur Leseoperationen möglich - In schreibenden Row-Kontexten: Vollständige Schreiboperationen verfügbar
14.4.2 Technische Grundlage
Verschachtelte Tabellen sind eigenständige Tabellen, die in Blob-Feldern des übergeordneten Datensatzes gespeichert sind. Die Schreibbarkeit der verschachtelten Tabelle hängt direkt von der Schreibbarkeit des übergeordneten Blob-Feldes ab:
- Übergeordneter Datensatz in schreibendem Row-Kontext → Blob-Feld schreibbar → Verschachtelte Tabelle schreibbar
- Übergeordneter Datensatz im Lese-Kontext → Blob-Feld nur lesbar → Verschachtelte Tabelle nur lesbar
14.4.3 Arbeiten mit verschachtelten Tabellen
mutation {
tblClient {
rowModify {
# Im schreibenden Row-Kontext
tblBnkVb {
# Startzustand der Einträge anzeigen
before: rowsRead {
fldNr
fldIBAN
fldGspKz
}
# Bestehende Einträge ändern: alle sperren
rowsModify {
fldNr
fldGspKz(set: {boolean: true})
}
# Neue Bankverbindung hinzufügen
rowNew {
fldNr # null
fldIBAN(set: { text: "DE12345678901234567890" })
rowSave {
fldNr # Wurde automatisch vergeben
fldGspKz # nicht gesperrt
}
}
# Endzustand der Einträge anzeigen
after: rowsRead {
fldNr
fldIBAN
fldGspKz
}
}
}
}
}
Änderungen in der verschachtelten Tabelle werden als Teil der übergeordneten Transaktion behandelt. Sie werden gemeinsam mit dem übergeordneten Datensatz gespeichert oder zurückgerollt.
14.5 Komplexe Workflows durch Verschachtelung
14.5.1 Kombinationsmöglichkeiten
Die verschiedenen Beziehungstypen können beliebig kombiniert werden, wobei die Verfügbarkeitsregeln aus Abschnitt 14.1 stets gelten. Es gibt keine technische Begrenzung der Verschachtelungstiefe.
14.5.2 Typische Muster
Aufbau vollständiger Datenstrukturen:
mutation {
tblAddresses {
# Neue Adresse
rowNewCustomer: rowNew {
fldStatus # Kunde
rowSave {
fldAdrNr # automatisch zugewiesen: 10000+
lnkPostalAddresses {
rowNewBillingAddress: rowNew (setAdrNrAnsNr: usingAdrNr) {
fldAdrNr # automatisch zugewiesen
fldNa2(set:{text:"Rechnungsanschrift"})
fldStdReKz(set:{boolean: true})
rowSave {
fldAnsNr # automatisch zugewiesen
lnkContactPeople {
rowNewA: rowNew (setAdrNrAnsNrNa:usingAdrNrAnsNr) {
fldAdrNr # automatisch zugewiesen
fldAnsNr # automatisch zugewiesen
fldAnr(set:{text:"Herr"})
fldVNa(set:{text:"Hubert"})
fldNNa(set:{text:"Hauptmann"})
rowSave {
fldAspNr # automatisch zugewiesen
}
}
rowNewB: rowNew (setAdrNrAnsNrNa:usingAdrNrAnsNr) {
fldAdrNr # automatisch zugewiesen
fldAnsNr # automatisch zugewiesen
fldAnr(set:{text:"Frau"})
fldVNa(set:{text:"Fiona"})
fldNNa(set:{text:"Fröhlich"})
rowSave {
fldAspNr # automatisch zugewiesen
}
}
}
}
}
rowNewShippingAddress: rowNew (setAdrNrAnsNr: usingAdrNr) {
fldAdrNr # automatisch zugewiesen
fldNa2(set:{text:"Lieferanschrift"})
fldStdLiKz(set:{boolean: true})
rowSave {
fldAnsNr # automatisch zugewiesen
lnkContactPeople {
rowNew (setAdrNrAnsNrNa:usingAdrNrAnsNr) {
fldAdrNr # automatisch zugewiesen
fldAnsNr # automatisch zugewiesen
fldAnr(set:{text:"Herr"})
fldVNa(set:{text:"Max"})
fldNNa(set:{text:"Musterman"})
rowSave {
fldAspNr # automatisch zugewiesen
}
}
}
}
}
}
}
}
rowNewSupplier: rowNew {
fldStatus(set:{text:"Lieferant"})
rowSave {
# Automatisch zugewiesene Felder
fldAdrNr # automatisch zugewiesen: 70000+
#lnkPostalAddresses { ...
}
}
}
}
Intelligente Datenverarbeitung:
mutation (
$wgrBez: String = null
) {
tblProducts {
rowsModify (
fastFilter: { isNotNull: { field: fldWgrNr } }
) {
fldArtNr
fldWgrNr
# Variable auf null setzen
clearVariable: _any(value: null) @store(in: $wgrBez)
# Warengruppen Bezeichnung lesen
rowWgrNr {
fldBez @store(in: $wgrBez)
}
# Wenn die Warengruppen Bezeichnung +++ enthält
updateIfWgrBezExists: _if(expr:{
gt: [
{ fnPos: [ { value: "+++" } { value: $wgrBez } ] }
{ value: 0}
]
}) {
# Rabattsatz 0 für Verkaufspreis 0 auf 10% setzen
fldVk0_Rab0_Sz(set:{float:10.0})
}
}
}
}
14.5.3 Reihenfolge und Abhängigkeiten
Die sequenzielle Abarbeitung von GraphQL-Operationen ermöglicht eine präzise Kontrolle über die Ausführungsreihenfolge. Explizite Speicheroperationen (rowSave
, rowSaveAndModify
) werden sofort ausgeführt, wodurch nachfolgende Operationen auf die aktualisierten Daten zugreifen können.
Nach einer expliziten Speicherung ändern sich die verfügbaren Operationen:
- Nach rowSave
: Wechsel zu RowMutationRead
, lnk...
verfügbar, keine Feldänderungen mehr
- Nach rowSaveAndModify
: Verbleib in schreibendem Row-Kontext, weitere Feldänderungen möglich, aber keine lnk...
14.6 Best Practices für komplexe Strukturen
14.6.1 Strukturierte Planung
Bei der Entwicklung komplexer Mutationen empfiehlt sich eine strukturierte Herangehensweise:
- Identifikation der Abhängigkeiten zwischen Datensätzen
- Planung der Erstellungsreihenfolge (vom Hauptdatensatz zu abhängigen Strukturen)
- Bestimmung der Stellen, an denen explizite Speicherungen erforderlich sind
- Gruppierung logisch zusammengehöriger Operationen
14.6.2 Fehlerrobustheit
Das Transaktionsmodell von Mutationen gewährleistet, dass entweder alle Änderungen erfolgreich durchgeführt oder komplett zurückgerollt werden. Diese Atomizität sollte genutzt werden, um konsistente Datenstrukturen zu gewährleisten.
14.6.3 Performance-Überlegungen
Während die Verschachtelungstiefe technisch unbegrenzt ist, sollten sehr tiefe Verschachtelungen vermieden werden: - Sie erschweren die Nachvollziehbarkeit - Sie können die Fehleranalyse verkomplizieren - Längere Transaktionen erhöhen die Wahrscheinlichkeit von Sperrenkonflikten
Die detaillierte Behandlung von Performance-Aspekten und Sperrenverwaltung erfolgt im nächsten Kapitel.
15. Transaktionssperren und Optimierung
Dieses Kapitel behandelt die fortgeschrittenen Aspekte der Performance-Optimierung von GraphQL-Mutationen. Nach den praktischen Grundlagen der vorherigen Kapitel liegt der Fokus nun auf dem Verständnis und der strategischen Nutzung des Sperrsystems zur Entwicklung robuster und effizienter Anwendungen.
15.1 Das Sperrsystem verstehen
15.1.1 Zwei unabhängige Sperrtypen
Die zugrundeliegende Datenbank-Engine arbeitet mit zwei voneinander unabhängigen Sperrtypen:
Transaktionssperren werden auf Tabellenebene vergeben und gehören zu einer spezifischen Transaktion. Sie koordinieren den Zugriff zwischen verschiedenen Transaktionen und werden automatisch beim Festschreiben oder Zurückrollen der Transaktion freigegeben.
Datensatzsperren schützen einzelne Datensätze während ihrer Bearbeitung. Im GraphQL-Kontext werden diese automatisch beim Öffnen eines Datensatzes gesetzt. In der regulären microtech ERP-Anwendung können solche Sperren jedoch deutlich länger bestehen - beispielsweise solange ein Benutzer einen Datensatz in einer Eingabemaske geöffnet hat.
15.1.2 Lesesperren und Schreibsperren
Beide Sperrtypen existieren in zwei Ausprägungen:
- Lesesperren (Shared Locks): Mehrere Transaktionen können gleichzeitig Lesesperren auf derselben Ressource halten
- Schreibsperren (Exclusive Locks): Nur eine Transaktion kann eine Schreibsperre halten, sie schließt alle anderen Zugriffe aus
Für eine bestimmte Ressource können entweder mehrere Lesesperren oder eine einzelne Schreibsperre existieren, niemals beides gleichzeitig.
15.1.3 Relevanz für GraphQL-Mutationen
Für die Performance-Optimierung von GraphQL-Mutationen sind primär die Transaktionssperren relevant. Diese werden automatisch vergeben: - Lesende Operationen setzen Lesesperren auf die betroffene Tabelle - Schreibende Operationen setzen Schreibsperren auf die betroffene Tabelle
Diese Sperren bleiben für die gesamte Dauer der Transaktion bestehen.
15.2 Das Sperrerweiterungsproblem
15.2.1 Interne Struktur von GraphQL-Operationen
Eine scheinbar einfache GraphQL-Operation wie rowModify
besteht intern aus mehreren Datenbankoperationen:
- Suchen des Datensatzes (lesende Operation) → Lesesperre auf Tabelle
- Öffnen zur Bearbeitung (lesende Operation mit Datensatzsperre) → Lesesperre bleibt
- Speichern der Änderungen (schreibende Operation) → Benötigt Schreibsperre
Der kritische Punkt ist Schritt 3: Die Transaktion hält bereits eine Lesesperre und benötigt nun eine Schreibsperre auf derselben Tabelle. Diese Erweiterung von Lese- zu Schreibsperre ist eine Hauptquelle für Performance-Probleme.
15.2.2 Entstehung von Deadlocks
Ein Deadlock entsteht, wenn zwei oder mehr Transaktionen gegenseitig auf Ressourcen warten, die die jeweils andere Transaktion hält. Bei GraphQL-Mutationen ist folgendes Szenario typisch:
- Transaktion A führt
rowModify
auf Tabelle X aus → erhält Lesesperre - Transaktion B führt
rowModify
auf Tabelle X aus → erhält ebenfalls Lesesperre - Transaktion A will speichern → benötigt Schreibsperre, wartet auf B
- Transaktion B will speichern → benötigt Schreibsperre, wartet auf A
- Deadlock erkannt → Eine Transaktion wird abgebrochen
15.2.3 Automatische Deadlock-Auflösung
Die Datenbank-Engine erkennt Deadlocks automatisch und löst sie durch Auswahl eines "Opfers" auf. Die gewählte Transaktion wird abgebrochen, ihre Sperren werden freigegeben, und alle ihre Änderungen werden zurückgerollt. Die andere Transaktion kann dann fortfahren.
Für die abgebrochene GraphQL-Mutation bedeutet dies einen Laufzeitfehler und den Verlust aller bereits durchgeführten Arbeiten.
15.3 Lösung durch präventive Sperrenvergabe
15.3.1 Die @acquireLocks
-Direktive
Die @acquireLocks
-Direktive löst das Sperrerweiterungsproblem durch präventive Sperrenvergabe:
mutation @acquireLocks(
forWriting: [tblAddresses, tblPostalAddresses],
forReading: [tblProducts]
) {
# Mutation wird nur ausgeführt, wenn alle Sperren verfügbar sind
tblAddresses {
rowModify(...) { ... }
}
}
15.3.2 Funktionsweise
Die Direktive versucht, alle angegebenen Sperren atomar zu erhalten, bevor die eigentliche Mutation startet:
- Erfolg: Alle Sperren sind verfügbar → Mutation wird ausgeführt
- Misserfolg: Nicht alle Sperren verfügbar → Sofortiger Abbruch mit Zeitüberschreitungsfehler
Da alle benötigten Schreibsperren bereits zu Beginn vorhanden sind, sind keine späteren Sperrerweiterungen mehr nötig. Dies eliminiert die Hauptursache für Deadlocks.
15.3.3 Parameter im Detail
forWriting
: Liste von Tabellen, für die Schreibsperren benötigt werdenforReading
: Liste von Tabellen, für die nur Lesesperren benötigt werden
Beide Parameter sind optional. Wird eine Tabelle in beiden Listen angegeben, hat forWriting
Vorrang. Die Sperren werden in einer konsistenten Reihenfolge angefordert, wodurch auch Deadlocks zwischen verschiedenen @acquireLocks
-Operationen vermieden werden.
15.4 Optimierungsstrategien
15.4.1 Wann @acquireLocks
verwenden?
Die Empfehlung ist eindeutig: Verwenden Sie @acquireLocks
bei allen Schreiboperationen. Dies mag zunächst übertrieben erscheinen, aber selbst eine einzelne rowModify
-Operation kann Deadlocks verursachen.
Besonders wichtig ist die Direktive bei: - Hochfrequenten Operationen - Operationen auf häufig genutzten Tabellen - Komplexen Workflows mit mehreren Tabellen - Operationen mit expliziten Leseoperationen vor Schreibzugriffen
15.4.2 Richtige Spezifikation der Sperren
Analysieren Sie Ihre Mutation und identifizieren Sie alle betroffenen Tabellen:
mutation @acquireLocks(
forWriting: [
tblAddresses, # rowModify hier
tblPostalAddresses, # rowModify über lnkPostalAddresses
tblContacts # rowNew über lnkContacts
],
forReading: [
tblProductGroups, # Gelesen über assignProductGroup
tblUsers # Gelesen über assignUser
]
) {
tblAddresses {
rowModify(...) {
# ...
rowSave {
lnkPostalAddresses {
rowModify(...) { ... }
}
lnkContacts {
rowNew(...) { ... }
}
}
}
}
}
Beachten Sie auch implizite Lesezugriffe durch:
- row...
-assign...
-Parameter (lesen aus der referenzierten Tabelle)
- fld...
-assign...
-Parameter (lesen aus der referenzierten Tabelle)
- Verweise über row...
-Felder
Diese sollten alle in der @acquireLocks
-Strategie berücksichtigt werden, um unerwartete Sperrenkonflikte zu vermeiden.
15.4.3 Balance zwischen Sicherheit und Performance
Während @acquireLocks
Deadlocks verhindert, kann übermäßige Sperrung die Parallelität reduzieren. Einige Richtlinien:
- Spezifizieren Sie nur Tabellen, auf die tatsächlich geschrieben wird
- Verwenden Sie
forReading
für Tabellen, die nur gelesen werden - Halten Sie Transaktionen so kurz wie möglich
- Vermeiden Sie unnötige Operationen innerhalb der Transaktion
15.5 Fehlerbehandlung und Diagnose
15.5.1 Typische Fehlermuster
Deadlock-Fehler haben charakteristische Meldungen und deuten auf fehlende präventive Sperrung hin. Sie treten typischerweise bei paralleler Last auf.
Zeitüberschreitungsfehler beim Anfordern von Sperren können auf verschiedene Ursachen hinweisen: - Hohe Systemlast - Lang laufende andere Transaktionen - Unzureichende Timeout-Konfiguration
15.5.2 Systematische Analyse
Bei wiederkehrenden Problemen empfiehlt sich:
- Identifikation problematischer Tabellen durch Analyse der Fehlermeldungen
- Überprüfung der Zugriffsmuster in betroffenen Mutationen
- Erweiterung der
@acquireLocks
-Strategien für diese Tabellen - Monitoring der Verbesserungen nach den Anpassungen
15.6 Best Practices
15.6.1 Entwicklungsstandards
Etablieren Sie klare Standards für Ihr Entwicklungsteam:
- Alle Mutationen mit Schreiboperationen verwenden
@acquireLocks
- Dokumentation der Sperrenstrategie in Kommentaren
- Konsistente Muster für ähnliche Operationen
- Regelmäßige Reviews der Sperrenstrategien
15.6.2 Testen unter Last
Deadlock-Probleme zeigen sich oft erst unter paralleler Last. Integrieren Sie Lasttests in Ihren Entwicklungsprozess:
- Simulieren Sie realistische Parallelität
- Testen Sie Kombinationen verschiedener Mutationen
- Überwachen Sie Performance-Metriken
- Identifizieren Sie Engpässe frühzeitig
15.6.3 Kontinuierliche Optimierung
Performance-Optimierung ist ein fortlaufender Prozess:
- Überwachen Sie Produktivsysteme auf Sperrenprobleme
- Analysieren Sie neue Geschäftsprozesse auf potenzielle Konflikte
- Passen Sie Sperrenstrategien bei Bedarf an
- Dokumentieren Sie Erkenntnisse für zukünftige Entwicklungen
15.7 Zusammenfassung
Die korrekte Verwendung von @acquireLocks
ist essentiell für robuste GraphQL-Mutationen. Durch präventive Sperrenvergabe werden Deadlocks vermieden und die Gesamtperformance des Systems verbessert. Die kleine zusätzliche Komplexität in der Entwicklung zahlt sich durch stabilere und vorhersagbarere Anwendungen aus.
Weitere Informationen in diesem Bereich: