Zaawansowane zagadnienia ORM – nazewnictwo, złożone klasy wewnętrzne i wielopolowe klucze główne

W przygotowaniach do certyfikacji OCEJPA zbliżamy się powoli do końca. Dzisiaj rozpocznę pierwszą część bardziej zaawansowanych tematów ORM. Ostatni wpis będzie dotyczył dziedziczenia w JPA i odwzorowywania tego w bazie danych.

Nazewnictwo tabel i kolumn

Do tej pory w naszych przykładach najczęściej grzecznie godziliśmy się na domyślne zachowanie JPA, które nazwę tabeli bezpośrednio kojarzyło z nazwą klasy encji. Zazwyczaj bazy danych nie robią z tym problemów – czyli nazwy kolumn i tabel są case-insensitive. Jednakże w zależności od bazy można czasem wymusić case-sensitivity. Co wtedy jeśli z bazy korzystamy z pomocą JPA? Jest to dość proste – korzystając z adnotacji @Table można wymusić użycie cudzysłowów w nazwie, a tym samym (jeśli dostawca bazy danych to obsługuje) korzystać z tabeli rozróżniając małe i wielkie litery. Wygląda to tak:

@Table(name="\"Dragon\"")

Oczywiście jeżeli pracujemy z bazą skonstruowaną w ten sposób wówczas adnotowanie w ten sposób wszystkich encji (lub też kolumn za pomocą adnotacji @Column) nie jest zbyt wygodne. Z pomocą może przyjść nam element konfiguracji JPA w XMLu o nazwie delimited-identifiers. Dzięki temu elementowi wszystkie wywołania SQL nazw tabel i kolumn zostaną opakowane w cudzysłowy.

Złożone obiekty wbudowane

O obiektach wbudowanych pisałem w jednym z wcześniejszych wpisów. W prosty sposób udało się nam opakować stan posiadania naszego bohatera w oddzielny obiekt, jednocześnie przechowując informację o tym w tej samej tabeli bazy daynych. Od wersji 2 specyfikacji JPA obiekty wbudowane posiadają też szereg innych możliwości – mogą zawierać w sobie inne obiekty, posiadać kolekcje oraz posiadać relacje do innych obiektów. Wszystko to jest możliwe wyłącznie przy założeniu, że to posiadająca encja jest podmiotem wszystkich tego typu relacji. Encja wbudowana nigdy nie występuje w oderwaniu od swojej encji posiadającej!

By sensownie pokazać działający kod musimy wrócić do przykładu ze stanem posiadania naszego bohatera. Wbudowana encja wygląda obecnie tak:

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

Zaś własność w encji bohatera wyglądała tak:

@Embedded    
private FinanceState finance;

Nasz obiekt wbudowany jest póki co dość ubogi – nie bardzo widać potencjał do rozszerzania go o relacje. Jednakże słusznie nazwaliśmy go dawno temu FinanceState – nasz bohater przecież może posiadać nie tylko pieniądze. Co z klejnotami czy posiadłościami? Bez problemu możemy wrzucić tego typu informacje do obiektu przechowującego stan posiadania. By zatem dać pokaz możliwości dodamy kolejną encję – Estate (reprezentującą posiadłość ziemską, a co, bohaterowie to nie zawsze przymierający głodem wiedźmini 😉 ).

@Entity
public class Estate {

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

    // Gettery i settery pominiete dla czytelnosci
}

Teraz naszą klasę z informacjami o stanie posiadania możemy poszerzyć o taką deklarację:

@ElementCollection        
private Set<Estate> estates;

I jesteśmy w domu. Przy uruchomieniu kodu w bazie danych pojawią się tabele Estate oraz tabela łącząca o nazwie Heroes_Estate (nie zmienialiśmy ustawień domyślnych). Tak jak pisałem encje tego typu mogą również posiadać relacje do innych encji. Jednakże używany przeze mnie model encji niespecjalnie może coś takiego pokazać, zatem by nie płodzić miliona klas by pokazać prosty koncept podam link do przykładowego kodu (zresztą cały materiał jest wart przeczytania).

Wielopolowe klucze główne

W idealnym świecie encje są zawsze jednoznacznie identyfikowane przez klucz główny będący kolumną liczbową. Niestety czasami zdarza się (głównie w starszych bazach, choć takie rozwiązanie może mieć uzasadnienie biznesowe), że klucz główny składa się z kilku kolumn. W przypadku JPA implementacji takiego rozwiązania możemy dokonać na dwa różne sposoby – obiektu wbudowanego reprezentującego klucz główny, albo też zwykłej klasy reprezentującej klucz główny.

Zaczniemy od wbudowanej klasy klucza głównego, gdyż moim zdaniem jest to prostsze rozwiązanie. Klasa taka nie różni się absolutnie niczym od zwykłej klasy wbudowanej, istotne jest tylko to, że by występować w roli reprezentanta klucza głównego, musi ona implementować metody hashCode() oraz equals(), aby dostawca JPA mógł jednoznacznie porównywać instancje między sobą. Klasa taka (zresztą zwykła klasa zewnętrzna, o której napiszę później też) musi posiadać bezparametrowy konstruktor, być serializowalną, a także nie powinna mieć metod ustawiających (bo co to za klucz główny, który można zmienić). Załóżmy zatem, że chcielibyśmy naszego bohatera identyfikować nie za pomocą zwykłej liczby, ale kombinacji jego imienia i daty narodzin. Wbudowana klasa wyglądałaby tak:

@Embeddable
public class HeroId implements Serializable {
	
    private String name;
    private Date creationDate;

    public HeroId() {}
            
    public HeroId(String name, Date creationDate) {
        this.name = name;
        this.creationDate = creationDate;
    }

    public String getName() {
        return name;
    }

    public Date getCreationDate() {
        return creationDate;
    }      
   
}

Tym samym musimy zmodyfikować klasę Hero – na sam początek musimy usunąć istniejące w niej pola z imieniem i datą kreacji. Występowanie podwójnych pól spowoduje błąd kompilacji. Dodajemy też nowy konstruktor, a także adnotujemy pole identyfikatora. Ogólnie zmiany wyglądają tak:

@EmbeddedId private HeroId id;

public Hero( String name, Date creationDate ) {
        this.id = new HeroId(name,creationDate);
}

I tyle. Uruchomienie kodu nie zmieni nic w strukturze bazy danych – jedyne co ulegnie zmianie to sposób odwoływania się do obiektu (np. jego wyszukiwanie). Metoda find() EntityManagera pobiera poza typem encji klucz główny. Jednakże deklaracja metody zakłada, że identyfikator jest typu Object, zatem bez problemu możemy przekazać odpowiedni obiekt identyfikujący.

Drugim podejściem jest zwykła klasa klucza głównego. Zgodnie z nazwą jest to zwykła klasa, która musi spełniać te same warunki co klasa wbudowana (serializowalność, konstruktor, equals(), itp). Podstawową różnicą w stosunku do klasy wbudowanej jest to, że pola, na których oparty jest klucz pozostają wciąż w głównej encji, ale występują też w klasie identyfikującej.

Wracając do naszego bohatera klasa klucza głównego wygląda tak:

public class HeroId implements Serializable {
	
    private String name;
    private Date creationDate;

    public HeroId() {}
            
    public HeroId(String name, Date creationDate) {
        this.name = name;
        this.creationDate = creationDate;
    }

    // Gettery i equals/hashCode pominiete     
   
}

Zaś w samej encji bohatera przywracamy pola z imieniem i datą kreacji, ale adnotujemy je za pomocą adnotacji @Id, zaś samą klasę adnotujemy z użyciem @IdClass.

@Entity
@Table( name = "HEROES" )
@IdClass(HeroId.class)
public class Hero {

@Id
@Temporal(TemporalType.TIMESTAMP)
private Date creationDate;
    
@Id
private String name;

// Reszta obcieta dla czytelnosci

}

I tyle. Wyszukiwanie encji odbywa się dokładnie tak samo jak w przypadku klasy wbudowanej. Moim zdaniem ten drugi sposób jest trochę bardziej czytelny – patrząc na kod klasy widzimy co jest kluczem, ale też jakie potencjalnie własności posiada klasa. Używanie klasy wbudowanej ma więcej sensu, jeśli własności, które składają się na klucz główny są ‘techniczne’ i raczej nie są nigdzie prezentowane.

Advertisements

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