How to map a composite identifier using an automatically @GeneratedValue with JPA and Hibernate
Introduction
One of my readers asked me to answer the following StackOverflow question.
While I already covered the best way to map composite identifiers with JPA and Hibernate, this use case is different because one column is automatically generated.
Domain Model
Considering we have the following book
database table:
The identifier is formed out of two columns:
publisher_id
which designates a given publisherregistration_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!
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
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.
Download free ebook sample

If you subscribe to my newsletter, you'll get:
- A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
- 3 chapters from my book, High-Performance Java Persistence,
- a 10% discount coupon for my book.
Hi Vlad!
I am trying this out. Both option one and option 2. However I am unable to insert the data. When using option one the select statement fails for next val. I am using sql server 2016.
Then, I moved to option 2. In here it is still trying to set the identity field to a null value. So could you elaborate this statement in your article and how to do this: ? ” 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.”
Regards
Sud
All the code is on GitHub and you can test it on SQL Server too. Do a comparison debug and see the difference.
I have very simple model entity using @idClass with in memory h2 db. My entity is simple, non-relational. Composite key is made of 2 fields, one id annotated with @GeneratedValue and when i try persist using spring repository i get exception saying cannot set my my composite key id via reflection. It works if i remove @GeneratedValue annotation. I’ve read numerous posts saying @idClass can’t be used with generated value so not sure
Do it as I described in this article and it will work.
Does it really work? can you show working code please? I have spent hours together to make it work without luck.
It works like a charm. Check out my high-performance java persistence GitHub repository if you want to get the source code. Use this free video episode from my course to find out how to set up the repository.
Hi, Im trying your example with @SQLInsert at my study:
@Entity
@SQLInsert(sql = “insert into book (title, publisher_id) values (?, ?)”)
public class Book {
@EmbeddedId
private Id id;
private String title;
public void setTitle(String title) {
this.title = title;
}
public void setId(Id id) {
this.id = id;
}
@Embeddable
public static class Id implements Serializable {
@Column(name = "registration_number")
private Long registrationNumber;
@Column(name = "published_id")
private Long publisherId;
public void setPublisherId(Long publisherId) {
this.publisherId = publisherId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Id that = (Id) o;
return Objects.equals(registrationNumber, that.registrationNumber) &&
Objects.equals(publisherId, that.publisherId);
}
@Override
public int hashCode() {
return Objects.hash(registrationNumber, publisherId);
}
}
}
I’m getting the following error:
Caused by: org.postgresql.util.PSQLException: The column index is out of range: 3, number of columns: 2.
My suspicion is that the insert is considering all attributes of my entity (title, publisher_id, registration_number) at the time of binding.
Is there any way to ignore the registration_number property at bind?
Is there any way to set the order in which these values in bind?
Either you remove the
registrationNumber
from the Embeddable or you set it to insertable/updatable false.setting the column as-
@Column(name = “registration_number”, insertable = false, updatable = false)
private Long registrationNumber;
is still not working.
All examples are on GitHub and work just fine.