Considering we have the following book database table:
The identifier is formed out of two columns:
publisher_id which designates a given publisher
registration_number which is an auto-incremented number given by the Publisher
Now, we need to map this relation using JPA and Hibernate, so let’s see how we can do it.
Database supports SEQUENCE objects
If the database supports SEQUENCE objects natively, the mapping is really simple, and it’s probably the only use case where we ever need to use the JPA @IdClass.
So, we start with the @IdClass definition which will be used to wrap the composite identifier:
public class PK implements Serializable {
private Long registrationNumber;
private Integer publisherId;
public PK(Long registrationNumber, Integer publisherId) {
this.registrationNumber = registrationNumber;
this.publisherId = publisherId;
}
private PK() {
}
//Getters and setters omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PK pk = (PK) o;
return Objects.equals( registrationNumber, pk.registrationNumber ) &&
Objects.equals( publisherId, pk.publisherId );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber, publisherId );
}
}
The Book entity will look as follows:
@Entity(name = "Book")
@Table(name = "book")
@IdClass( PK.class )
public class Book {
@Id
@Column(name = "registration_number")
@GeneratedValue
private Long registrationNumber;
@Id
@Column(name = "publisher_id")
private Integer publisherId;
private String title;
//Getters and setters omitted for brevity
}
Note that the registrationNumber uses the @GeneratedValue annotation since we want this column to be automatically generated on every insert.
Testing time
When running this test case:
Book _book = doInJPA(entityManager -> {
Book book = new Book();
book.setPublisherId( 1 );
book.setTitle( "High-Performance Java Persistence");
entityManager.persist(book);
return book;
});
doInJPA(entityManager -> {
PK key = new PK( _book.getRegistrationNumber(), 1);
Book book = entityManager.find(Book.class, key);
assertEquals(
"High-Performance Java Persistence",
book.getTitle()
);
});
Hibernate generates the following SQL statements:
SELECT NEXTVAL ('hibernate_sequence')
INSERT INTO book (title, publisher_id, registration_number)
VALUES ('High-Performance Java Persistence', 1, 1)
SELECT
b.publisher_id as publishe1_0_0_,
b.registration_number as registra2_0_0_,
b.title as title3_0_0_
FROM
book b
WHERE
b.publisher_id = 1 AND
b.registration_number = 1
The @GeneratedValue annotation tells Hibernate to assign the registration_number column with a value coming from the associated database sequence.
Database supports IDENTITY objects
Now, things get a little bit more complicated if the database does not support SEQUENCE objects (e.g. MySQL 5.7).
This time, we can wrap the composite identifier in an Embeddable type:
@Embeddable
public class EmbeddedKey implements Serializable {
@Column(name = "registration_number")
private Long registrationNumber;
@Column(name = "publisher_id")
private Integer publisherId;
//Getters and setters omitted for brevity
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
EmbeddedKey that = (EmbeddedKey) o;
return Objects.equals( registrationNumber, that.registrationNumber ) &&
Objects.equals( publisherId, that.publisherId );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber, publisherId );
}
}
And the Book entity is going to be mapped as follows:
@Entity(name = "Book")
@Table(name = "book")
@SQLInsert(
sql = "insert into book (title, publisher_id, version) values (?, ?, ?)"
)
public class Book implements Serializable {
@EmbeddedId
private EmbeddedKey key;
private String title;
@Version
@Column(insertable = false)
private Integer version;
//Getters and setters omitted for brevity
}
Because we can’t assign the @GeneratedValue on the Embeddable type, we rely on the database only to specify the IDENTITY column:
CREATE TABLE book (
publisher_id INT NOT NULL,
registration_number BIGINT IDENTITY NOT NULL,
title VARCHAR(255),
version INT,
PRIMARY KEY (publisher_id, registration_number)
)
So, we just need to make sure we omit the registration_number column when inserting the post table row. Now, because the identifier columns are mandatory, we can’t just set its insertable attribute to false.
So, we need to provide a custom INSERT statement using the @SQLInsert Hibernate annotation. But because the NULL value from registrationNumber attribute will still be bound to the PreparedStatement, we can instruct Hibernate to set the version column instead.
Testing time
When running the previous test:
doInJPA(entityManager -> {
Book book = new Book();
book.setTitle( "High-Performance Java Persistence");
EmbeddedKey key = new EmbeddedKey();
key.setPublisherId(1);
book.setKey(key);
entityManager.persist(book);
});
doInJPA(entityManager -> {
EmbeddedKey key = new EmbeddedKey();
key.setPublisherId(1);
key.setRegistrationNumber(1L);
Book book = entityManager.find(Book.class, key);
assertEquals(
"High-Performance Java Persistence",
book.getTitle()
);
});
Hibernate generates the following SQL statements:
INSERT INTO book (title, publisher_id, version)
VALUES ('High-Performance Java Persistence', 1, NULL(BIGINT)
SELECT
b.publisher_id as publishe1_0_0_,
b.registration_number as registra2_0_0_,
b.title as title3_0_0_,
b.version as version4_0_0_
FROM book
b
WHERE
b.publisher_id = 1 AND
b.registration_number = 1
That’s it!
Online Workshops
If you enjoyed this article, I bet you are going to love my upcoming Online Workshops.
Although not a very common mapping, you can map a composite identifier where one of the Primary Key columns is auto-generated. While for a SEQUENCE identifier, we can use the JPA specification, for IDENTITY, we need to use the Hibernate-specific @SQLInsert annotation. Nevertheless, this mapping is possible when using Hibernate.