Rozliczenie pojazdu REST JPA

Rozliczenie pojazdu REST, JPA, MySQL. To ostatni opisany przeze mnie sposób na komunikację z bazą danych. Jest to najczęściej wykorzystywana metodą do komunikacji z bazą danych i jednocześnie najszybsza. Wielu programistów używa jej na co dzień, ponieważ automatyzuje wiele elementów budowy projektu. Jest to metoda najbardziej wydajna, jeżeli wiesz, co robisz. Według mnie na początku nauki nie powinno się zaczynać od JPA, gdyż zbyt wiele rzeczy dzieje się tu automatycznie i dopóki działa, jest ok. Jednak gdy przestaje, brak doświadczenia utrudnia znalezienie rozwiązania. Tak jak w poprzednich dwóch projektach projekt wykorzystuje wzorzec projektowy REST API i stworzona aplikacja pozwalające na komunikację klient – server. Tak samo, jak w artykule Rozliczenie pojazdu REST MyBatisRozliczenie pojazdu REST JDBC nie będzie to w pełni działająca aplikacja. Pozwoli to porównać, jakie należy wprowadzić zmiany w zależności od wybranego sposobu komunikacji z bazą danych.

Rozliczenie pojazdu REST JPA – Tworzymy projekt

Zacznijmy od stworzenia nowego projektu. Na początku wejdę na stronę https://start.spring.io/, gdzie utworzę nasz projekt. W moim przypadku będzie on wyglądał następująco.

Rozliczenie pojazdu REST JPA – Budowa projektu

Konfiguracja projektu

W pierwszej kolejności będziemy dodawać odpowiednie biblioteki, dzięki którym w naszym projekcie będzie można korzystać z REST, JPA oraz MySQL. Przechodzimy do pliku pom.xml i dodajemy do sekcji dependences następujące elementy.

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

Pierwsze dwie sekcje dependencji informuje Spring, że ta aplikacja ma być aplikacją webową, czyli po jej uruchomieniu ma ciągle nasłuchiwać w oczekiwaniu na żądania. Trzecią to sterownik bazy danych MySQL. Czwarta dodaje bibliotekę JPA. Jak widać porównując plik pom.xml ze wszystkich trzech wersji zmieniamy tylko jeden wpis w dependecji i zmienia się użyty mechanizm komunikacji z bazą danych.

Konfiguracja w pliku property jest jednakowa dla wszystkich trzech wersji, ponieważ korzystamy z tej samej bazy danych.

spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://localhost:3306/transportMyBatis?serverTimezone=UTC
spring.datasource.username=transportMyBatis
spring.datasource.password=transportMyBatis

logging.file.name=log/transportJPA.log
server.address=localhost
server.port=8092
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl

Różnica jest tylko w dwóch ostatnich wpisach, które są specyficzne dla JPA.

Teraz zaczynamy wykorzystywać JPA.

W przypadku JPA tworzymy tylko interfejs TransportRepository i to wszystko. Nie piszmy żadnych pytań, jedynie na podstawie nazw metod i adnotacji system automatycznie tworzy zapytanie i je wykonuje. Dlatego gdy wiemy, co robimy, jest to bardzo szybka i przyjemna metoda. Jednak przy braku doświadczenia jest ciężko zrozumieć, co jest nie tak z nazwą.

package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common.Ride;

import java.util.List;

@Repository
public interface TransportRepository extends JpaRepository<Ride, Long> {
    List<Ride> findByVehicleId(long vehicleId);
    Ride findByVehicleIdAndId(long vehicleId, long rideId);
}

To wystarczy do komunikacji z bazą danych. Teraz musimy podłączyć to do naszego kontrolera. Robimy to w następujący sposób:

package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common.Ride;
import pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common.Settlement;
import pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.repository.TransportRepository;

import java.util.List;

@RestController
public class TransportController {
    private final static Logger log = LoggerFactory.getLogger(TransportController.class);
    private final TransportRepository transportRepository;

    @Autowired
    public TransportController(TransportRepository transportRepository) {
        this.transportRepository = transportRepository;
    }

    @GetMapping("/")
    public String getTest() {
        return "Działa JPA";
    }

    @GetMapping("/vehicle/{vehicleId}/rides")
    public List<Ride> getRides(@PathVariable("vehicleId") long vehicleId) {
        log.info("Wyświetlamy listę przejazdów");
        List<Ride> rides = transportRepository.findByVehicleId(vehicleId);
        rides.forEach(this::getGenerateRide);
        return rides;
    }
    @GetMapping("vehicle/{vehicleId}/ride/{rideId}")
    public Ride getRide(@PathVariable("vehicleId") long vehicleId, @PathVariable("rideId") long rideId) {
        log.info("Wyświetlamy przejazd");
        Ride ride = transportRepository.findByVehicleIdAndId(vehicleId, rideId);
        getGenerateRide(ride);
        return ride;
    }

    private void getGenerateRide(Ride ride) {
        List<Settlement> settlements = ride.getSettlements();
        double fuel = 0.0;
        double price = 0.0;
        if(!settlements.isEmpty()) {
            for (Settlement settlement : settlements) {
                fuel = fuel + settlement.getFuel();
                if(settlement.getRefuel()!=null)
                price = settlement.getRefuel().getPrice();
            }
        }
        ride.setFuel(fuel);
        ride.setPrice(price);
        ride.setCurrency(fuel * price);
    }
}

I to wystarczy do komunikacji z bazą danych, ponieważ taki krótki zapis pozwala na stworzenie naszej prostej aplikacji. Schody się zaczynają, gdy potrzebujemy je pobrać z wielu tabel i chcemy to zrobić w bardzo specyficzny sposób.

Teraz klasy dodatkowe JPA.

Pokażemy jeszcze, pozostałe pliki, pierwszy z nich to Ride

package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common;

import javax.persistence.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "Ride")
public class Ride {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(name = "idVehicle")
    private long vehicleId;
    private LocalDate rideDate;
    private int counterBefore;
    private int counterAfter;
    private int km;
    private String whence;
    private String where;
    private String rideKind;
    private double fuelCondition;
    private double fuelAdd;
    private double fuelAfter;
    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "idRide")
    private List<Settlement> settlements = new ArrayList<>();
    @Transient
    private Double fuel;
    @Transient
    private Double price;
    @Transient
    private Double currency;
    private double fuelNorm;

    public Ride() {}

    public long getId() {
        return id;
    }

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

    public LocalDate getRideDate() {
        return rideDate;
    }

    public void setRideDate(LocalDate rideDate) {
        this.rideDate = rideDate;
    }

    public int getCounterBefore() {
        return counterBefore;
    }

    public void setCounterBefore(int counterBefore) {
        this.counterBefore = counterBefore;
    }

    public int getCounterAfter() {
        return counterAfter;
    }

    public void setCounterAfter(int counterAfter) {
        this.counterAfter = counterAfter;
    }

    public int getKm() {
        return km;
    }

    public void setKm(int km) {
        this.km = km;
    }

    public String getWhence() {
        return whence;
    }

    public void setWhence(String whence) {
        this.whence = whence;
    }

    public String getWhere() {
        return where;
    }

    public void setWhere(String where) {
        this.where = where;
    }

    public String getRideKind() {
        return rideKind;
    }

    public void setRideKind(String rideKind) {
        this.rideKind = rideKind;
    }

    public double getFuelCondition() {
        return fuelCondition;
    }

    public void setFuelCondition(double fuelCondition) {
        this.fuelCondition = fuelCondition;
    }

    public double getFuelAdd() {
        return fuelAdd;
    }

    public void setFuelAdd(double fuelAdd) {
        this.fuelAdd = fuelAdd;
    }

    public double getFuelAfter() {
        return fuelAfter;
    }

    public void setFuelAfter(double fuelAfter) {
        this.fuelAfter = fuelAfter;
    }

    public Double getFuel() {
        return fuel;
    }

    public void setFuel(Double fuel) {
        this.fuel = fuel;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Double getCurrency() {
        return currency;
    }

    public void setCurrency(Double currency) {
        this.currency = currency;
    }

    public double getFuelNorm() {
        return fuelNorm;
    }

    public void setFuelNorm(double fuelNorm) {
        this.fuelNorm = fuelNorm;
    }

    public List<Settlement> getSettlements() {
        return settlements;
    }

    public void setSettlements(List<Settlement> settlements) {
        this.settlements = settlements;
    }
}

W tym przypadku jak widać w kodzie tej klasy, muszę pokazać jeszcze dwie klasy:

package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common;

import javax.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "Settlement")
public class Settlement {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(name = "idVehicle")
    private long vehicleId;
    @Column(name = "idRide")
    private long rideId;
    @Column(name = "idRefuel")
    private long refuelId;
    private LocalDate createDate;
    private double fuel;
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "idRefuel", referencedColumnName = "id", insertable = false, updatable = false)
    private Refuel refuel;

    public long getId() {
        return id;
    }

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

    public long getVehicleId() {
        return vehicleId;
    }

    public void setVehicleId(long vehicleId) {
        this.vehicleId = vehicleId;
    }

    public long getRideId() {
        return rideId;
    }

    public void setRideId(long rideId) {
        this.rideId = rideId;
    }

    public long getRefuelId() {
        return refuelId;
    }

    public void setRefuelId(long refuelId) {
        this.refuelId = refuelId;
    }

    public LocalDate getCreateDate() {
        return createDate;
    }

    public void setCreateDate(LocalDate createDate) {
        this.createDate = createDate;
    }

    public double getFuel() {
        return fuel;
    }

    public void setFuel(double fuel) {
        this.fuel = fuel;
    }

    public Refuel getRefuel() {
        return refuel;
    }

    public void setRefuel(Refuel refuel) {
        this.refuel = refuel;
    }
}
package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa.common;

import javax.persistence.*;
import java.time.LocalDate;

@Entity
@Table(name = "Refuel")
public class Refuel {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(name = "idVehicle")
    private long vehicleId;
    @Column(name = "idRide")
    private long rideId;
    private LocalDate createDate;
    private LocalDate refuelDate;
    private String name;
    private int counterBefore;
    private int counterAfter;
    private int km;
    private double refuel;
    private double price;
    private double value;
    private double fuelNorm;

    public long getId() {
        return id;
    }

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

    public long getVehicleId() {
        return vehicleId;
    }

    public void setVehicleId(long vehicleId) {
        this.vehicleId = vehicleId;
    }

    public long getRideId() {
        return rideId;
    }

    public void setRideId(long rideId) {
        this.rideId = rideId;
    }

    public LocalDate getCreateDate() {
        return createDate;
    }

    public void setCreateDate(LocalDate createDate) {
        this.createDate = createDate;
    }

    public LocalDate getRefuelDate() {
        return refuelDate;
    }

    public void setRefuelDate(LocalDate refuelDate) {
        this.refuelDate = refuelDate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getCounterBefore() {
        return counterBefore;
    }

    public void setCounterBefore(int counterBefore) {
        this.counterBefore = counterBefore;
    }

    public int getCounterAfter() {
        return counterAfter;
    }

    public void setCounterAfter(int counterAfter) {
        this.counterAfter = counterAfter;
    }

    public int getKm() {
        return km;
    }

    public void setKm(int km) {
        this.km = km;
    }

    public double getRefuel() {
        return refuel;
    }

    public void setRefuel(double refuel) {
        this.refuel = refuel;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public double getValue() {
        return value;
    }

    public void setValue(double value) {
        this.value = value;
    }

    public double getFuelNorm() {
        return fuelNorm;
    }

    public void setFuelNorm(double fuelNorm) {
        this.fuelNorm = fuelNorm;
    }
}

W porównaniu do wcześniejszych artykułów tym razem bardziej opieramy nasze klasy o adnotację, ponieważ to właśnie adnotacje informują system, w jaki sposób ma pobrać dane z tabeli i jak je połączyć.

@IDAdnotacja informująca system ze ta kolumna to klucz główny
@EntityAdnotacja informuje system że ta klasa jest powiązana z tabelą w bazie danych.
@Table(name = „Refuel”)Tu ustawiamy nazwę powiązanej tabeli.
@GeneratedValue(strategy = GenerationType.AUTO)Ustawiamy jak będzie generowany klucz główny
@Column(name = „idVehicle”)Ustawiamy nazwę kolumny w tabeli. W niektórych przypadkach nie musimy tego ustawiać jeżeli nazwy są takie same.
@OneToMany(fetch = FetchType.LAZY)Informujemy, że ta zmienna jest relacją jeden do wielu. Czyli do klasy w której znajdziemy taki zapis może być powiązane więcej niż jedna klasa lub wcale.
@OneToOne(cascade = CascadeType.ALL)Informujemy o tym że dla klasy w której jest przypisany ten wpis jest ona powiązana tylko jedna klasa lub wcale.
@JoinColumn(name = „idRide”)Informujemy o konfiguracji naszej relacji. Ustawiamy nazwę kolumny powiązanej i wiele innych rzeczy.
Tabela adnotacji

Zapraszam do zgłębienia tematu adnotacji, ponieważ ja w tym artykule opisałem je bardzo pobieżnie. Jednak to właśnie adnotacje odpowiadają za prawidłowe działanie JPA, dlatego bez tej wiedzy nie ma co rozpoczynać przygody z JPA.

Jeszcze jeden element od pokazania mianowicie główny plik projektu, który jest bardzo prosty.

package pl.lomza.programowanie.rozliczenie_pojazdu_rest_jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@SpringBootApplication
public class RozliczeniePojazduRestJpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(RozliczeniePojazduRestJpaApplication.class, args);
    }
}

Tak wygląda cała aplikacja z wykorzystaniem JPA. Pozostało nam wszystko przetestować i powinno działać. Jeżeli jednak nie działa, warto podejrzeć, jak ja to zrobiłem. Zapraszam na mojego GitHub pod adresem:

https://github.com/grzegorzkossa/rozliczenie_pojazdu_rest_jpa