How to implement a custom String-based sequence identifier generator with Hibernate
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
You can earn a significant passive income stream from promoting my book, courses, tools, training, or coaching subscriptions.
If you're interested in supplementing your income, then join my affiliate program.
Introduction
One of my blog readers bumped into the assigned generator with a sequence or an identity column post and wondered if it was possible to generate String-based identifiers instead.
I accepted the challenge and answered his question on StackOverflow. However, this post is going to explain this topic in greater detail, so there we go.
The custom identifier generator
We need a Hibernate identifier generator that can take any value that we manually assign, and it can also automatically generate a unique identifier when the entity identifier is null. However, the user does not want to use a UUID-like random identifier. Instead, the user needs to generate a String
value that combines a prefix and a numerical value that is obtained from a database sequence.
Our custom identifier generator looks like this:
public class StringSequenceIdentifier implements IdentifierGenerator, Configurable { public static final String SEQUENCE_PREFIX = "sequence_prefix"; private String sequencePrefix; private String sequenceCallSyntax; @Override public void configure( Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException { final JdbcEnvironment jdbcEnvironment = serviceRegistry .getService( JdbcEnvironment.class ); final Dialect dialect = jdbcEnvironment.getDialect(); final ConfigurationService configurationService = serviceRegistry .getService( ConfigurationService.class ); String globalEntityIdentifierPrefix = configurationService .getSetting( "entity.identifier.prefix", String.class, "SEQ_" ); sequencePrefix = ConfigurationHelper .getString( SEQUENCE_PREFIX, params, globalEntityIdentifierPrefix ); final String sequencePerEntitySuffix = ConfigurationHelper .getString( SequenceStyleGenerator.CONFIG_SEQUENCE_PER_ENTITY_SUFFIX, params, SequenceStyleGenerator.DEF_SEQUENCE_SUFFIX ); boolean preferSequencePerEntity = ConfigurationHelper .getBoolean( SequenceStyleGenerator.CONFIG_PREFER_SEQUENCE_PER_ENTITY, params, false ); final String defaultSequenceName = preferSequencePerEntity ? params.getProperty(JPA_ENTITY_NAME) + sequencePerEntitySuffix : SequenceStyleGenerator.DEF_SEQUENCE_NAME; sequenceCallSyntax = dialect .getSequenceNextValString( ConfigurationHelper.getString( SequenceStyleGenerator.SEQUENCE_PARAM, params, defaultSequenceName ) ); } @Override public Serializable generate( SharedSessionContractImplementor session, Object obj) { if (obj instanceof Identifiable) { Identifiable identifiable = (Identifiable) obj; Serializable id = identifiable.getId(); if (id != null) { return id; } } long seqValue = ((Number) Session.class.cast(session) .createNativeQuery(sequenceCallSyntax) .uniqueResult() ).longValue(); return sequencePrefix + String.format("%011d%s", 0 ,seqValue); } }
The sequenceCallSyntax
holds the underlying database-specific way of calling a sequence. When the generate
method is called, we only generate an identifier if the user hasn’t provided a non-nullable value. To construct the String-based identifier, we fetch a new sequence value from the database and concatenate it with the given prefix.
Both the database sequence name and the prefix are configurable, as demonstrated by the entity mapping:
@Entity(name = "Post") @Table(name = "post") public class Post implements Identifiable<String> { @Id @GenericGenerator( name = "assigned-sequence", strategy = "com.vladmihalcea.book.hpjp.hibernate.identifier.StringSequenceIdentifier", parameters = { @org.hibernate.annotations.Parameter( name = "sequence_name", value = "hibernate_sequence"), @org.hibernate.annotations.Parameter( name = "sequence_prefix", value = "CTC_"), } ) @GeneratedValue( generator = "assigned-sequence", strategy = GenerationType.SEQUENCE) private String id; @Version private Integer version; public Post() { } public Post(String id) { this.id = id; } @Override public String getId() { return id; } }
Testing time
With this mapping in place, it’s time to persist several entities and see what identifiers are saved in the database:
doInJPA(entityManager -> { entityManager.persist(new Post()); entityManager.persist(new Post("ABC")); entityManager.persist(new Post()); entityManager.persist(new Post("DEF")); entityManager.persist(new Post()); entityManager.persist(new Post()); });
When running the test case above on PostgreSQL, Hibernate generates the following statements:
SELECT nextval ('hibernate_sequence') SELECT nextval ('hibernate_sequence') SELECT nextval ('hibernate_sequence') SELECT nextval ('hibernate_sequence') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000001') INSERT INTO post (version, id) VALUES (0, 'ABC') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000002') INSERT INTO post (version, id) VALUES (0, 'DEF') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000003') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000004')
When running the same test case on Oracle, the sequence syntax changes appropriately:
SELECT hibernate_sequence.nextval FROM dual SELECT hibernate_sequence.nextval FROM dual SELECT hibernate_sequence.nextval FROM dual SELECT hibernate_sequence.nextval FROM dual INSERT INTO post (version, id) VALUES (0, 'CTC_000000000001') INSERT INTO post (version, id) VALUES (0, 'ABC') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000002') INSERT INTO post (version, id) VALUES (0, 'DEF') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000003') INSERT INTO post (version, id) VALUES (0, 'CTC_000000000004')
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
And there is more!
You can earn a significant passive income stream from promoting all these amazing products that I have been creating.
If you're interested in supplementing your income, then join my affiliate program.
Conclusion
Hibernate is highly extendable, and the identifier generators are no different. From custom types to identifier generators, @Formula
, @Where
, or @Any
mappings, Hibernate allows you to tackle any data binding requirement you may have.
Code available on GitHub.
