ORM – podstawy

W kolejnej części przygody z JPA zajmiemy się samym mięskiem – czyli mapowaniem relacyjno-obiektowym. W jaki sposób obiekty są odwzorowywane na bazę danych, jak się łączą (o relacjach będzie w kolejnym wpisie) oraz jak da się je jednoznacznie identyfikować. Temat dość długi i obszerny zatem do roboty.

Dostęp do pól encji oraz ich mapowanie

W poprzednim wpisie dość pobieżnie pokazałem kilka adnotacji używanych w encjach. Wystarczyło to, aby pokazać działający przykład. Teraz skupimy się troszkę bardziej dokładnie nad własnościami encji. JPA może używać własności obiektu na dwa sposoby – za pomocą dostępu do pól (field access) lub dostępu do własności (nie wiem czy dobrze to tłumaczę, w oryginale property access).

  • field access – dostęp providera bezpośrednio do pól encji. Nie są potrzebne gettery/settery, zaś jeśli występują są ignorowane. Pola encji nie powinny mieć dostępu publicznego (choć nie jest to wymóg JPA, ale dobra praktyka).
  • property access – przy uzyciu tego trybu dostępu encja musi spełniać kontrakt JavaBeans – posiadać odpowiednie gettery i settery. Typ pola jest określany na podstawie typu zwracanego przez getter, zaś metody dostępowe muszą rzecz jasna mieć poziom dostępu public lub protected.

Mógłbyś się zapytać czytelniku – co jest domyślnym sposobem dostępu? Ano tutaj sprawa jest dość prosta – wystarczy, że umieścisz adnotację @Id nad polem encji oznaczającym klucz główny i zostanie użyty dostęp field. Jeśli dokonasz tego na metodzie getId() włączy się property access. Innym sposobem jest użycie adnotacji @Access, którą można umieścić na poziomie klasy bądź pól/metod (wraz z jej własnościami – AccessType. Jest to nowość wprowadzona w wersji drugiej JPA. Nie ma zatem problemu by część własności była dostępna poprzez pola, a część poprzez gettery. Jednakże takie mieszanie nie jest dobre – zaciemnia kod. Może być przydatne w przypadku dodawania nowych pól w encjach dziedziczących po rodzicach (możemy nie mieć do tychże rodziców dostępu), bądź też chcąc dokonać jakiejś dodatkowej transformacji wartości.

Na sam koniec omawiania dostępu do własności encji należy wspomnieć o możliwości zablokowania zapisu danych do bazy. Służy do tego adnotacja @Transient. Różni się ona od działania słowa kluczowego transient w Javie, zaś najlepiej wyjaśnia to StackOverflow.

W pierwszym wpisie wspomniałem o adnotacji @Table. Najczęściej pozostawia się ją samodzielną – nie zmieniając atrybutów i zachowując domyślne wartości. Wyjątkiem może być nazwa docelowej tabeli, do której trafi dana encja. Służy do tego atrybut name. Z ciekawych atrybutów (radzę poczytać sobie JavaDoc dla wszystkich adnotacji, to naprawdę pomaga) można wspomnieć uniqueConstraints – dodatkowe ograniczenia dla tabeli jeśli chodzi o wartości unikalne (oczywiście chodzi o podanie nazw kolumn lub ich kombinacji).

Mapowanie kolumn i ich pobieranie

Tabelę już mamy – co zatem z jej kolumnami? Do tej pory zadowalaliśmy się domyślnymi wartościami dostarczanymi przez Hibernate – łancuchy tekstowe lądowały w naszej bazie jako VARCHAR, identyfikator był longiem, zaś data była datą ;). Bardzo często takie podejście wystarcza – jednakże w przypadku zastanego schematu bazy danych, bądź też mając na uwadzę wydajność dobrze jest zabrać sprawy w swoje ręce. Do operowania budową i własnościami kolumny użytej do zapisywania wartości encji służy adnotacja (a jakże) – @Column. Posiada ona istotne atrybuty i dobrze jest się z nimi zapoznać – do najbardziej popularnych należą choćby: nullable, length czy unique.

Co zrobić w przypadku kiedy dane w tabeli są ‘ciężkie’? Załóżmy, że posiadamy kolumnę z danymi binarnymi (np. obrazek). Załóżmy, że mógłby być to dowolny obrazek (mający nawet kilka MB), który ma przedstawiać naszego herosa w przykładowej grze RPG. Pole w naszej encji Hero mogłoby wyglądać tak:

 @Lob
 @Column(columnDefinition="CLOB NULL")
 private String heroImage;

Jednakże zgódźmy się – z całą pewnością o wiele częściej będziemy wyświetlać proste wartości naszego herosa – level czy imię. Cały (duży) obrazek o wiele rzadziej. Cóż zatem z tym zrobić? Zastosować lazy loading. Domyślnie wszystkie pola encji (poza tymi oznaczonymi jako transient) są opatrzone ‘niejawnie’ adnotacją @Basic. Jednym z jej atrybutów jest fetch, który przyjmuje wartości zdefiniowane w typie FetchType. Domyślnie użyta wartość to EAGER. Oznacza ona natychmiastowe (zachłanne) pobieranie wartości. Jednakże pobieranie za każdym razem z bazy danych potencjalnie dużej ilości danych mija się z celem. Tutaj z pomocą przychodzi nam drugi typ pobierania danych – LAZY (leniwy czy tez opóźniony). Dzięki adnotowaniu pola by jego wartość była pobierana w ten sposób, zostanie ona pobrana z bazy danych dopiero kiedy kod źródłowy odwoła się do jej wartości. W przypadku naszego pola wyglądałoby to tak:

@Lob
@Column(columnDefinition="CLOB NULL")
@Basic(fetch=FetchType.LAZY)
private String heroImage;

Nasz przykład jest dość ekstremalny (pliki tego typu raczej przechowuje się fizycznie na dysku), ale dobrze pokazuje samą ideę. Do tematu lazy-loadingu powrócę podczas omawiania mapowania relacji i kolekcji.

Jakie konkretnie typy zmiennych możemy używać w encjach? Sporo 😉 Lista jest dość rozbudowana (cytat z książki):

• Primitive Java types: byte, int, short, long, boolean, char, float, double
• Wrapper classes of primitive Java types: Byte, Integer, Short, Long, Boolean,
Character, Float, Double
• Byte and character array types: byte[], Byte[], char[], Character[]
• Large numeric types: java.math.BigInteger, java.math.BigDecimal
• Strings: java.lang.String
• Java temporal types: java.util.Date, java.util.Calendar
• JDBC temporal types: java.sql.Date, java.sql.Time, java.sql.Timestamp
• Enumerated types: Any system or user-defined enumerated type
• Serializable objects: Any system or user-defined serializable type

Typy wyliczeniowe

W tym miejscu chciałbym napisać kilka słów o ENUMach. Przyznam szczerze, iż bardzo lubię tę konstrukcję – zarówno w przypadku Javy jak i baz danych (natywnie choćby wspierany przez MySQL). Z całą pewnością typy wyliczeniowe są o wiele bardziej czytelne niż maski bitowe bądź po prostu czyste wartości INT wrzucone do bazy. Oczywiście w przypadku JPA głównie chcielibyśmy poruszać się w obrębie obiektów, a do bazy danych nawet nie zaglądać, użycie typów wyliczeniowych jest bardziej przejrzyste (zainteresowanych odsyłam do Effective Java Joshui Blocha). Wspomniałem powyżej, że posiłkowanie się czystymi INTami nie jest sensowne. Jest to jednakże domyślny sposób, w jaki typy wyliczeniowe są przechowywane w bazie danych. Załóżmy, że chcielibyśmy przerobić naszego bohatera w ten sposób, aby jego profesja była właśnie typem wyliczeniowym. Stwórzmy zatem taki typ:

package com.wordpress.chlebik.jpa.domain;

public enum Profession {
    MAGE, WARRIOR, THIEF, DRUID, ARCHER
}

Oraz oczywiśie zmodyfikujmy klasę domenową naszego bohatera:

private Profession profession;

I od teraz możemy tworzyć naszego bohatera nadając mu jedną z dostępnych profesji. Co się stanie w przypadku zapisu obiektu do bazy danych? Otóż domyślnie kolejne wartości typu wyliczeniowego otrzymają wartości INT – zaczynając od zera. W 99% przypadków absolutnie nam to wystarczy, zwłaszcza jeśli nie musimy np. wyświetlać wartości tej zmiennej użytkownikowi. To ORM bierze na siebie kwestię przerobienia numerka na obiekt. Problem pojawi się, jeśli nagle postanowimy dodać do naszego typu wyliczeniowego kolejną profesję, ale wepchniemy ją między już istniejące! Spowoduje to, że np. wojownik mający do tej pory numerek 1 nagle dostanie 2. Logika naszej aplikacji totalnie rozjedzie się z bazą danych – w związku z tym lepiej takiej operacji nie przeprowadzać! Jeśli już to dopisujmy kolejne wartości ‘na końcu’ wszystkich wartości – problemy mamy z głowy.

Co jednakże zrobić jeśli cyferki nam nie starczą? Dla przykładu – nasz typ wyliczeniowy prawdopodobnie będzie dość często zmieniany (sensowność stosowania wówczas ENUMa zostawiam na boku). Wówczas najlepiej nie zdawać się na domyślne zachowanie JPA, ale należy poinstruować mechanizm jak ma się zachowywać. Wówczas możemy wykorzystać adnotację @Enumerated wraz z jej wartością @EnumType. Adnotacja @Enumerated przyjmuje domyślnie wartość EnumType.ORDINAL, czyli mapujemy wartości na liczby. Istnieje też druga opcja – EnumType.STRING. Nazwa jest dość czytelna – nasze wartości typu wyliczeniowego zostaną w bazie zapisane nie w kolumnie numerycznej, ale tekstowej. Od tej pory nie boli nas w jakikolwiek sposób kolejność zadeklarowanych wartości w typie, a także przeglądając bazę danych (grubym klientem dla przykładu) widzimy od razu w czytelnej formie jaką profesję mają zapisani gracze.

Adnotacje czasu

W pierwszym wpisie cyklu pojawiła się adnotacja @Temporal. Służy ona poinstruowania JPA w jaki sposób ma zapisać jeden z obiektów daty pochodzącego z pakietu java.util.*. Standardowo bowiem w Javie obiektami, które ‘dotykają’ bazy danych w przypadku dat i czasu są reprezentacje klas z pakietu java.sql.*java.sql.Date, java.sql.Time oraz java.sql.Timestamp. Jeśli własności naszej encji będą jednym z tych typów wówczas nie ma problemu – zostaną w pełni zrozumiane przez JPA. W przypadku klas java.util.Calendar oraz java.util.Date nie jest już tak różowo. Używając adnotacji @Temporal wraz z nadaniem jej wartości za pomocą typu wyliczeniowego TemporalType określamy który z typów z pakietu java.sql.* zostanie użyty do mapowania konkretnego pola ( DATE, TIME, TIMESTAMP ). W przypadku braku tej adnotacji (przynajmniej z użyciem Hibernate i bazy H2) został użyty typ TIMESTAMP.

Mapowanie klucza głównego

Wspomniałem wcześniej, że posiadanie klucza głównego jest jednym z sine qua non bycia encją. W związku z tym dość istotnym jest, aby jednoznaczne określenie indywidualnej było proste, ale też dość elastyczne. Najprostszym (zasadniczo też chyba najbardziej rozpowszechnionym) sposobem mapowania klucza głównego jest jednoznacznie unikalna identyfikacja encji za pomocą wartości liczbowej. Póki co to właśnie do takiego sposobu ograniczaliśmy nasz kod. Wspomniałem jednakże o elastyczności – JPA zapewnia ją poprzez możliwość wykorzystania w charakterze klucza następujących typów Javy:

• Primitive Java types: byte, int, short, long, char
• Wrapper classes of primitive Java types: Byte, Integer, Short, Long, Character
• String: java.lang.String
• Large numeric type: java.math.BigInteger
• Temporal types: java.util.Date, java.sql.Date

Możliwe jest też używanie float, double bądź ich wrapperów, ale nie jest to oczywiście zalecane.

W pierwszym wpisie generowania identyfikatorów nie dokonywaliśmy ręcznie. Jest to dość zrozumiałe – w istniejących bazach danych mamy mniej lub bardziej zaawansowane mechanizmy generowania kolejnych wartości dla różnych typów. Z definicji jest to o wiele bardziej racjonalne podejście niż ręczne zapewnianie unikalności identyfikatorów. By wykorzystać możliwość baz danych musimy użyć adnotacji @GeneratedValue – jak sama nazwa wskazuje generowanie adnotowanych nią własności przejmie JPA. Nasza adnotacja posiada dwa atrybuty – generator oraz strategy. Pierwszy z nich ma wartość tekstową i określa po prostu nazwę używanego generatora (czyli np. sekwencji w przypadku wyboru takiej strategii generowania). Atrybut strategy ( typu wyliczeniowego GenerationType ) wskazuje na sposób w jaki będą generowane kolejne identyfikatory. Oto ich lista:

  • AUTO
  • – domyślna wartość, która wskazuje na to, że to JPA (konkretnie dostawca implementacji) powinno zadecydować o użytej strategii. Nie jest to do końca bezpieczne rozwiązanie, gdyż provider może zdecydować o wykorzystaniu zasobów, do których np. może nie mieć uprawnień (jak choćby tworzenie tabeli). Najlepiej sprawdza się przy testach i prototypowaniu.

  • SEQUENCE
  • – zakłada użycie istniejącej sekwencji dla generowania kolejnych wartości. W tym momencie wchodzi do gry adnotacja @SequenceGenerator. Jedynym obowiązkowym atrybutem jest nazwa generatora, sama nazwa użytej sekwencji domyślnie jest ustawiana przez dostawcę implementacji. Zatem bohaterowie w naszej grze mogliby mieć tak generowane id:

    @SequenceGenerator(name="heroesPKGenerator", sequenceName="SEQ_HEROES_ID")
    @Id @GeneratedValue(generator="heroesPKGenerator")
    private int id;
    
  • IDENTITY
  • – jeśli baza danych umożliwia wykorzystanie specjalnego typu kolumny (np. w MySQL to kolumny oznaczone atrybutem AUTO-INCREMENT) wówczas zostanie ona użyta. Taką strategię użyłem w swoim przykładzie. Jej zaletą jest prostota i brak dodatkowych obostrzeń (np. możliwość tworzenia sekwencji). Problemem jest brak przenoszalności takiego rozwiązania między niektórymi bazami, a także brak dostępności wygenerowanego ID przed zakończeniem transakcji.

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    
  • TABLE
  • – zostanie stworzona oddzielna tabela w bazie danych dla przechowywania kolejnej dostępnej wartości dla klucza. Jest to zasadniczo najbardziej przenośna strategia – baza danych nie musi wspierać kolumn identyfikujących lub strategii, ale tworzenie tabel raczej tak 😉 Używając tej strategii tworzona jest oddzielna tabela, która zawiera dwie kolumny – w jednej podany jest identyfikator będący nazwą generatora, w drugiej zaś liczbowa wartość ostatnio użytego identyfikatora. W przypadku używania tej strategii najlepiej posiłkować się adnotacją @TableGenerator, której atrybuty umożliwiają sterowanie nazwą używanej tabeli, nazwami kolumn i szeregiem innych. Przykładowy kod mógłby wyglądać tak:

            @TableGenerator(
                name="heroesPKGenerator", 
                table="ID_GEN", 
                pkColumnName="GEN_KEY", 
                valueColumnName="GEN_VALUE", 
                pkColumnValue="ID", 
                allocationSize=1)
            @Id
            @GeneratedValue(strategy=TABLE, generator="heroesPKGenerator")
            private int id;
    

Obiekty wbudowane

Rzecz dotyczy bytów, które po angielsku nazywane są embedded objects. Są one obiektami, ale nie posiadają jednoznacznej identyfikacji – innymi słowy zaś – nie są encjami. Dane, które reprezentują są przechowywane w wierszu tabeli w bazie danych i jako takie stanowią składową encji. Myślę, że przykład z naszej gry RPG będzie bardziej wymowny. Nasza klasa bohatera zawiera kilka prostych wartości, które opisują postać. Jednakże wraz ze wzrostem ilości informacji rośnie nam stopień komplikacji encji jako obiektu Javy. Gdybym miał operować na obiekcie Javy, który posiada 20 składowych podrapałbym się po głowie i stwierdził krótko, że coś tutaj z software craftsmanship nie do końca po drodze. Temu właśnie służą embedded objects – do grupowania logicznie powiązanych właściwości.

Z całą pewnością gracze w RPG powinni posiadać jakąś gotówkę. Jednakże w realiach RPG płacenie za bułkę czystym złotem niespecjalnie ma sens. Moglibyśmy zatem wprowadzić rozróżnienie na 3 typy monet w zależności od kruszczu, z którego są wykonane. Mielibyśmy zatem monety miedziane, srebrne i złote.

private int cooper;
private int silver;
private int gold;

Klasa Hero już posiada dokładnie 5 własności. Dodanie tak prostej informacji jak ilość posiadanych pieniędzy niemal podwaja ich ilość. Jednocześnie zaś informacje o posiadanych środkach finansowych zostaną zapisane w bazie zaraz obok imienia czy profesji. Aby nasza klasa jako byt programistyczny wyglądała troszeczkę bardziej sensownie możemy zastosować klasę wbudowaną. Kod wyglądałby tak:

@Embeddable
public class FinanceState {
	
	private int cooper;
	private int silver;
	private int gold;
	
        // Gettery i settery uciete
}

Klasę Hero obdarowujemy nową własnością.

@Embedded private FinanceState finance;

Zaś podczas tworzenia bohatera dorzucamy taki kod:

FinanceState finances = new FinanceState();
finances.setCooper(10);
finances.setSilver(20);
finances.setGold(1);
h.setFinance(finances);

Czyż nie jest czyściej? W dodatku do tego obiekty klasy FinanceState mogą posiadać użyteczne metody użytkowe (czy stać mnie na zakup towaru? czy mogę wymienić np. 20 sreber na 1 złoto?). Jeśli uruchomimy teraz naszą testową aplikację w bazie danych tabela Heroes zostanie rozszerzona o 3 kolumny z ilością posiadanych monet. Używanie obiektów wbudowanych ma jeszcze jedną zaletę – jeśli zdarzy się nam sytuacja, w której potrzebowalibyśmy podobnego rozwiązania (np. w naszej grze do zapisu informacji o przeciwnikach – ile monet jakiego typu będziemy mogli uzyskać kiedy takowego deilkwenta poślemy do piachu), klasę wbudowaną możemy bez problemu użyć raz jeszcze.

Reużywalność obiektów wbudowanych może napotkać pewne problemy. Wyobraźmy sobie dopiero co wspomnianą sytuację – nasz bohater posiada kolumny nazwane dokładnie tak samo jak własności w klasie FinanceState. Jednakże tabela z wrogami została dokoptowana z innego projektu/gry i konkretne kolumny mają nazwy skrócone – co, si, gl. Mamy problem. Jednakże da się go obejść za pomocą adnotacji @AttributeOverride oraz @AttributeOverrides. Umożliwiają one dla konkretnej klasy nadpisać wartości nazw kolumn obiektu wbudowanego. Przykładowo klasa reprezentująca konkretnego wroga problem finansów mogłaby rozwiązywać w ten sposób:

    @Embedded
    @AttributeOverrides({
	    @AttributeOverride(name="cooper", column=@Column(name="co")),
	    @AttributeOverride(name="silver", column=@Column(name="si")),
	    @AttributeOverride(name="gold", column=@Column(name="gl")),
    })
    private FinanceState finance;

To tyle na dziś. W następnym artykule przypatrzymy się bliżej relacjom.

Advertisements

One thought on “ORM – podstawy

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