Hibernate Composite Keys With Annotations

Hibernate, Llaves Compuestas Con Anotaciones

Dentro de las buenas y muy malas experiencias que me han tocado con Hibernate he aprendido que existen varios caminos y formas de utilizarlo, y en esta publicación quiero colocar la forma para crear una tabla con llaves compuestas utilizando anotaciones.

A pesar de que esto puede sonar una tarea común y corriente implica conocer algunos detalles que a veces son imperceptibles para los desarrolladores hasta que estamos frente al IDE (o editor de texto) y decimos “Y ahora como rayos hago esto??”, en fin en mi experiencia esto es lo que  he encontrado:

Nota: Los ejemplos los hice en MySQL pero llevan la anotación SequenceGenerator por que funcionan también en Oracle sin modificar nada de códig

Primer Forma ( @IdClass )

Esto es a lo que vamos a crear:

Customer.java:

@Entity
@Table(name = "CUSTOMER")
@SequenceGenerator(name = "SQ_CUSTOMER", sequenceName = "SQ_CUSTOMER")
public class Customer implements Serializable{

    @Id
    @Column(name = "id_customer")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idCustomer;

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "customer")
    private Collection<CustomerDetail> customerDetails;

    /* Getters And Setters */

}

No hay mucho que mencionar solo que las anotaciones están declaradas en los Fields (El mecanismo se le llama Field Access),  las coloque ahí para simplificar el ejemplo, pero también pueden ir declaradas sobre los métodos (Property Accessors),  la posición de las anotaciones debe ser considerado dependiedno del desarrollo, espero escribir unas notas sobre esto más a delante.

  • Las propiedades idCustomer y name son los campos de la tabla.
  • La propiedad customerDetails va a construir la relación 1:N (@OneToMany) de la tabla padre CUSTOMER a la taba CUSTOMER_DETAIL, la relación puede tener ligados uno o muchos  registros por eso su propiedad es de tipo Collection.

CustomerDetail.java:

@Entity
@Table(name = "CUSTOMER_DETAIL")
@IdClass(CustomerDetailPk.class)
public class CustomerDetail implements Serializable {

    @Id
    @Column(name = "id_customer")
    private Long idCustomer;

    @Id
    @Column(name = "id_detail")
    private Long idDetail;

    @ManyToOne
    @ForeignKey(name = "FK_CUS_CUSD")
    @JoinColumn(name = "id_customer")
    private Customer customer;

    /* Getters And Setters */

}

class CustomerDetailPk implements Serializable{

    @Column(name = "id_customer")
    private Long idCustomer;

    @Column(name = "id_detail")
    private Long idDetail;

    /* Getters And Setters */

    /* Override Equals And HashCode */

}

Tenemos dos clases en el mismo archivo (CustomerDetail y CustomerDetailPk), generalmente yo las coloco en un solo archivo pero pueden declararse por separado.

  • La anotación @IdClass indica la clase que se va a utilizar como llave compuesta (CustomerDetailPk).
  • Las propiedades idCustomer y idDetail son las mismas que están en la clase CustomerDetailPk, deben ser declarados exactamente iguales, pero tienen que tener la anotación @Id para indicar que son parte de la llave primaria.
  • La propiedad customer va a crear la relación muchos a uno ( @ManyToOne ) con la tabla padre CUSTOMER.
  • Cada registro en CUSTOMER_DEATIL esta ligado exclusivamente a un solo registro de CUSTOMER, la clase CustomerDetail tiene declarada la propiedad de tipo Customer porque la tabla puede almacenar muchos registros pero cada uno esta ligado a un solo customer (@ManyToOne).
  • La anotación @Foreignkey solo es para colocar un nombre a la relación.
  • La anotación @JoinColumn indica que la propiedad customer es el campo para crear la relación de llave foranea y va a tomar la columna id_customer de la tabla CUSTOMER_DETAIL para crear el join con la tabla padre CUSTOMER.

Segunda Forma ( @Embeddable y @EmbeddedId )

Es igual que el anterior solo que vamos a utilizar otras anotaciones:

Account.java:

@Entity
@Table(name = "ACCOUNT")
@SequenceGenerator(name = "SQ_ACCOUNT", sequenceName = "SQ_ACCOUNT")
public class Account implements Serializable{

    @Id
    @Column(name = "id_account")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long idAccount;

    @Column(name = "balance")
    private Double balance;

    @OneToMany(mappedBy = "account")
    private Collection<AccountDetail> accountDetails;

    /* Getters And Setters */

}

Es prácticamente igual que el ejemplo anterior.

AccountDetail.java:

@Entity
@Table(name = "ACCOUNT_DETAIL")
public class AccountDetail implements Serializable{

    @EmbeddedId
    private AccountDetailPk accountDetailPk;

    @ManyToOne
    @ForeignKey(name = "FK_ACC_ACCD")
    @JoinColumn(name = "id_account", insertable = false, updatable = false)
    private Account account;

    /* Getters And Setters */

}

@Embeddable
class AccountDetailPk implements Serializable{

    @Column(name = "id_account")
    private Long idAccount;

    @Column(name = "id_detail")
    private Long idDetail;

    /* Getters And Setters */
 /* Override Equals and HashCode */
}

Al igual que el anterior son dos clases AccountDetail y AccountDetailPk.

  • La clase AccountDetail tiene una propiedad llamada accountDetailPk con la anotación @EmdeddedId,  esta es la llave compuesta y es manejada como una sola propiedad, de esta manera no necesitamos crear las propiedades como el ejemplo anterior.
  • La propiedad account al igual que el ejemplo anterior, sirve para crear la relación con la tabla padre (Acount), sin embargo  y a diferencia con el ejemplo anterior, la anotación @JoinColumn debe especificar que las operaciones insertable y updatable serán marcadas con false, más adelante veremos para que sirven.
  • La anotación @Emdeddable en la clase AccountDetailPk indica que la clase va a ser  un objeto que puede ser manejado dentro de otro, en este caso va a ser nuestra llave primaría compuesta, y tiene dos propiedades idAccount y idDetail.

¿Cual debo de utilizar?  ( @IdClass VS @EmbeddedId )

Esto que voy a escribir son las cosas que uno aprende con la práctica y en el momento de estar desarrollando  puede hacer que tu trabajo sea sencillo o una pesadilla. Yo he utilizado ambas formas y no puedo decir que uno es mejor que otro, pero si hay detalles que se deben considerar de cada uno para saber cual utilizar:

Consultas HQL

Esto es un ejemplo y puede que no se vea reflejado el beneficio, pero cuando tienes classes con mas de 10 atributos y querys más complejos, se nota de inmediato.

Con @IdClass:

SELECT cd.idCustomer FROM CustomerDetail cd

Con @EmbeddedId:

SELECT ad.accountDetailPk.idAccount FROM AccountDetail ad

Para mi las consultas con @IdClass son un poco más claras y sencillas de entender. Sin embargo si en tu aplicación utilizas muchas consultas que únicamente toman los identificadores de la llave compuesta puede resultar más sencillo manejarlo con @EmbdeddedId, suponiendo que tienes una llave compuesta con varios campos tendrias que manejarlos uno por uno con @IdClass y con @EmbdeddedId puedes hacerlo solo con el objeto de la llave, por ejemplo:

Con @IdClass:

SELECT cd.idCustomer, cd.idDetail FROM CustomerDetail cd

Con @EmbeddedId:

SELECT ad.accountDetailPk FROM AccountDetail ad

En resumen, depende de tu aplicación y de la información que estes utilizando.

Código Java

La anotación que selecciones va a cambiar un poco la forma en como guardas y obtienes los datos de la base, para las llaves compuestas deben manejarse por separado los objetos, es decir no puede realizar la inserción automática como en una relación simple de @OneToMany.

Con @IdClass:

Customer customer= new Customer();
customer.setName("Customer Name");

sessionFactory.getCurrentSession().persist(customer);

CustomerDetail customerDetail = new CustomerDetail();
customerDetail.setIdDetail(1L);
customerDetail.setIdCustomer(customer.getIdCustomer());

sessionFactory.getCurrentSession().save(customerDetail);

Se guardan los datos en la tabla padre y luego en la hija.

Con @EmbeddedId:

Account account = new Account();
account.setBalance(33.33);

sessionFactory.getCurrentSession().persist(account);

AccountDetail accountDetail= new AccountDetail();
accountDetail.setAccountDetailPk(AccountDetail.getInstancePk());
accountDetail.setIdDetail(1L);
accountDetail.setIdAccount(account.getIdAccount());

sessionFactory.getCurrentSession().persist(accountDetail);

Resulta un poco más complicado porque hay que manipular la instancia de la llave primaria para insertar los datos.

Como se puede ver  con @EmbeddedId es un poco más complicado manejar el código java, lo mismo aplica para las operaciones de consulta o cualquier otra relacionada con la base, en este aspecto yo prefiero @IdClass.

Rendimiento

Las operaciones con @EmbeddedId requieren que por cada registro se cree un objeto en memoria de la llave compuesta y por lo tanto si tenemos muchos registros el consumo de memoria es mayor, por lo general esta anotación es menos eficiente por consumo para las inserciones, pero puede ser útil para manejar varios campos en un solo objeto.

Referencias

Saludos

20 comentarios en “Hibernate Composite Keys With Annotations

    • jaehoo dijo:

      Le dicen a hibernate que inserte solo los campos que están llenos.

      por ejemplo, si tienes una entidad con 3 campos hibernate al hacer un insert hace algo asi:

      INSERT INTO(CAMPO1, CAMPO2,CAMPO3) values(?,?,?);

      Al activar el “insertable = true” le dices a hibernate que solmanete inserte en los campos del objeto que tiene un valor y creara un insert parecido a esto:

      INSERT INTO(CAMPO1, CAMPO3) values(?,?);

      Así al crear los querys puede reconocer solo los datos que debe de actualizar o insertar.

      • Federico dijo:

        Muchas gracias por la respuesta y muy bueno el blog! Con respecto a las inserciones me surgió un problema. Estoy utilizando SQLite y Hibernate (JPA 1.0) pero no logro que se aplique la integridad referencial (uso de claves primarias) utilizando el entity manager. Haciendo uso de una conexión independiente (sin utilizar hibernate) si logro realizarlas. Alguna idea de cómo poder activarlas??

      • jaehoo dijo:

        pues posiblemente puede ser la relación entre tus entidades, deberias checar el mapeo de tus objetos y sus relaciones o sus anotaciones si es que utilizas, en todo caso una buena prueba puede ser generar el esquema de la base a través de los objetos.

        Si el esquema que genera es correcto puedes estar seguro de que tu relaciones y tu mapeo esta bien.

        puedes buscar hacerlo con el hbm2ddll… o algo asi era el tema

      • Federico dijo:

        El esquema lo genera correctamente pero no genera ni los nombres ni las referencias a otras tablas (solo genera los atributos que deberian referenciar otra tabla pero en ningun momento se verifica, ni mediante software (hibernate) ni mediante base de datos (en los esquemas no figuran las foreign keys). También se permite violar las restricciones de integridad referencial.
        Es decir, las tablas se generan correctamente haciendo caso omiso de las claves foráneas para toda transacción. Me parece que se debe a la relacion SQLite/Hibernate que no está bien pulida…

      • jaehoo dijo:

        Bueno probablemente tienes razon hace un tiempo utilice hibernate con el sqlite, pero no recuerdo bien el esquema pero si me costo trabajo para que funcionara.

        Porque no intentas utilizar mejor el hsqldb, tiene mejor rendimiento y soporte que sqlite, además se integra muy bien con hibernate

  1. smaugb dijo:

    Buen tuto y buenas explicacion de ambos métodos. Mirando la documentación oficial, me he encontrado con el siguiente aviso sobre el uso de @IdClass :
    “This approach is inherited from the EJB 2 days and we recommend against its use. But, after all it’s your application and Hibernate supports it.”
    ¿Qué opinas sobre esto??

    • jaehoo dijo:

      Vaya, pues dejan que el desarrollador decida :p, como mencionaba tu debes tomar lo que necesitas y me da gusto ver que recomiendan @IdClass para mi es de lo más comodo pero si un día lo necesito utilizaré la otra forma, además como bien dicen “es tu aplicación y Hibernate lo soporta”😀

  2. Maximiliano dijo:

    Hola! En primer lugar gracias por el tutorial…tengo un caso similar, con la diferencia que en mi caso, en la tabla hija, la segunda clave primaria (id_detail), referencia a otra tabla. Es decir, tengo una clave compuesta formada por dos claves primarias foraneas, una referencia a una tabla, y la otra a otra tabla. No sé si me expliqué bien, pero creo que debería ser lo mismo, sólo que agregando la referencia de la segunda clave, es decir algo así:

    @ManyToOne
    @ForeignKey(name = “FK_CUS_CUSD”)
    @JoinColumn(name = “id_customer”)
    @JoinColumn(name = “id_detail”)
    private Customer customer;

    Y obviamente agregando la relacion “onetomany”, en la correspondiente clase padre. Es asi??? Espero tu respuesta.
    Desde ya muchísimas gracias!

  3. Elias Vargas dijo:

    Hola! Muy buen tutorial, me ha servido bastante, pero tengo una duda, si por ejemplo existiera una tabla adicional a cualquiera de los dos ejemplos que tuviera 2 llaves primarias y 2 llaves foráneas, por ejemplo en el ACCOUNT y ACCOUNT_DETAIL existiera una tabla adicional llamada ACCOUNT_SERVICE y que sus atributos fueran IdService (PK) y IdAccountDetail(PK, FK), como tendria que mapearla con ACCOUNT_DETAIL ? , ya que lo estoy intentando y no me funciona.

    Gracias!¡
    Saludos.

    • jaehoo dijo:

      Hola hace tiempo que escribí este artículo y tiene algo de tiempo que no utilizao hibernate, lo siento creo que no podria resolver tu duda, sin embargo lo que quieres hacer es que en la trercer tabla una llave primaria sea al mismo tiempo una foranea de otra, esto es posible aunque la combinación si es un poco rar con hibernate.

      Espero puedas encontrarlo, saludos

  4. David Hernández dijo:

    Hola.

    El ejemplo es muy ilustrativo, pero me gustaría saber si es posible (que igual no lo es) tener una solo tabla (no dos) con un clave compuesta (digamos dos/tres campos) y hacerle un persist.

    Ejemplo:
    MiTabla
    int campo1 (PK)
    String campo2 (PK)
    String campo3

    Para crear el Composite-Key:
    Tendría un objecto MiTabla y un objecto MiTablaPK (usando @Embeddable), pero como persisto yo esto. Los valores se los paso yo (no los autogenero).

    Alguna sugerencia?

    Gracias.?

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s