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

元气小坏坏 提交于 2019-12-18 13:17:52

问题


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.


回答1:


You can "workaround" this forcing derived class to implement method which will ensure the Id is assigned and annotate this method with @PrePersist. You can provide default implementation for classes for which the Id will be auto generated.

Somethig like:

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

    @PrePersist
    public void ensureIdAssigned() {
          ensureIdAssignedInternal();  
    }


    public abstract void ensureIdAssignedInternal();  
}


@MappedSuperclass
public abstract class AutoIdMaintaintedEntity<T> extends MaintainedEntity<T> { // provide default implementation for Entities with Id generated by @GeneratedValue(strategy=GenerationType.IDENTITY) on BaseEntity superclass
    public void ensureIdAssignedInternal() {
        // nothing here since the Id will be automatically assigned
    }
}

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

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

    public void ensureIdAssignedInternal() {
         this.id = generateMyTextId();

    }

     private String generateMyTextId() {
         return "text id";
     }


}



回答2:


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.




回答3:


Inheritance hierarchies fight ORM. So keep things simple and stay a little closer to the database implementation. Don't map a hierarchy of abstract superclasses for this, but embed annotated POJO's for chunks of shared columns. They may well turn out to be handy to work with in the rest of your code as well.

Create @Embeddable classes for the shared fields, and make the class for your composite ID @Embeddable too.

@Embeddable
public class Maintained implements Serializable{
    private String maintainedBy;
    private String updatedBy;
    // getters and setters
}

@Embeddable
public class CompositeId implements Serializable{
    @Column
    private int id1;
    @Column
    private int id2;
    ...
}

The simplest version of your implementation classes then look like this:

@Entity
public class Table1 {
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Id
    protected Long id;
    @Version
    private String version;
    @Embedded
    private Maintained maintained;
    ...
    public Maintained getMaintained(){
        return maintained;
    }
}

For the String ID, no auto generation:

@Entity
public class Table2 {
    @Id
    private String id;
    @Column
    private String shortDescription;
    @Column
    private String longDescription;
    ...
}

And the composite ID as @EmbeddedId:

@Entity
public class Table3 { 
    @EmbeddedId 
    private CompositeId id;
    @Version
    private String version;
    @Column
    private int amount;
    ...
}

As an extra benefit, you can mix and match adding more of these traits if you like, since you're no longer constrained by the inheritance tree.

(But you can keep the hierarchy in place containing getters and setters and default delegates if existing code relies on it and/or benefits from it.)



来源:https://stackoverflow.com/questions/26109041/how-to-create-a-generic-entity-model-class-that-supports-generic-id-including-au

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!