Послідовність у сплячому режимі JPA (без ідентифікації)


138

Чи можливо використовувати послідовність БД для якогось стовпця, який не є ідентифікатором / не є частиною складеного ідентифікатора ?

Я використовую сплячий режим як постачальник 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.

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

Відповіді:


76

Шукаючи відповіді на цю проблему, я натрапив на це посилання

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

@GeneratedValueАнотацій просто говорить Hibernate , що база даних генерує саме це значення.

Запропонованим на цьому форумі рішенням (або навколо нього) є створення окремої сутності з генерованим ідентифікатором, приблизно таким:

@Entity
громадський клас GeneralSequenceNumber {
  @Id
  @GeneratedValue (...)
  приватний Довгий номер;
}

@Entity 
публічний клас MyEntity {
  @Id ..
  приватний Long id;

  @Один до одного(...)
  приватний GeneralSequnceNumber myVal;
}

Із java doc @GeneratedValue: "Анотація GeneratedValue може бути застосована до властивості первинного ключа або поля об'єкта чи відображеного в суперклассі у поєднанні з анотацією Id"
Kariem

11
Я виявив, що @Column (columnDefinition = "serial") працює ідеально, але лише для PostgreSQL. Для мене це було ідеальним рішенням, адже друга сутність - це "потворний" варіант
Сергій Ведерніков

@SergeyVedernikov, що було надзвичайно корисно. Чи проти зауважити це як окрему відповідь? Це вирішило мою проблему дуже просто та ефективно.
Метт Бал

@MattBall я опублікував це як окрему відповідь :) stackoverflow.com/a/10647933/620858
Сергій Ведерніков

1
Я відкрив пропозицію про дозвіл @GeneratedValueна поля, які не ідентифікують. Будь ласка, голосуйте, щоб бути включеним в 2.2 java.net/jira/browse/JPA_SPEC-113
Петро Тахчієв

44

Я виявив, що @Column(columnDefinition="serial")працює ідеально, але лише для PostgreSQL. Для мене це було ідеальним рішенням, оскільки друга сутність - це "потворний" варіант.


Привіт, мені знадобиться пояснення. Не могли б ви сказати більше, будь ласка?
Емаборса

2
@Emaborsa columnDefinition=Біт в основному вказує Hiberate не намагатися генерувати визначення стовпця, а використовувати текст, який ви надали. По суті, ваш DDL для стовпця буде буквально просто ім'ям + колонкиDefinition. У цьому випадку (PostgreSQL) mycolumn serialє допустимим стовпцем таблиці.
Патрік

7
Еквівалент для MySQL@Column(columnDefinition = "integer auto_increment")
Річард Кеннард

2
Чи створює цей авто свою вартість? Я намагався зберігати сутність із таким визначенням поля, як це, але воно не генерувало значення. він кинув нульове значення у стовпчик <стовпець> порушує
ненулеві

7
Я раніше @Column(insertable = false, updatable = false, columnDefinition="serial")не дозволяв сплячому режиму намагатися вставити нульові значення або оновити поле. Тоді вам потрібно буде повторно запитати db, щоб отримати згенерований ідентифікатор після вставки, якщо вам потрібно використовувати його відразу.
Роберт Ді Паоло

20

Я знаю, що це дуже давнє запитання, але це показано по-перше на результатах, і jpa сильно змінився з моменту запитання.

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

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

1
Це все ще вимагає, щоб значення генерувалося в базі даних, що насправді не відповідає на питання. Для баз даних Oracle до 12c все одно потрібно буде створити тригер бази даних, щоб генерувати значення.
Берні

9
також це анотація зі сплячки, а не JPA.
caarlos0

14

Зимова сплячка це безумовно підтримує. З документів:

"Генеровані властивості - це властивості, які мають свої значення, сформовані в базі даних. Як правило, програми глибокого сну, необхідні для оновлення об'єктів, які містять будь-які властивості, для яких база даних генерує значення. Позначення властивостей як генерованих, однак, дозволяє додатку делегувати цю відповідальність на 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 їх підтримує.


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

Варіант зі сплячкою 4.2.19 та оракул: SQLQuery sqlQuery = getSession().createSQLQuery("select NAMED_SEQ.nextval seq from dual"); sqlQuery.addScalar("seq", LongType.INSTANCE); return (Long) sqlQuery.uniqueResult();
Аарон

6

Я зафіксував генерацію UUID (або послідовностей) з Hibernate за допомогою @PrePersistпримітки:

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

5

Хоча це стара тема, я хочу поділитися своїм рішенням і сподіваюся отримати зворотний зв’язок з цього приводу. Попереджуйте, що я тестував це рішення лише з моєї локальної бази даних в якійсь тестовій шафі 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 на клас сутності та резервує пару порядкових номерів, визначених збільшеннямValue сутності SequenceNumber. Якщо у нього не вистачає послідовних чисел, він завантажує сутність SequenceNumber для цільового класу і резервує значення incrementValue для наступних викликів. Таким чином, мені не потрібно запитувати базу даних кожен раз, коли потрібно значення послідовності. Зверніть увагу на сеанс без громадянства, який відкривається для резервування наступного набору порядкових номерів. Не можна використовувати той самий сеанс, який цільовий об'єкт наразі зберігається, оскільки це призведе до ConcurrentModificationException в EntityPersister.

Сподіваюся, що це комусь допоможе.


5

Якщо ви використовуєте postgresql
І я використовую у весняному завантаженні 1.5.6

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

1
Це працювало і для мене, я використовую весняний завантажувач 2.1.6. РЕЛІЗ, Hibernate 5.3.10.Закінчення, Крім того, що вже вказувалося, мені довелося створити безпеку 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();
}

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

Повідомлення так.

//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

"Я не хочу використовувати тригер або будь-яку іншу річ, окрім самого сплячого, щоб генерувати значення для мого ресурсу"

У цьому випадку, як щодо створення реалізації UserType, яка генерує необхідне значення, та налаштування метаданих для використання цього UserType для збереження властивості mySequenceVal?


0

Це не те саме, що використання послідовності. Під час використання послідовності ви нічого не вставляєте та не оновлюєте. Ви просто отримуєте наступне значення послідовності. Схоже, сплячий не підтримує цього.


0

Якщо у вас є стовпець з типом UNIQUEIDENTIFIER і генерація за замовчуванням, необхідна для вставки, але стовпець не є ПК

@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, яка спричиняє виклик методу відразу після того, як програма ініціалізує боби, поєднану з методом оновлення 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. Я розмістив його пакет-приватний поруч 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, який викликається безпосередньо перед тим, як ActualEntityотримати. Він повинен демонструвати принцип:

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

}

Я не перевіряв його в реальному додатку, тому будь ласка, насолоджуйтесь обережно.


-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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.