#15 – Relacje jeden-do-wielu, wiele-do-jednego

By 4 grudnia 2014Kurs Javy
Wpis-Header-23

W tej lekcji poznamy sposób obsługi relacji jeden-do-wielu za pomocą JPA, różne rodzaje tych relacji oraz pewne ograniczenia i to, co się z nimi wiąże.
Sytuacja, którą do tej pory omawialiśmy (mamy tylko jeden typ obiektu) w rzeczywistości występuje bardzo rzadko – najczęściej mamy do czynienia z wieloma obiektami powiązanymi między sobą wieloma relacjami jeden-do wielu. Taką sytuację można rozłożyć na pojedyncze elementy – relacje jeden-do-wielu – i nauczyć się radzić sobie z pojedynczą relacją. Więcej takich relacji w systemie wymaga jedynie powielenia tego schematu :)

Lekcja

UML vs ERD

Często będziemy mieli do czynienia z dokumentacją w określonej postaci, ważne jest, żeby umieć z niej korzystać. Najczęściej informacje o relacjach pomiędzy obiektami będziemy czerpać z diagramu – albo będzie to diagram klas (UML) albo będzie to diagram bazy danych (ERD). Ważne jest, żeby wiedzieć jakie są róznice pomiędzy nimi:

  • w przypadku diagramu ERD, relacja ta będzie reprezentowana przez kolumnę po stronie ‚wiele’, której typ będzie taki sam jak typ klucza głównego tabeli po stronie ‚jeden’, a nazwa najczęściej ma postać {nazwa_tabeli_po_stronie_jeden}_id lub podobną
  • w przypadku diagramów klas (UML), po stronie ‚jeden’ będziemy mieli kolekcję (Listę lub Set – patrz niżej) obiektów, a po stronie ‚wiele’ relacji będziemy mieli pole określonego typu

Dowolna z powyższych informacji jest wskaźnikiem, że mamy do czynienia z relacją jeden-do-wielu. Najczęściej taka relacja powinna być także oznaczona linią na diagramie (oraz opcjonalnie, opisana).

Relacja JDW w JPA – mapowanie

W JPA relacje takie mapujemy za pomocą 2 adnotacji – @OneToMany oraz @ManyToOne. Adnotację @ManyToOne możemy połączyć dodatkowo z adnotacją @JoinColumn. Zobaczmy na przykładach:


@Entity
public class StronaJeden {
    
    @Id
    Long id;
    
    @OneToMany(mappedBy="stronaJeden")
    List stronyWiele;
}

@Entity
public class StronaWiele {
    
    @Id
    Long id;
    
    @ManyToOne
    @JoinColumn("strona_jeden_id")
    StronaJeden stronaJeden;
}

To, na co powinniśmy zwrócić uwagę, to parameter mappedBy – możemy go wskazać zarówno dla adnotacji @ManyToOne jak i dla adnotacji @OneToMany, ale dla jednej relacji, powinien on być użyty tylko raz. Parametr ten mówi, które pole w drugiej klasie odpowiada tej samej relacji. Jest to używane do budowania obiektów na podstawie danych z bazy danych.

W powyższym przykładzie widzimy też adnotację @JoinColumn – jest ona opcjonalna, możemy jej użyć do określenia, jak ma wyglądać kolumna w bazie danych, która odpowiada za tą relację (działa to podobnie jak adnotacja @Column)

Ta relacja jest dwukierunkowa (bi-directional) tzn. obie klasy mają pola, które reprezentują tą relację. Możliwe jest opisanie jej tylko po jednej stronie, ale nie będziemy się na chwilę obecną zajmować tego typu przykładem.

Rodzaje kolekcji

Po stronie ‚jeden’ relacji mamy do wyboru dwa rodzaje kolekcji – Set, który jest nieuporządkowany oraz List, która jest uporządkowana. W przypadku listy, możemy wskazać dodatkowy parametr – nazwę pola – na podstawie którego ma być sortowana (parametr orderBy w adnotacji @OneToMany), dzięki czemu móżemy sortować np. na podstawie nazwy albo jakiegoś innego atrybutu

FetchType – ładowanie kolekcji

Obie te adnotacje (@OneToMany oraz @ManyToOne) mają jeszcz jedną opcję – fetch . Jest to bardzo ważny element, który domyślnie ma wartość LAZY dla @OneToMany oraz EAGER dla @ManyToOne.
Parametr ten decyduje, kiedy obiekt/obiekty w tym polu powinny zostać pobrane:

  • EAGER – pobierz w momencie wykonywania zapytania
  • LAZY – pobierz dopiero kiedy będzie pierwsze odwołanie do tego obiektu

Każda z opcji ma swoje wady i zalety. Należy bardzo uważać z EAGER, ponieważ możemy przez przypadek spowodowac pobranie całej bazy danych w jednym zapytaniu. W przypadku LAZY, pobieranie ‚na żądanie’ zadziała tylko, jeżeli jesteśmy w ramach transakcji (metody z adnotacją @Transactional, a encja jest zarządzana – najczęściej jest to po prostu miejsce, w którym pobieramy tą encję z bazy danych). Nalezy więc bardzo dokładnie przemyśleć jak będziemy korzystać z danych oraz jaki ma to wpływ na ilosć danych, które pobieramy. Najczęściej domyślne ustawienia są w zupełności wystarczające

Zadanie

Dodaj do swojej aplikacji obsługę ‘zabawek’ kota

  • dodawanie i usuwanie powinno być dostępne na ekranie szczegółów kota
  • dla uproszczenia nie zakładamy edycji zabawki – możemy ją usunąć i dodać ponownie
  • zarówno dodawanie jak i usuwanie powinno odbywać się w osobnym kontrolerze (ZabawkiController) i po wykonaniu czynności wracać do ekranu szczegółów kota

progres

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • Jakub Zysnarski

    Wybrałem kota, jestem na ekranie z jego szczegółami. Chcę dodać zabawkę do kota. W jaki sposób dodawaną zabawkę podpiąć pod wybranego wcześniej kota? Czy wchodząc na ekran, na którym wprowadzę dane zabawki, powinienem jakoś zapamiętać id wybranego kota, a następnie odesłać go w ZabawkaDTO razem z danymi zabawki, po czym przy zapisie ponownie odszukać kota o danym ID, utworzyć nową zabawkę, dopiąć ją do kota i całego kota zapisać? Czy może jest inny sposób?

    • Najczęstszym wzorcem jest przekazanie id kota przez adres URL (np. /koty/mruczek/zabawka/add, gdzie ‚mruczek’ to id).
      Jeśli chodzi o samo dodanie, to dokładnie tak jak mówisz – musisz wyszukać kota, po czym dodać zabawkę do kolekcji zabawek w kocie lub uzupełnić pole kot w klasie zabawka (w zależności od tego, jak zdefiniowałeś mapowanie).

      • Jakub Zysnarski

        Ok, ale w ten sposób mogę sobie wyszukać jakiegoś kota, przejść do ekranu dodania zabawki, a następnie podmienić numer kota i dodać zabawkę do zupełnie innego. Mój pomysł jest taki, by na ekranie dodawania zabawek zrobić ukryte pole, które przechowa ID kota – dobrze kojarzę, że takie rozwiązanie też się praktykuje?

  • Da się pobierać w locie, ale wymaga to dodatkowej konfiguracji hibernate, którą możesz znaleźć pod hasłem ‚open session in view’ (jest to wzorzec). Jest on dosyć kontrowersyjny i opinie czy należy go stosować czy nie są podzielone, np tutaj kilka argumentów przeciwko: http://stackoverflow.com/a/1103371/1563204. Ogólnie lepiej jest pobierać osobno zabawki i przekazywać je przez model, jest to ‚czystsze’ i na dłuższą metę mniej problematyczne rozwiązanie.

    • Jakub Zysnarski

      Czyli co? Osobno dać w modelu obiekt kot, i osobno listę zabawek? Jest to konieczne, jeśli kot zawiera w sobie zabawki? Czy rozwiązanie z ustawieniem OneToMany Fetch na EAGER jest prawidłowe?

      • Tak, w modelu osobno powinien być kot i osobno lista zabawek, jeśli używasz encji także po to, aby wyświetlać dane na widoku.
        Ustawienie Fetch na EAGER rozwiąże problem, ale raczej nie jest to prawidłowe rozwiązanie – byłoby to rozwiązanie prawidłowe, jeśli kot nie mógłby istnieć bez listy zabawek. Ponieważ relacja ta nie jest tak ‚ścisła’ ustawienie Fetch na EAGER nie jest prawidłowym rozwiązaniem.