PRA08.rst

Pracownia Programowania

Spring Boot

Uwaga! Kod do tych zajęć znajduje się na gałęzi SpringBootStart w repozytorium https://github.com/WitMar/PRA2018-2019 . Kod końcowy w gałęzi SpringBootEnd.

Jeżeli nie widzisz odpowiednich gałęzi na GitHubie wykonaj Ctr+T, a jak to nie pomoże to wybierz z menu VCS->Git->Fetch.

Spring Boot

Spring Boot to lekki framework pozwalający na proste tworzenie aplikacji w oparciu o framework Spring. Spring Framework jest to platforma, której głównym celem jest uproszczenie procesu tworzenia oprogramowania w technologii Java/J2EE. Rdzeniem Springa jest kontener wstrzykiwania zależności, który zarządza komponentami i ich zależnościami. Obiekty zarządzane przez Springa nazywane są beanami.

O samym springu możesz poczytać tutaj:

Startery są grupami zależności, które pozwalają na uruchamianie projektów Springowych przy minimalnym wysiłku ich konfiguracji.

W naszym przypadku głównym starterem będzie:

spring-boot-starter-web służy do budowania aplikacji opartych na REST API z wykorzystaniem Spring MVC oraz Tomcata.

Podany wyżej starter sprawia, że projekt jest konfigurowany do tego by korzystać z :

Wbudowanego Serwera Tomcat
Hibernate for Object-Relational Mapping (ORM)
Apache Jackson for JSON binding
Spring MVC for the REST framework

Pełną listę starterów można znaleźć tutaj:

Aplikacje przy użyciu Springa, buduje się modułowo. Idealnie wpasowuje się to w model MVC i pozwala na iteracyjny rozrost naszej aplikacji o kolejne moduły.

Jeżeli pozwolisz (przy użyciu adnotacji @EnableAutoConfiguration) Spring Boot dokona w jak najszerszym sensie automatycznej konfiguracji projektu - wyszuka odpowiednie adnotacje i powoła obiekty do życia.

Do generowania szkieletu aplikacji springowej możemy także posłużyć się stroną:

Który wyprodukuje szkielet aplikacji mavenowej wraz z zależnościami.

Konfiguracja

Konfiguracja znajduje się w pliku application.properties

Wykorzystanie bazy in-memory HSQL

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.jpa.show-sql=true

spring.database.driverClassName=org.hsqldb.jdbcDriver
spring.datasource.url=jdbc:hsqldb:mem:spring
spring.datasource.username=sa
spring.datasource.password=

## Hibernate Properties

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.HSQLDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = create

Wykorzystanie bazy PostgeSQL

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.jpa.database=POSTGRESQL
spring.datasource.platform=postgres
spring.jpa.show-sql=true

spring.database.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres

## Hibernate Properties

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQL94Dialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = create

Ustawienia dotyczą głównie dostępu do bazy danych i są takie same jak dla Hibernate.

Uwaga! dla spring.jpa.hibernate.ddl-auto znów najlepszą opcją byłoby update. Jeżeli jednak mamy problemy z uruchomieniem warto korzystać z create i validate.

Ważne jest także to, że wiele elementów ustawień jest zależnych od silnika bazy danych, z którego korzystamy.

W pliku POM definiujemy:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.8.RELEASE</version>
</parent>

Jest to definicja wersji Spring Boota z jakiej korzystamy (1.5.8) ale także tzw. parent POM, dzięki czemu w naszym pliku POM możemy dziedziczyć wersje bibliotek z rodzica POM, tak by była między nimi pełna kompatybilność. Możemy więc w pliku POM zdefiniować:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Bez określania artifactId oraz wersji i oba te parametry zostaną wydziedziczone z parent POM (o ile są tam zdefiniowane).

Uwaga! Jeżeli chcemy korzystać z innej bazy danych niż PostgreSql to musimy w pliku POM zdefiniować zależnośc względem sterownika tejże bazy.

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

Do definicji sposobu budowania aplikacji służy sekcja plugin w POM.xml.

Do poprawnego budowania wystarczy teoretycznie wpis:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>1.5.8.RELEASE</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Problem spotkamy, gdy będziemy chcieli korzystać z JDK9. Wtedy proponujemy rozwiązanie z repozytorium, czyli skorzystanie z pluginów do budowania JAR (a następnie z niego) pliku WAR. Wersja ta powinna być kompatybilna z wcześniejszymi wersjami Javy.

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>2.4.1</version>
            <dependencies>
                <dependency>
                    <groupId>org.codehaus.plexus</groupId>
                    <artifactId>plexus-archiver</artifactId>
                    <version>2.4.4</version>
                </dependency>
            </dependencies>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.0.0</version>
            <dependencies>
                <dependency>
                    <groupId>org.codehaus.plexus</groupId>
                    <artifactId>plexus-archiver</artifactId>
                    <version>2.4.4</version>
                </dependency>
            </dependencies>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>3.2.0</version>
            <configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-archiver</artifactId>
                        <version>2.4.4</version>
                    </dependency>
                </dependencies>
            </configuration>
        </plugin>
    </plugins>
</build>

Test

W projekcie zdefiniowany jest pusty test z adnotacjami.

@RunWith(SpringRunner.class)
@SpringBootTest

Powoduje on uruchomienie środowiska springowego (bez połączenia z bazą danych). Dzięki czemu możemy łatwo sprawdzić w momencie budowania, czy nie występują błedy w definicji ustawień i środowiska Springowego.

Uruchomienie

Główną klasą projektu jest klasa SpringBootWebApplication. Znajduje się w niej funkcja main, inaczej mówiąc będzie to klasa, którą chcemy uruchamiać.

Aby uruchomić aplikację Springową potrzebujemy klasę wejściową opatrzoną adnotacją @SpringBootApplication, oraz wywołanie metody statycznej run() na klasie SpringApplication. Drugą adnotację z naszej klasy, związaną z bazą danych omówimy poniżej.

Przed klasą znajdują się adnotacje

@SpringBootApplication
@EnableJpaRepositories("com.pracownia.spring.repositories")

Pierwsza jest tak naprawdę jest zbiorem kilku innych adnotacji (@Configuration (informacja, że obsługujemy żądania HTTP), @EnableAutoConfiguration (dzięki niej, aplikacja dokona samokonfiguracji według domyślnych wartości, załaduje potrzebne moduły itp.) oraz @ComponentScan (informacja, że ma przeskanować projekt w poszukiwaniu adnotacji odnośnie entity, repository, service i controller i je załadować)), informujemy tym samym, że dana klasa może być klasą rozruchową dla Springa, i zawiera w sobie podstawową konfigurację.

Uwaga! Najlepiej stosować rozkład klas i pakietów taki jak w przykładowej aplikacji, w przypadku umieszczania klas w różnych pakietach może istnieć konieczność definicji dodatkowego parametru w ComponentScan - wskazania dokładnie pakietów do przeskanowania.

Dziedziczenie z klasy SpringBootServletInitializer oraz metoda

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    return application.sources(SpringBootWebApplication.class);
}

służą do tego, żeby nasza aplikacja mogła z sukcesem być zapisana i uruchamiana jako plik WAR.

Aby uruchomić serwis wystarczy kliknąć prawym klawiszem na klasie SpringBootWebApplication.java i wybrać Run. Spring Boot uruchamia wbudowanego Tomcata za nas i wgrywa tam aplikacje abyśmy sami mogli się skupić na tworzeniu kodu zamiast przejmowania się szczegółami technicznymi.

Tomcata uruchamia się domyślnie na porcie 8080.

Entity

Model danych jest klasą zawierającą klasy i pola takie jak w przypadku projektu Hibernatowego i oznaczane są adnotacją @Entity przed nazwą klasy.

Możemy w nich korzystać z klasycznych adnotacji Hibernatowych (jak i np Jacksonowych).

Przypomnienie: Hibernate potrzebuje żeby w klasie był bezargumentowy konstruktor!

Repository

Do zarządzania danymi (tabelami) w projekcie springowym służą repozytoria. Repozytoria będą definiować jakie operacje możemy wykonać na naszych danych. Podstawowe operacje CRUD (Create, Read, Update, Delete) dostajemy od Springa, inne operacje jak wyszukiwanie po polach musimy dodać sami.

Żeby używać repozytoriów musimy do klasy Main musimy dodać adnotację @EnableJpaRepositories i określić ścieżkę, w której się znajdują.

Najprostsze repozytoria tworzone są jako interfejsy. Cały kod który jest potrzebny do wykonania akcji jest generowany przez Springa.

public interface ProductRepository extends CrudRepository<Product, Integer>

Definiuje najprostsze repozytorium, które dziedziczy najprostsze operacje CRUD ze springowego repozytorium Crud (nie musimy sami już pisać tych zapytań).

Product to klasa (obiekt) jaki chcemy przechowywać - w bazie danych będzie to tabela, u nas w projekcie jest to klasa modelu.
Integer to typ klucza dla tabeli productów.
Lista operacji z interfejsu CRUD:
Product save(Product t) – zapisz task do bazy danych
Iterable save(Iterable t) – zapisanie kolekcji obiektów
Product findOne(Integer id) – znajduje wpis z kluczem podanym jako parametr
boolean exists(Integer id) – sprawdź czy wpis z kluczem istnieje
Iterable findAll() – pobierz wszystkie wpisy z bazy danych
Iterable findAll(Iterable IDs) – znajdź wszystkie elementy z kluczami na liście IDs
long count() – policz elementy w tabeli
void delete(Integer id) – usuń element z kluczem id
void delete(Product r) – usuń obiekt z tabelki
void delete(Iterable IDs) – usuń wszystkie obiekty których klucze znajdują się na liście IDs
deleteAll() – wyczyść tabelkę

Oczywiście możemy dziedziczyć po więcej niż jednym interfejsie. Zwracamy uwagę na PagingAndSortingRepository ułatwiające stronnicowanie przy pobieraniu obiektów z bazy.

Zapytania SQL możemy w Springu tworzyć na 4 sposoby:

* Zapytania "nazwane"
* Query DSL
* Z nazw metod
* @Query Annotation

DSL to zapytania tworzone w kodzie:

queryFactory.selectFrom(person)
    .where(
        person.firstName.eq("John"),
        person.lastName.eq("Doe"))
    .fetch();

Poniżej przedstawimy dwa ostatnie sposoby:

Definiowanie własnych zapytań odbywa się poprzez:
Zdefiniowanie metody w interfejsie
List<Product> findByName(String name);

Spring automatycznie zmienia metodę na zapytanie SQL (za nas) jeżeli stosujemy się do odpowiedniej konwencji nazewniczej. Kluczowe frazy to find…By, read…By, and get…By możemy do tego używać nazw pól oraz logicznych spójników And i Or. Tutaj Spring domyśla się co ma zrobić po nazwie metody. Można by w podobny sposób zapisać findByName(String name). Możemy również łączyć warunki i tworzyć nazwy takie jak findByDoneAndName(Boolean done, String name). Przykład:

public interface PersonRepository extends Repository<User, Long> {

    List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

    // Enables the distinct flag for the query
    List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
    List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

    // Enabling ignoring case for an individual property
    List<Person> findByLastnameIgnoreCase(String lastname);
    // Enabling ignoring case for all suitable properties
    List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

    // Enabling static ORDER BY for a query
    List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
    List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

Więcej przykładowych słów kluczowych w użyciu:

Drugi sposób to zdefiniowanie zapytania samemu korzystając z adnotacji @Query przed nazwą metody w interfejsie.

@Query("select u from User u where u.age = ?1")
List<User> findUsersByAge(int age);

@Query("select u from User u where u.firstname = :#{#customer.firstname}")
List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);

Parametry zapytań są definiowane poprzez adnotacje @Param lub poprzez numer parametru w definicji ?1.

Service

Jak zauważyliśmy powyżej repository definiuje bezpośredni dostęp do danych poprzez zapytania bazy danych. Z punktu widzenia inżynieryjnego repository nie zawiera więc żadnej logiki tylko proste zapytania bazodanowe. Jeżeli potrzebujemy opakować naszą interakcję z bazą danych przez logikę biznesową zastosujemy tzw. service - czyli warstwę pośrednią między danymi a widokiem.

W celu zdefiniowania serwisu przed nazwą klasy serwisu umieszczamy adnotację @Service.

Spring wykorzystuje tzw. mechanizm wstrzykiwania zależności. To znaczy, że silnik springa jest odpowiedzialny za zarządzanie komponentami i przekazywanie ich do innych komponentów tak by mogły być wykorzystywane razem. Kluczowa jest tu adnotacja

@Autowired

Mówi ona springowi, że chcemy w tym miejscy pobrać obiekt komponentu zdefiniowany w systemie.

Np. w przypadku serwisu będzie nam potrzebny komponent Repository, na którym chcemy wykonywać zapytania.

@Autowired
private ProductRepository productRepository;

Adnotacja taka spowoduje, że w obiekcie serwisu Spring zainicjalizuje zmienną productRepository niejako "za nas" a my będziemy mogli z niej dalej korzystać.

Dokładniej to co definiujemy to interfejs, którym możemy posługiwać się w naszej klasie a Spring wstrzykuje do niego implementację tego interfejsu. Stąd, aby utworzyć serwis definiujemy interfejs:

public interface ProductService

z nagłówkami metod. Jeżeli będziemy chcieli skorzystać w innej klasie z tego interfejsu zdefiniujemy go jako

@Autowired
private ProductService productService;

Natomiast implementację metod zawrzemy w klasie ProductServiceImpl.

@Service
public class ProductServiceImpl implements ProductService

Controller

Kontroler to klasa odpowiedzialna za komunikację ze światem zewnętrznym (powiązania danych z widokiem). W tym miejscu definiować będziemy REST API, oraz sposób reagowania na komunikację z zewnętrznymi podmiotami.

Przed nazwą klasy kontrolera umieszczamy adnotację @RestController.

Dodatkowo możemy zdefiniować na klasie adnotację

@RequestMapping("/api")

Która spowoduje, że do wszystkich ścieżek zdefiniowanych w klasie dodany będzie przedrostek "/api".

Konkretne metody API definiujemy poprzez adnotacje:

@RequestMapping(value = "/products", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)

oznacza ona ustawione mapowanie względem URI naszej apliakcji. Tzn. jeśli adres root naszej witryny to http://localhost:8080/myApp, to aby pobrać nasze dane będziemy musieli podać taki adres: http://localhost:8080/myApp/api/products. Pusty @RequestMapping oznacza, że dane metoda jest domyślną metodą wykonywaną dla klasowego mappingu.

Kolejne parametry to method określający sposób wywołania zgodnie z trybem komunikacji HTTP oraz produces która definiuje format komunikacji danych (w tym wypadku dane będą automatycznie serializowane do JSON).

Spring używa Jacksona do serializacji i deserializacji danych.

Parametrami metody są parametry z nagłówka i ciało zapytania HTTP jak i ścieżka zapytania. Jeżeli chcemy odczytać dane ze ścieżki skorzystamy w definicji parametru z adnotacji @PathVariable i nazwy którą kojarzymy z nazwą z parametry value umieszczoną w klamrach.

@RequestMapping(value = "/product/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Product getByPublicId(@PathVariable("id") Integer publicId)

W przypadku, gdy przekazujemy dane poprzez parametr zapytania HTTP stosujemy adnotację @RequestParam i nazwę paramteru taką jaka będzie w komunikacie HTTP.

@RequestMapping(value = "/product", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Product getByParamPublicId(@RequestParam("id") Integer publicId) {

W przypadku, gdy chcemy odczytać ciało zapytania HTTP stosujemy adnotację @RequestBody. Dodatkowo możemy określać takie elementy jak @Valid i @NotNull, które sprawdzają zawartość ciała metody (gdyż może tak się pojawić dowolna treść).

@RequestMapping(value = "/product", method = RequestMethod.POST)
public ResponseEntity<Void> create(@RequestBody @Valid @NotNull Product product)

Dla parametrów wyjściowych Iterable<NaszModel> jest uniwersalnym interfejsem reprezentującym kolekcje danych, które możemy zwrócić. Możemy także zwracać obiekty klas i przez parametr produces określać sposób ich deserializacji lub dodać adnotację @ResponseBody (mówi, że zwracany obiekt to JSON i zapisuje go do body komunikatu HTTP). Możemy także definiować standardowe komunikaty HTTP używając klasy ResponseEntity<>(HttpStatus.XXX); lub też w odpowiedzi przekierować do innego endpointa poprzez zastosowanie new RedirectView("/api/products", true);. Bądź ostrożny, ponieważ RedirectView nie zmienia metody HTTP, tzn. przekierowanie z metody DELETE wysyła żądanie DELETE do przekierowanego adresu URL.

Jeżeli chcemy mieć dostęp do bazy danych musimy wstrzyknąć do naszej klasy obiekt odpowiedzialny za komunikację z bazą, czyli serwis.

@Autowired
private ProductService productService;

Zadania

Uwaga jeżeli uruchamiamy springa poprzez główną klasę z Intellij nasz serwis będzie dostępny pod adresem http://localhost:8080/ bezpośrednio, gdyż w ramach wbudowanego tomacata jest tylko jedna aplikacja więc nie musimy podawać nazwy projektu. Stąd jeżeli chciałbyś użyć postmana do komunikacji z serwisem korzystaj tylko z tego adresu + ścieżki w ramach projektu.

Zadanie

Swagger 2 is an open-source project used to describe and document RESTful APIs. Swagger udostępnia bardzo proste i klarowny opis naszego API dla innych developerów, dodatkowo generuje się automatycznie z naszego kodu oraz umożliwia testowanie naszych endpointów. Dodaj do projektu Swaggera, aby tego dokonać dodaj do POM :

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.6.1</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.6.1</version>
    <scope>compile</scope>
</dependency>

Oraz do klasy głównej

@Bean
public Docket productApi() {
    return new Docket(DocumentationType.SWAGGER_2)
            .select().apis(RequestHandlerSelectors.basePackage("com.pracownia.spring.controllers"))
            .build();
}

oraz adnotację @EnableSwagger2 przed nazwą klasy.

Uruchom klasę SpringBootWebApplication sprawdź w przglądarce na http://localhost:8080/swagger-ui.html#/ czy swagger działa.

Zadanie

Jak zauważyłeś kontroller index "reaguje" na każdy typ zapytania HTTP, co powoduje, że w swaggerze pojawia się wielokrotnie. Zmień wpis na

@RequestMapping(value = "", method = RequestMethod.GET)
String index()
{
    return "index";
}

by to zmienić.

Zadanie

Dobrą praktyką w przypadku tworzenia obiektów w REST API jest zwracać ścieżkę do obiektu, lub cały obiekt w odpowiedzi (jako potwierdzenie / podanie użytkownikowi id stworzonego obiektu). Zamień sposób odpowiedzi na zapytanie POST na poniższą linijkę:

ResponseEntity.ok().body(product);

Zobacz różnice w odpowiedzi pomiędzy POST a PUT.

Zadanie

Dodaj do produktu datę ważności

@Column
private ZonedDateTime bestBeforeDate;

Zmień konstruktor produktu i dodaj gettery i settery dla nowego pola.

Zmień definicję generowania modelu na :

public String generateModel() {

    LocalDateTime localtDateAndTime = LocalDateTime.now();
    ZoneId zoneId = ZoneId.systemDefault();
    ZonedDateTime dateAndTime  = ZonedDateTime.of(localtDateAndTime, zoneId);

    Product p1 = new Product(UUID.randomUUID().toString(),"Jajko", new BigDecimal(2.50), dateAndTime.plusDays(7));
    Product p2 = new Product(UUID.randomUUID().toString(),"Masło", new BigDecimal(3.50), dateAndTime.plusDays(7));
    Product p3 = new Product(UUID.randomUUID().toString(),"Mąka", new BigDecimal(1.50), dateAndTime.plusDays(7));

    productService.saveProduct(p1);
    productService.saveProduct(p2);
    productService.saveProduct(p3);

    return "Model Generated";
}

zobacz jak serializuje się ta data (wywołaj GET wszystkich obiektów).

Dodaj własną definicję Jacksona do projektu.

Dodaj do POM:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Oraz do głównej klasy SpringBootWebApplication:

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    jsonConverter.setObjectMapper(objectMapper);
    return jsonConverter;
}

zobacz jak serializuje się teraz data (przaładuj aplikację i wywołaj GET wszystkich obiektów).

Zadanie

Dodaj nowy kontroller SellerController do wczytywania, dodawania i usuwania Sprzedawców.

W tym celu zdefiniuj w Repository nowy interfejs SellerRepository dziedziczące z CRUD repository.

Dodaj service do obsługi Sellers.

public interface SellerService {

    Iterable<Seller> listAllSellers();

    Seller getSellerById(Integer id);

    Seller saveSeller(Seller seller);

    void deleteSeller(Integer id);

}

Oraz implementację interfejsu serwisu SellerServiceImpl - analogicznie do produktu.

Teraz możesz wrócić do kontrollera. Nie zapomnij o adnotacjach:

@RestController
@RequestMapping("/api")
public class SellerController {
...
}

Dodaj do generowania modelu dodanie sprzedawcy:

Seller seller = new Seller("Biedra", "Poznan", Arrays.asList(p1, p2, p3));
Seller seller2 = new Seller("Lidl", "Krosno", Arrays.asList(p1, p2));

sellerService.saveSeller(seller);
sellerService.saveSeller(seller2);

Jak widzisz obiekt Sprzedawcy "ciągnie" za sobą wszystkie informacje o produktach. Przez to obiekt taki szybko może stać się bardzo duży, czego byśmy nie chcieli. .

Gdybyśmy dodali do kodu

p1.getSellers().add(seller);
p2.getSellers().add(seller);
p3.getSellers().add(seller);
p1.getSellers().add(seller2);
p2.getSellers().add(seller2);

productService.saveProduct(p1);
productService.saveProduct(p2);
productService.saveProduct(p3);

To po dodaniu do Sellera metody Get i Set na productsOb zobaczylibyśmy rekurencyjne wywołanie i błąd przy próbie odczytu obiektów (sprawdź!).

By temu zapobiec chcemy zapisywać u sprzedawcy tylko informacje na temat public_id produktu. W takim wypadku Seller powinien mieć pole będące listą Stringów. Do tworzenia list typów prostych w Hibernate służy adnotacja @ElementCollection.

Dodaj w modelu Sprzedawcy tablice produktów na wpis:

@ElementCollection
@CollectionTable(name = "products")
@Column(name = "product_id")
private List<String> products = new ArrayList<>();

i dokonaj innych niezbędnych poprawek.

Zadanie

> Dodaj do repository Seller zapytanie wyszukujące sprzedawcę po nazwie.

> Dodaj do repository Seller zapytanie wyszukujące sprzedawcę, który sprzedaje najwięcej produktów.

@Query("select count(*) from Seller s join s.products p where s.id = ?1")
Integer countProductsById(Integer id);

> Dodaj do repository Seller zapytanie wyszukujące sprzedawcę, który sprzedaje produkty o największym koszcie.

@Query("select p from Seller s join s.productsOb p where s.id = ?1")
List<Product> getProductsById(Integer id);

Dodaj w service:

@Override
public Seller getBestSeller() {
    double max = 0;
    int maxId = 0;
    Iterable<Seller> sellers = sellerRepository.findAll();
    for(Seller s : sellers) {
        double sum = 0.0;
        List<Product> products = sellerRepository.getProductsById(s.getId());
        for(Product pid : products) {
            sum += pid.getPrice().doubleValue();
        }
        if (sum > max) {
            max = sum;
            maxId = s.getId();
        }
    }
    return sellerRepository.findOne(maxId);
}

Uzupełnij brakujące metody.

Zadanie

Dodaj do Product repository : PagingAndSortingRepository.

public interface ProductRepository extends CrudRepository<Product, Integer>, PagingAndSortingRepository<Product, Integer> {

Dodaj do serwisu zapytanie stronnicowane o produkty (zwróć uwagę, że teraz findAll może przyjmować parametr PageRequest - nr strony, wielkość strony). Uwaga strony są numerowane od zera!

@Override
public Iterable<Product> listAllProductsPaging(Integer pageNr, Integer howManyOnPage) {
    return productRepository.findAll(new PageRequest(pageNr,howManyOnPage));
}

Dodaj kontroler:

@RequestMapping(value = "/products/{page}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Iterable<Product> list(@PathVariable("page") Integer pageNr,@RequestParam("size") Optional<Integer> howManyOnPage) {
    return productService.listAllProductsPaging(pageNr, howManyOnPage.orElse(2));
}

Zwróć uwagę na parametr size, który jest opcjonalny (i przy nie podaniu żadnej wartości ustawiamy go jako 2).

Zadanie

Zmień zapytanie seller/{id} tak by zwracało XML-a, a nie JSON-a. By to zrobić ustaw Media Type:

@RequestMapping(value = "/seller/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_ATOM_XML_VALUE)
@ResponseBody
public Seller getByPublicId(@PathVariable("id") Integer publicId) {
    return sellerService.getSellerById(publicId);
}

oraz dodaj zależność do POM-a

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
*

Wykorzystano materiały z:

http://silversableprog.blogspot.com/2015/11/javaspring-boot-jak-rozpoczac-pisanie-w.html

http://blog.mloza.pl/spring-boot-szybkie-tworzenie-aplikacji-web-w-javie/

http://blog.mloza.pl/spring-boot-interakcja-z-baza-danych-czyli-spring-data-jpa/#more-70

https://www.callicoder.com/spring-boot-rest-api-tutorial-with-mysql-jpa-hibernate/

https://spring.io/guides/tutorials/bookmarks/

https://fastfoodcoding.com/tutorials/1505105199524/crud-operations-using-spring-boot-jpa-hibernate-postgresql

https://shakeelosmani.wordpress.com/2017/02/13/step-by-step-spring-boot-hibernate-crud-web-application-tutorial/

https://github.com/Zianwar/springboot-crud-demo

https://kobietydokodu.pl/09-spring-mvc/

https://www.ibm.com/developerworks/library/j-spring-boot-basics-perry/index.html