JavaRush /Blog Java /Random-FR /Ajout d'une base de données PostgreSQL à un service RESTf...
Artur
Niveau 40
Tallinn

Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot. Partie 2

Publié dans le groupe Random-FR
Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot. Partie 1 Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot.  Partie 2 - 1 Ainsi, dans la dernière partie, nous avons appris comment installer une base de données PostgresSQL sur un ordinateur, créer une base de données dans pgAdmin, ainsi qu'y créer et supprimer des tables manuellement et par programme. Dans cette partie, nous allons réécrire notre programme pour qu'il apprenne à travailler avec cette base de données et ces tables. Pourquoi nous? Parce que j'apprends moi-même avec vous à partir de ce matériel. Et puis nous résoudrons non seulement la tâche à accomplir, mais corrigerons également les erreurs qui surviennent lors de nos déplacements, avec l'aide des conseils de programmeurs plus expérimentés. Pour ainsi dire, nous apprendrons à travailler en équipe ;) Tout d'abord, créons com.javarush.lectures.rest_exampleun nouveau package dans un dossier et appelons-le repository. Dans ce package nous allons créer une nouvelle interface 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>  {
}
Cette interface interagira « comme par magie » avec nos bases de données et nos tables. Pourquoi par magie ? Parce que nous n’aurons pas besoin d’écrire son implémentation, et le framework Spring nous la fournira. Il vous suffit de créer une telle interface, et vous pouvez déjà utiliser cette « magie ». L'étape suivante consiste à modifier la classe Clientcomme ceci :
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
}
Tout ce que nous avons fait dans ce cours, c'est simplement ajouter quelques annotations. Passons-les en revue :
  • @Entity - indique que ce bean (classe) est une entité.
  • @Table - indique le nom de la table qui sera affichée dans cette entité.
  • @Id - identifiant de colonne (clé primaire - la valeur qui sera utilisée pour garantir l'unicité des données dans la table actuelle. Remarque : Andrei )
  • @Column - indique le nom de la colonne mappée à la propriété d'entité.
  • @GeneratedValue - indique que cette propriété sera générée selon la stratégie spécifiée.
Les noms des champs de la table ne doivent pas nécessairement correspondre aux noms des variables de la classe. Par exemple, si nous avons une variable firstName, alors nous nommerons le champ dans le tableau first_name. Ces annotations peuvent être définies à la fois directement sur les champs et sur leurs getters. Mais si vous choisissez l’une de ces méthodes, essayez de conserver ce style tout au long de votre programme. J'ai utilisé la première méthode uniquement pour raccourcir les listes. Une liste plus complète d'annotations pour travailler avec des bases de données peut être trouvée ici . Passons maintenant à la classe ClientServiceImplet réécrivons-la comme suit :
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;
    }
}
Comme vous pouvez le voir sur la liste, nous n'avons fait que supprimer les lignes dont nous n'avions plus besoin :
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Nous avons déclaré notre interface à la place ClientRepositoryet avons également placé l' annotation @Autowired au-dessus afin que Spring ajoute automatiquement cette dépendance à notre classe. Nous avons également délégué tout le travail à cette interface, ou plutôt à son implémentation, que Spring ajoutera. Passons à l'étape finale et la plus intéressante : tester notre application. Ouvrons le programme Postman (voir comment l'utiliser ici ) et envoyons une requête GET à cette adresse : http://localhost:8080/clients. Nous obtenons cette réponse :
[
    {
        "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)"
    }
]
Nous envoyons une requête POST :
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
Et... nous attrapons notre premier bug dans le programme :
{
    "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"
}
Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot.  Partie 2 - 2 Nous examinons les journaux et trouvons l'erreur suivante :

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
On envoie à nouveau la même requête POST, le résultat est le même, mais avec cette différence : Ключ "(id)=(2)" уже существует. On envoie la même requête une troisième fois, et on obtient Status : 201 Created. Nous envoyons à nouveau la requête GET et recevons une réponse :
[
    {
        "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"
    }
]
Cela suggère que notre programme ignore le fait que cette table a déjà été pré-remplie et attribue à nouveau un identifiant à partir de un. Eh bien, un bug est un moment de travail, ne désespérez pas, cela arrive souvent. Par conséquent, je me tournerai vers des collègues plus expérimentés pour obtenir de l'aide : « Chers collègues, veuillez indiquer dans les commentaires comment résoudre ce problème afin que le programme fonctionne normalement. » L’aide n’a pas tardé à arriver et Stas Pasinkov m’a indiqué dans les commentaires dans quelle direction je devais regarder. Un merci spécial à lui pour cela ! Mais le problème était que dans la classe Client, j'avais mal spécifié la stratégie d'annotation @GeneratedValue(strategy = GenerationType.IDENTITY)du champ id. Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot.  Partie 2 - 3 Cette stratégie convient à MySQL. Si nous travaillons avec Oracle ou PostrgeSQL, nous devons alors définir une stratégie différente. Vous pouvez en savoir plus sur les stratégies pour les clés primaires ici . J'ai choisi la stratégie GenerationType.SEQUENCE. Pour l'implémenter, nous devrons réécrire légèrement le fichier initDB.sql, et, bien sûr, les annotations du champ id de la classe Client. Réécrivez 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;
Ce qui a changé : le type de la colonne id de notre table a changé, mais nous y reviendrons plus tard. Nous avons ajouté une ligne ci-dessous dans laquelle nous créons une nouvelle séquence clients_id_seq, indiquons qu'elle doit commencer par trois (car le dernier identifiant dans le fichier populateDB.sql est 2) et indiquons que l'incrément doit se produire de un. Revenons au type de colonne id. Ici, nous avons spécifié INTEGER, car si nous quittons SERIAL, la séquence sera créée automatiquement, avec le même nom clients_id_seq, mais partira de un (ce qui a conduit au bug du programme). Cependant, désormais, si vous souhaitez supprimer une table, vous devrez en plus supprimer cette séquence soit manuellement via l'interface pgAdmin, soit via un fichier .sql à l'aide des commandes suivantes :
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Mais si vous n'utilisez pas un fichier tel que populateDB.sql pour remplir initialement la table, vous pouvez alors utiliser les types SERIAL ou BIGSERIAL pour la clé primaire, et vous n'avez pas besoin de créer la séquence manuellement, et donc vous n'avez pas besoin de la supprimer. il séparément. Vous pouvez en savoir plus sur les séquences sur le site Web de. Documentation PostgreSQL . Passons aux annotations des champs idde classe Clientet formatons-les comme suit :
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
Ce que nous avons fait : nous avons installé une nouvelle annotation @SequenceGeneratorpour créer un générateur de séquence, lui avons attribué un nom clientsIdSeq, indiqué qu'il s'agit d'un générateur de séquence clients_id_seqet ajouté un attribut. allocationSize = 1 C'est un attribut facultatif, mais si nous ne le faisons pas, nous obtiendrons l'erreur suivante lorsque nous exécuterons le programme :

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]
Voici ce que l'utilisateur Andrei écrit à ce sujet dans les commentaires : allocationSize est principalement destiné à réduire le déplacement d'hibernate vers la base de données pour un "nouvel identifiant". Si la valeur == 1, mettez en veille prolongée pour chaque nouvelle entité, avant de l'enregistrer dans la base de données, « exécute » vers la base de données pour l'identifiant. Si la valeur est > 1 (par exemple, 5), hibernate contactera la base de données pour un « nouvel » identifiant moins souvent (par exemple, 5 fois), et lors du contact, hibernate demandera à la base de données de réserver ce numéro (dans notre cas, 5) valeurs. L'erreur que vous avez décrite suggère qu'Hibernate aimerait recevoir 50 identifiants par défaut, mais dans la base de données, vous avez indiqué que vous êtes prêt à émettre un identifiant pour cette entité uniquement en fonction du premier . Un autre bug a été détecté par l'utilisateur Nikolya Kudryashov : Si vous exécutez une requête à partir de l'article original http://localhost:8080/clients/1, l'erreur sera renvoyée :
{
    "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"
}
Cette erreur est liée à l'initialisation paresseuse d'Hibernate, et pour s'en débarrasser, nous devons ajouter une annotation supplémentaire à la classe Client :
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
De cette façon:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Lançons maintenant notre programme (après avoir supprimé la table clients de la base de données si elle y est restée la dernière fois) et commentons 3 lignes du fichier application.properties :
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
La dernière fois, nous n'avons commenté que la dernière ligne, mais... Puisque nous avons déjà créé et rempli le tableau, cela me paraissait plus logique pour le moment. Passons aux tests, effectuons les requêtes GET, POST, PUT et DELETE via Postman, et nous verrons que les bugs ont disparu et que tout fonctionne bien. Voilà, travail terminé. Nous pouvons maintenant résumer brièvement et considérer ce que nous avons appris :
  • Installez PostgreSQL sur votre ordinateur
  • Créer des bases de données dans pgAdmin
  • Créer et supprimer des tables manuellement et par programme
  • Remplir les tables via des fichiers .sql
  • Nous avons appris un peu sur l'interface « magique » JpaRepository du framework Spring
  • Nous avons découvert certains bugs pouvant survenir lors de la création d'un tel programme
  • Nous avons réalisé qu’il ne fallait pas être gêné de demander conseil à des collègues
  • Nous avons confirmé que la communauté JavaRush est une force qui viendra toujours à la rescousse ;)
Nous pouvons terminer ici pour l'instant. Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot.  Partie 2 - 4Merci à tous ceux qui ont pris le temps de lire ce matériel. Je serai heureux de voir vos commentaires, observations, ajouts et critiques constructives. Peut-être proposerez-vous des solutions plus élégantes à ce problème, que je promets d'ajouter à cet article via le « mot-clé » UPD, en mentionnant vous en tant qu'auteur, bien sûr. Eh bien, en général, écrivez si vous avez aimé cet article et ce style de présentation du matériel, et en général, si je dois continuer à écrire des articles sur JR. Voici les ajouts : UPD1 : l'utilisateur Justinian m'a fortement recommandé de renommer le package com.javarush.lectures.rest_exampleen com.javarush.lectures.rest.example, ainsi que le nom du projet, afin de ne pas violer les conventions de dénomination en Java. L'utilisateur UPD2 Alexander Pyanov a suggéré que pour initialiser un champ ClientRepositorydans une classe, ClientServiceImplil est préférable d'utiliser un constructeur plutôt qu'une annotation @Autowired. Cela s'explique par le fait que dans de rares cas, vous pouvez obtenir NullPointerException, et en général, c'est la meilleure pratique, et je suis d'accord avec cela. Logiquement, si un champ est requis pour la fonctionnalité initiale d'un objet, alors il vaut mieux l'initialiser dans le constructeur, car une classe sans constructeur ne sera pas assemblée en objet, donc ce champ sera initialisé au stade de création d’objets. J'ajouterai un fragment de code avec des corrections (ce qui doit être remplacé par quoi) :
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
Lien vers la première partie : Ajout d'une base de données PostgreSQL à un service RESTful sur Spring Boot. Partie 1 PS Si l'un d'entre vous souhaite continuer à développer cette application éducative, je serai heureux d'ajouter un lien vers vos guides dans cet article. Peut-être qu'un jour, ce programme deviendra quelque chose de similaire à une véritable application commerciale sur laquelle vous pourrez ajouter du travail à votre portefeuille. PPS Concernant ce modeste article, j'ai décidé de consacrer cet essai de plume à nos chères filles, femmes et dames. Qui sait, peut-être qu'il n'y aurait plus de Java, ni de JavaRush, ni de programmation dans la nature, sans cette femme . Félicitations pour vos vacances, nos chers gens intelligents ! Joyeux 8 mars ! Soyez heureuse et belle !
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION