Aggiunta di un database PostgreSQL a un servizio RESTful su Spring Boot. Parte 1 Quindi, nell'ultima parte abbiamo imparato come installare un database PostgresSQL su un computer, creare un database in pgAdmin e anche creare ed eliminare le tabelle al suo interno manualmente e in modo programmatico. In questa parte riscriveremo il nostro programma in modo che impari a lavorare con questo database e queste tabelle. Perché noi? Perché io stesso sto imparando con te da questo materiale. E poi non solo risolveremo il compito da svolgere, ma correggeremo anche gli errori che si presentano in movimento, con l'aiuto dei consigli di programmatori più esperti. Per così dire, impareremo a lavorare in squadra ;) Per prima cosa creiamo
com.javarush.lectures.rest_example
un nuovo pacchetto in una cartella e chiamiamolo repository
. In questo pacchetto creeremo una nuova interfaccia 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> {
}
Questa interfaccia interagirà “magicamente” con i nostri database e tabelle. Perché magicamente? Perché non avremo bisogno di scriverne l’implementazione e ce la fornirà il framework Spring. Devi solo creare un'interfaccia del genere e puoi già utilizzare questa "magia". Il prossimo passo è modificare la classe Client
in questo modo:
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
}
Tutto ciò che abbiamo fatto in questa lezione è stato semplicemente aggiungere alcune annotazioni. Esaminiamoli:
- @Entity : indica che questo bean (classe) è un'entità.
- @Table : indica il nome della tabella che verrà visualizzata in questa entità.
- @Id - ID colonna (chiave primaria - il valore che verrà utilizzato per garantire l'unicità dei dati nella tabella corrente. Nota: Andrei )
- @Column : indica il nome della colonna mappata alla proprietà dell'entità.
- @GeneratedValue : indica che questa proprietà verrà generata in base alla strategia specificata.
firstName
, chiameremo il campo nella tabella first_name
. Queste annotazioni possono essere impostate sia direttamente sui campi che sui loro getter. Ma se scegli uno di questi metodi, prova a mantenere questo stile durante l'intero programma. Ho usato il primo metodo solo per abbreviare gli elenchi. Un elenco più completo di annotazioni per lavorare con i database può essere trovato qui . Ora andiamo alla classe ClientServiceImpl
e riscriviamola come segue:
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;
}
}
Come puoi vedere dall'elenco, tutto ciò che abbiamo fatto è stato eliminare le righe che non ci servivano più:
// Хранorще клиентов
private static final Map<Integer, Client> CLIENT_REPOSITORY_MAP = new HashMap<>();
// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Abbiamo invece dichiarato la nostra interfaccia ClientRepository
e posizionato sopra anche l' annotazione @Autowired in modo che Spring aggiungesse automaticamente questa dipendenza alla nostra classe. Abbiamo delegato tutto il lavoro anche a questa interfaccia, o meglio alla sua implementazione, che Spring aggiungerà. Passiamo alla fase finale e più interessante: testare la nostra applicazione. Apriamo il programma Postman (vedi come usarlo qui ) e inviamo una richiesta GET a questo indirizzo: http://localhost:8080/clients. Otteniamo questa risposta:
[
{
"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)"
}
]
Inviamo una richiesta POST:
{
"name" : "Amigo",
"email" : "amigo@jr.com",
"phone" : "+7 (191) 746-43-23"
}
E... troviamo il nostro primo bug nel programma:
{
"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"
}
Esaminiamo i log e troviamo il seguente errore:
org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
Detail: Ключ "(id)=(1)" уже существует.
Inviamo nuovamente la stessa richiesta POST, il risultato è lo stesso, ma con questa differenza: Ключ "(id)=(2)" уже существует.
inviamo la stessa richiesta per la terza volta e otteniamo Status: 201 Created. Inviamo nuovamente la richiesta GET e riceviamo una risposta:
[
{
"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"
}
]
Ciò suggerisce che il nostro programma sta ignorando il fatto che questa tabella è già stata precompilata e sta assegnando nuovamente un ID partendo da uno. Beh, un bug è un momento lavorativo, non disperare, succede spesso. Pertanto, mi rivolgerò a colleghi più esperti per chiedere aiuto: "Cari colleghi, vi prego di consigliarmi nei commenti come risolvere questo problema in modo che il programma funzioni normalmente." Gli aiuti non hanno tardato ad arrivare e Stas Pasinkov mi ha detto nei commenti in quale direzione dovevo guardare. Un ringraziamento speciale a lui per questo! Ma il fatto era che nella classe Client
avevo specificato erroneamente la strategia per l'annotazione @GeneratedValue(strategy = GenerationType.IDENTITY)
del campo id
. Questa strategia è adatta per MySQL. Se lavoriamo con Oracle o PostrgeSQL, dobbiamo impostare una strategia diversa. Puoi leggere ulteriori informazioni sulle strategie per le chiavi primarie qui . Ho scelto la strategia GenerationType.SEQUENCE. Per implementarlo dovremo riscrivere leggermente il file initDB.sql e, ovviamente, le annotazioni del campo id della classe Client. Riscrivi 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;
Cosa è cambiato: è cambiato il tipo della colonna id della nostra tabella, ma ne parleremo più avanti. Abbiamo aggiunto una riga di seguito in cui creiamo una nuova sequenza client_id_seq, indichiamo che dovrebbe iniziare con un tre (perché l'ultimo ID nel file populateDB.sql è 2) e indichiamo che l'incremento dovrebbe avvenire di uno. Torniamo al tipo di colonna id. Qui abbiamo specificato INTEGER, perché se lasciamo SERIAL, la sequenza verrà creata automaticamente, con lo stesso nome client_id_seq, ma inizierà da uno (che ha portato al bug del programma). Tuttavia, ora se desideri eliminare una tabella, dovrai eliminare ulteriormente questa sequenza manualmente tramite l'interfaccia pgAdmin o tramite un file .sql utilizzando i seguenti comandi:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Ma se non utilizzi un file come populateDB.sql per popolare inizialmente la tabella, puoi utilizzare i tipi SERIAL o BIGSERIAL per la chiave primaria e non devi creare manualmente la sequenza e quindi non devi eliminare separatamente. Puoi leggere di più sulle sequenze sul sito web di. Documentazione PostgreSQL . Passiamo alle annotazioni dei campi id
della classe Client
e formattiamole come segue:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
Cosa abbiamo fatto: abbiamo installato una nuova annotazione @SequenceGenerator
per creare un generatore di sequenze, le abbiamo assegnato un nome clientsIdSeq
, abbiamo indicato che si tratta di un generatore per una sequenza clients_id_seq
e abbiamo aggiunto un attributo. allocationSize = 1
Questo è un attributo opzionale, ma se non lo facciamo, otterremo il seguente errore quando eseguiamo il programma:
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]
Ecco cosa scrive l'utente Andrei a riguardo nei commenti: allocationSize ha principalmente lo scopo di ridurre il viaggio dell'ibernazione nel database per un "nuovo ID". Se il valore == 1, iberna per ogni nuova entità, prima di salvarla nel database, “corre” al database per l'id. Se il valore è > 1 (ad esempio, 5), hibernate contatterà il database per un "nuovo" ID meno spesso (ad esempio, 5 volte) e, quando lo contatti, hibernate chiederà al database di riservare questo numero (nel nostro caso, 5) valori. L'errore che hai descritto suggerisce che l'ibernazione vorrebbe ricevere 50 ID predefiniti, ma nel database hai indicato che sei pronto a emettere l'ID per questa entità solo in base al primo . Un altro bug è stato rilevato dall'utente Nikolya Kudryashov : se esegui una richiesta dall'articolo originale http://localhost:8080/clients/1, verrà restituito l'errore:
{
"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"
}
Questo errore è legato all'inizializzazione lenta di Hibernate e per eliminarlo dobbiamo aggiungere un'ulteriore annotazione alla classe Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
In questo modo:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Ora eseguiamo il nostro programma (dopo aver rimosso la tabella client dal database se è rimasta lì dall'ultima volta) e commentiamo 3 righe dal file application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
L'ultima volta abbiamo commentato solo l'ultima riga, ma... Dato che abbiamo già creato e compilato la tabella, al momento mi è sembrato più logico. Passiamo al testing, effettuiamo richieste GET, POST, PUT e DELETE tramite Postman, e vedremo che i bug sono scomparsi e tutto funziona bene. Questo è tutto, lavoro finito. Ora possiamo riassumere brevemente e considerare ciò che abbiamo imparato:
- Installa PostgreSQL sul tuo computer
- Crea database in pgAdmin
- Crea ed elimina tabelle manualmente e a livello di codice
- Popolare le tabelle tramite file .sql
- Abbiamo imparato qualcosa sulla "magica" interfaccia JpaRepository del framework Spring
- Abbiamo appreso di alcuni bug che potrebbero verificarsi durante la creazione di un programma del genere
- Ci siamo resi conto che non dovremmo vergognarci di chiedere consiglio ai colleghi
- Abbiamo confermato che la comunità JavaRush è una forza che verrà sempre in soccorso ;)
com.javarush.lectures.rest_example
in com.javarush.lectures.rest.example
e il nome del progetto, in modo da non violare le convenzioni di denominazione in Java. L'utente UPD2 Alexander Pyanov ha suggerito che per inizializzare un campo ClientRepository
in una classe ClientServiceImpl
è meglio usare un costruttore piuttosto che un'annotazione @Autowired
. Ciò è spiegato dal fatto che in rari casi è possibile ottenere NullPointerException
e, in generale, questa è la migliore pratica e sono d'accordo con essa. Logicamente, se un campo è richiesto per la funzionalità iniziale di un oggetto, allora è meglio inizializzarlo nel costruttore, perché una classe senza costruttore non verrà assemblata in un oggetto, quindi questo campo verrà inizializzato nella fase della creazione di oggetti. Aggiungerò un frammento di codice con le correzioni (cosa deve essere sostituito con cosa):
@Autowired
private ClientRepository clientRepository;
private final ClientRepository clientRepository;
public ClientServiceImpl(ClientRepository clientRepository) {
this.clientRepository = clientRepository;
}
Link alla prima parte: Aggiunta di un database PostgreSQL a un servizio RESTful su Spring Boot. Parte 1 PS Se qualcuno di voi desidera continuare a sviluppare questa applicazione educativa, sarò lieto di aggiungere un collegamento alle vostre guide in questo articolo. Forse un giorno questo programma diventerà qualcosa di simile a una vera applicazione aziendale su cui potrai aggiungere lavoro al tuo portfolio. PPS Riguardo a questo modesto articolo, ho deciso di dedicare questa prova della penna alle nostre care ragazze, donne e signore. Chissà, forse ora non ci sarebbe Java, né JavaRush, né programmazione in natura, se non fosse stato per questa donna . Congratulazioni per le vostre vacanze, nostre care persone intelligenti! Buon 8 marzo! Sii felice e bella!
GO TO FULL VERSION