Como construir uma API de gestão financeira com Java, Spring Boot, JPA, Hibernate e H2 - Parte II
Introdução
Este artigo é uma continuação do artigo Como construir uma API de gestão financeira utilizando Java, Spring Boot, JPA, Hibernate e H2 - Parte I. Portanto, como pré-requisito e introdução propriamente aprofundada, peço que o leia.
Nesse artigo, como parte II, será apresentada a continuidade do projeto da API de gestão financeira através da criação dos outros elementos que o envolvem.
Projeto
Com base no último commit feito no artigo anterior, serão criadas todas as camadas para os elementos a seguir:
- Receita
- ReceitaStatus
- Despesa
- DespesaStatus
- Categoria
O projeto se encontra no seguinte repositório no Github: https://github.com/caiocv18/artigojava
Receita
A entidade Receita do sistema será utilizada para que o Usuario, criado no artigo anterior, consiga registrar todo dinheiro que entra no controle financeiro.
Para a classe Receita, teremos apenas os atributos a seguir:
- id;
- titulo;
- valor;
- data.
Com os métodos get e set para cada atributo.
Devido às anotações e funções que serão utilizadas, também se faz necessária a criação dos métodos equals e hashCode.
Criando a entidade Receita
- Crie a classe Receita dentro do pacote entidades;
- Adicione os atributos como private:
- Long id;
- String titulo;
- Double valor;
- Instant data;
3. Crie um construtor vazio;
4. Crie um construtor com todos os atributos;
5. Gere os getters e setters;
6. Gere hashcode e equals (selecionar apenas o atributo de ID para fazer a comparação);
7. Implemente a interface Serializable;
8. Adicione o número de série padrão sugerido pelo próprio IntelliJ para o Serializable;
9. Adicione @Entity acima da declaração da classe;
10. Adicione @Table(name = "tb_receita") logo abaixo de @Entity;
11. Adicione @Id logo acima da declaração do atributo id;
12. Adicione @GeneratedValue(strategy = GenerationType.IDENTITY) logo abaixo de @Id.
Despesa
A entidade Despesa do sistema será utilizada para que o Usuario, criado no artigo anterior, consiga registrar todo dinheiro que sai no controle financeiro.
Para a classe Despesa, teremos apenas os atributos a seguir:
- id;
- titulo;
- valor;
- data.
Com os métodos get e set para cada atributo.
Devido às anotações e funções que serão utilizadas, também se faz necessária a criação dos métodos equals e hashCode.
Criando a entidade Despesa
- Crie a classe Despesa dentro do pacote entidades;
- Adicione os atributos como private:
- Long id;
- String titulo;
- Double valor;
- Instant data;
3. Crie um construtor vazio;
4. Crie um construtor com todos os atributos;
5. Gere os getters e setters;
6. Gere hashcode e equals (selecione apenas o atributo de ID para fazer a comparação);
7. Implementer a interface Serializable;
8. Adicione o número de série padrão sugerido pelo próprio IntelliJ para o Serializable;
9. Adicione @Entity acima da declaração da classe;
10. Adicione @Table(name = "tb_despesa") logo abaixo de @Entity;
11. Adicione @Id logo acima da declaração do atributo id;
12. Adicione @GeneratedValue(strategy = GenerationType.IDENTITY) logo abaixo de @Id.
Relações entre entidades
Um ponto muito importante na arquitetura do projeto, é como os objetos se relacionam entre si. Neste projeto, cada usuário pode adicionar qualquer número que ele desejar de receitas e de despesas, portanto, isso nos leva a uma relação 1 → N, sendo um usuário adicionando N receitas ou despesas.
N simboliza que o usuário pode adicionar uma, nenhuma ou várias receitas/despesas.
Conforme previsto no diagrama:
Adicionando relação entre Receita e o Usuário
- Acesse a classe Receita;
- Adicione o atributo Usuario:
private Usuario usuario;
3. Adicione a anotação @ManyToOne;
ℹ️ Anotação JPA para informar que a classe em questão pode possuir muitos registros relacionados a um único registro do atributo especificado.
4. Adicione a anotação @JoinColumn(name = "usuario_id")
ℹ️ Informa qual coluna deve ser utilizada para fazer a junção que associa ambos os elementos envolvidos.
5. Gere o getter e setter para o novo atributo Usuario.
Adicionando relação entre Despesa e o Usuário
- Acesse a classe Despesa;
2. Adicione o atributo Usuario:
private Usuario usuario;
3. Adicione a anotação @ManyToOne;
ℹ️ Anotação JPA para informar que a classe em questão pode possuir muitos registros relacionados a um único registro do atributo especificado.
4. Adicione a anotação @JoinColumn(name = "usuario_id")
ℹ️ Informa qual coluna deve ser utilizada para fazer a junção que associa ambos os elementos envolvidos.
5. Gere o getter e setter para o novo atributo Usuario.
Adicionando relação entre Usuário e Despesa/Receita
- Acesse a classe Usuario;
- Adicione o atributo de lista de Receita:
private List<Receita> receitas = new ArrayList<>();
3. Adicione a anotação @OneToMany(mappedBy = "usuario") acima do atributo que foi criado no passo anterior;
ℹ️ Anotação JPA para informar que um registro da classe em questão pode possuir vários registros do atributo especificado.
4. Adicione o atributo de lista de Despesa:
private List<Despesa> receitas = new ArrayList<>();
5. Adicione a anotação @OneToMany(mappedBy = "usuario") acima do atributo que foi criado no passo anterior;
ℹ️ A anotação é exatamente igual devido ao fato de serem duas entidades diferentes relacionada à mesma entidade Usuario
6. Gere os getters e setters para os dois novos atributos de listas.
Sendo assim, ao executar novamente a aplicação e entrar no console do banco de dados H2, será possível visualizar as duas novas tabelas criadas para Receita e Despesa.
ℹ️ Link do meu commit relacionado aos passos realizados acima:
Receita e Despesa nas outras camadas de aplicação
Muitos dos conceitos envolvendo as camadas de serviço, repositório e recurso já foram explicadas e colocadas em prática na primeira parte deste artigo, portanto, seguiremos o mesmo modelo, copiando as classes criadas para a entidade Usuário.
Repositório
- Com base na classe UsuarioRepositorio, criar a classe ReceitaRepositorio;
- Com base na classe UsuarioRepositorio, criar a classe DespesaRepositorio;
Serviço
- Com base na classe UsuarioService, criar a classe ReceitaService;
- Com base na classe UsuarioService, criar a classe DepesaService;
Recurso
- Com base na classe UsuarioRecurso, criar a classe ReceitaRecurso;
- Com base na classe UsuarioRecurso, criar a classe DespesaRecurso;
ℹ️ De fato não há diferença entre o que foi implementado para a entidade Usuario e as entidades Receita e Despesa, basta apenas alterar em cada classe tudo que diz respeito a Usuario.
Dados de teste
Para que seja possível visualizar de forma “mockada” algumas receitas e despesas, é necessário instanciar alguns dados no arquivo de configuração de teste:
public class TesteConfiguracao implements CommandLineRunner {
@Autowired
private UsuarioRepositorio usuarioRepositorio;
@Autowired
private ReceitaRepositorio receitaRepositorio;
@Autowired
private DespesaRepositorio despesaRepositorio;
@Override
public void run(String... args) throws Exception {
Usuario usuario1 = new Usuario("Caio", "caio@gmail.com", "123456");
Usuario usuario2 = new Usuario("Vinicius", "vinicius@gmail.com" ,"123456");
Receita receita1 = new Receita("Salário", 2900.00, Instant.now(), usuario1);
Receita receita2 = new Receita("Salário", 4000.00, Instant.now(), usuario2);
Receita receita3 = new Receita("Bônus", 500.00, Instant.now(), usuario1);
Despesa despesa1 = new Despesa("Gasolina", 50.00, Instant.now(), usuario1);
Despesa despesa2 = new Despesa("Conta de luz", 150.00, Instant.now(), usuario2);
Despesa despesa3 = new Despesa("Almoço", 25.00, Instant.now(), usuario1);
usuarioRepositorio.saveAll(Arrays.asList(usuario1,usuario2));
receitaRepositorio.saveAll(Arrays.asList(receita1,receita2,receita3));
despesaRepositorio.saveAll(Arrays.asList(despesa1,despesa2,despesa3));
}
}
Últimas correções
Visando uma melhor visualização da data no formato JSON quando for feita a requisição no /receitas ou no /despesas, é necessário adicionar uma anotação responsável por formatar a informação da data, ficando da seguinte forma:
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "GMT-3")
private Instant data;
Ao utilizar a anotação de @ManyToOne e relacionar Receita/Despesa com Usuario, criamos uma relação dupla, onde Receita/Despesa aponta para o Usuario e Usuario também aponta para Receita/Despesa. Essa relação de ida e volta causa uma visualização confusa ao formar o JSON, pois faz com que entre em loop. Usuario “chama” os dados de Receita/Despesa que também “chama” o os dados do Usuario.
Para resolver este problema, precisamos escolher um dos lados para ignorar o “chamamento”. Neste caso, indico fazer com que Receita/Despesa ignore a etapa de “chamar” novamente a entidade Usuario utilizando @JsonIgnore da seguinte forma:
@ManyToOne
@JoinColumn(name = "usuario_id")
@JsonIgnore
private Usuario usuario;
Sendo assim, ao executar novamente a aplicação, será possível fazer uma requisição para o /receitas e para o /despesas, de forma que o JSON de retorno já está sendo feito da melhor forma:
ℹ️ Link do meu commit relacionado aos passos realizados acima: Criando Serviços, Repositórios e Recursos para as entidades …7eed1c4
Criação de Status
Para cada tipo de cadastro no sistema, sendo uma Receita ou Despesa, também será possível escolher um status para cada um. Portanto, é necessário criar essas novas entidades conforme apresentado anteriormente no fluxo e fazer as relações entre suas respectivas entidades.
Conforme ilustrado, a relação entre as entidades é de n para 1, sendo: para uma quantidade n de Receitas/Despesas, cada uma só poderá ter 1 status. O contrário sendo que 1 status pode estar em n Receitas/Despesas.
Criando ReceitaStatus e DespesaStatus
- Crie um pacote para enums;
- Crie um Enum dentro do pacote com o nome ReceitaStatus;
- Utilize os seguintes status:
- RECEBIDA
- PENDENTE
- AGENDADA
- ATRASADA
2. Crie um atributo chamado código;
3. Crie um construtor que utiliza o código como parâmetro na construção;
3. Crie um get para o código
4. Crie uma função estática chamada valorDoCodigo que retorna o Enum do código passado como parâmetro;
public static ReceitaStatus valorDoCodigo(int codigo){
for (ReceitaStatus valor : ReceitaStatus.values()){
if (value.getCodigo() == codigo){
return valor
}
}
throw new IllegalArgumentException("Código inválido para o status de uma Receita")
}
5. Crie um Enum dentro do pacote com o nome DespesaStatus
- Utilize os seguintes status:
- PAGA
- PENDENTE
- AGENDADA
- ATRASADA
2. Crie um atributo chamado código;
3. Crie um construtor que utiliza o código como parâmetro na construção;
6. Adicione o atributo utilizando o Enum em cada classe Receita e Despesa;
7. Gere os getters e setters;
8. Utilize a lógica de valorDoCodigo dentro de cada get e set;
9. Alterar as declarações das instâncias que são utilizadas no TesteConfiguracao.
ℹ️ Link do meu commit relacionado aos passos realizados acima: Adicionando status para Receitas e Despesasa2f16b8
Criação da Categoria e relação com Receita / Despesa
A entidade Categoria serve para que possamos classificar despesas e receitas em grupos de assunto como “Combustível”, “Alimentação”, “Trabalho” e “Contas mensais”.
Conforme previsto no diagrama, a entidade Categoria se relaciona com Receita e Despesa onde a relação é de 1 para N (uma categoria pode ser utilizada em N Receita / Despesa). Temos apenas uma entidade Categoria para ser utilizado em ambos os tipos de transação, sendo diferente do que tínhamos anteriormente para os Status.
Criando a entidade Categoria
- Crie a entidade Categoria dentro do pacote de entidades
- Coloque os seguinte atributos e respectivos tipos de dados:
Long id;
String titulo;
3. Crie um construtor vazio
4. Crie um construtor com todos os atributos
5. Gere os Getters e setters
6. Gere as funções Hashcode e equals utilizando o atributo id
7. Adicione a interface Serializable
8. Gere um SerialId
9. Adicione a anotação @Entity
10. Adicione a anotação@Table(name = "tb_categoria")
Repositório
Com base na classe UsuarioRepositorio, criar a classe CategoriaRepositorio;
Serviço
Com base na classe UsuarioService, criar a classe CategoriaService;
Recurso
Com base na classe UsuarioRecurso, criar a classe CategoriaRecurso;
Adicionando a relação com Receita e Despesa
- Dentro da Classe categoria:
- Crie um atributo do tipo List para Receita;
- Crie um atributo do tipo List para Despesa;
- Adicione em ambos a anotação @OneToMany;
- Adicione em ambos a anotação @JsonIgnore;
- Gere método Get para ambos
2. Ficando com a classe da seguinte forma:
@OneToMany(mappedBy = "categoriaReceita")
@JsonIgnore
private List<Receita> receitas = new ArrayList<>();
@OneToMany(mappedBy = "categoriaDespesa")
@JsonIgnore
private List<Despesa> despesas = new ArrayList<>();
...
public List<Receita> getReceitas() {
return receitas;
}
public List<Despesa> getDespesas() {
return despesas;
}
...
2. Dentro da classe Receita:
- Crie o atributo categoriaReceita do tipo Categoria;
- Adicione a anotação @ManyToOne;
- Adicione a anotação @JoinColumn(name = "categoria_receita_id");
- Adicione o novo atributo categoriaReceita ao construtor;
- Gere os métodos Get e Set para o atributo categoriaReceita.
3. Ficando com a classe da seguinte forma:
@ManyToOne
@JoinColumn(name = "categoria_receita_id")
private Categoria categoriaReceita;
...
public Receita(String titulo, Double valor, Instant data, ReceitaStatus status, Usuario usuario, Categoria categoriaReceita) {
super();
this.titulo = titulo;
this.valor = valor;
this.data = data;
setStatus(status);
this.usuario = usuario;
this.categoriaReceita = categoriaReceita;
}
...
public Categoria getCategoriaReceita() {
return categoriaReceita;
}
public void setCategoriaReceita(Categoria categoriaReceita) {
this.categoriaReceita = categoriaReceita;
}
Dados de teste
Para que seja possível visualizar de forma “mockada” algumas categorias, assim como foi feito para as outras entidades, é necessário instanciar alguns dados no arquivo de configuração de teste:
public class TesteConfiguracao implements CommandLineRunner {
@Autowired
private UsuarioRepositorio usuarioRepositorio;
@Autowired
private ReceitaRepositorio receitaRepositorio;
@Autowired
private DespesaRepositorio despesaRepositorio;
@Override
public void run(String... args) throws Exception {
Usuario usuario1 = new Usuario("Caio", "caio@gmail.com", "123456");
Usuario usuario2 = new Usuario("Vinicius", "vinicius@gmail.com" ,"123456");
Categoria categoria1 = new Categoria("Combustível");
Categoria categoria2 = new Categoria("Alimentação");
Categoria categoria3 = new Categoria("Trabalho");
Categoria categoria4 = new Categoria("Contas mensais");
categoriaRepositorio.saveAll(Arrays.asList(categoria1, categoria2, categoria3, categoria4));
Receita receita1 = new Receita("Salário", 2900.00, Instant.now(), usuario1, categoria3);
Receita receita2 = new Receita("Salário", 4000.00, Instant.now(), usuario2, categoria3);
Receita receita3 = new Receita("Bônus", 500.00, Instant.now(), usuario1, categoria3);
Despesa despesa1 = new Despesa("Gasolina", 50.00, Instant.now(), usuario1, categoria1);
Despesa despesa2 = new Despesa("Conta de luz", 150.00, Instant.now(), usuario2, categoria4);
Despesa despesa3 = new Despesa("Almoço", 25.00, Instant.now(), usuario1, categoria2);
usuarioRepositorio.saveAll(Arrays.asList(usuario1,usuario2));
receitaRepositorio.saveAll(Arrays.asList(receita1,receita2,receita3));
despesaRepositorio.saveAll(Arrays.asList(despesa1,despesa2,despesa3));
}
}
Sendo assim, ao executar novamente a aplicação, será possível fazer uma requisição para o /categorias, podendo ver a relação com cada Receita e Despesa também pelas requisições no /receitas e /despesas:
ℹ️ Link dos meus commits relacionados aos passos realizados acima: 1 - Adicionando a entidade Categoriacf3ef48 2 - Adicionando a relação entre Despesa e Categoriaeac0a46 3 - Adicionando a relação entre Receita e Categoriac652d75
Conclusão
Lembrando, este artigo é uma continuação da parte I, que é fundamental para o entendimento total, caso não tenha visualizado anteriormente, segue o link:
Depois de mais alguns parágrafos, commits, conceitos, da implementação de novas entidades em todas as camadas do sistema e de cerca de mais 30 minutos de leitura estimados, é um bom momento para digerir com calma todas as informações que foram apresentadas e checar se realmente foi possível consolidar todos os ensinamentos, além de colocá-los em prática em algum outro projeto.
Com toda certeza, o sistema ainda não está pronto, apesar de já estar todo desenhado de acordo com o diagrama, tendo todas as respectivas relações entre entidades.
O artigo, com a implementação das demais funcionalidades e os avanços em outros conceitos continua na parte III:
Após a apresentação de tantos conceitos também colocados em prática neste artigo, a parte III será a última e estará voltada para a implementação do restante das letras do CRUD, para que seja possível adicionar, atualizar e deletar cada um dos elementos do sistema e de colocar a aplicação em um ambiente hospedado na nuvem utilizando o Heroku.
Agradeço por chegar até aqui e te desejo sucesso nessa nova aventura utilizando Java, Spring, JPA, Hibernate e H2!
Te espero na parte III para continuarmos a implementação do sistema da API de gestão financeira.
Segue meu contato e meu site em caso de alguma dúvida, necessidade ou sugestão:
- caioviniciuscv18@gmail.com
- https://caiocv18.notion.site/
Referências
- Curso do Nélio Alves com diversos projetos utilizando Java disponível na Udemy
Java COMPLETO 2023 Programação Orientada a Objetos +Projetos - JPA - Muitos-para-Um (ManyToOne)
JPA - Muitos-para-Um (ManyToOne) - @JoinColumn Annotation Explained
@JoinColumn Annotation Explained | Baeldung
4. Hibernate One to Many Annotation Tutorial
Hibernate One to Many Annotation Tutorial | Baeldung
A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.