Uniwersalne DTO?

0

Witam,

Borykam się z takim małym problemem, otóż, korzystam ze Spring boot'a + Hibernate, przekazuję do modelu obiekt Usera, który ma powiązania z innymi encjami.
Użytkownik edytuje pola Usera (np zmienia dane osobowe etc) i w metodzie POST łapię ten obiekt, jednak encje, które były w relacji z tym obiektem, mają wartości null.

Znajomy zaproponował mi stworzenie klasy DTO, która będzie posiadała wszystkie pola głównej encji oraz tych encji, które zawiera - mówiąc prościej stworzyć klasę, która będzie miała +30 pól/getterów/setterów.
Od razu mi się to nie spodobało i szukam sposobu, jak stworzyć uniwersalną klasę DTO, która nie będzie tak obszerną klasą i dodatkowo będzie mogła być używana wielokrotnie, nie tylko dla encji User.

Czy są jakieś gotowe rozwiązania (biblioteki, mappery) ?
A może istnieje rozwiązanie, jak przekazać taki obiekt do modelu, a po "zwróceniu" tego obiektu wszystkie relacje zostaną zachowane?

Próbowałem stworzyć klasę DTO, która zawiera Map<String, String> properties, a w encji, sobie tworzyłem taką mapę, miałem z tym jednak problemy i porzuciłem pomysł - czy może wcale nie był taki głupi?

0

lepiej tak nie robić jak kaliega polecił
jedna encja - jeden DTOs
a jak chcesz sobie ułatwić trochę życie z getterami/setterami, to może skorzystaj z Lomboka

0

To nie ma sensu, zrób jedną DTO na jeden dany endpoint czy cokolwiek co tam masz. A poza tym sądze że settery musza być zniszczone

0

Tylko problem jest taki, że mam w encji Foo obiekt encji Bar. I w metodzie post po wypełnieniu formularza przez uzytkownika, w encji Foo obiekt Bar jest nullem.

A do tego modelu przekazuję obiekt (w metodzie GET) i on posiada ten obiekt, a z powrotem dostaje z nullami

0

To znaczy że masz coś nie tak zmapowany DTOs. Jutro pokaże kod jak prawilnie to zrobić żeby się to dobrze mapowało

0
@Entity
class User {
    private String login;
    private String password;
   
    @OneToOne
    @JoinColumn("id")
    private UserDetails userDetails;
   
    // getters and setters
}
 
@Entity
class UserDetails {
    private String name;
    private String surname;
    private String email;
    private String address;
    private String country;
    private String postCode;
    private String city;
    private String pesel;
 
    @OneToOne
    @JoinColumn("id")
    Employee employee;

    @OneToMany
    @JoinColumn("id")
     List<WorkTime> worktime;
 
    // getters and setters
}

@Entity
class Worktime {
    private Date dateIn;
    private Date dateOut;
    private Integer hours;

    @ManyToOne
    @JoinColumn("id"
    Employee employee;

    // getters and setters
}

Wrzucam dla przykładu taki pseudokod, który ma tylko zobrazować +/- co mam na myśli. Mam encję User, w której trzymam tylko najważniejsze informacje na temat usera, w innych encjach, trzymam bardziej szczegółowe informacje.

@GetMapping("/edit_user/{id}")
public String editUser(@PathVariable Long id, Model model) {
    model.addAttribute("user", userService.getById(id));
    return "editUser";
}
 
@PostMapping("/edit_User")
public String editUser(@Valid @ModelAttribute("user") User user,
                BindingResult result, ModelMap model)  {
    userService.saveOrUpdate(user); // Tutaj obiekt user, jest całkiem inny,
                                                          //  niż ten który przekazałem do modelu w GET
    return "redirect:/home";
}
0

@wyebani: nie powinno się wrzucać encji JPA do warstwy API... ona nie powinna nigdy opuszczać warstwy logiki biznesowej!

0
scibi92 napisał(a):

@wyebani: nie powinno się wrzucać encji JPA do warstwy API... ona nie powinna nigdy opuszczać warstwy logiki biznesowej!

Więc jak rozwiązać ten problem, jak poprawnie zaimplementować edycję użytkownika?

2

To co robisz nie ma nic wspólnego z DTO. Jak sama nazwa wskazuje DTO to Data Transfer Object a więc obiekty służące do wysyłania jakichś tam danych pomiędzy frontem a backendem lub między innymi usługami. To co napisał @scibi92 ma sens ponieważ w Twoim przypadku Twój frontend jest powiązany z backendem i dlatego masz takie problemy. Dodatkowo zauważ że wystarczy że będziesz chciał zmienić jakiś formularz czy cokolwiek na froncie i automatycznie twoja encja bazodanowa też będzie musiała być zmieniona a więc modyfikując frontend automatycznie musisz modyfikować bazę danych. Bezsensu.

Rozwiązaniem jest odseparowanie jednego od drugiego. Frontend sobie a logika sobie. Frontent niech ma formularze, tabelki i inne takie zbudowane w taki sposób jak tego potrzebujesz i na tę potrzeby dodawaj sobie nowe DTO... Np. masz rejestrację użytkownika więc potrzebujesz powiedzmy hasło i powtórzone hasło aby to zwalidować. Podobnie w powtarzaniem maila... Na backendzie w bazie danych tego nie potrzebujesz więc tworzysz sobie np. RegisterAccountDto który ma wszystkie potrzebne dla Ciebie pola i taki też obiekt przyjmujesz w kontrolerze. Kontroler robi co ma tam zrobić a następnie na podstawie dto robiony jest obiekt domenowy np. Account, User czy co tam potrzebujesz. Dopiero taki User trafia do bazy (inna sprawa że logika biznesowa też nie do końca powinna być zależna od bazy danych ale już to bym tutaj pominął).
Natomiast w drugą stronę możesz robić bardzo podobnie. Jeśli chcesz wyjąć coś z bazy danych i wyświetlić na froncie to też nie potrzebujesz przepychać pół bazy tylko po to aby wyświelić coś na stronie (np. Witaj jkowalski). Możesz skorzystać z projekcji albo innych mądrych rad poleconych w tym temacie Konwersja Entity na wiele DTO

W skrócie, porzuć pomysły z jakimiś generykami dto, utwórz prawdziwe dto, odseparuj to od logiki i dodaj jakiś interfejs komunikacyjny między nimi (jakiś serwis, fasade lub cokolwiek podobnego) i będzie śmigać.

0

@eL: Dzięki za odpowiedź. Zdaje sobie sprawę, że te rozwiązanie jest nie dobre. Jest to w sumie mój pierwszy projekt WEB i w dodatku oparty o frameworki (Spring Boot+Hibernate).
Przemyślę sprawę czy da się to co do tej pory napisałem, jakoś rozwiązać w miarę "bezboleśnie". Tak jak pisałem, to mój pierwszy projekt więc jeśli chodzi o architekturę to zdaje sobie sprawę, że robi się to inaczej.

Jeszcze raz dzięki.

0
wyebani napisał(a):

Przemyślę sprawę czy da się to co do tej pory napisałem, jakoś rozwiązać w miarę "bezboleśnie". Tak jak pisałem, to mój pierwszy projekt więc jeśli chodzi o architekturę to zdaje sobie sprawę, że robi się to inaczej.

Jak dla mnie to masz tu całkowicie bezbolesny refaktoring. Masz jakiś tam serwis z tego co widzę który przyjmuje entity, kontrolery które obsługują widoki. Potrzebujesz jedynie 2 rzeczy - DTO + jakiś parser który będzie konwertował DTO na Entity... To wszystko, W najprostszej postaci będą do 2 klasy (Dto i konwerter) + 1-2 linijki dodatkowo w kontrolerze.

0

@jarekr000000: coś chyba wspominałeś że masz jakieś rozwiązanie które można stosować zamiast pieczołowitego mapowania DTO na encję i z powrotem.
Lombok umożliwia skrócenie zapisu, ale nadal mamy dwie różne klasy (DTO na zewnątrz, encje w modelu).
https://www.baeldung.com/intro-to-project-lombok
Czy jest coś co samo mapuje między tymi dwoma rodzajami klas? A może nie potrzeba dwóch rodzajów klas w tym podejściu o którym wspominałeś.
No chyba, że pisałeś o NoDB, to wtedy wszystko jasne.

1

NIe mam niczego magicznego.
Z zasady nie mam regół typu 1 DTO dla jednego Entity. Uważam takie podejście za strasznie głupie (taką regułe). Czasem wychodzi, że jest rzeczywiście takie odwzoowanie i jakiś mapping, ale to przypadek, a nie reguła. Normalnie jest Wiele DTO do wielu obiektów Domenowych/ bazodanowych.

  1. Po prostu zaczynam od Dto, dlatego nie mam klasycznego (ProductDto <-> Product) tylko raczej (Product <-> ProductDB). (Formalnie to podejście api driven/ client first. (Czy też "z bazą danych w d..ie"). Można nawet mocniej powiedzieć, że z domeną w głebokiej pogardzie, ale jeśli robimy serwer to z zasady ma on służyć.

  2. Wszystkie te obiekty są niemutowalne więc nie ma szkód w robieniu skrótów ProductDB == Product (w kotlinie można zrobić typealias). Można to nazywać encja na twarz i jest super :-). Przez jakis czas daje rade zwykle. (btw. niemutowalne Entity działają też z JPA choć są dziwaczne).

  3. Czasem bawie sie w interfejsy czy dziedziczenie ProductDB extends/ implements Product. Wtedy przerzucam przez warstwy fizycznie ten sam obiekt tylko z różnymi interfejsami (np. w niektórych jest mniej pól).

  4. Wspomniane już select new T() na JPA też ułatwia. Tak samo w JOOQ od razu wyciągam widoki (czyli Dto).

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