Relaciones @OneToMany

JPA relaciones @OneToMany

Las relaciones uno a muchos (@OneToMany) se caracterizan por Entidad donde tenemos un objeto principal y colección de objetos de otra Entidad relacionados directamente. Estas relaciones se definen mediante colecciones, pues tendremos una serie de objetos pertenecientes al objeto principal.

Este articulo es parte de un guia completa de JPA, para ver el contenido completo puedes ir a Java Persistence API (JPA)


Un ejemplo clásico para entender este tipo de relaciones son las facturas, pues tendremos una Entidad cabecera donde tengamos los datos principales de la factura, como podría ser serie, cliente, total, fecha de expedición, etc. Por otra parte, la factura tendrá una serie de líneas que representa cada uno de los productos vendidos. 

Veamos como quedaría la entidad Invoice (factura):

package com.obb.jpa.jpaturorial.entity;

import java.util.Calendar;
import java.util.List;
import javax.persistence.*;

@Entity
@Table(name="INVOICES")
public class Invoice {
    
    @Id
    @Column(name="ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    @Column(name = "STATUS", length = 20, nullable = false)
    @Enumerated(EnumType.STRING)
    private Status status;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "REGIST_DATE", nullable = false)
    private Calendar registDate;
    
    @JoinColumn(name = "FK_CUSTOMER", nullable = false)
    @ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Customer customer;
    
    @OneToOne(mappedBy = "invoice", cascade = CascadeType.ALL)
    private Payment payment;
    
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "invoice")
    private List<InvoiceLine> lines;

    /** GET and SET */
}


Podemos apreciar como hemos definido la propiedad lines como una lista (List), lo cual nos permite relacionar la factura con un número indeterminado de líneas, por otro lado, hemos definido la propiedad mappedBy para indicar que es una relación bidireccional, es decir, la Entidad InvoiceLine tendrá también una relación hacia la Entidad Invoice.

La Entidad InvoiceLine se verá de la siguiente manera:

package com.obb.jpa.jpaturorial.entity;

import javax.persistence.*;

@Entity
@Table(name = "invoice_lines")
public class InvoiceLine {
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "PRODUCT", nullable = false)
    private String product;
    
    @Column(name = "PRICE", nullable = false)
    private double price;
    
    @Column(name = "TOTAL", nullable = false)
    private double quantity;
    
    @ManyToOne
    @JoinColumn(name = "FK_INVOICE", nullable = false, updatable = false)
    private Invoice invoice;
    
    /** GET and SET */

}


Como dijimos hace un momento, la Entidad InvoiceLine tiene una propiedad de tipo Invoice para poderla hacer bidireccional, observemos que el nombre de la propiedad invoice corresponde con el valor del mappedBy definido en la Entidad Invoice, ya que de lo contrario JPA nos arrojará un error.

Tambíen los quiero invitar a ver mi curso de JPA, donde explico todos estos temas aplicados con API REST, https://codmind.com/courses/jpa

 Los invito a mi Curso de Mastering JPA, donde habla de todos estos temas y crearemos un API REST para probar todos los conceptos de persistencia.


En este imagen podemos apreciar que la tabla invoice_lines tiene la columna FK_INVOICE que será utilizada para realizar el JOIN con la tabla invoices, de tal forma que cada registro de invoice_lines que sea encontrado será convertido en una instancia de la Entidad InvoiceLine.

public static void main(String[] args) {
    Invoice invoice = new Invoice();

    List<InvoiceLine> lines = new ArrayList<>();
    for(int c = 0; c<10; c++){
        InvoiceLine line = new InvoiceLine();
        line.setInvoice(invoice);
        line.setPrice(10);
        line.setProduct("Product " + (c+1));
        line.setQuantity(c+1);
        lines.add(line);
    }

    Customer customer = new Customer();
    customer.setName("Oscar Blancarte");

    invoice.setCustomer(customer);
    invoice.setLines(lines);
    invoice.setRegistDate(Calendar.getInstance());
    invoice.setStatus(Status.ACTIVE);

    EntityManager em = EntityManagerUtil.getEntityManager();
    em.getTransaction().begin();
    em.persist(invoice);
    em.getTransaction().commit();
}


Dando como resultado los siguientes registros en la base de datos:

JPA relaciones @OneToMany tabla invoices
Tabla invoices
JPA relaciones @OneToMany tabla invoice_lines
Tabla invoice_lines

Ahora si podemos ver todo materializado y nos queda más la forma en que la información es almacenada. 


Conclusiones

Hemos comprobado lo simple que es crear relaciones @OneToMany y hemos podido comprobar como  la información es almacenada, solo faltaría resaltar que JPA no garantiza el orden en que los elementos de la colección son insertados, es por ello, que siempre se aconseja tener una columna que indique el orden natural de los elementos para sobre esa columna hacer el ordenamiento, esto claro si nos interesa mantener el ordenamiento, en caso podemos ignorarlo.

AnteriorÍndiceSiguiente

18 thoughts to “Relaciones @OneToMany”

  1. tengo tres tablas que estan relacionadas uno a muchos susesivamente, cuando realizo la verificacion en Postman me salta el error _
    2019-07-27 20:38:18.136 ERROR 10912 — [nio-9092-exec-1] s.e.ErrorMvcAutoConfiguration$StaticView : Cannot render error page for request [/mscapacidad/listaDivisiones] and exception [Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)

    1. Hola Christian, seguramente estás retornando una Entity directamente en el servicio REST, cuando hace esto, lo que hace Java es hacer un parce del Objeto a JSON, sin embargo, seguramente tus objetos han de tener un relación ciclica, lo que hace que la conversión a JSON se cicle, lo que te recomiendo es que en lugar de retornar una Entity directamente, crees un DTO con una estructura que no sea ciclica, al mismo tiempo que podrías retornar solo los campos que necesites, te dejo mi artículo del patrón DTO para que entiendas como funciona https://www.oscarblancarteblog.com/2018/11/30/data-transfer-object-dto-patron-diseno/
      saludos.

  2. Hola Oscar, gracias por este excelente aporte y por la forma tan clara que tienes para exponer cada tema.
    Te quería preguntar, ya tengo una base de datos Oracle y solo necesito hacer consulta a una tabla buscando un campo especifico, ¿Cual sería la instrucción para realizar dicha consulta y luego llevar el resultado de esa consulta a una variable?
    Muchas gracias

    1. Hola Karlox, cuando realices la consulta, en lugar de seleccionar toda la entidad, debes acotar los campos consultados, por ejemplo:
      select e.nombre from e where e.id = 1, solo toma en cuenta que este query en lugar de retornarte una entidad, te va a regresar un arreglo de objectos de dos dimensiones Object[][], donde la primera dimensión es cada tupla y la segunda dimensión son los campos consultados en el mismo orden de la consulta.

      Saludos.

  3. Hola Oscar, una pregunta, cómo puedo manejar una relación de uno a cero o muchos? o sea no se cómo hacer con la llave foránea en una relación uno a cero.

    1. Para empezar, no existen las relaciones uno a cero, en tal caso, sería ManyToOne, donde la relación puede ser nullable, es decir, puede que la relación no existe. Para lograr eso, tienes que usar la anotación @ManyToOne en combinación con @JoinColumn, para definir el nombre de la columna

  4. Hola Oscar, quiero agradecerte por tan excelente articulo y de mucha ayuda para los principiantes, podrías apoyarme con un problema que tengo, Cannot add or update a child row: a foreign key constraint fails (`argentap_ArgentaGeneral`.`detalle_compras`, CONSTRAINT `detalle_compras_ibfk_1` FOREIGN KEY (`Folio_Compra`) REFERENCES `compras` (`Folio_Compra`)) … tengo varios objetos que se persisten a la hora de ejecutar el persist, para mi caso Compras –> oneToMany (detalle_compras) –> OnetoOne(Inventario), creo que tengo correctamente programado mis entities ya que si elimino la llave foránea en la base de datos SI funciona, hay forma de decirle a la aplicación en que orden me guarde los objetos? primero me cree los registros en compras o Inventarios, y al final en detalle_compras, te lo agradecería mucho, saludos!

    1. Puedes definir la cascada para indicarle que Entidades se deben de persistir en cascada y cuales no, lo que si no sabría decirte con certeza es si puede decir en que orden se deben de insetar, por lo que yo sé, no se puede, y es que al final, el orden no debería de ser un problema, pues JPA es lo suficientemente inteligente para determinar la jerarquía de dependecias para insertar las entidades en el orden correcto.

  5. Hola Oscar, buenas noches, espero te encuentres bien, me ha ayudado mucho tus articulos pero tengo una situacion que me ha salido, en la construccion de una aplicacion tengo una entidad que se llama “Vehiculo” y dentro de esta entidad vehiculo tengo un atributo que se llama “tipo vehiculo” lo tengo de esta manera

    @ManyToOne(optional = false, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name=”id_tipo_vehi” )
    private TipoVehiculo tipoVehiculo;

    Funciona bien cuando los valores en las 2 entidades son nuevos.
    La cantidad de tipos de vehiculos son mucho menores a los vehiculos, el tema es que cuando quiero ingresar un nuevo “vehiculo” con un “tipo vehiculo” que ya existe me sale error de llave duplicada en la tabla “TipoVehiculo”, como podria solucionar este incoveniente, agradecera mucho tu ayuda.

    1. Ese problema es muy frecuenta entre prinsipiantes con JPA, el problema es que cuando vas a relacionar una Entidad que ya existe, debes primero de agregarlo al contexto de persitencia mediante manager.find(), una vez que lo cargues, tendrás que reemplazar ese reemplazar el TipoVehiculo por el que te regrese el método find. En mi curso de Mastering JPA con Hibernate explico el ciclo de vida de las entidades a la perfección y vemos ejemplos de esto que comentas, por si quieres darle una revisada: https://codmind.com/courses/jpa

  6. Hola buena tarde soy nuevo en esto de jpa y tengo una consulta estoy creando una aplicación sencilla para un inventario ya tengo mi base de datos relacionada en la gran mayoría las relaciones me ha salido de uno a muchos pero viendo información de internet y tutoriales algunos recomienda no usar muchas relaciones OneToMany entonces con base en esa información decidí utilizar en todas mis entidades la relacion ManytoOne quería saber tu opinión ya que estoy algo confundido.

    1. Hola Cristhian, quizás entendiste mal el concepto, en realidad las anotaciones @ManyToOne y @OneToMany no son intercambiables, es decir, no es que por que sea mala una vas a usar la otra, por que no sirve para lo mismo, una es para el manejo de colecciones y la otra para relaciones a una Entidad, mi sugerencia es que ignores eso que leite y solo diseña las entidades para que te funcionen, con el tiempo aprenderás a dominarlas y podrás comprender cuando es mejor una que otra, así que mi recomendación es esa, por ahora enfocate en que funcione, luego lo perfeccionas.

  7. Hola, primero agradezco mucho la información que nos compartes muy buena. Por favor quisiera que me ayudes con la siguiente consulta, tengo dos tablas persona y direcciones, obviamente una relación unidireccional uno a muchos, cuando realizo un get de clientes digamos unos 10, me devuelve siempre los datos de los clientes como sus direcciones, y si estas direcciones tuvieran por ejemplo varios teléfonos sacarían los teléfonos. Como se puede realizar un servicio para que sólo traiga la información del cliente.

    @Entity
    @Data
    public class Persona implements java.io.Serializable {

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

    @Column(nullable = false, length = 80)
    private String nombres;

    @Column(nullable = false)
    private LocalDate fechaNacimiento;

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

    @Column(nullable = false)
    private String email;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = “persona_id”)
    private List direcciones = new ArrayList();

    }

    ———————————————————-

    @Entity
    @Data
    public class Direccion {

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

    @Column
    private String callePrincipal;

    @Column
    private String calleSecundaria;

    @Column
    private String numero;

    @Column
    private String codigoPostal;
    }

  8. hola una consulta, si pones al customer con la propiedad cascade.ALL eso significa que cuando borres un invoice estarias borrando tambien el customer asociado?

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *