Последовательность JPA гибернации (без идентификатора)

141

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

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

Мигель Пинг
источник

Ответы:

78

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

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

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

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

@Сущность
public class GeneralSequenceNumber {
  @Мне бы
  @GeneratedValue (...)
  частный длинный номер;
}

@Сущность 
public class MyEntity {
  @Мне бы ..
  частный длинный идентификатор;

  @Один к одному(...)
  частный GeneralSequnceNumber myVal;
}
Мортен Берг
источник
Из java-документа @GeneratedValue: «Аннотация GeneratedValue может быть применена к свойству или полю первичного ключа объекта или сопоставленного суперкласса в сочетании с аннотацией Id»
Карим
11
Я обнаружил, что @Column (columnDefinition = "serial") отлично работает, но только для PostgreSQL. Для меня это было идеальным решением, потому что вторая сущность - «уродливый» вариант
Сергей Ведерников
@SergeyVedernikov, это было очень полезно. Не могли бы вы опубликовать это как отдельный ответ? Это решило мою проблему очень просто и эффективно.
Мэтт Болл
@MattBall, я опубликовал это как отдельный ответ :) stackoverflow.com/a/10647933/620858
Сергей Ведерников,
1
Я открыл предложение, чтобы разрешить @GeneratedValueполя, которые не являются идентификаторами. Проголосуйте за включение в 2.2 java.net/jira/browse/JPA_SPEC-113
Петар Тахчиев
47

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

Также необходим вызов saveAndFlushсущности, и saveэтого недостаточно для заполнения значения из БД.

Сергей Ведерников
источник
Привет, мне нужно объяснение по этому поводу. Не могли бы вы рассказать мне поподробнее?
Emaborsa
2
@Emaborsa Этот columnDefinition=бит в основном указывает Hiberate не пытаться сгенерировать определение столбца и вместо этого использовать текст, который вы указали. По сути, ваш DDL для столбца будет буквально просто name + columnDefinition. В этом случае (PostgreSQL) mycolumn serial- допустимый столбец в таблице.
Патрик
7
Эквивалент для MySQL@Column(columnDefinition = "integer auto_increment")
Ричард Кеннард,
2
Создает ли это значение автоматически? Я попытался сохранить объект с таким определением поля, но оно не сгенерировало значение. он
выдал
7
Я использовал, @Column(insertable = false, updatable = false, columnDefinition="serial")чтобы спящий режим не пытался вставлять нулевые значения или обновлять поле. Затем вам нужно повторно запросить базу данных, чтобы получить сгенерированный идентификатор после вставки, если вам нужно использовать его сразу.
Роберт Ди Паоло
23

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

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

@Generated(GenerationTime.INSERT)
@Column(name = "column_name", insertable = false)
Румал
источник
1
Это по-прежнему требует, чтобы значение было сгенерировано базой данных, что на самом деле не отвечает на вопрос. Для баз данных Oracle до 12c вам все равно нужно будет написать триггер базы данных для генерации значения.
Берни
10
кроме того, это аннотация Hibernate, а не JPA.
caarlos0 08
14

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

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

Для свойств, созданных только при вставке, ваше сопоставление свойств (.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 их поддерживает.

увы
источник
1
Аннотация @Generated соответствует указанной выше конфигурации XML. См. Этот раздел документации по гибернации для получения более подробной информации.
Эрик
8

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

@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();
}
Павел
источник
Вариант с Hibernate 4.2.19 и oracle: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Аарон
8

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

@PrePersist
public void initializeUUID() {
    if (uuid == null) {
        uuid = UUID.randomUUID().toString();
    }
}
Матроска
источник
5

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

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

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

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

Себастьян Гётц
источник
5

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

@Column(columnDefinition = "serial")
@Generated(GenerationTime.INSERT)
private Integer orderID;
Сулаймон Хурсанов
источник
1
У меня это тоже сработало, я использую spring boot 2.1.6.RELEASE, Hibernate 5.3.10.Final. В дополнение к тому, что уже указывалось, мне пришлось создать защиту seq_orderи сделать ссылку из поля, nextval('seq_order'::regclass)
OJVM
3

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

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

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

Марио


источник
2

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

Густаво Ораир
источник
1

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

Решение - создать собственную аннотацию @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;
}
Субин Чалиль
источник
0

«Я не хочу использовать триггер или что-то еще, кроме самого Hibernate, для генерации значения моего свойства»

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

увы
источник
0

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

Камми
источник
0

Если у вас есть столбец с типом UNIQUEIDENTIFIER и генерация по умолчанию, необходимая для вставки, но столбец не является PK

@Generated(GenerationTime.INSERT)
@Column(nullable = false , columnDefinition="UNIQUEIDENTIFIER")
private String uuidValue;

В db у вас будет

CREATE TABLE operation.Table1
(
    Id         INT IDENTITY (1,1)               NOT NULL,
    UuidValue  UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL)

В этом случае вы не будете определять генератор для нужного вам значения (это будет автоматически благодаря columnDefinition="UNIQUEIDENTIFIER"). То же самое вы можете попробовать для других типов столбцов

Артём Новицкий
источник
0

Я нашел обходной путь для этого в базах данных MySql, используя @PostConstruct и JdbcTemplate в приложении Spring. Это может быть выполнено с другими базами данных, но вариант использования, который я представлю, основан на моем опыте работы с MySql, поскольку он использует auto_increment.

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

Именно здесь я пришел к идее создать столбец без определения auto_increment и добавить его после создания базы данных. Это возможно с помощью аннотации @PostConstruct, которая вызывает вызов метода сразу после инициализации bean-компонентов приложением в сочетании с методом обновления JdbcTemplate.

Код выглядит следующим образом:

В моей сущности:

@Entity
@Table(name = "MyTable", indexes = { @Index(name = "my_index", columnList = "mySequencedValue") })
public class MyEntity {
    //...
    @Column(columnDefinition = "integer unsigned", nullable = false, updatable = false, insertable = false)
    private Long mySequencedValue;
    //...
}

В классе PostConstructComponent:

@Component
public class PostConstructComponent {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @PostConstruct
    public void makeMyEntityMySequencedValueAutoIncremental() {
        jdbcTemplate.update("alter table MyTable modify mySequencedValue int unsigned auto_increment");
    }
}
Игнасио Веласкес Лагос
источник
0

Я хочу предложить альтернативу принятому решению @Morten Berg, которое лучше сработало для меня.

Этот подход позволяет определить поле с действительно желаемым Numberтипом - Longв моем случае использования - вместо GeneralSequenceNumber. Это может быть полезно, например, для (де-) сериализации JSON.

Обратной стороной является то, что это требует немного больше накладных расходов на базу данных.


Во-первых, нам нужен тип, ActualEntityв котором мы хотим автоматически увеличивать generatedтип Long:

// ...
@Entity
public class ActualEntity {

    @Id 
    // ...
    Long id;

    @Column(unique = true, updatable = false, nullable = false)
    Long generated;

    // ...

}

Далее нам понадобится вспомогательная сущность Generated. Я поместил его package-private рядом ActualEntity, чтобы он оставался деталью реализации пакета:

@Entity
class Generated {

    @Id
    @GeneratedValue(strategy = SEQUENCE, generator = "seq")
    @SequenceGenerator(name = "seq", initialValue = 1, allocationSize = 1)
    Long id;

}

Наконец, нам нужно место для подключения прямо перед сохранением ActualEntity. Там мы создаем и сохраняем Generatedэкземпляр. Затем это обеспечивает сгенерированную последовательность базы данных idтипа Long. Мы используем это значение, записывая его в ActualEntity.generated.

В моем случае я реализовал это с помощью Spring Data REST @RepositoryEventHandler, который вызывается прямо перед сохранением ActualEntityget. Он должен продемонстрировать принцип:

@Component
@RepositoryEventHandler
public class ActualEntityHandler {

    @Autowired
    EntityManager entityManager;

    @Transactional
    @HandleBeforeCreate
    public void generate(ActualEntity entity) {
        Generated generated = new Generated();

        entityManager.persist(generated);
        entity.setGlobalId(generated.getId());
        entityManager.remove(generated);
    }

}

Я не тестировал его в реальных приложениях, поэтому, пожалуйста, наслаждайтесь.

абогер
источник
0

Сегодня я боролся с этим, смог решить, используя этот

@Generated(GenerationTime.INSERT)
@Column(name = "internal_id", columnDefinition = "serial", updatable = false)
private int internalId;
Аритра Дас
источник
-1

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

Фредерик Морен
источник
-1

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

Для Oracle 12c:

ID NUMBER GENERATED as IDENTITY

Для H2:

ID BIGINT GENERATED as auto_increment

Также производят:

@Column(insertable = false)
весна
источник