Hibernate JPA Sequence (не Id)

Можно ли использовать последовательность DB для некоторого столбца, что не является идентификатором/не является частью составного идентификатора?

Я использую hibernate как поставщик jpa, и у меня есть таблица с несколькими столбцами, которые генерируют значения (используя последовательность), хотя они не являются частью идентификатора.

Я хочу использовать последовательность для создания нового значения для сущности, где столбец последовательности НЕ (часть) первичного ключа:

@Entity
@Table(name = "MyTable")
public class MyEntity {

    //...
    @Id //... etc
    public Long getId() {
        return id;
    }

   //note NO @Id here! but this doesn't work...
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "myGen")
    @SequenceGenerator(name = "myGen", sequenceName = "MY_SEQUENCE")
    @Column(name = "SEQ_VAL", unique = false, nullable = false, insertable = true, updatable = true)
    public Long getMySequencedValue(){
      return myVal;
    }

}

Тогда, когда я это сделаю:

em.persist(new MyEntity());

идентификатор будет сгенерирован, но свойство mySequenceVal также будет создано моим поставщиком JPA.

Просто, чтобы все было ясно: я хочу Hibernate генерировать значение для свойства mySequencedValue. Я знаю, что Hibernate может обрабатывать значения, созданные с помощью базы данных, но я не хочу использовать триггер или любую другую вещь, кроме самого Hibernate, для генерации значения для моего свойства. Если Hibernate может генерировать значения для первичных ключей, почему он не может генерировать для простого свойства?

Ответ 1

В поисках ответов на эту проблему я наткнулся на эту ссылку

Кажется, что Hibernate/JPA не может автоматически создать значение для ваших свойств, отличных от id. Аннотация @GeneratedValue используется только в сочетании с @Id для создания автоматических номеров.

Аннотация @GeneratedValue просто сообщает Hibernate, что база данных сама генерирует это значение.

Решение (или обход), предлагаемое на этом форуме, заключается в создании отдельного объекта с генерируемым идентификатором, что-то вроде этого:

@Entity
public class GeneralSequenceNumber {
  @Id
  @GeneratedValue(...)
  private Long number;
}

@Entity 
public class MyEntity {
  @Id ..
  private Long id;

  @OneToOne(...)
  private GeneralSequnceNumber myVal;
}

Ответ 2

Я обнаружил, что @Column(columnDefinition="serial") работает идеально, но только для PostgreSQL. Для меня это было идеальным решением, потому что второй объект является "уродливым" вариантом.

Ответ 3

Я знаю, что это очень старый вопрос, но он показал сначала результаты, и jpa сильно изменился со времени вопроса.

Правильный способ сделать это теперь с аннотацией @Generated. Вы можете определить последовательность, установить значение по умолчанию в столбце этой последовательности, а затем сопоставить столбец как:

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)

Ответ 4

Hibernate определенно поддерживает это. Из документов:

"Сгенерированные свойства - это свойства, которые имеют свои значения, сгенерированные базой данных. Обычно приложения Hibernate необходимы для обновления объектов, которые содержат любые свойства, для которых база данных генерировала значения. Однако свойства маркировки, сгенерированные, позволяют делегировать эту ответственность в Hibernate. По сути, всякий раз, когда Hibernate выдает SQL INSERT или UPDATE для объекта, который определил сгенерированные свойства, он немедленно выдает выбор потом для получения сгенерированных значений.

Для свойств, созданных только для вставки, ваше сопоставление свойств (.hbm.xml) будет выглядеть так:

<property name="foo" generated="insert"/>

Для свойств, сгенерированных при вставке и обновлении, сопоставление свойств (.hbm.xml) будет выглядеть так:

<property name="foo" generated="always"/>

К сожалению, я не знаю JPA, поэтому я не знаю, доступна ли эта функция через JPA (возможно, я подозреваю)

В качестве альтернативы вы должны иметь возможность исключить свойство из вставок и обновлений, а затем "вручную" вызвать session.refresh(obj); после того, как вы вставили/обновили его, чтобы загрузить сгенерированное значение из базы данных.

Вот как вы могли бы исключить свойство из использования в операторах вставки и обновления:

<property name="foo" update="false" insert="false"/>

Опять же, я не знаю, предоставляет ли JPA эти функции Hibernate, но Hibernate действительно поддерживает их.

Ответ 5

В качестве продолжения здесь, как я получил его для работы:

@Override public Long getNextExternalId() {
    BigDecimal seq =
        (BigDecimal)((List)em.createNativeQuery("select col_msd_external_id_seq.nextval from dual").getResultList()).get(0);
    return seq.longValue();
}

Ответ 6

Хотя это старый поток, я хочу поделиться своим решением и, надеюсь, получить от него некоторые отзывы. Будьте предупреждены, что я тестировал это решение только с моей локальной базой данных в каком-то тестовом сценарии JUnit. Так что пока это не продуктивная функция.

Я решил эту проблему для себя, введя пользовательскую аннотацию, называемую Sequence без свойства. Это всего лишь маркер для полей, которым должно быть присвоено значение из инкрементированной последовательности.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sequence
{
}

Используя эту аннотацию, я отметил свои сущности.

public class Area extends BaseEntity implements ClientAware, IssuerAware
{
    @Column(name = "areaNumber", updatable = false)
    @Sequence
    private Integer areaNumber;
....
}

Чтобы сохранить независимость базы данных, я ввел объект под названием SequenceNumber, который содержит текущее значение последовательности и размер приращения. Я выбрал className как уникальный ключ, поэтому каждый класс сущности получит свою собственную последовательность.

@Entity
@Table(name = "SequenceNumber", uniqueConstraints = { @UniqueConstraint(columnNames = { "className" }) })
public class SequenceNumber
{
    @Id
    @Column(name = "className", updatable = false)
    private String className;

    @Column(name = "nextValue")
    private Integer nextValue = 1;

    @Column(name = "incrementValue")
    private Integer incrementValue = 10;

    ... some getters and setters ....
}

Последний шаг, а самый сложный - PreInsertListener, который обрабатывает присвоение порядкового номера. Обратите внимание, что я использовал spring как контейнер bean.

@Component
public class SequenceListener implements PreInsertEventListener
{
    private static final long serialVersionUID = 7946581162328559098L;
    private final static Logger log = Logger.getLogger(SequenceListener.class);

    @Autowired
    private SessionFactoryImplementor sessionFactoryImpl;

    private final Map<String, CacheEntry> cache = new HashMap<>();

    @PostConstruct
    public void selfRegister()
    {
        // As you might expect, an EventListenerRegistry is the place with which event listeners are registered
        // It is a service so we look it up using the service registry
        final EventListenerRegistry eventListenerRegistry = sessionFactoryImpl.getServiceRegistry().getService(EventListenerRegistry.class);

        // add the listener to the end of the listener chain
        eventListenerRegistry.appendListeners(EventType.PRE_INSERT, this);
    }

    @Override
    public boolean onPreInsert(PreInsertEvent p_event)
    {
        updateSequenceValue(p_event.getEntity(), p_event.getState(), p_event.getPersister().getPropertyNames());

        return false;
    }

    private void updateSequenceValue(Object p_entity, Object[] p_state, String[] p_propertyNames)
    {
        try
        {
            List<Field> fields = ReflectUtil.getFields(p_entity.getClass(), null, Sequence.class);

            if (!fields.isEmpty())
            {
                if (log.isDebugEnabled())
                {
                    log.debug("Intercepted custom sequence entity.");
                }

                for (Field field : fields)
                {
                    Integer value = getSequenceNumber(p_entity.getClass().getName());

                    field.setAccessible(true);
                    field.set(p_entity, value);
                    setPropertyState(p_state, p_propertyNames, field.getName(), value);

                    if (log.isDebugEnabled())
                    {
                        LogMF.debug(log, "Set {0} property to {1}.", new Object[] { field, value });
                    }
                }
            }
        }
        catch (Exception e)
        {
            log.error("Failed to set sequence property.", e);
        }
    }

    private Integer getSequenceNumber(String p_className)
    {
        synchronized (cache)
        {
            CacheEntry current = cache.get(p_className);

            // not in cache yet => load from database
            if ((current == null) || current.isEmpty())
            {
                boolean insert = false;
                StatelessSession session = sessionFactoryImpl.openStatelessSession();
                session.beginTransaction();

                SequenceNumber sequenceNumber = (SequenceNumber) session.get(SequenceNumber.class, p_className);

                // not in database yet => create new sequence
                if (sequenceNumber == null)
                {
                    sequenceNumber = new SequenceNumber();
                    sequenceNumber.setClassName(p_className);
                    insert = true;
                }

                current = new CacheEntry(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue(), sequenceNumber.getNextValue());
                cache.put(p_className, current);
                sequenceNumber.setNextValue(sequenceNumber.getNextValue() + sequenceNumber.getIncrementValue());

                if (insert)
                {
                    session.insert(sequenceNumber);
                }
                else
                {
                    session.update(sequenceNumber);
                }
                session.getTransaction().commit();
                session.close();
            }

            return current.next();
        }
    }

    private void setPropertyState(Object[] propertyStates, String[] propertyNames, String propertyName, Object propertyState)
    {
        for (int i = 0; i < propertyNames.length; i++)
        {
            if (propertyName.equals(propertyNames[i]))
            {
                propertyStates[i] = propertyState;
                return;
            }
        }
    }

    private static class CacheEntry
    {
        private int current;
        private final int limit;

        public CacheEntry(final int p_limit, final int p_current)
        {
            current = p_current;
            limit = p_limit;
        }

        public Integer next()
        {
            return current++;
        }

        public boolean isEmpty()
        {
            return current >= limit;
        }
    }
}

Как видно из приведенного выше кода, слушатель использовал один экземпляр SequenceNumber для класса сущности и резервирует пару порядковых номеров, определяемых инкрементным значением объекта SequenceNumber. Если он не соответствует порядковым номерам, он загружает объект SequenceNumber для целевого класса и резервирует значения incrementValue для следующих вызовов. Таким образом, мне не нужно запрашивать базу данных каждый раз, когда требуется значение последовательности. Обратите внимание на StatelessSession, который открывается для резервирования следующего набора порядковых номеров. Вы не можете использовать тот же сеанс, который в настоящее время сохраняется в целевом объекте, поскольку это приведет к исключению ConcurrentModificationException в EntityPersister.

Надеюсь, это поможет кому-то.

Ответ 7

Я работаю в той же ситуации, что и вы, и я также не нашел серьезных ответов, если в принципе можно генерировать свойства non-id с JPA или нет.

Мое решение состоит в том, чтобы вызвать последовательность с собственным запросом JPA, чтобы установить свойство вручную, прежде чем его перенести.

Это не является удовлетворительным, но оно работает как обходной путь на данный момент.

Марио

Ответ 8

Я установил генерацию UUID (или последовательностей) с помощью Hibernate с помощью аннотации @PrePersist:

@PrePersist
public void initializeUUID() {
    if (uuid == null) {
        uuid = UUID.randomUUID().toString();
    }
}

Ответ 9

Я нашел эту конкретную заметку в сеансе 9.1.9 Аннотацию GeneratedValue из спецификации JPA: "[43] Портативные приложения не должны использовать аннотацию GeneratedValue для других постоянных полей или свойств". Поэтому я предполагаю, что невозможно автоматически генерировать значение для значений не первичного ключа, по крайней мере, используя просто JPA.

Ответ 10

"Я не хочу использовать триггер или любую другую вещь, кроме самого Hibernate, чтобы генерировать значение для моего свойства"

В этом случае, как насчет создания реализации UserType, который генерирует требуемое значение, и настройки метаданных для использования этого UserType для сохранения свойства mySequenceVal?

Ответ 11

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

Ответ 12

Если вы используете postgresql
И я использую spring загрузки 1.5.6

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;

Ответ 14

Похоже, что поток старый, я просто хотел добавить свое решение здесь (Использование AspectJ - AOP spring).

Решение заключается в создании пользовательской аннотации @InjectSequenceValue следующим образом.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectSequenceValue {
    String sequencename();
}

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

Теперь вы можете комментировать, как это.

//serialNumber will be injected dynamically, with the next value of the serialnum_sequence.
 @InjectSequenceValue(sequencename = "serialnum_sequence") 
  Long serialNumber;

До сих пор мы пометили поле, в которое нам нужно ввести значение последовательности. Итак, мы посмотрим, как ввести значение последовательности в отмеченные поля, это делается путем создания среза точки в AspectJ.

Мы запустим инъекцию непосредственно перед выполнением метода save/persist Это делается в следующем классе.

@Aspect
@Configuration
public class AspectDefinition {

    @Autowired
    JdbcTemplate jdbcTemplate;


    //@Before("execution(* org.hibernate.session.save(..))") Use this for Hibernate.(also include session.save())
    @Before("execution(* org.springframework.data.repository.CrudRepository.save(..))") //This is for JPA.
    public void generateSequence(JoinPoint joinPoint){

        Object [] aragumentList=joinPoint.getArgs(); //Getting all arguments of the save
        for (Object arg :aragumentList ) {
            if (arg.getClass().isAnnotationPresent(Entity.class)){ // getting the Entity class

                Field[] fields = arg.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(InjectSequenceValue.class)) { //getting annotated fields

                        field.setAccessible(true); 
                        try {
                            if (field.get(arg) == null){ // Setting the next value
                                String sequenceName=field.getAnnotation(InjectSequenceValue.class).sequencename();
                                long nextval=getNextValue(sequenceName);
                                System.out.println("Next value :"+nextval); //TODO remove sout.
                                field.set(arg, nextval);
                            }

                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }

        }
    }

    /**
     * This method fetches the next value from sequence
     * @param sequence
     * @return
     */

    public long getNextValue(String sequence){
        long sequenceNextVal=0L;

        SqlRowSet sqlRowSet= jdbcTemplate.queryForRowSet("SELECT "+sequence+".NEXTVAL as value FROM DUAL");
        while (sqlRowSet.next()){
            sequenceNextVal=sqlRowSet.getLong("value");

        }
        return  sequenceNextVal;
    }
}

Теперь вы можете комментировать любую сущность, как показано ниже.

@Entity
@Table(name = "T_USER")
public class UserEntity {

    @Id
    @SequenceGenerator(sequenceName = "userid_sequence",name = "this_seq")
    @GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "this_seq")
    Long id;
    String userName;
    String password;

    @InjectSequenceValue(sequencename = "serialnum_sequence") // this will be injected at the time of saving.
    Long serialNumber;

    String name;
}

Ответ 15

Я был в такой ситуации, как вы (последовательность JPA/Hibernate для поля non @Id), и я закончил создание триггера в моей схеме db, которая добавляет уникальный уникальный номер вставки. Я просто не получил его для работы с JPA/Hibernate

Ответ 16

Проведя часы, это аккуратно помогло мне решить мою проблему:

Для Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Для H2:

ID BIGINT GENERATED as auto_increment

Также сделайте:

@Column(insertable = false)