Saltar a contenido

Relaciones en Spring Boot con JPA

En Spring Boot, las relaciones entre entidades son fundamentales para modelar asociaciones entre tablas en una base de datos relacional utilizando Spring Data JPA. Estas relaciones se definen mediante anotaciones de JPA como @OneToOne, @OneToMany, @ManyToOne y @ManyToMany. Además, para manejar la serialización de objetos a JSON y evitar problemas como bucles infinitos, se utilizan anotaciones como @JsonManagedReference y @JsonBackReference de Jackson. Este documento explica en detalle todas las formas de relaciones en JPA, sus configuraciones, ejemplos prácticos con la entidad Producto y una nueva entidad Categoria, y el uso de estas anotaciones de Jackson.


Tipos de relaciones en JPA

JPA soporta cuatro tipos principales de relaciones entre entidades:

  1. One-to-One (@OneToOne): Una entidad está asociada a exactamente una instancia de otra entidad.
  2. One-to-Many (@OneToMany): Una entidad está asociada a múltiples instancias de otra entidad.
  3. Many-to-One (@ManyToOne): Múltiples instancias de una entidad están asociadas a una sola instancia de otra entidad.
  4. Many-to-Many (@ManyToMany): Múltiples instancias de una entidad están asociadas a múltiples instancias de otra entidad.

A continuación, se explican cada una con ejemplos prácticos usando las entidades Producto y Categoria, incluyendo las anotaciones @JsonManagedReference y @JsonBackReference para manejar la serialización JSON.


Anotaciones clave para relaciones

Anotaciones de JPA

  • @OneToOne: Define una relación uno a uno.
  • @OneToMany: Define una relación uno a muchos.
  • @ManyToOne: Define una relación muchos a uno.
  • @ManyToMany: Define una relación muchos a muchos.
  • @JoinColumn: Especifica la columna de la clave foránea en la base de datos.
  • @JoinTable: Define una tabla intermedia para relaciones muchos a muchos.
  • mappedBy: Indica el lado "inverso" de una relación bidireccional, especificando el campo en la otra entidad que gestiona la relación.

Anotaciones de Jackson

  • @JsonManagedReference: Se usa en el lado "padre" de una relación para indicar que este campo debe incluirse en la serialización JSON.
  • @JsonBackReference: Se usa en el lado "hijo" para evitar bucles infinitos durante la serialización JSON, excluyendo este campo de la serialización.

Ejemplo práctico: Entidades Producto y Categoria

Vamos a modelar una tienda en línea con dos entidades: Producto (un producto en venta) y Categoria (la categoría a la que pertenece el producto). Usaremos Lombok para reducir código repetitivo y definiremos todas las relaciones posibles entre estas entidades. Cada ejemplo incluye las anotaciones de JPA y Jackson, con explicaciones detalladas.


1. Relación One-to-One

Una relación uno a uno implica que una instancia de una entidad está asociada a exactamente una instancia de otra entidad. Por ejemplo, un Producto puede tener una única Categoria exclusiva (en este caso, asumimos que cada producto pertenece a una categoría única).

Código

Entidad Producto
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.time.LocalDate;

@Entity
@Table(name = "productos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "nombre_producto", nullable = false, length = 100)
    private String nombre;

    @Column(nullable = false)
    private Double precio;

    @Column(name = "fecha_creacion")
    private LocalDate fechaCreacion;

    @OneToOne
    @JoinColumn(name = "categoria_id", referencedColumnName = "id")
    @JsonManagedReference
    private Categoria categoria;
}
Entidad Categoria
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity
@Table(name = "categorias")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nombre;

    @OneToOne(mappedBy = "categoria")
    @JsonBackReference
    private Producto producto;
}

Explicación

  • En Producto:
  • @OneToOne: Define la relación uno a uno con Categoria.
  • @JoinColumn(name = "categoria_id"): Crea una columna categoria_id en la tabla productos como clave foránea que referencia la tabla categorias.
  • @JsonManagedReference: Indica que Producto es el lado "padre" y se incluirá en la serialización JSON.

  • En Categoria:

  • @OneToOne(mappedBy = "categoria"): Indica que Producto es el propietario de la relación (el lado que tiene la clave foránea). El campo categoria en Producto gestiona la relación.
  • @JsonBackReference: Evita que el campo producto se serialice, previniendo bucles infinitos (por ejemplo, Producto -> Categoria -> Producto).

  • Serialización JSON: Cuando serializas un Producto, obtendrás su Categoria, pero no al revés. Esto evita problemas de recursión infinita.

  • Estructura en la base de datos:

  • Tabla productos: Columnas id, nombre_producto, precio, fecha_creacion, categoria_id.
  • Tabla categorias: Columnas id, nombre.

2. Relación Many-to-One y One-to-Many

Una relación muchos a uno implica que múltiples instancias de una entidad están asociadas a una sola instancia de otra entidad. Por ejemplo, varios Productos pueden pertenecer a una misma Categoria. La relación inversa es uno a muchos, donde una Categoria puede tener varios Productos.

Código

Entidad Producto
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.time.LocalDate;

@Entity
@Table(name = "productos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "nombre_producto", nullable = false, length = 100)
    private String nombre;

    @Column(nullable = false)
    private Double precio;

    @Column(name = "fecha_creacion")
    private LocalDate fechaCreacion;

    @ManyToOne
    @JoinColumn(name = "categoria_id", nullable = false)
    @JsonBackReference
    private Categoria categoria;
}
Entidad Categoria
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.List;

@Entity
@Table(name = "categorias")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nombre;

    @OneToMany(mappedBy = "categoria", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JsonManagedReference
    private List<Producto> productos;
}

Explicación

  • En Producto:
  • @ManyToOne: Indica que muchos Productos pueden estar asociados a una sola Categoria.
  • @JoinColumn(name = "categoria_id"): Crea una columna categoria_id en la tabla productos como clave foránea.
  • @JsonBackReference: Evita serializar el campo categoria para prevenir bucles infinitos.

  • En Categoria:

  • @OneToMany(mappedBy = "categoria"): Define la relación inversa, indicando que el campo categoria en Producto gestiona la relación.
  • cascade = CascadeType.ALL: Propaga operaciones (como guardar o eliminar) de Categoria a sus Productos asociados.
  • fetch = FetchType.LAZY: Carga los Productos asociados solo cuando se accede a ellos (optimización).
  • @JsonManagedReference: Incluye la lista de Productos en la serialización JSON de Categoria.

  • Serialización JSON: Al serializar una Categoria, obtendrás su lista de Productos, pero al serializar un Producto, no se incluirá la Categoria para evitar recursión.

  • Estructura en la base de datos:

  • Tabla productos: Columnas id, nombre_producto, precio, fecha_creacion, categoria_id.
  • Tabla categorias: Columnas id, nombre.

3. Relación Many-to-Many

Una relación muchos a muchos implica que múltiples instancias de una entidad pueden estar asociadas a múltiples instancias de otra entidad. Por ejemplo, un Producto puede pertenecer a varias Categorias, y una Categoria puede estar asociada a varios Productos. Esto requiere una tabla intermedia.

Código

Entidad Producto
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.time.LocalDate;
import java.util.List;

@Entity
@Table(name = "productos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "nombre_producto", nullable = false, length = 100)
    private String nombre;

    @Column(nullable = false)
    private Double precio;

    @Column(name = "fecha_creacion")
    private LocalDate fechaCreacion;

    @ManyToMany
    @JoinTable(
        name = "producto_categoria",
        joinColumns = @JoinColumn(name = "producto_id"),
        inverseJoinColumns = @JoinColumn(name = "categoria_id")
    )
    @JsonManagedReference
    private List<Categoria> categorias;
}
Entidad Categoria
package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.util.List;

@Entity
@Table(name = "categorias")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nombre;

    @ManyToMany(mappedBy = "categorias")
    @JsonBackReference
    private List<Producto> productos;
}

Explicación

  • En Producto:
  • @ManyToMany: Define la relación muchos a muchos con Categoria.
  • @JoinTable: Crea una tabla intermedia producto_categoria con dos columnas: producto_id (clave foránea a productos) y categoria_id (clave foránea a categorias).
  • @JsonManagedReference: Incluye la lista de Categorias en la serialización JSON de Producto.

  • En Categoria:

  • @ManyToMany(mappedBy = "categorias"): Indica que Producto es el propietario de la relación, y el campo categorias en Producto gestiona la relación.
  • @JsonBackReference: Evita serializar la lista de Productos para prevenir bucles infinitos.

  • Serialización JSON: Al serializar un Producto, obtendrás sus Categorias, pero al serializar una Categoria, no se incluirán los Productos.

  • Estructura en la base de datos:

  • Tabla productos: Columnas id, nombre_producto, precio, fecha_creacion.
  • Tabla categorias: Columnas id, nombre.
  • Tabla producto_categoria: Columnas producto_id, categoria_id.

Notas sobre @JsonManagedReference y @JsonBackReference

  • Propósito: Estas anotaciones evitan bucles infinitos durante la serialización JSON en relaciones bidireccionales. Sin ellas, serializar un Producto incluiría su Categoria, que a su vez incluiría el Producto, y así sucesivamente.
  • Uso:
  • @JsonManagedReference se coloca en el lado que deseas incluir en la serialización (generalmente el "padre").
  • @JsonBackReference se coloca en el lado que deseas excluir (generalmente el "hijo").
  • Alternativas: Si no deseas usar estas anotaciones, puedes usar @JsonIgnore en uno de los lados de la relación, pero esto excluye completamente el campo de la serialización. Otra opción es usar DTOs (Data Transfer Objects) para controlar manualmente qué datos se serializan.

Buenas prácticas para relaciones

  1. Elegir el lado propietario: En relaciones bidireccionales, siempre define un lado propietario (el que tiene la clave foránea o la @JoinTable) usando mappedBy en el lado inverso.
  2. Usar cascade con cuidado: Configura cascade = CascadeType.ALL solo si deseas propagar operaciones (como eliminar) a las entidades relacionadas.
  3. Configurar fetch apropiadamente:
  4. FetchType.LAZY: Carga datos solo cuando se accede (mejor para rendimiento).
  5. FetchType.EAGER: Carga datos inmediatamente (puede causar problemas de rendimiento).
  6. Evitar bucles infinitos: Usa @JsonManagedReference y @JsonBackReference o DTOs para manejar serialización JSON.
  7. Validar datos: Usa @NotNull o restricciones en la base de datos para garantizar la integridad de las relaciones.
  8. Evitar relaciones innecesarias: Modela solo las relaciones necesarias para tu caso de uso para mantener el diseño simple.

Ejemplo Completo de Relaciones en Spring Boot con Jackson

Ejemplo completo que integra todas las relaciones en Spring Boot con Spring Data JPA: uno a uno, uno a muchos/muchos a uno, y muchos a muchos, utilizando las anotaciones @JsonManagedReference y @JsonBackReference de Jackson para gestionar la serialización JSON en relaciones bidireccionales. El caso práctico se basa en una tienda en línea con las entidades Cliente, Perfil, Pedido, Producto, y Categoria.


Descripción del caso

En una tienda en línea: - Un Cliente tiene un único Perfil (relación uno a uno bidireccional). - Un Cliente puede tener múltiples Pedidos, y cada Pedido pertenece a un único Cliente (relación uno a muchos/muchos a uno bidireccional). - Un Pedido puede contener múltiples Productos, y un Producto puede estar en múltiples Pedidos (relación muchos a muchos bidireccional). - Un Producto puede pertenecer a múltiples Categorias, y una Categoria puede estar asociada con múltiples Productos (relación muchos a muchos bidireccional).


Código de las entidades

Entidad Cliente

package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "clientes")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Cliente {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nombre;

    @Column(nullable = false, unique = true)
    private String email;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "perfil_id", nullable = false)
    @JsonManagedReference
    private Perfil perfil;

    @OneToMany(mappedBy = "cliente", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
    @JsonManagedReference
    private List<Pedido> pedidos = new ArrayList<>();
}

Entidad Perfil

package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.time.LocalDate;

@Entity
@Table(name = "perfiles")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Perfil {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String telefono;

    @Column
    private LocalDate fechaNacimiento;

    @Column(length = 200)
    private String direccion;

    @OneToOne(mappedBy = "perfil", fetch = FetchType.LAZY)
    @JsonBackReference
    private Cliente cliente;
}

Entidad Pedido

package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "pedidos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Pedido {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private LocalDate fecha;

    @Column(nullable = false)
    private Double total;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "cliente_id", nullable = false)
    @JsonBackReference
    private Cliente cliente;

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
    @JoinTable(
        name = "pedido_producto",
        joinColumns = @JoinColumn(name = "pedido_id"),
        inverseJoinColumns = @JoinColumn(name = "producto_id")
    )
    @JsonManagedReference
    private Set<Producto> productos = new HashSet<>();
}

Entidad Producto

package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "productos")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String nombre;

    @Column(nullable = false)
    private Double precio;

    @ManyToMany(mappedBy = "productos", fetch = FetchType.LAZY)
    @JsonBackReference
    private Set<Pedido> pedidos = new HashSet<>();

    @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
    @JoinTable(
        name = "producto_categoria",
        joinColumns = @JoinColumn(name = "producto_id"),
        inverseJoinColumns = @JoinColumn(name = "categoria_id")
    )
    @JsonManagedReference
    private Set<Categoria> categorias = new HashSet<>();
}

Entidad Categoria

package com.tienda.model;

import jakarta.persistence.*;
import lombok.*;
import com.fasterxml.jackson.annotation.JsonBackReference;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "categorias")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Categoria {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 50)
    private String nombre;

    @Column(length = 200)
    private String descripcion;

    @ManyToMany(mappedBy = "categorias", fetch = FetchType.LAZY)
    @JsonBackReference
    private Set<Producto> productos = new HashSet<>();
}