Struktura aplikacji web, hibernate lazy Initialization

0

Witam,

Mam zamiar zacząć pisać prostą aplikację webową opartą na Spring MVC i Hibernate. Mam kilka pytań odnośnie struktury modelu danych oraz Hibernate.

Co do struktury będzie ona dosyć standardowa:

  • encje jpa,
  • klasy DAO (@Repository),
  • serwisy (@Transactional, @Service),
  • controllery spring
  • widok - strony jsp.
  1. Zastanawiam się nad modelem danych przeznaczonym do komunikacji z widokiem. Aktualnie przychodzi mi do głowy albo przesyłanie obiektów encji (Czy to nie będzie mieszanie warstw aplikacji?) albo utworzenie specjalnych obiektów DTO i wrzucenie tam tylko potrzebnych danych (z tym, że trochę szkoda roboty na przepisywanie kodu) lub może utworzenie controllerow restowych. Jakie waszym zdaniem jest najlepsze rozwiązanie?

Teraz odnosnie Hibernate:

bazując na przykladowym modelu:


@Entity
@Table(name = "apps")
public class App {
    @Id
    @GeneratedValue
    private int id;
    private String city;
    @OneToMany(mappedBy = "app", cascade = CascadeType.ALL)
    private Set<User> users = new HashSet();

    //settery, gettery
}

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue
    private int id;
    private String name;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private Set<Phone> phones = new HashSet();
    @ManyToOne
    @JoinColumn(name = "app_id")
    private App app;
    
    //gettery, settery

}

@Entity
@Table(name = "phones")
public class Phone {

    @Id
    @GeneratedValue
    private int id;
    private String number;

    //settery, gettery
}

W celu uniknięcia LazyInitializationException wykonuję fetch joiny. I teraz tak (pewnie to jest głupie pytanie):

  1. Załóżmy, że chce pobrac sobie obiekt App o określonym identyfikatorze dołączając do niego zbiór Userow ALE WYKLUCZAJĄC WSZYSTKIE RELACJE w obiekcie User ( w tym przypadku Set<Phone>). Jak to najprościej zrobic?

Dlaczego o to pytam. Załóżmy że pobiorę obiekt App (dla powyższych warunkow) wyjdę nim poza warstwę serwisową, czyli poza sesje hibernate i niechcący pobiorę sobie Zbior Set<Phone> z obiektu User. Wtedy poleci LAzyInitializationException - tego chce uniknąć.

Ale może coś zle rozumiem?

0
  1. DTO, na pewno nie Encje. Z przepychaniem encji mogą być różne dziwne problemy bo na przykład chcesz sobie przygotować w serwisie dane do wysłania do widoku i coś tam mieszasz a zapomnisz że masz transakcje na tym serwisie i nagle skasowałeś sobie pół bazy(tak tak, widziałem takie akcje ;) )
  2. Nie bardzo rozumiem co to znaczy "niechcący". Albo potrzebujesz tych danych i robisz fetch join, albo nie. Poza tym jak przepiszesz potrzebne dane do DTO to problem znika bo nie będzie tych danych do których mógłbyś się "przypadkiem" odwołac.
0
  1. Tak też myslalem ze stanie na DTO. W jaki sposób najlepiej tworzyć obiekty DTO, jest jakis sprawdzony pattern? Utworzyć sobie jakis interfejs który bedą implementowaly serwisy (Interfejs do mapowania dto -> encje, encje na dto) i zwracaly juz przemapowane obiekty na DTO do kontrolera? Czy może konstruktory w w klasach przyjmujące odpowiednio encje lub dto inicjalizujace obiekt? Czy tez tworzyć obiekty tylko w razie potrzeby?

2."Poza tym jak przepiszesz potrzebne dane do DTO to problem znika" - tak w tym przypadku problem się rozwiązuje.

0
  1. A może mappery do przepisywania encja -> dto, dto -> encja? np. ModelMapper, Dozer
0

myślałem o tym ale jednak nie chce angażować dodatkowych frameworkow (tym bardziej generatorów kodu, nie przepadam za nimi). Poniżej napisze na co się zdecydowałem może się komuś przyda.

frameworki: Spring MVC, Hibernate, Maven

Komunikacja z bazą: encje JPA, warstwa dao

Komunikacja controllera z modelem z biznesowym (baza) poprzez serwisy i tutaj ustaliłem że zrobie to w taki sposób:

  • Utworzę klasy DTO (posiadające tylko wymagane pola i gettery, obiekty będą readonly, plus odpowiednie konstruktory przyjmujące encje i inicjalizujące obiekty dto) odpowiednio dla każdej encji która będzie się komunikowała z widokiem (JSP).
  • Będę szedł raczej drogą aby większość relacji była 'lazy' i w razie potrzeby pobierac odpowiednie dane 'fetch joinem', zeby uniknąć milionów selektow do bazy.
  • Metody serwisowe pobierające dane, będą przyjmowały z dao odpowiednio kolekcje lub obiekty encji, następnie na poziomie serwisu będą się odbywały wszystkie operacje na nich, i zwracane dane do controllera będą już przemapowane na DTO. (Dzieki temu na poziomie widoku i controllera będą bezpieczne obiekty uniemożliwiające modyfikacje i przede wszystkim wykluczające LazyInitilizationException)
  • W przypadku tworzenia nowych obiektów po stronie widoku: Będą tworzone obiekty encyjne (w stanie transient, także też będą bezpieczne), i w tym przypadku metody serwisowe zapisujace dane będa przyjmowały encje a nie dto. (trochę to wymieszałem ale nic lepszego nie wywnioskowałem na tę chwilę, chcialem też, uniknąć mapowania w drugą strone, było by to dość kłopotliwe)

Czy to się sprawdzi? To się okażę :P

Jeśli macie jakieś uwagi i propozycje to piszcie, myślę ze to dość powszechny problem więc jeśli ktoś ma większe doświadczenie, miło będzie wysłuchać :) Może ktoś zaproponuje jakieś inne ciekawe i dobre podejscie.

0

Jeśli chodzi o kwestię przepisywania encji na DTO, to IMHO jak na razie nie ma idealnego rozwiązania. Albo używasz zewnętrznych frameworków, które generalnie pod maską używają refleksji do kopiowania danych, albo tworzysz assemblery i robisz to ręcznie. Jest to bezpieczniejsze, bo widzisz explicite co jest kopiowane, ale co tu dużo mówić - czasochłonne i upierdliwe. Wybór należy do Ciebie. Od bidy, jak nie chcesz dodawać kolejnego framweworka, to możesz użyć BeanUtils.copyProperties ze Springa, co jednak i tak nie zwolni Cię pewnie z napisania assemblerów.

A co do implementacji, to tak na pierwszy rzut oka:

  • dobrze, że DTO będzie immutable.
  • IMHO DAO powinno enkapsulować obiekty zasobów (encje w tym wypadku) i powinno zwracać już DTO do serwisu. Serwis powinien operować na DTO w dół i w górę.
  • chyba bardziej eleganckim rozwiązaniem będzie jednak translacja z encji na DTO i z DTO na encje niż robienie hybrydy w zależności od odczytu i zapisu.
0
Shalom napisał(a):
  1. DTO, na pewno nie Encje. Z przepychaniem encji mogą być różne dziwne problemy bo na przykład chcesz sobie przygotować w serwisie dane do wysłania do widoku i coś tam mieszasz a zapomnisz że masz transakcje na tym serwisie i nagle skasowałeś sobie pół bazy(tak tak, widziałem takie akcje ;) )
    (...)

Ale przecież zazwyczaj po odczycie danych do wyświetlenia zamyka się transakcję, więc nie można już nic namieszać w bazie. Poza tym od czego jest walidacja. Ja jestem zwolennikiem przepychania encji 8)

2

@student pro od kiedy sie niby zamyka transakcje po odczycie danych? o_O Transakcje ustawia się możliwie wysoko, żeby operacje w warstwie logiki był atomowe i zeby nie zabić wydajności otwieraniem i zamykaniem połączeń do bazy.
Gdyby ustawić transakcje na DAO to może byłoby tak jak piszesz, ale wtedy miałbyś problem z:

  1. Niespójnym stanem - polowa logiki jakiegoś serwisu się wykonała a potem był błąd i co teraz? Ta polowa operacji już została wykonana i zapisana w bazie i nie bardzo da sie to cofnąć...
  2. Pracą z niespójnymi danymi - połowa logiki jakiegoś serwisu się wykonała a teraz inny wątek już pracuje na tych danych, mimo że inne dane na których pracuje są jeszcze "stare".
  3. Wydajnością - co operacje marnujesz czas na narzut związany z otwieraniem i zamykaniem transakcji.

W efekcie w praktyce, w systemach gdzie istnieje jakaś logika (czyli nie w studenckich CRUDach gdzie masz tylko web-ui i warstwę DAO) transakcje ustawia się na metody serwisów. A taki serwis zajmuje się między innymi przygotowaniem danych do obsługi przez kontroler. W efekcie trzeba bardzo uważać jak cokolwiek robisz z encjami, bo na tym etapie transakcja nie jest zamknięta. W szczególności trzeba mieć na uwadze że zmiana obiektu encji propaguje sie jako zmiana w bazie, nawet jeśli nie wywolasz ręcznie żadnego update() czy saveOrUpdate().

Nie wiem co do tego mają walidatory, bo przecież w zaden sposób w niczym tu nie pomogą.

0

@Shalom: nie chodzi mi o zamykanie transakcji po każdym pojedynczym odczycie z bazy, tylko po wczytaniu danych do wyświetlenia.

W JSF:

@ViewScoped
class Kontroler{
  @Getter MyEntity rootEntity;
  //to jest wykonywane automatycznie przed wyrenderowaniem strony (w JSF nazywa się to 'viewAction')
  void init(){
    // wczytajGrafEncji zaczyna i kończy transakcję
    rootEntity = serwis.wczytajGrafEncji( id_korzenia_grafu_encji_ktory_wyswietle );
    //od tego momentu rootEntity moge sobie edytowac, nie ma juz transakcji wiec nic mi sie nie zapisze
  }
  String akcjaUsera(){
     serwis.metodaBiznesowa( dane_z_formularza );
     // na koncu robie redirect-get
  }
}

Problem może wystąpić gdy chce zblokować encje, bo wtedy musiałbym chyba trzymać otwartą transakcję (wczytajGrafEncji nie zamknęła by transakcji), i wtedy rzeczywiście jest niebezpiecznie (tego przypadku jeszcze nie rozwiązałem :)

Shalom napisał(a):

Nie wiem co do tego mają walidatory, bo przecież w zaden sposób w niczym tu nie pomogą.

Chodziło automatyczną walidację encji (BeanValidation) przed ich zapisaniem (PreUpdate), powinna pilnować poprawności danych

0

No dobra ale teraz to pokazałes tylko trywialny przykład kiedy tylko coś czytasz z bazy, a przecież w prawdziwym życiu logika biznesowa nie jest taka prosta i ten model sie zupełnie nie sprawdzi. Już jeden głupi zapis do bazy albo konieczność atomowego wczytania danych z kilku niepowiązanych encji sprawia że twój model nadaje się do kosza...

Walidator to sobie może sprawdzić czy format danych jest poprawny. Nie może sprawdzić spójności logicznej danych, szczególnie nie bez blokowania całej bazy a przecież zapis tak zwalidowanych danych tez musi być atomowy razem z całym sprawdzeniem, bo inaczej masz kalsyczny błąd check-then-act. Zwalidowałeś i było ok a w trakcie zapisu już może nie być ok bo coś się zmieniło w międzyczasie.

0

@Kundel Burek
Wydaje mi się ze enkapsulowanie encji na poziomie dao jest trochę bez sensu. W takim razie po co nam transakcyjność na serwisie? Chyba serwis daje nam tą zaletę, że możemy w nim zaimplementować logike związaną z modelem w ramach transkacji i bezpiecznie operować na obiektach. Ale założmy, że zwrocimy dto do serwisu, jeżeli będą one immutable, przy każdej drobnej zmianie wymagane będzie tworzenie nowego obiektu i wykonywanie mapowania w obydwie strony, chyba złożoność operacji w takich przypadkach niepotrzebnie wzrośnie ale mogę się mylić...Z kolei "hybryda" którą sobie wymysliłem też mi się nie bardzo podoba trochę pomieszanie z poplątaniem

Jeszcze co do DTO, nie chce ich mapować 1:1 z encjami, bo w większości przypadków wszystkie pola encji nie będą mi potrzebne w widoku (pewnie nigdy), chce tam umieszac potrzebne dane, to też trochę zawsze zmniejszy obciążenie przesyłanych danych. Dlatego jeżeli bym zrobił translacje obiektow w obydwie strony znowu pojawia się porąbana logika odwzorowania niepełnego dto na encje... Natomiast w przypadku operowania nawet encyjnym obiektem w widoku ale nowo utworzonym byłoby to bezpieczne i zapis byłby sprawny

Przypomina mi to wybory w naszym kraju, mniejsze zło będzie górą.

0

@mateuszq

Osobiście jestem zwolennikiem enkapsulowania szczegółów implementacyjnych persystencji w DAO. IMHO DAO powinno przyjmować POJO i zwracać POJO. Dzięki temu, gdy zmienisz np. sposób persystencji z bazki na cokolwiek innego, nie będziesz potrzebować zmieniać niczego w serwisach, jedynie w DAO. Serwisy powinny skupić się na logice biznesowej, a DAO na obsłudze persystencji. Natomiast transakcyjność nie ma tutaj nic do rzeczy - jeśli serwis jest transakcyjny, to nie ważne gdzie będziesz dokonywać translacji z encji na DTO, transakcja przeprowadzona będzie tak samo.

Tym bardziej myślę, że należy odseparować kontrolery widoku od encji. Zmieniając jakąkolwiek tabelkę, będziesz musiał przeorać serwisy i widok. Jeśli będzie Cię męczyć kopiowanie propertisów ręcznie, użyj choćby tooli ze Springa czy Apache Commons.

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