Hibernate JPA Sequence (без идентификатора) - PullRequest
114 голосов
/ 10 ноября 2008

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

Я использую 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 может генерировать значения для первичных ключей, почему он не может генерировать для простого свойства?

Ответы [ 15 ]

61 голосов
/ 11 февраля 2009

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

Похоже, что 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;
}
33 голосов
/ 18 мая 2012

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

16 голосов
/ 23 мая 2014

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

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

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)
14 голосов
/ 12 ноября 2008

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 поддерживает их.

7 голосов
/ 19 апреля 2010

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

@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();
}
4 голосов
/ 07 августа 2012

Хотя это старая ветка, я хочу поделиться своим решением и, надеюсь, получить некоторую обратную связь по этому вопросу. Имейте в виду, что я тестировал это решение только с моей локальной базой данных в некотором тестовом примере 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, который обрабатывает присвоение порядкового номера. Обратите внимание, что я использовал весну в качестве контейнера для фасоли.

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

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

3 голосов
/ 09 марта 2016

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

@PrePersist
public void initializeUUID() {
    if (uuid == null) {
        uuid = UUID.randomUUID().toString();
    }
}
3 голосов
/ 21 ноября 2008

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

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

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

Mario

1 голос
/ 23 ноября 2018

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

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;
1 голос
/ 24 ноября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...