How to implement a custom String-based sequence identifier generator with Hibernate

(Last Updated On: January 29, 2018)

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();

        sequencePrefix = ConfigurationHelper.getString(
                SEQUENCE_PREFIX,
                params,
                "SEQ_");

        final String sequencePerEntitySuffix = ConfigurationHelper.getString(
                SequenceStyleGenerator.CONFIG_SEQUENCE_PER_ENTITY_SUFFIX,
                params,
                SequenceStyleGenerator.DEF_SEQUENCE_SUFFIX);

        final String defaultSequenceName = ConfigurationHelper.getBoolean(
                SequenceStyleGenerator.CONFIG_PREFER_SEQUENCE_PER_ENTITY,
                params,
                false)
                ? params.getProperty(JPA_ENTITY_NAME) + sequencePerEntitySuffix
                : SequenceStyleGenerator.DEF_SEQUENCE_NAME;

        sequenceCallSyntax = dialect.getSequenceNextValString(
                ConfigurationHelper.getString(
                        SequenceStyleGenerator.SEQUENCE_PARAM,
                        params,
                        defaultSequenceName));
    }

    @Override
    public Serializable generate(SessionImplementor 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)
            .createSQLQuery(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.

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.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

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. 
Get the most out of your persistence layer!

Advertisements

12 thoughts on “How to implement a custom String-based sequence identifier generator with Hibernate

  1. Great article!

    Any ideas how to make Hibernate initialize sequence in database? Currently there is no sequence created during ddl initializaton 🙁

  2. Hi Vlad,

    I was trying down this option and in this below case i’m running in to Stackoverflow errors as shown below.

    So i have Classroom object and Student object and so, i create a classroom object and then have a collection of students inside it. So i’m iterating through the students and creating a new instance and adding it

    code is something like
    for (StudentDTO s : studentDTOs) {
    Student s1 = new Student();
    :
    :
    classroom.getStudents().add(s1);
    }

    In this case it looks like the sequence generator is called repeatedly and errors out with the below error. any suggestions?

    Caused by: java.lang.StackOverflowError
    at org.hibernate.engine.spi.CascadingAction$8.noCascade(CascadingAction.java:372)
    at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:177)
    at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:424)

    1. Fork my high-performance-java-persistence GitHub repository, copy the original test case and modify it so that you can replicate the issue. Then send me a Pull Request so I can see what’s wrong with it.

  3. Hello,

    I have this error:
    Caused by: org.hibernate.MappingException: org.hibernate.dialect.MySQL5InnoDBDialect does not support sequences
    at org.hibernate.dialect.Dialect.getSequenceNextValString(Dialect.java:822) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
    at com.insurance.ins.config.StringSequenceIdentifier.configure(StringSequenceIdentifier.java:59) ~[classes/:na]
    at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.createIdentifierGenerator(DefaultIdentifierGeneratorFactory.java:118) ~[hibernate-core-5.2.14.Final.jar:5.2.14.Final]
    … 37 common frames omitted

    what I should do?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.