Jak sprawdzać poprawność nadesłanych danych i zwracać odpowiedź lub błąd

0

Mam aplikacje napisaną w spring-boot oraz frontend w angularze. Do tej pory jak chciałem sprawdzić poprawność nadesłanych danych robiłem to w serwisie a zwracanym obiektem był zawsze wrapper (dwa pola: data - zwracany obiekt, status - wynik operacji). Np. przy tworzeniu użytkownika wykryłem że login jest zajęty to zwracałem: {data: null, status "USER_LOGIN_IS_TAKEN"} a jeśli wszystko OK to zwracałem: {data: {USER_OBJECT},status "OK"}. Czyli http code był zawsze 200. We froncie moge z łatwością wyświetlać odpowiednie komunikaty np. w formularzach.

Dzisiaj przeczytałem te dwa artykuły:
https://restfulapi.net/http-methods/
https://www.baeldung.com/javax-validation

Według tych artykułów moje rozwiązanie nie jest dobre ponieważ powinienem operować na http code oraz mogę (chce) wykorzystać javax-validation.
W 2. artykule jest opisane jak zrobić walidacje czy pole jest uzupełnione itp. ale nie ma np. czy taki użytkownik istnieje.

Dalsze rozważania ponumeruje żeby łatwiej się odpowiadało:

  1. Walidacja
    Czyli tak dodaje adnotacje i w kontrolerze daje @Valid. To mi sprawdzi tylko te podstawowe rzeczy jak np. czy nie jest null itp.
    Potem muszę w serwisie sprawdzić czy taki użytkownik istnieje. Chciałbym dostać jakiś unikatowy błąd (o tym w punkcie 2).
    Tutaj pytanie jak to robić? - bo widzę dwie opcje:
  • Strzelam do bazy sprawdzam jak jest zwracam błąd a jak nie ma to strzelam drugi raz żeby dodać
  • Próbuje zapisać użytkownika dostaje exception (bo jest constrain na bazie) - jak go złapać? Dostane jakiś konkret żebym mógł stwierdzić co poszło nie tak? Jakie exceptiony mogę dostać? - gdzie znajdę ich liste?
  1. Zwracany błąd
    Według pierwszego artykułu mam zwracać odpowiedni kod. Chciałbym do tego też zwracać odpowiednią wiadomość np. "USER_LOGIN_IS_TAKEN".
    Tutaj dwa pytania:
  • Jak zwracać taką odpowiedz?
  • Jak ustawiać status

Chwiałbym osiągnąć coś takiego że zwracam normalne dto czyli od razu użytkownika jeśli jest OK. A jak jest jakiś błąd to chciałbym dostać informacje co poszło nie tak.

  1. Przy okazji zapytam jeszcze o nazewnictwo url przy rest
    Chodzi mi o jeden przypadek.
    Użytkownika można aktywować i dezaktywować. Jest to zmiana wartości pola status.
    Pytania:
  • Jak metoda?
  • Jaki url?
  • jedna metoda - changeStatus/editUser / czy dwie - activate() / deactivate()

Mogę to potraktować jako edycje użytkownika i uzyć PUT i wsyłać całego użytkownika na 'users/{id}' albo potraktować to jako edycje częściową i wysłać tylko część - wtedy było by to PATCH i wysyłam tylko status pod 'users/{id}' albo 'users/{id}' (hmm jak to zwaliduję? Jak zapisze tzn wypada zrobić ładny update ale to tego potrzebuje całego użytkownika? )
Mam nadzieję że ktoś znajdzie chwilke czasu na odpowiedz. Z niecierpliwością czekam na odpowiedź

0

1.To tak: według mnie np. to, że taki użytkownik istnieje w już np. w bazie nie jest sytuacją aż tak wyjątkową żeby rzucać do niej Exception - lepiej chyba zwrócić odpowiedni kod, np. Either<UserError, User>

Strzelam do bazy sprawdzam jak jest zwracam błąd a jak nie ma to strzelam drugi raz żeby dodać

Czemu drugi raz strzelasz? Nie lepiej obsłużyć to takim właśnie Either? Jeśli istnieje to zwróć UserError (jakiś opakowany http code np.), a jeśli nie istnieje to stwórz i zwróć User.
3.
Według mnie lepiej to rozdzielić na dwie activate/deactivate, a umieścić je w Twoim obiekcie domenowym.

0
  1. @Valid sprawdzi podstawową poprawność requestu, Spring automatycznie zwróci status 4xx. Pamiętaj, że statusy HTTP z rodziny 4xx oznaczają, że błąd jest po stronie klienta i ponowienia nic pomogą. Tutaj lista statusów: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
  2. Wyjątki z niższych warstw, np. warstwy dostępu do danych, repozytoriów powinieneś i tak przechwytywać i rzucać własny wyjątek "biznesowy" (do domeny nie powinny przedostawać się wyjątki "infrastrukturalne"). Taki wyjątek możesz złapać np. w @ControllerAdvice i tam zmapować go na zwrotkę z odpowiednim statusem HTTP oraz body: https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Przykład:

 @ResponseStatus(HttpStatus.BAD_REQUEST)  // 400
 @ExceptionHandler(UserLoginAlreadyTakenException.class)
 public UserCreationFailureResponse handleUserLoginAlreadyTaken(UserLoginAlreadyTakenException ex) {
        return new UserCreationFailureResponse("USER_LOGIN_IS_TAKEN", ex.getLogin());
 }
  1. PUT /users bym wykluczył, ponieważ idziesz w kierunku CRUD-a na userze. Po co masz komunikować światu, że user ma pole status? Musi mieć? Druga kwestia - security w obu przypadkach będzie różne. Dałbym 2 endpointy POST /users/{userid}/activation i POST /users/{userid}/deactivation.

  2. Kwestia użycia Either, Try, Optional lub innego typu zamiast wyjątku - i tak na warstwie weba musisz to coś przemapować na status HTTP i body.

0
Charles_Ray napisał(a):
  1. Kwestia użycia Either, Try, Optional lub innego typu zamiast wyjątku - i tak na warstwie weba musisz to coś przemapować na status HTTP i body.

NIby tak. Ale jak używasz funkcyjnego WebFlux. Ratpack czy nawet SparkJava to Either jest bardziej naturalny.
@Valid oczywiście do tego w ogóle nie pasuje.

0

Zgoda, ale wówczas wychodzimy od stylu architektonicznego, gdzie obsługa błędów jest jego wypadkową, a nie odwrotnie. Kolega pyta o obsługę błędów w Springu, więc narzucanie odmiennego stylu może być overkillem (nikt nie przesiądzie się na webfluksa z dnia na dzień)

0

Kolega pyta o obsługę błędów w Springu, więc narzucanie odmiennego stylu może być overkillem (nikt nie przesiądzie się na webfluksa z dnia na dzień)

Nieprawda, wystarczy że sprawdzi się wartość Eithera w zależności od ResponseBody zwróci się co innego.
Overkilem dla serwera są wyjątki które nie sa wyjątkami, i overkilem jest szukanie po kodzie gdzie ten wyjątek mógł być rzucony, bo tak masz jasne wywołanie kolejnej metody po metodzie

0

@Charles_Ray: ale czy naprawdę to, że w bazie istnieje już taki user to jest sytuacja aż tak wyjątkowa by powodowała ona Exception?

0

Podaj definicję „sytuacji aż tak wyjątkowej” :) z pragmatycznego punktu widzenia, zakładając, że robisz założenie usera atomowo, ze sterownika poleci coś w stylu ConstraintViolationException. Opakowanie tego w Either/Mono/Observable/... jest dla mnie wtórne, oby pasowało do architektury aplikacji. Możemy się spierać na ile jest to sytuacja wyjątkowa - będę adwokatem diabła i na potrzeby dyskusji napisze, ze dla mnie jest to wystarczająco wyjątkowa sytuacja, aby szczeknąć błędem.

0

Większość sytuacji w warstwie aplikacyjnej z założenia nie może być wyjątkowa, bo się ich spodziewasz (co wynika z tego, że zeby coś w tym przypadku uznać za "nieodpowiedniego" jak np duplikacja userów musisz mieć wymaganie które zabrania takiej sytuacji (więc się tego spodziewasz ;) )

0

Wyjątkowa sytuacja to może być np. brak możliwosci wysłania requestu do 2 serwisu bo padł, albo to że przeszło przez aplikacje rejestracja i nazwa użytkownika jest już zajęta mimo że teoretycznie była wcześniej walidacja :)
Próba zajęcia loginu który już istnieje nie jest wyjątkowa

0

@danek: a jak to zrobić, żeby mieć pewność, że login nie jest zajęty?

0

@Charles_Ray: trochę źle się może wyraziłem. Sprawdzasz to jak chcesz, kwestia rozbija się tylko o to jak zakomunikujesz rezultat (wyjątek vs np Either), ale sam fakt ze sprawdzasz coś takiego oznacza, że nie jest to dla Ciebie coś wyjątkowego ;)

0

Tu nie chodzi o fakt, żeby mieć pewność, że login nie będzie zajęty. @danek bardziej chodzi o to, że Twoja aplikacja ma wymaganie aby login był unikalny - tak więc spodziewasz się tego i w żaden sposób nie jest to wyjątkowa sytuacja. Ba, jest to jedno z wymagań, więc lepiej user'a potraktować wyłącznie komunikatem, nie sądzisz?

0

Komunikat to kwestia rozegrania tego na frontendzie przecież :D

0

No racja, źle się wysłowiłem - bardziej chodziło mi o fakt żeby nie traktować go Exceptionem tylko żeby nasza aplikacja potraktowała to jako normalny flow.

0

Front i tak dostanie tylko odpowiedź na requesta nie wiedząc co się wcześniej działo

1

Wątek o prostej rejestracji użytkownika ma już 2 strony. Strach pomyśleć ile czasu programistów jest przepalonego na takie dywagacje, zamiast ogarnąć problem „standardowo”. Używasz Springa? Zrób to korzystając ze Springa. Masz Vavra? Użyj Either. Prosta sprawa.

To tylko takie moje luźne spostrzeżenie. Generalnie popieram nieużywania wyjątków do sterowania flow programów, ale litości :) a potem sami seniorzy po 2 latach programowania, którzy brzydzą się wyjątkami, a kolejny sprint uwalony, bo wpinamy rxJave albo Vavra

0

To o seniorach po dwóch latach to jakaś uszczypliwość czy coś? To co robię dla siebie, do szuflady wolę robić tak, w pracy w której jestem dwa miesiące wszystko jest sterowane wyjątkami - nie zbyt to ułatwia pracę, ale skoro tak to stoi to jako junior nie będę się wychylał.

To, że wszystkoi trzeba dostosować do tego co się obecnie ma to prawda, ale jeśli gadamy luźno w temacie o poprawności danych gdzie ktoś tworzy projekt zapewne dla siebie i nie jest to coś wielkiego to chyba warto przedstawić różne wizje.

0

Jestem zaskoczony ilością odpowiedzi ale mam wrażenie że trochę popłynęliście :p

Wróćmy do sedna. Co chce osiągnąć?
Chce w łatwy sposób walidować dane, które przychodzą i sprytnie zwrócić ewentualne błędy do frontu.
Walidacje możemy podzielić na dwie części:

  1. wstępną - mam tu na myśli sprawdzenie czy pole nie jest null'em itp
  2. Tą dokładniejszą która wymaga sprawdzenia np. czy to co próbuje dodać nie naruszy constrainów na bazie.

Sprawdzenie tego pierwszego nie jest trudne ale z tym drugim nie wiem jak fajnie zrobić.

Pisaliście coś o: Either, Try, Optional. Nie wiem co to Either i Try (Tylko jak strukture Try-Catch znam)

Wstawię kod będzie łatwiej. Pisałem to na szybko i nie odpalałem, chodzi mi o same ramy. Prosiłbym tylko komentować to czego dotyczy ten post a uwagi typu że lepiej wstrzykiwać zależności przez konstruktor sobie darować :p
Więc mam taki kontroler:

@Data
public class UserDto implements Serializable {
    private Integer id;

    @NotNull
    @NotEmpty
    private String name;

    //todo coś tam dalej
}

@Valid sprawdzić podstawową walidacje. Tylko kiedy wystąpi błąd chciałbym swój obiekt błędu.

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping()
    public Integer createUser(@RequestBody @Valid UserDto userDto) {
        return userService.createUser(userDto);
    }
}
@Component
public class UserService {

    @Autowired
    private UserRepository userRepository;

    /**
     * Wersja że sprawdzam wszystko sam
     */
    public Integer createUser(UserDto userDto){
        // sprawdzam wszystkie przypadki
        // i np robie coś takiego
        Optional<User> byName = userRepository.findByName(userDto.getName()); //chciałbym tego uniknąć tzn. bić do bazy tylko raz

        if(byName.isPresent()){
            //no i tutaj zwracam błąd że taki uzytkownik istnieje
        }

        // następnie sprawdzam jakąś inną rzecz i też bym chciał zwrócić jakiś błąd


        //i w końcu zapisuje bo jestem pewny że nie wybuchnie
        User user = new User(null, userDto.getName());

        userRepository.save(user);
        return user.getId();
    }

    public Integer createUser2(UserDto userDto){
        User user = new User(null, userDto.getName());

        userRepository.save(user);
        // i np tutaj poleci ConstraintViolationException i musze to obsłużyć żeby ładnie zwrócić coś do frontu
        // ale obstawiam ze Exceptiony beda jakies ogolne i nie bede wiedzial co konkretnie poszło nie tak
        return user.getId();
    }
}

Tworząc użytkownika (jest on tu tylko dla przykładu - to może być cokolwiek), zwracam id (tu też może być cokolwiek np. userdto). To ma się wydarzyć jak wszystko poszło ok.
Ale jak wystąpi błąd chciałbym dostać dokładną informacje co poszło nie tak (taki swój obiekt błędu) i prosiłbym o przykłady jak to zrobić w springu bez jakiś dodatkowych rzeczy. Bo to co dodaliście to pierwszy raz słyszę.

Chyba w tej odpowiedzi jest to o co mi chodzi ale nie do końca czuje to :/

Charles_Ray napisał(a):
  1. @Valid sprawdzi podstawową poprawność requestu, Spring automatycznie zwróci status 4xx. Pamiętaj, że statusy HTTP z rodziny 4xx oznaczają, że błąd jest po stronie klienta i ponowienia nic pomogą. Tutaj lista statusów: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
  2. Wyjątki z niższych warstw, np. warstwy dostępu do danych, repozytoriów powinieneś i tak przechwytywać i rzucać własny wyjątek "biznesowy" (do domeny nie powinny przedostawać się wyjątki "infrastrukturalne"). Taki wyjątek możesz złapać np. w @ControllerAdvice i tam zmapować go na zwrotkę z odpowiednim statusem HTTP oraz body: https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Przykład:

 @ResponseStatus(HttpStatus.BAD_REQUEST)  // 400
 @ExceptionHandler(UserLoginAlreadyTakenException.class)
 public UserCreationFailureResponse handleUserLoginAlreadyTaken(UserLoginAlreadyTakenException ex) {
        return new UserCreationFailureResponse("USER_LOGIN_IS_TAKEN", ex.getLogin());
 }
  1. PUT /users bym wykluczył, ponieważ idziesz w kierunku CRUD-a na userze. Po co masz komunikować światu, że user ma pole status? Musi mieć? Druga kwestia - security w obu przypadkach będzie różne. Dałbym 2 endpointy POST /users/{userid}/activation i POST /users/{userid}/deactivation.

  2. Kwestia użycia Either, Try, Optional lub innego typu zamiast wyjątku - i tak na warstwie weba musisz to coś przemapować na status HTTP i body.

Bardzo bym prosił o jakiś taki cały przykład. Controller + Serwis + inne potrzebne klasy <3

0
Charles_Ray napisał(a):

Wątek o prostej rejestracji użytkownika ma już 2 strony.

Bo to nie jest już wątek o rejestracji użytkownika tylko o tym jak zwracać informacje o błędzie ;)

Tak samo powiedzenie "Używasz Springa? Zrób to korzystając ze Springa. Masz Vavra? Użyj Either. Prosta sprawa." też jest pewnym uproszczeniem bo co znaczy "używasz". Jeśli Twoja domena wie, o tym co używasz na zewnątrz (czyli spring) to raczej nie za dobrze.

@shimizu
Przeczytaj mój jeden wpis o podobnej tematyce i jeszcze inny

Ewentualnie cały projekt z tym

0

Pisałem o mapowaniu wyjątku na status HTTP, w Springu możesz to zrobić poza kodem domenowym. To dalej jest „Spring-way”.

1 użytkowników online, w tym zalogowanych: 0, gości: 1