JPA – 以编程方式通过序列递增数字字段
我有一个JPA 2 Web应用程序(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来实现它。
我知道我可以定义回调方法,但是Eval 2.0 Eval的JSR-000317持久性规范不鼓励它内部的一些特定操作:
3.5实体监听器和回调方法
– 生命周期回调可以调用JNDI,JDBC,JMS和企业bean。
– 通常,可移植应用程序的生命周期方法不应调用EntityManager或Query操作,访问其他实体实例或修改同一持久性上下文中的关系 。 生命周期回调方法可以修改调用它的实体的非关系状态。可以在本说明书的未来版本中标准化这些操作的语义。
总结一下,是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);
例外
引起: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”不存在
我究竟做错了什么 ? 我对JPA 2很新,我从未使用过与物理表无关的实体……这种方法对我来说是全新的。
我想我需要把@Column
定义放在某处:JPA怎么可能知道new_field
列(通过ImprovedNamingStrategy映射到数据库上的new_field
)是通过FooSequence
实体的value
属性检索的?
缺少一些拼图。
编辑
正如评论中所述,这是persistence.xml
:
java:jboss/datasources/myDS
一种可能的解决方案是使用具有其自己的表的单独实体,该表将仅封装新字段并且具有与该实体的OneToOne映射。 然后,只有在遇到需要附加序列号的对象时,才会实例化新实体。 然后,您可以使用任何生成器策略来填充它。
@Entity public class FooSequence { @Id @GeneratedValue(...) private Long value; } @Entity public class Whatever { @OneToOne(...) private FooSequnce newColumn; }
看到:
- Hibernate JPA序列(非Id)
- https://forum.hibernate.org/viewtopic.php?p=2405140
一个gradle 1.11可运行的SSCCE(使用Spring Boot):
的src /主/爪哇/ 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
对于某些AOP来说,这似乎是一个很好的例子。 首先创建自定义字段注释@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"); } } } } }
然后使用
扫描方面,并将其应用于此类型的任何Spring DAO保存实体。 该方面将以对用户透明的方式基于序列填充序列生成的值。
你提到过开放使用JDBC。 以下是如何使用实体回调和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; } }
我过去简单易懂的hacky解决方案如下:
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