ORM – Relacje

Bardzo wiele osób utożsamia JPA z używaniem ORM. Jest to zasadniczo wcale nie aż tak głupie myślenie – przy prostych projektach bez większych problemów poradzić można sobie tworząc kilka klas domenowych oraz odwołując się do nich prostymi metodami z EntityManagera. By jednakże w pełni to osiągnąć trzeba poruszyć bardzo ważny aspekt ORM – relacje.

Współczesne bazy danych nie od biedy nazywane są relacyjnymi – dane przechowywane są w oddzielnych tabelach, kluczowe zaś są relacje jakie zachodzą między nimi. Zrozumienie podstaw ORM jest dość łatwe – opanowanie arkanów mapowania relacji oraz tego w jaki sposób będą się one zachować jest cokolwiek trudniejsze. Dlatego też temu zagadnieniu poświęcę dzisiejszy wpis. By jednakże w spokoju pracować nad relacjami potrzebujemy kolejnych obiektów domenowych, aby móc się do nich odwoływać. Przykładową encją niech będzie smok (a co, każdy bohater może mieć smoka):

@Entity
public class Dragon {

    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;

    private String name;

    public Dragon() { }

    // Gettery i settery pominiete
}

W podobny sposób trzeba zadeklarować kilka obiektów, które wyglądają dokładnie tak samo, tylko mają inną nazwę. Dodamy zatem jeszcze Weapon (sztuka broni) oraz Deity (bóstwo). Dzięki temu tłumaczenie relacji będzie delikatnie mówiąc – łatwiejsze.

Relacje one-to-one

Zasadniczo są one najłatwiejsze do zrozumienia zatem od nich zaczniemy. Relacja tego typu zakłada sytuację, w której dokładnie jednej encji odpowiada dokładnie jedna inna encja. Przykładem z życia jest choćby człowiek i jego numer PESEL – każdy człowiek ma jeden numer pesel, każdy pesel reprezentuje jednego człowieka – tertium non datur. Na przykładzie naszej gry będzie to relacja pomiędzy naszym bohaterem i smokiem (pomysł pochodzi z serii Eragon). Każdy bohater ma dokładnie jednego smoka, nikt inny tego smoka dosiąść nie może. Co więcej – jak smoka ubiją (lub się nażre czegoś, potem wypije Wisłę wody i pęknie) nasz bohater jest skazany na konie i własne nogi. Perfekcyjna relacja one-to-one. Naszą bohaterską encję musimy rozszerzyć o obiekt ze smokiem.

@OneToOne
@JoinColumn(name="dragon_id")
private Dragon dragon;

Tadam, mamy smoka 😉 Adnotacje omówię za chwilkę – naszego smoka dodajemy do naszego bohatera w ten sposób.

Dragon d = new Dragon();
d.setName("Smok Wawelski");
entityManager.persist( d );
hero.setDragon(d);

Jak widać dodanie relacji do encji jest banalnie proste. Wyjaśnienia wymagają z całą pewnością użyte adnotacje. @OneToOne należy do grupy adnotacji, które pojawiają się zawsze przy mapowaniu relacji (inne nazywają się równie obrazowo). Zalecam zapoznanie się z dokumentacją dotyczącą atrybutów tej adnotacji – na przykładzie powyżej pozostawiłem wszystkie wartości domyślne i zasadniczo działa to dobrze. Akurat twórcy JPA pomyśleli sensownie w tym przypadku.

Drugą istotną adnotacją jest @JoinColumn. Zgodnie z nazwą określa ona jaka kolumna (no i z jakimi atrybutami) będzie odwzorowywać relację. W naszym przypadku nasz bohater jest stroną posiadającą (owning side) relację. W związku z tym będziemy trzymać w wygenerowanej tabeli z bohaterami referencję do tabeli ze smokami (klucz obcy). W powyższym przykładzie wskazałem nazwę kolumny – domyślnie jest ona tworzona z nazwy własności, podreślnika + kilka zasad – jak zawsze polecam zapoznanie się z dokumentacją by poznać szczegóły.

Relację, którą pokazałem na powyższym przykładzie możemy nazwać jednostronną (unidirectional). Nasz heros wie wszystko o swoim smoku, ale sam smok nie bardzo ma pojęcie o istnieniu naszego bohatera. Czyli zamiast grzecznie dać się ujeździć prędzej zeżre potencjalnego jeźdźca. Zresztą w opisie smoka wyszło nam jasno, że smok ma tylko jednego jeźdźca, zatem dobrze by było, aby coś o nim wiedział. Taką relację (gdzie obie strony wiedzą o sobie) nazywamy bidirectional.

@OneToOne(mappedBy="dragon")
private Hero rider;

Teraz nasz smok również ma wiedzę o drugiej stronie relacji. W przypadku bazy danych nie zmieniło się nic – jednakże nie ma problemu by wykonać następujący kod:

Dragon dragon = entityManager.find(Dragon.class, 1L);
Hero rider = dragon.getRider();
System.out.println("Jezdziec nazywa sie " + rider.getName() );

U mnie jeźdźcem jest ‘Chlebikowy mag’ 😉

Relacje many-to-one i one-to-many

Wspomniałem na samym początku by utworzyć też encję reprezentującą broń (Weapon). Zasadniczo zgodzimy się, że każdy bohater może mieć kilkanaście sztuk broni (jakiś mieczyk, sztylet, może i łuk dla odmiany). Mamy zatem kilkanaście sztuk konkretnego przedmiotu, które przynależą do jednej encji (naszego bohatera). Podręcznikowy zatem przykład relacji many-to-one. W jej przypadku to strona many jest ‘posiadaczem’ relacji, gdyż to w niej będzie zapisany klucz obcy do encji bohatera. Ma to zasadniczo sens – każda sztuka broni trzyma informację o swoim właścicielu. Sam zaś bohater (na poziomie tabeli w bazie danych) nie ma o broni pojęcia. W klasie broni dodajemy zatem taki kod:

@ManyToOne
private Hero owner;
// Getterki i setterki pominięte

Tym samym każda sztuka broni posiada referencję do swojego posiadacza. Jak już wspomniałem w przypadku takiej relacji to strona obdarzona adnotacją @ManyToOne jest stroną posiadającą. Dzięki temu w tabeli Weapon będzie składowana kolumna z kluczem obcym do encji bohaterów. Przy domyślnym zachowaniu adnotacji – zostanie ona wygenerowana z nazwy własności encji oraz nazwy kolumny z kluczem głównym w docelowej encji (czyli u nas będzie to kolumna owner_id w tabeli Weapon). Podobnie jak w przypadku poprzednich relacji możemy sterować tą relacją za pomocą adnotacji @JoinColumn. Moglibyśmy zatem nasz kod zmodyfikować by wyglądał w ten sposób:

@ManyToOne
@JoinColumn(name="hero_id")
private Hero owner;

Co sprawia, że przynajmniej na poziomie bazy danych nasza tabela jest trochę bardziej jednoznaczna i czytelna. W powyższym kodzie powtarzamy jednak sytuację z bohaterem i jego smokiem. Z całą pewnością dobrze by było, aby bohater wiedział jaką broń ma do dyspozycji. Wtedy będzie to niejako odwrócenie relacji many-to-one, czyli będziemy mieć do czynienia z relacją typu one-to-many. Zmodyfikujemy zatem klasę Hero.

@OneToMany(mappedBy="owner")
private List weapons;

Jak już wspomniałem bohater jest w tej stronie stroną podrzędną w relacji (inverse side) – jedynym zatem elementem poza adnotacją @OneToMany jest wskazanie na własność, która jest posiadającą relację w klasie Weapon (czyli na owner). Istnieje jednakże możliwość, aby nie specyfikować atrybutu mappedBy. W tym przypadku zajdą zmiany na poziomie bazy danych (przy użyciu tego atrybutu tak naprawdę wszystko pozostaje po staremu) – powstanie tabela łącząca encję bohatera z bronią. Kod encji broni w tym przypadku nie posiadałby w ogóle informacji o właścicielu (należy usunąć własność owner), zaś encja bohatera powinna wyglądać tak:

@OneToMany
@JoinTable(name="HERO_WEAPON",
	joinColumns=@JoinColumn(name="HERO_ID"),
	inverseJoinColumns=@JoinColumn(name="WEAPON_ID"))
private List weapons;

W tym momencie powstaje tabela łącząca o nazwie HERO_WEAPON z nazwami kolumn jak podaliśmy w adnotacji @JoinTable. Na poziomie bazy danych encje biorące udział w relacji w ogóle nie wiedzą o swym istnieniu (podobnie jak w przypadku relacji many-to-many nie posiadają kolumn z kluczami obcymi w tabelach).

Relacje many-to-many

Ten typ relacji jest równie łatwy do zrozumienia co jeden do jednego. Relacja wiele-do-wielu zakłada istnienie tabeli pośredniczącej. W tabeli tej występują dwie kolumny – z parami kluczy obcych wskazującymi na encje znajdujące się w innych tabelach. Przykładem takiej relacji są bóstwa (w naszej grze RPG istnieje politeizm). Jedno bóstwo może mieć miliony wyznawców, ale też i każdy bohater może jednocześnie wyznawać kilka bóstw. Zaczniemy zatem od modyfikacji naszego bohatera:

@ManyToMany
List deities;

Jak i również trzeba w klasie bóstwa dorzucić wyznawców:

@ManyToMany
List believers;

I zasadniczo nie ma nic więcej do dodania. Jeśli uruchomimy kod aplikacji (z create-drop w persistence.xml), wówczas pojawi się w bazie danych ciekawa sytuacja – dwie tabele łączące! Dlaczego tak? W przypadku relacji wiele-do-wielu nie można (w sensie logicznym) wskazać właściciela tej relacji. Jednakże programista musi dokonać arbitralnego wyboru tej encji, która zostanie potraktowana jako właściciel relacji, zaś tym samym druga strona relacji staje się podrzędną. Załóżmy, że w powyższym przykładzie za posiadającą uznamy encję bóstwa (tak dla odmiany).

@ManyToMany
@JoinTable(name="DEITY_HERO",
	joinColumns=@JoinColumn(name="DEITY_ID"),
 	inverseJoinColumns=@JoinColumn(name="HERO_ID"))
List believers;

Bohater zaś jest elementem ‘podrzędnym’:

@ManyToMany(mappedBy="believers")
List deities;

Przy powyższym kodzie w bazie danych powstanie tylko jedna tabela łącząca zawierająca klucze obce. Więcej ciekawostek można znaleźć w dokumentacji adnotacji @ManyToMany

Sposoby pobierania danych

W zależności od typu relacji dane pobierane są przez JPA w różny sposób. Sensowniej będzie podeprzeć się cytatem z książki:

The fetch mode can be specified on any of the four relationship mapping types. When not specified on a single-valued relationship, the related object is guaranteed to be loaded eagerly. Collectionvalued relationships default to be lazily loaded, but because lazy loading is only a hint to the provider, they can be loaded eagerly if the provider decides to do so.

Dość często spotykanym podejściem jest specyfikowanie różnego sposobu pobierania (poprzez adnotację @FetchType) – kiedy pobieramy bohatera może nie jest dobrym od razu pobierać całą jego broń, a w późniejszym pewnie terminie całe drzekwo umiejętności, bóstw i tak dalej. Wtedy specyfikujemy relacje jako lazy-loaded. Jednakże kiedy pobieramy informacje o konkretnej sztuce broni – dobrze by było wiedzieć kto jest jej właścicielem. W sumie w końcu jak np. naostrzyć broń bez właściela – wszak ktoś musi za to zapłacić 😉

Advertisements

2 thoughts on “ORM – Relacje

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s