1. Mechanismus für Hooks

Der Mechanismus für Hooks ist eine Methode, um benutzerdefinierte Logik vor oder nach bestimmten Änderungen bei Datenbankoperationen hinzuzufügen. Wenn die Datenbankschema modifiziert wird, z. B. beim Hinzufügen neuer Knoten, Löschen von Verbindungen zwischen Knoten oder Löschen mehrerer Knoten, können wir Hooks verwenden, um Datengültigkeitsprüfungen, Protokollierung, Berechtigungsprüfungen oder benutzerdefinierte Operationen durchzuführen. Dies ist entscheidend, um Datengenauigkeit und die Einhaltung von Geschäftsregeln sicherzustellen, während es den Entwicklern ermöglicht, weitere Funktionen hinzuzufügen, ohne die ursprüngliche Geschäftslogik zu ändern.

2. Methode zur Registrierung von Hooks

2.1 Globale Hooks und Lokale Hooks

Globale Hooks (Laufzeit-Hooks) gelten für alle Arten von Operationen im Graphen. Sie eignen sich für das Hinzufügen von Logik zur gesamten Anwendung, wie Protokollierung und Überwachung. Lokale Hooks (Schema-Hooks) sind in bestimmten Typschemas definiert und gelten nur für Mutationsoperationen, die dem Typ des Schemas entsprechen. Die Verwendung von lokalen Hooks ermöglicht es, alle Logiken, die sich auf bestimmte Knotentypen beziehen, an einem Ort zu zentralisieren, nämlich in der Schemadefinition.

2.2 Schritte zur Registrierung von Hooks

Die Registrierung eines Hooks im Code umfasst in der Regel die folgenden Schritte:

  1. Definieren der Hook-Funktion. Diese Funktion nimmt einen ent.Mutator entgegen und gibt einen ent.Mutator zurück. Zum Beispiel, Erstellen eines einfachen Protokollierungs-Hooks:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Protokolle vor der Mutationsoperation ausgeben
        log.Printf("Vor der Mutation: Typ=%s, Operation=%s\n", m.Type(), m.Op())
        // Die Mutationsoperation durchführen
        v, err := next.Mutate(ctx, m)
        // Protokolle nach der Mutationsoperation ausgeben
        log.Printf("Nach der Mutation: Typ=%s, Operation=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Den Hook mit dem Client registrieren. Bei globalen Hooks können sie mit der Use-Methode des Clients registriert werden. Bei lokalen Hooks können sie im Schema mit der Hooks-Methode des Typs registriert werden.
// Einen globalen Hook registrieren
client.Use(logHook)

// Einen lokalen Hook registrieren, der nur auf den Benutzertyp angewendet wird
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Spezifische Logik hinzufügen
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Sie können mehrere Hooks verketten, und sie werden in der Reihenfolge der Registrierung ausgeführt.

3. Ausführungsreihenfolge der Hooks

Die Ausführungsreihenfolge der Hooks wird durch die Reihenfolge bestimmt, in der sie mit dem Client registriert werden. Zum Beispiel wird client.Use(f, g, h) bei der Mutationsoperation in der Reihenfolge von f(g(h(...))) ausgeführt. In diesem Beispiel wird f zuerst ausgeführt, gefolgt von g und schließlich h.

Es ist wichtig zu beachten, dass Laufzeit-Hooks (Laufzeit-Hooks) Vorrang vor Schema-Hooks (Schema-Hooks) haben. Dies bedeutet, dass, wenn g und h als Hooks im Schema definiert sind, während f mit client.Use(...) registriert ist, die Ausführungsreihenfolge f(g(h(...))) sein wird. Dies gewährleistet, dass globale Logik, wie Protokollierung, vor allen anderen Hooks ausgeführt wird.

4. Umgang mit Problemen durch Hooks

Bei der Anpassung von Datenbankoperationen mit Hooks können wir auf das Problem von Importzyklen stoßen. Dies tritt normalerweise auf, wenn versucht wird, Schema-Hooks zu verwenden, da das ent/schema-Paket das ent-Kernpaket integrieren kann. Wenn das ent-Kernpaket auch versucht, ent/schema zu importieren, entsteht eine zirkuläre Abhängigkeit.

Ursachen für zirkuläre Abhängigkeiten

Zirkuläre Abhängigkeiten entstehen normalerweise aus bidirektionalen Abhängigkeiten zwischen Schemadefinitionen und generiertem Entitätscode. Das bedeutet, dass ent/schema von ent abhängt (weil es die vom ent-Framework bereitgestellten Typen verwenden muss), während der von ent generierte Code auch von ent/schema abhängt (weil er auf die darin definierten Schema-Informationen zugreifen muss).

Beheben von zirkulären Abhängigkeiten

Sollte ein Fehler aufgrund einer zirkulären Abhängigkeit auftreten, können die folgenden Schritte durchgeführt werden:

  1. Zunächst sollten alle Hooks in ent/schema auskommentiert werden.
  2. Anschließend können die benutzerdefinierten Typen, die in ent/schema definiert sind, in ein neues Paket verschoben werden. Zum Beispiel kann ein Paket mit dem Namen ent/schema/schematyp erstellt werden.
  3. Führen Sie den Befehl go generate ./... aus, um das ent-Paket zu aktualisieren, sodass es auf den neuen Paketpfad zeigt, und aktualisieren Sie die Typreferenzen im Schema. Zum Beispiel ändern Sie schema.T in schematyp.T.
  4. Entkommentieren Sie die zuvor auskommentierten Hooks-Verweise und führen Sie den Befehl go generate ./... erneut aus. An diesem Punkt sollte die Codegenerierung ohne Fehler fortgesetzt werden.

Durch Befolgen dieser Schritte können wir das durch Hooks-Importe verursachte Problem der zirkulären Abhängigkeit lösen und sicherstellen, dass die Logik des Schemas und die Implementierung der Hooks reibungslos fortgesetzt werden können.

5. Verwendung von Hook-Hilfsfunktionen

Das ent-Framework bietet eine Reihe von Hook-Hilfsfunktionen, die uns dabei helfen können, den Zeitpunkt der Hook-Ausführung zu kontrollieren. Im Folgenden sind einige Beispiele für häufig verwendete Hook-Hilfsfunktionen aufgeführt:

// Führen Sie HookA nur für UpdateOne- und DeleteOne-Vorgänge aus
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Führen Sie HookB nicht während des Erstellenvorgangs aus
hook.Unless(HookB(), ent.OpCreate)

// Führen Sie HookC nur aus, wenn die Mutation das Feld "status" ändert und das Feld "dirty" gelöscht wird
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Verbieten Sie das Ändern des Felds "password" bei Update (multiple) Vorgängen
hook.If(
    hook.FixedError(errors.New("Passwort kann bei Mehrfachupdates nicht bearbeitet werden")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Diese Hilfsfunktionen ermöglichen es uns, die Aktivierungsbedingungen von Hooks für verschiedene Vorgänge genau zu steuern.

6. Transaktionshooks

Transaktionshooks ermöglichen es, bestimmte Hooks auszuführen, wenn eine Transaktion abgeschlossen (Tx.Commit) oder zurückgesetzt (Tx.Rollback) wird. Dies ist sehr nützlich, um die Datenkonsistenz und die Atomarität von Operationen sicherzustellen.

Beispiel für Transaktionshooks

client.Tx(ctx, func(tx *ent.Tx) error {
    // Registrierung eines Transaktionshooks - hookBeforeCommit wird vor dem Commit ausgeführt.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // Die Logik vor dem tatsächlichen Commit kann hier platziert werden.
            fmt.Println("Vor dem Commit")
            return next.Commit(ctx, tx)
        })
    })

    // Führen Sie eine Reihe von Operationen innerhalb der Transaktion durch...

    return nil
})

Der obige Code zeigt, wie ein Transaktionshook registriert wird, um vor einem Commit in einer Transaktion ausgeführt zu werden. Dieser Hook wird nachdem alle Datenbankoperationen ausgeführt wurden und bevor die Transaktion tatsächlich durchgeführt wird aufgerufen.