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