问题
I have three kinds of primary keys for tables:
INT
auto generated primary key which useAUTO_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