#14 – Spring Data

By 27 listopada 2014Kurs Javy
Wpis-Header (1)

W dzisiejszej lekcji poznamy narzędzie, które znacznie uprości i skróci nam pracę, związaną z obsługą bazy danych. Czy pamiętasz, jak w ostatniej lekcji pisałaś kod pobierający listę obiektów z bazy danych? Wymagało to utworzenia nowej metody, w niej utworzenia zapytania za pomocą EntityManagera, pobrania wyników i obsłużenia sytuacji, w której nie ma wyniku lub otrzymujemy wyjątek. A gdybym powiedział, że wystarczy tylko pierwszy krok, nazwa metody? Przedstawiam zatem Spring Data :)

Lekcja

Interfejsy

Na wstępie powiemy sobie o interfejsach – do tej pory nie używaliśmy ich bezpośrednio, dzisiaj będą nam one niezbędne. Interfejsy tworzymy podobnie do klas, przykładowy interfejs wygląda następująco:

public interface Interfejs {
    public void zrobCos();
}

Jak zapewne sama widzisz, deklarujemy metody (nazwy, zwracany typ, argumenty), ale nie tworzymy ich implementacji – tym zajmuje się klasa która implementuje interfejs:

public class ImplementacjaInterfejsu implements Interfejs {
    //...
}

Interfejsy służą do ustalania kontraktu pomiędzy modułami (czyli sposobu ich interakcji, korzystania z siebie nawzajem) – podczas gdy implementacja może się zmieniać, sam interfejs pozostaje ten sam, dzięki czemu ukrywamy nieistotne szczegóły przed innymi modułami. Więcej o zastosowaniu interfejsów i dlaczego są tak ważne w programowaniu obiektowym powiemy sobie w dalszych lekcjach, kiedy będziemy pracować nad warsztatem.

Dodawanie Spring Data do projektu

Maven

Do pom.xml dodajemy zależnosć do org.springframework.data:spring-data-jpa

Konfiguracja Spring XML

Aby SpringData poprawnie ‚widziało’ nasze repozytoria, musimy dodać następujący fragment:

<jpa:repositories base-package="pl.kobietydokodu"/>

Podobnie jak w przypadku JPA i mapowań, base-package musi wskazywać na pakiet, pod którym są wszystkie nasze repozytoria.

Aby plik XML nadal był poprawny, musimy do tagu <beans> (na samej górze) dodać dwa elementy: atrybut xmlns:jpa=”http://www.springframework.org/schema/data/jpa” oraz linijkę „http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd” (bez cudzysłowów) wewnątrz atrybutu schemaLocation

Spring Data

Spring Data to narzędzie, które bazuje na JPA – nadal potrzebujemy więc EntityManagerFactory w naszej konfiguracji Springa. Różnica w stosunku do JPA jest taka, że biblioteka ta na podstawie samej nazwy metody buduje zapytanie i obsługuje zwracanie danych, niektóre wyjątki itp. Jednocześnie, jeśli potrzebujemy specyficznych funkcjonalności, sami możemy zdefiniować zapytanie które ma być użyte (domyślnie jest ono automatycznie generowane na podstawie nazwy metody).

Jak się pewnie domyślasz, w takiej sytuacji nazewnictwem metod rządzą pewne reguły – masz oczywiście rację, są pewne zasady ale są one proste do opanowania i logiczne, ale wszystko po kolei :)

Przykładowe DAO, które używa Spring Data wygląda następująco:

@Repository
public interface Interfejs extends Repository<Kot, Long> {
    public Kot findById(Long id);
}

Zwróć uwagę na fragment extends Repository<Kot, Long> – ten fragment mówi Springowi, że mamy do czynienia z repozytorium zarządzanym przez Spring Data.

Taki obiekt używamy jak każdy inny w Springu – dodajemy go jako pole naszej klasy (np. kontrolera) i oznaczamy adnotacją @Autowired :

public class Kontroler {

    @Autowired
    protected Interfejs dao;

    //...

}

Spring Data samo stworzy implementację naszego interfejsu i wstrzyknie ją w tym miejscu.

Z pewnością zauwazyłaś też adnotację @Repository – to adnotacja, która mówi Spring Data, że ta klasa odpowiada za dostęp do danych.

Konstruowanie zapytań na podstawie nazw metod

Szczegóły znajdziesz w dokumentacji, ale tutaj omówimy kilka najważniejszych elementów i podstawowych przykładów.

findBy{Query}

Podstawowe metody do pobierania zaczynają się od frazy findBy, po czym tworzymy właściwe zapytanie. Załóżmy, że chcemy pobrać obiekt na podstawie pól (mówimy o JPA i obiektach, więc patrzymy wg pól klas a nie kolumn tabel!) imie oraz kolorFuterka. Oba te pola są typu String, chcemy pobrać listę rekordów, które spełniają oba te kryteria. Nasza deklaracja metody będzie wyglądała więc następująco:

List<Kot> findByImieAndKolorFuterka(String imie, String kolorFuterka);

Jak sama widzisz, zamiast pisać zapytanie w HQL, wystarczy:

  • określić typ zwracany przez metodę (u nas lista obiektów typu Kot)
  • rozpocząć nazwę od findBy
  • Wypisać po kolei nazwy pól, po których chcemy filtrować, łącząc je spójnikiem Or lub And
    • Można uzywać też dodatkowych modyfikatorów, np. AndWiekGreaterThan albo AndImieStartsWith, szczegóły znajdziesz w dokumentacji
  • Zadeklarować argumenty metody w kolejności (i o typach) w jakiej pojawiają się w zapytaniu (nazwie metody)

Prawda, że banalne?

Tworzenie/aktualizacja obiektów

Aby udostępnić metody do tworzenia/aktualizacji obiektów, wystarczy aby nasz interfejs rozszerzał interfejs CrudRepository. Bazując na przykładzie powyżej:

@Repository
public interface Interfejs extends CrudRepository<Kot, Long> {
    public Kot findById(Long id);
}

Rozszerzając CrudRepository musimy podać typ obiektu oraz typ klucza głównego obiektu (tego pola, które ma adnotację @Id) – w naszym przypadku będą to odpowiednio typy Kot oraz Long . Sama konstrukcja to tzw. generics, o których powiemy sobie w lekcjach służących szlifowaniu warsztatu :)

Po rozszerzeniu możemy użyć metody save(Kot kot), która zarówno aktualizuje obiekt (jeśli obiekt ten ma id, które znajduje się w bazie danych) lub tworzy nowy rekord.

Podsumowanie

Celem ostatnich 3 lekcji było nie tylko pokazanie, jak korzystać z technologii, które można znaleźć w projektach z którymi się spotkasz, ale także aby pokazać Ci, jak zmienia się programowanie i w jakim kierunku te zmiany idą. Rozwój bibliotek i narzędzi dąży do tego, aby programista musiał pisać jak najmniej tzw. boilerplate code (mówiliśmy o nim w lekcji o JDBC, w skrócie jest to kod, który nie realizuje żadnej szczególnej funkcjonalności, a musimy go napisać żeby inne elementy działały).

Zadanie

Zmodyfikuj program, który już napisałaś tak, aby zamiast EntityManagera używał Spring Data.

Licencja Creative Commons

Jeśli uważasz powyższą lekcję za przydatną, mamy małą prośbę: polub nasz fanpage. Dzięki temu będziesz zawsze na bieżąco z nowymi treściami na blogu ( i oczywiście, z nowymi częściami kursu Javy). Dzięki!

  •  
  •  
  •  
  •  
  •  
  • Klara

    Cześć,
    A co zrobić jeśli mi się pojawiło:
    CrudRepository cannot be resolved to a type?

    Wszystko ustawione jak pisaliście.

    • Klara

      Już sobie poradziłam. Ale mam pytanie: czy spring data powoduje, to, że jeśli dotychczas miałam: KotDao i KotDaoImpl to teraz KotDaoImpl staje się zbędne, dobrze rozumiem?
      W takim razie co się dzieje z Service? Czy KotService również podmieniam na Repository i KotServiceImpl jest mi zbędne?

      Piszę ten projekt trochę po swojemu, korzystając przy okazji z Waszego poradnika ;)

      • Jeśli chodzi o Dao to masz rację – KotDaoImpl staje się zbędnę. W przypadku serwisów nie możemy ich usunąć – Spring Data ‚generuje’ nam obiekty, które pozwalają na bezpośrednie korzystanie z bazy danych. W serwisach mamy z kolei logikę biznesową (czyli np. dodatkowo logowanie, zapisywanie czy modyfikacja informacji w wielu miejscach, jeśli to potrzebne itp) – pomimo tego, że obecnie są tylko ‚pośrednikiem’ do korzystania z Dao.

  • Łukasz Kołakowski

    Nic z tej lekcji nie można zrozumieć, gdzie dodać poszczególne linie kodu?. Albo bardziej rozbudować tą lekcje albo dodawać przykłady do ściągnięcia.

    • Cześć, staramy się, aby lekcje te były kontynuacją po sobie i większość opisów odnosi się do tego, jak zmienić coś, co było omawiane w poprzedniej lekcji.
      W którym miejscu masz największy problem? Może możemy jakoś to przeformułować, żeby było czytelniej?
      Nad rozwiązaniami pracujemy, ale też prosimy o nieco wyrozumiałości – nie da się zrobić wszystkiego na raz ;)

  • piotrowicki

    Mam pytanie nie bezpośrednio do Spring Data, ale bezpośrednio z nim powiązane. Otóż szukam w miarę skutecznego i prostego sposobu na paginację/sortowanie danych zawartych w tabelach. Wiem że metoda kontrolera powinna przyjmować interfejs Pageable:

    @RequestMapping(„/users”)
    public String list(Model model, Pageable pageable, …)
    {
    Page pageable = new PageRequest(pageable);
    model.addAttributte(„users”,pageable.getContent());
    }

    Następnie owe implementacje Spring Data podawane są dalej do Repository gdzie zwracany jest wynik wyszukiwania. Mam problem z tym jak skonstruować URL dla atrybutu kontrolera „Pageable” i jak go poźniej wyświetlić w .jsp? Podobnie ma się sprawa z sortowaniem po nagłowkach tabel… czy poprawnym adresem może być: users?page=4&sort=name&order=desc i przesyłanie parametrow do kontrolera po czym przetwarzanie ich i odsyłanie do .jsp? Robiłem tak, ale muszę przyznać że powoduje to sporo chaosu wewnątrz metody.. Jak jest to rozwiązane w profesjonalnych aplikacjach z użyciem Springa? Dziękuję za odpowiedź.

    • Cześć, bardzo ciekawe pytanie :) Przede wszystkim – raczej odradzałbym używanie obiektów SpringData jako DTO w kontrolerach czy do przekazywania do widoku.

      Co do adresu URL – z uwagi na to, że parametry sortowania i paginacji są (najczęściej) opcjonalne, budowanie ładnego adresu url z ich użyciem jest trudne i niewygodne – podanie ich jako ?param=value&param2=value2 jest jak najbardziej poprawną praktyką i często stosowaną.

      Co do samego sposobu budowania i korzystania z tego obiektu – czy myślałeś nad metodą kontrolera z adnotacją @ModelAttribute ? W ten sposób logikę konstruowania obiektu stronicowania wyciągniesz do osobnej metody, która nie będzie śmieciła innych metod, zyskasz też rozwiązanie, które możesz stosować wszędzie, gdzie używasz tabelek z danymi. Pamiętaj tylko, ze atrybuty są opcjonalne i odpowiednio się przed tym zabezpiecz ;) Polecam też wpis http://ankursinghal86.blogspot.in/2014/07/how-modelattribute-annotation-works-in.html

      • piotrowicki

        Dziękuję za odpowiedź, aczkolwiek przyznam że zbiła mnie ona trochę z tropu :) O ile dobrze rozumiem należałoby by zrobić metodę oznaczoną adnotacją @ModelAttribute która przechwyci parametry żadania(page, sort, order) opakuje je w coś na kształt PageRequest i udostępni dla innych metod kontrolera okraszonych @RequestMapping?

        Co do pierwszego akapitu poprawniejszym rozwiązaniem było by wysłanie do widoku atrybutu „users” w postaci listy i page osobno w celu iterowania?

  • MILO

    Tego interfejsu używamy zamiast naszej dotychczasowej klasy DAO, czy implemetujemy go w niej?

    • Tak, używamy go zamiast dotychczasowej klasy DAO – Spring Data sam dostarczy nam obiekt, który go implementuje

  • Paweł Kalbarczyk

    Witajcie. Napotkałem na dwa problemy związane z bazą danych.

    Po pierwsze, nie mam w MySQLu polskich znaków, ustawiłem kodowanie na utf8 z poradnika z internetu, ale znaków polskich dalej nie ma.

    Drugi problem to fakt, że Spring Data przy użyciu metody save() inkrementuje poprzez @GeneratedValue id wiersza. Mam zaimplementowaną metodę, która ten wiersz kasuje, ale niestety Spring Data dalej generuje indeks wiersza o jeden większy, nie uwzględniając tego usunięcia. Problem pojawia się w tym, że nie mogę wejść na szczegóły danego wiersza, ponieważ jest to inny (większy) indeks.

    • Cześć,
      kodowanie w MySQL może dotyczyć wielu elementów: pola, całej tabeli, bazy danych, sesji oraz połączenia. Upewniej się, że deklaracje pól i tabel korzystają z utf8, a także że Twoje połączenie jest nawiązywane z obsługą unicode (do ciązgu znaków określającego Twoją bazę dodaj &characterEncoding=utf8 na końcu (lub ?characterEncoding=utf8 , jeśli nie masz podanych innych parametrów).

      Co do inkrementacji id przy save, obawiam się że powinieneś poszukać rozwiązania poprzez zmianę podejścia w kodzie niż w bazie danych. Spring Data bazuje na JPA do obsługi generowania kolejnego ID, z kolei JPA bazuje na mechanizmach bazy danych, o ile to możliwe. W przypadku MySQL ten mechanizm to AUTO_INCREMENT, który nie uwzględnia kasowanych wierszy. Co więcej – nie uwzględnia też wierszy już dodanych (jeśli zmienisz ręcznie wartość tego parametru, może wystąpić błąd przy dodawaniu, jeśli wskazany id jest już w użyciu). Ma to swoje powody związane z wydajnością i kosztem takiej walidacji, dlatego we wszystkich znanych mi silnikach baz danych działa ona w ten sam sposób – ignorując operacje delete.

      Daj znać do czego potrzebujesz takiego mechanizmu, być może będziemy w stanie pomóc wymyślić jakieś alternatywne podejście :)

      • Paweł Kalbarczyk

        Dziękuje za odpowiedz :-)
        Kodowanie znaków w mysql sprawdzę dziś wieczorem. Atrybut auto increment w polu Id powinien być aktywny? Tak mam teraz.

        Co do zwiększania indeksu, to potrzebuje takiej funkcjonalności: dodanie listy treningu do bazy treningów, jak i możliwość usunięcia go klikając na link w ostatnim wierszu tabeli. Po usunięciu danego wiersza indeksy nie są cofane. W planie jest rownież możliwość edycji informacji o danego treningu.

        A tak w ogóle, to bardzo ciekawy kurs:-)

        • Paweł Kalbarczyk

          Co do kodowania bazy:
          W STS mam ustawione kodowanie na UTF8, więc tu jest ok.

          W połączeniu do bazy danych dodałem też kodowanie:

          Co do kodowania bazy i tabel, użyłem w mysqlu:

          ALTER DATABASE trening CHARACTER SET utf8 COLLATE utf8_unicode_ci;
          ALTER TABLE treningi CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;

          Niestety, pomimo tych zmian sytuacja kompletnie się nie zmieniła:-(
          Jakieś inne pomysły?

          • Spróbuj zamienić jdbc:mysql://localhost:3306/trening?characterEncoding=utf8 na:
            jdbc:mysql://localhost:3306/trening?useUnicode&characterEncoding=utf8 – powinno pomóc

          • Paweł Kalbarczyk

            Niesetety – bez zmian.

        • Co do auto_increment – tak, powinien być aktywny.

          W przypadku funkcjonalności usuwania czy modyfikacji dowolnego rekordu, powinieneś przekazywać jako parametr id z bazy danych, a nie numer kolejny wiersza w tabeli :) To powinno pomóc rozwiązać ten problem

          • Paweł Kalbarczyk

            Mam teraz coś takiego:

            @RequestMapping(„/usunTrening-{id}”)
            public String usunTrening(@PathVariable(„id”) Long id) {
            dao.removeById(id);
            return „index”;
            }

            Jak moge to zrobić w taki sposób, żeby pobierało to jako parametr dany id z bazy danych?

  • Paweł

    Używając CRUD- a wystąpił problem z walidacją za pomocą KotDTO.
    Za pomocą JPA entityManagera wszystko walidacja uruchamiała się.
    Po uzupełnieniu wszystkich pól formularza, kot zostaje prawidłowo zapisany do bazy, po czym wyświetlam listę z kotami.

    Problem pojawia się, gdy próbuję przesłać pusty formularz, dostaję :

    ——————————————————————————————
    HTTP Status 400 –
    type Status report
    message
    description The request sent by the client was syntactically incorrect.
    ——————————————————————————————

    Repozytorium:

    @Repository

    public interface KotRepository extends CrudRepository{

    public Kot findById(Long id);
    }

    ———————————————————————————————
    Fragment kontrolera odpowiadający za wyświetlanie i zapisywanie kota:

    @RequestMapping(value = „/dodaj”, method = RequestMethod.GET)

    public String dodaj() {

    return „dodaj”;

    }

    @ModelAttribute(„kotDto”)

    public KotDTO getKotDTO() {

    return new KotDTO();

    }

    @RequestMapping(value = „/dodaj”, method = RequestMethod.POST)

    public String obslugaFormDodajKota(@ModelAttribute(„kotDto”) @Valid KotDTO kotDto) {

    createAndSetKota(kotDto);

    return „redirect:/lista”;

    }

    private void createAndSetKota(KotDTO kotDto) {

    Kot kot = new Kot();

    kot.setDataUrodzenia(kotDto.getDataUrodzenia());

    kot.setImie(kotDto.getImie());

    kot.setImieOpiekuna(kotDto.getImieOpiekuna());

    kot.setWaga(kotDto.getWaga());

    kotRepository.save(kot);

    }

    Z góry dzięki za odpowiedź

    • Paweł

      Dodam jeszcze, że gdy pojawi się ten komunikat:

      HTTP Status 400 –
      type Status report
      message
      description The request sent by the client was syntactically incorrect.

      po przeładowaniu strony ponownie wyświetla się formularz do dodawania kota…

      • Paweł

        Rozwiązane: zjadłem „BindingResult”

        • :) Super, że sobie poradziłeś sam z rozwiązaniem! Powodzenia w dalszej nauce!

  • Patryk Kotlarz

    Czy aby na pewno możliwe jest stworzenie interfejsu z adnotacją @Repository (bez extends CrudRepository,,,), aby działał on tak, jak jest tu opisane (operacje na bazie za pomocą nazw metod)? Za żadne skarby Spring nie chce wstrzyknąć takiego beana bez dołączania przeze mnie interfejsu CrudRepository (albo jego bazowego Repository).

  • Monika Senderecka

    U mnie wszystko ok, dziękuje za lekcje. Jest to jedna z najbardziej zaskakujących mnie technologi :)