Skip to main content

Das ist oft sogar die bessere und sicherere Methode, da die Datenbank die Datenintegrität am zuverlässigsten gewährleistet. Du musst das nicht in PHP machen.


 

IDs mit Form in SQL generieren

 

Die gängigste Methode hierfür sind Datenbank-Trigger in Kombination mit einer Hilfstabelle oder einer Sequenz. Ein Trigger ist eine Prozedur, die automatisch ausgeführt wird, bevor (BEFORE) oder nachdem (AFTER) ein Datensatz eingefügt (INSERT), aktualisiert (UPDATE) oder gelöscht (DELETE) wird.

Konzept am Beispiel Rechnungsnummer (2025-1, 2025-2, ...)

  1. Hilfstabelle erstellen: Du legst eine kleine Tabelle an, die für jedes Jahr den zuletzt vergebenen Zähler speichert.

    SQL

    CREATE TABLE rechnungs_zaehler (
        jahr INT PRIMARY KEY,
        letzte_nr INT NOT NULL DEFAULT 0
    );
    
  2. Trigger erstellen: Du schreibst einen BEFORE INSERT Trigger für deine rechnungen Tabelle. Bevor eine neue Rechnung gespeichert wird, macht der Trigger Folgendes:
    • Er schaut in der rechnungs_zaehler Tabelle nach, was die letzte Nummer für das aktuelle Jahr war.
    • Wenn für das aktuelle Jahr noch kein Eintrag existiert, legt er einen an.
    • Er erhöht die Nummer um 1.
    • Er speichert die neue, erhöhte Nummer in der rechnungs_zaehler Tabelle.
    • Er weist der neuen Rechnung, die gerade eingefügt wird, die zusammengesetzte Rechnungsnummer zu (z.B. ‚2025-123‘).

Vorteil dieser SQL-Lösung:

  • Atomarität: Da alles innerhalb der Datenbank und des Triggers abläuft, ist der Vorgang atomar und sicher vor Race Conditions. Die Datenbank kümmert sich um die notwendigen Sperren.
  • Zentralisierung: Die Logik ist an einem Ort (der Datenbank) und nicht in der Anwendung verteilt. Jede Anwendung, die auf diese Datenbank zugreift, folgt automatisch denselben Regeln.

 

Transaktionen in PHP gegen Race Conditions

 

Deine zweite Frage passt perfekt hierzu. Wenn du die Logik doch in PHP umsetzen möchtest, sind Transaktionen unerlässlich, um Race Conditions zu verhindern. Eine Race Condition tritt auf, wenn zwei Benutzer fast gleichzeitig dieselbe Aktion ausführen und sich gegenseitig in die Quere kommen.

Beispiel-Szenario ohne Transaktion (Problem):

  1. Benutzer A liest die letzte Rechnungsnummer für 2025: Es ist die 12.
  2. Genau jetzt liest auch Benutzer B die letzte Rechnungsnummer für 2025: Es ist ebenfalls die 12.
  3. Benutzer A berechnet die neue Nummer (12 + 1 = 13) und speichert die Rechnung 2025-13.
  4. Benutzer B berechnet ebenfalls die neue Nummer (12 + 1 = 13) und speichert seine Rechnung.
  5. Ergebnis: Du hast zwei Rechnungen mit der Nummer 2025-13, was zu einem Fehler oder, schlimmer noch, zu inkonsistenten Daten führt.

 

Lösung mit Transaktionen und Sperren in PHP (PDO)

 

Eine Transaktion sorgt dafür, dass eine Reihe von Datenbankoperationen als eine einzige, unteilbare Einheit behandelt wird. Entweder klappt alles oder gar nichts. Um die Race Condition beim Lesen zu verhindern, kombinierst du die Transaktion mit einer Sperre (FOR UPDATE).

Hier ist ein praktisches Beispiel mit PHP Data Objects (PDO):

PHP

// Annahme: $pdo ist deine bestehende Datenbankverbindung

try {
    // 1. Transaktion starten
    $pdo->beginTransaction();

    // 2. Die letzte Nummer für das aktuelle Jahr ermitteln UND die Zeile sperren.
    // Das 'FOR UPDATE' sorgt dafür, dass kein anderer Prozess diese Zeile lesen
    // oder ändern kann, bis unsere Transaktion beendet ist.
    $jahr = date('Y');
    $stmt = $pdo->prepare("SELECT letzte_nr FROM rechnungs_zaehler WHERE jahr = ? FOR UPDATE");
    $stmt->execute([$jahr]);
    $result = $stmt->fetch();

    $neueNr = 1;
    if ($result) {
        $neueNr = $result['letzte_nr'] + 1;
        // Zähler aktualisieren
        $updateStmt = $pdo->prepare("UPDATE rechnungs_zaehler SET letzte_nr = ? WHERE jahr = ?");
        $updateStmt->execute([$neueNr, $jahr]);
    } else {
        // Erster Eintrag für dieses Jahr
        $insertStmt = $pdo->prepare("INSERT INTO rechnungs_zaehler (jahr, letzte_nr) VALUES (?, ?)");
        $insertStmt->execute([$jahr, $neueNr]);
    }

    // 3. Jetzt die neue Rechnung mit der garantiert eindeutigen Nummer einfügen
    $rechnungsNummer = $jahr . '-' . $neueNr;
    $insertRechnung = $pdo->prepare("INSERT INTO rechnungen (rechnungs_nr, betrag, datum) VALUES (?, ?, NOW())");
    $insertRechnung->execute([$rechnungsNummer, 199.99]); // Beispiel-Betrag

    // 4. Wenn alles geklappt hat, die Änderungen dauerhaft speichern
    $pdo->commit();

    echo "Rechnung erfolgreich mit der Nummer " . $rechnungsNummer . " angelegt!";

} catch (Exception $e) {
    // 5. Wenn irgendein Fehler aufgetreten ist, alle Änderungen zurückrollen
    $pdo->rollBack();
    echo "Fehler: Die Rechnung konnte nicht erstellt werden. " . $e->getMessage();
}

Zusammengefasst:

  • $pdo->beginTransaction();: Startet den „Alles oder Nichts“-Modus.
  • SELECT ... FOR UPDATE;: Liest den aktuellen Zähler und sperrt ihn, sodass kein zweiter Prozess ihn lesen kann, bevor der erste fertig ist. Der zweite Prozess muss warten.
  • $pdo->commit();: Bestätigt alle Änderungen, wenn kein Fehler aufgetreten ist, und gibt die Sperre frei.
  • $pdo->rollBack();: Macht alle Änderungen seit beginTransaction() rückgängig, falls ein Fehler auftritt.

Leave a Reply