Java JPA - 以编程方式通过序列增加数字字段

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22486446/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 16:01:38  来源:igfitidea点击:

JPA - Increment a numeric field through a sequence programmatically

javasqlhibernatejpa

提问by Andrea Ligios

I have a JPA 2 web application (Struts 2, Hibernate 4 as JPA implementation only).

我有一个 JPA 2 Web 应用程序(Struts 2、Hibernate 4 仅作为 JPA 实现)。

The current requirement is to add a (non-id) numeric sequential field, filled for certain rows only, to an existing entity. When inserting a new row, based on a certain condition, I need to set the new field to its highest value + 1or to NULL.

当前的要求是向现有实体添加一个(非 ID)数字顺序字段,仅填充某些行。插入新行时,根据特定条件,我需要将新字段设置为its highest value + 1NULL

For example:

例如:

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

In the good old SQL, it would be something like:

在旧的 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....'
)

But I've no clue on how to achieve it with JPA 2.

但我不知道如何使用 JPA 2 实现它。

I know I can define callbacks methods, but JSR-000317 Persistence Specification for Eval 2.0 Evaldiscourages some specific operations from inside it:

我知道我可以定义回调方法,但JSR-000317 Eval 2.0 的持久性规范 Eval不鼓励在其中进行一些特定的操作:

3.5 Entity Listeners and Callback Methods
- Lifecycle callbacks can invoke JNDI, JDBC, JMS, and enterprise beans.
- In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context.[43]A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.

[43]The semantics of such operations may be standardized in a future release of this specification.

3.5 实体侦听器和回调方法
- 生命周期回调可以调用 JNDI、JDBC、JMS 和企业 bean。
- 一般而言,可移植应用程序的生命周期方法不应调用 EntityManager 或 Query 操作、访问其他实体实例或修改同一持久性上下文中的关系[43]生命周期回调方法可以修改调用它的实体的非关系状态。

[43]此类操作的语义可能会在本规范的未来版本中标准化。

Summarizing, yes to JDBC (!) and EJB, no to EntityManager and other Entities.

总而言之,对 JDBC (!) 和 EJB 是,对 EntityManager 和其他实体不是。



EDIT

编辑

I'm trying to achieve the solution described in the answer from @anttix, but I'm encoutering some problem, so please correct me where I'm wrong.

我正在尝试实现@anttix 的答案中描述的解决方案,但我遇到了一些问题,所以请纠正我的错误。

Table

桌子

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

Main Entity

主要实体

@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 */
}

Sub entity

子实体

@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

例外

Caused by: javax.transaction.RollbackException: ARJUNA016053: Could not commit transaction.

[...]

Caused by: javax.persistence.PersistenceException: org.hibernate.exception.SQLGrammarException: ERROR: relation "new_field" does not exist

[...]

Caused by: org.hibernate.exception.SQLGrammarException: ERROR: relation "new_field" does not exist

[...]

Caused by: org.postgresql.util.PSQLException: ERROR: relation "new_field" does not exist

引起:javax.transaction.RollbackException:ARJUNA016053:无法提交事务。

[...]

引起:javax.persistence.PersistenceException:org.hibernate.exception.SQLGrammarException:错误:关系“new_field”不存在

[...]

引起:org.hibernate.exception.SQLGrammarException:错误:关系“new_field”不存在

[...]

引起:org.postgresql.util.PSQLException:错误:关系“new_field”不存在

What am I doing wrong ? I'm pretty new to JPA 2 and I've never used an entity not associated to a physical table... this approach is totally new to me.

我究竟做错了什么 ?我对 JPA 2 很陌生,我从未使用过与物理表无关的实体……这种方法对我来说是全新的。

I guess I need to put the @Columndefinition somewhere: how could JPA possibly know that the newFieldcolumn (mapped through ImprovedNamingStrategyto new_fieldon the database) is retrieved through the valueproperty of the FooSequenceentity ?

我想我需要把@Column定义的地方:JPA怎么可能知道newField列(通过映射ImprovedNamingStrategynew_field数据库上)通过获取value的财产FooSequence实体?

Some pieces of the puzzle are missing.

拼图的某些部分丢失了。



EDIT

编辑

As asked in comments, this is the persistence.xml:

正如评论中所问,这是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>

采纳答案by anttix

One possible solution is to use a separate entity with its own table that will encapsulate only the new field and have an OneToOne mapping with that entity. You will then instantiate the new entity only when you encounter an object that needs the additional sequence number. You can then use any generator strategy to populate it.

一种可能的解决方案是使用单独的实体及其自己的表,该实体将仅封装新字段并与该实体具有 OneToOne 映射。只有当您遇到需要附加序列号的对象时,您才会实例化新实体。然后您可以使用任何生成器策略来填充它。

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

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

See:

看:

A gradle 1.11 runnable SSCCE (using Spring Boot):

一个 gradle 1.11 可运行的 SSCCE(使用 Spring Boot):

src/main/java/JpaMultikeyDemo.java

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

构建.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
}

See also: http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-configure-datasource

另见:http: //docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-configure-datasource

回答by Angular University

This looks like it could be a good case for some AOP. First start by creating a custom field annotation @CustomSequenceGeneratedValue, and then annotate the field on the entity with it:

这看起来对于某些 AOP 来说可能是一个很好的例子。首先创建一个自定义字段注释@CustomSequenceGeneratedValue,然后用它注释实体上的字段:

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

    public void setGeneratedValue(long generatedValue) {

    }
}

Then an aspect is created to increment generated values:

然后创建一个方面来增加生成的值:

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

Then the aspect is scanned with <aop:aspectj-autoproxy />, and applied to any Spring DAO saving entities of this type. The aspect would populate the sequence generated values based on a sequence, in a transparent way for the user.

然后使用 扫描方面<aop:aspectj-autoproxy />,并将其应用于任何这种类型的 Spring DAO 保存实体。该方面将以对用户透明的方式基于序列填充序列生成的值。

回答by SergeyB

You mentioned being open to using JDBC. Here is how you can you use Entity Callback with JdbcTemplate, the example uses Postgres's syntax for selecting next value in a sequence, just update it to use the right syntax for your DB.

您提到对使用 JDBC 持开放态度。以下是如何将实体回调与 JdbcTemplate 一起使用,该示例使用 Postgres 的语法来选择序列中的下一个值,只需更新它以使用适用于您的数据库的正确语法。

Add this to your entity class:

将此添加到您的实体类:

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

And here is listener implementation (@Qualifierand required = trueare necessary for it to work):

这是侦听器实现(@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;
    }
}

回答by Andrea Ligios

The hacky solution I used to keep it simple is the following:

我用来保持简单的hacky解决方案如下:

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

The complete code ("pseudo-code", I've written it directly on SO so it could have typos) with transaction handling would be like :

带有事务处理的完整代码(“伪代码”,我直接在 SO 上编写,因此可能有拼写错误)类似于:

Entity

实体

@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 */
}

Main EJB:

主要 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

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

This will guarantee there will be no gaps in the FooSequence generation.

这将保证在 FooSequence 生成中没有间隙。

The only drawback, that I don't care at all in my use case, is that FooSequence and the @Id sequence are not synchronized, so two concurrent inserts may have "inverted" FooSequence values, respecto to their order of arrive, eg.

在我的用例中我根本不关心的唯一缺点是 FooSequence 和 @Id 序列不同步,因此两个并发插入可能具有“反转” FooSequence 值,相对于它们的到达顺序,例如。

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