Liv Ro Java Spring
Liv Ro Java Spring
Professional
Coleção Java Spring
Nelio Alves
Dados Internacionais de Catalogação na Publicação (CIP)
(Câmara Brasileira do Livro, SP, Brasil)
Alves, Nelio
Java spring professional [livro eletrônico] /
Nelio Alves. -- Belo Horizonte, MG : Neme Digital,
2025. -- (Coleção java spring)
PDF
ISBN 978-65-999289-6-3
25-263290 CDD-005.133
Índices para catálogo sistemático:
Estrutura do Curso
Este livro está organizado em seis capítulos detalhados, cada um abordando aspectos funda-
mentais para dominar o Spring Boot. Você começará entendendo os Componentes e Injeção
de Dependência, essenciais para qualquer aplicação Spring. Este capítulo inicial não só fun-
damenta seu conhecimento nas capacidades básicas do Spring, mas também o prepara para
explorar configurações mais complexas e personalizadas.
À medida que avançamos, você mergulhará no mundo dos Modelos de Domínio e ORM,
descobrindo como mapear objetos para um banco de dados de forma eficiente, passando por
todas as complexidades de JPA e Hibernate. Essas habilidades são cruciais para trabalhar com
dados de maneira profissional e eficaz.
No terceiro capítulo, o foco será em construir e estruturar uma API REST. Você aprenderá
sobre padrões de design de API, CRUD, e como lidar com exceções e validações, garantindo
que sua aplicação não só funcione bem, mas também seja fácil de manter e expandir.
Os capítulos subsequentes cobrirão tópicos avançados como JPA, Consultas SQL e JPQL,
onde você aprimorará sua habilidade em otimizar consultas e entenderá melhor a performance
de aplicações. O capítulo sobre Login e Controle de Acesso demonstra a implementação de
autenticação e segurança, aspectos cruciais para qualquer aplicativo moderno.
Por fim, o livro conclui com práticas essenciais de Homologação e Implantação com
CI/CD, preparando você para o mundo real das aplicações empresariais, onde a entrega con-
tínua e a integração contínua definem o ritmo do desenvolvimento.
O sistema DSCommerce
Vamos construir passo a passo um sistema de comércio, o qual chamamos de DSCommerce.
Durante o processo, vamos consultar o documento de requisitos, bem como o design Figma
do front end, valendo lembrar que este livro é sobre a construção do back end deste sistema,
então na verdade o design Figma nos servirá para ajudar a compreender melhor o que deve ser
implementado.
O documento de requisitos (que inclusive contém o link para o design Figma) pode ser acessado
no material de apoio anexo.
Design Figma do sistema DSCommerce. Será utilizado durante o desenvolvimento para nos ajudar a
compreender melhor o que deve ser implementado.
Escolha da IDE
Para desenvolver com Java e Spring, você pode utilizar a IDE que preferir. Aqui estão algumas
opções populares:
• IntelliJ IDEA: Conhecida por sua eficiência e integração com o ecossistema Spring.
A versão Community é gratuita, enquanto a versão Ultimate oferece recursos adicionais
voltados para o desenvolvimento web e empresarial.
• Spring Tool Suite (STS): Uma versão do Eclipse customizada para desenvolvimento
Spring. Inclui ferramentas específicas para o Spring e é totalmente gratuita.
• Visual Studio Code (VS Code): Leve e versátil, com excelente suporte para Java atra-
vés de extensões, como o Java Extension Pack, que inclui tudo necessário para começar
a programar em Java.
Conteúdo
1
3.12 CRUD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3.13 Busca Paginada de Produtos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.14 Inserindo Novo Produto com POST . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.15 Customizando Resposta com ResponseEntity . . . . . . . . . . . . . . . . . . . . 67
3.16 Atualizando Produto com PUT . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
3.17 Deletando Produto com DELETE . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.18 Criando Exceções de Serviço Customizadas . . . . . . . . . . . . . . . . . . . . . 73
3.19 Tratando Exceção com Resposta Customizada . . . . . . . . . . . . . . . . . . . 75
3.20 Implementando Outras Exceções . . . . . . . . . . . . . . . . . . . . . . . . . . 77
3.21 Validação com Bean Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.22 Customizando a Resposta da Validação . . . . . . . . . . . . . . . . . . . . . . . 82
2
4.36 Evitando consultas lentas muitos-para-muitos . . . . . . . . . . . . . . . . . . . 154
4.37 Evitando Consultas Lentas Muitos-Para-Um com countQuery . . . . . . . . . . 157
3
6.13 Perfil dev de homologação local . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
6.14 Script SQL de criação da base de dados . . . . . . . . . . . . . . . . . . . . . . 237
6.15 Criando Projeto e Base de Dados no Heroku . . . . . . . . . . . . . . . . . . . . 237
6.16 Preparando o Projeto para Implantação no Heroku . . . . . . . . . . . . . . . . 238
6.17 Implantando a aplicação . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
6.18 Testando a aplicação no Heroku . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
4
Capítulo 1
Componentes Coesos
A coesão refere-se à medida em que as responsabilidades de um único módulo ou componente
são unicamente definidas e estritamente alinhadas. Componentes coesos possuem várias carac-
terísticas distintas:
• Responsabilidade Única: Cada componente deve ter uma única responsabilidade ou
função dentro do sistema. Isso simplifica o entendimento do sistema, facilita a manuten-
ção e minimiza os impactos de mudanças, pois as alterações em um componente coeso
raramente afetam outros componentes.
• Clareza: A clareza na definição de responsabilidades ajuda outros desenvolvedores a
entender rapidamente o papel de cada componente no sistema, sem a necessidade de
decifrar uma teia complexa de dependências e funcionalidades misturadas.
5
Desacoplamento de Componentes
Desacoplamento é o princípio de reduzir as dependências diretas entre diferentes componentes
de um sistema. A adoção dessa prática traz diversos benefícios:
• Independência: Componentes desacoplados podem ser desenvolvidos, testados, e mo-
dificados de forma independente uns dos outros, o que aumenta a eficiência do desenvol-
vimento e reduz o risco de erros propagados.
• Flexibilidade: Menos dependências facilitam a reconfiguração e a adaptação do sistema
para atender a novas necessidades ou integrar novas tecnologias.
• Facilidade de substituição: Substituir um componente se torna uma tarefa trivial
quando ele é desacoplado, pois suas interfaces com o restante do sistema são bem definidas
e limitadas.
Enunciado do Problema
O problema proposto é desenvolver um programa que leia os dados de um funcionário (nome e
salário bruto) e depois calcule e mostre o salário líquido, considerando descontos de impostos
e previdência. As regras para os cálculos são as seguintes: 1) Imposto é 20% 2) Previdência é
10%
Exemplo:
6
Nome: Maria
Salário bruto: 4000.00
Salário líquido = 2800.00
[Link] ();
}
7
System .out. print ("Nome: ");
String name = sc. nextLine ();
System .out. print (" Salario bruto : ");
double grossSalary = sc. nextDouble ();
[Link] ();
}
package entities ;
public Employee () {
}
package services ;
8
public double discount ( double amount ) {
return amount * 0.1;
}
}
package services ;
package services ;
// FORMA ERRADA
private TaxService taxService = new TaxService ();
private PensionService pensionService = new PensionService ();
Discussão
A abordagem baseada em componentes, embora mais extensa, traz uma série de benefícios:
• Modularidade: Cada parte do programa tem uma responsabilidade clara e definida.
• Manutenabilidade: É mais fácil de modificar ou corrigir bugs em um sistema modula-
rizado, já que as mudanças em um componente geralmente não afetam outros.
• Reaproveitamento: Componentes como TaxService ou PensionService podem ser reuti-
lizados em outras partes do sistema ou em outros projetos.
Considerações Finais
Por enquanto, as dependências nos componentes foram instanciadas diretamente dentro dos
serviços, o que não é uma prática ideal. Nas próximas seções, introduziremos conceitos de
9
inversão de controle e injeção de dependência para melhorar ainda mais a estrutura do
código, aumentando sua flexibilidade e a capacidade de manutenção.
Inversão de Controle
Analogia do carro: Pense em um carro, onde o motor depende da bateria para funcionar.
Embora essa dependência exista, a base de encaixe da bateria não está localizada dentro do
motor. Isso ocorre porque, se fosse necessário trocar a bateria, não seria preciso desmontar o
motor inteiro. Este design facilita a manutenção e substituição da bateria.
Generalizando: Essa analogia pode ser aplicada ao desenvolvimento de software. Se um
componente A depende de um componente B, idealmente, A não deve controlar como essa
dependência é gerenciada ou instanciada. Se A fosse responsável por criar B diretamente,
qualquer mudança em B poderia exigir uma mudança correspondente em A, o que viola o
princípio de baixo acoplamento e alta coesão. A ideia é “inverter o controle”, transferindo a
responsabilidade de gerenciar a dependência para fora do componente A, geralmente para um
framework ou container.
Injeção de Dependência
Implementando a inversão de controle: Uma vez que a inversão de controle é estabelecida
como princípio, surge a necessidade de “injetar” as dependências necessárias nos componentes.
A injeção de dependência pode ser realizada de várias maneiras:
• Construtor: A dependência é fornecida através do construtor do componente, garan-
tindo que o componente não possa ser criado sem suas dependências. Este método é
amplamente utilizado por sua simplicidade e pela garantia de que as dependências são
imutáveis após a criação do objeto.
• Método set: A dependência é injetada através de um método setter. Isso permite a
alteração das dependências de um componente após sua criação. Embora flexível, este
método pode levar a estados inconsistentes se o componente for usado antes de todas as
suas dependências serem definidas.
• Container de injeção de dependência: Frameworks modernos, como o Spring, usam
containers que automaticamente gerenciam as dependências. Esses containers cuidam da
criação e fornecimento de todas as dependências necessárias, baseando-se em configura-
ções definidas pelo desenvolvedor, como anotações no código ou configurações XML.
A inversão de controle e a injeção de dependência são essenciais para a construção de aplicações
que são fáceis de testar, manter e evoluir. Eles permitem que os desenvolvedores se concentrem
na lógica de negócios, enquanto o gerenciamento de dependências é tratado de forma trans-
parente pelo framework. No contexto do Spring Boot, estas práticas não apenas facilitam a
10
manutenção e a expansão de aplicações, mas também promovem um design limpo e modu-
lar. Ao longo deste livro, você verá como esses princípios são aplicados para criar aplicações
eficientes e elegantes.
package services ;
@Override
public double tax( double amount ) {
return amount * 0.3; // Alíquota de imposto específica para o Brasil
}
}
package services ;
11
public double netSalary ( Employee employee ) {
return employee . getGrossSalary () - taxService .tax( employee .
getGrossSalary ())
- pensionService . discount ( employee . getGrossSalary ());
}
}
Demonstração na Aplicação
Na aplicação principal, a troca do serviço de impostos é feita simplesmente passando uma
instância de BrazilTaxService ao invés de uma instância de TaxService para o SalaryService.
Isso ilustra como a inversão de controle e a injeção de dependência permitem a substituição de
componentes sem necessidade de alterar os componentes que os utilizam.
package app;
[Link] ();
}
12
Essa flexibilidade demonstra o poder da inversão de controle combinada com a injeção de
dependência. SalaryService se mantém inalterado, mesmo quando a implementação concreta
de TaxService muda, mantendo o sistema aderente ao princípio OCP e facilitando a manutenção
e evolução do software.
1.6 Frameworks
No mundo do desenvolvimento de software, os frameworks desempenham um papel crucial
ao oferecer uma estrutura ou uma “armação” robusta que simplifica e acelera a criação de
aplicações complexas. Essas ferramentas não só fornecem uma fundação sobre a qual os de-
senvolvedores podem construir, mas também gerenciam aspectos repetitivos e complexos do
desenvolvimento, permitindo que os desenvolvedores se concentrem na lógica de negócios espe-
cífica de seus projetos.
O que é um Framework?
Literalmente traduzido como “estrutura” ou “estrutura de trabalho”, um framework em progra-
mação é um conjunto de ferramentas que oferece uma infraestrutura para desenvolver sistemas
de maneira produtiva. Essa infraestrutura geralmente inclui:
• Injeção de Dependência: Facilita o gerenciamento de dependências entre os compo-
nentes, promovendo um acoplamento mais fraco e uma maior modularidade.
• Gerenciamento de Transações: No back end, os frameworks podem gerenciar auto-
maticamente o início e o fim de transações, garantindo consistência e integridade dos
dados.
• Ciclo de Vida e Escopo de Componentes: Controla como e quando os componentes
são criados, destruídos e reutilizados, otimizando o uso de recursos e a performance da
aplicação.
• Configurações: Permite definir e modificar configurações de maneira centralizada, sem
alterar o código base.
• Integrações: Facilita a integração com outras aplicações, serviços ou APIs, expandindo
as funcionalidades da aplicação sem grandes complicações.
• Outras funcionalidades: Pode incluir segurança, mapeamento de dados, rotinas de
teste, entre outros.
13
• Instanciar Componentes: Cria instâncias dos componentes conforme necessário, ge-
renciando sua criação de forma eficiente.
• Resolver Dependências: Automagicamente conecta os componentes com as depen-
dências requeridas, garantindo que todas as necessidades sejam satisfeitas antes de um
componente ser utilizado.
• Reaproveitar Componentes: Mantém instâncias de componentes que podem ser reu-
tilizados, reduzindo a necessidade de criação constante de novos objetos e, assim, melho-
rando a performance.
• Gerenciar Escopo e Ciclo de Vida: Controla a duração e a visibilidade dos compo-
nentes, decidindo se são de longa duração, de sessão, de requisição ou de aplicação, além
de gerenciar quando são criados e destruídos.
Frameworks são fundamentais no desenvolvimento moderno por encapsularem práticas de co-
dificação complexas e repetitivas, permitindo aos desenvolvedores focar na criação de funciona-
lidades únicas para suas aplicações. Ao abstrair a complexidade de tarefas como gerenciamento
de dependências e ciclo de vida de componentes, eles não só aumentam a produtividade, mas
também promovem um código mais limpo, testável e manutenível.
14
• Artifact: Digite ‘aula’. Este é o nome do seu projeto e do artefato que será cons-
truído.
• Name: O nome será preenchido automaticamente com base no artefato. Você pode
deixá-lo como está ou personalizá-lo.
• Description: Forneça uma breve descrição do projeto.
• Package name: Será automaticamente preenchido com base no grupo e no artefato,
mas você pode modificar se necessário.
• Packaging: Deixe como ‘Jar’, que é o padrão para projetos Spring Boot.
• Java: Selecione a versão do Java que você escolheu anteriormente.
4. Dependências:
• Adicione a dependência ‘Spring Web’. Esta dependência permite que o Spring Boot
sirva conteúdo web e RESTful APIs.
5. Gere o Projeto: Clique em ‘Generate’ para baixar um arquivo zip contendo o projeto.
15
• Clique em Browse e navegue até o diretório onde você extraiu o projeto Spring Boot.
• Selecione o diretório e clique em Finish. O STS detectará o arquivo [Link] e confi-
gurará o projeto automaticamente.
4. Verificar o Projeto:
• Após a importação, o projeto aparecerá no Package Explorer.
• Expanda o projeto para verificar se a estrutura de diretórios e arquivos está corre-
tamente importada.
IntelliJ IDEA
IntelliJ IDEA é conhecida por sua poderosa funcionalidade e suporte integrado para projetos
Spring Boot. Siga estes passos para importar seu projeto:
1. Abrir o IntelliJ IDEA:
• Inicie o IntelliJ IDEA.
2. Importar Projeto:
• Na tela inicial, clique em Open.
• Alternativamente, se você já tem o IntelliJ aberto, vá até o menu File e selecione
Open.
3. Selecionar o Projeto:
• Navegue até o diretório onde o projeto Spring Boot foi extraído.
• Selecione o diretório do projeto ou o arquivo [Link] e clique em OK.
4. Configurar Projeto:
• O IntelliJ irá abrir uma janela perguntando se você deseja abrir o diretório como um
projeto, anexar à janela atual ou abrir em uma nova janela. Escolha Open as Project.
• O IntelliJ automaticamente configura o projeto baseado no [Link], identificando-o
como um projeto Maven.
5. Verificar e Configurar JDK:
• Após abrir o projeto, vá para File > Project Structure > Project.
• Verifique se o projeto está configurado com a JDK correta (a mesma versão que você
selecionou no Spring Initializr).
• Se a JDK não estiver configurada, você pode adicionar uma JDK através desta
janela.
16
• Inicie o Spring Tools Suite.
2. Importar Projeto:
• No menu, vá em File > Import....
• Na janela de importação, escolha Existing Maven Projects sob o diretório Maven.
• Clique em Next.
3. Selecionar o Diretório do Projeto:
• Clique em Browse e encontre o diretório onde seu projeto Spring Boot foi descom-
pactado.
• Selecione o diretório e certifique-se de que o [Link] está selecionado.
• Clique em Finish. O STS agora deverá resolver as dependências do Maven e confi-
gurar o projeto automaticamente.
4. Verificar o Projeto:
• O projeto agora aparecerá na Project Explorer ou Package Explorer.
• Expanda o projeto para ver a estrutura de arquivos, incluindo pacotes e recursos.
IntelliJ IDEA
IntelliJ IDEA é uma IDE desenvolvida pela JetBrains que suporta uma integração extensa com
o ecossistema Spring, incluindo Spring Boot. Siga estes passos para importar um projeto Spring
Boot:
1. Abrir IntelliJ IDEA:
• Inicie o IntelliJ IDEA. Se for a primeira inicialização, você pode importar o projeto
diretamente na tela de boas-vindas. Caso contrário, você pode selecionar File >
Open no menu principal.
2. Selecionar o Projeto:
• Navegue até o diretório onde o projeto Spring Boot foi descompactado.
• Você pode selecionar o diretório do projeto ou diretamente o arquivo [Link]. Clique
em OK.
3. Configurar o Projeto:
• IntelliJ reconhecerá automaticamente que se trata de um projeto Maven. Se for
solicitado, configure a JDK para o projeto, selecionando a versão do Java que você
usou no Spring Initializr.
• O IntelliJ irá configurar o projeto e resolver as dependências.
4. Verificar o Projeto:
• Uma vez que o projeto esteja aberto, explore a estrutura de arquivos no painel à
esquerda.
• Abra algumas classes ou recursos para garantir que tudo foi importado e está sendo
exibido corretamente.
17
Estrutura do Projeto
Primeiro, vamos organizar o nosso projeto em pacotes e classes, utilizando as convenções e
práticas recomendadas pelo Spring Boot.
Classe Employee
Esta classe representa a entidade Employee. Ela armazena informações como nome e salário
bruto do funcionário.
public Employee () {
}
Serviço TaxService
Este serviço é responsável pelo cálculo do imposto. A anotação @Service indica que o Spring
deve gerenciar esta classe como um componente de serviço.
@Service
public class TaxService {
18
public double tax( double amount ) {
return amount * 0.2; // Imposto de 20%
}
}
Serviço PensionService
Semelhante ao TaxService, este serviço calcula a contribuição da previdência.
@Service
public class PensionService {
Serviço SalaryService
Este é o serviço central que utiliza TaxService e PensionService para calcular o salário líquido.
A injeção de dependências é feita via anotação @Autowired, permitindo que o Spring Boot cuide
da criação e gerenciamento desses componentes.
@Service
public class SalaryService {
@Autowired
private TaxService taxService ;
@Autowired
private PensionService pensionService ;
19
Classe de Aplicação AulaApplication
Finalmente, a classe de aplicação onde o Spring Boot é inicializado e onde é realizado o teste
do cálculo do salário líquido.
@SpringBootApplication
public class AulaApplication implements CommandLineRunner {
@Autowired
private SalaryService salaryService ;
@Override
public void run( String ... args) throws Exception {
Employee employee = new Employee (" Maria ", 4000.0) ;
System .out. println (" Salario líquido = " + salaryService . netSalary (
employee ));
}
}
Execução do projeto
A execução do projeto dentro da IDE é bastante simples. Geralmente você vai contar com uma
opção menu similar a run a partir do clique com o botão direito sobre o nome do projeto.
Particularmente na IDE STS, por exemplo, há também uma aba “Boot Dashboard” na interface
gráfica da IDE, onde pode-se clicar com o botão direito sobre o nome do projeto e escolher (
Re)start para executar o projeto. Esta opção é recomendada, pois se o projeto já estiver em
execução, a execução será automaticamente interrompida e reiniciada.
Uma vez executado o projeto, você pode acompanhar o log de execução na aba Console da sua
IDE. Nesta aba o projeto vai imprimindo informações sobre a execução, e você deverá visualizar
lá o valor 2800.0, que é o resultado do processamento do salário líquido do funcionário.
20
do código, pois desacopla a criação de objetos do uso dos mesmos, seguindo os princípios de
inversão de controle e injeção de dependência discutidos anteriormente.
Vale ressaltar que poderíamos ter utilizado a annotation @Component ao invés da @Service, pois
ambas tem o mesmo efeito, com a diferença que @Service apenas possui um nome mais sugestivo
para o tipo de classe que estamos registrando como componentes, que são serviços do sistema.
Vale ressaltar também que a injeção dos componentes poderia ter sido feita por meio de cons-
trutores ao invés do @Autowired. O framework Spring aceita ambas as formas. Um exemplo
de injeção de dependência com construtor é mostrado a seguir na implementação alternativa
da classe SalaryService.
@Service
public class SalaryService {
21
Capítulo 2
Nossa jornada pela modelagem de domínio será conduzida principalmente pelo uso do Java
Persistence API (JPA) e Spring Data JPA. O JPA é uma poderosa especificação padrão
para mapeamento objeto-relacional, que facilita a integração de aplicações Java com bancos de
dados relacionais. Com ele, podemos definir como objetos em nossa aplicação correspondem às
tabelas em um banco de dados, manipulando esses dados de forma mais intuitiva e orientada a
objetos.
Por sua vez, o Spring Data JPA é um subframework do ecossistema Spring que simplifica
ainda mais a implementação do JPA, reduzindo a quantidade de código boilerplate necessário
e automatizando muitas das tarefas repetitivas associadas ao gerenciamento de dados. Com
22
Spring Data JPA, você pode focar mais na lógica do negócio enquanto o framework cuida da
complexidade do acesso a dados.
Durante este capítulo, adotaremos a abordagem code first para o mapeamento objeto-relaci-
onal (ORM). Isso significa que começaremos escrevendo o código das nossas classes de domínio
em Java, equipadas com annotations do JPA que definem como essas classes e seus campos
se relacionam com as tabelas e colunas de um banco de dados. A partir dessas definições, a
estrutura do banco de dados será gerada automaticamente, permitindo uma integração fluida
e direta entre o código da aplicação e o banco de dados.
Utilizar essa abordagem nos permite modelar o domínio de nossa aplicação de maneira clara
e coesa, refletindo as necessidades reais do negócio diretamente no código. As annotations do
JPA servirão como pontes, traduzindo nosso modelo de objetos para o modelo relacional de
maneira eficiente e controlada.
Este capítulo é uma oportunidade fantástica para aprofundar seus conhecimentos e habilidades
no trabalho com banco de dados em aplicações Java. Ao dominar a modelagem de domínio
com JPA e Spring Data JPA, você estará equipado para construir aplicações mais robustas,
escaláveis e fáceis de manter. Prepare-se para transformar seu modo de interagir com dados em
suas aplicações, tornando o processo mais intuitivo, estruturado e produtivo. Vamos embarcar
juntos nesta jornada de aprendizado e descoberta, e transformar os desafios em oportunidades
para crescer como desenvolvedores!
23
fundamental para aplicações que necessitam de interação direta com o banco de dados, sem o
uso de abstrações adicionais.
24
2.4 Documento de requisitos do sistema DSCommerce
Introdução ao Documento de Requisitos
Ao iniciar o desenvolvimento de qualquer sistema, é fundamental ter uma compreensão clara
dos requisitos. O documento de requisitos do sistema DSCommerce, o qual pode ser aces-
sado no material anexo, serve como um mapa para desenvolvedores e stakeholders, delineando
funcionalidades, regras de negócio, e a interface do usuário. Este documento é essencial para
garantir que todos os envolvidos no projeto compartilhem uma visão comum do que precisa ser
construído.
Estrutura do Documento
O documento de requisitos do DSCommerce está organizado em várias seções principais, cada
uma abordando diferentes aspectos do sistema:
1. Premissas: Esta seção estabelece as bases do sistema, explicando o contexto e os obje-
tivos por trás do desenvolvimento. Ela especifica que o DSCommerce deve ser simples,
porém abrangente, permitindo a aplicação de conhecimentos fundamentais em um con-
texto prático.
2. Visão Geral do Sistema: Fornece uma descrição concisa do que o sistema deve fa-
zer, detalhando as funcionalidades principais, como manutenção de cadastro de usuários,
produtos, categorias, e a funcionalidade do carrinho de compras. Ela descreve como os
usuários interagem com o sistema, diferenciando os acessos e permissões entre clientes e
administradores.
3. Protótipos de Tela: Embora não detalhado textualmente aqui, esta seção do documento
original inclui links para protótipos visuais que ajudam a ilustrar como as interfaces do
usuário devem aparecer e funcionar. Protótipos são cruciais para alinhar as expectativas
visuais e funcionais entre desenvolvedores e stakeholders.
4. Modelo Conceitual: Descreve a estrutura de dados do sistema, incluindo entidades
importantes como produtos, pedidos, e usuários, e suas relações. Este modelo ajuda os
desenvolvedores a entender como organizar e relacionar os dados no banco de dados.
5. Casos de Uso (Visão Geral e Detalhamento): Detalha cada operação que os usuários
podem realizar no sistema, dividido em visões gerais e cenários de uso detalhados. Cada
caso de uso descreve as ações dos usuários, as respostas do sistema, e as condições sob
as quais essas ações ocorrem. Esta seção é vital para compreender como o sistema deve
responder às interações dos usuários. ### A Importância do Documento de Requisitos
Compreender este documento é essencial para qualquer pessoa envolvida no desenvolvimento do
DSCommerce, seja implementando diretamente o código ou realizando testes e manutenções.
Ele serve como uma diretriz clara para o que precisa ser desenvolvido e como cada parte do
sistema interage com as outras.
Ao modelar o domínio usando Java e Spring, utilizaremos JPA e Spring Data JPA para facilitar
o mapeamento objeto-relacional. Com essas ferramentas, transformaremos as especificações dos
modelos de dados e casos de uso em estruturas de código que o Spring pode gerenciar, usando
uma abordagem code first que nos permite criar o banco de dados relacional diretamente a
partir das classes Java.
25
À medida que você se aprofundar neste capítulo e começar a implementar o sistema DSCom-
merce, mantenha este documento de requisitos sempre acessível. Ele será seu guia através do
processo de desenvolvimento, garantindo que todos os aspectos do sistema sejam abordados
e implementados conforme esperado. Encorajamos você a mergulhar nos detalhes e a utilizar
este documento como uma ferramenta para construir um sistema robusto e eficiente. Vamos
transformar esses requisitos em realidade com precisão e criatividade!
26
– Spring Data JPA: Para integração com bancos de dados usando Java Persistence
API.
– H2 Database: Um banco de dados em memória, ideal para desenvolvimento e
testes.
4. Gerar o Projeto:
• Após adicionar as dependências, clique em Generate para baixar o projeto como um
arquivo ZIP.
5. Descompacte o Arquivo:
• Salve e descompacte o arquivo em uma pasta de sua escolha no seu computador.
27
Estrutura do Código da Classe User
@Entity
@Table (name = " tb_user ")
public class User {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
public User () {
}
28
public void setEmail ( String email ) {
this. email = email ;
}
@Override
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
@Override
public int hashCode () {
return id != null ? id. hashCode () : 0;
}
}
Anotações e Campos
• @Entity: Declara que a classe é uma entidade JPA, que será mapeada para uma tabela
no banco de dados.
• @Table(name = “tb_user”): Especifica o nome da tabela no banco de dados.
• @Id: Indica que o campo é a chave primária da tabela.
• @GeneratedValue(strategy = [Link]): Configura a gera-
ção automática dos valores da chave primária, delegando ao banco de dados a responsa-
bilidade de gerar o identificador único.
• @Column(unique = true): Define que a coluna email deve ser única no banco de
29
dados.
• LocalDate birthDate: Utiliza a classe LocalDate para armazenar datas sem informação
de tempo.
Arquivo [Link]:
Arquivo [Link]:
30
1. Abra o IntelliJ IDEA e localize a classe principal da aplicação (DscommerceApplication).
2. Execute a aplicação clicando com o botão direito do mouse sobre o arquivo e selecio-
nando Run 'DscommerceApplication'.
Para acessar o H2 Console:
1. Abra um navegador e digite o URL [Link]
2. Configure o JDBC URL como jdbc:h2:mem:testdb, deixe o usuário como sa e a senha
em branco.
3. Conecte-se para gerenciar o banco de dados através do console web.
Assim, nesta seção, criamos nossa primeira entidade do sistema, a classe User, e configuramos o
mapeamento objeto-relacional para gerar automaticamente a base de dados no banco H2. Este
processo demonstra a eficiência do Spring Boot e do Spring Data JPA na gestão e abstração
do acesso a dados, facilitando a manipulação e o gerenciamento do banco de dados de forma
automatizada e simplificada.
@Entity
@Table (name = " tb_order ")
public class Order {
31
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
@ManyToOne
@JoinColumn (name = " client_id ")
private User client ;
public Order () {
}
Explicação do Código:
• @Entity e @Table(name = “tb_order”): Anotações que indicam que esta classe é
uma entidade JPA e será mapeada para a tabela tb_order no banco de dados.
• @Id e @GeneratedValue(strategy = [Link]): Indicam que
o campo id é a chave primária da tabela e que será gerado automaticamente pelo banco
de dados.
• @Column(columnDefinition = “TIMESTAMP WITHOUT TIME ZONE”):
Especifica que o campo moment deve ser armazenado como um timestamp sem informações
de fuso horário.
• @ManyToOne e @JoinColumn(name = “client_id”): Estabelecem um relaciona-
mento muitos-para-um entre Order e User. O JoinColumn indica que a coluna client_id na
tabela tb_order é a chave estrangeira que referencia a tabela tb_user.
32
@Entity
@Table (name = " tb_user ")
public class User {
A Classe Payment
A classe Payment representa os detalhes de pagamento associados a um pedido. Vamos analisar
o código e entender cada parte do seu mapeamento com JPA.
33
import [Link]. Instant ;
@Entity
@Table (name = " tb_payment ")
public class Payment {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
@OneToOne
@MapsId
private Order order ;
public Payment () {
}
Explicação do Código:
• @Entity e @Table(name = “tb_payment”): Define a classe Payment como uma
entidade e mapeia para a tabela tb_payment.
• @Id e @GeneratedValue(strategy = [Link]): A chave pri-
mária id é gerada automaticamente pelo banco de dados.
• @Column(columnDefinition = “TIMESTAMP WITHOUT TIME ZONE”):
Especifica como o campo moment deve ser armazenado no banco de dados.
• @OneToOne e @MapsId: Estas anotações estabelecem um relacionamento um-para-
-um com a entidade Order. @MapsId indica que o id de Payment será mapeado para o mesmo
id de Order, compartilhando assim a chave primária entre as duas entidades.
@Entity
@Table (name = " tb_order ")
public class Order {
// Codigo existente
34
@OneToOne ( mappedBy = "order ", cascade = CascadeType .ALL)
private Payment payment ;
// Codigo existente
// Codigo existente
}
Explicação do Mapeamento:
• @OneToOne(mappedBy = “order”, cascade = [Link]): Configura
o relacionamento de mão dupla, onde mappedBy aponta para o campo order na entidade
Payment. A opção cascade = [Link] significa que as operações de persistência,
atualização ou exclusão em Order serão propagadas para Payment.
35
Classe Category
A classe Category representa uma categoria de produtos. Vejamos seu código e as configurações
relevantes:
@Entity
@Table (name = " tb_category ")
public class Category {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
public Category () {
}
@Override
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
36
Category category = ( Category ) o;
@Override
public int hashCode () {
return id != null ? id. hashCode () : 0;
}
}
Explicação do Código:
• @Entity e @Table(name = “tb_category”): Define a classe como uma entidade
JPA e mapeia para a tabela tb_category.
• @Id e @GeneratedValue(strategy = [Link]): Especifica
que o campo id é a chave primária e é gerado automaticamente pelo banco de dados.
• @ManyToMany(mappedBy = “categories”): Estabelece um relacionamento mui-
tos-para-muitos com a entidade Product. O mappedBy indica que a entidade Product é o
proprietário do relacionamento.
Classe Product
A classe Product representa produtos que podem pertencer a múltiplas categorias.
@Entity
@Table (name = " tb_product ")
public class Product {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
@ManyToMany
@JoinTable (name = " tb_product_category ",
joinColumns = @JoinColumn (name = " product_id "),
inverseJoinColumns = @JoinColumn (name = " category_id "))
private Set <Category > categories = new HashSet < >();
37
public Product () {
}
@Override
38
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
@Override
public int hashCode () {
return id != null ? id. hashCode () : 0;
}
}
Explicação do Código:
• @Column(columnDefinition = “TEXT”): Define que o campo description deve ser
armazenado como texto, o que é útil para descrições longas que excedem o limite padrão
de caracteres.
• @ManyToMany e @JoinTable: Configura o relacionamento muitos-para-muitos, es-
pecificando a tabela de junção tb_product_category e os campos de chave estrangeira para
ambas as entidades.
Classe OrderItemPK
A classe OrderItemPK representa a chave composta da entidade OrderItem. No JPA, chaves
compostas são implementadas usando a anotação @Embeddable, que indica que a classe pode ser
embutida em outra entidade.
39
package com. devsuperior . dscommerce . entities ;
@Embeddable
public class OrderItemPK {
@ManyToOne
@JoinColumn (name = " order_id ")
private Order order ;
@ManyToOne
@JoinColumn (name = " product_id ")
private Product product ;
public OrderItemPK () {
}
@Override
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
@Override
public int hashCode () {
int result = order != null ? order . hashCode () : 0;
result = 31 * result + ( product != null ? product . hashCode () : 0);
return result ;
}
}
40
Explicação do Código:
• @Embeddable: Marca a classe como incorporável em outras entidades.
• @ManyToOne: Define um relacionamento muitos-para-um, significando que muitos
itens de pedido podem estar associados a um único pedido ou produto.
• @JoinColumn: Especifica o nome da coluna na tabela que estabelece o vínculo.
Classe OrderItem
OrderItem usa a chave composta OrderItemPK para estabelecer relações com as entidades Product
e Order e armazenar informações adicionais como quantity e price.
@Entity
@Table (name = " tb_order_item ")
public class OrderItem {
@EmbeddedId
private OrderItemPK id = new OrderItemPK ();
public OrderItem () {
}
41
return quantity ;
}
@Override
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
@Override
public int hashCode () {
return id != null ? id. hashCode () : 0;
}
}
Explicação do Código:
• @Entity e @Table(name = “tb_order_item”): Indica que esta é uma entidade e
mapeia para uma tabela específica.
• @EmbeddedId: Utiliza OrderItemPK como chave primária da entidade, aproveitando as
configurações de chave composta.
Classe Product
@Entity
@Table (name = " tb_product ")
42
public class Product {
// codigo existente
// codigo existente
// codigo existente
}
Classe Order
@Entity
@Table (name = " tb_order ")
public class Order {
// codigo existente
// codigo existente
// codigo existente
}
43
e Order permitem uma interação fácil e direta, facilitando o acesso aos pedidos e produtos a
partir de cada classe, respectivamente. Essa abordagem não apenas atende aos requisitos de
modelagem de dados complexos mas também alinha com as boas práticas de design de banco
de dados e desenvolvimento de aplicativos.
44
• Nomes de tabelas e campos: os nomes das tabelas e campos do seu script SQL deve
ser exatamente equivalente aos nomes definidos nas suas classes Java. Se algum nome
estiver errado no script, vai ocorrer um erro na execução do seeding.
45
Capítulo 3
46
3.2 Material de apoio do capítulo
Para este capítulo, vamos utilizar os seguintes arquivos contidos no material de apoio anexo:
• 03 API REST, camadas, CRUD, exceções, validações (slides).pdf
• 03 DESAFIO CRUD de [Link]
• Projeto DSCommerce ao final do capítulo 3
API Web
Uma API Web estende o conceito de APIs para o ambiente web. Neste contexto, as funciona-
lidades são acessadas por meio de endpoints web. Estes são pontos de contato onde as APIs
podem ser acessadas e incluem:
• Host e Porta: O endereço do servidor onde a API está hospedada e a porta através da
qual as comunicações ocorrem.
• Rota: O caminho específico no servidor onde a API ou um recurso específico pode ser
acessado.
• Parâmetros e Corpo (Payload): Dados enviados para a API para refinar uma solici-
tação ou enviar informações para processamento.
• Cabeçalhos: Informações adicionais enviadas junto com a solicitação ou resposta que
podem incluir tokens de autenticação, tipos de conteúdo esperados, entre outros.
47
1. Cliente/Servidor com HTTP
O modelo REST é baseado na separação entre cliente e servidor, uma separação de responsabi-
lidades que permite que ambos evoluam de forma independente. O cliente não precisa saber os
detalhes de armazenamento de dados do servidor, enquanto o servidor não precisa se preocupar
com a interface e o estado do usuário, permitindo que a interface do usuário seja melhorada
sem afetar a lógica do servidor.
• HTTP: REST utiliza protocolo HTTP para as comunicações, empregando seus métodos
padrão (GET, POST, PUT, DELETE, etc.) para realizar operações CRUD (Create,
Read, Update, Delete) em recursos representados geralmente em formatos como JSON
ou XML.
2. Comunicação Stateless
Cada requisição de cliente para o servidor deve conter toda a informação necessária para enten-
der e completar a requisição. O servidor não deve armazenar nada sobre o estado mais recente
do cliente entre as requisições. Isso permite que o sistema seja mais fácil de escalar, pois não
depende do contexto armazenado no servidor para processar as requisições.
3. Cache
A capacidade de cache é uma característica integral do REST. As respostas devem, implícita
ou explicitamente, definir a si mesmas como cacheáveis ou não, permitindo que os clientes
reutilizem respostas armazenadas para melhorar a eficiência e a escalabilidade. Isso reduz a
carga no servidor e melhora a latência percebida pelo usuário final.
4. Interface Uniforme
Um dos principais princípios do REST é a uniformidade da interface entre componentes, que
simplifica e desacopla a arquitetura, o que permite que cada parte evolua independentemente.
As seguintes condições devem ser satisfeitas para alcançar uma interface uniforme:
• Identificação de recursos: Os recursos são identificados em requisições usando URIs
de forma padronizada.
• Manipulação de recursos através de representações: Os recursos são manipulados
através de suas representações (como JSON ou XML), e as operações são realizadas no
servidor.
• Mensagens autoexplicativas: As mensagens devem ser suficientemente autoexplicati-
vas para descrever como processá-las.
• HATEOAS (Hypermedia as the Engine of Application State): Os clientes inte-
ragem com a aplicação inteiramente através de hyperlinks fornecidos dinamicamente pelo
servidor.
5. Sistema em Camadas
O sistema em camadas restringe a interação entre componentes, permitindo que eles operem
dentro de camadas hierárquicas. Um cliente não pode geralmente ver além da camada com a
qual está interagindo, permitindo que sistemas intermediários melhorem a escalabilidade por
meio de balanceamento de carga ou caches compartilhados.
48
6. Código sob Demanda (Opcional)
Este é o único princípio opcional do REST, permitindo que servidores transfiram executáveis
ou scripts temporários para os clientes quando necessário para estender a funcionalidade do
cliente. Isso oferece uma forma de estender e personalizar a lógica do cliente sem a necessidade
de um novo deployment ou atualização.
Para mais detalhes, consulte:
[Link]
Recursos
Num sistema web, um recurso é uma entidade ou uma coleção de entidades que são con-
sideradas parte essencial da funcionalidade do sistema. Por exemplo, em uma aplicação de
e-commerce, produtos, clientes, pedidos, e categorias são todos recursos. Cada recurso é iden-
tificado e acessado através de uma URL única.
49
• GET host:port/products: Esta URL acessa a lista de todos os produtos disponíveis.
Usando o método HTTP GET, essa operação é destinada a apenas recuperar dados sem
modificar nada no servidor.
• GET host:port/products?page=3: Acessa a lista de produtos, mas com um parâmetro de
consulta page que especifica que queremos os produtos listados na página 3. Este é um
exemplo de como parâmetros de consulta podem ser usados para modificar a resposta do
servidor sem alterar o recurso em si.
• GET host:port/products/1: Acessa um produto específico identificado pelo ID, que neste
caso é 1. Este é um exemplo de parâmetro de rota, onde o identificador do recurso faz
parte do caminho da URL.
• GET host:port/products/1/categories: Recupera as categorias associadas ao produto
com ID 1, demonstrando como os caminhos da URL podem ser usados para acessar
recursos relacionados ou sub-recursos.
Padrões de URL
Em uma API REST, a ação desejada sobre um recurso deve ser expressa principalmente pelo
verbo HTTP e não pela URL. A URL deve identificar o recurso, enquanto o verbo HTTP indica
a ação a ser realizada nesse recurso.
50
Verbos (Métodos) HTTP Mais Utilizados
Os verbos HTTP definem o tipo de operação que se deseja realizar em um recurso. Os mais
comuns são:
• GET: Usado para obter um ou mais recursos. GET deve ser seguro e, idealmente,
idempotente, o que significa que não altera nenhum estado no servidor.
• POST: Utilizado para criar um novo recurso. POST não é idempotente, o que significa
que enviar várias solicitações POST idênticas podem resultar em múltiplas criações.
• PUT: Empregado para atualizar um recurso existente ou criar um recurso se ele não
existir, de maneira idempotente.
• DELETE: Usado para remover um recurso. DELETE é idempotente pois deletar o
mesmo recurso repetidamente tem o mesmo efeito que deletá-lo uma única vez.
Operação Idempotente: Uma operação é idempotente se não causar efeitos adicionais se for
executada mais de uma vez com o mesmo input.
Para mais detalhes, consulte:
[Link]
Organização em Camadas
A organização em camadas divide os componentes do sistema em grupos horizontais, cada um
com uma responsabilidade específica. Cada camada só pode interagir com a camada direta-
51
mente abaixo dela, garantindo um acoplamento baixo e uma alta coesão dentro das camadas.
Essa separação clara facilita tanto a manutenção quanto a escalabilidade do sistema.
52
seu papel e implementação em APIs REST.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@GetMapping
public String teste () {
return "Ola mundo !";
}
}
53
• @RequestMapping(value = “/products”): Define que todas as requisições que che-
gam para a rota /products serão tratadas por este controlador.
• @GetMapping: Especifica que o método teste() deve responder a requisições GET.
Quando não se especifica um caminho dentro da anotação @GetMapping, assume-se que o
método responde pela URL definida no @RequestMapping da classe.
Testando o Controlador
Após implementar o controlador, o próximo passo é testar se ele está funcionando como espe-
rado. Para isso, basta acessar a URL [Link] através de um navega-
dor web. Se tudo estiver configurado corretamente, a página deverá exibir a mensagem “Ola
mundo!”, que é o retorno do método teste() no nosso controlador.
Introdução ao Postman
O Postman é uma ferramenta poderosa usada para testar APIs. Ele permite que desenvolvedo-
res façam requisições a endereços de API de forma controlada, podendo manipular facilmente
os cabeçalhos, o corpo da requisição e ver as respostas HTTP. Usar o Postman é uma forma
eficiente de testar, desenvolver e documentar APIs.
54
3.9 Primeiro Teste com Repository
Nesta seção, vamos explorar como realizar um teste inicial com o componente Repository em
uma aplicação Spring Boot, utilizando o Spring Data JPA. O objetivo é verificar a funcionali-
dade de busca de um produto no banco de dados e retornar seu nome através de um endpoint
da API.
Implementação do ProductRepository
Vamos criar um repositório para a entidade Product. Este repositório estenderá JpaRepository,
permitindo que utilizemos os métodos fornecidos para interagir com o banco de dados.
Neste código, ProductRepository é uma interface que não requer implementação manual dos
métodos. O Spring Data JPA se encarrega de prover a implementação em tempo de execução.
55
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductRepository repository ;
@GetMapping
public String teste () {
Optional <Product > result = repository . findById (1L);
Product product = result .get ();
return product . getName ();
}
}
Explicação do Código
• @Autowired: Anotação que solicita ao Spring a injeção de dependência do
ProductRepository.
• [Link](1L): Busca no banco de dados o produto com o ID 1. Esta ope-
ração retorna um Optional<Product>, que pode ou não conter um produto.
• [Link](): Obtém o produto do Optional. Em uma aplicação real, seria prudente
verificar se o Optional realmente contém um valor para evitar exceções.
Testando a Aplicação
Para testar essa funcionalidade, acesse a URL [Link] em um navegador
ou através de uma ferramenta como Postman. Se o banco de dados estiver corretamente
configurado com os dados iniciais (seed), e o produto com ID 1 for “The Lord of the Rings”, o
endpoint retornará este nome.
Este primeiro teste com o ProductRepository demonstra a capacidade de integrar a camada de
persistência com a camada web de uma aplicação Spring Boot. Reforça a importância da sepa-
ração de responsabilidades e introduz as práticas recomendadas de acesso a dados em aplicações
modernas. Em etapas futuras, vamos refinar essa implementação para incluir tratamentos de
erros adequados e melhorar a estrutura do código para suportar uma manutenção fácil e uma
expansão segura.
56
• Controlador: Responsável por responder às interações do usuário, neste contexto de
uma API REST, essas interações são as requisições HTTP.
• Service: Realiza operações de negócio. Por exemplo, um método na camada de serviço
como registrarPedido pode realizar várias operações como verificar estoque, salvar pedido,
baixar estoque, e enviar email.
• Repository: Executa operações individuais de acesso ao banco de dados, funcionando
como a ponte entre a camada de negócios e o banco de dados.
Implementação do ProductDTO
Vamos definir um DTO para a entidade Product, que servirá para transferir informações de
produtos de maneira eficiente e segura.
57
private String name;
private String description ;
private Double price ;
private String imgUrl ;
public ProductDTO () {
}
58
package com. devsuperior . dscommerce . services ;
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
Uso de @Transactional
• @Transactional(readOnly = true): Esta anotação é usada para definir as caracterís-
ticas da transação.
– readOnly = true significa que a transação é somente leitura. Esta configuração é
importante pois informa ao gerenciador de transações e ao banco de dados que essa
operação não irá realizar nenhuma alteração no estado dos dados. Isso pode ajudar
a otimizar o desempenho das operações, especialmente em operações que envolvem
muitos dados ou são chamadas frequentemente.
– A presença de @Transactional também garante que se ocorrerem exceções durante a
operação, a transação será revertida (rollback), mantendo a consistência dos dados.
Método findById
• O método findById(Long id) é responsável por recuperar um produto específico pelo seu
ID. O uso do método findById do repositório retorna um Optional<Product>, o que é uma
prática recomendada para lidar com a possibilidade de que o ID fornecido não corresponda
a nenhum produto.
59
• A chamada get() no Optional é usada para extrair o produto, se presente. No entanto, em
uma aplicação real, você deve tratar a possibilidade de Optional estar vazio para evitar
NoSuchElementException, por exemplo, usando orElseThrow() com uma exceção customi-
zada.
• O produto recuperado é então convertido em um ProductDTO usando o construtor cor-
respondente que aceita um objeto Product. Isso desacopla o modelo de dados do banco
de dados do modelo usado nas respostas da API, permitindo que alterações internas no
modelo de entidade não afetem os clientes da API.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
O que é ModelMapper?
ModelMapper é uma biblioteca Java que automatiza o processo de mapeamento de objetos
de um tipo para outro. É amplamente utilizada em aplicações Java para mapear objetos de
60
domínio (entidades) para objetos DTO e vice-versa. O objetivo principal da ModelMapper é
simplificar o código que mapeia entre tipos de objeto, reduzindo a quantidade de código manual
que os desenvolvedores precisam escrever e manter.
Para aqueles interessados em explorar mais sobre como usar a biblioteca ModelMapper, in-
cluindo exemplos práticos, configurações avançadas, e melhores práticas, recomendo a leitura
do seguinte artigo disponível em Baeldung, uma respeitada fonte de aprendizado para desen-
volvedores Java e Spring:
[Link]
3.12 CRUD
O termo CRUD é fundamental na construção de aplicações web e sistemas de gerenciamento de
informações. Vamos explorar o que significa CRUD, como ele se aplica tanto no desenvolvimento
de front-end quanto de back-end, e preparar o terreno para uma exploração detalhada dessas
operações no contexto de Java e Spring.
O que é CRUD?
CRUD é a sigla para Create, Retrieve, Update, Delete. Essas quatro operações representam as
ações básicas que são realizadas em dados em aplicações de banco de dados. A capacidade de
completar operações CRUD é essencial para permitir que os usuários interajam com qualquer
sistema de dados de maneira eficaz. Vamos detalhar cada uma dessas operações:
• Create: A operação de criar novos registros no banco de dados.
• Retrieve: A operação de buscar ou recuperar dados já existentes. Esta operação pode
ser subdividida em buscar todos os registros ou buscar um registro específico por um
identificador.
• Update: A operação de modificar registros existentes.
• Delete: A operação de remover registros do banco de dados.
CRUD no Frontend
No contexto do frontend, um CRUD geralmente se refere a uma interface de usuário que permite
aos usuários realizar todas essas quatro operações. Uma “tela de cadastro”, por exemplo,
típicamente permite que os usuários vejam uma listagem dos registros existentes, que podem
ser filtrados por critérios específicos, além de oferecer opções para adicionar novos registros,
editar registros existentes ou excluir registros. Essas interfaces são essenciais para a gestão eficaz
dos dados dentro de sistemas empresariais, e-commerce, blogs, e outros sistemas que necessitam
de interação contínua com o banco de dados. No contexto do nosso sistema DSCommerce, o
exemplo que vamos explorar é o que seria nossa tela de cadastro de produtos, conforme mostrado
na imagem.
CRUD no Backend
No backend, as operações de CRUD correspondem às funcionalidades implementadas para
manipular dados no sistema. No contexto de APIs, estas são algumas das operações que os
desenvolvedores implementam para permitir que o frontend interaja com o banco de dados:
61
Tela de cadastro de produtos, no sistema DSCommerce
62
package com. devsuperior . dscommerce . services ;
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
Explicação do Código
• @Transactional(readOnly = true): Assegura que o método é apenas para leitura,
otimizando o acesso ao banco de dados e garantindo que nenhuma alteração será feita
durante a execução deste método.
• findAll(Pageable pageable): Este método faz uso do Spring Data JPA para buscar
todos os produtos. O objeto Pageable é um parâmetro do Spring que contém informações
sobre a paginação e ordenação. Esse método retorna uma Page<ProductDTO>, que é uma
coleção paginada de ProductDTO.
63
import org. springframework .[Link]. annotation . GetMapping ;
import org. springframework .[Link]. annotation . PathVariable ;
import org. springframework .[Link]. annotation . RequestMapping ;
import org. springframework .[Link]. annotation . RestController ;
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
@GetMapping
public Page < ProductDTO > findAll ( Pageable pageable ) {
return service . findAll ( pageable );
}
}
Explicação do Código
• @GetMapping: Define que o método findAll responderá a requisições GET para a URL
base /products. Quando acompanhado de parâmetros Pageable, o Spring automaticamente
configura a paginação baseada nos parâmetros recebidos na requisição, como page, size,
e sort.
{
" content ": [
{
"id ": 1,
"name ": "The Lord of the Rings ",
" description ": " Lorem ipsum ..." ,
" price ": 90.5 ,
" imgUrl ": " https :// raw. githubusercontent .com/ devsuperior /dscatalog -
resources / master / backend /img /1- [Link]"
},
...
],
" pageable ": {
"sort ": {
" empty ": false ,
" sorted ": true ,
" unsorted ": false
64
},
" offset ": 0,
" pageSize ": 12,
" pageNumber ": 0,
" unpaged ": false ,
"paged ": true
},
"last ": false ,
" totalElements ": 25,
" totalPages ": 3,
"size ": 12,
" number ": 0,
"sort ": {
"empty ": false ,
" sorted ": true ,
" unsorted ": false
},
"first ": true ,
" numberOfElements ": 12,
"empty ": false
}
Parâmetros de Paginação
É possível utilizar parâmetros de paginação na busca paginada. Os principais parâmetros são:
• size: Define o número de registros por página.
• page: Especifica o número da página que você deseja recuperar.
• sort: Permite especificar por qual campo os resultados devem ser ordenados e a direção
(ascendente ou descendente).
Por exemplo, a URL [Link] solicitará
ao servidor que retorne a primeira página de produtos, com 12 produtos por página, ordenados
pelo nome em ordem descendente.
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
65
@Transactional
public ProductDTO insert ( ProductDTO dto) {
Product entity = new Product ();
entity . setName (dto. getName ());
entity . setDescription (dto. getDescription ());
entity . setPrice (dto. getPrice ());
entity . setImgUrl (dto. getImgUrl ());
Explicação do Código
• @Transactional: Esta anotação inicia uma transação de banco de dados. Isso significa
que se algo der errado durante a execução deste método, todas as alterações feitas no
banco de dados serão automaticamente revertidas.
• Construção da Entidade: Um novo objeto Product é criado e suas propriedades são
configuradas com base nos valores fornecidos no ProductDTO. Isso desacopla o objeto de
transferência de dados (DTO) do objeto de entidade, o que é uma prática recomendada
em aplicações de camadas múltiplas.
• Salvar a Entidade: O objeto entity é salvo no banco de dados usando o método save
do repositório. Este método retorna a entidade persistida, agora com um id gerado pelo
banco de dados.
• Retorno como DTO: Por fim, a entidade salva é convertida de volta para um ProductDTO
antes de ser retornada ao cliente. Isso garante que a resposta contenha a representação
mais atualizada do produto, incluindo seu identificador único.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
@PostMapping
public ProductDTO insert ( @RequestBody ProductDTO dto) {
return service . insert (dto);
}
}
66
Explicação do Código
• @PostMapping: Indica que este método responde a requisições POST na URL especi-
ficada em @RequestMapping.
• @RequestBody ProductDTO dto: Este parâmetro indica que o método espera rece-
ber dados no corpo da requisição, e que esses dados devem ser convertidos automatica-
mente para um objeto ProductDTO pelo Spring.
{
"name ": "Meu produto ",
" description ": " Descrição do produto ",
" imgUrl ": " https :// teste .com/ produto .jpg",
"price ": 50
}
import [Link];
67
import org. springframework .[Link]. annotation . GetMapping ;
import org. springframework .[Link]. annotation . PathVariable ;
import org. springframework .[Link]. annotation . PostMapping ;
import org. springframework .[Link]. annotation . RequestBody ;
import org. springframework .[Link]. annotation . RequestMapping ;
import org. springframework .[Link]. annotation . RestController ;
import org. springframework .web. servlet . support . ServletUriComponentsBuilder ;
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
@GetMapping
public ResponseEntity <Page < ProductDTO >> findAll ( Pageable pageable ) {
Page < ProductDTO > dto = service . findAll ( pageable );
return ResponseEntity .ok(dto); // 200 OK with paged body
}
@PostMapping
public ResponseEntity < ProductDTO > insert ( @RequestBody ProductDTO dto) {
dto = service . insert (dto);
URI uri = ServletUriComponentsBuilder . fromCurrentRequest ().path("/{id}")
. buildAndExpand (dto. getId ()). toUri ();
return ResponseEntity . created (uri).body(dto); // 201 Created with
location header
}
}
Explicação do Código
• @GetMapping(value = “/{id}”):
– Este método usa [Link](dto) para retornar uma resposta HTTP 200
(OK), com o DTO do produto como corpo da resposta.
• @GetMapping:
– Similar ao método anterior, mas aplicado à busca paginada. A resposta também é
200 (OK), mas inclui uma página de ProductDTO como corpo.
• @PostMapping:
– O método insert é mais complexo e demonstra o poder do ResponseEntity para
manipular respostas HTTP de forma eficaz:
∗ Criação do URI: Após a inserção do produto, um URI é construído usando
ServletUriComponentsBuilder. Este URI representa o endereço do novo recurso
68
criado. A função fromCurrentRequest() pega a URI da requisição corrente, path
("/{id}") adiciona o ID do produto ao caminho, e buildAndExpand([Link]())
substitui {id} pelo ID real do produto.
∗ [Link](uri): Retorna uma resposta com o status 201 (Cre-
ated), que é ideal para operações de criação. O método created também con-
figura o header ‘Location’ para o URI do novo recurso, conforme construído
anteriormente. O corpo da resposta contém o ProductDTO do produto inserido.
Testando no Postman
Para testar a inserção de um novo produto:
1. Configure o Postman:
• Método: POST
• URL: [Link]
• Na aba Body, selecione raw e JSON.
• Corpo da requisição:
{
"name ": "Meu produto ",
" description ": " Descrição do produto ",
" imgUrl ": " https :// teste .com/ produto .jpg",
" price ": 50
}
2. Envie a requisição:
• Ao enviar, o Postman deve mostrar uma resposta com status 201 (Created). O
header ‘Location’ mostrará o URI do novo produto criado, e o corpo da resposta
incluirá os detalhes do produto.
O uso de ResponseEntity no Spring Boot oferece um controle detalhado sobre as respostas HTTP,
permitindo aos desenvolvedores configurar precisamente como os endpoints devem responder às
requisições. Isso não apenas melhora a semântica das respostas da API, mas também enriquece
a experiência do desenvolvedor e do usuário final ao interagir com a aplicação.
@Service
public class ProductService {
69
...
@Transactional
public ProductDTO insert ( ProductDTO dto) {
Product entity = new Product ();
copyDtoToEntity (dto , entity );
entity = repository .save( entity );
return new ProductDTO ( entity );
}
@Transactional
public ProductDTO update (Long id , ProductDTO dto) {
Product entity = repository . getReferenceById (id);
copyDtoToEntity (dto , entity );
entity = repository .save( entity );
return new ProductDTO ( entity );
}
Explicação do Código
• getReferenceById(id): Este método do JPA é usado para buscar uma referência à
entidade Product com o ID fornecido sem necessariamente carregar todos os seus dados
imediatamente. Isso é útil para operações de atualização onde você não precisa trabalhar
com todos os dados da entidade, mas apenas modificar alguns atributos.
• copyDtoToEntity(dto, entity): Este método auxiliar é criado para evitar a duplicação
de código. Ele copia os dados de um ProductDTO para a entidade Product. Isso assegura
que os campos da entidade sejam atualizados conforme os dados recebidos do DTO.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
70
}
Explicação do Código
• @PutMapping(value = “/{id}”): Define que este método responderá a requisições
PUT enviadas para /products/{id}, onde {id} é o ID do produto a ser atualizado.
• ResponseEntity: O método retorna um ResponseEntity contendo o DTO atualizado. O
uso de [Link](dto) encapsula o DTO atualizado em uma resposta HTTP 200
(OK), indicando que a atualização foi bem-sucedida.
{
"name ": " Produto Atualizado ",
" description ": "Nova descrição do produto ",
" price ": 75.5 ,
" imgUrl ": " https :// novaurl .com/ produto .jpg"
}
3. Envie a Requisição:
• O Postman deve mostrar uma resposta 200 OK com os detalhes do produto atuali-
zado.
A implementação da funcionalidade de atualização é essencial para permitir que os usuários
modifiquem dados existentes de maneira controlada e segura. O uso do método getReferenceById
para otimizar o acesso ao banco de dados e a separação clara entre a lógica de transferência de
dados e a lógica de negócios ajudam a manter o código organizado e eficiente.
71
Método delete na Classe ProductService
A responsabilidade da deleção de um produto no banco de dados recai sobre a camada de
serviço, onde utilizamos o ProductRepository para executar essa operação.
@Service
public class ProductService {
...
@Transactional
public void delete (Long id) {
repository . deleteById (id);
}
}
Explicação do Código
• @Transactional: Garante que a operação de exclusão será executada dentro de uma
transação. Se algum erro ocorrer durante a execução do método, a transação será rever-
tida.
• deleteById(id): Esse método do JpaRepository busca a entidade pelo ID forne-
cido e a exclui do banco de dados. Se o ID fornecido não existir, uma exceção
EmptyResultDataAccessException será lançada. Essa exceção ainda não está sendo tratada,
mas aprenderemos como lidar com ela em seções futuras.
• Retorno void: Como a deleção não retorna um objeto específico, o método foi definido
com retorno void, apenas executando a remoção.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
...
72
Explicação do Código
• @DeleteMapping(value = “/{id}”): Define que esse método responderá a requisições
HTTP DELETE enviadas para a URL /products/{id}, onde {id} é o identificador do
produto a ser excluído.
• @PathVariable Long id: Captura o ID do produto a ser deletado da URL.
• [Link](id): Chama o método delete do ProductService, que executa a operação
de exclusão.
• ResponseEntity: O tipo de retorno Void indica que o corpo da resposta não conterá
nenhum conteúdo, pois não há necessidade de retornar dados ao cliente após a exclusão.
• [Link]().build():
– Retorna uma resposta HTTP com status 204 (No Content), que é o código de
status recomendado para operações de deleção bem-sucedidas.
– Esse status indica que a requisição foi processada corretamente, mas não há conteúdo
na resposta.
73
• 403 Forbidden: O servidor entendeu a requisição, mas se recusa a autorizá-la.
• 404 Not Found: O recurso solicitado não foi encontrado mas poderá estar disponível
no futuro.
• 409 Conflict: Indica um conflito na requisição, como tentar criar um recurso duplicado.
• 415 Unsupported Media Type: O tipo de mídia dos dados requisitados não é supor-
tado pelo servidor.
• 422 Unprocessable Entity: A requisição está bem formada, mas foi impossível segui-la
devido a erros semânticos.
Esta classe estende RuntimeException, permitindo que ela seja lançada sem exigir tratamento
obrigatório (catch ou declaração throws).
@Service
public class ProductService {
...
Explicação do Código
• orElseThrow(): Este método é parte da classe Optional e é usado aqui para lançar
uma ResourceNotFoundException caso o método findById não encontre o produto com o
74
ID fornecido. A mensagem “Recurso não encontrado” ajuda a identificar claramente o
problema.
Testando a Implementação
Se o endpoint for testado neste momento com um ID que não existe, a API ainda retornará
uma resposta de erro 500. No entanto, se você verificar o stack trace de erros no console da
aplicação Spring Boot, poderá observar que a exceção ResourceNotFoundException está sendo
lançada corretamente.
Esta seção preparou o terreno para um sistema robusto de tratamento de exceções. Implementa-
mos uma exceção customizada para lidar com situações específicas de recursos não encontrados.
No próximo passo, vamos aprender como capturar essa exceção dentro do controlador ou com
um manipulador global de exceções para retornar respostas apropriadas e informativas aos
usuários da API.
// Getters
public Instant getTimestamp () {
return timestamp ;
75
}
@ControllerAdvice
public class ControllerExceptionHandler {
76
Explicação do Código
• @ControllerAdvice: Indica que esta classe é um conselheiro de controladores, capaz de
interceptar exceções lançadas por métodos anotados com @RequestMapping e similares.
• @ExceptionHandler([Link]): Especifica que o mé-
todo resourceNotFound deve ser invocado para tratar exceções do tipo ResourceNotFoundException
.
• ResponseEntity: Constrói a resposta para a exceção com o status HTTP apropriado e
o corpo contendo um objeto CustomError.
• CustomError Creation: Um novo objeto CustomError é criado com o timestamp atual,
o status HTTP, a mensagem de erro e o caminho da URI solicitada.
{
" timestamp ": "2023 -09 -29 T12 :34:56.789 Z",
" status ": 404 ,
" error ": " Recurso não encontrado ",
"path ": "/ products /{ id }"
}
Classe DatabaseException
A DatabaseException é utilizada para capturar e tratar exceções que ocorrem devido a operações
de banco de dados que falham por razões de integridade de dados ou restrições do banco.
77
public class DatabaseException extends RuntimeException {
Essa exceção é derivada de RuntimeException, permitindo que seja lançada sem exigências de
tratamento explícito (try-catch).
@ControllerAdvice
public class ControllerExceptionHandler {
78
Atualização dos Métodos em ProductService
Ampliamos a funcionalidade de ProductService para lidar com as exceções customizadas durante
as operações de CRUD.
@Service
public class ProductService {
...
@Transactional
public ProductDTO update (Long id , ProductDTO dto) {
try {
Product entity = repository . getReferenceById (id);
copyDtoToEntity (dto , entity );
entity = repository .save( entity );
return new ProductDTO ( entity );
}
catch ( EntityNotFoundException e) {
throw new ResourceNotFoundException (" Recurso não encontrado ");
}
}
...
}
Explicações Detalhadas
• @Transactional(propagation = [Link]): Esta configuração é usada no mé-
todo delete para indicar que o método não necessariamente precisa ser executado dentro
de uma transação, mas pode participar de uma se já existir.
• Tratamento de Erros:
– ResourceNotFoundException: Lançada quando um produto não é encontrado, seja na
busca por ID ou antes de uma exclusão.
79
– DatabaseException: Lançada em caso de violação de integridade, como tentar deletar
um produto que está vinculado a pedidos.
<dependency >
<groupId >org. springframework .boot </ groupId >
<artifactId >spring -boot -starter - validation </ artifactId >
</ dependency >
Ao salvar o arquivo [Link], a IDE (Spring Tools ou IntelliJ, por exemplo) reconhece a nova
dependência e automaticamente realiza o download das bibliotecas necessárias.
80
import com. devsuperior . dscommerce . entities . Product ;
import jakarta . validation . constraints . NotBlank ;
import jakarta . validation . constraints . Positive ;
import jakarta . validation . constraints .Size;
@Size(min = 10, message = " Descrição precisa ter no mínimo 10 caracteres ")
@NotBlank ( message = " Campo requerido ")
private String description ;
...
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
...
@PostMapping
public ResponseEntity < ProductDTO > insert ( @Valid @RequestBody ProductDTO dto)
{
dto = service . insert (dto);
URI uri = ServletUriComponentsBuilder . fromCurrentRequest ().path("/{id}")
. buildAndExpand (dto. getId ()). toUri ();
81
return ResponseEntity . created (uri).body(dto);
}
...
}
Testando a Validação
Se o projeto for testado agora, com as validações configuradas, qualquer violação das regras
de validação resultará em uma resposta com erro, mas as mensagens específicas dos erros de
validação ainda não serão apresentadas de forma clara. O tratamento e a customização das
mensagens de erro serão abordados na próxima seção, permitindo uma melhor comunicação dos
problemas aos consumidores da API.
82
public String getMessage () {
return message ;
}
}
Classe ValidationError
A ValidationError é uma extensão da classe CustomError, que além das informações básicas de
erro, inclui uma lista de FieldMessage, permitindo representar múltiplos erros de validação que
podem ocorrer durante o processamento de uma única requisição.
private List < FieldMessage > errors = new ArrayList < >();
83
import org. springframework .http. ResponseEntity ;
import org. springframework . validation . FieldError ;
import org. springframework .[Link]. MethodArgumentNotValidException ;
import org. springframework .[Link]. annotation . ControllerAdvice ;
import org. springframework .[Link]. annotation . ExceptionHandler ;
@ControllerAdvice
public class ControllerExceptionHandler {
...
Explicação do Código
• @ExceptionHandler([Link]): Esta anota-
ção define que o método methodArgumentNotValidation deve ser chamado quando uma
exceção do tipo MethodArgumentNotValidException ocorrer. Essa exceção é comumente
lançada pelo Spring quando a validação de um objeto falha no nível do argumento de
método anotado com @Valid.
• HttpStatus.UNPROCESSABLE_ENTITY (422): Utilizamos este status HTTP
para indicar que a requisição foi bem formada mas não pôde ser seguida devido a erros
semânticos, ou seja, dados inválidos fornecidos pelo cliente.
• Percorrendo os Erros de Campo: Usamos [Link]().getFieldErrors()
para obter uma lista de FieldError, onde cada FieldError representa um erro específico
de validação que ocorreu. Estes são adicionados à resposta de erro através do método
addError.
Testando as Validações
Encorajamos o leitor a testar os vários cenários possíveis que podem gerar erros de validação,
utilizando a classe ProductDTO que definimos anteriormente. Isso inclui:
• Enviar um name com menos de 3 caracteres ou mais de 80.
• Enviar uma description com menos de 10 caracteres.
• Definir um price negativo.
84
Cada um desses testes deve resultar em uma resposta com código 422 (Unprocessable Entity),
contendo detalhes específicos sobre cada erro de validação encontrado. Essa abordagem me-
lhora significativamente a usabilidade e a robustez da API, fornecendo aos desenvolvedores
informações precisas para corrigir os dados enviados.
85
Capítulo 4
86
4.3 Sessão JPA e Estados de Entidades
Gerenciamento de Entidades com JPA
Java Persistence API (JPA) é uma especificação padrão que gerencia o relacionamento entre
objetos Java e dados de um banco de dados relacional. A JPA facilita o gerenciamento de
entidades em uma aplicação Java, abstraindo muitos dos detalhes complexos associados ao
acesso direto ao banco de dados.
Sessão JPA
Uma sessão JPA é o contexto durante o qual a JPA gerencia as entidades enquanto interage
com o banco de dados. Esse gerenciamento ocorre através de uma unidade de persistência
chamada EntityManager, que é responsável por gerenciar o ciclo de vida das entidades, realizar
operações de banco de dados e manter o contexto de persistência.
• EntityManager: Este objeto central na JPA encapsula uma conexão com o banco de
dados e mantém o estado das entidades que estão sendo gerenciadas. As operações como
persistir, remover, ou buscar entidades são feitas através do EntityManager.
Neste exemplo, um EntityManager é criado e usado para gerenciar uma transação onde um novo
produto é persistido no banco de dados.
@Autowired
private ProductRepository repository ;
@Transactional
public void meuMetodo () {
Product prod = new ...
repository .save(prod);
}
87
Aqui, o ProductRepository abstrai a complexidade do gerenciamento direto do EntityManager,
utilizando anotações como @Transactional para controlar as transações.
{
"name ": "Nova Pessoa ",
" salary ": 8000.0 ,
" department ": {
"id": 1
88
}
}
{
"name ": "Nova Pessoa ",
" salary ": 8000.0 ,
" departmentId ": 1
}
Para isto, vamos utilizar um projeto Spring Boot iniciado, o qual está disponível no link a seguir.
Caso queira seguir o passo a passo desta seção, por favor abra o projeto no seu computador:
[Link]
Observe que no projeto temos duas entidades Person e Department, onde uma pessoa está asso-
ciada com um departamento.
Classe Department
@Entity
@Table (name = " tb_department ")
public class Department {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
public Department () {
}
89
public void setId (Long id) {
[Link] = id;
}
Classe Person
@Entity
@Table (name = " tb_person ")
public class Person {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
private Double salary ;
@ManyToOne
@JoinColumn (name = " department_id ")
private Department department ;
public Person () {
}
90
public String getName () {
return name;
}
91
public String getName () {
return name;
}
}
Classe PersonDepartmentDTO
92
Implementando DTO para pessoa com id do departamento
Para permitir o envio de um Json com os dados da pessoa e o id do departamento apenas,
precisamos criar um DTO correspondente.
Classe PersonDTO
Nas próximas seções vamos prosseguir com nosso estudo de caso, implementando os códigos
restantes para atingir nosso objetivo.
93
4.5 Salvando entidade associada para um PARTE 2
Dando prosseguimento ao nosso estudo de caso, agora vamos mostrar a implementação da
operação responsável por salvar uma pessoa e seu respectivo departamento, considerando o
caso em que informamos na requisição um objeto Json com os dados da pessoa e um objeto
aninhado para o departamento.
Classe PersonService
Vamos começar com a implementação da operação insert na classe PersonService. Caso seja
necessário, revise a implementação da classe PersonDepartmentDTO para compreender sua estru-
tura.
@Service
public class PersonService {
@Autowired
private PersonRepository repository ;
@Autowired
private DepartmentRepository departmentRepository ;
Aqui é importante notar que primeiro precisamos preparar os objetos Person e Department,
devidamente associados, antes de chamar o método save do objeto repository. Repare que
nós instanciamos um objeto Department usando a cláusula new, e depois definimos o id deste
94
departamento como sendo o id do objeto aninhado:
Classe PersonController
Para disponibilizar um endpoint para inserir uma pessoa e seu respectivo departamento, vamos
implementar uma classe controladora PersonController, de forma similar à que já aprendemos
anteriormente.
import [Link];
@RestController
@RequestMapping ( value = "/ people ")
public class PersonController {
@Autowired
private PersonService service ;
@PostMapping
public ResponseEntity < PersonDepartmentDTO > insert ( @RequestBody
PersonDepartmentDTO dto) {
dto = service . insert (dto);
URI uri = ServletUriComponentsBuilder . fromCurrentRequest ().path("/{id}")
. buildAndExpand (dto. getId ()). toUri ();
return ResponseEntity . created (uri).body(dto);
}
}
Testando a requisição
Para testar esta requisição, devemos preparar no Postman uma requisição POST no caminho
[Link] com o seguinte corpo Json:
{
"name ": "Nova Pessoa ",
95
" salary ": 8000.0 ,
" department ": {
"id": 1
}
}
Classe PersonService
Aqui temos o código do novo método na classe PersonService. Caso seja necessário, revise a
implementação da classe PersonDTO para compreender sua estrutura.
@Service
public class PersonService {
...
Novamente, aqui é importante notar que primeiro precisamos preparar os objetos Person e
Department, devidamente associados, antes de chamar o método save do objeto repository. Re-
pare que nós instanciamos um objeto Department usando a cláusula new, e depois definimos o id
deste departamento como sendo o id do departamento, representado pelo campo departmentId:
96
Classe PersonController
O método que implementa o endpoint é praticamente o mesmo anterior, com a diferença que
agora o parâmetro é do tipo PersonDTO.
import [Link];
@RestController
@RequestMapping ( value = "/ people ")
public class PersonController {
@Autowired
private PersonService service ;
@PostMapping
public ResponseEntity <PersonDTO > insert ( @RequestBody PersonDTO dto) {
dto = service . insert (dto);
URI uri = ServletUriComponentsBuilder . fromCurrentRequest ().path("/{id}")
. buildAndExpand (dto. getId ()). toUri ();
return ResponseEntity . created (uri).body(dto);
}
}
Testando a requisição
Para testar esta requisição, devemos preparar no Postman uma requisição POST no caminho
[Link] com o seguinte corpo Json:
{
"name ": "Nova Pessoa ",
" salary ": 8000.0 ,
" departmentId ": 1
}
97
4.7 Salvando entidades associadas para muitos
Outra operação muito comum que o desenvolvedor backend precisa dominar, é quando se deseja
salvar objetos associados por meio de um relacionamento “para muitos”.
Para compreender este tópico, vamos usar um estudo de caso de um sistema de produtos e
categorias. O objetivo aqui é salvar, em uma única requisição, um produto e suas respectivas
categorias, presumindo que as categorias já estejam previamente cadastradas.
No corpo da requisição web, vamos passar os dados do produto, juntamente com uma lista de
objetos com o id de cada categoria:
POST [Link]
{
"name ": " Produto novo",
"price ": 1000.0 ,
" categories ": [
{
"id ": 2
},
{
"id ": 3
}
]
}
Vamos utilizar um projeto Spring Boot iniciado, o qual está disponível no link a seguir. Caso
queira seguir o passo a passo desta seção, por favor abra o projeto no seu computador:
[Link]
Observe que no projeto temos duas entidades Product e Category, onde um produto está associado
a várias categorias.
Classe Product
@Entity
@Table (name = " tb_product ")
public class Product {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
private Double price ;
98
@ManyToMany
@JoinTable (name = " tb_product_category ",
joinColumns = @JoinColumn (name = " product_id "),
inverseJoinColumns = @JoinColumn (name = " category_id "))
private Set <Category > categories = new HashSet < >();
public Product () {
}
Classe Category
@Entity
@Table (name = " tb_category ")
public class Category {
99
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
public Category () {
}
Classes DTO
Para representar os dados que serão trafegados na requisição, vamos criar um tipo CategoryDTO
e ProductDTO, de modo que cada objeto tipo ProductDTO esteja associado com vários objetos tipo
CategoryDTO. Vamos já criar também o construtor de ProductDTO para instanciar um objeto a
partir de uma entidade tipo Product.
Classe CategoryDTO
100
private String name;
public CategoryDTO () {
}
Classe ProductDTO
private List < CategoryDTO > categories = new ArrayList < >();
public ProductDTO () {
}
101
this. price = price ;
}
Classe ProductService
Agora que já temos a representação DTO de um produto associado com suas categorias, vamos
implementar a classe ProductService, que será responsável por preparar os objetos das entidades
Product e Category, devidamente associados, para que depois sejam salvos no banco de dados.
102
import com. devsuperior .aula. entities . Category ;
import com. devsuperior .aula. entities . Product ;
import com. devsuperior .aula. repositories . ProductRepository ;
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
Classe ProductController
Para disponibilizar um endpoint em nossa API, vamos implementar a classe ProductController.
import [Link];
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
103
@PostMapping
public ResponseEntity < ProductDTO > insert ( @RequestBody ProductDTO dto) {
dto = service . insert (dto);
URI uri = ServletUriComponentsBuilder . fromCurrentRequest ().path("/{id}")
. buildAndExpand (dto. getId ()). toUri ();
return ResponseEntity . created (uri).body(dto);
}
}
Testando a requisição
Para testar esta requisição, devemos preparar no Postman uma requisição POST no caminho
[Link] com o seguinte corpo Json:
{
"name ": " Produto novo",
"price ": 1000.0 ,
" categories ": [
{
"id ": 2
},
{
"id ": 3
}
]
}
Projeto Exemplo
Para demonstrar as práticas de otimização de performance na prática, disponibilizamos um
projeto exemplo que será explorado nas próximas seções. Este projeto ilustra diversos cenários
de uso da JPA e apresenta soluções para os problemas comuns de performance relacionados ao
carregamento lazy. O projeto está disponível no GitHub e pode ser baixado a partir do seguinte
link:
104
[Link]
105
• Cláusula JOIN FETCH: Utilizada em consultas JPQL para especificar que entida-
des associadas devem ser carregadas juntamente com a entidade principal para evitar
consultas subsequentes:
Esta abordagem é eficaz para evitar o problema de N+1 em casos específicos onde sabemos
que vamos precisar das entidades associadas imediatamente após a carga da entidade
principal.
• Consultas Customizadas: Criar consultas específicas que buscam exatamente o que
é necessário pode ser a melhor solução, pois permite um controle total sobre o que é
carregado e quando. Utilizar o JPQL ou o Criteria API para definir essas consultas
garante que apenas os dados necessários sejam recuperados, otimizando a performance.
A compreensão profunda do carregamento EAGER e LAZY e de como manipular esses com-
portamentos é crucial para desenvolver aplicações eficientes com JPA. A escolha entre EAGER
e LAZY deve ser feita com base nas necessidades específicas de cada caso de uso.
@Service
public class DepartmentService {
@Autowired
private DepartmentRepository repository ;
106
Passo 1: Busca do Departamento
Inicialmente, o método findById do DepartmentRepository é chamado para buscar o departamento
com o ID fornecido:
• Consulta ao Banco de Dados: Uma consulta SQL é executada para buscar o de-
partamento especificado pelo ID. Neste ponto, apenas os dados do departamento são
recuperados, não incluindo os empregados associados, assumindo que o relacionamento
entre Department e Employee está configurado como lazy.
Considerações de Performance
• Dupla Consulta: Este método ilustra uma situação típica onde o carregamento lazy
pode resultar em múltiplas consultas ao banco de dados, conhecido como o problema
N+1. Cada acesso a uma lista de entidades lazy dentro de um loop ou operação similar
pode resultar em uma nova consulta ao banco.
• Otimização: Para otimizar esse comportamento, poderíamos considerar o uso de técni-
cas como JOIN FETCH em uma consulta JPQL customizada ou Entity Graphs para carregar
os empregados simultaneamente com o departamento, se o cenário de uso exigir frequen-
temente ambos os dados.
107
import [Link]. ArrayList ;
import [Link];
@Entity
@Table (name = " tb_department ")
public class Department {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
public Long id;
public String name;
public Department () {
}
108
Implicações do Carregamento Eager
1. Carregamento Imediato: Com [Link], a JPA carrega todas as entidades
associadas (Employee neste caso) imediatamente junto com a entidade Department, inde-
pendentemente de se elas serão usadas ou não.
2. Consumo de Recursos: Esse carregamento imediato pode resultar em um uso signifi-
cativo de recursos de memória e processamento, especialmente se o número de Employee
associados for grande ou se as consultas forem frequentes e não necessitarem dos dados
dos empregados.
3. Impacto no Desempenho: Em sistemas com muitos dados ou em operações que en-
volvem muitos departamentos, o carregamento eager pode levar a uma degradação per-
ceptível no desempenho devido ao grande número de dados carregados em cada consulta.
Além disso, pode ocorrer o problema de N+1 mesmo com eager fetching se o mapeamento
não for feito cuidadosamente.
4. Comportamento da Aplicação: Alterar para eager fetching pode mudar o compor-
tamento da aplicação de maneiras não previstas, especialmente em sistemas complexos
onde as interações entre entidades são intrincadas e diversas partes do sistema dependem
dessas interações de maneiras específicas.
A decisão entre usar [Link] ou [Link] deve ser baseada numa compreensão
detalhada das necessidades específicas de acesso a dados da aplicação. Em muitos casos, é
preferível manter o carregamento lazy padrão e utilizar técnicas como JOIN FETCH em consultas
específicas onde a carga imediata de entidades associadas é necessária. Isso oferece um equi-
líbrio entre desempenho e disponibilidade de dados, permitindo otimizações mais granulares e
contextualizadas conforme a necessidade.
@Entity
@Table (name = " tb_employee ")
public class Employee {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
public Long id;
public String name;
public String email ;
109
@ManyToOne
@JoinColumn (name = " department_id ")
public Department department ;
...
}
Para buscar funcionários junto com seus respectivos departamentos, podemos definir um mé-
todo no repositório EmployeeRepository que utiliza a cláusula JOIN FETCH. Isso garante que o JPA
carregue os funcionários e seus departamentos em uma única consulta, reduzindo o overhead
de múltiplas idas e vindas ao banco de dados.
import [Link];
@Query (" SELECT obj FROM Employee obj JOIN FETCH obj. department ")
List <Employee > findEmployeesWithDepartments ();
}
Benefícios
• Redução de Latência: Menos consultas ao banco, pois dados associados são carregados
na mesma consulta da entidade principal.
• Prevenção do Problema N+1: Evita múltiplas consultas que normalmente ocorrem
quando acessamos entidades relacionadas configuradas com fetch tipo lazy.
Vale ressaltar que, embora JOIN FETCH seja muito útil, ele tem limitações, especialmente quando
se trata de consultas paginadas, onde ele não se comporta corretamente, necessitando outras
abordagens para recuperar dados associados.
110
4.13 Entendendo Transactional e Open-In-View
Annotation @Transactional do Spring
A anotação @Transactional do Spring Framework é uma ferramenta poderosa para gerenciar
transações com o banco de dados de maneira declarativa. Ela pode ser aplicada em métodos
ou classes inteiras para definir o comportamento de transação de operações de banco de dados.
Funcionalidades da @Transactional
• Gerenciamento de Transações: A anotação @Transactional automaticamente cria e
gerencia transações de banco de dados, garantindo que as operações realizadas dentro
do método anotado sejam completadas com sucesso ou, em caso de falha, revertidas
(rollback). Isso simplifica significativamente o desenvolvimento, pois o desenvolvedor não
precisa manualmente controlar a abertura, commit ou rollback de transações.
• Resolução de Dependências Lazy: Ao utilizar esta anotação, qualquer acesso a da-
dos que necessite de carregamento lazy será resolvido dentro do escopo da transação.
Isso significa que, enquanto a transação está ativa, todas as pendências de carregamento
lazy com o banco de dados serão atendidas sem problemas, evitando exceções como
LazyInitializationException, que ocorre quando se tenta acessar dados lazy fora do con-
texto de uma transação ativa.
Exemplo de Uso
@Transactional
public void updateProduct ( Product product ) {
repository .save( product );
}
Neste exemplo, o método updateProduct é envolto em uma transação. Se ocorrer alguma exceção
durante o save, todas as modificações feitas no banco de dados serão revertidas.
Propriedade Open-In-View
A propriedade open-in-view está relacionada ao padrão Open Session in View, que é comumente
utilizado em aplicações Hibernate/JPA. Esta propriedade controla se a sessão do Hibernate
(ou sessão JPA) fica aberta durante toda a requisição HTTP, incluindo a renderização da view
(camada de apresentação).
Configuração e Implicações
• [Link]-in-view=false: Configurar esta propriedade como false significa que
a sessão JPA será fechada antes de a execução retornar à camada de apresentação (con-
troller). Isso ajuda a evitar problemas de performance e uso de recursos associados à
manutenção de sessões abertas por mais tempo que o necessário. No entanto, também
significa que qualquer acesso a dados que requer carregamento lazy não será possível após
a conclusão do método de serviço, a menos que a transação ainda esteja ativa.
111
Problemas Associados ao Open-In-View
Quando open-in-view está habilitado (o padrão em muitas configurações do Spring Boot é true),
a sessão JPA permanece aberta durante toda a requisição, o que pode levar a:
• Problemas de Performance: Manter a sessão aberta pode resultar em uma maior
utilização de recursos, pois a sessão pode ficar retida por mais tempo que o necessário.
• Riscos de Segurança: A extensão da sessão aumenta o risco de ataques, como aqueles
que tentam manipular a sessão persistente.
Exemplo Básico
Consideremos uma entidade Product com campos name e category. Podemos definir um repo-
sitório com métodos que busquem produtos baseados em diferentes critérios usando apenas a
assinatura do método:
Neste exemplo, o Spring Data JPA geraria consultas para buscar produtos por name ou pela
automaticamente, sem a necessidade de escrever qualquer SQL ou JPQL.
categoryName
112
• Manutenção: Facilita a manutenção do código, pois as alterações nas entidades ou nos
requisitos de consulta podem frequentemente ser geridas apenas alterando os nomes dos
métodos.
• Segurança: Reduz o risco de injeção de SQL, pois as consultas são construídas automa-
ticamente pelo framework usando parâmetros de consulta seguros.
Limitações e Considerações
Apesar de sua utilidade, os Query Methods têm limitações:
• Complexidade: Para consultas extremamente complexas, os nomes dos métodos podem
se tornar muito longos e menos intuitivos.
• Customização: Em alguns casos, pode ser necessário maior controle sobre a consulta
do que o que pode ser alcançado através de um Query Method. Para esses casos, o
Spring Data JPA permite o uso de anotações @Query para definir consultas JPQL ou SQL
customizadas diretamente.
Propósito da JPQL
A principal vantagem da JPQL é sua capacidade de abstrair a complexidade das operações de
banco de dados, permitindo que desenvolvedores escrevam consultas de maneira mais natural
e alinhada com o modelo de domínio da aplicação. Com JPQL, é possível realizar operações
de seleção, inserção, atualização e exclusão utilizando a sintaxe familiar do SQL, mas de uma
maneira que respeite a orientação a objetos do código Java.
Características da JPQL
• Independência de Plataforma: Uma das maiores vantagens da JPQL é sua inde-
pendência de banco de dados específico. Isso significa que o mesmo código JPQL pode
ser executado em diferentes bancos de dados sem modificação, desde que o JPA esteja
corretamente configurado.
• Integração com JPA: JPQL é parte integrante da especificação JPA e trabalha em
conjunto com o EntityManager, que gerencia o ciclo de vida das entidades JPA. Isso pro-
porciona uma integração profunda e eficiente entre as consultas e as operações de persis-
tência.
• Orientação a Objetos: Ao contrário do SQL tradicional, JPQL permite realizar con-
sultas considerando a estrutura das classes e dos objetos. Por exemplo, é possível realizar
uma consulta sobre objetos de uma classe com herança, tratando-as através de suas es-
pecificidades de classe.
113
Exemplo de Uso de JPQL
Consideremos a seguinte consulta JPQL que busca todos os produtos de uma determinada
categoria:
String jpql = " SELECT p FROM Product p WHERE p. category .name = : categoryName ";
Query query = entityManager . createQuery (jpql);
query. setParameter (" categoryName ", " Electronics ");
List <Product > products = query . getResultList ();
Neste exemplo, Product e Category são classes de entidade, e a consulta é escrita em termos
dessas classes e de suas propriedades, como [Link].
Vantagens da JPQL
1. Simplicidade em Alguns Casos: JPQL pode simplificar a escrita de consultas em
cenários onde a lógica de negócios se alinha estreitamente com o modelo de domínio,
permitindo consultas que são mais legíveis e diretas comparadas ao SQL tradicional.
2. Integração com Spring Data JPA: JPQL se beneficia plenamente das capacidades
do Spring Data JPA, como suporte automático para paginação e a habilidade de retornar
diretamente objetos dentro do contexto de persistência do Spring. Isso facilita o desen-
volvimento, reduzindo a quantidade de código boilerplate necessário para implementar
funcionalidades comuns de aplicativos de dados.
3. Entidades Gerenciadas: Os objetos resultantes de uma consulta JPQL são automa-
ticamente entidades gerenciadas pela JPA. Isso significa que qualquer alteração nessas
entidades pode ser automaticamente persistida no banco de dados ao final da transação,
simplificando a manipulação de dados.
114
Desvantagens da JPQL
1. Complexidade em Consultas Avançadas: Para consultas particularmente comple-
xas, a JPQL pode ser mais desafiadora para escrever e validar. A natureza orientada
a objetos da JPQL pode tornar difícil expressar algumas consultas que são trivialmente
implementadas em SQL puro, especialmente aquelas que requerem operações avançadas
de junção ou subconsultas complexas.
2. Ausência de Operações de União: JPQL não suporta operações de união (UNION), o
que pode ser um grande limitador para certos tipos de consultas que necessitam combinar
resultados de múltiplas tabelas de formas não suportadas por JOINs.
3. Curva de Aprendizado: Aprender JPQL pode exigir um investimento significativo em
tempo e recursos, especialmente para desenvolvedores que já estão confortáveis com SQL.
Além disso, especializar-se em JPQL pode não ser tão benéfico se o ambiente de trabalho
demandar flexibilidade entre diferentes tecnologias de banco de dados ou se a aplicação
fizer uso intensivo de funcionalidades SQL que estão fora do escopo da JPA.
115
• Grupo 1: projeção, restrição: exercícios 2602, 2603, 2604, 2607, 2608, 2615, 2624
• Grupo 2: JOIN: exercícios 2605, 2606, 2611, 2613, 2614, 2617, 2618, 2619, 2620, 2621,
2622, 2623, 2742
• Grupo 3: GROUP BY, subconsultas: exercícios 2609, 2993, 2994, 2995, 2996
• Grupo 4: Expressões na projeção: exercícios 2610, 2625, 2738, 2739, 2741, 2743,
2744, 2745, 2746, 3001
• Grupo 5: Diferença, União: exercícios 2616, 2737, 2740, 2990
• Grupo 6: Difíceis: exercícios 2988, 2989, 2991, 2992, 2997, 2998, 2999
Preparação do Postgresql
Os exercícios sobre SQL e JPQL que vamos apresentar nas próximas seções serão para o banco
de dados relacional Postgresql, que inclusive é o banco de dados padrão para os exercícios
da plataforma Beecrowd. Sendo assim, você precisará instalar o Postgresql, bem como uma
ferramenta para acessar e fazer consultas ao banco de dados. Sugerimos duas opções:
• Opção 1 - instalação direta
– Instale o Postgresql (versão 12 ou 13 ou 14)
– Instale o pgAdmin ou o DBeaver
• Opção 2 - Instalação via Docker Compose
– Se você tem experiência com Docker, pode instalar o Postgresql e o pgAdmin usando
Docker Compose. Um exemplo de script pode ser obtido no link:
[Link]
116
Estrutura da Base de Dados
Para abordar essa tarefa, trabalharemos com a tabela customers, que inclui informações deta-
lhadas sobre cada cliente. Veja o script de criação da tabela:
A tabela contém várias colunas que descrevem o cliente, mas estamos particularmente interes-
sados na coluna state para este exercício.
Dados Inseridos
Os dados dos clientes são inseridos da seguinte forma:
SELECT name
FROM customers
WHERE state = 'RS '
Explicação da Consulta
• SELECT name: Esta parte da consulta especifica que queremos retornar apenas o nome dos
clientes.
• FROM customers: Especifica que a consulta será realizada sobre a tabela customers.
• WHERE state = 'RS': Filtra os registros para incluir somente aqueles onde a coluna state
é igual a ‘RS’, que é o critério dado pelo exercício.
117
4.19 Baixando os projetos iniciados dos estudos de caso
Esta seção é para avisar que neste momento você deve estar com os projetos Spring Boot dos
estudos de casos iniciados. Caso não tenha baixado os projetos ainda, eles estão no repositório
Github a seguir:
[Link]
Você precisa também estar com o banco de dados Postgresql instalado no seu computador.
Essas configurações especificam como o Spring Boot deve conectar-se ao PostgreSQL e como
o Hibernate deve se comportar em relação ao esquema do banco de dados e à formatação das
consultas SQL.
@Entity
118
@Table (name = " customers ")
public class Customer {
@Id
private Long id;
private String name;
private String street ;
private String city;
private String state ;
private Double creditLimit ;
...
}
public CustomerMinDTO () {
}
String getName () {
return name;
}
119
public void setName ( String name) {
[Link] = name;
}
@Override
public String toString () {
return " CustomerMinDTO [name=" + name + "]";
}
}
Implementação do Repositório
O repositório CustomerRepository inclui uma consulta SQL nativa que busca nomes de clientes
no estado especificado:
import [Link];
Teste da Consulta
Finalmente, a classe principal Uri2602Application executa a consulta ao iniciar a aplicação,
imprimindo os resultados:
@SpringBootApplication
public class Uri2602Application implements CommandLineRunner {
@Autowired
private CustomerRepository repository ;
@Override
public void run( String ... args) throws Exception {
120
List < CustomerMinProjection > list = repository . search1 ("RS");
List < CustomerMinDTO > result1 = list. stream ().map(x -> new CustomerMinDTO
(x)). collect ( Collectors . toList ());
@Query (" SELECT new com. devsuperior . uri2602 .dto. CustomerMinDTO ([Link]) "
+ "FROM Customer obj "
+ " WHERE UPPER (obj. state ) = UPPER (: state )")
List < CustomerMinDTO > search2 ( String state );
Explicação do Código
• @Query Annotation: Esta anotação define a consulta JPQL que será executada quando o
método search2 for chamado. O uso de JPQL permite integrar mais naturalmente com o
modelo de objetos da aplicação, diferentemente do SQL nativo que trabalha diretamente
com as tabelas e colunas do banco de dados.
• Construtor de DTO: new [Link]([Link]) cria
uma nova instância de CustomerMinDTO diretamente na consulta. Isso é uma característica
poderosa do JPQL, permitindo que a transformação de entidades em DTOs seja feita
diretamente na camada de persistência.
• Filtro de Estado: A cláusula WHERE filtra os clientes pelo estado, usando a função UPPER
para garantir que a comparação seja insensível a maiúsculas e minúsculas. A variável
:state é um parâmetro que será passado ao método search2 em tempo de execução.
121
@SpringBootApplication
public class Uri2602Application implements CommandLineRunner {
...
@Override
public void run( String ... args) throws Exception {
...
Criação de Tabelas
122
id_genres numeric REFERENCES genres (id)
);
Inserção de Dados
Explicação da Consulta
• Seleção de Colunas: SELECT [Link], [Link] indica que queremos retornar o
identificador (id) e o nome (name) dos filmes.
• Fonte de Dados: FROM movies especifica que a tabela principal para a consulta é movies.
• Junção com Genres: INNER JOIN genres ON movies.id_genres = [Link] cria um vín-
culo entre as tabelas movies e genres com base nos identificadores de gênero. Isso permite
acessar informações de ambas as tabelas na mesma consulta.
• Filtro de Gênero: WHERE [Link] = 'Action' restringe os resultados aos filmes
cuja descrição do gênero é ‘Action’. Este filtro é crucial para satisfazer o requisito de listar
apenas filmes de ação.
123
4.23 URI 2611 Spring Boot SQL e JPQL
Avançando no exercício URI 2611, vamos implementar as consultas SQL e JPQL no projeto
Spring Boot. Para isso, abra o projeto “uri2611” fornecido e prepare-se para integrar o código
com uma nova base de dados chamada “uri2611” criada no PostgreSQL.
Modelo de Domínio
O projeto Spring Boot já está equipado com o modelo de domínio correspondente ao modelo
relacional do banco de dados:
@Entity
@Table (name = " movies ")
public class Movie {
@Id
private Long id;
private String name;
@ManyToOne
@JoinColumn (name = " id_genres ")
private Genre genre ;
...
@Entity
@Table (name = " genres ")
public class Genre {
@Id
private Long id;
private String description ;
Projections
Implementamos uma projeção para representar o resultado da consulta SQL de forma simpli-
ficada:
124
DTO
Implementamos também um DTO para resultado de consultas:
public MovieMinDTO () {
}
@Override
public String toString () {
return " MovieMinDTO [id=" + id + ", name=" + name + "]";
}
}
125
• search2: Método que emprega JPQL para obter o mesmo resultado, porém utilizando a
abordagem orientada a objetos do JPA.
import [Link];
@Query ( nativeQuery = true , value = " SELECT movies .id , movies .name "
+ "FROM movies "
+ "INNER JOIN genres ON movies . id_genres = genres .id "
+ "WHERE genres . description = : genreName ")
List < MovieMinProjection > search1 ( String genreName );
@Query (" SELECT new com. devsuperior . uri2611 .dto. MovieMinDTO ([Link] , [Link])
"
+ "FROM Movie obj "
+ "WHERE obj. genre . description = : genreName ")
List < MovieMinDTO > search2 ( String genreName );
}
Testando as Consultas
A classe principal Uri2611Application é configurada para testar ambas as consultas ao iniciar a
aplicação.
• Executamos ambas as consultas para o gênero “Action”.
• Os resultados são mapeados para MovieMinDTO e impressos para verificar a consistência
entre as abordagens SQL e JPQL.
@SpringBootApplication
public class Uri2611Application implements CommandLineRunner {
@Autowired
private MovieRepository repository ;
@Override
public void run( String ... args) throws Exception {
List < MovieMinProjection > list = repository . search1 (" Action ");
List < MovieMinDTO > result1 = list. stream ().map(x -> new MovieMinDTO (x)).
126
collect ( Collectors . toList ());
List < MovieMinDTO > result2 = repository . search2 (" Action ");
127
(4, 'Elon Electro ', 'Rua Apolo ', 'São Paulo ', 'SP '),
(5, 'Mike Electro ', 'Rua Pedro da Cunha ', 'Curitiba ', 'PR ');
Saída Esperada
Com base nos dados fornecidos, a saída esperada da consulta SQL seria:
name
Executive Chair
Solução do Exercício
A consulta SQL para resolver o exercício é:
128
Modelo de Domínio
O projeto já contém o modelo de domínio correspondente ao esquema relacional, que é crucial
para a persistência dos dados:
Entidade Product:
@Entity
@Table (name = " products ")
public class Product {
@Id
private Long id;
private String name;
private Integer amount ;
private Double price ;
@ManyToOne
@JoinColumn (name = " id_providers ")
private Provider provider ;
Entidade Provider:
@Entity
@Table (name = " providers ")
public class Provider {
@Id
private Long id;
private String name;
private String street ;
private String city;
private String state ;
Projections
A projeção ProductMinProjection é criada para simplificar o resultado das consultas, focando
apenas no nome do produto:
129
String getName ();
}
DTO
A classe ProductMinDTO é responsável por encapsular os dados do resultado em um objeto de
transferência de dados:
public ProductMinDTO () {
}
@Override
public String toString () {
return " ProductMinDTO [name=" + name + "]";
}
}
import [Link];
130
import org. springframework .[Link]. repository . Query ;
@Query (" SELECT new com. devsuperior . uri2621 .dto. ProductMinDTO ([Link]) "
+ "FROM Product obj "
+ "WHERE obj. amount BETWEEN :min AND :max "
+ "AND obj. provider .name LIKE CONCAT (: beginName , '%')")
List < ProductMinDTO > search2 ( Integer min , Integer max , String beginName );
}
Método search1: - Tipo de Consulta: SQL Nativo. Este método executa uma consulta SQL
diretamente no banco de dados usando a anotação @Query com o atributo nativeQuery = true.
- Consulta Realizada: A consulta faz um INNER JOIN entre as tabelas products e providers
baseado na relação entre products.id_providers e [Link]. - Filtro de Dados: Filtra
produtos cuja quantidade está entre os valores especificados por :min e :max e cujo nome do
fornecedor começa com uma letra ou mais especificada por :beginName. O CONCAT(:beginName,
'%') constrói uma string que é usada no operador LIKE para filtrar os nomes dos fornecedores.
- Projeção de Resultados: Retorna uma lista de ProductMinProjection, que são projeções
focadas apenas no nome do produto.
Método search2: - Tipo de Consulta: JPQL. Este método usa JPQL para formular a
consulta, permitindo trabalhar de forma mais alinhada ao modelo de domínio do projeto. -
Consulta Realizada: A consulta acessa a entidade Product e faz referência ao seu relacio-
namento com Provider diretamente pelo objeto, [Link]. - Filtro de Dados: Similar ao
método search1, porém utilizando a sintaxe JPQL que se alinha melhor com o gerenciamento
de entidades do JPA. Filtros são aplicados diretamente sobre os atributos das entidades. -
Criação de DTO: Utiliza o construtor da classe ProductMinDTO para criar novos objetos DTO
diretamente na consulta. Este método é típico de JPQL e permite a construção eficiente de
objetos de transferência de dados sem necessidade de conversões adicionais no código Java.
@SpringBootApplication
public class Uri2621Application implements CommandLineRunner {
@Autowired
131
private ProductRepository repository ;
@Override
public void run( String ... args) throws Exception {
List < ProductMinProjection > list = repository . search1 (10 , 20, "P");
List < ProductMinDTO > result1 = list. stream ().map(x -> new ProductMinDTO (x
)). collect ( Collectors . toList ());
132
CREATE TABLE categories (
id numeric PRIMARY KEY ,
name varchar
);
Saída Esperada
Com base nos dados inseridos na base de dados, a saída esperada para a consulta, em formato
de tabela, é a seguinte:
name sum
luxury 350
modern 13000
wood 850
vintage 1000
Solução do Exercício
A consulta SQL que resolve este exercício é:
133
Explicação Detalhada da Consulta
• SELECT [Link], SUM([Link]):
– Esta linha seleciona duas colunas: name da tabela categories e a soma (SUM()) dos
valores da coluna amount da tabela products. SUM([Link]) calcula o total de
produtos para cada categoria.
• FROM categories:
– Especifica que a tabela principal para a consulta é categories.
• INNER JOIN products ON products.id_categories = [Link]:
– Este comando realiza uma junção interna (INNER JOIN) entre as tabelas categories
e products. A junção é feita onde o id das categorias corresponde ao id_categories
dos produtos.
• GROUP BY [Link]:
– Agrupa os resultados pelo nome da categoria. Isso é necessário porque a função
SUM() é uma função de agregação que combina várias linhas de entrada em grupos
de resumo, neste caso, baseados nos nomes das categorias.
Modelo de Domínio
O projeto Spring Boot contém o modelo de domínio que corresponde ao modelo relacional do
banco de dados:
@Entity
@Table (name = " categories ")
public class Category {
@Id
private Long id;
private String name;
...
@Entity
@Table (name = " products ")
public class Product {
@Id
private Long id;
private String name;
private Integer amount ;
134
private BigDecimal price ;
@ManyToOne
@JoinColumn (name = " id_categories ")
private Category category ;
...
Projections
Foi necessário implementar uma projection para representar o resultado da consulta SQL:
DTO
Foi implementado também um DTO para representar os dados resultantes:
public CategorySumDTO () {
}
135
[Link] = name;
}
@Override
public String toString () {
return " CategorySumDTO [name=" + name + ", sum=" + sum + "]";
}
}
import [Link];
@Query ( nativeQuery = true , value = " SELECT categories .name , SUM( products .
amount ) "
+ "FROM categories "
+ "INNER JOIN products ON products . id_categories = categories .id "
+ "GROUP BY categories .name")
List < CategorySumProjection > search1 ();
@Query (" SELECT new com. devsuperior . uri2609 .dto. CategorySumDTO (obj. category .
name , SUM(obj. amount )) "
+ "FROM Product obj "
+ "GROUP BY obj. category .name")
List < CategorySumDTO > search2 ();
}
• search1(): Este método utiliza uma consulta SQL nativa para somar a quantidade de
produtos por categoria. O resultado é agrupado pelo nome da categoria.
• search2(): Este método utiliza JPQL para realizar a mesma tarefa, criando instâncias de
136
CategorySumDTOdiretamente na consulta, o que pode melhorar a performance ao evitar o
carregamento completo dos objetos de domínio.
Testando a Solução
O código abaixo foi implementado na classe principal do projeto Spring Boot para testar as
consultas:
import [Link];
import [Link]. stream . Collectors ;
@SpringBootApplication
public class Uri2609Application implements CommandLineRunner {
@Autowired
private CategoryRepository repository ;
@Override
public void run( String ... args) throws Exception {
137
4.29 URI 2737 Elaborando a consulta
Enunciado do Exercício
O diretor da Mangojata Advogados ordenou que lhe fosse entregue um relatório sobre seus
advogados atuais.
O diretor quer que você mostre para ele o nome do advogado que têm mais clientes, o nome do
advogado que tem menos clientes e a média de clientes entre todos os advogados.
OBS: Antes de apresentar a média mostre um campo chamado Average a fim de deixar o
relatório mais apresentável. A média deverá ser apresentada em inteiros.
Saída Esperada
A saída esperada para a base de dados apresentada é a seguinte:
name customers_number
Chelsey D. Sanders 20
Marty M. Harrison 5
Average 12
Solução do Exercício
A consulta SQL que soluciona o exercício é a seguinte:
138
FROM lawyers
))
UNION ALL
UNION ALL
UNION ALL
139
( SELECT name , customers_number
FROM lawyers
ORDER BY customers_number ASC
LIMIT 1)
UNION ALL
1. Primeira Subconsulta:
• SELECT name, customers_number FROM lawyers ORDER BY customers_number DESC LIMIT 1
• Esta subconsulta seleciona o advogado com o maior número de clientes. Ela ordena
todos os advogados pelo número de clientes em ordem decrescente (DESC) e limita o
resultado ao primeiro registro (LIMIT 1), ou seja, o advogado com o maior número
de clientes.
2. Segunda Subconsulta:
• SELECT name, customers_number FROM lawyers ORDER BY customers_number ASC LIMIT 1
• De forma similar à primeira subconsulta, mas em contraste direto, esta consulta
ordena os advogados pelo número de clientes em ordem ascendente (ASC) e seleciona
o primeiro na lista, que será o advogado com o menor número de clientes.
3. Terceira Subconsulta:
• SELECT 'Average', ROUND(AVG(customers_number), 0) FROM lawyers
• Esta parte da consulta calcula a média do número de clientes entre todos os advoga-
dos, utilizando a função AVG(). A função ROUND() é utilizada para arredondar o valor
médio para o número inteiro mais próximo. O uso de 'Average' como uma string
literal serve para etiquetar o resultado, tornando o relatório mais claro e legível.
Essas subconsultas são novamente unidas usando UNION ALL, que permite combinar os resultados
de múltiplas consultas em um único conjunto de resultados, mantendo todas as linhas de cada
subconsulta, incluindo duplicatas. Esta abordagem alternativa, ao contrário da primeira que
se baseia em funções agregadas condicionais, emprega ordenação e limitação para obter os
registros desejados, que pode ser mais direta e intuitiva em certos contextos de banco de dados,
especialmente em casos onde o desempenho não é crítico ou as tabelas não são excessivamente
grandes.
Modelo de Domínio
O projeto Spring Boot já possui o modelo de domínio implementado, que reflete o modelo
relacional da base de dados:
140
@Entity
@Table (name = " lawyers ")
public class Lawyer {
@Id
private Long register ;
private String name;
private Integer customersNumber ;
...
}
Este modelo contém as entidades correspondentes à tabela lawyers do banco de dados, com
campos para o registro (chave primária), nome e número de clientes.
Projections
Foi necessário implementar uma projection para representar o resultado esperado da consulta
SQL:
Esta projection é utilizada para capturar o nome e o número de clientes dos advogados direta-
mente da consulta ao banco de dados.
DTO
Foi implementado um DTO para padronizar o resultado da consulta:
public LawyerMinDTO () {
}
141
public LawyerMinDTO ( LawyerMinProjection projection ) {
name = projection . getName ();
customersNumber = projection . getCustomersNumber ();
}
@Override
public String toString () {
return " LawyerMinDTO [name=" + name + ", customersNumber =" +
customersNumber + "]";
}
}
Consulta SQL
O repositório foi configurado com a consulta SQL necessária para executar o relatório desejado
pelo exercício:
import [Link];
142
+ " SELECT name , customers_number "
+ "FROM lawyers "
+ "WHERE customers_number = ( "
+ " SELECT MIN( customers_number ) "
+ " FROM lawyers "
+ ") "
+ "UNION ALL "
+ " SELECT 'Average ', ROUND (AVG( customers_number ), 0) "
+ "FROM lawyers ")
List < LawyerMinProjection > search1 ();
}
A consulta utiliza subconsultas para encontrar o advogado com o maior e menor número de
clientes, e para calcular a média do número de clientes, apresentando os resultados em um
formato fácil de entender.
Testando a Solução
A classe principal do projeto Spring Boot é configurada para executar e testar a consulta:
import [Link];
import [Link]. stream . Collectors ;
@SpringBootApplication
public class Uri2737Application implements CommandLineRunner {
@Autowired
private LawyerRepository repository ;
@Override
public void run( String ... args) throws Exception {
143
}
}
}
144
INSERT INTO trabalha (cpf_emp , pnumero )
VALUES
('049382234322 ', 2010) ,
('586733922290 ', 2020) ,
('049382234322 ', 2020) ;
Saída Esperada
O resultado esperado, apresentado em formato de tabela, é:
Solução do Exercício
A consulta SQL proposta para resolver o problema é:
145
4.33 URI 2990 Solução alternativa com LEFT JOIN
Após revisar o exercício e entender as consultas anteriores, vamos explorar uma abordagem
alternativa para resolver o mesmo problema usando um LEFT JOIN. Esta técnica oferece uma
maneira diferente de filtrar empregados que não participam de nenhum projeto, e pode ser mais
intuitiva em certos contextos.
O script SQL proposto para uma solução alternativa é:
146
4.34 URI 2990 Spring Boot SQL e JPQL
Agora vamos dar continuidade ao exercício, elaborando as consultas SQL e JPQL no projeto
Spring Boot. Abra o projeto “uri2990” que foi fornecido e crie uma base de dados chamada
“uri2990” no banco de dados Postgresql do seu computador.
Modelo de Domínio
O projeto Spring Boot já contém o modelo de domínio implementado, que corresponde ao
modelo relacional do banco de dados:
@Entity
@Table (name = " departamentos ")
public class Departamento {
@Id
private Long dnumero ;
private String dnome ;
@ManyToOne
@JoinColumn (name = " cpf_gerente ")
private Empregado gerente ;
...
@Entity
@Table (name = " empregados ")
public class Empregado {
@Id
private String cpf;
private String enome ;
private Double salary ;
@ManyToOne
@JoinColumn (name = " cpf_supervisor ")
private Empregado supervisor ;
@ManyToOne
@JoinColumn (name = " dnumero ")
private Departamento departamento ;
@ManyToMany
@JoinTable (name = " trabalha ",
joinColumns = @JoinColumn (name = " cpf_emp "),
inverseJoinColumns = @JoinColumn (name = " pnumero "))
private Set <Projeto > projetosOndeTrabalha = new HashSet < >();
147
...
Projections
Implementamos uma projection para representar o resultado da consulta SQL:
DTO
Implementamos um DTO para representar o resultado da consulta:
public EmpregadoDeptDTO () {
}
148
}
@Override
public String toString () {
return " EmpregadoDeptDTO [cpf=" + cpf + ", enome =" + enome + ", dnome ="
+ dnome + "]";
}
}
import [Link];
@Query ( nativeQuery = true , value = " SELECT empregados .cpf , empregados .enome ,
departamentos . dnome "
+ "FROM empregados "
+ "INNER JOIN departamentos ON departamentos . dnumero = empregados .
dnumero "
+ "WHERE empregados .cpf NOT IN ( "
+ " SELECT empregados .cpf "
+ " FROM empregados "
+ " INNER JOIN trabalha ON empregados .cpf = trabalha . cpf_emp "
+ ") "
+ "ORDER BY empregados .cpf")
149
List < EmpregadoDeptProjection > search1 ();
@Query (" SELECT new com. devsuperior . uri2990 .dto. EmpregadoDeptDTO ([Link] , obj
.enome , obj. departamento . dnome ) "
+ "FROM Empregado obj "
+ "WHERE [Link] NOT IN ( "
+ " SELECT [Link] "
+ " FROM Empregado obj "
+ " INNER JOIN obj. projetosOndeTrabalha "
+ ") "
+ "ORDER BY [Link]")
List < EmpregadoDeptDTO > search2 ();
Testando a Solução
O código abaixo na classe principal do projeto Spring Boot testa as três consultas:
import [Link];
import [Link]. stream . Collectors ;
@SpringBootApplication
public class Uri2990Application implements CommandLineRunner {
@Autowired
private EmpregadoRepository repository ;
@Override
public void run( String ... args) throws Exception {
150
List < EmpregadoDeptProjection > list = repository . search1 ();
List < EmpregadoDeptDTO > result1 = list. stream ().map(x -> new
EmpregadoDeptDTO (x)). collect ( Collectors . toList ());
@Entity
@Table (name = " tb_product ")
public class Product {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
@Column ( columnDefinition = "TEXT")
private String description ;
private Double price ;
private String imgUrl ;
151
@ManyToMany
@JoinTable (name = " tb_product_category ",
joinColumns = @JoinColumn (name = " product_id "),
inverseJoinColumns = @JoinColumn (name = " category_id "))
private Set <Category > categories = new HashSet < >();
...
}
@Size(min = 10, message = " Descrição precisa ter no mínimo 10 caracteres ")
@NotBlank ( message = " Campo requerido ")
private String description ;
...
}
152
}
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
...
}
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
@Autowired
private ProductService service ;
@GetMapping
public ResponseEntity <Page < ProductDTO >> findAll (
@RequestParam (name = "name", defaultValue = "") String name ,
Pageable pageable ) {
Page < ProductDTO > dto = service . findAll (name , pageable );
return ResponseEntity .ok(dto);
}
...
}
153
Testando a Requisição no Postman
Para testar essa funcionalidade no Postman, você pode acessar as seguintes URLs, dependendo
dos parâmetros desejados:
• Sem parâmetros de consulta: [Link]
• Com parâmetros de paginação: [Link]
,desc
• Filtrando pelo nome do produto: [Link]
name,desc&name=mac
Cada uma dessas requisições irá retornar uma página de produtos de acordo com os critérios
especificados, permitindo um controle refinado sobre os resultados exibidos.
Modelo de Domínio
Este é um projeto com um modelo de domínio de produtos e categorias, com um relacionamento
muitos-para-muitos entre eles:
@Entity
@Table (name = " tb_product ")
public class Product {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
@ManyToMany
@JoinTable (name = " tb_product_category ",
joinColumns = @JoinColumn (name = " product_id "),
inverseJoinColumns = @JoinColumn (name = " category_id "))
private Set <Category > categories = new HashSet < >();
}
@Entity
@Table (name = " tb_category ")
public class Category {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
154
private String name;
private List < CategoryDTO > categories = new ArrayList < >();
}
@Service
public class ProductService {
@Autowired
private ProductRepository repository ;
155
Controlador para Produtos
@RestController
@RequestMapping ( value = "/ products ")
public class ProductResource {
@Autowired
private ProductService service ;
@GetMapping
public ResponseEntity <Page < ProductDTO >> findAll (
@RequestParam ( value = "page", defaultValue = "0") Integer page ,
@RequestParam ( value = "size", defaultValue = "10") Integer size
) {
Modificação do Repository
@Query (" SELECT obj FROM Product obj JOIN FETCH obj. categories WHERE obj IN :
products ")
List <Product > findProductsCategories (List <Product > products );
Neste método findProductsCategories, usamos a cláusula JOIN FETCH para carregar os produtos
juntamente com suas categorias em uma única consulta. O JOIN FETCH é essencial para evitar
consultas adicionais (lazy loading) quando acessamos as categorias de cada produto. Este mé-
todo espera uma lista de produtos (:products), que são passados como parâmetro. O resultado
156
é que cada produto na lista já vem com suas categorias carregadas, eliminando a necessidade
de consultas subsequentes quando essas categorias são acessadas.
Modificação no Serviço
No serviço ProductService, ajustamos o método find para fazer uso do novo método do reposi-
tório. Primeiro, buscamos uma página de produtos com [Link](pageRequest). Este
passo inicialmente carrega apenas os produtos sem suas categorias devido ao comportamento
padrão do JPA, que não inclui relacionamentos muitos-para-muitos em uma consulta básica de
paginação.
Depois dessa primeira busca, passamos os produtos recuperados como argumento para
findProductsCategories. Este método carrega efetivamente as categorias para os produtos da
página atual, evitando o problema das consultas N+1.
Finalmente, convertemos a página de entidades Product em ProductDTO, mapeando cada produto
para seu respectivo DTO. O método map é usado para transformar cada entidade Product em
um ProductDTO, que já inclui as categorias devido ao passo anterior. Este passo é crucial para
garantir que os dados são transferidos corretamente do modelo de domínio para o modelo
de transferência de dados (DTO), que será usado nas camadas superiores, especialmente na
interface de usuário ou em APIs externas.
Essas modificações garantem que todas as operações relacionadas ao carregamento de dados
são concluídas eficientemente, reduzindo o número total de consultas ao banco de dados e
melhorando a performance da aplicação.
Modelo de Domínio
O projeto de exemplo utiliza um modelo de domínio de empregados e departamentos, com a
seguinte estrutura:
157
@Entity
@Table (name = " tb_department ")
public class Department {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
...
@Entity
@Table (name = " tb_employee ")
public class Employee {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
private Double salary ;
@ManyToOne
@JoinColumn (name = " department_id ")
private Department department ;
...
@Query (value = " SELECT obj FROM Employee obj JOIN FETCH obj. department ")
Page <Employee > searchAll ( Pageable pageable );
}
158
@Query (value = " SELECT obj FROM Employee obj JOIN FETCH obj. department ",
countQuery = " SELECT COUNT (obj) FROM Employee obj JOIN obj.
department ")
Page <Employee > searchAll ( Pageable pageable );
}
Implementação do Controlador
O controlador a seguir disponibiliza um endpoint para testar a consulta paginada de emprega-
dos:
@RestController
@RequestMapping ( value = "/ employees ")
public class EmployeeController {
@Autowired
private EmployeeRepository employeeRepository ;
@GetMapping
public Page <Employee > findAll ( Pageable pageable ) {
return employeeRepository . searchAll ( pageable );
}
}
Testando a Solução
Para testar essa implementação, você pode usar o Postman para enviar requisições GET para
o endpoint /employees com parâmetros de paginação, como:
• [Link]
Observando o console da aplicação Spring durante a execução da requisição, notaremos que
apenas uma única consulta SQL é enviada ao banco de dados para buscar os empregados e seus
departamentos, e uma segunda consulta é usada para contar o número total de registros. Esse
método evita múltiplas idas ao banco de dados para cada empregado, solucionando o problema
das n+1 consultas.
159
Capítulo 5
160
5.4 Ideia Geral do Login e Controle de Acesso
O processo de login e controle de acesso em sistemas informatizados é crucial para a segurança da
informação e para garantir que apenas usuários autorizados tenham acesso a recursos específicos.
Vamos explorar a ideia geral desses processos, dividindo-os em duas partes principais: o login
e o acesso a recursos protegidos.
Login
O processo de login é a primeira etapa para acessar um sistema protegido. Ele envolve a
seguinte sequência de ações:
1. Autenticação de Credenciais: O usuário fornece suas credenciais, como nome de
usuário e senha, através de uma interface de login.
2. Validação: O sistema verifica as credenciais fornecidas. Se as credenciais estiverem
corretas, o sistema gera e retorna um token de acesso. Esse token serve como uma chave
temporária que permite ao usuário acessar os recursos protegidos durante um período
definido.
3. Resposta do Sistema:
• Sucesso: Se as credenciais forem validadas com sucesso, o usuário recebe um token
de acesso.
• Falha: Se as credenciais estiverem incorretas, o sistema retorna um erro 401 (Não
Autorizado), indicando que o acesso foi negado.
161
de acesso. Vamos explorar os conceitos básicos, como o OAuth2 funciona e seus principais
componentes.
Conceitos Básicos
1. OAuth2: Protocolo de autorização que permite que um aplicativo obtenha acesso limi-
tado a um serviço HTTP, seja em nome de um usuário ou em nome do próprio aplicativo.
2. Tokens de Acesso: São credenciais usadas para acessar recursos protegidos e são emi-
tidos pelo servidor de autorização.
3. Escopos: Definem o nível de acesso que o aplicativo tem aos recursos do usuário. Eles
são definidos durante o processo de autorização e são incluídos no token de acesso.
Componentes do OAuth2
1. Cliente: O aplicativo que deseja acessar os recursos do usuário.
2. Servidor de Recursos: O servidor onde os recursos do usuário estão armazenados.
3. Servidor de Autorização: O servidor que autentica a identidade do usuário e emite
tokens de acesso ao cliente após a autorização adequada.
4. Proprietário do Recurso: Geralmente o usuário que concede permissão ao cliente para
acessar seus recursos no servidor.
Fluxos de Concessão
OAuth2 define quatro fluxos de concessão para cobrir diversos cenários de aplicação:
1. Authorization Code: Usado por aplicativos clientes que são executados em um servidor
web. Este é o fluxo mais seguro e comum, pois permite a autenticação do cliente.
2. Implicit: Uma versão simplificada e menos segura do fluxo anterior, geralmente usada
por aplicativos clientes executados em um navegador usando um script, como JavaScript.
3. Password Credentials: Usado por clientes que têm uma relação de confiança com o
usuário, como o aplicativo de um serviço que o próprio usuário instalou em seu dispositivo.
4. Client Credentials: Usado para controlar o acesso entre aplicações sem a intervenção
do usuário, ideal para cenários de servidor para servidor.
Segurança e Considerações
• Segurança: OAuth2 usa SSL/TLS para garantir que todos os dados transmitidos, in-
cluindo tokens e credenciais, sejam seguros.
• Transparência: Os usuários podem especificar o escopo dos acessos que estão conce-
dendo aos aplicativos clientes.
• Flexibilidade: Diversos fluxos de concessão atendem a diferentes tipos de aplicações e
necessidades de segurança.
162
faz a requisição pode ser considerada confiável pelo usuário, como uma aplicação desenvolvida
pelo mesmo serviço que fornece os recursos.
Resposta da Requisição
Após o envio da requisição com as credenciais corretas, o servidor de autorização responde com
um token de acesso, geralmente no formato JWT (JSON Web Token). O JWT é uma maneira
compacta e segura de transmitir informações entre partes como um objeto JSON. Este token
pode incluir:
• Header: Tipicamente consiste no tipo do token (typ), que é JWT, e o algoritmo da
assinatura (alg), como HS256 ou RS256.
• Payload (Claims): Contém as declarações (claims) que incluem informações sobre o
usuário, a validade do token (exp), e outras informações necessárias.
• Assinatura: Utilizada para verificar se o token não foi alterado.
Exemplo de Token JWT:
{
"alg ": "HS256 ",
163
"typ ": "JWT"
}
{
"sub ": "1234567890" ,
"name ": "John Doe",
"admin ": true ,
"iat ": 1516239022 ,
"exp ": 1516242622
}
HMACSHA256 (
base64UrlEncode ( header ) + "." +
base64UrlEncode ( payload ),
your -256 -bit - secret
)
O token JWT oferece uma forma segura e eficiente de representar as credenciais do usuário para
acessar recursos protegidos. O processo de login utilizando o fluxo password grant no OAuth2
é direto, mas deve ser usado com cautela, especialmente em aplicações onde a confiança entre
o usuário e a aplicação é absoluta. A manipulação correta do JWT é crucial para a segurança
da aplicação, garantindo que os tokens sejam sempre validados e tratados de forma segura.
164
quanto o Servidor de Recursos.
• Abra o(s) projeto(s) Spring Boot no seu IDE favorito e execute
• Testes no Postman:
– PARTE 1: não logado
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar 401)
∗ Requisição POST /products {“name”:“Tablet”} (deve retornar 401)
– PARTE 2: logado como Alex
∗ Defina alex@[Link] como nome de usuário no ambiente do Postman
∗ Requisição de login (deve retornar 200 Ok com token JWT. Esse token será
salvo na variável de ambiente ‘token’)
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar produto)
∗ Requisição POST /products {“name”:“Tablet”} (deve retornar 403)
– PARTE 3: logado como Maria
∗ Defina maria@[Link] como nome de usuário no ambiente do Postman
∗ Requisição de login (deve retornar 200 Ok com token JWT. Esse token será
salvo na variável de ambiente ‘token’)
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar produto)
∗ Requisição POST /products {“name”:“Tablet”} (deve inserir produto)
165
@Entity
@Table (name = " tb_role ")
public class Role {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String authority ;
public Role () {
}
...
@Entity
@Table (name = " tb_user ")
public class User {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
@ManyToMany
@JoinTable (name = " tb_user_role ",
joinColumns = @JoinColumn (name = " user_id "),
inverseJoinColumns = @JoinColumn (name = " role_id "))
private Set <Role > roles = new HashSet < >();
public User () {
}
166
if (role. getAuthority (). equals ( roleName )) {
return true;
}
}
return false ;
}
...
}
O prefixo ROLE_ é padrão do Spring Security, e devemos utilizá-lo no nome de nossos perfis de
usuário.
Testando o projeto
Neste momento, é recomendado executar o projeto e conferir no H2 Console se as tabelas
tb_role e tb_user_role foram criadas corretamente com os dados do seed inseridos.
<dependency >
<groupId >org. springframework .boot </ groupId >
<artifactId >spring -boot -starter - security </ artifactId >
</ dependency >
<dependency >
<groupId >org. springframework . security </ groupId >
<artifactId >spring -security -test </ artifactId >
<scope >test </ scope >
</ dependency >
167
Depois de adicionar as dependências, se o projeto for executado, todos endpoints estarão blo-
quados por padrão. Este é o comportamento padrão do Spring Security.
Entretanto, para que possamos prosseguir com o desenvolvimento do projeto fazendo testes
básicos nos endpoints, vamos provisoriamente fazer a liberação de todos endpoints, criando
uma classe de configuração SecurityConfig no projeto, conforme código:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain ( HttpSecurity http) throws Exception {
[Link](csrf -> csrf. disable ());
http. authorizeHttpRequests (auth -> auth. anyRequest (). permitAll ());
return http. build ();
}
@Configuration
public class SecurityConfig {
@Bean
PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder ();
}
@Bean
public SecurityFilterChain filterChain ( HttpSecurity http) throws Exception {
[Link](csrf -> csrf. disable ());
http. authorizeHttpRequests (auth -> auth. anyRequest (). permitAll ());
return http. build ();
}
168
Testando o BCrypt
É possível testar o funcionamento do algoritmo BCrypt fazendo um pequeno teste na classe
principal do projeto. Vamos fazer a classe principal implementar a interface CommandLineRunner,
de modo que, no método run, podemos colocar algum código para executar quando a aplicação
Spring iniciar. Por exemplo, o código abaixo faz a aplicação Spring imprimir na tela o código
BCrypt equivalente ao texto 123456:
@SpringBootApplication
public class DemoApplication implements CommandLineRunner {
@Autowired
private PasswordEncoder encoder ;
@Override
public void run( String ... args) throws Exception {
System .out. println ( encoder . encode (" 123456 "));
}
}
É importante ressaltar que um mesmo texto pode ter mais de um código BCrypt válido. No caso
do texto 123456, um código BCrypt válido é $2a$10$BZEayVp6X1Ry93e44/Rnze0hpK5J3ThbAdUm2OzH.
GSWjA4zmtGHW. Execute a aplicação Spring, e observe no console da aplicação que um código
BCrypt similar a este será impresso no log do console.
INSERT INTO tb_user (name , email , password ) VALUES ('Alex ', 'alex@gmail .com ', '
$2a$10$BZEayVp6X1Ry93e44 / Rnze0hpK5J3ThbAdUm2OzH . GSWjA4zmtGHW ');
INSERT INTO tb_user (name , email , password ) VALUES ('Maria ', 'maria@gmail .com ',
'$2a$10$BZEayVp6X1Ry93e44 / Rnze0hpK5J3ThbAdUm2OzH . GSWjA4zmtGHW ');
169
5.11 Implementando o checklist do Spring Security
PARTE 1
Nesta seção vamos implementar o checklist do Spring Security, ou seja, precisamos implementar
os tipos requeridos pelo Spring Security, para que nosso projeto possa executar apropriadamente
seu fluxo de segurança. Os tipos básicos são:
• GrantedAuthority: tipo que representa cada perfil de usuário.
• UserDetails: tipo que representa o usuário gerenciado pelo Spring Security.
• UserDetailsService: tipo responsável pela operação de recuperar um usuário UserDetails
do banco de dados.
• UsernameNotFoundException: exceção lançada caso o usuário não seja encontrado.
Nesta seção vamos comerçar implementando GrantedAuthority e UserDetails, que são interfaces
a serem implementadas. Para isto, vamos fazer que estas duas interfaces sejam implementadas
pelas nossas classes Role e User, respectivamente.
Classe Role
170
import org. springframework . security .core. GrantedAuthority ;
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String authority ;
public Role () {
}
@Override
public String getAuthority () {
return authority ;
}
@Override
public int hashCode () {
return Objects .hash( authority );
}
@Override
public boolean equals ( Object obj) {
if (this == obj)
return true;
if (obj == null)
return false ;
if ( getClass () != obj. getClass ())
return false ;
Role other = (Role) obj;
return Objects . equals (authority , other . authority );
171
}
}
Classe User
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
@ManyToMany
@JoinTable (name = " tb_user_role ",
joinColumns = @JoinColumn (name = " user_id "),
inverseJoinColumns = @JoinColumn (name = " role_id "))
private Set <Role > roles = new HashSet < >();
public User () {
}
172
public Long getId () {
return id;
}
@Override
public boolean equals ( Object o) {
if (this == o) return true;
if (o == null || getClass () != o. getClass ()) return false ;
@Override
public int hashCode () {
173
return id != null ? id. hashCode () : 0;
}
@Override
public Collection <? extends GrantedAuthority > getAuthorities () {
return roles ;
}
@Override
public String getUsername () {
return email ;
}
@Override
public boolean isAccountNonExpired () {
return true;
}
@Override
public boolean isAccountNonLocked () {
return true;
}
@Override
public boolean isCredentialsNonExpired () {
return true;
}
@Override
public boolean isEnabled () {
return true;
}
}
```java
@Configuration
174
public class SecurityConfig {
@Bean
@Profile ("test")
@Order (1)
SecurityFilterChain h2SecurityFilterChain ( HttpSecurity http) throws
Exception {
...
}
```cd ..
```java
package com. devsuperior .demo. projections ;
Em seguida, vamos definir a consulta SQL dentro do nosso UserRepository, conforme código a
seguir:
import [Link];
@Repository
public interface UserRepository extends JpaRepository <User , Long > {
175
@Query ( nativeQuery = true , value = """
SELECT tb_user . email AS username , tb_user .password , tb_role .id
AS roleId , tb_role . authority
FROM tb_user
INNER JOIN tb_user_role ON tb_user .id = tb_user_role . user_id
INNER JOIN tb_role ON tb_role .id = tb_user_role . role_id
WHERE tb_user . email = : email
""")
List < UserDetailsProjection > searchUserAndRolesByEmail ( String email );
}
Implementação de UserService
Agora, vamos criar a classe UserService dentro do pacote services, e vamos implementar a
interface UserDetailsService nesta classe, conforme código a seguir.
import [Link];
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository repository ;
@Override
public UserDetails loadUserByUsername ( String username ) throws
UsernameNotFoundException {
176
}
return user;
}
}
Variáveis de ambiente
Vamos começar incluindo alguns valores de configuração no arquivo [Link] do
projeto:
cors. origins =${ CORS_ORIGINS :http :// localhost :3000 , http :// localhost :5173}
Dependências Maven
Agora vamos definir novas dependências Maven para implementar o Authorization Server e o
Resource Server em nossa aplicação. Estas dependências devem ser adicionadas ao arquivo
[Link] do projeto:
<dependency >
<groupId >org. springframework . security </ groupId >
<artifactId >spring -security -oauth2 - authorization - server </ artifactId >
</ dependency >
<dependency >
177
<groupId >org. springframework .boot </ groupId >
<artifactId >spring -boot -starter -oauth2 -resource - server </ artifactId >
</ dependency >
• [Link]
• [Link]
• [Link]
• [Link]
3. Dentro do seu projeto Spring Boot, crie um subpacote [Link], de forma similar
à que está no projeto referência, e copie as quatro classes para esse pacote.
Authorization Server
Atenção: caso não tenha excluído ainda a classe SecurityConfig do projeto, faça isso neste
momento.
Agora vamos incluir no projeto a implementação do Authorization Server, que será a classe
AuthorizationServerConfig, dentro do pacote config.
Resource Server
Agora vamos incluir no projeto a implementação do Resource Server, que será a classe
ResourceServerConfig, dentro do pacote config.
178
Resource Server, como o próprio nome sugere, é a parte do sistema responsável por disponibilizar
os recursos do sistema. O Resource Server tem a responsabilidade de tratar requisições recebidas
pelo sistema e, caso seja um recurso protegido, deverá decidir se o recurso deve ou não ser
disponibilizado ao usuário, conforme as políticas de controle de acesso aplicadas àquele usuário.
Por favor, acesse novamente o projeto referência da versão desejada do Spring Boot no nosso
repositório Github, e copie a classe [Link] do subpacote config para seu
projeto. Salve a classe em um subpacote config no seu projeto, de forma similar ao projeto
referência.
Configuração no Postman
1. Ambiente do Postman: Verifique se as variáveis de ambiente no Postman estão corre-
tamente configuradas. As variáveis a serem utilizadas são:
• host: [Link]
• client-id: myclientid
• client-secret: myclientsecret
• username: alex@[Link]
• password: 123456
2. Configuração da Requisição:
• URL da Requisição: Configure o Postman para fazer uma requisição POST para
a URL {{host}}/oauth2/token.
• Body da Requisição: No corpo da requisição, configure para enviar os dados como
x-www-form-urlencoded. Adicione os seguintes campos:
– username: {{username}}
– password: {{password}}
– grant_type: password
• Header de Autorização:
– Utilize o método Basic Auth no Postman, onde você deve inserir {{client-id}}
como o username e {{client-secret}} como o password. O Postman automati-
camente irá gerar o header de autorização adequado.
Testando a Requisição
1. Envio da Requisição:
• Após configurar a requisição, clique em “Send” no Postman para enviar a requisição
de login ao servidor.
• Observe a resposta retornada pelo servidor. Se as credenciais estiverem corretas,
você receberá um token JWT no corpo da resposta, o qual será utilizado para acessar
recursos protegidos na aplicação.
179
2. Análise da Resposta:
• O token JWT deve estar presente no campo access_token da resposta JSON. Este
token será usado em requisições subsequentes para autenticação e autorização.
• Em caso de credenciais incorretas, o servidor responderá com um status HTTP 401
(Unauthorized), indicando que as credenciais fornecidas não são válidas.
Esse script verifica se a requisição ocorreu com sucesso, ou seja, o código de resposta tem que
ser 2xx. Em caso afirmativo, o script acessa o corpo da resposta e salva o valor do campo
access_token na variável token do environment do Postman.
Com esta automação, basta executar a requisição de login, que o token já estará salvo em nosso
environment, pronto para ser usado nas outras requisições.
180
@Autowired
private UserDetailsService userDetailsService ;
@Bean
@Order (2)
SecurityFilterChain asSecurityFilterChain ( HttpSecurity httpSecurity ) throws
Exception {
// @formatter :off
http. getConfigurer ( OAuth2AuthorizationServerConfigurer . class )
. tokenEndpoint ( tokenEndpoint -> tokenEndpoint
. accessTokenRequestConverter (new
CustomPasswordAuthenticationConverter ())
. authenticationProvider (new CustomPasswordAuthenticationProvider
( authorizationService () , tokenGenerator () , userDetailsService ,
passwordEncoder ())));
@Bean
OAuth2AuthorizationService authorizationService () {
return new InMemoryOAuth2AuthorizationService ();
}
@Bean
OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService () {
return new InMemoryOAuth2AuthorizationConsentService ();
}
@Bean
AuthorizationServerSettings authorizationServerSettings () {
return AuthorizationServerSettings . builder (). build ();
}
Componente PasswordEncoder
181
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder ();
}
@Bean
RegisteredClientRepository registeredClientRepository () {
// @formatter :off
RegisteredClient registeredClient = RegisteredClient
. withId (UUID. randomUUID (). toString ())
. clientId ( clientId )
. clientSecret ( passwordEncoder (). encode ( clientSecret ))
.scope("read")
.scope(" write ")
. authorizationGrantType (new AuthorizationGrantType (" password "))
. tokenSettings ( tokenSettings ())
. clientSettings ( clientSettings ())
.build ();
// @formatter :on
@Bean
ClientSettings clientSettings () {
return ClientSettings . builder (). build ();
}
@Bean
TokenSettings tokenSettings () {
// @formatter :off
return TokenSettings . builder ()
. accessTokenFormat ( OAuth2TokenFormat . SELF_CONTAINED )
. accessTokenTimeToLive ( Duration . ofSeconds ( jwtDurationSeconds ))
.build ();
// @formatter :on
}
@Bean
OAuth2TokenGenerator <? extends OAuth2Token > tokenGenerator () {
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder ( jwkSource ());
JwtGenerator jwtGenerator = new JwtGenerator ( jwtEncoder );
jwtGenerator . setJwtCustomizer ( tokenCustomizer ());
OAuth2AccessTokenGenerator accessTokenGenerator = new
OAuth2AccessTokenGenerator ();
return new DelegatingOAuth2TokenGenerator ( jwtGenerator , accessTokenGenerator
);
}
182
@Bean
OAuth2TokenCustomizer < JwtEncodingContext > tokenCustomizer () {
return context -> {
OAuth2ClientAuthenticationToken principal = context . getPrincipal ();
CustomUserAuthorities user = ( CustomUserAuthorities ) principal .
getDetails ();
List <String > authorities = user. getAuthorities (). stream ().map(x -> x.
getAuthority ()). toList ();
if ( context . getTokenType (). getValue (). equals (" access_token ")) {
// @formatter :off
context . getClaims ()
.claim (" authorities ", authorities )
.claim (" username ", user. getUsername ());
// @formatter :on
}
};
}
@Bean
JwtDecoder jwtDecoder (JWKSource < SecurityContext > jwkSource ) {
return OAuth2AuthorizationServerConfiguration . jwtDecoder ( jwkSource );
}
@Bean
JWKSource < SecurityContext > jwkSource () {
RSAKey rsaKey = generateRsa ();
JWKSet jwkSet = new JWKSet ( rsaKey );
return (jwkSelector , securityContext ) -> jwkSelector . select ( jwkSet );
}
183
Obtenção dos valores das variáveis de ambiente
@Bean
@Profile ("test")
@Order (1)
public SecurityFilterChain h2SecurityFilterChain ( HttpSecurity http) throws
Exception {
@Bean
@Order (3)
public SecurityFilterChain rsSecurityFilterChain ( HttpSecurity httpSecurity )
throws Exception {
HttpSecurity http = httpSecurity . securityMatcher ("/**");
[Link](csrf -> csrf. disable ());
http. authorizeHttpRequests ( authorize -> authorize . anyRequest (). permitAll ());
http. oauth2ResourceServer ( oauth2ResourceServer -> oauth2ResourceServer .jwt(
Customizer . withDefaults ()));
[Link](cors -> cors. configurationSource ( corsConfigurationSource ()));
return http. build ();
}
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter () {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new
JwtGrantedAuthoritiesConverter ();
grantedAuthoritiesConverter . setAuthoritiesClaimName (" authorities ");
grantedAuthoritiesConverter . setAuthorityPrefix ("");
184
Componentes de configuração de CORS
@Bean
CorsConfigurationSource corsConfigurationSource () {
@Bean
FilterRegistrationBean < CorsFilter > filterRegistrationBeanCorsFilter () {
FilterRegistrationBean < CorsFilter > bean = new FilterRegistrationBean <>(
new CorsFilter ( corsConfigurationSource ()));
bean. setOrder ( Ordered . HIGHEST_PRECEDENCE );
return bean;
}
185
– Acesso autorizado somente para perfil: ROLE_ADMIN
@Bean
@Order (3)
public SecurityFilterChain rsSecurityFilterChain ( HttpSecurity httpSecurity )
throws Exception {
HttpSecurity http = httpSecurity . securityMatcher ("/**");
...
...
}
Esta definição dentro do filtro do ResourceServer tem efeito global no sistema. Sendo assim,
para simplificar nossa lógica de controle de acesso, decidimos definir globalmente que todos
endpoints estão liberados e, aqueles endpoints que tiverem que ser protegidos, serão assim
definidos individualmente, conforme vamos mostrar a seguir.
@GetMapping
public ResponseEntity <List < ProductDTO >> findAll () {
List <ProductDTO > list = productService . findAll ();
return ResponseEntity .ok(list);
}
186
acrescentando a annotation @PreAuthorize, especificando estes perfis como autorizados a acessar
o endpoint:
Testando a aplicação
Neste momento você já pode executar a aplicação e testar os endpoints. A seguir apresentamos
as orientações de como fazer.
Preparando o Postman
Primeiro é preciso que cada requisição no Postman esteja devidamente configurada para passar
o token de acesso no cabeçado Authorization da requisição, se for uma requisição a um recurso
protegido. Para cada requisição da sua collection Postman, acesse a aba Authorization para
configurar o token, como se segue:
• Requisição GET /products: como este é um recurso público, ou seja, que não precisa de
usuário logado, na aba Authorization, no campo Type selecione o valor “No Auth”.
• Requisição GET /products{id}: como este é um recurso protegido, na aba Authorization,
no campo Type selecione o valor “Bearer Token”, e no campo Token digite o valor {{token
}} para que o valor do token seja obtido a partir da variável token do environment do
Postman.
• Requisição POST /products: como este é um recurso protegido, na aba Authorization, no
campo Type selecione o valor “Bearer Token”, e no campo Token digite o valor {{token
}} para que o valor do token seja obtido a partir da variável token do environment do
Postman.
187
Pronto. Agora é possível testar cada um dos cenários de teste, conforme especificamos anteri-
ormente:
• Testes no Postman:
– PARTE 1: não logado
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar 401)
∗ Requisição POST /products {“name”:“Tablet”} (deve retornar 401)
– PARTE 2: logado como Alex
∗ Defina alex@[Link] como nome de usuário no ambiente do Postman
∗ Requisição de login (deve retornar 200 Ok com token JWT. Esse token será
salvo na variável de ambiente ‘token’)
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar produto)
∗ Requisição POST /products {“name”:“Tablet”} (deve retornar 403)
– PARTE 3: logado como Maria
∗ Defina maria@[Link] como nome de usuário no ambiente do Postman
∗ Requisição de login (deve retornar 200 Ok com token JWT. Esse token será
salvo na variável de ambiente ‘token’)
∗ Requisição GET /products (deve retornar produtos)
∗ Requisição GET /products/1 (deve retornar produto)
∗ Requisição POST /products {“name”:“Tablet”} (deve inserir produto)
Classe Role
Vamos implementar a entidade Role para representar um perfil de usuário.
@Entity
@Table (name = " tb_role ")
public class Role {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String authority ;
public Role () {
188
}
...
}
Classe User
Vamos acrescentar o relacionamento entre usuário e role na classe User, e vamos acrescentar os
métodos addRole, hasRole e getRoles.
@Entity
@Table (name = " tb_user ")
public class User {
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
private Long id;
private String name;
public User () {
}
189
}
...
}
INSERT INTO tb_user (name , email , phone , password , birth_date ) VALUES ('Maria
Brown ', 'maria@gmail .com ', '988888888 ', '$2a$10$BZEayVp6X1Ry93e44 /
Rnze0hpK5J3ThbAdUm2OzH . GSWjA4zmtGHW ', '2001 -07 -25 ');
INSERT INTO tb_user (name , email , phone , password , birth_date ) VALUES ('Alex
Green ', 'alex@gmail .com ', '977777777 ', '$2a$10$BZEayVp6X1Ry93e44 /
Rnze0hpK5J3ThbAdUm2OzH . GSWjA4zmtGHW ', '1987 -12 -13 ');
Testando o projeto
Neste momento, é recomendado executar o projeto e conferir no H2 Console se as tabelas
tb_role e tb_user_role foram criadas corretamente com os dados do seed inseridos.
190
Dependências Maven
Primeiramente, acrescente todas as dependências de segurança ao projeto:
<dependency >
<groupId >org. springframework .boot </ groupId >
<artifactId >spring -boot -starter - security </ artifactId >
</ dependency >
<dependency >
<groupId >org. springframework . security </ groupId >
<artifactId >spring -security -test </ artifactId >
<scope >test </ scope >
</ dependency >
<dependency >
<groupId >org. springframework . security </ groupId >
<artifactId >spring -security -oauth2 - authorization - server </ artifactId >
</ dependency >
<dependency >
<groupId >org. springframework .boot </ groupId >
<artifactId >spring -boot -starter -oauth2 -resource - server </ artifactId >
</ dependency >
...
@Override
public String getAuthority () {
return authority ;
}
...
Classe User
191
...
@Override
public Collection <? extends GrantedAuthority > getAuthorities () {
return roles ;
}
@Override
public String getUsername () {
return email ;
}
@Override
public boolean isAccountNonExpired () {
return true;
}
@Override
public boolean isAccountNonLocked () {
return true;
}
@Override
public boolean isCredentialsNonExpired () {
return true;
}
@Override
public boolean isEnabled () {
return true;
}
}
Interface UserRepository
192
public interface UserRepository extends JpaRepository <User , Long > {
Classe UserService
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserRepository repository ;
@Override
public UserDetails loadUserByUsername ( String username ) throws
UsernameNotFoundException {
return user;
}
}
Variáveis de ambiente
Confira como deve ficar o arquivo [Link] do projeto DSCommerce:
193
spring . profiles . active =test
spring .[Link] -in -view=false
cors. origins =${ CORS_ORIGINS :http :// localhost :3000 , http :// localhost :5173}
1. Acesse o projeto referência da versão desejada do Spring Boot no nosso repositório Github.
Por exemplo, se a versão desejada do Spring Boot for a 3.4.3, você deve acessar a subpasta
correspondente no repositório:
[Link]
pring-boot-3-4-3
2. Dentro do projeto, acesse a subpasta /src/main/java/com/devsuperior/demo/config/
customgrant/, onde estarão contidas as quatro classes que vamos usar:
• [Link]
• [Link]
• [Link]
• [Link]
3. Dentro do seu projeto Spring Boot, crie um subpacote [Link], de forma similar
à que está no projeto referência, e copie as quatro classes para esse pacote.
Authorization Server
Agora vamos incluir no projeto a implementação do Authorization Server, que será a classe
AuthorizationServerConfig, dentro do pacote config.
Por favor, acesse novamente o projeto referência da versão desejada do Spring Boot no nosso
repositório Github, e copie a classe [Link] do subpacote config para
seu projeto. Salve a classe em um subpacote config no seu projeto, de forma similar ao projeto
referência. Não esqueça de editar nome do pacote da classe para [Link].
config.
194
Resource Server
Agora vamos incluir no projeto a implementação do Resource Server, que será a classe
ResourceServerConfig, dentro do pacote config.
Por favor, acesse novamente o projeto referência da versão desejada do Spring Boot no nosso
repositório Github, e copie a classe [Link] do subpacote config para seu
projeto. Salve a classe em um subpacote config no seu projeto, de forma similar ao projeto
referência. Não esqueça de editar nome do pacote da classe para [Link].
config.
Requisição de login
Na sua collection Postman do projeto DSCommerce, prepare uma requisição de login de forma
similar à que aprendemos anteriormente no projeto referência. Prepare também o environment
do Postman conforme nos aprendemos no projeto referência.
Depois, execute o projeto DSCommerce e execute a requisição de login. Neste momento o
projeto DSCommerce deve ser capaz de executar a requisição com sucesso e retornar um token.
Vale ressaltar que, como os endpoints GET são públicos, não foi preciso adicionar controle de
acesso a estes endpoints.
195
A seguir mostramos os trechos de código de ProductController onde definirmos os controles de
acesso.
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
...
Testando os endpoints
Execute o projeto e faça os seguintes testes para conferir os comportamentos desejados:
• Requisições GET /products{id} e GET /products: devem retornar os produtos, com ou sem
usuários logados. Para simular usuários não logados, basta apagar o valor da variável
token no environment do Postman.
196
5.24 Obtendo o usuário logado
Nesta seção vamos mostrar a implementação de uma operação muito útil, que é a obtenção dos
dados do usuário logado.
...
@Service
public class UserService implements UserDetailsService {
...
Classe UserDTO
Para que possamos criar um endpoint para retornar os dados do usuário logado, e customizar
os dados que queremos retornar, vamos criar uma classe UserDTO conforme código a seguir.
197
package com. devsuperior . dscommerce .dto;
public UserDTO (Long id , String name , String email , String phone , LocalDate
birthDate ) {
[Link] = id;
[Link] = name;
this. email = email ;
this. phone = phone ;
this. birthDate = birthDate ;
}
198
public List <String > getRoles () {
return roles ;
}
}
@Service
public class UserService implements UserDetailsService {
...
Classe UserController
Agora, para que possamos disponibilizar um endpoint para retornar os dados do usuário logado,
vamos implementar este endpoint em uma nova classe UserController dentro do subpacote
controllers, conforme código a seguir. Repare que estamos usando a annotation @PreAuthorize
para indicar que este endpoint pode ser acessado por qualquer usuário logado, independente do
perfil.
@RestController
@RequestMapping ( value = "/users ")
public class UserController {
@Autowired
private UserService service ;
199
@PreAuthorize (" hasAnyRole (' ROLE_ADMIN ', 'ROLE_CLIENT ')")
@GetMapping ( value = "/me")
public ResponseEntity <UserDTO > getMe () {
UserDTO dto = service . getMe ();
return ResponseEntity .ok(dto);
}
}
Testando a requisição
Acrescente uma requisição no Postman GET /users/me. Defina o cabeçalho Authorization com o
tipo “Bearer Token” e o token {{token}}. Com o projeto Spring Boot em execução, execute a
requisição de login para o usuário maria@[Link], depois execute a requisição GET /users/me.
O resultado do corpo da resposta deve ser como mostrado a seguir.
{
"id": 1,
"name ": " Maria Brown ",
"email ": " maria@gmail .com",
"phone ": "988888888" ,
" birthDate ": "2001 -07 -25" ,
"roles ": [
" ROLE_CLIENT "
]
}
Classe ProductMinDTO
Vamos criar um DTO para customizar o retorno da nossa busca paginada de produtos.
200
[Link] = id;
[Link] = name;
this. price = price ;
this. imgUrl = imgUrl ;
}
Classe ProductService
Agora vamos atualizar o método findAll na classe ProductService para retornar uma página de
objetos ProductMinDTO:
@Service
public class ProductService {
...
...
}
Classe ProductController
Vamos atualizar também o controlador para retornar uma página de objetos ProductMinDTO:
201
@RestController
@RequestMapping ( value = "/ products ")
public class ProductController {
...
@GetMapping
public ResponseEntity <Page < ProductMinDTO >> findAll (
@RequestParam (name = "name", defaultValue = "") String name ,
Pageable pageable ) {
Page < ProductMinDTO > dto = service . findAll (name , pageable );
return ResponseEntity .ok(dto);
}
...
}
## Classe CategoryDTO
```java
package com. devsuperior . dscommerce .dto;
public CategoryDTO () {
}
202
public String getName () {
return name;
}
}
Classe ProductDTO
Agora, vamos atualizar o código da nossa classe ProductDTO. Repare que incluímos uma lista
categories do tipo List<CategoryDTO>, incluímos o método getCategories, e incluímos no cons-
trutor public ProductDTO(Product entity) uma lógica para instanciar as categorias do DTO a
partir das categorias de uma entidade entity.
@Size(min = 10, message = " Descrição precisa ter no mínimo 10 caracteres ")
@NotBlank ( message = " Campo requerido ")
private String description ;
public ProductDTO () {
}
203
this. description = description ;
this. price = price ;
this. imgUrl = imgUrl ;
}
Classe ProductService
Como agora nosso ProductDTO é um objeto que contém não só os dados básicos do produto,
mas também uma lista com as categorias do produto, na classe ProductService foi necessário
atualizar a forma como copiamos os dados do DTO para a entidade que será salva no banco de
dados.
@Service
public class ProductService {
...
@Transactional
204
public ProductDTO insert ( ProductDTO dto) {
Product entity = new Product ();
copyDtoToEntity (dto , entity );
entity = repository .save( entity );
return new ProductDTO ( entity );
}
@Transactional
public ProductDTO update (Long id , ProductDTO dto) {
try {
Product entity = repository . getReferenceById (id);
copyDtoToEntity (dto , entity );
entity = repository .save( entity );
return new ProductDTO ( entity );
}
catch ( EntityNotFoundException e) {
throw new ResourceNotFoundException (" Recurso não encontrado ");
}
}
{
"name ": "Meu produto ",
" description ": " Lorem ipsum , dolor sit amet",
" imgUrl ": " https :// [Link]/ image .jpg",
"price ": 50.0 ,
" categories ": [
{
"id ": 2
},
{
"id ": 3
}
205
]
}
{
"id": 1,
" moment ": "2022 -07 -25 T13 :00:00 Z",
" status ": "PAID",
" client ": {
"id": 1,
"name ": " Maria Brown "
},
" payment ": {
"id": 1,
" moment ": "2022 -07 -25 T15 :00:00 Z"
},
"items ": [
{
" productId ": 1,
"name ": "The Lord of the Rings ",
"price ": 90.5 ,
" quantity ": 2,
" subTotal ": 181.0
},
{
" productId ": 3,
"name ": " Macbook Pro",
"price ": 1250.0 ,
" quantity ": 1,
" subTotal ": 1250.0
}
],
"total ": 1431.0
}
Repare que nosso objeto JSON possui objetos aninhados para os campos client, payment e
items,onde este último é uma lista de objetos que representam um item do pedido. Vamos
começar então implementando classes DTO para representar esses objetos aninhados. Repare
como vamos definindo os nomes dos campos das classes para corresponder com os nomes dos
campos mostrados no JSON mostrado anteriormente.
Classe ClientDTO
206
package com. devsuperior . dscommerce .dto;
Classe PaymentDTO
207
public Instant getMoment () {
return moment ;
}
}
Classe OrderItemDTO
public OrderItemDTO () {
}
208
return price * quantity ;
}
Agora que as três classes DTO auxiliares estão implementadas, vamos implementar a classe
OrderDTO para representar todo o objeto do pedido conforme JSON apresentado anteriormente.
Classe OrderDTO
private List < OrderItemDTO > items = new ArrayList < >();
public OrderDTO () {
}
209
for ( OrderItem item : entity . getItems ()) {
OrderItemDTO itemDto = new OrderItemDTO (item);
items .add( itemDto );
}
}
210
Depois, vamos implementar a classe OrderService, para buscar os dados do banco de dados e
gerar como resposta o DTO completo com todos objetos aninhados.
Classe OrderService
@Service
public class OrderService {
@Autowired
private OrderRepository repository ;
Por fim, vamos implementar a classe OrderController responsável por disponibilizar o endpoint
para buscar um pedido por id. Repare que já fizemos o controle de acesso básico a este endpoint,
definindo que só pode ser acessado por um usuário logado, seja cliente ou administrador.
@RestController
@RequestMapping ( value = "/ orders ")
public class OrderController {
211
@Autowired
private OrderService service ;
Testando o endpoint
Agora podemos testar nosso endpoint no Postman. Crie uma requisição na collection do projeto,
para acessar o caminho /orders/{id}. Defina o cabeçalho Authorization com o tipo “Bearer
Token” e o token com o valor {{token}}. Ao executar a requisição, deverá ser retornado o id do
produto informado na URL.
{
"items ": [
{
" productId ": 1,
" quantity ": 2
},
{
" productId ": 5,
" quantity ": 1
}
]
}
Repare, entretanto, que essa estrutura de dados está contida no tipo ProductDTO que criamos
anteriormente. Assim, podemos utilizar o próprio tipo ProductDTO para receber os dados dos
produtos e quantidades para salvar um novo pedido. A única modificação que faremos na classe
ProductDTO será acrescentar uma validação para não aceitar uma lista de itens vazia:
...
212
...
Agora vamos apresentar o código fonte atualizado da classe OrderService, com o novo método
insert para salvar um pedido. A explicação do código será apresentada em seguida.
@Service
public class OrderService {
@Autowired
private OrderRepository repository ;
@Autowired
private ProductRepository productRepository ;
@Autowired
private OrderItemRepository orderItemRepository ;
@Autowired
private UserService userService ;
@Transactional
public OrderDTO insert ( OrderDTO dto) {
213
order . setMoment ( Instant .now ());
order . setStatus ( OrderStatus . WAITING_PAYMENT );
O método insert prepara um objeto Order, associado a um ou mais objetos OrderItem, e depois
salva todos objetos no banco de dados.
Repare que os dados do objeto order são iniciados com os valores apropriados:
• Campo moment é iniciado com o instante atual.
• Campo status é iniciado com o valor WAITING_PAYMENT.
• Campo client é iniciado com o usuário logado.
Além disso, os itens do pedido são instanciados conforme valores advindos do objeto dto: o id
de cada produto e sua respectiva quantidade.
Por fim, os objetos são salvos no banco de dados, e o novo objeto salvo order é utilizado para
retorna o objeto OrderDTO resultante.
@RestController
@RequestMapping ( value = "/ orders ")
public class OrderController {
...
214
. buildAndExpand (dto. getId ()). toUri ();
return ResponseEntity . created (uri).body(dto);
}
}
Testando a requisição
Para testar a requisição, basta criar uma nova requisição com acesso protegido de maneira
similar a que fizemos anteriormente. A requisição será uma operação POST no caminho /orders,
e o corpo da requisição será uma lista de objetos contendo id do produto e quantidade, conforme
mostrado no início desta seção.
Vamos também implementar um método específico para tratar esta exceção em nossa classe
ControllerExceptionHandler.
Classe ControllerExceptionHandler
@ControllerAdvice
public class ControllerExceptionHandler {
215
...
@Service
public class AuthService {
@Autowired
private UserService userService ;
216
@Service
public class OrderService {
...
@Autowired
private AuthService authService ;
...
}
Implementações
Interface CategoryRepository
217
Classe CategoryService
import [Link];
@Service
public class CategoryService {
@Autowired
private CategoryRepository repository ;
Classe CategoryController
import [Link];
@RestController
@RequestMapping ( value = "/ categories ")
public class CategoryController {
@Autowired
private CategoryService service ;
@GetMapping
public ResponseEntity <List < CategoryDTO >> findAll () {
List < CategoryDTO > list = service . findAll ();
return ResponseEntity .ok(list);
}
}
218
Testando o endpoint
O endpoint para buscar as categorias é um endpoint público, que não exige um usuário logado.
Assim, basta criar uma nova requisição na collection do Postman, com o método GET no caminho
categories. Ao executar esta requisição, deve ser retornada uma resposta cujo corpo possua
uma lista de categorias, similar ao JSON mostrado a seguir.
[
{
"id": 1,
"name ": " Livros "
},
{
"id": 2,
"name ": " Eletrônicos "
},
{
"id": 3,
"name ": " Computadores "
}
]
@Size(min = 10, message = " Descrição precisa ter no mínimo 10 caracteres ")
@NotBlank ( message = " Campo requerido ")
private String description ;
...
Conforme podemos observar, alguns campos possuem mais de uma tratativa de erro. Porém, na
219
resposta de nossas requisições, queremos mostrar apenas uma mensagem de erro por vez para
cada campo. Assim, para assegurar esse comportamento em nossa resposta, vamos recapitular
nossa classe ValidationError, responsável por carregar os dados de validação:
private List < FieldMessage > errors = new ArrayList < >();
Para assegurar que apenas um erro seja armazenado para cada campo do objeto, vamos acres-
centar um comando no método addError para remover algum possível erro do mesmo campo,
antes de adicionar um novo erro para este campo:
Esta é uma forma de assegurar que apenas um erro para cada campo esteja presente no objeto
ValidationError, e consequentemente na resposta da requisição.
220
Capítulo 6
221
deixar o projeto como portfolio para seu currículo no seu Github, com um bom README de
apresentação.
Aproveitamos para sugerir um conteúdo nosso do Youtube sobre como fazer um README
para portfolio no Github:
[Link]
222
• Vantagens: Fácil de usar, suporta várias linguagens de programação populares
como Ruby, Java, PHP, Python, Node, Go, Scala e Clojure.
2. Railway:
• Características: Plataforma que oferece uma experiência simplificada para lançar
rapidamente aplicações e bancos de dados.
• Vantagens: Integração fácil com GitHub, provisionamento rápido e interface in-
tuitiva.
3. Netlify:
• Características: Foca na hospedagem e automação de front-ends modernos, com
suporte robusto para JAMstack.
• Vantagens: Deploy automático a partir de sistemas de controle de versão, funcio-
nalidades de pré-visualização ao vivo, e alto desempenho para aplicações estáticas.
4. Firebase:
• Características: Plataforma do Google que oferece uma solução completa para o
desenvolvimento de aplicações móveis e web que necessitam de backend, análise e
recursos de ligação com o cliente.
• Vantagens: Integração com outros serviços Google, autenticação simplificada, e
uma excelente base de dados em tempo real.
Escolher entre um serviço de cloud completo e um PaaS depende das necessidades específicas
do projeto e da equipe de desenvolvimento. Enquanto os serviços completos oferecem controle
total sobre os recursos e a configuração, os serviços PaaS proporcionam conveniência e eficiência,
especialmente para projetos com requisitos de infraestrutura mais simples ou para equipes que
desejam evitar a complexidade da gestão de servidores. Ambos os tipos de serviços têm o
potencial de oferecer soluções robustas e escaláveis para uma variedade de necessidades de
desenvolvimento e implantação de aplicações.
Agora, dentro da pasta do projeto, vamos fazer o comando Git para deixar o projeto somente
até o commit SQL, projection, get games by list:
223
git reset --hard cc7be51
Neste momento, você pode conferir o histórico de commits do projeto fazendo o seguinte co-
mando:
git remote set -url origin git@github .com: meuusuario / meuprojeto .git
Agora que seu projeto local está devidamente associado com o repositório no Github, podemos
salvar o projeto no Github com o comando:
224
do mesmo projeto.
Visão Geral
Perfis de projeto são uma maneira de segmentar as configurações do aplicativo para diferentes
ambientes, facilitando a gestão de variáveis específicas de cada ambiente, como configurações
de banco de dados, variáveis de ambiente e customizações de comportamento. No Spring Boot,
esses perfis são definidos no [Link] ou [Link], permitindo ativar ou
desativar certos beans e configurações baseadas no perfil ativo.
Exemplos de Perfis
Perfil test
• Descrição: Utilizado principalmente durante o desenvolvimento e testes.
• Configuração típica:
– Utilização do banco de dados H2, que é um banco de dados em memória.
– Configurações simplificadas que não necessitam de setup externo.
– Esse perfil facilita a execução de testes automatizados e verificações rápidas de in-
tegridade e funcionalidade do código.
Perfil dev
• Descrição: Usado para desenvolvimento contínuo e homologação em um ambiente local.
• Configuração típica:
– Conexão com uma instância local do PostgreSQL.
– Configurações podem incluir detalhes mais próximos ao ambiente de produção, mas
ainda em um cenário controlado.
Perfil prod
• Descrição: Configurações destinadas ao ambiente de produção.
• Configuração típica:
– Conexão com o banco de dados PostgreSQL em um ambiente de nuvem.
– Configurações de segurança e performance otimizadas para carga alta.
Os perfis de projeto permitem flexibilidade para gerenciar as diferenças entre os ambientes de
desenvolvimento, teste e produção de uma aplicação Spring Boot. Com o uso adequado dos
perfis, é possível manter uma base de código comum e, ao mesmo tempo, garantir que cada
ambiente funcione com as configurações apropriadas sem necessidade de alterações manuais
contínuas nas configurações de desenvolvimento.
225
Docker Compose para Postgresql e pgAdmin
O Docker Compose é uma ferramenta que permite definir e rodar multi-containers Docker.
Vamos utilizar um arquivo [Link] para definir os serviços necessários para nosso
ambiente de desenvolvimento com Postgresql e pgAdmin. Abaixo está o script Docker Compose
explicado:
version : "3.7"
services :
# Postgres Server
pg - docker :
image: postgres :14 - alpine
container_name : dev - postgresql
environment :
POSTGRES_DB : mydatabase
POSTGRES_PASSWORD : 1234567
ports:
- 5433:5432
volumes :
- ./. data/ postgresql /data :/ var/lib/ postgresql /data
networks :
- dev - network
# pgAdmin
pgadmin - docker :
image: dpage / pgadmin4
container_name : dev - pgadmin
environment :
PGADMIN_DEFAULT_EMAIL : me@example .com
PGADMIN_DEFAULT_PASSWORD : 1234567
ports:
- 5050:80
volumes :
- ./. data/ pgadmin :/ var/lib/ pgadmin
depends_on :
- pg - docker
networks :
- dev - network
networks :
dev - network :
driver : bridge
226
– Mapeia a porta 5050 do host para 80 do container, permitindo acesso via navegador.
– Configura volumes para persistir configurações do pgAdmin.
– Depende do serviço pg-docker, garantindo que o Postgres esteja disponível antes do
pgAdmin iniciar.
docker - compose up -d
Este comando baixa as imagens necessárias, cria e inicia os containers em modo desane-
xado.
3. Acessar o pgAdmin:
• Abra um navegador e visite [Link]
• Use o email me@[Link] e a senha 1234567 para entrar.
4. Conectar ao Postgresql pelo pgAdmin:
• Crie uma nova conexão de servidor no pgAdmin.
• No campo hostname, use dev-postgresql, que é o nome do serviço Postgres definido
no Docker Compose.
• A porta será 5432, o nome do usuário padrão é postgres, e a senha é 1234567.
• O nome do banco de dados é mydatabase.
Testando a Configuração
Verifique se consegue conectar ao banco de dados pelo pgAdmin e execute comandos SQL
básicos para testar a conectividade. Se tudo estiver configurado corretamente, você poderá
gerenciar o banco de dados Postgresql através do pgAdmin rodando em containers Docker.
Perfil dev
Vamos começar criando o perfil dev, que será o perfil de projeto que utilizaremos para homologar
localmente a aplicação com o banco de dados Postgresql local.
Dentro da pasta src/main/resources do projeto, crie um arquivo [Link] com
o seguinte conteúdo:
227
[Link]
Repare no padrão de nome do arquivo de configuração do perfil do projeto, que possui o sufixo
dev, conforme o nome do perfil. O próprio framework reconhece o nome do perfil de projeto
pelo padrão de nome do arquivo.
Sobre o código de configuração, as primeiras quatro linhas de configuração estão comentadas
com o símbolo #, pois são configurações para gerar o script SQL, que serão utilizadas mais
adiante.
As três linhas seguintes são configurações de conexão com a base de dados Postgresql, que
definem as URL de conexão, o nome de usuário e a senha. Repare que a porta utilizada na
URL de conexão é a 5433, pois nossa aplicação Spring Boot vai realizar um acesso externo ao
container do servidor do Postgresql.
As três últimas linhas são configurações adicionais do banco de dados Postgresql:
1. [Link]-platform=[Link]
• Esta linha especifica o dialeto do banco de dados que o Hibernate deve usar. Os
dialetos do Hibernate são configurações que permitem que o Hibernate traduza suas
consultas de entidades para o SQL específico que é compreendido pelo banco de
dados em uso, neste caso, o PostgreSQL. O uso do dialeto correto assegura que o
Hibernate possa gerar SQL que é otimizado para o tipo de banco de dados que você
está utilizando.
2. [Link].non_contextual_creation=true
• Esta configuração é um pouco específica e trata de um problema que pode ocorrer
quando o Hibernate tenta lidar com tipos de dados LOB (Large Objects, como BLOB
e CLOB) em um banco de dados PostgreSQL. O parâmetro non_contextual_creation
quando definido como true, instrui o Hibernate a evitar certas operações contextuais
ao criar LOBs. Isso é útil para evitar exceções devido a uma tentativa de acesso a
métodos LOB em um contexto JDBC inadequado.
3. [Link]-auto=none
• Esta propriedade controla o comportamento do Hibernate em relação ao schema do
banco de dados durante a inicialização e o fechamento da aplicação. Os valores co-
muns para essa configuração incluem none, update, create, create-drop, entre outros.
228
O valor none especificado aqui indica que o Hibernate não deve fazer nenhuma alte-
ração no esquema do banco de dados automaticamente. Isso significa que nenhum
esquema será gerado nem modificado ao iniciar ou parar a aplicação, o que é útil
em ambientes de produção onde as alterações de esquema devem ser controladas de
forma mais rigorosa.
Perfil prod
Agora vamos preparar o perfil prod, que será o perfil do projeto para executar em produção,
online na nuvem.
Dentro da pasta src/main/resources do projeto, crie um arquivo [Link]
com o seguinte conteúdo:
[Link]
Repare que, no arquivo de configuração do perfil de produção, nós não utilizamos os valores
reais de conexão ao banco de dados, mas sim variáveis de ambiente ${DB_URL}. ${DB_USERNAME} e
${DB_PASSWORD}. Os valores destas variáveis serão configurados dentro do ambiente de execução
na nuvem na plataforma onde a aplicação será implantada.
Arquivo [Link]
Vamos também criar um arquivo chamado [Link] na pasta raiz do projeto Spring
Boot. Nós vamos criar este arquivo, pois ele é eventualmente necessário por algumas platafor-
mas PaaS. O conteúdo do arquivo deverá ser conforme mostrado a seguir. Você pode mudar a
versão do Java conforme seu projeto:
229
[Link]
cors. origins =${ CORS_ORIGINS :http :// localhost :5173 , http :// localhost :3000}
Neste momento, acesse seu pgAdmin e crie uma base de dados vazia de nome dslist, conforme
especificado na URL de conexão.
Depois disso, execute o projeto Spring Boot, e observe que será criado um script SQL de nome
[Link] na pasta do projeto Spring Boot. O código desse script conterá todos comandos
para criar a base de dados e inserir os dados:
create table tb_belonging ( position integer , game_id bigint not null , list_id
bigint not null , primary key (game_id , list_id ));
create table tb_game ( game_year integer , score float (53) , id bigint generated by
default as identity , genre varchar (255) , img_url varchar (255) ,
long_description TEXT , platforms varchar (255) , short_description TEXT , title
varchar (255) , primary key (id));
create table tb_game_list (id bigint generated by default as identity , name
varchar (255) , primary key (id));
230
short_description , long_description ) VALUES ('Mass Effect Trilogy ', 4.8 ,
2012 , 'Role - playing (RPG), Shooter ', 'XBox , Playstation , PC', 'https :// raw.
githubusercontent .com/ devsuperior /java -spring - dslist /main/ resources /1. png ', '
Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Red Dead Redemption 2', 4.7 ,
2018 , 'Role - playing (RPG), Adventure ', 'XBox , Playstation , PC ', 'https :// raw.
githubusercontent .com/ devsuperior /java -spring - dslist /main/ resources /2. png ', '
Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('The Witcher 3: Wild Hunt ', 4.7 ,
2014 , 'Role - playing (RPG), Adventure ', 'XBox , Playstation , PC', 'https :// raw
. githubusercontent .com/ devsuperior /java -spring - dslist /main/ resources /3. png ',
'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Sekiro : Shadows Die Twice ',
3.8, 2019 , 'Role - playing (RPG), Adventure ', 'XBox , Playstation , PC ', 'https
:// raw. githubusercontent .com/ devsuperior /java -spring - dslist /main/ resources /4.
png ', 'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Ghost of Tsushima ', 4.6 , 2012 ,
'Role - playing (RPG), Adventure ', 'XBox , Playstation , PC ', 'https :// raw.
githubusercontent .com/ devsuperior /java -spring - dslist /main/ resources /5. png ', '
Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Super Mario World ', 4.7 , 1990 ,
'Platform ', 'Super Ness , PC ', 'https :// raw. githubusercontent .com/ devsuperior /
java -spring - dslist /main/ resources /6. png ', 'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Hollow Knight ', 4.6 , 2017 , '
Platform ', 'XBox , Playstation , PC ', 'https :// raw. githubusercontent .com/
devsuperior /java -spring - dslist /main/ resources /7. png ', 'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Ori and the Blind Forest ', 4,
2015 , 'Platform ', 'XBox , Playstation , PC ', 'https :// raw. githubusercontent .com
/ devsuperior /java -spring - dslist /main/ resources /8. png ', 'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Cuphead ', 4.6 , 2017 , 'Platform '
, 'XBox , Playstation , PC ', 'https :// raw. githubusercontent .com/ devsuperior /
java -spring - dslist /main/ resources /9. png ', 'Lorem ipsum ... ');
INSERT INTO tb_game (title , score , game_year , genre , platforms , img_url ,
short_description , long_description ) VALUES ('Sonic CD ', 4, 1993 , 'Platform ',
'Sega CD , PC ', 'https :// raw. githubusercontent .com/ devsuperior /java -spring -
dslist /main/ resources /10. png ', 'Lorem ipsum ... ');
231
Agora é possível executar este script SQL na base de dados dslist. No pgAdmin, clique com
o botão direito na base de dados dslist e selecione “Query tool” para abrir uma janela de
execução de consultas SQL. Copie o script no conteúdo dessa janela e execute o script. Depois
disso, clique novamente com o botão direito na base de dados dslist e selecione “Refresh”. As
tabelas tb_belonging, tb_game e tb_game_list deverão aparecer na base de dados.
Repare que, na consulta utilizamos uma renomeação de campo com a cláusula AS, atribuindo
o nome do campo para year entre crases. Esta renomeação funciona corretamente no banco de
dados H2, porém gerou um erro ao executar no Postgresql.
Sendo assim, foi preciso mudar esta consulta, atribuindo um nome diferente de year, sem crases.
A consulta corrigida ficou como mostrado a seguir:
232
List < GameMinProjection > searchByList (Long listId );
}
Desta forma, durante o processo de homologação manual, pudemos fazer as adequações neces-
sárias para assegurar que todo o sistema executasse corretamente no banco de dados Postgresql.
Após realizar a homologação manual de todos endpoints, deve-se fazer os seguintes passos para
finalizar o procedimento:
1. Voltar o perfil padrão do projeto para test no arquivo [Link]:
cors. origins =${ CORS_ORIGINS :http :// localhost :5173 , http :// localhost :3000}
233
Pré-requisitos e Criação de Conta no Railway
Antes de começarmos, você precisa criar uma conta no Railway. Siga os passos abaixo para
configurar sua conta:
1. Acesse o site: Vá para [Link] e clique em “Start for Free”.
2. Autenticação: Você pode se registrar usando uma conta GitHub, GitLab, ou Bitbucket
para autenticação rápida. Alternativamente, você pode usar um endereço de e-mail.
3. Confirmação de E-mail: Se você se registrar com um e-mail, será necessário confirmar
o endereço através de um link enviado para o seu e-mail antes de prosseguir.
234
Acesso e Gerenciamento
Após o deploy, o Railway fornece uma URL pública onde você pode acessar sua aplicação.
Além disso, você pode gerenciar sua aplicação através do painel do Railway, onde é possível
visualizar logs, reiniciar serviços, e modificar configurações.
Este processo permite que você rapidamente configure, teste e implante aplicações sem a ne-
cessidade de gerenciar a infraestrutura subjacente, tornando o Railway uma opção conveniente
para desenvolvedores que desejam focar mais no desenvolvimento do que na administração do
sistema.
@Configuration
public class WebConfig {
@Bean
WebMvcConfigurer corsConfigurer () {
return new WebMvcConfigurer () {
@Override
public void addCorsMappings ( CorsRegistry registry ) {
registry . addMapping ("/**"). allowedMethods ("*"). allowedOrigins (
corsOrigins );
}
};
}
Agora, salve um novo commit e envie a modificação para o Github. Observe no painel do
projeto no Railway se um novo deploy é automaticamente iniciado.
Se tudo funcionar corretamente, um novo deploy será executado no Railway e a aplicação Spring
Boot estará atualizada conforme as modificações realizadas.
235
6.12 Preparando projeto DSCommerce para o estudo de
caso com Heroku
Nesta seção e nas próximas, vamos realizar um estudo de caso de implantação no Heroku.
Vale ressaltar novamente que, como o Heroku é uma plataforma que exige cartão de crédito
internacional e possui custo, caso você não tenha disponibilidade no momento para realizar o
procedimento na prática, você pode apenas consumir este conteúdo para conhecimento neste
momento.
Para iniciarmos nosso estudo de caso, é preciso que seu projeto DSCommerce esteja preparado
na sua IDE, e devidamente salvo no Github.
Como o projeto DSCommerce já foi largamente abordado neste curso, vamos presumir que você
já tem o projeto preparado no seu computador. Vamos apenas te fazer a recomendação que
você execute o projeto e teste todos endpoints no Postman conforme aprendemos nos capítulos
anteriores. Feito isso, você estará pronto para seguir com o estudo de caso nas seções seguintes.
<dependency >
<groupId >org. postgresql </ groupId >
<artifactId >postgresql </ artifactId >
<scope >runtime </ scope >
</ dependency >
Perfil dev
Em seguida, vamos adicionar o perfil dev ao projeto Spring Boot. Na pasta src/main/resources,
crie o arquivo [Link] com o seguinte conteúdo:
236
Agora, acesse seu pgAdmin e crie uma base de dados local chamada dscommerce conforme o
nome da base de dados especificado na URL de conexão do arquivo [Link].
Mude o perfil padrão de execução da aplicação para dev no arquivo [Link] e
execute o projeto. O projeto neste momento deve executar sem gerar nenhum erro no console
da IDE.
237
2. Provisionar uma Base de Dados PostgreSQL no Projeto Heroku
Para adicionar um banco de dados PostgreSQL ao seu projeto:
1. Acesse o Dashboard do Aplicativo: Vá para o painel de controle do seu aplicativo
no Heroku.
2. Adicionar Add-ons: Clique em “Resources” e no campo de busca sob “Add-ons”, digite
e selecione “Heroku Postgres”.
3. Escolha do Plano: Selecione um plano adequado. Para testes, você pode começar com
o plano gratuito.
4. Provisionamento: Clique em “Provision” para adicionar o PostgreSQL ao seu projeto.
238
2. Instalação: Execute o arquivo baixado e siga as instruções de instalação. Certifique-se
de incluir o Heroku CLI no PATH do sistema.
3. Verificação da Instalação: Abra o terminal (cmd ou PowerShell) e digite heroku --
version para verificar se o CLI foi instalado corretamente.
239
6.17 Implantando a aplicação
Configurar variáveis de ambiente no Heroku
Antes de enviar a aplicação para o Heroku, precisamos primeiro configurar as variáveis de
ambiente do projeto Spring Boot dentro do ambiente de execução do projeto Heroku.
Para isto, acesse a aba Settings do projeto Heroku e, dentro da seção “Config Vars”, configure
cada uma das variáveis de ambiente especificadas no arquivo [Link] do projeto
Spring Boot, bem como alguma eventual variável de ambiente específica do perfil prod que esteja
especificada no arquivo [Link]. Vale ressaltar que, dentre as variáveis de
ambiente que você for configurar na seção “Config Vars” do seu projeto Heroku, o valor da
variável de ambiente APP_PROFILE deve ser prod, para que o projeto no Heroku execute no perfil
prod.
git remote -v
Se tudo estiver correto, você verá duas associações: o remote origin apontando para o reposi-
tório remoto do Github, e o remote heroku apontando para o repositório do Heroku.
Basta agora executar o seguinte comando para enviar o projeto para o Heroku:
240
6.18 Testando a aplicação no Heroku
Agora que a aplicação foi implantada como um projeto Heroku, precisamos testar a aplicação.
Abra o painel do Heroku e acesse sua aplicação. No dashboard da aplicação, haverá um botão
“Open app” no canto da tela. Ao clicar nesse botão, a aplicação Spring Boot no Heroku
será aberta em uma nova aba do navegador. Se a aplicação Spring Boot estiver corretamente
implantada, deverá aparecer a mensagem padrão de erro da aplicação Spring Boot “Whitelabel
Error Page” na tela do navegador. Observe a URL da aplicação Spring Boot na barra de
navegação do navegador.
Uma vez validado no navegador que a aplicação está executando no Heroku, você pode pegar a
URL da aplicação que aparece na barra de navegação do navegador, e usar esse valor de URL
na variável de ambiente host do seu ambiente do Postman.
Depois de atualizar o valor da variável host, faça o teste de cada um dos endpoints para conferir
se a aplicação Heroku está respondendo apropriadamente.
Parabéns! Agora você está com seu projeto backend implantado na nuvem, com a API respon-
dendo às requisições.
241