Recap on former solution
The approach from Jonas Gröger (here) and rbento (here) is working fine with an up-to-date hibernate version (at least since 5.2.17). The configuration mentioned here, hibernate.jpa.compliance.global_id_generators (default false) is relevant, to have this working. See documentation here and here:
The JPA spec says that the scope of TableGenerator and SequenceGenerator names is global to the persistence unit (across all generator types).
Traditionally, Hibernate has considered the names locally scoped.
If enabled, the names used by @TableGenerator and @SequenceGenerator will be considered global so configuring two different generators with the same name will cause a java.lang.IllegalArgumentException to be thrown at boot time.
However, this approach has a major drawback. I have been running into a mean bug with it.
If one has the setup, mentioned in the other answers, but one omits or forgets one @SequenceGenerator annotation on one entity, one runs into a problem and does not notice it directly.
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ID")
private Long id;
}
@SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_USER")
public class User extends BaseEntity { ... }
// no @SequenceGenerator here...
public class Order extends BaseEntity { ... }
There are two things, which may happen now.
- If the
Order entity is processed first, it will fall back on trying to use a sequence, named like the generator SEQ_ID, depending on some more configuration (see SequenceStyleGenerator and StandardNamingStrategy; implementation has changed slightly from the version I'm using right now).
- If the
User entity is processed first, the SEQ_USER is stored globally for the generator name SEQ_ID. With this, the SEQ_USER will as well be used for the Order entity because it uses the same generator (from the base entity).
private static IdentifierGeneratorDefinition getIdentifierGenerator( ... ) {
if ( localGenerators != null ) {
final IdentifierGeneratorDefinition result = localGenerators.get( name );
if ( result != null ) {
return result;
}
}
final IdentifierGeneratorDefinition globalDefinition = buildingContext.getMetadataCollector().getIdentifierGenerator( name );
if ( globalDefinition != null ) {
return globalDefinition;
}
...
}
(old v5 code from org.hibernate.cfg.BinderHelper; localGenerators are the ones annotated on a class like User, the global definition stores already used sequences per generator)
Both ways are pretty much unintended and the latter one a pretty well hidden bug leading possibly to unique constraint violations.
It even becomes more complex and error-prone, if one uses multiple derived entities.
Primary Problem
Sadly, it is not possible, to annotate the base entity with a proper default, like
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ID")
@SequenceGenerator(name = "SEQ_ID", sequenceName = "DEFAULT_SEQ")
private Long id;
}
and overwrite it in the derived entity.
@SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_USER")
public class User extends BaseEntity { ... }
The generator on the BaseEntity will always win.
private static void processId( ... ) {
...
if ( ... ) {
...
} else {
//clone classGenerator and override with local values
HashMap<String, IdentifierGeneratorDefinition> localGenerators = (HashMap<String, IdentifierGeneratorDefinition>) classGenerators.clone();
localGenerators.putAll( buildGenerators( idXProperty, buildingContext ) );
BinderHelper.makeIdGenerator(
idValue,
idXProperty,
generatorType,
generatorName,
buildingContext,
localGenerators
);
}
...
}
(old v5 code from org.hibernate.cfg.AnnotationBinder; classGenerator is the one from the class-level annotation and the buildGenerators-call ends up with the globally defined sequence generator)
Solution
However, I found a solution (which one may call a little hacky) based on some overridden functionality, inspired by implement a custom String sequence identifier generator and this blogpost.
I use a custom generator, which is actually the same as a @SequenceGenerator with a specific implementation extending the SequenceStyleGenerator
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ID")
@GenericGenerator( name = "SEQ_ID",
strategy = "my.package.PreferClassSequenceStyleGenerator",
parameters = {
@org.hibernate.annotations.Parameter( name = "sequence_name", value = "DEFAULT_SEQ" ),
// default values from @SequenceGenerator
@org.hibernate.annotations.Parameter( name = "initial_value", value = "1" ),
@org.hibernate.annotations.Parameter( name = "increment_size", value = "50" )
} )
private Long id;
}
and overwrite it in the derived entity.
@SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_USER")
public class User extends BaseEntity { ... }
PreferClassSequenceStyleGenerator
(based on hibernate 5.x)
public class PreferClassSequenceStyleGenerator extends SequenceStyleGenerator
{
@Override
protected QualifiedName determineSequenceName(
Properties params,
Dialect dialect,
JdbcEnvironment jdbcEnv,
ServiceRegistry serviceRegistry )
{
String entityName = params.getProperty( IdentifierGenerator.ENTITY_NAME );
String generatorName = params.getProperty( IdentifierGenerator.GENERATOR_NAME );
try {
Class<?> entityClass = getClass().getClassLoader().loadClass( entityName );
SequenceGenerator annotation = entityClass.getAnnotation( SequenceGenerator.class );
if( annotation != null ) {
String name = annotation.name();
if( StringUtils.equalsIgnoreCase( name, generatorName ) ) {
IdGeneratorInterpreterImpl idGeneratorInterpreter = new IdGeneratorInterpreterImpl();
Builder definitionBuilder = new IdentifierGeneratorDefinition.Builder();
idGeneratorInterpreter.interpretSequenceGenerator( annotation, definitionBuilder );
Map<String, String> parameters = definitionBuilder.build().getParameters();
params.putAll( parameters );
}
}
}
catch( Exception e ) {
// do some logging
}
return super.determineSequenceName( params, dialect, jdbcEnv, serviceRegistry );
}
}
With this custom SequenceStyleGenerator, I just take the class-based annotation and use it instead of the proposed one (which would be the one from the id field), but only if the class based annotation is present. Otherwise, I fall back to a proper default without any weird and surprising behavior as described above.
Outlook
I'm pretty sure, that one can find a better solution with hibernate v6. Looking into the migration guide and the sections @IdGeneratorType (see here as well), Implicit Identifier Sequence and Table Name and Defaults for implicit sequence generators. Especially hibernate.id.db_structure_naming_strategy with org.hibernate.id.enhanced.ImplicitDatabaseObjectNamingStrategy may provide a better solution.