JPA - добавление числового поля через последовательность программно

У меня есть веб-приложение JPA 2 (Struts 2, Hibernate 4 как реализация только JPA).

Текущее требование состоит в том, чтобы добавить (не id) числовое последовательное поле, заполненное только для определенных строк, к существующей сущности. При вставке новой строки, основанной на определенном условии, мне нужно установить новое поле в its highest value + 1 или NULL.

Например:

ID     NEW_FIELD     DESCRIPTION
--------------------------------
1          1           bla bla
2                      bla bla       <--- unmatched: not needed here
3                      bla bla       <--- unmatched: not needed here
4          2           bla bla
5          3           bla bla
6          4           bla bla
7                      bla bla       <--- unmatched: not needed here
8          5           bla bla
9                      bla bla       <--- unmatched: not needed here
10         6           bla bla

В хорошем старом SQL это будет что-то вроде:

INSERT INTO myTable (
    id, 
    new_field, 
    description
) VALUES (
    myIdSequence.nextVal, 
    (CASE myCondition
        WHEN true 
        THEN myNewFieldSequence.nextVal
        ELSE NULL
    END),
    'Lorem Ipsum and so on....'
)

Но я не знаю, как это сделать с JPA 2.

Я знаю, что могу определить методы обратных вызовов, но JSR-000317 Спецификация сохранения для Eval 2.0 Eval препятствует некоторым конкретным операциям изнутри:

3.5 Прослушиватели и методы обратного вызова
- Обратные вызовы Lifecycle могут вызывать JNDI, JDBC, JMS и enterprise beans.Суб >
- В общем, метод жизненного цикла переносимого приложения не должен вызывать операции EntityManager или Query, доступ к другому объекту экземпляры или изменять отношения в пределах одного и того же контекст. [43] Метод обратного вызова жизненного цикла может изменять не связанное с состоянием объекта, на который он вызывается.

[43] Семантика таких операций может быть стандартизирована в будущем выпуске этой спецификации.

Подведение итогов, да JDBC (!) и EJB, нет для EntityManager и других объектов.


ИЗМЕНИТЬ

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

Таблица

MyTable
-------------------------
ID            number (PK)
NEW_FIELD     number
DESCRIPTION   text

Основной объект

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

    @Id
    @SequenceGenerator(name="seq_id", sequenceName="seq_id", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_id")
    private Long id;

    @OneToOne(cascade= CascadeType.PERSIST) 
    private FooSequence newField;

    private String description

    /* Getters and Setters */
}

Субъект

@Entity
public class FooSequence {

    @Id
    @SequenceGenerator(name="seq_foo", sequenceName="seq_foo", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_foo")
    private Long value;

    /* Getter and Setter */
}

DAO

myEntity.setNewField(new FooSequence());
entityManager.persist(myEntity);

Exception

Вызвано: javax.transaction.RollbackException: ARJUNA016053: Не удалось выполнить транзакцию.

[...]

Вызвано: javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: ERROR: отношения "new_field" не существует

[...]

Вызвано: org.hibernate.exception.SQLGrammarException: ERROR: отношения "new_field" не существует

[...]

Вызвано: org.postgresql.util.PSQLException: ОШИБКА: отношения "new_field" не существует

Что я делаю неправильно? Я новичок в JPA 2, и я никогда не использовал объект, не связанный с физической таблицей... этот подход для меня совершенно незначителен.

Думаю, мне нужно где-то поместить определение @Column: как JPA, возможно, знает, что столбец newField (сопоставленный ImprovedNamingStrategy с new_field на база данных) извлекается через свойство value объекта FooSequence?

Некоторые фрагменты головоломки отсутствуют.


EDIT

Как указано в комментариях, это persistence.xml:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
                     http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">

    <persistence-unit name="MyService"  transaction-type="JTA">

        <jta-data-source>java:jboss/datasources/myDS</jta-data-source>      

        <properties>             

            <property name="hibernate.dialect" 
                     value="org.hibernate.dialect.PostgreSQLDialect" />

            <property name="hibernate.ejb.naming_strategy" 
                     value="org.hibernate.cfg.ImprovedNamingStrategy"/>

            <property name="hibernate.query.substitutions" 
                     value="true 'Y', false 'N'"/>           

         <property name="hibernate.show_sql" value="true" />
         <property name="format_sql"         value="true" />
         <property name="use_sql_comments"   value="true" />

        </properties>

    </persistence-unit>
</persistence>

Ответ 1

Одним из возможных решений является использование отдельного объекта со своей собственной таблицей, который будет инкапсулировать только новое поле и сопоставить OneToOne с этим объектом. Затем вы создадите новый объект только тогда, когда вы столкнетесь с объектом, которому нужен дополнительный порядковый номер. Затем вы можете использовать любую стратегию генератора для его заполнения.

@Entity
public class FooSequence {
    @Id
    @GeneratedValue(...)
    private Long value;
}

@Entity 
public class Whatever {
    @OneToOne(...)
    private FooSequnce newColumn;
}

См:

A gradle 1.11 runnable SSCCE (с помощью Spring Boot):

SRC/Основной/Java/JpaMultikeyDemo.java

import java.util.List;
import javax.persistence.*;
import lombok.Data;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;

@Configuration
@EnableTransactionManagement
@EnableAutoConfiguration
public class JpaMultikeyDemo {
    @Entity @Data
    public static class FooSequence {
        @Id @GeneratedValue private Long value;
    }

    @Entity @Data
    public static class FooEntity {
        @Id @GeneratedValue private Long id;
        @OneToOne           private FooSequence sequence;
    }

    @PersistenceContext
    EntityManager em;

    @Transactional
    public void runInserts() {
        // Create ten objects, half with a sequence value
        for(int i = 0; i < 10; i++) {
            FooEntity e1 = new FooEntity();
            if(i % 2 == 0) {
                FooSequence s1 = new FooSequence();
                em.persist(s1);
                e1.setSequence(s1);
            }
            em.persist(e1);
        }
    }

    public void showAll() {
        String q = "SELECT e FROM JpaMultikeyDemo$FooEntity e";
        for(FooEntity e: em.createQuery(q, FooEntity.class).getResultList())
            System.out.println(e);
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(JpaMultikeyDemo.class);
        context.getBean(JpaMultikeyDemo.class).runInserts();
        context.getBean(JpaMultikeyDemo.class).showAll();
        context.close();
    }
}

build.gradle

apply plugin: 'java'
defaultTasks 'execute'

repositories {
    mavenCentral()
    maven { url "http://repo.spring.io/libs-milestone" }
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-data-jpa:1.0.0.RC5"
    compile "org.projectlombok:lombok:1.12.6"
    compile "com.h2database:h2:1.3.175"
}

task execute(type:JavaExec) {
    main = "JpaMultikeyDemo"
    classpath = sourceSets.main.runtimeClasspath
}

Смотрите также: http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-configure-datasource

Ответ 2

Похоже, это может быть хорошим примером для некоторых АОП. Сначала запустите создание пользовательской аннотации полей @CustomSequenceGeneratedValue, а затем аннотируйте поле с сущностью с ним:

public class MyEntity {
...
    @CustomSequenceGeneratedValue
    private Long generatedValue;

    public void setGeneratedValue(long generatedValue) {

    }
}

Затем создается аспект для увеличения генерируемых значений:

@Aspect
public class CustomSequenceGeneratedValueAspect {

    @PersistenceContext 
    private EntityManager em;

    @Before("execution(* com.yourpackage.dao.SomeDao.*.*(..))")
    public void beforeSaving(JoinPoint jp) throws Throwable {
        Object[] args = jp.getArgs();
        MethodSignature ms = (MethodSignature) jp.getSignature();
        Method m = ms.getMethod();

        Annotation[][] parameterAnnotations = m.getParameterAnnotations();

        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotations = parameterAnnotations[i];
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == CustomSequenceGeneratedEntity.class) {
                       ... find generated properties run query and call setter ...

                      ... Query query = em.createNativeQuery("select MY_SEQUENCE.NEXTVAL from dual");
                }
            }
        }
    } 
}

Затем аспект сканируется с помощью <aop:aspectj-autoproxy /> и применяется к любым сохраняющим объект Spring DAO этого типа. Аспект будет заполнять последовательность сгенерированных значений на основе последовательности, прозрачным образом для пользователя.

Ответ 3

Вы упомянули, что открыты для использования JDBC. Вот как вы можете использовать Entity Callback с JdbcTemplate, в примере используется синтаксис Postgres для выбора следующего значения в последовательности, просто обновите его, чтобы использовать правильный синтаксис для вашей базы данных.

Добавьте это в свой класс сущностей:

@javax.persistence.EntityListeners(com.example.MyEntityListener.class)

И вот для этого необходимо выполнение слушателя (@Qualifier и required = true):

package com.example;

import javax.persistence.PostPersist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class MyEntityListener {

    private static JdbcTemplate jdbcTemplate;

    @Autowired(required = true)
    @Qualifier("jdbcTemplate")
    public void setJdbcTemplate(JdbcTemplate bean) {
        jdbcTemplate = bean;
    }

    @PostPersist
    @Transactional
    public void postPersis(MyEntity entity) {
        if(isUpdateNeeded(entity)) { 
            entity.setMyField(jdbcTemplate.queryForObject("select nextval('not_hibernate_sequence')", Long.class));
        }
    }

    private boolean isUpdateNeeded(MyEntity entity) {
        // TODO - implement logic to determine whether to do an update
        return false;
    }
}

Ответ 4

Хакерное решение, которое я использовал для упрощения, следующее:

MyEntity myEntity = new MyEntity();
myEntity.setDescription("blabla");
em.persist(myEntity);
em.flush(myEntity);
myEntity.setNewField(getFooSequence());

Полный код ( "псевдокод", я написал его непосредственно на SO, чтобы он мог иметь опечатки) с обработкой транзакций был бы следующим:

Объект

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

    @Id
    @SequenceGenerator(name="seq_id", sequenceName="seq_id", allocationSize=1)
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq_id")
    private Long id;

    private Long newField; // the optional sequence
    private String description
    /* Getters and Setters */
}

Основной EJB:

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER) // default
public class MainEjb implements MainEjbLocalInterface {

    @Inject 
    DaoEjbLocalInterface dao;

    // Create new session, no OSIV here
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) 
    public Long insertMyEntity(boolean myCondition) throws Exception {

        try {
            MyEntity myEntity = dao.insertMyEntity(); 
            // if this break, no FooSequence will be generated

            doOtherStuff();
            // Do other non-database stuff that can break here. 
            // If they break, no FooSequence will be generated, 
            // and no myEntity will be persisted.                                

            if (myCondition) {
                myEntity.setNewField(dao.getFooSequence());
                // This can't break (it would have break before). 
                // But even if it breaks, no FooSequence will be generated,
                // and no myEntity will be persisted.
            }
        } catch (Exception e){
            getContext().setRollbackOnly();
            log.error(e.getMessage(),e);
            throw new MyException(e);
        }    
    }
}

DAO EJB

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER) // default
public class DaoEjb implements DaoEjbLocalInterface {

    @PersistenceContext( unitName="myPersistenceUnit")
    EntityManager em;

    // default, use caller (MainEJB) session
    @TransactionAttribute(TransactionAttributeType.REQUIRED) 
    public MyEntity insertMyEntity() throws Exception{
        MyEntity myEntity = new MyEntity();
        myEntity.setDescription("blabla");
        em.persist(myEntity);
        em.flush(); // here it will break in case of database errors, 
                    // eg. description value too long for the column.
                    // Not yet committed, but already "tested".
        return myEntity;
    }

    // default, use caller (MainEJB) session
    @TransactionAttribute(TransactionAttributeType.REQUIRED) 
    public Long getFooSequence() throws Exception {
        Query query = em.createNativeQuery("SELECT nextval('seq_foo')");
        return ((BigInteger) query.getResultList().get(0)).longValue();
    }
}

Это гарантирует отсутствие пробелов в генерации FooSequence.

Единственный недостаток, который мне не нравится в моем случае использования, заключается в том, что последовательность FooSequence и @Id не синхронизирована, поэтому две параллельные вставки могут иметь "инвертированные" значения FooSequence, по отношению к их порядку поступления, например.

ID  NEW FIELD
-------------
 1      2
 2      1