JavaRush /Blogue Java /Random-PT /Adicionando um banco de dados PostgreSQL a um serviço RES...
Artur
Nível 40
Tallinn

Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot. Parte 2

Publicado no grupo Random-PT
Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot. Parte 1 Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot.  Parte 2 - 1 Assim, na última parte aprendemos como instalar um banco de dados PostgresSQL em um computador, criar um banco de dados no pgAdmin e também criar e excluir tabelas nele de forma manual e programática. Nesta parte iremos reescrever nosso programa para que ele aprenda a trabalhar com este banco de dados e tabelas. Por que nós? Porque eu mesmo estou aprendendo com você com este material. E então não apenas resolveremos a tarefa em questão, mas também corrigiremos os erros que surgirem em movimento, com a ajuda dos conselhos de programadores mais experientes. Por assim dizer, aprenderemos a trabalhar em equipe ;) Primeiro, vamos criar com.javarush.lectures.rest_exampleum novo pacote em uma pasta e chamá-lo repository. Neste pacote criaremos uma nova 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>  {
}
Esta interface irá interagir “magicamente” com nossos bancos de dados e tabelas. Por que magicamente? Porque não precisaremos escrever sua implementação, e o framework Spring nos fornecerá isso. Você só precisa criar essa interface e já pode usar essa “mágica”. O próximo passo é editar a classe Clientassim:
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
}
Tudo o que fizemos nesta aula foi apenas adicionar algumas anotações. Vamos examiná-los:
  • @Entity – indica que este bean (classe) é uma entidade.
  • @Table – indica o nome da tabela que será exibida nesta entidade.
  • @Id - id da coluna (chave primária - o valor que será usado para garantir a exclusividade dos dados na tabela atual. Nota: Andrei )
  • @Column – indica o nome da coluna mapeada para a propriedade da entidade.
  • @GeneratedValue – indica que esta propriedade será gerada de acordo com a estratégia especificada.
Os nomes dos campos da tabela não precisam corresponder aos nomes das variáveis ​​da classe. Por exemplo, se tivermos uma variável firstName, nomearemos o campo na tabela first_name. Essas anotações podem ser definidas diretamente nos campos e em seus getters. Mas se você escolher um desses métodos, tente manter esse estilo em todo o programa. Usei o primeiro método apenas para encurtar as listagens. Uma lista mais completa de anotações para trabalhar com bancos de dados pode ser encontrada aqui . Agora vamos para a aula ClientServiceImple reescrevemos da seguinte forma:
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;
    }
}
Como você pode ver na listagem, tudo o que fizemos foi excluir as linhas que não precisávamos mais:
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Em vez disso, declaramos nossa interface ClientRepositorye também colocamos a anotação @Autowired acima dela para que o Spring adicionasse automaticamente essa dependência à nossa classe. Também delegamos todo o trabalho a esta interface, ou melhor, à sua implementação, que o Spring irá adicionar. Vamos para a etapa final e mais interessante - testar nosso aplicativo. Vamos abrir o programa Postman (veja como usá-lo aqui ) e enviar uma solicitação GET para este endereço: http://localhost:8080/clients. Obtemos esta resposta:
[
    {
        "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)"
    }
]
Enviamos uma solicitação POST:
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
E... detectamos nosso primeiro bug no programa:
{
    "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"
}
Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot.  Parte 2 - 2 Examinamos os logs e encontramos o seguinte erro:

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
Enviamos novamente a mesma solicitação POST, o resultado é o mesmo, mas com esta diferença: Ключ "(id)=(2)" уже существует. Enviamos a mesma solicitação pela terceira vez e obtemos Status: 201 Criado. Enviamos a solicitação GET novamente e recebemos uma resposta:
[
    {
        "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"
    }
]
Isso sugere que nosso programa está ignorando o fato de que esta tabela já foi pré-preenchida e está atribuindo um ID novamente a partir de um. Bom, bug é um momento de trabalho, não se desespere, isso acontece com frequência. Portanto, recorrerei à ajuda de colegas mais experientes: “Caros colegas, por favor, informem nos comentários como consertar isso para que o programa funcione normalmente”. A ajuda não demorou muito para chegar, e Stas Pasinkov me disse nos comentários em que direção eu precisava olhar. Agradecimentos especiais a ele por isso! Mas o problema é que na aula Clientespecifiquei incorretamente a estratégia de anotação @GeneratedValue(strategy = GenerationType.IDENTITY)do campo id. Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot.  Parte 2 - 3 Esta estratégia é adequada para MySQL. Se trabalharmos com Oracle ou PostrgeSQL, precisaremos definir uma estratégia diferente. Você pode ler mais sobre estratégias para chaves primárias aqui . Eu escolhi a estratégia GenerationType.SEQUENCE. Para implementá-lo, precisaremos reescrever um pouco o arquivo initDB.sql e, claro, as anotações do campo id da classe Client. Reescreva 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;
O que mudou: o tipo da coluna id da nossa tabela mudou, mas falaremos mais sobre isso depois. Adicionamos uma linha abaixo na qual criamos uma nova sequência clients_id_seq, indicamos que ela deve começar com três (pois o último id do arquivo populateDB.sql é 2) e indicamos que o incremento deve ocorrer em um. Vamos voltar ao tipo de coluna id. Aqui especificamos INTEGER, pois se sairmos de SERIAL, a sequência será criada automaticamente, com o mesmo nome clients_id_seq, mas começará a partir de um (o que gerou o bug do programa). No entanto, agora se quiser excluir uma tabela, você precisará excluir adicionalmente essa sequência manualmente por meio da interface pgAdmin ou por meio de um arquivo .sql usando os seguintes comandos:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Mas se você não usar um arquivo como populateDB.sql para preencher inicialmente a tabela, poderá usar os tipos SERIAL ou BIGSERIAL para a chave primária e não precisará criar a sequência manualmente e, portanto, não precisará excluir separadamente. Você pode ler mais sobre as sequências no site da. Documentação do PostgreSQL . Vamos passar para as anotações dos campos idde classe Cliente formatá-las da seguinte forma:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
O que fizemos: instalamos uma nova anotação @SequenceGeneratorpara criar um gerador de sequência, atribuímos um nome a ele clientsIdSeq, indicamos que este é um gerador para uma sequência clients_id_seqe adicionamos um atributo. allocationSize = 1 Este é um atributo opcional, mas se não fizermos isso, obteremos o seguinte erro ao executar o programa:

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]
Aqui está o que o usuário Andrei escreve sobre isso nos comentários: alocaçãoSize tem como objetivo principal reduzir a viagem do hibernate ao banco de dados em busca de um “novo id”. Se o valor == 1, hiberna para cada nova entidade, antes de salvá-la no banco de dados, “executa” no banco de dados para obter o id. Se o valor for > 1 (por exemplo, 5), o hibernate entrará em contato com o banco de dados para obter um “novo” id com menos frequência (por exemplo, 5 vezes) e, ao entrar em contato, o hibernate solicitará ao banco de dados que reserve esse número (em nosso caso, 5) valores. O erro que você descreveu sugere que o hibernate gostaria de receber 50 ids padrão, mas no banco de dados você indicou que está pronto para emitir id para esta entidade apenas de acordo com o 1º . Outro bug foi detectado pelo usuário Nikolya Kudryashov : Se você executar uma solicitação do artigo original http://localhost:8080/clients/1, o erro será retornado:
{
    "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"
}
Este erro está relacionado à inicialização lenta do Hibernate e, para nos livrar dele, precisamos adicionar uma anotação adicional à classe Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
Desta maneira:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Agora vamos executar nosso programa (depois de remover a tabela de clientes do banco de dados se ela permanecer lá desde a última vez) e comentar 3 linhas do arquivo application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
Da última vez comentamos apenas a última linha, mas... Como já criamos e preenchemos a tabela, isso me pareceu mais lógico no momento. Vamos passar aos testes, realizar solicitações GET, POST, PUT e DELETE via Postman, e veremos que os bugs desapareceram e tudo está funcionando bem. É isso, trabalho concluído. Agora podemos resumir brevemente e considerar o que aprendemos:
  • Instale o PostgreSQL no seu computador
  • Crie bancos de dados no pgAdmin
  • Crie e exclua tabelas manualmente e programaticamente
  • Preencher tabelas por meio de arquivos .sql
  • Aprendemos um pouco sobre a interface “mágica” JpaRepository do framework Spring
  • Aprendemos sobre alguns bugs que podem surgir ao criar tal programa
  • Percebemos que não deveríamos ter vergonha de pedir conselhos aos colegas
  • Confirmamos que a comunidade JavaRush é uma força que sempre virá em socorro ;)
Podemos terminar aqui por enquanto. Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot.  Parte 2 - 4Obrigado a todos que reservaram um tempo para ler este material. Ficarei feliz em ver seus comentários, observações, acréscimos e críticas construtivas. Talvez você ofereça soluções mais elegantes para este problema, que prometo acrescentar a este artigo através da “palavra-chave” UPD, com menção a você como autor, é claro. Bem, em geral, escreva se você gostou deste artigo e desse estilo de apresentação do material e, em geral, se devo continuar escrevendo artigos sobre JR. Aqui estão as adições: UPD1: o usuário Justinian recomendou fortemente que eu renomeasse o pacote com.javarush.lectures.rest_examplepara com.javarush.lectures.rest.example, e o nome do projeto, para não violar as convenções de nomenclatura em Java. O usuário do UPD2 Alexander Pyanov sugeriu que para inicializar um campo ClientRepositoryem uma classe ClientServiceImplé melhor usar um construtor do que uma anotação @Autowired. Isso se explica pelo fato de que, em casos raros, você pode obter NullPointerExceptione, em geral, essa é a melhor prática, e eu concordo com ela. Logicamente, se um campo é necessário para a funcionalidade inicial de um objeto, então é melhor inicializá-lo no construtor, pois uma classe sem construtor não será montada em um objeto, portanto, este campo será inicializado no estágio de criação de objetos. Adicionarei um fragmento de código com correções (o que precisa ser substituído por quê):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
Link para a primeira parte: Adicionando um banco de dados PostgreSQL a um serviço RESTful no Spring Boot. Parte 1 PS Se algum de vocês quiser continuar desenvolvendo este aplicativo educacional, terei prazer em adicionar um link para seus guias neste artigo. Talvez algum dia este programa se transforme em algo semelhante a um aplicativo de negócios real ao qual você possa adicionar trabalho ao seu portfólio. PPS Em relação a este modesto artigo, resolvi dedicar este teste da caneta às nossas queridas meninas, mulheres e senhoras. Quem sabe, talvez agora não houvesse Java, nem JavaRush, nem programação na natureza, se não fosse por esta mulher . Parabéns pelas suas férias, nossos queridos espertinhos! Feliz 8 de março! Seja feliz e lindo!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION