Bo ja chciałbym poczytać o Springu

spripr Spring to słowo-wytrych w świecie Javy. Pojawia się w olbrzymiej ilości projektów, w ogłoszeniach o pracę, sporo technologii ocenia się porównując je właśnie do Springa. Powiedziałbym, że czasem boję się otworzyć lodówkę…

Z książkami poświęconymi Springowi w języku polskim zawsze był problem. Jeden problem. Pojawiały się kiedy opisywana przez nie wersja Springa była już dość leciwa. Dlatego też rzecz jasna pozostawały materiały sieciowe albo wydania angielskojęzyczne. Zasadniczo nie bym miał z tym problem, ale miło jest czasem poczytać coś w rodzimym języku. Jakiś czas temu trafiła w moje ręce książka Spring w praktyce, która moim zdaniem, jest bardzo ciekawą pozycją.

Przede wszystkim początkujący otrzymują kilka pierwszych rozdziałów, które wyraźnie przedstawiają o co chodzi w tym całym frameworku, a także dostarczają gotowych przykładów w jaki sposób rozpocząć pracę ze Springiem. Bardziej obeznani z tematem programiści dostają z kolei kilkanaście rozdziałów przedstawiających interesujące możliwości wykorzystania Springa – testy integracyjne, integracja za pomocą Spring Data czy obsługa użytkowników z pomocą Spring Security. Mając do wyboru reference-docs w witrynie SpringSource, a tę zgrabną książkę, na początek zdecydowanie wybrałbym papier – to dobre miejsce by rozpocząć bardziej szczegółowe dociekania.

Koniec kursu MongoDB

Kilka tygodni temu informowałem o rozpoczęciu kursu MongoDB dla developerów Javy. Kurs oficjalnie skończył się 15 lipca, kilka dni przed tym dniem odpowiedziałem na wszystkie 10 pytań z finalnego testu. Kolejny certyfikat do kolekcji 😉

Przy okazji chciałbym napisać kilka słów. Muszę przyznać, że cała koncepcja kursu, jego wykonanie i sposób oceniania jest fantastyczny. W skrócie – cały kurs podzielony jest na 6 tygodni. W każdym z tych tygodni otrzymujemy dostęp do nowej porcji filmików edukacyjnych, które przedstawiają kolejne zagadnienia MongoDB. Każde zagadnienie najczęściej kończy się pytaniem (tzw. quizem), które nie jest w jakikolwiek sposób oceniane, ma służyć tylko utrwaleniu wiedzy.

Każdy tygodniowy blok kończy się kilkoma pytaniami egzaminacyjnymi. I tutaj moim zdaniem leży klucz do sukcesu tego kursu. Pomijając pierwszy tydzień (kiedy lekcje są krótkie, zadania zaś bardzo proste) jesteśmy zmuszeni do używania dedykowanej aplikacji o nazwie MongoProc. Z góry uprzedzam, że skonfigurowanie tego wszystkiego trochę trwa. Dlatego na realizację zadań z drugiego tygodnia dobrze jest zarezerwować sobie trochę więcej czasu. MongoProc to aplikacja desktopowa, której celem jest weryfikacja poprawnego wykonania zadań z kolejnych lekcji (nie wszystkich, ale większości). Gotowe rozwiązania są testowane przez aplikację (pod spodem strzela do odpalonego lokalnie kontenera servletów lub MongoDB), a następnie istnieje możliwość przesłania odpowiedzi do organizatorów kursu.

Zadania w dużej mierze polegają na modyfikacji lub napisaniu kodu Java (w końcu kurs jej dotyczy). Gotowe projekty są dostarczane przez organizatorów, my tylko je importujemy do IDE, wprowadzamy swoje zmiany i tyle. Po weryfikacji poprawnego działania przez MongoProc zadanie można uznać za rozwiązane.

Ostateczna ocena kursu składa się w 50% z wyniku wykonania zadań domowych, a w 50% z oceny z testu końcowego. Dlatego opłaca się poświęcić zadaniom domowym trochę czasu i mieć z nich dobre wyniki. Końcowym testem nie załatwimy wszystkiego (punkt zdawalności to bodajże 65% łącznej ilości możliwych do zdobycia punktów). Drugą rzeczą jest czas potrzebny każdego tygodnia na zapoznanie sięę z materiałem i wykonanie zadań – organizatorzy podają orientacyjne wartości i nie są one zbyt przesadzone. Zabieranie się za to w poniedziałek wieczorem nie jest dobrym pomysłem 😉

Na sam koniec pozostaje mi tylko po raz kolejny zarekomendowanie tego kursu wszystkim zainteresowanym MongoDB. Wprowadza w świat NoSQL bardzo płynnie, jakość filmów oraz kodu jest bardzo dobra, z całą pewnością czas spędzony na treningu nie będzie czasem straconym.

Zaawansowane zagadnienia ORM – dziedziczenie encji

To już ostatni materiał z książki dotyczący JPA jako takiego. Temat jednak dość istotny, zwłaszcza w różnego typu legacy-systems, których model danych potrafi być delikatnie mówiąc – zagmatwany. Napiszę dziś o dziedziczeniu encji.

Podstawy

Encje jako takie są zwykłymi klasami Javy. Tym samym jak najbardziej mogą rozszerzać inne klasy, a także być rozszerzane. Model obiektowy pozwala bardzo efektywnie przedstawić złożoność rozwiązywanych problemów biznesowych poprzez sensowne zastosowanie dziedziczenia. Z drugiej zaś strony mamy relacyjną bazę danych, która opiera się o tabele i relacje. Nie za wiele wie ona o dziedziczeniu. Istnieje kilka sposobów na rozwiązanie tego problemu.

Podstawowym pojęciem przy dziedziczeniu encji jest mapped superclass. Jest to klasa oznaczona adnotacją @MappedSuperclass. Oznaczenie nią encji spowoduje, że jej własności i relacje zostaną odzwierciedlone w encjach dziedziczących po tej klasie (i nie chodzi o same pola Javy, ale o rozpoznanie tego faktu przez JPA). Jednakże klasa taka sama w sobie nie posiada oddzielnej tabeli w bazie danych! Klasa taka nie musi być również deklarowana jako abstrakcyjna, jednakże jest to dobra praktyka – klasa taka nie powinna mieć instancji – powinna być reprezentowana tylko i wyłącznie przez klasy dziedziczące po niej. Klasy oznaczone tą adnotacją nie mogą również być używane do wyszukiwania oraz w zapytaniach (np. jPQL). Służą tylko jako opakowanie podstawowych własności w jednym miejscu, tak by klasy po niej dziedziczące zawierały tylko specyficzne siebie pola czy zachowania.

Należy wspomnieć, że same encje też mogą być abstrakcyjne. Możemy ich normalnie używać w aplikacji, jedynie nie możemy fizycznie tworzyć ich instancji, ale mogą bez problemu służyć jako typ obiektu, który po nich dziedziczy.Zanim przejdę do przykładów kodu dodam jeszcze, że dziedziczenie przez encje po zwykłych klasach Javy jest oczywiście możliwe. Te zwykłe klasy nazywa się transient-classes – oczywiście dziedziczy się po nich zachowanie oraz stan, ale nie są one odzwierciedlane w bazie danych.

Poznaliśmy już adnotację @MappedSuperclass. Wspomniałem również, iż nie posiadają one swojej reprezentacji w bazie danych (w formie tabeli). Dlatego też za podstawę ‘bazodanową’ dziedziczenia uznaje się klasę będącą encją. Może ona rozszerzać klasy oznaczone @MappedSuperclass, ale oczywiście nie musi. W specyfikacji JPA zostały przedstawione trzy strategie realizacji dziedziczenia po stronie bazy danych. Są to Single-Table Strategy, Joined Strategy oraz przedstawiona, ale niewymagana do implementacji przez dostawców Table-per-Concrete-Class Strategy. Przedstawię je poniżej opierając się na przykładzie encji broni, którą może używać bohater w naszej wzorcowej implementacji gry RPG. Do tej pory broń była reprezentowana przez jedną encję – Weapon. Dziś dodamy trochę urozmaicenia.

Single-Table Strategy

Zgodnie z nazwą dziedziczenie jest realizowane w oparciu o jedną tabelę w bazie danych. Automatycznie implikuje to istnienie w niej kolumn, które będą w stanie przechować stan wszystkich encji, które realizują dziedziczenie w oparciu o tę metodę. Przerobimy zatem naszą encję reprezentującą broń na trzy encje, które będą reprezentowały różne typy broni.

@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="WEAPON_TYPE",discriminatorType=DiscriminatorType.STRING)
public abstract class Weapon  {

    @Id
    @GeneratedValue(generator="increment")
    @GenericGenerator(name="increment", strategy = "increment")
    private Long id;
        
    private String name;   
    
    // Getterki i setterki ominiete	
}

@Entity
@DiscriminatorValue("StingWeaponDiscValue")
public class StingWeapon extends Weapon {

      private int stingAttack;

      // Getterki i setterki ominiete
}

@Entity
@DiscriminatorValue("RangeWeaponDiscValue")
public class RangeWeapon extends Weapon {

      private int rangeAttack;

      // Getterki i setterki ominiete
}

Jak widać trochę się tutaj dzieje. Zaczniemy od encji Weapon – jest to klasa abstrakcyjna, która jednakże zostanie odwzorowana na tabelę, gdyż posiada adnotację @Entity. Jest ona również oznaczona adnotacją @Inheritance, która pozwala określić typ użytej strategii dziedziczenia. Jeżeli adnotacja ta nie zostanie użyta, albo też nie zostanie podany typ strategii, wówczas zostanie zastosowany domyślny typ czyli właśnie Single-Table Strategy. By jednak pokazać możliwości tej adnotacji w powyższym przykładzie podałem wartość tego atrybutu. Użyta została też adnotacja @DiscriminatorColumn. Określa ona nazwę kolumny w tabeli, w której będzie przechowywana informacja o klasie, która ma zostać użyta do stworzenia instancji encji. Podajemy też typ kolumny – domyślnie jest to podana przeze mnie wartość STRING, ale możliwe są też INTEGER oraz CHAR.

Poszczególne klasy dziedziczące po encji broni są dość proste – dodajemy tylko jedną własność określającą wartość punktową zadawanego ataku. Jedyną nowością jest dodanie adnotacji @DiscriminatorValue, której znaczenie jest dość oczywiste. W przypadku jej braku dostawca JPA użyje nazwy klasy. To wszystko. Teraz w kodzie aplikacji możemy stworzyć takie cos:

entityManager.getTransaction().begin();
                
StingWeapon sword = new StingWeapon();
sword.setName("Super-Duper-Miecz");
sword.setStingAttack(10);
entityManager.persist(sword);
                
RangeWeapon bow = new RangeWeapon();
bow.setName("Super-Duper-Dlugi-Luk");
bow.setRangeAttack(10);
entityManager.persist(bow);                                            
                
entityManager.getTransaction().commit();

W bazie danych po uruchomieniu powyższego otrzymamy taki efekt:

ZrzutBroni

Jak widać niewykorzystane kolumny zostały wypełnione wartościami NULL. Kolumna zawierająca informacje o typie klasy została wypełniona automatycznie wartościami, które określiliśmy w definicji encji. Wszystko pięknie działa 😉

Joined Strategy

Zastosowanie strategii jednej tabeli może być na dłuższą metę dość zasobożerne. Nagle okazuje się, że w przypadku rozbudowanych hierarchii nasza tabela zawiera dużą ilość kolumn, które w znacznej mierze zawierają wartości NULL. Jeżeli nasza hierarchia dziedziczenia będzie dość rozbudowana można zastosować inną strategię – joined strategy. Wykorzysta ona siłę współczesnych baz danych – relacje. W przypadku naszego kodu wystarczy, że zmienimy wartości w adnotacjach, aby stworzyć podstawową tabelę z bronią (klasa Weapon), zaś dodatkowe informacje przechowywane w każdej z klas zostaną zapisane w oddzielnych tabelach.

@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="WEAPON_TYPE",discriminatorType=DiscriminatorType.STRING)
public abstract class Weapon { ... }

I ponowne uruchomienie naszego kodu spowoduje utworzenie 3 różnych tabel:

ZrzutBroni

O dziwo nie zostały wygenerowane kolumny rozróżniające, ale taki kod działa:

 entityManager.getTransaction().begin();                

 RangeWeapon bowNew = (RangeWeapon) entityManager.find(Weapon.class,2L);
 System.out.println("Range attack: " + bowNew.getRangeAttack() );
 System.out.println("Name: " + bowNew.getName());

 entityManager.getTransaction().commit();

W konsoli (po uprzednim wyczyszczeniu persistence-context) generowane jest takie zapytanie SQL:

select weapon0_.id as id1_10_0_, weapon0_.name as name2_10_0_, weapon0_1_.rangeAttack as rangeAtt1_8_0_, weapon0_2_.stingAttack as stingAtt1_9_0_, case when weapon0_1_.id is not null then 1 when weapon0_2_.id is not null then 2 when weapon0_.id is not null then 0 end as clazz_0_ from Weapon weapon0_ left outer join RangeWeapon weapon0_1_ on weapon0_.id=weapon0_1_.id left outer join StingWeapon weapon0_2_ on weapon0_.id=weapon0_2_.id where weapon0_.id=?

Co wiele mówi o wydajności zapytań w przypadku używania tego typu strategii. Jednakże jeśli łatwość odwzorowania modelu oraz porządek logiczny w bazie danych jest dość istotny wówczas jest to rozwiązanie warte rozważenia.

Table-per-Concrete-Class Strategy

Jest to strategia, która niekoniecznie musi być zaimplementowana przez naszego dostawcę. Jej znaczenie jest w sumie proste – każda klasa w hierarchii dziedziczenia otrzymuje swoją własną tabelę. Po zmianie wartości adnotacji @Inheritance na InheritanceType.TABLE_PER_CLASS nasz kod po uruchomieniu spowoduje wygenerowanie ponownie 3 tabel, ale wyglądających tak:

ZrzutBroni

Zatem w bazie będą występowały informacje w sposób zdublowany. Jednakże rozwiązanie takie jest o wiele szybsze niż strategia relacyjna – zapytanie wybierające rekordy jest kierowane od razu do konkretnej tabeli, z pominięciem złączeń. Zatem o ile miejsce zajmowane przez dane nie jest istotne, albo też dane w bazie są prawie niezmienne, zaś często odpytywane warto zwrócić uwagę na tę strategię.

Próbując odpowiedzieć sobie na pytanie, która strategia jest ‘najlepsza’, odpowiedź będzie prosta – to zależy. Dość ciekawą dyskusję w tym temacie można znaleźć w wątku na GoldenLine.