# Praxisbeispiel: Adressen, Anschriften und Ansprechpartner verwalten (GraphQL)

<span class="custom-button red-button">Gen. 24 Enterprise</span>

Dieses Beispiel zeigt, wie Sie über GraphQL Adressen mit ihren Anschriften und Ansprechpartnern lesen, anlegen, ändern und löschen. Dabei wird die verschachtelte Tabellenstruktur über Links navigiert, sodass zusammengehörige Daten in einer einzigen Operation verarbeitet werden können.

Grundlegende GraphQL-Kenntnisse werden vorausgesetzt. Sollten Ihnen Konzepte wie Tabellenzugriff, Verknüpfungen oder Filter noch nicht vertraut sein, empfehlen wir zunächst die **[GraphQL Doku - Abfragen (Queries)](./graphqldoku.md)**. Für Mutationen siehe **[GraphQL Doku - Mutationen](./graphqlmutations.md)**. 

---

## Inhaltsverzeichnis

1. **[Überblick](#1-uberblick)**
2. **[Adressen lesen](#2-adressen-lesen)**
3. **[Adresse mit Anschrift und Ansprechpartner anlegen](#3-adresse-mit-anschrift-und-ansprechpartner-anlegen)**
4. **[Adresse mit Anschrift und Ansprechpartner ändern](#4-adresse-mit-anschrift-und-ansprechpartner-andern)**
    - **[4.1 Nur Adressdaten ändern](#41-nur-adressdaten-andern)**
    - **[4.2 Nur Anschrift ändern](#42-nur-anschrift-andern)**
    - **[4.3 Nur Ansprechpartner ändern](#43-nur-ansprechpartner-andern)**
    - **[4.4 Alle Ebenen gleichzeitig ändern](#44-alle-ebenen-gleichzeitig-andern)**
5. **[Adresse löschen](#5-adresse-loschen)**
6. **[Erweiterte Muster](#6-erweiterte-muster)**
7. **[Tipps und Stolpersteine](#7-tipps-und-stolpersteine)**

---

## 1. Überblick

Die Adressverwaltung in microtech ERP verteilt Stammdaten auf drei verknüpfte Tabellen. 

*    Eine **Adresse** (`tblAddresses`) kann **mehrere Anschriften** (`tblPostalAddresses`) besitzen

*    Jede **Anschrift** kann **mehrere Ansprechpartner** (`tblContactPeople`) enthalten.

### Tabellenstruktur

```
tblAddresses (1)
  └─ lnkPostalAddresses → tblPostalAddresses (N)
                             └─ lnkContactPeople → tblContactPeople (N)
```

| Tabelle | Beschreibung | Schlüsselfeld |
|---------|-------------|---------------|
| `tblAddresses` | Stammdaten der Adresse (Suchbegriff, Status, Info) | `fldAdrNr` |
| `tblPostalAddresses` | Anschriftdaten (Name, Straße, PLZ, Ort) | `fldAdrNr` + `fldAnsNr` |
| `tblContactPeople` | Ansprechpartner (Name, Funktion, Kontaktdaten) | `fldAdrNr` + `fldAnsNr` + `fldAspNr` |

Die Navigation zwischen den Tabellen erfolgt über **Links** (`lnk...`). Dieser verschachtelte Zugriff ist der empfohlene Weg, um zusammengehörige Daten in einer Operation zu lesen oder zu schreiben.

!!! warning "Beachten Sie"

    *    Alle drei Tabellen sind auch einzeln über `tblAddresses`, `tblPostalAddresses` und `tblContactPeople` erreichbar. 

    *    Für den **typischen Anwendungsfall** — Adresse mit Anschriften und Ansprechpartnern — ist der **verschachtelte Zugriff über Links** jedoch **übersichtlicher und performanter**.

---

## 2. Adressen lesen

### 2.1 Einzelne Adresse mit Anschriften und Ansprechpartnern

Die folgende Query liest eine Adresse per Adressnummer und navigiert über Links zu den zugehörigen Anschriften und Ansprechpartnern.

```graphql
query AdresseMitAnschriftenUndAnsprechpartnern {
  tblAddresses {
    # Adresse per keyFilter lesen (schnellster Zugriff)
    rowRead(kf1AdrNr: { text: "10001" }) {
      fldAdrNr                            # Adressnummer
      fldSuchBeg                          # Suchbegriff
      fldStatus(as: TEXT)                 # Status (z.B. "Kunde")
      fldInfo                             # Infofeld

      # Direktzugriff auf die Standard-Rechnungs- und Lieferanschrift:
      # rowReAnsNr/rowLiAnsNr lösen fldReAnsNr/fldLiAnsNr direkt auf —
      # ohne den Umweg über lnkPostalAddresses
      fldReAnsNr                          # Rechnungsanschriftennummer (Referenz)
      rowReAnsNr {                        # → aufgelöste Rechnungsanschrift
        fldNa2                            # Name / Firma
        fldStr                            # Straße
        fldPLZ                            # PLZ
        fldOrt                            # Ort
      }
      fldLiAnsNr                          # Lieferanschriftennummer (Referenz)
      rowLiAnsNr {                        # → aufgelöste Lieferanschrift
        fldNa2
        fldStr
        fldPLZ
        fldOrt
      }

      # Alle Anschriften dieser Adresse (inkl. der oben einzeln abgerufenen)
      lnkPostalAddresses {
        # Sortierfolge byAdrNrAnsNr: nach Adressnummer und Anschriftnummer
        rowsRead(byAdrNrAnsNr: { usingAdrNr: {} }) {
          fldAnsNr                        # Anschriftnummer
          fldNa2                          # Name / Firma
          fldStr                          # Straße
          fldPLZ                          # Postleitzahl
          fldOrt                          # Ort

          # Ansprechpartner dieser Anschrift
          lnkContactPeople {
            rowsRead(byAdrNrAnsNrAspNr: { usingAdrNrAnsNr: {} }) {
              fldAspNr                    # Ansprechpartnernummer
              fldAnr                      # Anrede
              fldVNa                      # Vorname
              fldNNa                      # Nachname
              fldPos                      # Position / Funktion
              fldTel1                     # Telefonnummer
              fldEMail1                   # E-Mail-Adresse
            }
          }
        }
      }
    }
  }
}
```

!!! warning "Beachten Sie"

    Wenn die Adressnummer nicht existiert, gibt `rowRead` den Wert `null` zurück. Verwenden Sie die Direktive `@onNull`, um diesen Fall gezielt zu behandeln.

---

### 2.2 Paginierte Adressliste

Sobald Sie **mehrere Adressen abfragen**, verwenden Sie **`conRead` mit Paginierung**. 

Anders als `rowsRead` liefert `conRead` die Ergebnisse seitenweise zurück — das ist der bevorzugte Weg, weil Sie im Voraus nie wissen, wie viele Datensätze eine Abfrage liefert. Ohne `first` werden alle Datensätze durchlaufen, was bei großen Datenbeständen zu langen Antwortzeiten und hohem Speicherverbrauch führen kann.

```graphql
# Erste Seite: 3 Adressen lesen, sortiert nach Adressnummer (Standardsortierung)
query AdressenPaginiert {
  tblAddresses {
    conRead(first: 3) {
      # edges enthält die eigentlichen Datensätze als Liste
      edges {
        cursor                            # Eindeutiger Cursor dieses Datensatzes (für Paginierung)
        node {
          fldAdrNr                        # Adressnummer
          fldSuchBeg                      # Suchbegriff
          fldStatus(as: TEXT)             # Status als Text (z.B. "Kunde")
        }
      }
      # pageInfo liefert Metadaten zur Paginierung — immer NACH edges abfragen
      pageInfo {
        hasNextPage                       # true = es gibt weitere Seiten
        endCursor                         # Cursor des letzten Datensatzes — für die nächste Seite
        edgeCount                         # Anzahl der Datensätze auf dieser Seite
      }
    }
  }
}
```

Für die nächste Seite übergeben Sie den `endCursor` aus `pageInfo` als `after`-Parameter. Wiederholen Sie dies, bis `hasNextPage` den Wert `false` zurückgibt:

```graphql
# Folgeseite: nächste 3 Adressen ab dem endCursor der vorherigen Abfrage
query AdressenNaechsteSeite {
  tblAddresses {
    conRead(first: 3, after: "endCursor aus vorheriger pageInfo") {
      edges {
        node {
          fldAdrNr
          fldSuchBeg
        }
      }
      pageInfo {
        hasNextPage
        endCursor
        edgeCount
      }
    }
  }
}
```

Um vorab die **Gesamtanzahl** zu ermitteln, rufen Sie `conRead` ohne `first` und ohne `edges` auf. 

Die API durchläuft dann alle Datensätze, überträgt aber keine Felddaten:

```graphql
# Nur zählen — keine Datensätze übertragen
query AdressenAnzahl {
  tblAddresses {
    conRead {
      pageInfo {
        edgeCount                         # Gesamtanzahl aller Adressen
      }
    }
  }
}
```

!!! tip "Tipp"

    `conRead` kann dieselben verschachtelten Abfragen wie `rowRead` enthalten.

Das folgende Beispiel kombiniert die Gesamtzahl mit einer vollständigen Adressabfrage inklusive Anschriften und Ansprechpartnern:

```graphql
# Paginierte Adressliste mit Anschriften und Ansprechpartnern
query AdressenMitDetails {
  # Gesamtanzahl vorab ermitteln (ohne edges, ohne first)
  gesamt: tblAddresses {
    conRead {
      pageInfo { edgeCount }              # Gesamtanzahl aller Adressen
    }
  }
  # Erste Seite mit Detaildaten abrufen
  tblAddresses {
    conRead(first: 5) {
      edges {
        node {
          fldAdrNr                        # Adressnummer
          fldSuchBeg                      # Suchbegriff
          fldStatus(as: TEXT)             # Status (z.B. "Kunde")

          # Alle Anschriften dieser Adresse über den Link abrufen
          lnkPostalAddresses {
            rowsRead(byAdrNrAnsNr: { usingAdrNr: {} }) {
              fldAnsNr                    # Anschriftnummer
              fldNa2                      # Name / Firma
              fldStr                      # Straße
              fldPLZ                      # Postleitzahl
              fldOrt                      # Ort
              fldTel                      # Telefon
              fldEMail1                   # E-Mail

              # Ansprechpartner dieser Anschrift
              lnkContactPeople {
                rowsRead(byAdrNrAnsNrAspNr: { usingAdrNrAnsNr: {} }) {
                  fldAspNr                # Ansprechpartnernummer
                  fldAnr                  # Anrede
                  fldVNa                  # Vorname
                  fldNNa                  # Nachname
                  fldPos                  # Position / Funktion
                  fldTel1                 # Telefon direkt
                  fldEMail1              # E-Mail direkt
                }
              }
            }
          }
        }
      }
      # pageInfo am Ende: Anzahl der Treffer und Cursor für die nächste Seite
      pageInfo {
        hasNextPage
        endCursor
        edgeCount                         # Anzahl der Adressen auf dieser Seite
      }
    }
  }
}
```

!!! tip "Tipp: Performance: edges vor pageInfo"

    Fragen Sie `edges` immer **vor** `pageInfo` ab. Die API berechnet `pageInfo` (insbesondere `edgeCount`) erst nach dem Durchlaufen der Ergebnismenge. 

*    Wenn `edges` zuerst steht, werden die Datensätze dabei direkt ausgeliefert.

*    Steht `pageInfo` zuerst, muss die Ergebnismenge zweimal durchlaufen werden.

---

### 2.3 Adressen filtern

Die API bietet drei Filtermechanismen mit unterschiedlicher Performance. Wählen Sie immer den schnellsten, der Ihren Anforderungen genügt.

#### Sortierfolge mit Schlüsselfilter — Schnellster Zugriff

Über `allBetween` wählen Sie eine Sortierfolge und schränken den Bereich über Schlüsselfelder (`kf1...`) ein. Die Abfrage nutzt den Index direkt und ist daher am schnellsten.

Ohne `edges` und ohne `first` erhalten Sie nur die Anzahl der Treffer — ideal, um vorab zu prüfen, wie viele Datensätze eine Filterung liefert:

```graphql
# Nur die Anzahl aller Kunden ermitteln — keine Datensätze übertragen
query AnzahlKunden {
  tblAddresses {
    conRead(allBetween: {
      byStatus: {
        kf1Status: { text: "Kunde" }
      }
    }) {
      pageInfo {
        edgeCount                         # Anzahl aller Kunden
      }
    }
  }
}
```

Mit `edges` und `first` rufen Sie die eigentlichen Datensätze seitenweise ab:

```graphql
# Erste 10 Kunden mit Daten abrufen
query AlleKunden {
  tblAddresses {
    conRead(first: 10, allBetween: {
      byStatus: {
        kf1Status: { text: "Kunde" }
      }
    }) {
      edges {
        node {
          fldAdrNr                        # Adressnummer
          fldSuchBeg                      # Suchbegriff
          fldStatus(as: TEXT)             # Status — hier immer "Kunde"
        }
      }
      pageInfo { hasNextPage endCursor edgeCount }
    }
  }
}
```

Mit `from`/`to` auf dem Schlüsselfeld können Sie Bereichsabfragen über den Index durchführen — beispielsweise alle Adressen, deren Suchbegriff mit "MÜLLER" beginnt. Die API liest dabei nur den relevanten Indexbereich, nicht die gesamte Tabelle. Details zu Sortierfolgen und Indexstrukturen finden Sie in der **[GraphQL Doku - Abfragen (Queries)](https://hilfe.microtech.de/18_graphql/graphqldoku/)**.

```graphql
# Präfixsuche — alle Adressen mit Suchbegriff "MÜLLER..."
query SuchbegriffMueller {
  tblAddresses {
    conRead(first: 10, allBetween: {
      bySuchBeg: {
        # Bereich von "MÜLLER" bis "MÜLLERZZZ" erfasst alle Präfix-Treffer
        kf1SuchBeg: {
          from: { text: "MÜLLER" }
          to: { text: "MÜLLERZZZ" }
        }
      }
    }) {
      edges {
        node {
          fldAdrNr
          fldSuchBeg                      # z.B. "MÜLLER MEIER", "MÜLLER-LÜDENSCHEID"
        }
      }
      pageInfo { hasNextPage endCursor edgeCount }
    }
  }
}
```

#### fastFilter — Schneller Feldwertfilter

`fastFilter` lässt sich mit einer Sortierfolge kombinieren, um die Ergebnismenge zusätzlich einzuschränken. Verfügbare Felder sind im Enum `AddressFastAnyFields` definiert.

```graphql
# Alle Kunden mit Suchbegriff "M..." — Sortierfolge + fastFilter kombiniert
query KundenMitSuchbegriffM {
  tblAddresses {
    conRead(first: 10,
      # Sortierfolge bySuchBeg: nur Suchbegriffe von "M" bis "MZZZ"
      allBetween: { bySuchBeg: { kf1SuchBeg: {
        from: { text: "M" }
        to: { text: "MZZZ" }
      } } },
      # fastFilter schränkt zusätzlich auf Status "Kunde" ein
      fastFilter: { eq: [{ field: fldStatus }, { value: "Kunde" }] }
    ) {
      edges {
        node {
          fldAdrNr
          fldSuchBeg                      # z.B. "MAIER", "MICROTECH", "MÜLLER-LÜDENSCHEID"
          fldStatus(as: TEXT)             # immer "Kunde"
        }
      }
      pageInfo { hasNextPage edgeCount }
    }
  }
}
```

#### slowFilter — Flexibler Filter

`slowFilter` prüft jeden Datensatz einzeln und ist daher langsamer, bietet aber volle Flexibilität. Über Funktionen wie `fnPos` (Teilstring-Suche) oder `fnLeft`/`fnRight` können Sie Bedingungen formulieren, die mit `fastFilter` nicht möglich sind.

```graphql
# Teilstring-Suche — alle Adressen, die "MÜLLER" irgendwo im Suchbegriff enthalten
query AdressenMitSlowFilter {
  tblAddresses {
    conRead(first: 10, slowFilter: {
      # fnPos("MÜLLER", fldSuchBeg) > 0 — prüft ob "MÜLLER" im Feld vorkommt
      gt: [
        { fnPos: [{ value: "MÜLLER" }, { field: fldSuchBeg }] },
        { value: 0 }
      ]
    }) {
      edges {
        node {
          fldAdrNr
          fldSuchBeg                      # z.B. "MÜLLER MEIER", "MÜLLER-LÜDENSCHEID"
          fldStatus(as: TEXT)
        }
      }
      pageInfo { hasNextPage edgeCount }
    }
  }
}
```

!!! tip "Performance-Reihenfolge"

    **Sortierfolge mit keyFilter** (nutzt Index) > **fastFilter** (Feldvergleich) > **slowFilter** (prüft jeden Datensatz). Verwenden Sie `slowFilter` nur, wenn kein passender Schlüsselzugriff oder `fastFilter` ausreicht.

---

## 3. Adresse mit Anschrift und Ansprechpartner anlegen

Das Anlegen einer Adresse mit Anschrift und Ansprechpartner erfolgt als **verschachtelte Mutation**. In einer einzigen Operation wird die Adresse erstellt, gespeichert, dann über den Link eine Anschrift angelegt und gespeichert, und schließlich über einen weiteren Link ein Ansprechpartner hinzugefügt.

### Ablauf der verschachtelten Mutation

```
rowNew (Adresse)
  └─ Felder setzen (fldSuchBeg, fldStatus, ...)
  └─ rowSave
       └─ lnkPostalAddresses
            └─ rowNew (Anschrift) mit setAdrNrAnsNr: usingAdrNr
                 └─ Felder setzen (fldNa2, fldStr, fldPLZ, fldOrt)
                 └─ rowSave
                      └─ lnkContactPeople
                           └─ rowNew (Ansprechpartner) mit setAdrNrAnsNrNa: usingAdrNrAnsNr
                                └─ Felder setzen (fldAnr, fldVNa, fldNNa, ...)
                                └─ rowSave
```

Der Parameter `setAdrNrAnsNr: usingAdrNr` sorgt dafür, dass die neue Anschrift automatisch die `fldAdrNr` der übergeordneten Adresse übernimmt. Analog setzt `setAdrNrAnsNrNa: usingAdrNrAnsNr` beim Ansprechpartner automatisch Adressnummer und Anschriftnummer.

!!! warning "Beachten Sie"

    Jeder `rowNew`-Block **muss** ein `rowSave` enthalten, damit der Datensatz tatsächlich in die Datenbank geschrieben wird. Dies gilt insbesondere auch für den innersten Ansprechpartner — ein `rowNew` ohne `rowSave` verwirft den Datensatz.

### Neuanlegen von Adresse mit Anschrift und Ansprechpartner

```graphql
mutation AdresseMitAnschriftUndAnsprechpartnerAnlegen
  @acquireLocks(forWriting: [tblAddresses, tblPostalAddresses, tblContactPeople])
{
  tblAddresses {
    # Neue Adresse anlegen
    rowNew {
      fldSuchBeg(set: { text: "BEISPIEL GMBH" })        # Suchbegriff
      fldStatus(set: { text: "Kunde" })                  # Adressstatus — bestimmt den Nummernkreis

      # Adresse speichern — liefert RowMutationRead zurück
      rowSave {
        fldAdrNr                                          # Vom System vergebene Adressnummer

        # Anschrift über Link anlegen
        lnkPostalAddresses {
          # setAdrNrAnsNr: usingAdrNr übernimmt die AdrNr automatisch
          rowNew(setAdrNrAnsNr: usingAdrNr) {
            fldNa2(set: { text: "Beispiel GmbH" })       # Name / Firma
            fldStr(set: { text: "Industriestraße 42" })   # Straße
            fldPLZ(set: { text: "60311" })                # Postleitzahl
            fldOrt(set: { text: "Frankfurt am Main" })    # Ort
            fldStdReKz(set: { boolean: true })            # Als Standard-Rechnungsanschrift setzen

            # Anschrift speichern
            rowSave {
              fldAnsNr                                    # Vom System vergebene Anschriftnummer

              # Ansprechpartner über Link anlegen
              lnkContactPeople {
                # setAdrNrAnsNrNa: usingAdrNrAnsNr übernimmt AdrNr und AnsNr
                rowNew(setAdrNrAnsNrNa: usingAdrNrAnsNr) {
                  fldAnr(set: { text: "Herr" })            # Anrede
                  fldVNa(set: { text: "Thomas" })          # Vorname
                  fldNNa(set: { text: "Schmidt" })         # Nachname
                  fldPos(set: { text: "Einkauf" })         # Position
                  fldTel1(set: { text: "069 12345678" })   # Telefon
                  fldEMail1(set: { text: "t.schmidt@beispiel.de" })  # E-Mail

                  # Ansprechpartner speichern
                  rowSave {
                    fldAspNr                              # Vom System vergebene Nummer
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

!!! info "Adressstatus und Nummernkreise"

    Der `fldStatus` bestimmt den Nummernkreis für die automatisch vergebene `fldAdrNr` — z. B. erhalten Kunden eine Nummer ab 10000, Lieferanten eine Nummer ab 70000. Die verfügbaren Statuswerte (Interessent, Kunde, Lieferant etc.) sind in `tblAddressStatuses` konfiguriert. Siehe **[Parameter - Auslesen: 3.1 Adressstatus](graphqlparametertabellenauslesen.md#31-adressstatus-tbladdressstatuses)**.

!!! warning "Beachten Sie"

    Die Direktive `@acquireLocks(forWriting: [...])` fordert **Schreibsperren** für die angegebenen Tabellen **zu Beginn** der Transaktion an. Ohne diese Direktive werden Sperren erst beim tatsächlichen Schreibzugriff gesetzt, was zu Deadlocks führen kann, wenn parallel andere Transaktionen auf dieselben Tabellen zugreifen. Weitere Details finden Sie Kapitel zu den Mutations unter folgendem Punkt: **[Wann @acquireLocks verwenden?](./graphqlmutations.md#1541-wann-acquirelocks-verwenden)**.

---

## 4. Adresse mit Anschrift und Ansprechpartner ändern

Zum Ändern navigieren Sie per `rowModify` zur gewünschten Adresse, setzen die neuen Feldwerte und speichern. Über die Links können Sie in derselben Mutation auch Anschriften und Ansprechpartner ändern. Es muss dabei nicht auf jeder Ebene etwas geändert werden — Sie können frei wählen, welche Daten Sie aktualisieren.

### 4.1 Nur Adressdaten ändern

Wenn nur Felder der Adresse selbst geändert werden sollen, genügt `rowModify` ohne verschachtelte Links:

```graphql
mutation AdresseAendern {
  tblAddresses {
    rowModify(kf1AdrNr: { text: "10001" }) {
      fldSuchBeg(set: { text: "STEFFIS TINTENFASS NEU" })  # Suchbegriff aktualisieren
      fldInfo(set: { text: "Stammkunde seit 2020" })        # Infofeld aktualisieren

      rowSave {
        fldAdrNr
        fldSuchBeg                                           # Aktualisierter Wert
        fldInfo                                              # Aktualisierter Wert
      }
    }
  }
}
```

### 4.2 Nur Anschrift ändern

Wenn Sie nur eine Anschrift ändern möchten, greifen Sie direkt über `tblPostalAddresses` zu. Der zusammengesetzte Schlüssel besteht aus `kf1AdrNr` (Adressnummer) und `kf2AnsNr` (Anschriftnummer):

```graphql
mutation AnschriftDirektAendern {
  tblPostalAddresses {
    # Direktzugriff: Adresse 10001, Anschrift Nr. 1
    rowModify(kf1AdrNr: { text: "10001", kf2AnsNr: { int: 1 } }) {
      fldStr(set: { text: "Bahnhofstraße 14" })             # Neue Straße
      fldOrt(set: { text: "Marburg" })                       # Neuer Ort

      rowSave {
        fldAdrNr                                             # Bestätigung
        fldAnsNr
        fldStr                                               # Aktualisierter Wert
        fldOrt                                               # Aktualisierter Wert
      }
    }
  }
}
```

#### Rechnungs- und Lieferanschrift ändern

Über die Adresse können Sie per `usingAdrNrReAnsNr` bzw. `usingAdrNrLiAnsNr` direkt auf die hinterlegte Rechnungs- oder Lieferanschrift zugreifen — ohne die Anschriftnummer vorher nachschlagen zu müssen:

```graphql
# Rechnungsanschrift ändern — usingAdrNrReAnsNr löst fldReAnsNr automatisch auf
mutation RechnungsanschriftAendern {
  tblAddresses {
    rowRead(kf1AdrNr: { text: "10001" }) {
      fldReAnsNr                                             # Zur Kontrolle: welche AnsNr ist hinterlegt?
      lnkPostalAddresses {
        # usingAdrNrReAnsNr: navigiert direkt zur Rechnungsanschrift
        rowsModify(byAdrNrAnsNr: { usingAdrNrReAnsNr: {} }) {
          fldStr(set: { text: "Rheingasse 1B" })

          rowSave {
            fldAnsNr                                         # Bestätigung der Anschriftnummer
            fldNa2
            fldStr
            fldPLZ
            fldOrt
          }
        }
      }
    }
  }
}
```

```graphql
# Lieferanschrift ändern — usingAdrNrLiAnsNr löst fldLiAnsNr automatisch auf
mutation LieferanschriftAendern {
  tblAddresses {
    rowRead(kf1AdrNr: { text: "10001" }) {
      fldLiAnsNr                                             # Zur Kontrolle: welche AnsNr ist hinterlegt?
      lnkPostalAddresses {
        # usingAdrNrLiAnsNr: navigiert direkt zur Lieferanschrift
        rowsModify(byAdrNrAnsNr: { usingAdrNrLiAnsNr: {} }) {
          fldStr(set: { text: "Industriepark 7" })

          rowSave {
            fldAnsNr
            fldNa2
            fldStr
            fldPLZ
            fldOrt
          }
        }
      }
    }
  }
}
```

!!! info "Wann `rowReAnsNr`/`rowLiAnsNr`, wann `lnkPostalAddresses`?"

    *    **`rowReAnsNr` / `rowLiAnsNr`**: Wenn die Standard-Rechnungs- oder Lieferanschrift **gelesen** werden soll. Löst `fldReAnsNr`/`fldLiAnsNr` direkt auf — schnell und ohne Sortierfolge. Nur lesend verfügbar.

    *    **`lnkPostalAddresses`**: Wenn Anschriften **geändert**, **hinzugefügt** oder **gelöscht** werden sollen. Mit `usingAdrNrReAnsNr`/`usingAdrNrLiAnsNr` gezielt die Standard-Anschrift, mit `usingAdrNr` alle Anschriften der Adresse.

---

### 4.3 Nur Ansprechpartner ändern

Auch Ansprechpartner können direkt über `tblContactPeople` geändert werden. Der zusammengesetzte Schlüssel besteht aus drei Teilen — `kf1AdrNr`, `kf2AnsNr` und `kf3AspNr`:

```graphql
mutation AnsprechpartnerDirektAendern {
  tblContactPeople {
    # Direktzugriff: Adresse 10001, Anschrift Nr. 1, Ansprechpartner Nr. 1
    rowModify(kf1AdrNr: { text: "10001", kf2AnsNr: { int: 1, kf3AspNr: { int: 1 } } }) {
      fldTel1(set: { text: "06421 98765" })                  # Neue Telefonnummer
      fldEMail1(set: { text: "neu@beispiel.de" })            # Neue E-Mail

      rowSave {
        fldAdrNr
        fldAnsNr
        fldAspNr
        fldTel1
        fldEMail1
      }
    }
  }
}
```

#### Standard-Ansprechpartner einer Anschrift ändern

Jede Anschrift hat einen Standard-Ansprechpartner, referenziert über `fldAspNr`. Über `tblPostalAddresses` navigieren Sie zur Anschrift und ändern von dort per `lnkContactPeople` mit `usingAdrNrAnsNrAspNr` den Standard-Ansprechpartner — ohne dessen Nummer vorher nachschlagen zu müssen:

```graphql
mutation StandardAnsprechpartnerAendern {
  tblPostalAddresses {
    # Anschrift öffnen (nur lesen — Anschrift selbst wird nicht geändert)
    rowRead(kf1AdrNr: { text: "10001", kf2AnsNr: { int: 1 } }) {
      fldAspNr                                               # Zur Kontrolle: welcher ASP ist Standard?

      lnkContactPeople {
        # usingAdrNrAnsNrAspNr: löst fldAspNr der Anschrift automatisch auf
        # → ändert gezielt den Standard-ASP, nicht alle Ansprechpartner
        # (usingAdrNrAnsNr: {} ohne AspNr würde ALLE Ansprechpartner ändern)
        rowsModify(byAdrNrAnsNrAspNr: { usingAdrNrAnsNrAspNr: {} }) {
          fldTel1(set: { text: "069 12345678" })              # Neue Telefonnummer

          rowSave {
            fldAspNr
            fldVNa
            fldNNa
            fldTel1
          }
        }
      }
    }
  }
}
```

!!! info "Wann `rowAspNr`, wann `lnkContactPeople`?"

    *    **`rowAspNr`**: Wenn der Standard-Ansprechpartner einer Anschrift **gelesen** werden soll. Löst `fldAspNr` direkt auf — schnell und ohne Sortierfolge. Nur lesend verfügbar.

    *    **`lnkContactPeople`**: Wenn Ansprechpartner **geändert**, **hinzugefügt** oder **gelöscht** werden sollen. Mit `usingAdrNrAnsNrAspNr` gezielt den Standard-Ansprechpartner, mit `usingAdrNrAnsNr` alle Ansprechpartner der Anschrift.

---

### 4.4 Alle Ebenen gleichzeitig ändern

Im komplexesten Fall können Sie mit nur einer einzigen Mutation sowohl Adresse, Standard-Rechnungsanschrift und deren Standard-Ansprechpartner ändern. `usingAdrNrReAnsNr` navigiert automatisch zur Rechnungsanschrift, `usingAdrNrAnsNrAspNr` zum Standard-Ansprechpartner dieser Anschrift:

```graphql
mutation AdresseMitReAnschriftUndStdAspAendern
  @acquireLocks(forWriting: [tblAddresses, tblPostalAddresses, tblContactPeople])
{
  tblAddresses {
    # Adresse zur Bearbeitung öffnen
    rowModify(kf1AdrNr: { text: "10001" }) {
      fldInfo(set: { text: "Stammkunde seit 2020" })     # Infofeld aktualisieren

      # Adresse speichern
      rowSave {
        fldAdrNr                                          # Bestätigung der Adressnummer
        fldInfo                                           # Aktualisierter Wert

        # Standard-Rechnungsanschrift ändern
        lnkPostalAddresses {
          # usingAdrNrReAnsNr: löst fldReAnsNr automatisch auf
          rowsModify(byAdrNrAnsNr: { usingAdrNrReAnsNr: {} }) {
            fldStr(set: { text: "Bahnhofstraße 14" })    # Neue Straße

            rowSave {
              fldAnsNr                                    # Bestätigung
              fldStr                                      # Aktualisierter Wert

              # Standard-Ansprechpartner dieser Anschrift ändern
              lnkContactPeople {
                # usingAdrNrAnsNrAspNr: löst fldAspNr der Anschrift automatisch auf
                rowsModify(byAdrNrAnsNrAspNr: { usingAdrNrAnsNrAspNr: {} }) {
                  fldTel1(set: { text: "06421 98765" })   # Neue Telefonnummer

                  rowSave {
                    fldAspNr
                    fldTel1                               # Aktualisierter Wert
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

!!! warning "Beachten Sie"

    Bei **verschachtelten Änderungen über Links** werden `assignAddress` und `assignPostalAddress` **nicht** benötigt. 

    Diese Parameter sind nur beim eigenständigen Zugriff auf `tblPostalAddresses` oder `tblContactPeople` erforderlich (siehe **[Abschnitt 6.5](#65-assign-parameter)**).

---

## 5. Adresse löschen

Das Löschen einer Adresse erfolgt über `rowDelete`. Optional kann der Parameter `ignoreWarnings` angegeben werden, um Warnmeldungen zu unterdrücken.

```graphql
mutation AdresseLoeschen {
  tblAddresses {
    # Adresse per Adressnummer löschen
    # ignoreWarnings: true unterdrückt Warnungen bei bestehenden Abhängigkeiten
    # Ohne diesen Parameter erhalten Sie Hinweise, falls z.B. Vorgänge referenziert werden
    rowDelete(kf1AdrNr: { text: "10005" }, ignoreWarnings: true) {
      fldAdrNr                            # Bestätigung: welche Adresse wurde gelöscht
      fldSuchBeg                          # Suchbegriff — zur Kontrolle im Rückgabewert
    }
  }
}
```

!!! warning "Beachten Sie"

    Das Löschen einer Adresse kann kaskadierende Auswirkungen haben, d. h. weitere aufeinanderfolgende Effekte. 

    Prüfen Sie vor dem Löschen, ob die Adresse in Vorgängen, Offenen Posten oder anderen Bereichen referenziert wird. Ohne `ignoreWarnings: true` erhalten Sie Warnmeldungen, die auf bestehende Abhängigkeiten hinweisen.

---

## 6. Erweiterte Muster

### 6.1 Optimistic Locking (modifyLSN)

Wenn mehrere Benutzer oder Systeme gleichzeitig auf Adressen zugreifen, kann es zu Konflikten kommen. Das Konzept des **Optimistic Locking** verhindert, dass versehentlich Änderungen eines anderen Benutzers überschrieben werden.

Der Ablauf ist zweistufig:

1. Beim Lesen den aktuellen `fldModifyLSN`-Wert merken
2. Beim Schreiben den gemerkten Wert als `modifyLSN` übergeben

Stimmt der Wert nicht mehr überein (weil ein anderer Benutzer den Datensatz zwischenzeitlich geändert hat), wird die Änderung übersprungen. Über eine Variable und `@include` können Sie in derselben Mutation den aktuellen Stand nachlesen.

```graphql
# Schritt 1: Adresse lesen und LSN merken
query AdresseMitLSN {
  tblAddresses {
    rowRead(kf1AdrNr: { text: "10001" }) {
      fldAdrNr
      fldSuchBeg
      fldModifyLSN                        # Diesen Wert für die Änderung merken
    }
  }
}
```

```graphql
# Schritt 2: Adresse ändern mit LSN-Prüfung
mutation AdresseAendernMitLSN(
  $adrNr: String = "10001"
  $modifyLSN: BigInt = 9161168561880151768   # Wert aus Schritt 1
  $notFound: Boolean! = false                # Steuervariable für den Konfliktfall
) {
  tblAddresses {
    # Änderung nur durchführen, wenn modifyLSN übereinstimmt
    modified: rowModify(
      kf1AdrNr: { text: $adrNr }
      modifyLSN: $modifyLSN
    )
      @onNull(
        returnValue: skip                    # Bei LSN-Mismatch: Änderung überspringen
        set: { var: $notFound }              # $notFound auf true setzen
      )
    {
      fldSuchBeg(set: { text: "STEFFIS TINTENFASS NEU" })

      rowSave {
        fldModifyLSN                         # Neuer LSN-Wert nach der Änderung
      }
    }

    # Nur bei Konflikt: aktuellen Stand nachlesen
    mismatchedLSN: rowRead(
      kf1AdrNr: { text: $adrNr }
    )
      @include(if: $notFound)                # Wird nur ausgeführt wenn LSN nicht passte
    {
      fldAdrNr
      fldSuchBeg
      fldModifyLSN                           # Aktueller LSN — für erneuten Versuch
    }
  }
}
```

!!! info "Info"

    Der `modifyLSN`-Wert ist ein Beispiel. Verwenden Sie **immer** den tatsächlichen Wert aus der vorherigen Leseabfrage. Bei LSN-Mismatch liefert `modified` den Wert `null` (durch `returnValue: skip`) und `mismatchedLSN` den aktuellen Datensatz mit dem neuen `fldModifyLSN` — damit kann der Client den Konflikt erkennen und erneut versuchen.

---

### 6.2 Bedingte Erstellung (ifNotExists)

Mit dem Parameter `ifNotExists` können Sie eine Adresse nur dann anlegen, wenn sie noch nicht existiert. Über `@onNull` mit einer Steuervariable und `@include` können Sie in derselben Mutation den bestehenden Datensatz nachlesen.

```graphql
mutation AdresseNurWennNichtVorhanden(
  $adrNr: String = "10001"
  $exists: Boolean! = false                  # Steuervariable für den Existenzfall
) {
  tblAddresses {
    # Adresse nur anlegen, wenn AdrNr noch nicht existiert
    created: rowNew(ifNotExists: { kf1AdrNr: { text: $adrNr } })
      @onNull(
        returnValue: skip                    # Bei Existenz: Anlage überspringen
        set: { var: $exists }                # $exists auf true setzen
      )
    {
      fldSuchBeg(set: { text: "NEUER KUNDE" })

      rowSave {
        fldAdrNr
        fldSuchBeg
      }
    }

    # Nur wenn Adresse bereits existiert: bestehenden Datensatz nachlesen
    existing: rowRead(
      kf1AdrNr: { text: $adrNr }
    )
      @include(if: $exists)                  # Wird nur ausgeführt wenn Adresse schon da war
    {
      fldAdrNr
      fldSuchBeg
      fldStatus(as: TEXT)
    }
  }
}
```

Wenn die Adresse bereits existiert, liefert `created` den Wert `null` (durch `returnValue: skip`) und `existing` den bestehenden Datensatz. So kann der Client erkennen, ob die Adresse neu angelegt oder bereits vorhanden war.

---

### 6.3 Datensatz kopieren (rowCopy)

Mit `rowCopy` erstellen Sie eine Kopie eines bestehenden Datensatzes. Die Kopie kann vor dem Speichern noch angepasst werden.

```graphql
mutation AdresseKopieren {
  tblAddresses {
    # rowCopy erstellt eine Kopie von Adresse 10001
    # Nicht explizit gesetzte Felder behalten die Werte des Originals
    rowCopy(kf1AdrNr: { text: "10001" }) {
      # Nur den Suchbegriff ändern — alle anderen Felder werden vom Original übernommen
      fldSuchBeg(set: { text: "KOPIE VON STEFFIS TINTENFASS" })

      rowSave {
        fldAdrNr                          # Neue, vom System vergebene Adressnummer
        fldSuchBeg                        # Angepasster Suchbegriff
        fldStatus(as: TEXT)               # Status — vom Original übernommen
      }
    }
  }
}
```

---

### 6.4 Massenoperationen (rowsModify, rowsDelete)

Für die Änderung oder Löschung mehrerer Datensätze stehen `rowsModify` und `rowsDelete` zur Verfügung. Diese Operationen arbeiten auf allen Datensätzen, die der angegebenen Sortierfolge oder dem Filter entsprechen.

#### Mehrere Anschriften einer Adresse ändern

```graphql
mutation AnschriftenMassenAenderung {
  tblAddresses {
    # Adresse per rowRead öffnen (nur lesen — Adresse selbst wird nicht geändert)
    rowRead(kf1AdrNr: { text: "10001" }) {

      # Über den Link alle Anschriften dieser Adresse erreichen
      lnkPostalAddresses {
        # rowsModify wirkt auf ALLE Datensätze der Sortierfolge
        # usingAdrNr: {} übernimmt die AdrNr aus dem übergeordneten rowRead
        rowsModify(byAdrNrAnsNr: { usingAdrNr: {} }) {
          fldOrt(set: { text: "Marburg an der Lahn" })   # Ort bei ALLEN Anschriften ändern

          rowSave {
            fldAnsNr                    # Welche Anschrift wurde geändert
            fldOrt                      # Aktualisierter Wert zur Kontrolle
          }
        }
      }
    }
  }
}
```

#### Mehrere Ansprechpartner löschen

```graphql
mutation AnsprechpartnerMassenLoeschung {
  tblAddresses {
    # Navigation: Adresse → Anschrift Nr. 1 → alle Ansprechpartner löschen
    rowRead(kf1AdrNr: { text: "10001" }) {
      lnkPostalAddresses {
        # Nur Anschrift Nr. 1 auswählen (kf2AnsNr innerhalb usingAdrNr verschachtelt)
        rowsRead(byAdrNrAnsNr: {
          usingAdrNr: {
            kf2AnsNr: { int: 1 }
          }
        }) {
          lnkContactPeople {
            # rowsDelete löscht ALLE Ansprechpartner dieser Anschrift
            # usingAdrNrAnsNr: {} übernimmt AdrNr + AnsNr aus dem Kontext
            rowsDelete(byAdrNrAnsNrAspNr: {
              usingAdrNrAnsNr: {}
            }, ignoreWarnings: true) {
              fldAspNr                     # Nummer des gelöschten Ansprechpartners
              fldNNa                       # Nachname — zur Kontrolle im Rückgabewert
            }
          }
        }
      }
    }
  }
}
```

---

### 6.5 assign-Parameter

Die Parameter `assignAddress` und `assignPostalAddress` werden benötigt, wenn Sie **eigenständig** (nicht über Links) auf `tblPostalAddresses` oder `tblContactPeople` zugreifen. Sie stellen die Zuordnung zur übergeordneten Adresse bzw. Anschrift her.

| Parameter | Tabelle | Zweck |
|-----------|---------|-------|
| `assignAddress` | `tblPostalAddresses` | Verknüpft die Anschrift mit einer Adresse |
| `assignPostalAddress` | `tblContactPeople` | Verknüpft den Ansprechpartner mit einer Anschrift |

**Beim verschachtelten Zugriff über Links sind diese Parameter nicht erforderlich**, da die Zuordnung automatisch über den Kontext der übergeordneten Tabelle erfolgt. Ebenso wenig beim Direktzugriff über `tblPostalAddresses` oder `tblContactPeople` mit vollständigem Schlüssel (`kf1AdrNr` + `kf2AnsNr` + ggf. `kf3AspNr`), da der Datensatz dadurch eindeutig identifiziert ist.

#### Anschrift eigenständig anlegen (assignAddress)

```graphql
mutation AnschriftEigenstaendigAnlegen {
  tblPostalAddresses {
    # assignAddress: Verknüpft die neue Anschrift mit Adresse 10001
    rowNew(assignAddress: { kf1AdrNr: { text: "10001" } }) {
      fldNa2(set: { text: "Zweigstelle Nord" })
      fldStr(set: { text: "Hafenstraße 12" })
      fldPLZ(set: { text: "20095" })
      fldOrt(set: { text: "Hamburg" })

      rowSave {
        fldAdrNr                                # Übernommen aus assignAddress
        fldAnsNr                                # Vom System vergeben
        fldNa2
        fldStr
        fldPLZ
        fldOrt
      }
    }
  }
}
```

#### Ansprechpartner eigenständig anlegen (assignPostalAddress)

```graphql
mutation AnsprechpartnerEigenstaendigAnlegen {
  tblContactPeople {
    # assignPostalAddress: Verknüpft den neuen ASP mit Adresse 10001, Anschrift 1
    rowNew(assignPostalAddress: {
      kf1AdrNr: { text: "10001", kf2AnsNr: { int: 1 } }
    }) {
      fldAnr(set: { text: "Frau" })
      fldVNa(set: { text: "Lisa" })
      fldNNa(set: { text: "Weber" })
      fldTel1(set: { text: "040 9876543" })
      fldEMail1(set: { text: "l.weber@beispiel.de" })

      rowSave {
        fldAdrNr                                # Übernommen aus assignPostalAddress
        fldAnsNr                                # Übernommen aus assignPostalAddress
        fldAspNr                                # Vom System vergeben
        fldVNa
        fldNNa
      }
    }
  }
}
```

!!! tip "Tipp"

    Bevorzugen Sie den verschachtelten Zugriff über Links (`lnkPostalAddresses`, `lnkContactPeople`). Der eigenständige Tabellenzugriff mit `assign`-Parametern ist nur dann sinnvoll, wenn Sie gezielt einzelne Anschriften oder Ansprechpartner ohne den Kontext der übergeordneten Adresse anlegen müssen — z.B. wenn die übergeordnete Adresse in derselben Mutation nicht verarbeitet wird.

---

## 7. Tipps und Stolpersteine

1. **keyFilter vor slowFilter**: Verwenden Sie für den Zugriff auf Adressen **immer** den Schlüsselzugriff `kf1AdrNr`, wenn die Adressnummer bekannt ist. Dies ist der schnellste Zugriffspfad. `slowFilter` durchsucht jeden Datensatz einzeln und ist deutlich langsamer.

2. **rowSave nicht vergessen**: Jeder schreibende Row-Kontext (`rowNew`, `rowModify`, `rowCopy`) **muss** mit `rowSave` abgeschlossen werden. Ohne `rowSave` werden die Änderungen verworfen — auch bei verschachtelten Ansprechpartnern.

3. **Sperren vorab anfordern**: Verwenden Sie `@acquireLocks(forWriting: [...])` bei Mutationen, die mehrere Tabellen betreffen. Ohne vorab angeforderte Sperren können Deadlocks auftreten.

4. **Gesamtanzahl ermitteln**: `conRead` ohne `first`-Parameter liefert `edgeCount` über alle Datensätze. Für die Gesamtanzahl: `conRead { pageInfo { edgeCount } }` ohne `edges`.

5. **edges vor pageInfo**: Fragen Sie `edges` immer **vor** `pageInfo` ab — das ist performanter, da die API `pageInfo` erst nach dem Durchlaufen der Ergebnismenge berechnet.

6. **Sortierfolgen über Links**: Innerhalb von Links stehen andere Sortierfolgen zur Verfügung als auf Tabellenebene. `lnkPostalAddresses` bietet `byAdrNrAnsNr` und `byAdrNrNa2`, `lnkContactPeople` bietet `byAdrNrAnsNrAspNr`, `byAdrNrAnsNrNa` und `byAdrNrAnsNrNNa`.

7. **Enum-Werte bei rowNew über Links**: Beim Anlegen über Links müssen Sie den passenden `set...`-Enum angeben, damit die Schlüsselfelder automatisch aus dem übergeordneten Kontext übernommen werden:
    - Anschrift: `setAdrNrAnsNr: usingAdrNr`
    - Ansprechpartner: `setAdrNrAnsNrNa: usingAdrNrAnsNr`

8. **Adressnummer automatisch vergeben**: Wenn Sie beim Anlegen einer Adresse keine `fldAdrNr` setzen, vergibt das System automatisch die nächste freie Nummer.

9. **Verschachteltes Anlegen nur nach rowSave**: Links (`lnk...`) sind erst nach `rowSave` des übergeordneten Datensatzes verfügbar. Die Anschrift kann erst angelegt werden, nachdem die Adresse gespeichert wurde.

10. **Transaktionale Sicherheit**: Die gesamte verschachtelte Mutation wird atomar ausgeführt. Schlägt ein Teilschritt fehl (z.B. Ansprechpartner kann nicht gespeichert werden), werden **alle** Änderungen zurückgerollt — auch die bereits gespeicherte Adresse und Anschrift.

11. **Standard-Anschrift und Standard-Ansprechpartner per Shortcut**: Über Links stehen Shortcuts zur Verfügung, die hinterlegte Referenznummern automatisch auflösen:
    - `usingAdrNrReAnsNr` / `usingAdrNrLiAnsNr` → navigiert zur Standard-Rechnungs-/Lieferanschrift (`fldReAnsNr`/`fldLiAnsNr`)
    - `usingAdrNrAnsNrAspNr` → navigiert zum Standard-Ansprechpartner einer Anschrift (`fldAspNr`)
    - Zum **Lesen** gibt es zusätzlich die Direktfelder `rowReAnsNr`, `rowLiAnsNr` und `rowAspNr`.

12. **rowSaveAndModify**: Nach `rowSave` steht auch `rowSaveAndModify` zur Verfügung, das den Datensatz speichert und sofort wieder zur Bearbeitung öffnet. Dies ist nützlich, wenn Sie nach dem Speichern weitere Änderungen am selben Datensatz vornehmen möchten.
