Hibernate 和 Spring - 具有从同一父级继承的多个成员的实体会导致 JDBCException、@Transactional 怪异
(这是对我的其他问题的重写)
我有一个 Spring WebMVC 应用程序,它使用Hibernate 作为其后端。由于我的域模型不断变化,并且我没有使用旧数据库作为后端,因此我通过设置
问题来自于我的一个实体与共享同一父类的实体类型有两个一对多关系。以下是我的类的非常基本的版本,仍然显示问题:
@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 中,错误就会消失,容器和子容器都会成功坚持下来了。 另外,如果我删除带有 ChildTypeB 的 OneToMany 并且仅与 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>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
只有一个问题:
Base
应该用@MappedSuperClass
注释,而不是@Entity
。之后 - 并提供一个 pom - 无论测试方法上有或没有@Transactional
,它都可以正常工作。我把它放在github上了。您可以浏览它或克隆并运行它 我认为在提炼它的过程中,你就排除了导致问题的原因。有时,当你完全迷失时,创建项目的一个分支(如果你在 git 中)或在单独的目录中创建一个副本(如果你背负着 SVN)并开始破解这些内容会很有帮助。除非您能准确地找到导致问题的代码/配置,否则这不会有问题。
更新:了解黎明。第一个问题:根据测试是否为@Transactional,其行为有所不同的原因是您的测试事务与您的
DataGenerator
事务不同。在测试中,事务为flush()
是导致向数据库发出 SQL 的原因,这就是错误的来源。如果您使用 EntityManager 注入测试并在测试方法末尾调用flush()
,那么您将在 @Transactional 测试中看到与您在中看到的相同的行为现在你的非@Transactional 了。当您进行事务回滚以保持数据库干净时,这是非常标准的测试费用。第二个问题:因为 Base 是一个实体,而不仅仅是像我最初想象的那样包含一些公共字段的超类,所以您正在处理 继承映射。 (基于注释的继承在单独的 Hibernate Annotations 参考。)无论您是否意识到,您都已隐式选择了 “单表”继承策略。无论好坏,它都是 JPA 映射继承的默认值。这意味着 Base、ChildTypeA 和 ChildTypeB 的所有字段都包含在 Base 表中。如果您关闭日志记录以进行调试,您将看到 Hibernate 正在生成此表结构:
您的问题来自 Container_Base。如果仔细观察,您会发现该表中的条目需要同时具有 ChildTypeA 主键和 ChildTypeB 主键。这不能准确反映您的对象模型,它们是两个不相关的集合。我不确定 Hibernate 为什么要这样做。我的猜测是,它只看到两个(隐式)联接表映射(即一对多关系的两个联接表),它们具有相同的表名并将它们组合在一起。对我来说,这看起来像是错误报告的候选者。无论如何,至少有两种方法可以解决这个问题:
@Inheritance(strategy = InheritanceType.JOINED)
< /a> 到基地。这将为 Base、ChildTypeA 和 ChildTypeB 创建单独的表。然后连接表将自行排序,因为没有歧义。@JoinTable(name = "containerChildB")
根据情况而定。这将创建您需要的两个单独的联接表,但将 Base、ChildTypeA 和 ChildTypeB 全部保留在同一个表中。There's only one problem with that:
Base
should be annotated with@MappedSuperClass
instead of@Entity
. After that--and supplying a pom--it works fine, either with or without@Transactional
on the test method. I put it on github. You can browse it or clone and run it withI think in distilling it, you cut out what was causing the problem. Sometimes when you're just completely lost, it's helpful to make a branch of your project (if you're in git) or a copy of it in a separate directory (if you're burdened with SVN) and start hacking out the stuff that isn't problematic until you can get it down to exactly the code/configuration that's causing the problem.
Update: Understanding dawns. First issue: the reason it behaves differently depending on whether the test is @Transactional or not is that your test transaction is different from your
DataGenerator
transaction. In the test, the transaction is rolled back at the end of the test. InDataGenerator
, it's committed. More importantly the EntityManager isn't flushed in the test because that typically only happens at commit time.flush()
is what causes SQL to be issued to the database, and that's where your error is coming from. If you inject your test with an EntityManager and callflush()
on it at the end of the test method, then you'll see the same behavior in your @Transactional test as what you're seeing in your non-@Transactional one now. This is pretty standard fare for tests when you're doing transaction rollbacks to keep your db clean.Second issue: Because Base is an entity and not just a superclass containing some common fields like I initially thought, you're dealing with inheritance mapping. (Annotation-based inheritance is covered in the separate Hibernate Annotations reference.) Whether you realized it or not, you've implicitly chosen the "single table" inheritance strategy. It's the JPA default for mapping inheritance, for better or worse. That means that all fields of Base, ChildTypeA, and ChildTypeB are contained in the Base table. If you turn logging down to debug, you'll see that Hibernate is generating this table structure:
Your trouble is coming from Container_Base. If you look carefully, you'll see that an entry in that table is required to have both a ChildTypeA primary key and a ChildTypeB primary key. This doesn't accurately reflect your object model, where they're two, unrelated collections. I'm not sure why Hibernate is doing that. My guess is that it just sees two (implicit) join table mappings--that's the two join tables for your one-to-many relationships--that have the same table name and combines them together. This looks like a candidate for a bug report to me. Anyway, there are at least two ways to solve this:
@Inheritance(strategy = InheritanceType.JOINED)
to Base. This will create separate tables for Base, ChildTypeA, and ChildTypeB. Then the join tables will sort themselves out on their own because there's no ambiguity.@JoinTable(name = "containerChildA")
and@JoinTable(name = "containerChildB")
as appropriate. This will create the two separate join tables you need but leave Base, ChildTypeA, and ChildTypeB all living in the same table.