1. Mechanizm Hooks

Mechanizm Hooks to metoda dodawania niestandardowej logiki przed lub po konkretnych zmianach zachodzących w operacjach bazodanowych. Podczas modyfikowania schematu bazy danych, takich jak dodawanie nowych węzłów, usuwanie krawędzi między węzłami lub usuwanie wielu węzłów, możemy użyć Hooks do przeprowadzania walidacji danych, zapisywania logów, sprawdzania uprawnień lub wykonywania niestandardowych operacji. Jest to kluczowe dla zapewnienia spójności danych i zgodności z zasadami biznesowymi, pozwalając programistom dodawać dodatkową funkcjonalność bez zmieniania oryginalnej logiki biznesowej.

2. Metoda Rejestracji Hooków

2.1 Globalne Hooki i Lokalne Hooki

Globalne hooki (Hooki czasu wykonania) są skuteczne dla wszystkich rodzajów operacji w grafie. Nadają się do dodawania logiki do całej aplikacji, takiej jak logowanie i monitorowanie. Lokalne hooki (Hooki schematu) są definiowane w ramach konkretnych schematów typów i stosują się tylko do operacji mutacji, które pasują do typu schematu. Korzystanie z lokalnych hooków pozwala na skonsolidowanie całej logiki związanej z konkretnymi typami w jednym miejscu, mianowicie w definicji schematu.

2.2 Kroki Rejestracji Hooków

Rejestracja hooka w kodzie zazwyczaj obejmuje następujące kroki:

  1. Definiowanie funkcji hooka. Ta funkcja przyjmuje ent.Mutator i zwraca ent.Mutator. Na przykład, tworzenie prostego hooka logowania:
logHook := func(next ent.Mutator) ent.Mutator {
    return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
        // Wypisywanie logów przed operacją mutacji
        log.Printf("Przed mutacją: Typ=%s, Operacja=%s\n", m.Type(), m.Op())
        // Wykonanie operacji mutacji
        v, err := next.Mutate(ctx, m)
        // Wypisywanie logów po operacji mutacji
        log.Printf("Po mutacji: Typ=%s, Operacja=%s\n", m.Type(), m.Op())
        return v, err
    })
}
  1. Rejestracja hooka w kliencie. Dla globalnych hooków można je zarejestrować przy użyciu metody Use klienta. Dla lokalnych hooków można je zarejestrować w schemacie przy użyciu metody Hooks typu.
// Rejestracja globalnego hooka
client.Use(logHook)

// Rejestracja lokalnego hooka, zastosowanego tylko do typu User
client.User.Use(func(next ent.Mutator) ent.Mutator {
    return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
        // Dodanie konkretnej logiki
        // ...
        return next.Mutate(ctx, m)
    })
})
  1. Możesz łączyć wiele hooków, a zostaną one wykonane w kolejności rejestracji.

3. Kolejność Wykonywania Hooków

Kolejność wykonania hooków jest określana przez kolejność ich rejestracji w kliencie. Na przykład, client.Use(f, g, h) zostanie wykonany w operacji mutacji w kolejności f(g(h(...))). W tym przykładzie, f jest pierwszym hookiem do wykonania, a następnie g, i na końcu h.

Warto zauważyć, że hooki czasu wykonania (Hooki czasu wykonania) mają pierwszeństwo nad hookami schematu (Hooki schematu). Oznacza to, że jeśli g i h są hookami zdefiniowanymi w schemacie, podczas gdy f jest zarejestrowany przy użyciu client.Use(...), kolejność wykonania będzie f(g(h(...))). Zapewnia to, że globalna logika, taka jak logowanie, zostanie wykonana przed wszystkimi innymi hookami.

4. Radzenie Sobie z Problemami Wynikającymi z Hooków

Podczas dostosowywania operacji bazodanowych przy użyciu hooków, możemy napotkać problem cykli importu. Zazwyczaj występuje to przy próbie użycia hooków schematu, ponieważ pakiet ent/schema może wprowadzić pakiet rdzenia ent. Jeśli pakiet rdzenia ent również próbuje importować ent/schema, powstaje cykliczna zależność.

Przyczyny Cyklicznych Zależności

Cykliczne zależności zazwyczaj wynikają z dwukierunkowych zależności między definicjami schematu i wygenerowanym kodem jednostki. Oznacza to, że ent/schema zależy od ent (ponieważ musi używać typów dostarczanych przez framework ent), podczas gdy wygenerowany kod przez ent również zależy od ent/schema (ponieważ musi mieć dostęp do informacji o schemacie zdefiniowanym w nim).

Rozwiązywanie Cyklicznych Zależności

Jeśli napotkasz błąd cyklicznej zależności, możesz postępować zgodnie z poniższymi krokami:

  1. Po pierwsze, zakomentuj wszystkie haki używane w ent/schema.
  2. Następnie przenieś niestandardowe typy zdefiniowane w ent/schema do nowego pakietu, na przykład możesz utworzyć pakiet o nazwie ent/schema/schematype.
  3. Uruchom polecenie go generate ./..., aby zaktualizować pakiet ent, aby wskazywał na nową ścieżkę pakietu, aktualizując odwołania do typów w schemacie. Na przykład zmień schema.T na schematype.T.
  4. Odkomentuj wcześniej zakomentowane odwołania do haków i ponownie uruchom polecenie go generate ./.... W tym momencie generacja kodu powinna przebiegać bez błędów.

Dzięki tym krokom możemy rozwiązać problem cyklicznej zależności spowodowany importem haków, zapewniając płynne przejście logiki schematu i implementacji haków.

5. Użycie Funkcji Pomocniczych Haka

Framework ent udostępnia zestaw funkcji pomocniczych haka, które mogą pomóc nam kontrolować moment wykonania haków. Oto kilka przykładów często używanych funkcji pomocniczych haka:

// Wykonaj wyłącznie HookA dla operacji UpdateOne i DeleteOne
hook.On(HookA(), ent.OpUpdateOne|ent.OpDeleteOne)

// Nie wykonuj HookB podczas operacji Create
hook.Unless(HookB(), ent.OpCreate)

// Wykonaj HookC tylko wtedy, gdy Mutacja modyfikuje pole "status" i czyści pole "dirty"
hook.If(HookC(), hook.And(hook.HasFields("status"), hook.HasClearedFields("dirty")))

// Zabroń modyfikowania pola "password" w operacjach Update (wielokrotnych)
hook.If(
    hook.FixedError(errors.New("password nie może być edytowany przy aktualizacji wielu")),
    hook.And(
        hook.HasOp(ent.OpUpdate),
        hook.Or(
            hook.HasFields("password"),
            hook.HasClearedFields("password"),
        ),
    ),
)

Te funkcje pomocnicze pozwalają nam precyzyjnie kontrolować warunki aktywacji haków dla różnych operacji.

6. Haki Transakcyjne

Haki transakcyjne pozwalają na wykonanie określonych haków po zatwierdzeniu transakcji (Tx.Commit) lub jej wycofaniu (Tx.Rollback). Jest to bardzo przydatne w zapewnianiu spójności danych i atomowości operacji.

Przykład Haka Transakcyjnego

client.Tx(ctx, func(tx *ent.Tx) error {
    // Rejestracja haka transakcyjnego - hookBeforeCommit zostanie wykonany przed zatwierdzeniem.
    tx.OnCommit(func(next ent.Committer) ent.Committer {
        return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
            // Logika przed faktycznym zatwierdzeniem może być umieszczona tutaj.
            fmt.Println("Przed zatwierdzeniem")
            return next.Commit(ctx, tx)
        })
    })

    // Wykonaj serię operacji wewnątrz transakcji...

    return nil
})

Powyższy kod pokazuje, jak zarejestrować hak transakcyjny do wykonania przed zatwierdzeniem transakcji. Ten hak zostanie wywołany po wykonaniu wszystkich operacji na bazie danych i przed faktycznym zatwierdzeniem transakcji.