JavaRush /Blog Java /Random-PL /Dodanie bazy danych PostgreSQL do usługi RESTful w Spring...
Artur
Poziom 40
Tallinn

Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot. Część 2

Opublikowano w grupie Random-PL
Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot. Część 1 Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot.  Część 2 - 1 Zatem w ostatniej części dowiedzieliśmy się jak zainstalować bazę danych PostgresSQL na komputerze, stworzyć bazę danych w pgAdmin, a także ręcznie i programowo tworzyć i usuwać w niej tabele. W tej części przepiszemy nasz program tak, aby nauczył się pracować z tą bazą danych i tabelami. Dlaczego my? Ponieważ sam uczę się z Tobą z tego materiału. A wtedy nie tylko rozwiążemy stojące przed nami zadanie, ale także poprawimy błędy, które pojawiają się w trakcie, korzystając z porad bardziej doświadczonych programistów. Że tak powiem, nauczymy się pracować w zespole ;) Najpierw utwórzmy com.javarush.lectures.rest_examplew folderze nową paczkę i nazwijmy ją repository. W tym pakiecie utworzymy nowy interfejs ClientRepository:
package com.javarush.lectures.rest_example.repository;

import com.javarush.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ClientRepository extends JpaRepository<Client, Integer>  {
}
Interfejs ten będzie w „magiczny sposób” współdziałał z naszymi bazami danych i tabelami. Dlaczego magicznie? Ponieważ nie będziemy musieli pisać jego implementacji, a framework Spring nam to zapewni. Wystarczy stworzyć taki interfejs i już można korzystać z tej „magii”. Następnym krokiem jest edycja klasy Clientw następujący sposób:
package com.javarush.lectures.rest_example.model;

import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    @Column(name = "phone")
    private String phone;

    //… getters and setters
}
Jedyne, co zrobiliśmy na tych zajęciach, to po prostu dodaliśmy kilka adnotacji. Przejrzyjmy je:
  • @Entity — wskazuje, że ten komponent bean (klasa) jest jednostką.
  • @Table - wskazuje nazwę tabeli, która będzie wyświetlana w tej encji.
  • @Id - identyfikator kolumny (klucz podstawowy - wartość, która będzie używana w celu zapewnienia unikalności danych w bieżącej tabeli. Uwaga: Andrei )
  • @Kolumna — wskazuje nazwę kolumny, która jest mapowana na właściwość encji.
  • @GeneratedValue — wskazuje, że ta właściwość zostanie wygenerowana zgodnie z określoną strategią.
Nazwy pól tabeli nie muszą odpowiadać nazwom zmiennych w klasie. Na przykład, jeśli mamy zmienną firstName, to nazwiemy pole w tabeli first_name. Adnotacje te można ustawić zarówno bezpośrednio na polach, jak i na ich modułach pobierających. Ale jeśli wybierzesz jedną z tych metod, spróbuj utrzymać ten styl w całym programie. Pierwszą metodę zastosowałem tylko po to, aby skrócić listę. Pełniejszą listę adnotacji dotyczących pracy z bazami danych można znaleźć tutaj . Przejdźmy teraz do klasy ClientServiceImpli przepiszmy ją w następujący sposób:
package com.javarush.lectures.rest_example.service;

import com.javarush.lectures.rest_example.model.Client;
import com.javarush.lectures.rest_example.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ClientServiceImpl implements ClientService {

    @Autowired
    private ClientRepository clientRepository;

    @Override
    public void create(Client client) {
        clientRepository.save(client);
    }

    @Override
    public List<Client>  readAll() {
        return clientRepository.findAll();
    }

    @Override
    public Client read(int id) {
        return clientRepository.getOne(id);
    }

    @Override
    public boolean update(Client client, int id) {
        if (clientRepository.existsById(id)) {
            client.setId(id);
            clientRepository.save(client);
            return true;
        }

        return false;
    }

    @Override
    public boolean delete(int id) {
        if (clientRepository.existsById(id)) {
            clientRepository.deleteById(id);
            return true;
        }
        return false;
    }
}
Jak widać z listy, jedyne, co zrobiliśmy, to usunęliśmy niepotrzebne linie:
// ХранLubще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Zamiast tego zadeklarowaliśmy nasz interfejs ClientRepositoryi umieściliśmy nad nim adnotację @Autowired , aby Spring automatycznie dodał tę zależność do naszej klasy. Oddelegowaliśmy także całą pracę do tego interfejsu, a raczej jego implementacji, którą doda Spring. Przejdźmy do ostatniego i najciekawszego etapu – testowania naszej aplikacji. Otwórzmy program Postman (zobacz jak z niego korzystać tutaj ) i wyślij żądanie GET na ten adres: http://localhost:8080/clients. Otrzymujemy taką odpowiedź:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    }
]
Wysyłamy żądanie POST:
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
I... łapiemy nasz pierwszy błąd w programie:
{
    "timestamp": "2020-03-06T13:21:12.180+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
    "path": "/clients"
}
Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot.  Część 2 - 2 Patrzymy na logi i znajdujemy następujący błąd:

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся oznaczający ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
Wysyłamy ponownie to samo żądanie POST, wynik jest taki sam, ale z tą różnicą: Ключ "(id)=(2)" уже существует. wysyłamy to samo żądanie po raz trzeci i otrzymujemy Status: 201 Utworzono. Wysyłamy ponownie żądanie GET i otrzymujemy odpowiedź:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    },
    {
        "id": 3,
        "name": "Amigo",
        "email": "amigo@jr.com",
        "phone": "+7 (191) 746-43-23"
    }
]
Sugeruje to, że nasz program ignoruje fakt, że tabela została już wstępnie wypełniona i ponownie przypisuje identyfikator, zaczynając od jednego. Cóż, błąd to moment roboczy, nie rozpaczaj, zdarza się to często. Dlatego zwrócę się o pomoc do bardziej doświadczonych kolegów: „Drodzy koledzy, proszę o poradę w komentarzach, jak to naprawić, aby program działał normalnie”. Pomoc nie trwała długo, a Staś Pasinkow w komentarzach powiedział mi, w którym kierunku mam szukać. Specjalne podziękowania dla niego za to! Rzecz jednak w tym, że na zajęciach Clientbłędnie określiłem strategię adnotacji @GeneratedValue(strategy = GenerationType.IDENTITY)dla pola id. Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot.  Część 2 - 3 Ta strategia jest odpowiednia dla MySQL. Jeśli pracujemy z Oracle lub PostrgeSQL, musimy ustalić inną strategię. Więcej o strategiach dotyczących kluczy podstawowych możesz przeczytać tutaj . Wybrałem strategię GenerationType.SEQUENCE. Aby to zaimplementować będziemy musieli nieco przepisać plik initDB.sql i oczywiście adnotacje pola id klasy Client. Przepisz plik initDB.sql:
CREATE TABLE IF NOT EXISTS clients
(
    id    INTEGER PRIMARY KEY ,
    name  VARCHAR(200) NOT NULL ,
    email VARCHAR(254) NOT NULL ,
    phone VARCHAR(50)  NOT NULL
);
CREATE SEQUENCE clients_id_seq START WITH 3 INCREMENT BY 1;
Co się zmieniło: zmienił się typ kolumny id naszej tabeli, ale o tym później. Dodaliśmy poniżej linię, w której tworzymy nową sekwencję Client_id_seq, wskazujemy, że powinna zaczynać się od trójki (ponieważ ostatni identyfikator w pliku populateDB.sql to 2) i wskazujemy, że przyrost powinien nastąpić o jeden. Wróćmy do typu kolumny id. Tutaj określiliśmy INTEGER, ponieważ jeśli opuścimy SERIAL, sekwencja zostanie utworzona automatycznie, o tej samej nazwie client_id_seq, ale zacznie się od jednego (co doprowadziło do błędu programu). Jednak teraz, jeśli chcesz usunąć tabelę, będziesz musiał dodatkowo usunąć tę sekwencję ręcznie poprzez interfejs pgAdmin lub poprzez plik .sql za pomocą następujących poleceń:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Jeśli jednak do początkowego zapełnienia tabeli nie używasz pliku takiego jak populateDB.sql, możesz użyć typu SERIAL lub BIGSERIAL dla klucza podstawowego i nie musisz tworzyć sekwencji ręcznie, a zatem nie musisz jej usuwać to osobno. Więcej o sekwencjach można przeczytać na stronie internetowej. Dokumentacja PostgreSQL'a . Przejdźmy do adnotacji pól idklas Clienti sformatujmy je w następujący sposób:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
Co zrobiliśmy: zainstalowaliśmy nową adnotację @SequenceGenerator, aby utworzyć generator sekwencji, przypisaliśmy mu nazwę clientsIdSeq, wskazali, że jest to generator sekwencji clients_id_seqi dodaliśmy atrybut. allocationSize = 1 Jest to atrybut opcjonalny, ale jeśli tego nie zrobimy, po uruchomieniu programu otrzymamy następujący błąd:

org.hibernate.MappingException: The increment size of the [clients_id_seq] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [1]
Oto, co pisze na ten temat w komentarzach użytkownik Andrei : alokacjaSize ma przede wszystkim na celu ograniczenie podróży hibernacji do bazy danych w celu uzyskania „nowego identyfikatora”. Jeśli wartość == 1, hibernacja dla każdej nowej jednostki, przed zapisaniem jej w bazie danych, „uruchamia się” do bazy danych dla identyfikatora. Jeśli wartość wynosi > 1 (na przykład 5), hibernacja będzie rzadziej kontaktować się z bazą danych w celu uzyskania „nowego” identyfikatora (na przykład 5 razy), a podczas kontaktu hibernacja poprosi bazę danych o zarezerwowanie tego numeru (w naszym przypadku przypadek, 5) wartości. Opisany przez Ciebie błąd sugeruje, że hibernacja chciałaby otrzymać 50 domyślnych identyfikatorów, ale w bazie danych wskazałeś, że jesteś gotowy nadać temu podmiotowi identyfikator tylko według pierwszego . Kolejny błąd został wyłapany przez użytkownika Nikolyę Kudryashov : Jeśli uruchomisz żądanie z oryginalnego artykułu http://localhost:8080/clients/1, zostanie zwrócony błąd:
{
    "timestamp": "2020-04-02T19:20:16.073+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.javarush.lectures.rest_example.model.Client$HibernateProxy$ZA2m7agZ[\"hibernateLazyInitializer\"])",
    "path": "/clients/1"
}
Ten błąd jest związany z leniwą inicjalizacją Hibernate i aby się go pozbyć, musimy dodać dodatkową adnotację do klasy Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
W ten sposób:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Uruchommy teraz nasz program (po usunięciu tabeli klientów z bazy danych, jeśli pozostała tam od ostatniego razu) i zakomentujmy 3 linie z pliku application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
Ostatnim razem skomentowaliśmy tylko ostatnią linijkę, ale... Ponieważ już stworzyliśmy i wypełniliśmy tabelę, w tej chwili wydawało mi się to bardziej logiczne. Przejdźmy do testowania, wykonaj żądania GET, POST, PUT i DELETE za pośrednictwem Postmana, a zobaczymy, że błędy zniknęły i wszystko działa dobrze. To wszystko, zadanie wykonane. Teraz możemy krótko podsumować i rozważyć, czego się nauczyliśmy:
  • Zainstaluj PostgreSQL na swoim komputerze
  • Utwórz bazy danych w pgAdmin
  • Twórz i usuwaj tabele ręcznie i programowo
  • Wypełniaj tabele za pomocą plików .sql
  • Dowiedzieliśmy się trochę o „magicznym” interfejsie JpaRepository frameworka Spring
  • Dowiedzieliśmy się o niektórych błędach, które mogą pojawić się podczas tworzenia takiego programu
  • Zdaliśmy sobie sprawę, że nie powinniśmy się wstydzić zwracać się o radę do kolegów
  • Potwierdziliśmy, że społeczność JavaRush to siła, która zawsze przyjdzie na ratunek ;)
Na tym możemy na razie zakończyć. Dodanie bazy danych PostgreSQL do usługi RESTful w Spring Boot.  Część 2 - 4Dziękuję wszystkim, którzy poświęcili czas na przeczytanie tego materiału. Chętnie zapoznam się z Waszymi komentarzami, spostrzeżeniami, uzupełnieniami i konstruktywną krytyką. Być może zaproponujesz bardziej eleganckie rozwiązania tego problemu, co obiecuję dodać do tego artykułu za pomocą „słowa kluczowego” UPD, oczywiście z wzmianką o Tobie jako autorze. No i ogólnie napisz, czy spodobał Ci się ten artykuł i taki styl przedstawienia materiału, i w ogóle, czy mam dalej pisać artykuły o JR. Oto dodatki: UPD1: użytkownik Justinian stanowczo zalecił zmianę nazwy pakietu com.javarush.lectures.rest_examplena com.javarush.lectures.rest.example, oraz nazwy projektu, aby nie naruszać konwencji nazewnictwa w Javie. Użytkownik UPD2, Alexander Pyanov, zasugerował, że do zainicjowania pola ClientRepositoryw klasie ClientServiceImpllepiej jest użyć konstruktora niż adnotacji @Autowired. Wyjaśnia to fakt, że w rzadkich przypadkach można uzyskać NullPointerExceptioni ogólnie jest to najlepsza praktyka, z którą się zgadzam. Logicznie rzecz biorąc, jeśli do początkowej funkcjonalności obiektu wymagane jest pole, to lepiej je zainicjować w konstruktorze, ponieważ klasa bez konstruktora nie zostanie zmontowana w obiekt, dlatego to pole zostanie zainicjowane na etapie tworzenia obiektów. Dodam fragment kodu z poprawkami (co należy zastąpić czym):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
Link do pierwszej części: Dodanie bazy danych PostgreSQL do usługi RESTful na Spring Boot. Część 1 PS Jeśli ktoś z Was chce dalej rozwijać tę aplikację edukacyjną, z przyjemnością dodam link do Waszych poradników w tym artykule. Być może pewnego dnia ten program wyrośnie na coś przypominającego prawdziwą aplikację biznesową, nad którą będziesz mógł dodać prace do swojego portfolio. PPS Nawiązując do tego skromnego artykułu, postanowiłem zadedykować ten test pióra naszym kochanym dziewczynom, kobietom i paniom. Kto wie, może teraz nie byłoby Javy, JavaRusha, żadnego programowania w naturze, gdyby nie ta kobieta . Gratulujemy wakacji, nasi drodzy, mądrzy ludzie! Szczęśliwego 8 marca! Bądź szczęśliwy i piękny!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION