Construye una API de gestión financiera con Java, Spring Boot, JPA, Hibernate y H2-Parte II

Construye una API de gestión financiera con Java, Spring Boot, JPA, Hibernate y H2-Parte II

Introducción

Este artículo es una continuación del artículo Cómo construir una API de gestión financiera utilizando Java, Spring Boot, JPA, Hibernate y H2 - Parte I. Por lo tanto, como requisito previo e introducción propiamente profunda, te pido que lo leas.

En este artículo, como parte II, se presentará la continuidad del proyecto de la API de gestión financiera a través de la creación de los otros elementos que lo involucran.

Proyecto

Con base en el último commit hecho en el artículo anterior, se crearán todas las capas para los siguientes elementos:

  • Ingreso
  • EstadoIngreso
  • Gasto
  • EstadoGasto
  • Categoría

El proyecto se encuentra en el siguiente repositorio en Github: https://github.com/caiocv18/artigojava

Ingreso

La entidad Ingreso del sistema se utilizará para que el Usuario, creado en el artículo anterior, pueda registrar todo el dinero que entra en el control financiero.

Imagen I - Entidad Ingreso

Para la clase Ingreso, tendremos solo los siguientes atributos:

  • id;
  • título;
  • valor;
  • fecha.

Con los métodos get y set para cada atributo.

Debido a las anotaciones y funciones que se utilizarán, también es necesaria la creación de los métodos equals y hashCode.

Creando la entidad Ingreso

  1. Crea la clase Ingreso dentro del paquete entidades;
  2. Agrega los atributos como privados:
  3. Long id;
  4. String título;
  5. Double valor;
  6. Instant fecha;

3. Crea un constructor vacío;

4. Crea un constructor con todos los atributos;

5. Genera los getters y setters;

6. Genera hashCode y equals (selecciona solo el atributo ID para hacer la comparación);

7. Implementa la interfaz Serializable;

8. Agrega el número de serie predeterminado sugerido por el propio IntelliJ para Serializable;

9. Agrega @Entity encima de la declaración de la clase;

10. Agrega @Table(name = "tb_receita") justo debajo de @Entity;

11. Agrega @Id justo encima de la declaración del atributo id;

12. Agrega @GeneratedValue(strategy = GenerationType.IDENTITY) justo debajo de @Id.

Gasto

La entidad Gasto del sistema se utilizará para que el Usuario, creado en el artículo anterior, pueda registrar todo el dinero que sale en el control financiero.

Imagen II - Entidad Gasto

Para la clase Gasto, tendremos solo los siguientes atributos:

  • id;
  • título;
  • valor;
  • fecha.

Con los métodos get y set para cada atributo.

Debido a las anotaciones y funciones que se utilizarán, también es necesaria la creación de los métodos equals y hashCode.

Creando la entidad Gasto

  1. Crea la clase Gasto dentro del paquete entidades;
  2. Agrega los atributos como privados:
  3. Long id;
  4. String titulo;
  5. Double valor;
  6. Instant fecha;

3. Crea un constructor vacío;

4. Crea un constructor con todos los atributos;

5. Genera los getters y setters;

6. Genera hashCode y equals (selecciona solo el atributo ID para hacer la comparación);

7. Implementa la interfaz Serializable;

8. Adicione o número de série padrão sugerido pelo próprio IntelliJ para o Serializable;

9. Agrega @Entity encima de la declaración de la clase;

10. Agrega @Table(name = "tb_despesa") justo debajo de @Entity;

11. Agrega @Id justo encima de la declaración del atributo id;

12. Agrega @GeneratedValue(strategy = GenerationType.IDENTITY) justo debajo de @Id.

Relaciones entre entidades

Un punto muy importante en la arquitectura del proyecto es cómo se relacionan los objetos entre sí. En este proyecto, cada usuario puede agregar cualquier número de ingresos y gastos que desee, lo que nos lleva a una relación 1 → N, siendo un usuario agregando N ingresos o gastos

N simboliza que el usuario puede agregar uno, ninguno o varios ingresos/gastos.

Conforme previsto en el diagrama:

Imagen III - Relaciones entre las entidades Usuario, Ingreso y Gasto

Agregando relación entre Ingreso y el Usuario

  1. Accede a la clase Ingreso;
  2. Agrega el atributo Usuario:
private Usuario usuario;

3. Agrega la anotación @ManyToOne;

ℹ️ Anotación JPA para informar que la clase en cuestión puede tener muchos registros relacionados a un único registro del atributo especificado.

4. Agrega la anotación @JoinColumn(name = "usuario_id")

ℹ️ Informa qué columna debe utilizarse para hacer la unión que asocia ambos elementos involucrados.

5. Genera el getter y setter para el nuevo atributo Usuario.

Agregando relación entre Gasto y el Usuario

  1. Accede a la clase Gasto;

2. Agrega el atributo Usuario:


private Usuario usuario;

3. Agrega la anotación @ManyToOne;

ℹ️ Anotación JPA para informar que la clase en cuestión puede tener muchos registros relacionados a un único registro del atributo especificado.

4. Agrega la anotación @JoinColumn(name = "usuario_id")

ℹ️ Informa qué columna debe utilizarse para hacer la unión que asocia ambos elementos involucrados.

5. Genera el getter y setter para el nuevo atributo Usuario.

Agregando relación entre Usuario e Ingreso/Gasto

  1. Accede a la clase Usuario;
  2. Agrega el atributo de lista de Ingreso:
private List<Ingreso> ingresos = new ArrayList<>();

3. Agrega la anotación @OneToMany(mappedBy = "usuario") encima del atributo que se creó en el paso anterior;

ℹ️ Anotación JPA para informar que un registro de la clase en cuestión puede tener varios registros del atributo especificado.

4. Agrega el atributo de lista de Gasto:

private List<Gasto> gastos = new ArrayList<>();

5. Agrega la anotación @OneToMany(mappedBy = "usuario") encima del atributo que se creó en el paso anterior;

ℹ️ La anotación es exactamente igual debido al hecho de que son dos entidades diferentes relacionadas con la misma entidad Usuario.

6. Genera los getters y setters para los dos nuevos atributos de listas.

De esta manera, al ejecutar nuevamente la aplicación y entrar en la consola de la base de datos H2, será posible visualizar las dos nuevas tablas creadas para Ingreso y Gasto.

GIF I - Visualizando las tablas Ingreso y Gasto en la consola de H2

ℹ️ Enlace a mi commit relacionado con los pasos realizados anteriormente:

Ingreso y Gasto en las otras capas de la aplicación

Muchos de los conceptos relacionados con las capas de servicio, repositorio y recurso ya fueron explicados y puestos en práctica en la primera parte de este artículo, por lo tanto, seguiremos el mismo modelo, copiando las clases creadas para la entidad Usuario.

Repositorio

  1. Basándose en la clase UsuarioRepositorio, crear la clase ReceitaRepositorio;
  2. Basándose en la clase UsuarioRepositorio, crear la clase DespesaRepositorio;

Servicio

  1. Basándose en la clase UsuarioService, crear la clase ReceitaService;
  2. Basándose en la clase UsuarioService, crear la clase DepesaService;

Recurso

  1. Basándose en la clase UsuarioRecurso, crear la clase ReceitaRecurso;
  2. Basándose en la clase UsuarioRecurso, crear la clase DespesaRecurso;

ℹ️ De hecho no hay diferencia entre lo que se implementó para la entidad Usuario y las entidades Ingreso y Gasto, solo es necesario cambiar en cada clase todo lo relacionado a Usuario.

Datos de prueba

Para poder visualizar de forma “mockada” algunos ingresos y gastos, es necesario instanciar algunos datos en el archivo de configuración de prueba:

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 correcciones

Para una mejor visualización de la fecha en formato JSON cuando se haga la solicitud en /ingresos o /gastos, es necesario agregar una anotación responsable de formatear la información de la fecha, quedando de la siguiente manera:

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "GMT-3")

private Instant data;

Al utilizar la anotación @ManyToOne y relacionar Ingreso/Gasto con Usuario, creamos una relación doble, donde Ingreso/Gasto apunta al Usuario y Usuario también apunta a Ingreso/Gasto. Esta relación de ida y vuelta causa una visualización confusa al formar el JSON, pues hace que entre en un bucle. Usuario “llama” los datos de Ingreso/Gasto que también “llama” los datos del Usuario.

Para resolver este problema, necesitamos elegir uno de los lados para ignorar el “llamado”. En este caso, sugiero hacer que Ingreso/Gasto ignore la etapa de “llamar” nuevamente a la entidad Usuario utilizando @JsonIgnore de la siguiente manera:

@ManyToOne

@JoinColumn(name = "usuario_id")

@JsonIgnore

private Usuario usuario;

De esta manera, al ejecutar nuevamente la aplicación, será posible hacer una solicitud a /ingresos y a /gastos, de forma que el JSON de retorno ya esté siendo generado de la mejor manera:

GIF II - Visualizando las solicitudes de /ingresos y /gastos

ℹ️ Enlace a mi commit relacionado con los pasos realizados anteriormente: Criando Serviços, Repositórios e Recursos para as entidades …7eed1c4

Creación de Estado

Para cada tipo de registro en el sistema, ya sea un Ingreso o un Gasto, también será posible elegir un estado para cada uno. Por lo tanto, es necesario crear estas nuevas entidades conforme presentado anteriormente en el flujo y hacer las relaciones entre sus respectivas entidades.

Imagen IV - Relación entre Ingreso y EstadoIngreso e Imagen V - Relación entre Gasto y EstadoGasto

Conforme ilustrado, la relación entre las entidades es de n a 1, siendo: para una cantidad n de Ingresos/Gastos, cada uno solo podrá tener 1 estado. El contrario siendo que 1 estado puede estar en n Ingresos/Gastos.

Creando ReceitaStatus y DespesaStatus

  1. Crea un paquete para enums;
  2. Crea un Enum dentro del paquete con el nombre ReceitaStatus;

  1. Utiliza los siguientes estados:

  1. RECIBIDA
  2. PENDIENTE
  3. PROGRAMADA
  4. ATRASADA

2. Crea un atributo llamado código;

3. Crea un constructor que utiliza el código como parámetro en la construcción;

3. Crea un getter para el código;

4. Crea una función estática llamada valorDoCodigo que devuelve el Enum del código pasado 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 el status de una Receita")

}

5. rea un Enum dentro del paquete con el nombre DespesaStatus;

  1. Utiliza los siguientes estados:

  1. PAGADA
  2. PENDIENTE
  3. PROGRAMADA
  4. ATRASADA

2. Crea un atributo llamado código;

3. Crea un constructor que utiliza el código como parámetro en la construcción;

6.  Añade el atributo utilizando el Enum en cada clase Receita y Despesa;

7. Genera los getters y setters;

8. Utiliza la lógica de valorDoCodigo dentro de cada getter y setter;

9. Modifica las declaraciones de las instancias que se utilizan en TesteConfiguracao.

ℹ️ Enlace a mi commit relacionado con los pasos realizados anteriormente: Añadiendo estados para Receitas y Despesasa2f16b8

Creación de la Categoría y relación con Ingreso / Gasto

La entidad Categoría sirve para que podamos clasificar gastos e ingresos en grupos de temas como "Combustible", "Alimentación", "Trabajo" y "Cuentas mensuales"

Conforme al diagrama, la entidad Categoría se relaciona con Ingreso y Gasto donde la relación es de 1 a N (una categoría puede ser utilizada en N Ingresos / Gastos). Tenemos solo una entidad Categoría para ser utilizada en ambos tipos de transacción, siendo diferente de lo que teníamos anteriormente para los Estados.

Imagen VI - Relación entre Categoría y Ingreso y Gasto

Creando la entidad Categoría

  1. Crea la entidad Categoría dentro del paquete de entidades
  2. Coloca los siguientes atributos y respectivos tipos de datos:

Long id;

String titulo;

3. Crea un constructor vacío

4. Crea un constructor con todos los atributos

5. Genera los Getters y Setters

6. Genera las funciones HashCode y equals utilizando el atributo id

7. Añade la interfaz Serializable

8. Genera un SerialId

9. Añade la anotación @Entity

10. Añade la anotación@Table(name = "tb_categoria")

Repositorio

Basándose en la clase UsuarioRepositorio, crear la clase CategoriaRepositorio;

Servicio

Basándose en la clase UsuarioService, crear la clase CategoriaService;

Recurso

Basándose en la clase UsuarioRecurso, crear la clase CategoriaRecurso;

Añadiendo la relación con Ingreso y Gasto

  1. Dentro de la clase Categoría:

  1. Crea un atributo del tipo List para Ingreso;
  2. Crea un atributo del tipo List para Gasto;
  3. Añade en ambos la anotación @OneToMany;
  4. Añade en ambos la anotación @JsonIgnore;
  5. Genera métodos Get para ambos

2. Quedando la clase de la siguiente forma:

@OneToMany(mappedBy = "categoriaIngreso")

@JsonIgnore

private List<Receita> receitas = new ArrayList<>();

@OneToMany(mappedBy = "categoriaDespesa")

@JsonIgnore

private List<Despesa> despesas = new ArrayList<>();

...

public List<Ingreso> getIngreso() {

return receitas;

}

public List<Despesa> getDespesas() {

return despesas;

}

...

2. Dentro de la clase Ingreso:

  1. Crea el atributo categoriaIngreso del tipo Categoría;
  2. Añade la anotación @ManyToOne;
  3. Añade la anotación @JoinColumn(name = "categoria_receita_id");
  4. Añade el nuevo atributo categoriaIngreso al constructor;
  5. Genera los métodos Get y Set para el atributo categoriaIngreso.

3. Quedando la clase de la siguiente forma:

@ManyToOne

@JoinColumn(name = "categoria_receita_id")

private Categoria categoriaIngreso;

...

public Ingreso(String titulo, Double valor, Instant data, IngresosStatus status, Usuario usuario, Categoria categoriaReceita) {

super();

this.titulo = titulo;

this.valor = valor;

this.fecha = fecha;

setStatus(status);

this.usuario = usuario;

this.categoriaIngreso = categoriaIngreso;

}

...

public Categoria getCategoriaIngreso() {

return categoriaIngreso;

}

public void setCategoriaIngreso(Categoria categoriaIngreso) {

this.categoriaIngreso = categoriaIngreso;

}

Datos de prueba

Para poder visualizar de forma “mockeada” algunas categorías, así como se hizo con las otras entidades, es necesario instanciar algunos datos en el archivo de configuración de prueba:

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("Combustible");

Categoria categoria2 = new Categoria("Alimentación");

Categoria categoria3 = new Categoria("Trabajo");

Categoria categoria4 = new Categoria("Cuentas mensuales");

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));

}

}

De esta manera, al ejecutar nuevamente la aplicación, será posible hacer una solicitud a /categorias, pudiendo ver la relación con cada Receita y Despesa también a través de las solicitudes en /receitas y /despesas:

ℹ️ Enlace a mis commits relacionados con los pasos realizados anteriormente: 1 - Adicionando a entidade Categoriacf3ef48 2 - Adicionando a relação entre Despesa e Categoriaeac0a46 3 - Adicionando a relação entre Receita e Categoriac652d75

Conclusión

Recordando, este artículo es una continuación de la parte I, que es fundamental para la comprensión total. En caso de no haberla visto anteriormente, aquí está el enlace:

Cómo construir una API de gestión financiera utilizando Java, Spring Boot, JPA, Hibernate e H2 - Parte I

Después de algunos párrafos más, commits, conceptos, la implementación de nuevas entidades en todas las capas del sistema y de aproximadamente 30 minutos de lectura estimada, es un buen momento para digerir con calma toda la información que se ha presentado y verificar si realmente fue posible consolidar todos los conocimientos, además de ponerlos en práctica en algún otro proyecto.

Con toda certeza, el sistema aún no está listo, a pesar de ya estar todo diseñado de acuerdo con el diagrama, teniendo todas las respectivas relaciones entre entidades.

El artículo, con la implementación de las demás funcionalidades y los avances en otros conceptos, continúa en la parte III:

Cómo construir una API de gestión financiera utilizando Java, Spring Boot, JPA, Hibernate e H2 - Parte III

Después de la presentación de tantos conceptos también puestos en práctica en este artículo, la parte III será la última y estará enfocada en la implementación del resto de las letras del CRUD, para que sea posible añadir, actualizar y eliminar cada uno de los elementos del sistema y poner la aplicación en un ambiente alojado en la nube utilizando Heroku.

¡Gracias por llegar hasta aquí y te deseo éxito en esta nueva aventura utilizando Java, Spring, JPA, Hibernate y H2!

Te espero en la parte III para continuar la implementación del sistema de la API de gestión financiera.

Aquí están mis datos de contacto y mi sitio web en caso de alguna duda, necesidad o sugerencia:

Referencias

  1. Curso de Nélio Alves con diversos proyectos utilizando Java disponible en Udemy
    Java COMPLETO 2023 Programação Orientada a Objetos +Projetos
  2. JPA - Muchos-a-Uno (ManyToOne)
    JPA - Muitos-para-Um (ManyToOne)
  3. @JoinColumn Annotation Explained
    @JoinColumn Annotation Explained | Baeldung

4. Hibernate One to Many Annotation Tutorial
Hibernate One to Many Annotation Tutorial | Baeldung

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.