En el mapeo de relaciones entre entidades en JPA (Java Persistence API), se utilizan varias anotaciones para establecer las relaciones entre las clases que representan entidades en una base de datos relacional. Estas anotaciones permiten definir relaciones:
Uno a muchos y muchos a uno
Uno a uno
Muchos a muchos
Creemos un proyecto nuevo para probar el mapeo de relaciones.
Creamos el proyecto llamado: proyecto009. Agregando las dependencias de Spring Data JPA, MySQL Driver y Spring Web.
Ahora configuramos el acceso a la base de datos 'bd1' en el archivo 'application.properties':
spring.application.name=proyecto009 spring.datasource.url=jdbc:mysql://localhost:3306/bd1?useSSL=false spring.datasource.username=root spring.datasource.password=123456 spring.jpa.hibernate.ddl-auto=update
Creamos las carpetas model, repository, service y controller.
En la carpeta model creamos las dos entidades (2 clases)
Rubro.java
package com.example.demo.model; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; @Entity @Table(name = "rubros") public class Rubro { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long codigo; private String descripcion; @OneToMany(mappedBy = "rubro", cascade = CascadeType.ALL) @JsonIgnore private List<Articulo> articulos; public Rubro() { } public Long getCodigo() { return codigo; } public void setCodigo(Long codigo) { this.codigo = codigo; } public String getDescripcion() { return descripcion; } public void setDescripcion(String descripcion) { this.descripcion = descripcion; } public List<Articulo> getArticulos() { return articulos; } public void setArticulos(List<Articulo> articulos) { this.articulos = articulos; } }
Articulo.java
package com.example.demo.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @Entity @Table(name = "articulos") public class Articulo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long codigo; private String descripcion; private double precio; @ManyToOne @JoinColumn(name = "rubro_codigo", referencedColumnName = "codigo") private Rubro rubro; public Articulo() { } public Long getCodigo() { return codigo; } public void setCodigo(Long codigo) { this.codigo = codigo; } public String getDescripcion() { return descripcion; } public void setDescripcion(String descripcion) { this.descripcion = descripcion; } public double getPrecio() { return precio; } public void setPrecio(double precio) { this.precio = precio; } public Rubro getRubro() { return rubro; } public void setRubro(Rubro rubro) { this.rubro = rubro; } }
En la entidad Rubro, hemos utilizado la anotación @OneToMany para indicar que un rubro puede tener muchos artículos. Esta relación se especifica mediante el atributo articulos, que es una lista de objetos Articulo. La anotación @OneToMany toma un parámetro mappedBy, que especifica el nombre del campo en la entidad Articulo que mapea esta relación. En este caso, estamos utilizando el campo rubro en la clase Articulo para mapear la relación.
En la entidad Articulo, hemos utilizado la anotación @ManyToOne para indicar que muchos artículos pertenecen a un solo rubro. La anotación @JoinColumn se utiliza para especificar la columna en la tabla de artículos que contiene la clave externa que hace referencia al código del rubro. El parámetro referencedColumnName especifica el nombre de la columna en la tabla de rubros que es la clave primaria referenciada.
Con esta configuración, hemos establecido una relación uno a muchos entre las entidades Rubro y Artículo. Cada rubro puede tener múltiples artículos, y cada artículo pertenece a un solo rubro. Cuando recuperes un rubro de la base de datos, Spring Data JPA automáticamente cargará todos los artículos asociados a ese rubro. Esta es una forma eficiente de manejar relaciones complejas entre entidades en una aplicación Spring Data JPA.
RubroRepository.java
package com.example.demo.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.example.demo.model.Rubro; public interface RubroRepository extends JpaRepository<Rubro, Long> { }
ArticuloRepository.java
package com.example.demo.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.example.demo.model.Articulo; public interface ArticuloRepository extends JpaRepository<Articulo, Long> { }
En la carpeta 'service' y creamos las clases RubroService y ArticuloService.
RubroService.java
package com.example.demo.service; import org.springframework.stereotype.Service; import java.util.List; import com.example.demo.model.Rubro; import com.example.demo.repository.RubroRepository; @Service public class RubroService { private final RubroRepository rubroRepository; public RubroService(RubroRepository rubroRepository) { this.rubroRepository = rubroRepository; } public List<Rubro> getAllRubros() { return rubroRepository.findAll(); } public Rubro getRubroById(Long id) { return rubroRepository.findById(id).orElse(null); // Devuelve null si el rubro no se encuentra } public Rubro saveRubro(Rubro rubro) { return rubroRepository.save(rubro); } public void deleteRubro(Long id) { rubroRepository.deleteById(id); } }
ArticuloService.java
package com.example.demo.service; import org.springframework.stereotype.Service; import java.util.List; import com.example.demo.model.Articulo; import com.example.demo.repository.ArticuloRepository; @Service public class ArticuloService { private final ArticuloRepository articuloRepository; public ArticuloService(ArticuloRepository articuloRepository) { this.articuloRepository = articuloRepository; } public List<Articulo> getAllArticulos() { return articuloRepository.findAll(); } public Articulo getArticuloById(Long id) { return articuloRepository.findById(id).orElse(null); // Devuelve null si el artículo no se encuentra } public Articulo saveArticulo(Articulo articulo) { return articuloRepository.save(articulo); } public void deleteArticulo(Long id) { articuloRepository.deleteById(id); } }
Por último en la carpeta 'controller' creamos las clases RubroController y ArticuloController.
RubroController.java
package com.example.demo.controller; import org.springframework.web.bind.annotation.*; import java.util.List; import com.example.demo.model.Rubro; import com.example.demo.service.RubroService; @RestController public class RubroController { private final RubroService rubroService; public RubroController(RubroService rubroService) { this.rubroService = rubroService; } @GetMapping("/rubros") public List<Rubro> getAllRubros() { return rubroService.getAllRubros(); } @GetMapping("/rubros/{id}") public Rubro getRubroById(@PathVariable Long id) { return rubroService.getRubroById(id); } @PostMapping("/rubros") public Rubro createRubro(@RequestBody Rubro rubro) { return rubroService.saveRubro(rubro); } @DeleteMapping("/rubros/{id}") public void deleteRubro(@PathVariable Long id) { rubroService.deleteRubro(id); } }
ArticuloController.java
package com.example.demo.controller; import org.springframework.web.bind.annotation.*; import java.util.List; import com.example.demo.model.Articulo; import com.example.demo.service.ArticuloService; @RestController public class ArticuloController { private final ArticuloService articuloService; public ArticuloController(ArticuloService articuloService) { this.articuloService = articuloService; } @GetMapping("/articulos") public List<Articulo> getAllArticulos() { return articuloService.getAllArticulos(); } @GetMapping("/articulos/{id}") public Articulo getArticuloById(@PathVariable Long id) { return articuloService.getArticuloById(id); } @PostMapping("/articulos") public Articulo createArticulo(@RequestBody Articulo articulo) { return articuloService.saveArticulo(articulo); } @DeleteMapping("/articulos/{id}") public void deleteArticulo(@PathVariable Long id) { articuloService.deleteArticulo(id); } }
Las carpetas y archivos de cada carpeta son:
Recordemos que tenemos que tener creada la base de datos 'bd1' (lo podemos comprobar desde Workbench)
Procedamos a ejecutar los endpoints principales desde Postman:
Para cargar un rubro (y luego otro):
Podemos comprobar que la tabla rubros tiene los dos registros:
Para cargar un artículo debemos utilizar la siguiente sintaxis:
Es importante la estructura JSON:
{ "descripcion" : "manzana", "precio" : 1700, "rubro" : { "codigo" : 1 } }
Procedamos a insertar otros 3 artículos en la tabla (recordar de hacerlo uno a uno):
{ "descripcion" : "naranja", "precio" : 2500, "rubro" : { "codigo" : 1 } } { "descripcion" : "pomelo", "precio" : 900, "rubro" : { "codigo" : 1 } } { "descripcion" : "lechuga", "precio" : 450, "rubro" : { "codigo" : 2 } }
Probemos el endpoint que recupera todos los artículos:
Si eliminamos un rubro, luego se eliminan todos los artículos de dicho rubro:
Agreguemos los archivos index.html, index.js y estilos.css en el proyecto para consumir el endpoints que retorna todos los artículos junto con la descripción del rubro que pertenece.
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Lista de Artículos</title> <link rel="stylesheet" href="estilos.css"> </head> <body> <h1>Lista de Artículos</h1> <table id="articulos-table"> <thead> <tr> <th>Código</th> <th>Descripción</th> <th>Precio</th> <th>Rubro</th> </tr> </thead> <tbody id="articulos-body"> <!-- Aquí se llenará la tabla con los datos de los artículos --> </tbody> </table> <script src="index.js"></script> </body> </html>
index.js
document.addEventListener("DOMContentLoaded", () => { fetchArticulos() }) function fetchArticulos() { fetch('/articulos') .then(response => response.json()) .then(data => { const articulosBody = document.getElementById('articulos-body'); articulosBody.innerHTML = '' data.forEach(articulo => { const row = document.createElement('tr'); row.innerHTML = ` <td>${articulo.codigo}</td> <td>${articulo.descripcion}</td> <td>${articulo.precio}</td> <td>${articulo.rubro.descripcion}</td> ` articulosBody.appendChild(row) }) }) .catch(error => console.error('Error al obtener los artículos:', error)) }
estilos.css
body { font-family: Arial, sans-serif; margin: 20px; } h1 { text-align: center; } table { width: 100%; border-collapse: collapse; } th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #f2f2f2; }
Cuando ejecutamos la aplicación tenemos como resultado: