1. Wstęp do transakcji i spójności danych
Transakcja to logiczna jednostka w procesie wykonywania systemu zarządzania bazą danych, składająca się z serii operacji. Te operacje albo wszystkie się udają, albo wszystkie zawodzą i są traktowane jako nierozdzielna całość. Główne cechy transakcji można streścić jako ACID:
- Atomowość: Wszystkie operacje w transakcji są albo całkowicie wykonane, albo w ogóle nie zostały wykonane; częściowe wykonanie nie jest możliwe.
- Spójność: Transakcja musi przenieść bazę danych z jednego spójnego stanu do innego spójnego stanu.
- Izolacja: Wykonanie transakcji musi być odizolowane od ingerencji innych transakcji, a dane pomiędzy wieloma jednoczesnymi transakcjami muszą być izolowane.
- Trwałość: Po zatwierdzeniu transakcji, modyfikacje dokonane przez nią będą trwałe w bazie danych.
Spójność danych odnosi się do utrzymania poprawnego i ważnego stanu danych w bazie danych po serii operacji. W scenariuszach z udziałem równoczesnego dostępu lub awarii systemu, spójność danych jest szczególnie ważna, a transakcje zapewniają mechanizm, który gwarantuje, że spójność danych nie jest naruszona nawet w przypadku błędów lub konfliktów.
2. Przegląd frameworka ent
ent
to framework encyjny, który poprzez generowanie kodu w języku programowania Go zapewnia bezpieczne API typów do operowania na bazach danych. Dzięki temu operacje bazodanowe stają się bardziej intuicyjne i bezpieczne, umożliwiając uniknięcie problemów związanych z bezpieczeństwem, takich jak wstrzykiwanie SQL. W zakresie przetwarzania transakcji, framework ent
zapewnia silne wsparcie, umożliwiając programistom wykonywanie skomplikowanych operacji transakcyjnych za pomocą zwięzłego kodu i zapewnienie spełnienia właściwości ACID transakcji.
3. Inicjowanie transakcji
3.1 Jak rozpocząć transakcję w ent
W frameworku ent
, nową transakcję można łatwo zainicjować w określonym kontekście, używając metody client.Tx
, która zwraca obiekt transakcji Tx
. Przykład kodu wygląda następująco:
tx, err := client.Tx(ctx)
if err != nil {
// Obsłuż błędy podczas rozpoczynania transakcji
return fmt.Errorf("Wystąpił błąd podczas rozpoczynania transakcji: %w", err)
}
// Wykonywanie kolejnych operacji za pomocą tx...
3.2 Wykonywanie operacji w ramach transakcji
Po pomyślnym utworzeniu obiektu Tx
, można go użyć do wykonywania operacji na bazie danych. Wszystkie operacje tworzenia, usuwania, aktualizacji i zapytania wykonane na obiekcie Tx
staną się częścią transakcji. Poniższy przykład przedstawia serię operacji:
hub, err := tx.Group.
Create().
SetName("Github").
Save(ctx)
if err != nil {
// Jeśli wystąpi błąd, cofnij transakcję
return rollback(tx, fmt.Errorf("Nie udało się utworzyć grupy: %w", err))
}
// Można tutaj dodać dodatkowe operacje...
// Zatwierdź transakcję
tx.Commit()
4. Obsługa błędów i cofanie transakcji
4.1 Ważność obsługi błędów
Pracując z bazami danych, różne błędy, takie jak problemy z siecią, konflikty danych czy naruszenia ograniczeń, mogą wystąpić w dowolnym momencie. Prawidłowa obsługa tych błędów jest kluczowa dla utrzymania spójności danych. W transakcji, jeśli operacja zawiedzie, transakcja musi zostać cofnięta, aby zapewnić, że częściowo wykonane operacje, które mogą naruszać spójność bazy danych, nie pozostaną.
4.2 Jak zaimplementować cofanie
W frameworku ent
, można użyć metody Tx.Rollback()
do całkowitego cofnięcia transakcji. Zazwyczaj definiuje się funkcję pomocniczą rollback
, aby obsłużyć cofanie i błędy, jak pokazano poniżej:
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
// Jeśli cofnięcie zawiedzie, zwróć oryginalny błąd i błąd cofnięcia razem
err = fmt.Errorf("%w: Wystąpił błąd podczas cofania transakcji: %v", err, rerr)
}
return err
}
Dzięki tej funkcji rollback
można bezpiecznie obsługiwać błędy i cofanie transakcji, gdy dowolna operacja w ramach transakcji zawiedzie. Zapewnia to, że nawet w przypadku błędu nie będzie to miało negatywnego wpływu na spójność bazy danych.
5. Użycie klienta transakcyjnego
W praktycznych zastosowaniach mogą wystąpić sytuacje, w których potrzebujemy szybko przekształcić kod niedziałający transakcyjnie w kod działający transakcyjnie. W takich przypadkach możemy skorzystać z klienta transakcyjnego, aby bezproblemowo przenieść kod. Poniżej znajduje się przykład, jak przekształcić istniejący kod klienta niedziałającego transakcyjnie, aby wspierał transakcje:
// W tym przykładzie pakujemy oryginalną funkcję Gen w transakcję.
func WrapGen(ctx context.Context, client *ent.Client) error {
// Najpierw tworzymy transakcję
tx, err := client.Tx(ctx)
if err != nil {
return err
}
// Pobieramy klienta transakcyjnego z transakcji
txClient := tx.Client()
// Wykonujemy funkcję Gen przy użyciu klienta transakcyjnego, niezmieniając oryginalnego kodu Gen
if err := Gen(ctx, txClient); err != nil {
// Jeśli wystąpi błąd, cofamy transakcję
return rollback(tx, err)
}
// W przypadku powodzenia, zatwierdzamy transakcję
return tx.Commit()
}
W powyższym kodzie używany jest klient transakcyjny tx.Client()
, umożliwiając wykonanie oryginalnej funkcji Gen
podczas gwarancji transakcji. Ten sposób pozwala nam wygodnie przekształcić istniejący kod niedziałający transakcyjnie w kod działający transakcyjnie przy minimalnym wpływie na oryginalną logikę.
6. Najlepsze praktyki dotyczące transakcji
6.1 Zarządzanie transakcjami za pomocą funkcji wywoławczych
Gdy nasza logika kodu staje się złożona i obejmuje wiele operacji na bazie danych, scentralizowane zarządzanie tymi operacjami w ramach transakcji staje się szczególnie istotne. Poniżej znajduje się przykład zarządzania transakcjami za pomocą funkcji wywoławczych:
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
// Używamy defer i recover do obsługi potencjalnych scenariuszy wystąpienia panic
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
}()
// Wywołujemy dostarczoną funkcję wywoławczą w celu wykonania logiki biznesowej
if err := fn(tx); err != nil {
// W przypadku wystąpienia błędu, cofamy transakcję
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: cofanie transakcji: %v", err, rerr)
}
return err
}
// Jeśli logika biznesowa jest bezbłędna, zatwierdzamy transakcję
return tx.Commit()
}
Korzystając z funkcji WithTx
do owinięcia logiki biznesowej, możemy zapewnić, że nawet jeśli wystąpią błędy lub wyjątki w obrębie logiki biznesowej, transakcja będzie poprawnie obsługiwana (zatwierdzona lub cofnięta).
6.2 Użycie haków transakcji
Podobnie jak w przypadku haseł schematu i haseł czasu wykonania, możemy także zarejestrować haki w ramach aktywnej transakcji (Tx), które zostaną wywołane podczas Tx.Commit
lub Tx.Rollback
:
func Do(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
tx.OnCommit(func(next ent.Committer) ent.Committer {
return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// Logika przed zatwierdzeniem transakcji
err := next.Commit(ctx, tx)
// Logika po zatwierdzeniu transakcji
return err
})
})
tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
// Logika przed cofnięciem transakcji
err := next.Rollback(ctx, tx)
// Logika po cofnięciu transakcji
return err
})
})
// Wykonujemy inną logikę biznesową
//
//
//
return err
}
Dodając haki podczas zatwierdzania i cofania transakcji, możemy obsługiwać dodatkową logikę, taką jak logowanie czy oczyszczanie zasobów.
7. Zrozumienie różnych poziomów izolacji transakcji
W systemie baz danych ustawienie poziomu izolacji transakcji ma kluczowe znaczenie dla zapobiegania różnym problemom z równoczesnością (takim jak brudne odczyty, odczyty nierekurencyjne i odczyty upiory). Oto kilka standardowych poziomów izolacji i jak je ustawić w frameworku ent
:
- READ UNCOMMITTED: Najniższy poziom, umożliwia odczyt zmian danych, które nie zostały jeszcze zatwierdzone, co może prowadzić do brudnych odczytów, odczytów nierekurencyjnych i odczytów upiornych.
- READ COMMITTED: Umożliwia odczyt i zatwierdzanie danych, zapobiegając brudnym odczytom, ale odczyty nierekurencyjne i odczyty upiorne mogą wciąż wystąpić.
- REPEATABLE READ: Zapewnia, że odczytywanie tych samych danych wielokrotnie w ramach tej samej transakcji daje spójne wyniki, zapobiegając odczytom nierekurencyjnym, ale odczyty upiorne mogą wciąż wystąpić.
- SERIALIZABLE: Najbardziej rygorystyczny poziom izolacji, próbuje zapobiec brudnym odczytom, odczytom nierekurencyjnym i odczytom upiornym poprzez blokowanie zaangażowanych danych.
W ent
, jeśli sterownik bazy danych obsługuje ustawianie poziomu izolacji transakcji, można go ustawić w następujący sposób:
// Ustaw poziom izolacji transakcji na repeatable read
tx, err := client.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
Zrozumienie poziomów izolacji transakcji i ich zastosowanie w bazach danych jest kluczowe dla zapewnienia spójności danych i stabilności systemu. Deweloperzy powinni wybrać odpowiedni poziom izolacji w oparciu o konkretne wymagania aplikacji, aby osiągnąć najlepsze praktyki zapewnienia bezpieczeństwa danych i optymalizacji wydajności.