How to create a generic entity model class that supports generic id including auto generated ids?

Luiggi Mendoza picture Luiggi Mendoza · Sep 29, 2014 · Viewed 8.4k times · Source

I have three kinds of primary keys for tables:

  • INT auto generated primary key which use AUTO_INCREMENT capacity from database vendor (MySQL)
  • CHAR(X) primary key to store a user readable value as key (where X is a number and 50 <= X <= 60)
  • Complex primary keys, composed by 2 or 3 fields of the table.

Also, there are some group of fields that may be present (or not):

  • version, INT field.
  • createdBy, VARCHAR(60) field, and lastUpdatedBy, VARCHAR(60) field (there are more fields but these covers a basic example).

Some examples of above:

  • Table1
    • id int primary key auto_increment
    • version int
    • value char(10)
    • createdBy varchar(60)
    • lastUpdatedBy varchar(60)
  • Table2
    • id char(60) primary key
    • shortDescription varchar(20)
    • longDescription varchar(100)
  • Table3
    • field1 int primary key
    • field2 int primary key
    • amount decimal(10, 5)
    • version int

With all this in mind, I need to create a generic set of classes that supports these requirements and allows CRUD operations using Hibernate 4.3 and JPA 2.1.

Here's my current model (getters/setters avoided to shorten the code sample):

@MappedSuperclass
public abstract class BaseEntity<T> implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    protected T id;
}

@MappedSuperclass
public abstract class VersionedEntity<T> extends BaseEntity<T> {
    @Version
    protected int version;
}

@MappedSuperclass
public abstract class MaintainedEntity<T> extends VersionedEntity<T> {
    @Column
    protected String createdBy;
    @Column
    protected String lastUpdatedBy;
}

@Entity
public class Table1 extends MaintainedEntity<Long> {
    @Column
    private String value;
}

@Entity
public class Table2 extends BaseEntity<String> {
    @Column
    private String shortDescription;
    @Column
    private String longDescription;
}

I'm currently testing save instances of Table1 and Table2. I have the following code:

SessionFactory sf = HibernateUtils.getSessionFactory();
Session session = sf.getCurrentSession();
session.beginTransaction();

Table1 newTable1 = new Table1();
newTable1.setValue("foo");
session.save(newTable1); //works

Table2 newTable2 = new Table2();
//here I want to set the ID manually
newTable2.setId("foo_id");
newTable2.setShortDescription("short desc");
newTable2.setLongDescription("long description");
session.save(newTable2); //fails

session.getTransaction().commit();
sf.close();

It fails when trying to save Table2 and I get the following error:

Caused by: java.sql.SQLException: Field 'id' doesn't have a default value
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:996)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3887)

The error message is obvious because a CHAR(X) field doesn't have a default value and won't have it (AFAIK). I tried changing the generation strategy to GenerationType.AUTO and got the same error message.

How can I remodel these classes in order to support these requirements? Or even better, how could I provide a generation strategy that depends on the key of the entity I'm saving, which could be auto generated or provided by me?

Involved technologies:

  • Java SDK 8
  • Hibernate 4.3.6
  • JPA 2.1
  • MySQL and Postgres databases
  • OS: Windows 7 Professional

Note: the above may (and probably will) change in order to be supported for other implementations of JPA 2.1 like EclipseLink.

Answer

walkeros picture walkeros · Oct 2, 2014

Did not try this, but according to Hibernate's api this should not be complicated by creating custom implementation of IdentityGenerator.

It's generate method gets and object for which you are generating the value so you can check the type of the id field and return appropriate value for your primary key.

public class DynamicGenerator  implements IdentityGenerator

        public Serializable generate(SessionImplementor session, Object object)
                throws HibernateException {

             if (shouldUseAutoincrementStartegy(object)) { // basing on object detect if this should be autoincrement or not, for example inspect the type of id field by using reflection - if the type is Integer use IdentityGenerator, otherwise another generator 
                 return new IdentityGenerator().generate(seession, object)
             } else { // else if (shouldUseTextKey)

                 String textKey = generateKey(session, object); // generate key for your object

                 // you can of course connect to database here and execute statements if you need:
                 // Connection connection = session.connection();
                 //  PreparedStatement ps = connection.prepareStatement("SELECT nextkey from text_keys_table");
                 // (...)

                 return textKey;

            }

        }
    }

Having this simply use it as your generation strategy:

@MappedSuperclass
public abstract class BaseEntity<T> implements Serializable {
    @Id
    @GenericGenerator(name="seq_id", strategy="my.package.DynamicGenerator")
    protected T id;
}

For Hibernate 4, you should implement IdentifierGenerator interface.


As above is accepted for Hibernate it should be still possible to create it in more generic way for any "jpa compliant" provider. According to JPA api in GeneratedValue annotation you can provide your custom generator. This means that you can provide the name of your custom generator and you should implement this generator for each jpa provider.

This would mean you need to annotate BaseEntity with following annotation

@MappedSuperclass
public abstract class BaseEntity<T> implements Serializable {
    @Id
    @GeneratedValue(generator="my-custom-generator")
    protected T id;
}

Now you need to register custom generator with name "my-custom-generator" for each jpa provider you would like to use.

For Hibernate this is surly done by @GenericGenerator annotation as shown before (adding @GenericGenerator(name="my-custom-generator", strategy="my.package.DynamicGenerator" to BaseEntity class on either id field or BaseEntity class level should be sufficient).

In EclipseLink I see that you can do this via GeneratedValue annotation and registering it via SessionCustomizer:

            properties.put(PersistenceUnitProperties.SESSION_CUSTOMIZER,
                    "my.custom.CustomIdGenerator");

public class CustomIdGenerator extends Sequence implements SessionCustomizer {


    @Override
    public Object getGeneratedValue(Accessor accessor,
            AbstractSession writeSession, String seqName) {
        return  "Id"; // generate the id
    }

    @Override
    public Vector getGeneratedVector(Accessor accessor,
            AbstractSession writeSession, String seqName, int size) {
        return null;
    }

    @Override
    protected void onConnect() {
    }

    @Override
    protected void onDisconnect() {
    }

    @Override
    public boolean shouldAcquireValueAfterInsert() {
        return false;
    }

    @Override
    public boolean shouldOverrideExistingValue(String seqName,
            Object existingValue) {
        return ((String) existingValue).isEmpty();
    }

    @Override
    public boolean shouldUseTransaction() {
        return false;
    }

    @Override
    public boolean shouldUsePreallocation() {
        return false;
    }

    public void customize(Session session) throws Exception {
        CustomIdGenerator sequence = new CustomIdGenerator ("my-custom-generator");

        session.getLogin().addSequence(sequence);
    }

}    

Each provider must give a way to register id generator, so you would need to implement and register custom generation strategy for each of the provider if you want to support all of them.