14 - Spring Data JPA - Mapeo de relaciones entre entidades - uno a muchos y muchos a uno

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

Relación uno a muchos y muchos a uno

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.

    creación 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.

  • Ahora en la carpeta 'repository' creamos las interfaces RubroRepository y ArticuloRepository.

    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:

    carpetas y archivos del proyecto

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

post en la tabla rubros

Podemos comprobar que la tabla rubros tiene los dos registros:

tabla rubros

Para cargar un artículo debemos utilizar la siguiente sintaxis:

carga tabla articulos

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:

recuperar todos los articulos

Si eliminamos un rubro, luego se eliminan todos los artículos de dicho rubro:

borrar un rubro y todos los articulos de dicho rubro

Accesos a los endpoints desde un sitio web.

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:

aplicación web que accede a un endpoint