Monthly Archives: April 2013

Mapowanie kolekcji

W poprzednim wpisie skupiłem się na mapowaniu relacji pomiędzy encjami. Jednakże praktyka programistyczna jasno pokazuje, iż dość często w użyciu są kolekcje typów prostych (lub ich wrapperów). W przypadku JPA mogą też (o czym jeszcze nie pisałem) występować kolekcje obiektów zagnieżdżonych. I dlaczego tylko mowa o kolekcjach w taki prosty sposób? Przecież mamy i listy i mapy no i nawet zbiory. Dzisiejszy wpis będzie dotyczył możliwości JPA jeśli chodzi o wszystkie powyższe zagadnienia.

Typy proste i embedded

Relacje między encjami w przypadku JPA wymagają użycia adnotacji pokazującej ‘krotność’ relacji (@OneToMany, @ManyToMany, itp). W przypadku typów prostych i embedded nie ma takiej potrzeby. By oznaczyć, że konkretna encja zawiera w sobie kolekcję tego typu obiektów wystarczy zastosować adnotację @ElementCollection. Załóżmy, że chcemy obdarować naszego bohatera listą jego przydomków (Chlebik the Butcher, Ripper Chlebik, itp) – wyglądałoby to tak:

@ElementCollection
private Collection nicknames;

// Getterki i setterki pominięte

Jeśli uruchomimy kod z poprzednich rozdziałów i podejrzymy strukturę bazy danych, zobaczymy nową tabelę o nazwie HERO_NICKNAMES. Zawiera ona oczywyście klucz obcy do tabeli HEROES oraz wartość tekstową. Adnotacja @ElementCollection nie potrzebowała dodatkowych atrybutów, gdyż przy używaniu generyków automatycznie wie jakiego typu obiekt znajduje się w kolekcji. Domyślnym zaś typem pobierania relacji jest EAGER (można to zmienić w formie podpowiedzi dla providera poprzez atrybut fetch). Określenie docelowej tabeli przechowującej informację o kolekcji jest możliwe za pomocą adnotacji @CollectionTable, w której możemy podać dość standardowe atrybuty jak nazwę tabeli, schemat, kolumny łączące czy dodatkowe ograniczenia (jak zawsze zachęcam do zapoznania się z dokumentacją). Istnieje też możliwość zmiany nazwy kolumny przechowującej informacje o przydomkach (adnotacja @Column), a także w przypadku typów embedded zmienić dla tej konkretnej relacji nazwy kolumn w bazie danych adnotacją @AttributeOverride).

Interfejsy szczegółowe – Set

Powyższy kawałek kodu operował dość uniwersalnym interfejsem Collection. Każdy programista Javy wie jednak, że na co dzień ma do dyspozycji o wiele większy wachlarz możliwości. Zaczniemy od tego najprostszego – interfejsu Set. W przypadku kolekcji nie będących encjami zachowuje się on bardzo podobnie do interfejsu Collection z jednym zasadniczym wyjątkiem – nie dopuszcza do użycia duplikatów w kolekcji. Zamieńmy podany wyżej przykład kodu:

@ElementCollection
private Set nicknames;

// Getterki i setterki pominięte

zaś przy deklaracji naszego bohatera użyjmy:

Set nickNames = new HashSet();
nickNames.add("Ripper");
nickNames.add("Ripper");
nickNames.add("Butcher");
h.setNicknames(nickNames);

W bazie danych po zapisaniu obiektów zostaną zapisane tylko i wyłącznie 2 wartości – ‘Ripper’ oraz ‘Butcher’.

Interfejsy szczegółowe – List

Użycie interfejsu List jest wymagane wówczas, kiedy chcemy by elementy naszej kolekcji były w jakiś sposób poukładane (posortowane). Możemy osiągnąć to na dwa sposoby:

  • przechowując informację o tym fakcie w bazie danych – tworzymy oddzielną kolumnę w tabeli, której jedynym celem jest przechowywanie informacji o kolejności danej wartości na liście. Można to osiągnąć za pomocą adnotacji @OrderColumn. Domyślnie tworzy ona kolumnę o nazwie składającej się z nazwy naszej własności w encji i końcówki ‘_ORDER’. Jeżeli nasz kod encji zamienimy na:
    @ElementCollection
    @OrderColumn
    private List nicknames;
    

    To przy dodawaniu przydomków w ten sposób:

    // Zmiana dodawania przydomków do bohatera
    List nickNames = new ArrayList();
    nickNames.add("Ripper");
    nickNames.add("Butcher");
    nickNames.add("Killer");
    h.setNicknames(nickNames);
    

    i zapisaniu danych w bazie danych zobaczymy, że powstała dodatkowa kolumna o nazwie NICKNAMES_ORDER, w której (zaczynając od wartości 0!) mamy zapisane kolejne cyfry pokazujące kolejność przydomków. Teraz wystarczyłoby poprzestawiać kolejność obiektów w encji, np:

    entityManager.getTransaction().begin();
    
    Hero hh = entityManager.find(Hero.class, 1L);
    List nickNames = hh.getNicknames();
    nickNames.remove("Ripper");
    nickNames.add("Puncher");
    
    entityManager.persist(hh);
    
    entityManager.getTransaction().commit();
    

    by zobaczyć w bazie danych, że nasze dane zostały uaktualnione (co zresztą również widać w logu Hibernate – zostały wygenerowane 3 zapytania UPDATE). Powyższa metoda jest z całą pewnością dobra w przypadku encji i typów wbudowanych. Należy mieć jednak na uwadze jedną dość istotną sprawę – wydajność. Jak widać jakakolwiek zmiana kolejności wymaga aktualizacji danych WSZYSTKICH WARTOŚCI. W przypadku częstych zmian i pod dużym obciążeniem możemy całkiem nieźle rozgrzać bazę danych. Dobrze jest mieć to na uwadze.

  • używając jednego z atrybutów przechowywanego obiektu (dotyczy encji i typów wbudowanych) – wykorzystując adnotację @OrderBy. Istotny jest fakt, że możemy adnotacji tej używać również w przypadku zwykłych relacji (czyli z użyciem adnotacji @OneToMany i podobnych, wówczas domyślnie zakładamy sortowanie po kluczu głównym). Adnotacja ta nie posiada atrybutów, a jedynie wartość – jest to łańcuch tekstowy obejmujący listę używanych do sortowania własności z opcjonalną informacją o kolejności (ASC, DESC – domyślnie ASC). Nasze przydomki póki co są zwykłymi łańcuchami tekstowymi, ale co gdybyśmy przerobili je na obiekt wbudowany?
    @Embeddable
    public class Nickname {
    
    private String nick;
    
    // Uciete getterki/setterki oraz ew. dodatkowe
    // wlasnosci, ktore pewnie sprawilyby, ze ten
    // obiekt mialby wiecej sensu 😉
    
    }
    

    Przy tworzeniu naszego bohatera użylibyśmy takiego kodu:

    List nickNames = new ArrayList();
    Nickname n1 = new Nickname(); n1.setNick("Ripper");
    Nickname n2 = new Nickname(); n2.setNick("Butcher");
    Nickname n3 = new Nickname(); n3.setNick("Killer");
    nickNames.add(n1);
    nickNames.add(n2);
    nickNames.add(n3);
    h.setNicknames(nickNames);
    

    Jeśli zatem chcielibyśmy posortować nasze przydomki alfabetycznie przy pobieraniu encji z bohaterem musielibyśmy zrobić coś takiego:

    // Zmiana encji
    @ElementCollection
    @OrderBy("nick ASC")
    private List nicknames;
    
    // Kod wyciagajacy encje
    Hero hh = entityManager.find(Hero.class, 1L);
    List nickNames = hh.getNicknames();
    
    for( Nickname nn : nickNames ) {
        System.out.println("Nick: " + nn.getNick() );
    }
    

    Efekt działania kodu to:

    Nick: Butcher
    Nick: Killer
    Nick: Ripper

    Czyli mamy pięknie posortowane alfabetycznie obiekty bez konieczności dodatkowej pętli przy pobieraniu danych czy tym podobnych udziwnień.

Interfejsy szczegółowe – Mapy

Przyznam szczerze, że z mapami jest problem. Albowiem do tej pory w omawianym temacie pojawiały się w użyciu trzy typy – proste, encje i embedded. Teraz dodajmy do tego fakt, że mapy posiadają nie tylko wartość, ale również klucz, który może być przecież również typem prostym, encją lub embedem. Łącznie dziewięć kombinacji. Od razu stwierdzę, że przepisywanie książki tutaj niespecjalnie ma sens dlatego ograniczę się do kilku zdań wstępu, a potem po prostu skopiuję ładną tabelkę i zachęcę do odwiedzenia dokumentacji 😉

W przypadku map kwestią do rozwiązania jest miejsce składowania kluczy, pod którymi będą trzymane wartości kolekcji. Zasadniczo klucz niczym się zasadniczo nie różni (technicznie) od informacji o kolejności danej wartości na liście. Zatem rozsadnym wydaje się być przechowywanie klucza mapy w tej samej tabeli, w której znajdują się nasze wartości. W przypadku tabel łączących (jak np. w relacji many-to-many) ta tabela jest najlepszym miejscem by składować tę informację. Pod kątem struktury połączenia najlepiej będzie gdy oddam głos autorom książki:

It is always the type of the value object in the Map that determines what kind of mapping must be used. If the values are entities, the Map must be mapped as a one-to-many or many-to-many relationship, whereas if the values of the Map are either embeddable or basic types, the Map is mapped as an element collection.

Zakładam, że powyższe jest dość jasne. Do tworzenia połączeń i mapowań wykorzystać można kilkanaście adnotacji:

  • @MapKeyColumn – wskazuje na kolumnę, w której będzie przechowywany klucz dla konkretnych wartości mapy w przypadku gdy kluczem jest wartość prosta!
  • @MapKeyEnumerated – sytuacja bardzo podobna jak w powyższym punkcie, z tym tylko, że rolę klucza odgrywa typ wyliczeniowy (ENUM). Ze strony JPA typ wyliczeniowy jest uznawany za typ prosty (ponieważ ‘pod spodem’ jest reprezentowany przez łancuch tekstowy lub liczbę porządkową)
  • @MapKeyTemporal – sytuacja dotyczy klucza będącego typu java.util.Date lub java.util.Calendar – dokładną klasę określa wartość adnotacji.
  • @MapKey – adnotacja używana wówczas, gdy kluczem mapy będzie jedna z własności prostych obiektów, które będą wartościami.
  • @MapKeyJoinColumn – używana w sytuacji kiedy kluczem jest encja. W tabelach bazy danych (konkretnie w jednej z kolumn) jest zapisywana o kluczu głównym encji (nie przechowujemy jej samej).
  • @MapKeyClass – adnotacja używana w sytuacji kiedy nasza mapa nie korzysta z generyków. Wówczas konieczne jest użycie tej adnotacji wraz z podaniem (jako atrybut) klasy typu, który będzie używany jako klucz.

To powiedziawszy poniżej zamieszczam ładną tabelkę, którą przekopiowałem z książki 😉

Map Mapping Key annotation Value annotation
Map<Basic, Basic> @ElementCollection @MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
@Column
Map<Basic, Embeddable> @ElementCollection @MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map<Basic,  Entity> @OneToMany, @ManyToMany @MapKey,
@MapKeyColumn,
@MayKeyEnumerated,
@MapKeyTemporal
Mapped by entity
Map @ElementCollection Mapped by embeddable,
@AttributeOverride
@Column
Map @ElementCollection Mapped by embeddable,
@AttributeOverride
Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map @OneToMany, @ManyToMany Mapped by embeddable Mapped by entity
Map<Entity, Basic> @ElementCollection @MapKeyJoinColumn @Column
Map<Entity, Embeddable> @ElementCollection @MapKeyJoinColumn Mapped by
embeddable,
@AttributeOverride,
@AssociationOverride
Map<Entity, Entity> @OneToMany, @ManyToMany @MapKeyJoinColumn Mapped by entity