Zum Inhalt

GraphQL Entwickler Dokumentation - Mutationen (Mutations)

Gen. 24 Enterprise


Sie befinden sich auf der Seite: Grundlagen der Mutations und ihr Verhältnis zu Queries.


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 Speichern
  • RowMutationCopy: Eine Kopie eines bestehenden Datensatzes vor dem Speichern
  • RowMutationModify: Ein zur Bearbeitung geöffneter bestehender Datensatz
  • RowMutationDelete: 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 den RowMutationRead-Kontext
  • rowSaveAndModify: Speichert den Datensatz und wechselt/verbleibt im RowMutationModify-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 RowMutationCopyRowMutationModify - 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"
        }
      }
    }
  }
}
Dieses Muster ermöglicht es: - Zu erkennen, ob ein Datensatz zwischen Lesen und Schreiben geändert wurde - Bei Konflikten die aktuelle Version zu lesen - Dem Client die Möglichkeit zu geben, auf Konflikte zu reagieren

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 ein fld...-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:

  1. Identifikation der Abhängigkeiten zwischen Datensätzen
  2. Planung der Erstellungsreihenfolge (vom Hauptdatensatz zu abhängigen Strukturen)
  3. Bestimmung der Stellen, an denen explizite Speicherungen erforderlich sind
  4. 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:

  1. Suchen des Datensatzes (lesende Operation) → Lesesperre auf Tabelle
  2. Öffnen zur Bearbeitung (lesende Operation mit Datensatzsperre) → Lesesperre bleibt
  3. 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:

  1. Transaktion A führt rowModify auf Tabelle X aus → erhält Lesesperre
  2. Transaktion B führt rowModify auf Tabelle X aus → erhält ebenfalls Lesesperre
  3. Transaktion A will speichern → benötigt Schreibsperre, wartet auf B
  4. Transaktion B will speichern → benötigt Schreibsperre, wartet auf A
  5. 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 werden
  • forReading: 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:

  1. Identifikation problematischer Tabellen durch Analyse der Fehlermeldungen
  2. Überprüfung der Zugriffsmuster in betroffenen Mutationen
  3. Erweiterung der @acquireLocks-Strategien für diese Tabellen
  4. 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: