How to Implement a Custom, Sequence-Based ID Generator

By Thorben Janssen

Mapping, Primary Key

A lot of applications use primary keys that are based on a sequence but use an additional prefix that contains semantic information. Here are some typical examples:

  1. A fixed String as a prefix followed by a sequence-based value of fixed or variable length, e.g., B_00001 or B_1
  2. Year and month as a prefix followed by a sequence-based value of fixed or variable length, e.g., 2018-08_00001 or 2018-08_1
  3. An attribute value of a parent entity as a prefix followed by a sequence-based value of fixed or variable length, e.g., MP_00001 or MP_1

You can easily support all 3 of these examples by implementing a custom generator. If your database supports sequences and at least a part of your ID consists of an automatically incremented value, the best way to do that is to extend Hibernate’s SequenceStyleGenerator class. That enables you to use your generator in the same way as any other id generator. You can also benefit from all Hibernate-specific optimizations, like the high-low algorithm which reduces the number of times Hibernate requests a new value from the database sequence.

Let’s use this approach to implement 3 custom sequence generators that you can use with the following Book entity. Each generator will create values that follow the 3 previously discussed primary key formats.

@Entity
public class Book {

	@Id
	private String id;

	@Version
	@Column(name = "version")
	private int version;

	@Column
	private String title;

	@Column
	private LocalDate publishingDate;

	@ManyToOne
	@JoinColumn(name = "publisherid")
	private Publisher publisher;

	...
}

A fixed String followed by a sequence-based value

Hibernate’s SequenceStyleGenerator already does most of the heavy lifting, like the handling of different database dialects or the implementation of various performance optimizations. If you extend that class, you only need to add your prefix and format the sequence value in your preferred way.

public class StringPrefixedSequenceIdGenerator extends SequenceStyleGenerator {

	public static final String VALUE_PREFIX_PARAMETER = "valuePrefix";
	public static final String VALUE_PREFIX_DEFAULT = "";
	private String valuePrefix;

	public static final String NUMBER_FORMAT_PARAMETER = "numberFormat";
	public static final String NUMBER_FORMAT_DEFAULT = "%d";
	private String numberFormat;

	@Override
	public Serializable generate(SharedSessionContractImplementor session,
			Object object) throws HibernateException {
		return valuePrefix + String.format(numberFormat, super.generate(session, object));
	}

	@Override
	public void configure(Type type, Properties params,
			ServiceRegistry serviceRegistry) throws MappingException {
		super.configure(LongType.INSTANCE, params, serviceRegistry);
		valuePrefix = ConfigurationHelper.getString(VALUE_PREFIX_PARAMETER,
				params, VALUE_PREFIX_DEFAULT);
		numberFormat = ConfigurationHelper.getString(NUMBER_FORMAT_PARAMETER,
				params, NUMBER_FORMAT_DEFAULT);
	}

}

As you can see in the code snippet, you only need to override the configure and the generate methods to customize the generator.

The configure method

Hibernate calls the configure method when it instantiates a StringPrefixedSequenceIdGenerator. Within this method, you need to do 2 things:

  1. You need to call the configure method on the superclass and make sure to set the Type parameter to LongType. This is necessary because the sequence value will be part of a String, but Hibernate can’t handle String-based sequences. So, you need to tell Hibernate to generate a sequence value of type Long and convert it afterward.
  2. You can also read all configuration parameters provided for this generator. I will show you how to set these parameters in the following code snippet. Let’s, for now, focus on the handling of these parameters.
    All parameters are part of the Properties params object. You can use the ConfigurationHelper class to get a parameter by its name and to use a default value for undefined parameters. In this example, I read the parameter valuePrefix, which defines the prefix of the generated value, and the numberFormat parameter, which specifies the format of the sequence number. Both of these parameters are used by the generate method.

The generate method

The generate method gets called when Hibernate needs a primary key value to persist a new entity. The implementation of it is pretty simple. You call the generate method on the superclass to get the next value from the sequence, transform that value into the configured format and add it to the defined prefix.

Use the StringPrefixedSequenceIdGenerator

That’s all you need to do to implement your custom, sequence-based generator. You can now add a @GeneratedValue and a @GenericGenerator annotation to your Book entity to use the generator.

@Entity
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
	@GenericGenerator(
		name = "book_seq", 
		strategy = "org.thoughts.on.java.generators.StringPrefixedSequenceIdGenerator", 
		parameters = {
			@Parameter(name = StringPrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "50"),
			@Parameter(name = StringPrefixedSequenceIdGenerator.VALUE_PREFIX_PARAMETER, value = "B_"),
			@Parameter(name = StringPrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = "%05d") })
	private String id;

	...
}

You probably already used the @GeneratedValue annotation to tell Hibernate to use a database sequence or an auto-incremented database column to generate your primary key values.

If you want to use a custom generator, you need to define the generator in a @GenericGenerator annotation and provide the fully-qualified classname as the strategy. You can also configure a set of parameters that will be provided to the configure method when Hibernate instantiates the generator. In this example, I set the following 3, optional parameters:

  1. INCREMENT_PARAM activates Hibernate’s high-low optimization. The @SequenceGenerator annotation sets this parameter by default, and I highly recommend that you configure it for your custom generator.
  2. VALUE_PREFIX_PARAMETER defines the prefix of your primary key value. This is one of the parameters handled in the configure method.
  3. NUMBER_FORMAT_PARAMETER specifies the format String of the sequence value. In this example, I add padding zeros to ensure that value consists of 5 digits.

When you now persist a new Book entity, Hibernate generates a primary key following the format B_00001.

14:58:34,262 DEBUG [org.hibernate.SQL] - insert into Book (publisherid, publishingDate, title, version, id) values (?, ?, ?, ?, ?)
14:58:34,263 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [1] as [BIGINT] - [1]
14:58:34,264 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [2] as [DATE] - [2017-04-04]
14:58:34,265 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [3] as [VARCHAR] - [Hibernate Tips - More than 70 solutions to common Hibernate problems]
14:58:34,266 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [4] as [INTEGER] - [0]
14:58:34,266 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [5] as [VARCHAR] - [B_00001]

Year and month followed by a sequence-based value

The implementation of a custom id generator that uses parts of the current date as a prefix is pretty similar to the one that uses a configurable String. The main difference is that you need to configure a date format that will be used to create the prefix. The rest of it follows the same principle:

  1. You extend Hibernate’s SequenceStyleGenerator class,
  2. override the configure method to read the additional configuration parameters and
  3. override the generate method to add the date as a prefix.

Here you can see an example implementation:

public class DatePrefixedSequenceIdGenerator extends SequenceStyleGenerator {
	
	public static final String DATE_FORMAT_PARAMETER = "dateFormat";
	public static final String DATE_FORMAT_DEFAULT = "%tY-%tm";
	
	public static final String NUMBER_FORMAT_PARAMETER = "numberFormat";
	public static final String NUMBER_FORMAT_DEFAULT = "%05d";
	
	public static final String DATE_NUMBER_SEPARATOR_PARAMETER = "dateNumberSeparator";
	public static final String DATE_NUMBER_SEPARATOR_DEFAULT = "_";
	
	private String format;
	
	@Override
	public Serializable generate(SharedSessionContractImplementor session,
			Object object) throws HibernateException {
		return String.format(format, LocalDate.now(), super.generate(session, object));
	}

	@Override
	public void configure(Type type, Properties params,
			ServiceRegistry serviceRegistry) throws MappingException {
		super.configure(LongType.INSTANCE, params, serviceRegistry);

		String dateFormat = ConfigurationHelper.getString(DATE_FORMAT_PARAMETER, params, DATE_FORMAT_DEFAULT).replace("%", "%1"); 
		String numberFormat = ConfigurationHelper.getString(NUMBER_FORMAT_PARAMETER, params, NUMBER_FORMAT_DEFAULT).replace("%", "%2"); 
		String dateNumberSeparator = ConfigurationHelper.getString(DATE_NUMBER_SEPARATOR_PARAMETER, params, DATE_NUMBER_SEPARATOR_DEFAULT); 
		this.format = dateFormat+dateNumberSeparator+numberFormat; 
	} 
}

A common scenario for this kind of id generator requires you to reset the sequence number at the end of each day or month or year. This can’t be handled by the id generator. You need to configure a job that resets your database sequence in the required intervals and the DatePrefixedSequenceIdGenerator will automatically get the new values when it requests the next value from the sequence.

Use the DatePrefixedSequenceIdGenerator

If you use the default configuration, the DatePrefixedSequenceIdGenerator generates ids that follow the format 2018-08_00001. You can customize the format by setting the DATE_FORMAT_PARAMETER, NUMBER_FORMAT_PARAMETER and DATE_NUMBER_SEPARATOR_PARAMETER parameters.

@Entity
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
	@GenericGenerator(
		name = "book_seq", 
		strategy = "org.thoughts.on.java.generators.DatePrefixedSequenceIdGenerator", 
		parameters = {@Parameter(name = DatePrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "50")})
	private String id;

	...
}
15:51:03,318 DEBUG [org.hibernate.SQL] - insert into Book (publisherid, publishingDate, title, version, id) values (?, ?, ?, ?, ?)
15:51:03,318 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [1] as [BIGINT] - [1]
15:51:03,321 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [2] as [DATE] - [2017-04-04]
15:51:03,324 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [3] as [VARCHAR] - [Hibernate Tips - More than 70 solutions to common Hibernate problems]
15:51:03,327 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [4] as [INTEGER] - [0]
15:51:03,328 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [5] as [VARCHAR] - [2018-08_00001]

An attribute value of a parent entity followed by a sequence-based value

You’re probably not surprised if I tell you that the implementation of this id generator is pretty similar to the 2 previous ones. The main difference is that I use the Object object parameter of the generate method to create the primary key value.

That Object contains the entity object for which the PublisherCodePrefixedSequenceIdGenerator got called. You can use it to access all attributes of that entity including all initialized associations. In this example, I use it to get the value of the code attribute of the associated Publisher entity.

Please be aware, that this code requires an initialized publisher association and will fail if this Book has no Publisher.

public class PublisherCodePrefixedSequenceIdGenerator extends SequenceStyleGenerator {
	
	public static final String CODE_NUMBER_SEPARATOR_PARAMETER = "codeNumberSeparator";
	public static final String CODE_NUMBER_SEPARATOR_DEFAULT = "_";
	
	public static final String NUMBER_FORMAT_PARAMETER = "numberFormat";
	public static final String NUMBER_FORMAT_DEFAULT = "%05d";
	
	private String format;
	
	@Override
	public Serializable generate(SharedSessionContractImplementor session,
			Object object) throws HibernateException {
		return String.format(format, ((Book)object).getPublisher().getCode(), super.generate(session, object));
	}
	
	@Override
	public void configure(Type type, Properties params,
			ServiceRegistry serviceRegistry) throws MappingException {
		super.configure(LongType.INSTANCE, params, serviceRegistry);
		String codeNumberSeparator = ConfigurationHelper.getString(CODE_NUMBER_SEPARATOR_PARAMETER, params, CODE_NUMBER_SEPARATOR_DEFAULT);
		String numberFormat = ConfigurationHelper.getString(NUMBER_FORMAT_PARAMETER, params, NUMBER_FORMAT_DEFAULT).replace("%", "%2"); 
		this.format = "%1$s"+codeNumberSeparator+numberFormat; 
	} 
}

Use the PublisherCodePrefixedSequenceIdGenerator

You can use the PublisherCodePrefixedSequenceIdGenerator in the same ways as the previously discussed id generators.

@Entity
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
	@GenericGenerator(
			name = "book_seq", 
			strategy = "org.thoughts.on.java.generators.PublisherCodePrefixedSequenceIdGenerator", 
			parameters = { 
					@Parameter(name = PublisherCodePrefixedSequenceIdGenerator.INCREMENT_PARAM, value = "50"),
					@Parameter(name = PublisherCodePrefixedSequenceIdGenerator.CODE_NUMBER_SEPARATOR_PARAMETER, value = "_"), 
					@Parameter(name = PublisherCodePrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = "%05d")})
	private String id;

	...
}

For a Publisher with the code MP, this generator creates primary key values that follow the format MP_00001.

16:03:29,307 DEBUG [org.hibernate.SQL] - insert into Book (publisherid, publishingDate, title, version, id) values (?, ?, ?, ?, ?)
16:03:29,308 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [1] as [BIGINT] - [1]
16:03:29,309 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [2] as [DATE] - [2017-04-04]
16:03:29,311 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [3] as [VARCHAR] - [Hibernate Tips - More than 70 solutions to common Hibernate problems]
16:03:29,312 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [4] as [INTEGER] - [0]
16:03:29,312 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] - binding parameter [5] as [VARCHAR] - [MP_00001]

Summary

JPA and Hibernate support a set of standard id generators which enable you to use database sequences and auto-incremented columns. If you need to generate more complex values that contain additional semantic information, you can extend the existing generators. We used that approach in this tutorial to create id generators that add a configurable String, date information or an attribute value of an associated entity as a prefix.


Tags

Mapping, Primary Key


About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

Leave a Repl​​​​​y

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.

  1. 2020-06-07 19:53:18.795 WARN 1988 — [nio-2424-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 42P01
    2020-06-07 19:53:18.795 ERROR 1988 — [nio-2424-exec-3] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: relation “event_seq” does not exist
    Position: 17
    2020-06-07 19:53:18.803 ERROR 1988 — [nio-2424-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet] with root cause

    org.postgresql.util.PSQLException: ERROR: relation “event_seq” does not exist
    Position: 17

    my problem is that the generator name is said to be not exist

    this is entity part:

    @Entity
    @Table(name = “tbl_events”)
    public class Event {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY, generator = “event_seq”)
    @GenericGenerator(name = “event_seq”,
    strategy = “com.dipesh.springsecurity.generator.StringPrefixedSequenceIdGenerator”,
    parameters = {
    @Parameter(name = StringPrefixedSequenceIdGenerator.INCREMENT_PARAM, value = “50”),
    @Parameter(name = StringPrefixedSequenceIdGenerator.VALUE_PREFIX_PARAMETER, value = “E_”),
    @Parameter(name = StringPrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = “%05d”)
    })
    @Column(name = “event_id”, nullable = false, updatable = false)
    private String eventId;

    1. Hi dimebag,

      The generator expects that there is a database sequence with the configured name. In your case, that’s “event_seq”.
      Based on the error message, that database sequence is missing. This is indicated by the exception class “org.postgresql.util.PSQLException”, which shows that the error occurred in PostgreSQL and not in Hibernate.

      Regards,
      Thorben

    2. Hi dimebag,

      Your strategy should also be GenerationType.SEQUENCE because you’re using a sequence-based generator. @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = “event_seq”)

      Regards,
      Thorben

  2. I receive error due to sequence style mapping with oracle database

    org.hibernate.MappingException: Could not interpret id generator strategy

    What needs to be fixed? because it seems the strategy and the sequence made in the oracle looks different

    1. Hi Herman,

      That’s impossible to say without seeing your code. Please share your mapping code here or on stackoverflow.

      Regards,
      Thorben

  3. Thanks for this blog,
    is it somehow possible to tell hibernate to automatically generate that sequence for me when using hbm2ddl.auto=create

  4. Thanks for the great article. I have a sequence in oracle. How do I specify the name of the sequence in this approach?

    1. Hibernate’s SequenceStyleGenerator class enables you to set the name of the sequence using a @Parameter with name SequenceStyleGenerator.SEQUENCE_PARAM.
      Because the custom generator extends that class, you can use the same paramter.

      @GenericGenerator(
      name = “book_seq”,
      strategy = “org.thoughts.on.java.generators.PublisherCodePrefixedSequenceIdGenerator”,
      parameters = {
      @Parameter(name = PublisherCodePrefixedSequenceIdGenerator.INCREMENT_PARAM, value = “50”),
      @Parameter(name = PublisherCodePrefixedSequenceIdGenerator.CODE_NUMBER_SEPARATOR_PARAMETER, value = “_”),
      @Parameter(name = PublisherCodePrefixedSequenceIdGenerator.NUMBER_FORMAT_PARAMETER, value = “%05d”),
      @Parameter(name = SequenceStyleGenerator.SEQUENCE_PARAM, value=”my_seq”)})
      private String id;

  5. Hello,
    First of all thank you for this great article.
    I’have a issue for the first case of : “A fixed String followed by a sequence-based value”.
    How to handle duplicates ? For example the select next value for custom_sequence equal 10 but it’s already there as : B_00010 ? How to jump to the next available value which will be : B_00011 ?
    Thank you so much in advance !

    1. Hi,

      The database sequence provides the next value and you only add the prefix. As long as you don’t assign any IDs manually (which you should never do when using a sequence), there shouldn’t be any duplicates.

      Regards,
      Thorben

  6. I think it may not be possible to use these in a clustered environment where different nodes may generate the same sequence.
    Is it possible to create generators by composing generators? Thereby, we can use the database sequence to generate the unique part.

    1. Hi Chandra,

      This generator works fine in a clustered environment. It extends Hibernate’s SequenceStyleGenerator class, which calls a database sequence to generate the unique part of the ID.

      Regards,
      Thorben

  7. Thanks for the article Thorben. While it’s easy to use database features to generate a primary key, if you want something with a string (as you’ve mentioned), it’s great to be able to do this in code. I didn’t realise it was this easy to do that. Thanks for sharing.

    1. That’s, of course, also an option and I did that in some of my projects. But you then need to make sure that you add the prefix in all reports, views, …

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}