Как создать универсальный класс модели сущностей, поддерживающий универсальный идентификатор, включая автоматически генерируемые идентификаторы?


У меня есть три вида первичных ключей для таблиц:

  • INT автоматически генерируемый первичный ключ, который использует AUTO_INCREMENT емкость от поставщика базы данных (MySQL)
  • CHAR(X) первичный ключ для хранения читаемого пользователем значения в виде ключа (где X-число, а 50
  • сложные первичные ключи, состоящие из 2 или 3 полей таблицы.

Также существует некоторая группа полей, которые могут присутствовать (или не присутствовать):

  • версия, INT поле.
  • createdBy, VARCHAR(60) field и lastUpdatedBy, VARCHAR(60) field (есть еще поля, но они охватывают базовый пример).

Некоторые примеры выше:

  • Таблица 1
    • id int первичный ключ auto_increment
    • version int
    • значение char (10)
    • created by varchar (60)
    • lastUpdatedBy varchar (60)
  • Таблица 2
    • id char (60) primary key
    • краткое описание varchar (20)
    • длинное описание varchar (100)
  • Таблица 3
    • field1 int первичный ключ
    • field2 int первичный ключ
    • сумма десятичная (10, 5)
    • version int

Учитывая все это, мне нужно создать универсальный набор классов, который поддерживает эти требования и позволяет выполнять операции CRUD с использованием Hibernate 4.3 и JPA 2.1.

Вот моя текущая модель (геттеры / сеттеры избегали сокращать код Образец):

@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;
}

В настоящее время я тестирую сохраненные экземпляры Table1 и Table2. У меня есть следующий код:

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();

Это не удается при попытке сохранить Table2 и я получаю следующую ошибку:

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)

Сообщение об ошибке очевидно, потому что поле CHAR(X) не имеет значения по умолчанию и не будет иметь его (AFAIK). Я попытался изменить стратегию генерации на GenerationType.AUTO и получил то же самое сообщение об ошибке.

Как я могу реконструировать эти классы, чтобы поддержать эти требования? Или еще лучше, как я могу обеспечить стратегию генерации, которая зависит от ключа сохраняемой мной сущности, который может быть автоматически сгенерирован или предоставлен мной?

Задействованные технологии:

  • Java SDK 8
  • Hibernate 4.3.6
  • JPA 2.1
  • базы данных MySQL и Postgres
  • ОС: Windows 7 Professional

Примечание: вышеизложенное может (и, вероятно, будет) изменяться, чтобы быть поддержанным для других реализаций JPA 2.1, таких как EclipseLink.

3 8

3 ответа:

Вы можете "обойти" этот принудительный производный класс для реализации метода, который обеспечит присвоение идентификатора и аннотацию этого метода с помощью @PrePersist. Вы можете предоставить реализацию по умолчанию для классов, для которых идентификатор будет автоматически сгенерирован.

Что-то вроде:

@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";
     }


}

Не пробовал этого, но в соответствии с api Hibernate это не должно усложняться созданием пользовательской реализацииIdentityGenerator .

Это метод generate получает и объект, для которого вы генерируете значение, чтобы вы могли проверить тип поля id и вернуть соответствующее значение для вашего первичного ключа.

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;

            }

        }
    }

Имея это просто использовать его в качестве стратегии поколения:

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

Для Hibernate 4 необходимо реализовать IdentifierGenerator взаимодействие.


Как выше принято для Hibernate, все еще должно быть возможно создать его более универсальным способом для любого" JPA-совместимого " поставщика. В соответствии с api JPA в аннотации GeneratedValue вы можете предоставить свой пользовательский генератор. Это означает, что вы можете указать имя вашего пользовательского генератора, и вы должны реализовать этот генератор для каждого поставщика jpa.

Это означает, что вам нужно аннотировать BaseEntity следующей аннотацией

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

Теперь вам нужно зарегистрировать пользовательский генератор с именем "my-custom-generator" для каждого поставщика jpa, которого вы хотите использовать.

Для Hibernate это делается с помощью аннотации @ GenericGenerator, как показано выше (добавление @GenericGenerator(name="my-custom-generator", strategy="my.package.DynamicGenerator" к классу BaseEntity либо на поле id, либо на уровне класса BaseEntity должно быть достаточным).

В EclipseLink я вижу, что вы можете сделать это черезGeneratedValue аннотацию и зарегистрировать ее через 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);
    }

}    

Каждый поставщик должен уступить дорогу зарегистрируйте генератор идентификаторов, поэтому вам нужно будет реализовать и зарегистрировать пользовательскую стратегию генерации для каждого поставщика, если вы хотите поддерживать их все.

Иерархии наследования борются с ORM. Так что будьте проще и немного ближе к реализации базы данных. Не сопоставить иерархию абстрактных суперклассов, но добавьте аннотированный POJO-объект на куски общие столбцы. Они вполне могут оказаться удобными для работы с и в остальной части вашего кода.

Создайте классы @Embeddable для общих полей, а также создайте класс для вашего составного идентификатора @Embeddable.

@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;
    ...
}

Самая простая версия вашей реализации классы тогда выглядят так:

@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;
    }
}

Для идентификатора строки нет автоматической генерации:

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

И составной идентификатор в виде @EmbeddedId:

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

В качестве дополнительного преимущества вы можете смешивать и сопоставлять, добавляя больше этих признаков, если хотите, так как вы больше не ограничены деревом наследования.

(но вы можете сохранить иерархию, содержащую геттеры, сеттеры и делегаты по умолчанию, если существующий код полагается на нее и/или извлекает из нее выгоду.)