Hibernate и Spring - сущность с несколькими членами, наследующими одного и того же родителя, вызывает JDBCException, @Transactional weirdness - PullRequest
0 голосов
/ 18 октября 2011

(это перезапись мой другой вопрос )

У меня есть приложение Spring WebMVC, которое использует Hibernate в качестве своего бэкэнда.Поскольку моя модель домена постоянно меняется, и я не использую устаревшую базу данных в качестве бэкэнда, я настраиваю Hibernate на автоматическое создание таблиц каждый раз, когда запускаю свое приложение, установив <property name="generateDdl" value="true"/> в моей конфигурации весны.проблема возникает из-за того, что один из моих объектов имеет два отношения «один ко многим» с типами объектов, которые имеют один и тот же родительский класс.Вот очень простые версии моих классов, которые все еще показывают проблему:

@Entity
public abstract class Base {
    @Id
    @GeneratedValue
    private long id;
}


@Entity
public class ChildTypeA extends Base{

}

@Entity
public class ChildTypeB extends Base{

}




@Entity
public class Container {

    @Id
    @GeneratedValue
    private long id;

    @OneToMany(cascade = CascadeType.ALL)
    Set<ChildTypeA> childrenA = new HashSet<ChildTypeA> ();

    @OneToMany(cascade = CascadeType.ALL)
    Set<ChildTypeB> childrenB = new HashSet<ChildTypeB>();

    public void addChildA(ChildTypeA child){
        childrenA.add(child);
    }
    public void addChildB(ChildTypeB child){
        childrenB.add(child);
    }
}

Выполнение теста ниже:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = "classpath*:WEB-INF/spring-data.xml")
public class NewTest {

    @Autowired
    DataGenerator dataGenerator;

    @Test
    //@Transactional
    public void test(){
        dataGenerator.generateTestData();
    }
}




@Component
public class DataGenerator {
    @PersistenceContext
    EntityManager em;

    @Transactional
    public void generateTestData(){
        Container c = new Container();
        c.addChildA(new ChildTypeA());
        em.persist(c);
    }
}

дает мне следующий журнал:

Hibernate: insert into Base (id, DTYPE) values (null, 'ChildTypeA')
Hibernate: call identity()
Hibernate: insert into Container (id) values (null)
Hibernate: call identity()
Hibernate: insert into Container_Base (Container_id, childrenA_id) values (?, ?)
WARN : org.hibernate.util.JDBCExceptionReporter - SQL Error: 0, SQLState: null
ERROR: org.hibernate.util.JDBCExceptionReporter - failed batch
ERROR: org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:140)
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:128)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:262)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:182)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:76)
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:467)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:621)
    at com.whiteboard.wb.data.sample.DataGenerator$$EnhancerByCGLIB$$5c355801.generateTestData(<generated>)
    at entity.NewTest.test(NewTest.java:39)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:71)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:199)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:62)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: java.sql.BatchUpdateException: failed batch
    at org.hsqldb.jdbc.jdbcStatement.executeBatch(Unknown Source)
    at org.hsqldb.jdbc.jdbcPreparedStatement.executeBatch(Unknown Source)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
    ... 49 more
Hibernate: select container0_.id as id3_ from Container container0_

org.springframework.orm.hibernate3.HibernateJdbcException: JDBC exception on Hibernate data access: SQLException for SQL [insert into Container_Base (Container_id, childrenA_id) values (?, ?)]; SQL state [null]; error code [0]; Could not execute JDBC batch update; nested exception is org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
    at org.springframework.orm.hibernate3.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:645)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:102)
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:471)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:621)
    at com.whiteboard.wb.data.sample.DataGenerator$$EnhancerByCGLIB$$5c355801.generateTestData(<generated>)
    at entity.NewTest.test(NewTest.java:39)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:82)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:71)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:199)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:62)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:140)
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:128)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:262)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:182)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137)
    at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:76)
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:467)
    ... 40 more
Caused by: java.sql.BatchUpdateException: failed batch
    at org.hsqldb.jdbc.jdbcStatement.executeBatch(Unknown Source)
    at org.hsqldb.jdbc.jdbcPreparedStatement.executeBatch(Unknown Source)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:70)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:268)
    ... 49 more

Однако, если я оберну метод @Test в @Transactional, ошибка исчезнет, ​​и контейнер и дочерний объект будут успешно сохранены.Кроме того, если я удаляю OneToMany с ChildTypeB и имею только отношения с ChildTypeA, и контейнер, и дочерний объект успешно сохраняются.

Является ли это ошибкой в ​​сохраняемом приложением Spring управляемом постоянстве?Или я неправильно понимаю @Transactional?Или мне нужно сказать Hibernate разделить детей на разные таблицы, либо с помощью аннотации на отношения или на сами классы?

Спасибо за помощь.

Редактировать:

Добавление моих Persistence.xml и spring-data.xml

Persistence.xml (вся настоящая работа выполняется в spring-data.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="spring-jpa" />
</persistence>

spring-data.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
        http://www.springframework.org/schema/tx
     http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
       http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <!-- Scans the classpath of this application for @Components to deploy as beans -->
    <context:component-scan base-package="com.whiteboard.wb"/>
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="persistenceUnitName" value="spring-jpa"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="showSql" value="true"/>
                <property name="generateDdl" value="true"/>
                <property name="database" value="HSQL"/>
            </bean>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager"/>

    <jdbc:embedded-database id="dataSource" type="HSQL"/>

</beans>

1 Ответ

2 голосов
/ 18 октября 2011

Есть только одна проблема: Base следует аннотировать @MappedSuperClass вместо @Entity.После этого - и поставка пом - он работает нормально, с или без @Transactional в методе тестирования.Я положил его на GitHub.Вы можете просмотреть его или клонировать и запустить его с помощью

git clone git://github.com/zzantozz/testbed tmp
cd tmp
mvn clean test -pl stackoverflow/7809543-hibernate-spring-jpa

. Я думаю, что, перегоняя его, вы исключаете причину проблемы.Иногда, когда вы просто полностью потерялись, полезно создать ветку вашего проекта (если вы в git) или скопировать его в отдельный каталог (если вы обременены SVN) и начать взламывать материалэто не является проблематичным, пока вы не сможете точно описать код / ​​конфигурацию, вызывающую проблему.

Обновление: Понимание рассвета.Первая проблема: причина, по которой он ведет себя по-разному в зависимости от того, является ли тест @Transactional или нет, заключается в том, что ваша тестовая транзакция отличается от вашей DataGenerator транзакции.В тесте транзакция откатывается в конце теста DataGenerator оно совершено.Что еще более важно EntityManager не сбрасывается в тесте, потому что это обычно происходит только во время фиксации.flush() - это то, что вызывает выдачу SQL в базу данных, и именно отсюда ваша ошибка.Если вы вводите свой тест с помощью EntityManager и вызываете flush() в конце метода теста, то вы увидите то же поведение в вашем тесте @Transactional, что и в тесте, не являющемся @ Transactional.,Это довольно стандартная плата за тесты, когда вы выполняете откат транзакций для поддержания чистоты вашей базы данных.

Вторая проблема: поскольку Base - это сущность, а не просто суперкласс, содержащий некоторые общие поля, как я изначально думал, вы 'имеем дело с отображением наследования .(Основанное на аннотациях наследование рассматривается в отдельной справке Hibernate Annotations .) Независимо от того, осознали вы это или нет, вы неявно выбрали стратегию наследования "одна таблица" .Это JPA по умолчанию для отображения наследования, к лучшему или к худшему.Это означает, что все поля Base, ChildTypeA и ChildTypeB содержатся в базовой таблице.Если вы выключите вход в систему для отладки, вы увидите, что Hibernate генерирует эту структуру таблицы:

create table Base (DTYPE varchar(31) not null, id bigint generated by default as identity (start with 1), primary key (id))
create table Container (id bigint generated by default as identity (start with 1), primary key (id))
create table Container_Base (Container_id bigint not null, childrenB_id bigint not null, childrenA_id bigint not null, primary key (Container_id, childrenA_id), unique (childrenB_id), unique (childrenA_id))

Ваша проблема исходит от Container_Base.Если вы посмотрите внимательно, то увидите, что запись в этой таблице должна иметь первичный ключ ChildTypeA и первичный ключ ChildTypeB.Это не точно отражает вашу объектную модель, где это две не связанные коллекции.Я не уверен, почему Hibernate делает это.Я предполагаю, что он просто видит два (неявных) отображения таблиц соединений - это две таблицы соединений для ваших отношений «один ко многим», которые имеют одно и то же имя таблицы и объединяют их вместе.Это выглядит как кандидат на сообщение об ошибке для меня.В любом случае, есть как минимум два способа решить эту проблему:

  1. Переключиться на стратегию наследования таблиц на класс , добавив @Inheritance(strategy = InheritanceType.JOINED) к базе,Это создаст отдельные таблицы для Base, ChildTypeA и ChildTypeB.Тогда объединяемые таблицы будут сортироваться самостоятельно, потому что нет никакой двусмысленности.
  2. Явно назовите объединяющие таблицы в ваших отображениях на Контейнере, используя, например, @JoinTable(name = "containerChildA") и @JoinTable(name = "containerChildB") какподходящее.Это создаст две отдельные таблицы объединения, которые вам нужны, но при этом все Base, ChildTypeA и ChildTypeB будут находиться в одной таблице.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...