Wysłanie maila po aktualizacji statusu w bazie

0

Hey, temat wydaje się dość powszechny i zastanawiam się jak do tego podchodzicie/co googlować.

Stack: JPA + Spring

Problem:
Wyślij mail gdy dane pole w encji zostanie zaktualizowane. Np status płatności. Status ten może być aktualizowany przez różne klasy (powszechnie znane jako OrderServce, UpdateOrderUseCase, ChangeOrederUseCase, czy inne takie w zależności od kontekstu). Mail oczywiście powinien być wysłany gdy zapytanie wykona się na bazie - tj. gdy transakcja się zakończy i wiemy że wszystko cacy na bazie.

rozwiązanie ChatGPT to @EntityListeners, z

public class OrderListener {

    @PreUpdate
    public void beforeUpdate(Order order) {
        // Assuming you have a way to fetch the previous state (like with `@Version` or by querying the DB)
        Order oldOrder = getPreviousOrder(order.getId());

        if (!Objects.equals(oldOrder.getStatus(), order.getStatus())) {
            // If status has changed, publish the event
            ApplicationContextProvider.getApplicationContext()
                .publishEvent(new OrderStatusChangedEvent(order));
        }
    }

    private Order getPreviousOrder(Long orderId) {
        // Fetch the previous order state from the DB (for comparison)
        // Typically, you'd use a repository to retrieve the old state
        return orderRepository.findById(orderId).orElseThrow();
    }
}

Wydaje się sensowne, ale troche mierzi getPreviousOrder
Jest to dobre rozwiązanie czy polecacie inne podejści ?

2

Jest jeszcze coś takiego jak Change Data Capture:
https://ishansoninitj.medium.com/change-data-capture-cdc-using-debezium-in-a-springboot-application-97ddde8b991a

Nie używałem ale słyszałem, że nie polecane.
Ogólnie zapinanie się na gołą tabelę/kolumnę jest słabe bo to mocny coupling.
Imho lepiej byłoby gdyby właściciel tabeli emitował eventy o zmianie ale to już kwestia projektu czy macie w infrze brokera wiadomości, czy tabelka ma właściciela, czy tylko włąściciel w niej zmienia wartości itp itd (suma sumarum sprowadza się to do słabego desingu z couplingiem na poziomie bazy danych ale w zastanych systemach wiadomo jak jest) ;)

Co do rozwiązania które zaproponował (nie znam go) to upewniłbym się czy nie działą to (nie zdziwiłbym się) tylko w obrębie zmian przeprowadzanych w ramach tej samej aplikacji (inaczej Twoja aplikacja i tak musiałaby jakoś reagować na zmiany spowodowane przez inną aplikację na tej samej tabelce).

1

Zapinasz się na afterCommit transakcji i emitujesz jakiś event na kolejkę. Możesz też wykorzystać outbox pattern.

1

Jakbym miał to robić na szybko i bez zastanowienia to bym zrobił trigger na tabeli Order (który wstawia wiersze do tabeli OrderEvents). Jest wtedy bezpiecznie nawet jak to pan Mietek admin zmieni wiersz w bazie. W aplikacji byłby job który czesze tabelkę co minutę, wydobywa eventy i przekazuje do handlerów. Wymaga to trochę pracy, ale często firmy mają już gotowe rozwiązania na tego typu problem.

Wada: część logiki siedzi w DB a nie w kodzie. Trzeba pamietać o uaktualnieniu triggera jeżeli struktura tabeli się zmieni (choć nie zawsze). Jest duży czynnik zaskoczenia, coś dzieje się "magicznie". Baza może zdechnąć jak ktoś zrobi update na całej tabeli.

Order - zwykłe POJO, w każdym setterze wywołujesz metodę <T> onUpdate(String propertyName, T oldValue, T newValue) W tej metodzie generujesz eventy i dodajesz do ukrytego pola List<OrderEvent> events przy zapisie używasz interceptora Hibernate żeby zapisać eventy w bazie (https://www.baeldung.com/hibernate-interceptor) lub robisz to po prostu w metodzie save(Order o) an repo. Wada: biblioteki do mapowania etc. mogą wywoływać settery. Nie masz dostępu do oryginalnego obiektu. Przy magii takiej jak w Hibernate może nie działać. To podejście działa świetnie gdy używasz EvenSourceingu - oprócz eventów "storage"owych będą też eventy powiadamiające inne części systemu typu OrderChanged.

Kolejna możliwość to wstrzyknąć do encji EventPublisher i publikować eventy przy zmianie stanu encji. Znów działa najlepiej jak się używa podejścia DDD i agregatów.

Twój przypadek to jest niestety CRUD (goła anemiczna encja). Użycie interceptorów to jest jakaś tam opcja: https://vladmihalcea.com/hibernate-event-listeners/ (ten gość się zna).

Jakbym sam to pisał to pewnie zrobił bym jeszcze inaczej: w tych miejsach gdzie order jest jawnie zmieniany publikował bym event:

order.markShipped();
eventPublished.publish(new OrderShippedEvent(...));
// eventualnie
order.markShipped(eventPublisher); // publishes events inside
// will not work when an uncivilised savage does
order.setStatus(SHIPPED);
// that's why I have said it works best with DDD when you can hide most of the setters.

Event publisher mógłby zapisywać eventy w bazie lub wrzucać na jakiegoś message bus'a typu Kafka.
Nie jest to ładne bo najlepiej jak to obiekt sam zarządza swoimi eventami ale oszczędzi męki z modyfikacją 40+ setterów.

Generalnie jeżeli to jest tylko kilka znaczących pól to szedł bym w rozwiąznaie pure Java - wszystko w kodzie na POJO i jawne zapisywanie eventów do bazy na repo.save(...) oraz job do czesania i wykonywania eventów + deadletter (pogoogaj to się dowiesz).

1

Sensownym też wydaje się programistyczne opakowanie transakcji i ich kontekstu, wtedy można zrobic wrappera / utilsa do przekazania jakiegoś Suppliera do afterCommit naszego, który wykona się po commicie transakcji. Wtedy spokojnie możemy opublikować w dowolny sposób event o czymś, i w danych miejscach na ten event nasłuchiwać.

0

Dzięki wszystkim za pomoc ! ❤️

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.