调用hibernateTamplate的merge方法,抛出异常ConstraintViolationException
违背一致性异常,merge
时难道不是当数据库存在此主键时update
,不存在时insert
吗,怎么会报主键冲突呢?即hibernate认为数据库没有,所以merge
时执行了insert
。
ConstraintViolationException
刚开始把这个单词都理解错了,此单词意为违背数据完整性约束(database integrity constraint),它是造成SpringDataIntegrityViolationException
的最常见的原因: spring-dataIntegrityviolationexception;(英语不达标不利于解决问题)
StackOverflow: merge performs insert instead of update
有回答:
I had a version column which was not set when seed data was inserted into database. Hence all the problems with update and delete
当原始数据插入数据库时有个version字段没有设置,因此所有的update和delete都出现问题。
即又是version
字段引起的,hibernate用version
字段做乐观锁,所以问题又回到了之前遇到的entity删不掉
:
hibernate Locking: https://quizix.gitbooks.io/hibernate/content/hibernate_user_guide/9_locking.html
A version or timestamp property can never be null for a detached instance. Hibernate detects any instance with a null version or timestamp as transient, regardless of other unsaved-value strategies that you specify. Declaring a nullable version or timestamp property is an easy way to avoid problems with transitive reattachment in Hibernate, especially useful if you use assigned identifiers or composite keys.
对于一个detached实例,version和timestamp属性永远不能为null。Hibernate检测到任何version或者timestamp属性为null的instance均视为transient。忽视你指定的其他未被保存的值。Hibernate中声明一个空的version或者timestamp属性是一个简单的办法来避免转换重附着,当你使用了指定Id或组合键时非常有用
回答中另一个例子:hibernate-merge-may-insert-new-record
级联查出的属性entity(User
的UserDetail
属性),在保存的时候并没有Id
,所以merge
时执行了insert而不是update。
Detached状态到Entity的所有状态
detach
不受EntityManager管理的对象,但仍代表数据库里的一个记录;文中列举了三个属性:
- 许多JPA方法不支持操作detached实例
- Retrieval by navigation from detached objects is not supported, so only persistent fields that have been loaded before detachment should be used. 这句话没读懂,大意应该是只有在detached前的persistent域才能使用(好像还能部分field受管理,即取回部分,取决于策略)
- 修改一个detached实例并不会影响到数据库,除非merge,重新受
EntityManager
管理
另外提一点关于merge(文中的级联merge与detach就不讲了):
The content of the specified detached entity object is copied into an existing managed entity object with the same identity (i.e. same type and primary key). If the EntityManager does not manage such an entity object yet a new managed entity object is constructed. The detached object itself, however, remains unchanged and detached.
对于merge
方法,如果EntityManager
中没有同样Id的实例,则new一个,否则复制其状态;但是这个待merge对象(merge
的参数)本身依然没有变化、依然detached,但merge
返回的是受管理的实例。
有人就提问了JPA的persist
与merge
的区别:jpa-entitymanager-why-use-persist-over-merge第一个回答中我们得知,persist
区别在,传入的实例就是那个受管理的实例,后续对实例的修改在flush
后也会持久化到数据库。
再来看Entity的所有状态:
Working with JPA Entity Objects entity都有哪些状态?Transient Persistent Detached
JAP的Entity状态变迁(来自StackOverflow回答:https://stackoverflow.com/a/30168342)
Hibernate中的状态变迁:
hibernate对EntityManager
的实现在类:org.hibernate.impl.SessionImpl
hibernate-core-3.6.10.Final.jar
baeldung对各种hibernate API的详细讲解,非常值得阅读:hibernate-save-persist-update-merge-saveorupdate
回到这个问题
问题简化成了:从数据库取出一个version
字段为NULL的Entity—>修改某个属性后保存—>hibernate merge
时抛出异常ConstraintViolationException
大致追了下hibernate的代码,看到每个数据库操作(不是非常准确)都被转换成一个Event
(org.hibernate.event.spi),比如MergeEvent
、LoadEvent
、SaveOrUpdateEvent
、PersistEvent
,然后都会提供默认的Listeners
去处理这些事件,Merge
对应一个DefaultMergeEventListener
,虽然是没看太明白。
所以原因就是,因为受管理的Entity的version
为NULL
,则认为它是Transient
状态的,于是就new
了一个managed entity
,然后把merge参数那个实例的所有属性都复制过来(包括primaryKey),然后执行insert
,报错。
回顾老问题:No EntityManager with actual transaction avaliable for current thread
找到Spring的代码: spring-orm-5.1.18-release.jar: org.springframework.orm.jpa.SharedEntityManagerCreator.SharedEntityManagerInvocationHandler
else if (transactionRequiringMethods.contains(method.getName())) {
// We need a transactional target now, according to the JPA spec.
// Otherwise, the operation would get accepted but remain unflushed...
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() &&
!target.getTransaction().isActive())) {
throw new TransactionRequiredException("No EntityManager with actual transaction available " +
"for current thread - cannot reliably process '" + method.getName() + "' call");
}
}
分解这个if条件:
transactionRequiringMethods
,
// transactionRequiringMethods
transactionRequiringMethods.add("joinTransaction");
transactionRequiringMethods.add("flush");
transactionRequiringMethods.add("persist");
transactionRequiringMethods.add("merge");
transactionRequiringMethods.add("remove");
transactionRequiringMethods.add("refresh");
可以看到remove
, flush
都需要事务,persist
后也需要flush
,为什么对应到CrudRepository
方法中save
不需要加@Transactional
而delete、update
相关的方法需要?看CrudRepository
的实现类:SimpleJpaRepository
的确给save
加了@Transactional
注解,估计是因为设计CrudRepository
接口时不需要关心这个。
TransactionSynchronizationManager.isActualTransactionActive()
public static boolean isActualTransactionActive() {
return actualTransactionActive.get() != null;
}
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
另外那里的target.getTransaction().isActive()
中target
是什么?是EntityManager
其注释为是否有事务正在执行。同时注意到hibernate中EntityManager
的实现类:org.hibernate.internal.SessionImpl
,可以看其对应的isActive
的实现
TransactionSynchronizationManager#bindResource
引申到spring-tx.jarorg.springframework.transaction.support.TransactionSynchronizationManager#bindResource(Object key, Object value)
方法:为什么到了这个方法,因为HibernateTemplate
中对任意Event
的发射(fire
)都会跳到protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNativeSession)
我见过另一种Dao的写法就是使用这个tamplate
进行save、merge、delete
等,默认情况下enforceNativeSession
为true
,而执行这个动作时,session是哪里来的呢?是从sessionFactory来的,没有就重新打开一个session,(还记得古老的application.xml
中对sessionFactory
的配置?)
按道理说sessionFactory就自身持有session,但实际上人家用的是org.hibernate.context.spi.CurrentSessionContext
让它来提供当前的session,注意这里是个SPI,就说明可灵活替换,实现这个接口的类主要有三个有:hibernate的两个:ManagedSessionContext
ThreadLocalSessionContext
,Spring的一个 org.springframework.orm.hibernate5.SpringSessionContext
Hibernate这两个类 比较像,都提供了ThreadLocal<Map<SessionFactory,Session>>
保存映射关系以便currentSession
方法(通过sessionFactory为键)获取session,但是后者有个优势是提供了三个方法isAutoCloseEnabled
isAutoFlushEnabled
buildOrObtainSession
来获取定制化的session;
另外贴一下 org.hibernate.internal.SessionFactoryImpl
SPI选择CurrentSessionContext
的过程:
private CurrentSessionContext buildCurrentSessionContext() {
String impl = (String) properties.get( Environment.CURRENT_SESSION_CONTEXT_CLASS );
// for backward-compatibility
if ( impl == null ) {
if ( canAccessTransactionManager() ) {
impl = "jta";
}
else {
return null;
}
}
if ( "jta".equals( impl ) ) {
// if ( ! transactionFactory().compatibleWithJtaSynchronization() ) {
// LOG.autoFlushWillNotWork();
// }
return new JTASessionContext( this );
}
else if ( "thread".equals( impl ) ) {
return new ThreadLocalSessionContext( this );
}
else if ( "managed".equals( impl ) ) {
return new ManagedSessionContext( this );
}
else {
try {
Class implClass = serviceRegistry.getService( ClassLoaderService.class ).classForName( impl );
return (CurrentSessionContext)
implClass.getConstructor( new Class[] { SessionFactoryImplementor.class } )
.newInstance( this );
}
catch( Throwable t ) {
LOG.unableToConstructCurrentSessionContext( impl, t );
return null;
}
}
}
并且SpringORM悄悄把自己的放进去了: spring-orm.jar org.springframework.orm.hibernate5.LocalSessionFactoryBuilder
:
getProperties().put(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName());
贴一个baeldung的问题诊断博文:no-hibernate-session-bound-to-thread-exception它描述了一种异常即和我上述是相关的。
接下来来到SpringSessionContext
的currentSession
: 先直接从ThreadLocal
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Session session = this.sessionFactory.openSession();
if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
session.setFlushMode(FlushMode.MANUAL);
}
SessionHolder sessionHolder = new SessionHolder(session);
// 新建一个SpringSession同步通知器并且将它注册进本线程事务同步器集合
TransactionSynchronizationManager.registerSynchronization(
new SpringSessionSynchronization(sessionHolder, this.sessionFactory, true));
TransactionSynchronizationManager.bindResource(this.sessionFactory, sessionHolder);
sessionHolder.setSynchronizedWithTransaction(true);
return session;
}
hibernate删除一个Transient Entity会怎样?
首先new出一个entity而不是从数据库查出,并且此Entity有version注解或配置,然后分两种情况:1. 有PK,version不为NULL (成功从DB删除) 2. 有PK,version为NULL(不能从DB删除)