[Java][Spring] Baza danych w Spring

W poprzednim kursie odnoszącym się do Spring Boot, skończyliśmy na utworzeniu pierwszego projektu. Nie mamy w nim nic, oprócz domyślnych plików i ustawień. Naszym celem jest utworzenie aplikacji do wyświetlania wyników lotto. Aby nie obciążać obcego systemu z którego będziemy pobierać te wyniki http://www.mbnet.com.pl/wyniki.htm, będziemy je wczytywać do naszej bazy danych przy każdym uruchomieniu aplikacji.

W tym celu dodajemy do naszego pliku pom.xml następującą zależność:

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

Dzięki temu będziemy mogli korzystać z JPA w naszym projekcie. Czym jest JPA? w skrócie jest to uniwersalny system do korzystania z baz danych. Pod spodem kryją się jego konkretne implementacje jak chociażby Hibernate czy OpenJPA.

*Spring korzysta z Hibernate

Oto jak wygląda sekcja <dependencies> w naszym pliku pom.xml:

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

Ok, dodaliśmy nasze JPA do projektu, teraz należy utworzyć nasze pierwsze repozytorium, które posłuży nam do zarządzania danymi. W tym celu tworzymy nowy interfejs LottoResultRepository w pakiecie com.example.repository:


package com.example.repository;

import org.springframework.data.repository.Repository;

public interface LottoResultRepository extends CrudRepository<LottoResultModel, Long> {
    
}

Long, reprezentujący typ całkowitoliczbowy podwójnego rozmiaru jest naszym ID. Natomiast jako typ danym wpiszemy LottoResultModel, będzie to klasa przedstawiająca wyniki losowania.
Utwórzmy zatem klasę o nazwie LottoResultModel, w pakiecie com.example.model:



O następującej treści:

package com.example.model;

import java.util.Collection;
import java.util.Date;

import javax.persistence.CollectionTable;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class LottoResultModel {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private Date lotteryDrawingDate;
    
    @ElementCollection(fetch = FetchType.EAGER)
    private Collection<Integer> numbers;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public Collection<Integer> getIntegers() {
        return numbers;
    }

    public void setIntegers(Collection<Integer> integers) {
        this.numbers = integers;
    }

    public Date getLotteryDrawingDate() {
        return lotteryDrawingDate;
    }

    public void setLotteryDrawingDate(Date lotteryDrawingDate) {
        this.lotteryDrawingDate = lotteryDrawingDate;
    }

    @Override
    public String toString() {
        return "LottoResultModel [id=" + id + ", lotteryDrawingDate=" + lotteryDrawingDate + ", integers=" + numbers
                + "]";
    }
}

Jak widzimy, korzystamy tutaj z 4 adnotacji:

@Entity

Służy do zaznaczenia klasy jako należącej do mapowania w naszej bazie. Jedynie klasy opatrzone w tę adnotację mogą być przechowywane w bazie danych.

@Id
@GeneratedValue(strategy = GenerationType.AUTO)

Tutaj określamy która zmienna będzie ID naszego rekordu w bazie danych, i wskazujemy że jej wartość ma zostać wygenerowana automatycznie.

@ElementCollection(fetch = FetchType.EAGER)

Oznaczamy elementy występujące jako kolekcja, czyli obiekty mogące występować w liczbie mnogiej. Najprostsza analogia to tablica. Ustawienie FetchType.EAGER powoduje że pobierając z bazy danych obiekt LottoResultModel od razu pobierzemy przypisane do niego wylosowane numery. Jest to operacja kosztowne, z tego względu że liczby te przechowywane są w innej tabeli i muszą zostać połączone z aktualnym model. Zazwyczaj będziemy korzystać z LAZY zamiast EAGER, ale żeby uprościć proces tworzenia aplikacji skorzystamy z wiązania EAGER.

Oprócz tego nasz obiekt nie różni się od innego obiektu w Javie. Przechowywać będzie w nim informacje odnośnie daty i wylosowanych liczb.
Aby jednak móc działać na tych danych, musimy je skądś pobrać. Możemy to uczynić z strony http://www.mbnet.com.pl/wyniki.htm, gdzie przechowywane są dane archiwalne wszystkich losować lotto od jego początku. Cofamy się więc do początku naszej aplikacji czyli klasy MyFirstSpringBootAppApplication tam przekształcamy kod do takiej postaci:

package com.example;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class MyFirstSpringBootAppApplication{

    public static void main(String[] args) {
        SpringApplication.run(MyFirstSpringBootAppApplication.class, args);
    }
    
    @Bean
    public CommandLineRunner prepareDatabase() {
        return (args) -> {
            //nasza logika
        };
    }
}

Dzięki temu, przy każdym uruchomieniu aplikacji, będzie wywoływana metoda prepareDatabase. Warto zaznaczyć że przy wywoływaniu tej metody działają już wszystkie funkcję Springa takie jak DI czy nasze Repozytoria/Serwisy/Componenty oraz możemy w prosty sposób odczytać argumenty przekazane na starcie aplikacji. Jeśli podany zapis:

 return (args) -> {
            //nasza logika
 };

wydaje się nam dziwny, to na razie wystarczy nam wiedzieć że użyto tutaj lambdy dostępnej w Javie 8. I jest to skrócony zapis funkcji przyjmującej nasze argumenty o typie CommandLineRunner.

Logika przygotowania bazy danych:


Aby pobrać plik z sieci, wykorzystamy bibliotekę Apache Common IO, która udostępnia zestaw ciekawych operacji na plikach. Między innymi kopiowanie danych z URL do pliku.

A więc, dodajemy do naszego pom.xml następującą zależność:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>

Dodatkowo pobierzemy bibliotekę Joda-time do operacji na czasie:

        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>


Wygląd sekcji <dependencies>:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

A oto cała logika przygotowania bazy danych:

@Bean
    public CommandLineRunner prepareDatabase(LottoResultRepository lottoRepository) {
        return (args) -> {
            File urlFile = File.createTempFile("lotto", "url");
            URL url = new URL("http://www.mbnet.com.pl/dl.txt");
            FileUtils.copyURLToFile(url, urlFile);
            
            String fileText = FileUtils.readFileToString(urlFile);
            String[] linesText = fileText.split(System.lineSeparator());
            
            DateTimeFormatter formatter = DateTimeFormat.forPattern("dd.MM.yyyy");
            
            for(int x = 0; x < linesText.length; x++){
                String[] dataText = linesText[x].split(" ");
                LottoResultModel lottoResultModel = new LottoResultModel();
                //Parsowanie daty
                DateTime dt = formatter.parseDateTime(dataText[1]);
                lottoResultModel.setLotteryDrawingDate(dt.toDate());
                //Parsowanie liczb
                Set<Integer> lotteryNumbers = new LinkedHashSet();
                String[] numbers = dataText[2].split(",");
                for(int i = 0; i < numbers.length; i++){
                    lotteryNumbers.add(Integer.parseInt(numbers[i]));
                }
                lottoResultModel.setIntegers(lotteryNumbers);
                
                lottoRepository.save(lottoResultModel);
            }
            System.out.println("Ilość zapisanych pozycji: " + lottoRepository.count());
        };
    }

Pozostało jeszcze dodanie biblioteki z naszą bazą. W tym wypadku korzystać będziemy z bazy danych H2, która umożliwia tworzenie bazy bezpośrednio w pamięci operacyjnej lub na plikach w systemie gdzie została odpalona nasza aplikacja. Możemy także wykorzystać dowolną inną wspieraną przez Springa.

Dodajemy do pom.xml:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>


Nasz kod przygotowuje nam już naszą bazę danych i wypełnia ją liczbami z losowań dużego lotka od początku jego historii. W kolejnej części umożliwimy wyświetlenie tych danych za pomocą REST.