39

I'm using JPA2 with Hibernate and try to introduce a common base class for my entities. So far it looks like that:

@MappedSuperclass
public abstract class BaseEntity {

    @Id
    private Long id;

    @Override
    public int hashCode() {
        // ...
    }

    @Override
    public boolean equals(Object obj) {
        // ...
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

However, for every table theres a sequence $entityname_seq which I want to use as my sequence generator. How can I set that from my subclass? I think I need to override @GeneratedValue and create a new SequenceGenerator with @SequenceGenerator.

5 Answers 5

42

Yes, it is possible. You can override the default generator name with the @SequenceGenerator annotation.

  • Base class
    @MappedSuperclass
    public abstract class PersistentEntity implements Serializable
    {
        private static final long serialVersionUID = 1L;

        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "default_gen")
        protected Long id = 0L;

        public Long getId()
        {
            return id;
        }

        public void setId(Long id)
        { 
            this.id = id;
        }
    }
  • Sequence (SQL)

    create sequence role_seq;
  • Derived class

    @Entity
    @Table(name = "role")
    @SequenceGenerator(name = "default_gen", sequenceName = "role_seq", allocationSize = 1)
    public class Role extends PersistentEntity implements Serializable
    {
        private static final long serialVersionUID = 1L;

        @NotNull
        @Size(max = 32)
        private String name;

        public String getName()
        {
             return name;
        }

        public void setName(String name)
        {
             this.name = name;
        }   
    }
  • This approach worked fine in Hibernate 4.1.x, but it didn't in EclipseLink 2.x.

edit

  • As per the comment, it seems to be working with EclipseLink 2.6.1-RC1.
Sign up to request clarification or add additional context in comments.

11 Comments

I've tested with EclipseLink 2.6.1-RC1 and it worked too.
That's great news. Thank you! I'm gonna edit the answer.
This is an incorrect solution. It will only work as long as you only have one sub-class of PersistentEntity. Say you create entity "Group" which also extends PersistentEntity. You'll then get error because Sequence Generator default_gen is defined twice within the same Persistency Unit. (EclipseLink 2.6.2)
Indeed as specified by JEE-7 "The scope of the generator name is global to the persistence unit (across all generator types)." So even if it works at some time with some implementations it is not standard behavior.
@AleksandrSavvopulo - It does still work for me with hibernate 5.6.10.
|
16

In JPA that cannot be done with annotations. Annotation itself cannot be overridden. Entity inherits all the mapping information from MappedSuperClass. There is only two annotations that can be used to redefine mappings inherited from mapped superClass:

  1. AttributeOverride to override column mappings and
  2. AssociationOverride to override join columns / table.

Neither of them helps with GeneratedValue.

Comments

5

I'm writing this as it gets too unreadable as the comment on the accepted answer:

I have a BaseEntity that every other Entity inherits from:

BaseEntity.java:

@MappedSuperclass
public abstract class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_ID")
    private Long id;

I then have two Entities User and Order that both inherit from BaseEntity whilst also having the @SequenceGenerator annotation:

User.java:

@SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_USER", allocationSize = 1)
public class User extends BaseEntity { ... }

Order.java:

@SequenceGenerator(name = "SEQ_ID", sequenceName = "SEQ_ORDER", allocationSize = 1)
public class Order extends BaseEntity { ... }

It works on H2 at least with 2 Sequences SEQ_USER and SEQ_ORDERS:

select SEQ_USER.nextval from dual;
select SEQ_ORDERS.nextval from dual;

2 Comments

It works for multiple inherited entities, because Hibernate 5.18.3++ is tolerant by default. But you get warnings during startup: org.hibernate.boot.internal.InFlightMetadataCollectorImpl - HHH000069: Duplicate generator name SEQ_ID. See also hibernate.atlassian.net/browse/HHH-12454. If you get annoyed by the warnings, have a look at: stackoverflow.com/questions/5257921/#5258090
The link from the comment above (stackoverflow.com/questions/5257921/#5258090) is a fully plausible approach, but is changing the way, the annotations are used. One is switching from field-based annotations to method-based annotations, which one should do in the whole entity then, see here.
4

With EclipseLink, you can use a Customizer. DescriptorCustomizer interface defines a way to customize all the information about a jpa descriptor (aka a persistent entity).

public class SequenceCustomizer implements DescriptorCustomizer {

    @Override
    public void customize(ClassDescriptor descriptor) throws Exception {
        descriptor.setSequenceNumberName(descriptor.getTableName());
    }
}

and in your mapped superclass:

@MappedSuperclass
@Customizer(SequenceCustomizer.class)
public abstract class AbstractEntity implements Serializable {
    ...
}

Comments

0

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.

  1. 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).
  2. 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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.