Relaciones @OneToOne

Las relaciones One to One (@OneToOne) se caracterizan porque solo puede existir una y solo una relación con la Entidad de destino, de esta forma, la entidad marcada como @OnoToOne deberá tener una referencia a la Entidad destino y por ningún motivo podrá ser una colección. De la misma forma, la Entidad destino no podrá pertenecer a otra Instancia de la Entidad origen.

Solo para ponernos en contexto, las relaciones @OneToOne se utilizan cuando existe una profunda relación entre la Entidad origen y destino,de tal forma que la entidad destino le pertenece a la Entidad origen y solo a ella, por ende, la existencia de la entidad destino depende de la Entidad origen.

Para comprender mejor todo este trabalenguas, analicemos el caso práctico de una factura, cuando creamos una factura, la llevamos con nuestro cliente para que la pague, este pago puede ser una entidad por separada que lleve el control de la fecha de pago, monto pagado, método de pago, etc. Veamos como quedarían esta Entidades

Entidad Invoice:

package com.obb.jpa.jpaturorial.entity;

import java.util.Calendar;
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;

    /** GET and SET */
    
}

Entidad Payment:

package com.obb.jpa.jpaturorial.entity;

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

/**
 * @author Oscar Blancarte <oscarblancarte3@gmail.com>
 */
@Entity
@Table(name = "payments")
public class Payment {
    
    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name="PAY_DATE", nullable = false, updatable = false)
    private Calendar payDate;
    
    @Column(name = "AMOUNT", nullable = false, updatable = false)
    private double amount;
    
    @Column(name = "PAY_METHOD", nullable = false, updatable = false)
    private PaymentMethod paymentMethod;
    
    @OneToOne
    @JoinColumn(name = "FK_INVOICE", updatable = false, nullable = false)
    private Invoice invoice;

    /** GET and SET */
}

Estas dos Entidades crearán las siguientes tablas:

Como podemos observar, la tabla payments tiene la columna FK_INVOICE que hace relación con la tabla invoices.

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.

Algo que hay que tomar en cuenta, es que tanto la Entidad Invoice como Payment tiene anotada como @OneToOne a la otra entidad, sin embargo, solo payments ha creado la columna para hacer el JOIN, esto se debe a que esta es la que tiene la anotación @JoinColumn, la cual nos permite establecer como se llamará la columna para realizar el JOIN, adicional, la Entidad Invoice tiene definida la propiedad mappedBy de la anotación @OneToOne, esta propiedad es muy importante, pues le permite hacer una relación bidireccional, si no la pusiéramos, JPA agregaría una columna adicional en la tabla invoices, y guardaría el pago como un nuevo registro.

Veamos un ejemplo de cómo se crearía un registro con esta relación:

public static void main(String[] args) {
        EntityManager manager = EntityManagerUtil.getEntityManager();
        
        Customer customer = new Customer();
        customer.setName("Oscar Blancarte");
        
        Payment payment = new Payment();
        payment.setAmount(100);
        payment.setPayDate(Calendar.getInstance());
        payment.setPaymentMethod(PaymentMethod.CASH);
                
        
        Invoice invoice = new Invoice();
        invoice.setCustomer(customer);
        invoice.setRegistDate(Calendar.getInstance());
        invoice.setStatus(Status.ACTIVE);
        
        payment.setInvoice(invoice);
        invoice.setPayment(payment);
        
        manager.getTransaction().begin();
        manager.persist(invoice);
        manager.getTransaction().commit();
    }

Dando como resultado lo siguiente:

JPA OneToOne payments table
Tabla payments
JPA OneToOne invoices table
Tabla invoices

Conclusiones

Crear relaciones OneToOne es muy simple como hemos podido comprobar, e incluso, podríamos crear una relación unidireccionar omitiendo la propiedad payment de la entidad Invoice, lo que diera como resultado exactamente la misma estructura de base de datos, con la única limitante que no podríamos recuperar el pago por medio de la factura y es por ello que utilizamos relacionar bidireccionales con mappedBy.

AnteriorÍndiceSiguiente

24 thoughts to “Relaciones @OneToOne”

  1. Una consulta, si la relación a nivel de base de datos de este ejemplo es de uno a muchos, por qué se utiliza la anotación @OneToOne ? No debería utilizarse la anotación @OneToMany o @ManyToOne?

    Saludos,

    Christian

    1. Lo que dices es incorrecto, por que una factura tiene una relación uno a uno con los pagos, en otro caso estaríamos diciendo que un pago pude pertenecer a muchas facturas o que una factura puede tener muchos pagos.

        1. Excelente pregunta estimado Christian, pero me gustaria hacerte una aclaración, el modelo Físico y el modelo Lógico, sin diferentes, cuando modelamos entidades lo hacemos pensando en un modelo lógico, mientras que cuando construimos las tablas, lo hacemos pensando en un modelo físico. dicho esto, la imagen que ves aquí de las tablas, corresponde a un modelo físico, mientras que la entidad es un modelo lógico.
          Ahora bien, te preguntas cual es la diferencia, la realidad es que el modelo lógico es imposible de hacer en un modelo físico, solo piensa, lo siguiente, como puedes hacer un Many To Many en un modelo físico, la realidad es que es imposible, por que necesitarías campos de tipo array para guardar todas las relaciones, en su lugar, el modelo fisico se hace mediante 3 tablas, donde cada una de ellas donde las dos principales se una por una intermedia.
          Espero que esto te de una pequeña luz en tu duda.
          saludos.

  2. Me pasa algo Curioso, tengo una entidad Empresa y una entidad dirección, con la misma relación que muestras, OneToOne, pero la generación de valor ID en la tabla dirección es por secuencia a oracle. Cuando hago el persist con el objeto empresa, que contiene el objeto direccion, guarda perfectamente la id generada por el mapeo de la secuencia , pero en la tabla empresa me guarda la id en null, el JPA con eclipselink no es capaz de asignar el valor de id_direccion a en la tabla empresa, luego de aplicarse la secuencia en el objeto dirección.

    1. Bueno, para comenzar seguramente tienes algo mal en el mapeo, ya que la base de datos no permite guardar null como llave primaria, por lo que seguramente no estas mapeando bien Entidad, por otro lado, revisa que estes usando un GeneratorValues de tipo de sequencia en la llave primaria

      1. Claro lo genero con sequence oracle y guarda bien el dato en el objeto dirección, pero la id de dirección en el objeto empresa lo deja nulo, parece que debiera usar flush() después de persist() porque la transacción de jpa no se entera

        1. Es que regreso a lo mismo, no debería de ser posible guardar NULL en una llave primaria, seguramente tu tabla no te la esta generando con llave primaria, por lo tanto, no le esta generando el ID, si el campo fuera realmente una llave primaria, ni siquiera pudieras guardar el registro.

          1. tal vez no me di a explicar bien, el id_dirección de la tabla dirección es clave primaria y la secuencia la genera y guarda un valor no nulo, la id_dirección para la tabla Empresa queda en nulo, porque no se entera de @SequenceGenerator(name=”seq”, initialValue=1, allocationSize=1) de este mapeo de la tabla dirección, la que queda nula es la fk en empresa, no la pk, la pk si genera la nueva id para el registro con la secuencia.

          2. Hola Ricardo, bueno es dificil para mi saber que esta pasando desde aquí, has intentado debugear el código para ver los valores de la entidad previo y despues de insertar?

  3. Estimado, estuve investigando y si tengo una tabla persona y una tabla usuario que emula una herencia, estuve viendo que funciona:

    public class Persona{

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(unique=true, nullable=false)
    private int id_persona;

    @OneToOne(cascade=CascadeType.PERSIST, mappedBy = “persona”)
    private Usuario usuario;
    }

    public class Usuario{

    @Id
    @OneToOne
    @JoinColumn(name = “id_persona”)
    private Persona persona;
    }

    No he visto aquí tratamiento de id como objeto y vi una segunda alternativa como tratamiento de herencia con algo así, que tampoco lo vi tratado aquí como para ser implementado:

    @Entity
    @Table(name=”CUST”)
    @Inheritance(strategy=JOINED)
    @DiscriminatorValue(“CUST”)
    public class Customer { … }

    @Entity
    @Table(name=”VCUST”)
    @DiscriminatorValue(“VCUST”)
    @PrimaryKeyJoinColumn(name=”CUST_ID”)
    public class ValuedCustomer extends Customer { … }

    1. Fijate que yo nunca he usado la herencia con JPA mas allá de ejemplos de laboratorio, en lo particular no me gusta usar herencia para JPA

  4. Hola una pregunta por lo que leí del @JoinColumn tiene una propiedad de unique, mi pregunta es ¿En una relación de @OneToOne es necesario decirle al campo que va hacer único, o ya automáticamente JPA lo realiza?
    Y otra duda en muchas ocasiones es necesario poner el “ON DELETE” o “ON UPDATE” en “CASCADE”, al menos usando postgresql es de esta manera, como se pondría esto con JPA, por lo que veo, el por defecto me los deja en “NO ACTION”….. Muchas gracias

    1. hola Carlos,

      El unique te sirve para forzar a JPA para crear la restricción, por lo que mi sugerencia es que la pongas si quieres que se cree, referente a si se crear por default en una relación onoToOne, bueno, es dificil darte una respuesta, por que muchas veces depende de la implementación de JPA.
      Respecto a tu segunda pregunta, puedes personalizar eso con la propiedad cascade.
      saludos.

  5. Hola! Tengo una relación oneToOne, de categorización. Una Actividad pertenece a un TipoActividad, y ya. Pero me están pidiendo mostrar una lista de actividades agrupadas por TipoActividad. Acá no tengo manera de hacer un fecth. Obligatoriamente necesitaría un JPQL?

    1. Hola Mariana, me confunde tu modelo, por que si tienes una relación OneToOne, como podrías agruparlas, si en tenoría es una relación uno a uno, por lo tanto, no habría más de un registro.

  6. Hola! por qué no sería posible dejar el campo id de payments tanto como llave primaria y llave foránea a la vez ? y así no tener la columna adicional de FK_INVOICE ?

  7. Hola que tal, estoy tratando de hacer un @OneToMany , la tabla padre tiene 5 pk’s y la tabla hija (detalle), no tiene pk pero se relacionan mediante dos PK de la tabla padre. Me esta saliendo un error de PK para la tabla hija y cuando le agrego @ID sobre @ManyToOne, me sale error de JoinColumns.
    pag (tabla padre)
    //bi-directional many-to-one association to Doc
    @OneToMany(mappedBy=”pag”, fetch=FetchType.EAGER)
    private List docs;

    doc(tabla hija)
    @ManyToOne
    @JoinColumns({
    @JoinColumn(name=”doc_formul”, referencedColumnName=”pag_formul”),
    @JoinColumn(name=”doc_numdoc”, referencedColumnName=”pag_ndocpa”)
    })
    private Pag pag;

    alguna recomendación ?.

    Saludos

    1. Lo que si veo, es que dices que la tabla Hija (detalle) no tiene PK, lo cual en JPA es inválido, ya que todas las tablas deberían de tener Llave primaria, además, la lista docs, debería de ser usando genéricos List

  8. excelente info amigo, una duda por ejemplo si yo tengo la tabla direcciones y esta se relaciona a usuarios y sucursales y ambas cardinalidades forman 1to1 es decir mi sucursal solo tiene una dirección y una dirección solo tiene una sucursal, y esto se replica para usuarios es correcto hacer el mapeo en direcciones de la misma forma pero haciendo el mapeo con su correspondiente atributo

Deja un comentario

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