hibernate ConstraintViolationException, Duplicate primaryKey

调用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(UserUserDetail属性),在保存的时候并没有Id,所以merge时执行了insert而不是update。

Detached状态到Entity的所有状态

detach
不受EntityManager管理的对象,但仍代表数据库里的一个记录;文中列举了三个属性:

  1. 许多JPA方法不支持操作detached实例
  2. 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受管理,即取回部分,取决于策略)
  3. 修改一个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的persistmerge的区别: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),比如MergeEventLoadEventSaveOrUpdateEventPersistEvent,然后都会提供默认的Listeners去处理这些事件,Merge 对应一个DefaultMergeEventListener,虽然是没看太明白。
所以原因就是,因为受管理的Entity的versionNULL,则认为它是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不需要加@Transactionaldelete、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等,默认情况下enforceNativeSessiontrue,而执行这个动作时,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.SessionFactoryImplSPI选择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它描述了一种异常即和我上述是相关的。
接下来来到SpringSessionContextcurrentSession: 先直接从ThreadLocal拿,有可能拿到各种乱七八糟的东西 Session SessionHolder EntityManagerHolder 没有且有Transaction synchronizations活着就新建,(事务同步器? 这是个什么,前面非直接从乱七八糟的东西中拿出来的session(Holder)都要确保(确保,就是先check否则再do嘛)注册进session同步器容器中),这就到了本节的关注的bindResource,所以我们看到bindResourcekey都是sessionFactory,而value就是各种直接、间接可获取session的Object
那么这个Transaction synchronizations事务同步器是什么 Oracle s1204transactionsynchronization01.html spring-tx: TransactionSynchronization:Interface for transaction synchronization callbacks,这个东西就是一个观察者模式的应用,当事务开始、结束等,会对同步器进行通知,
代码在这:

       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删除)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
二、完善其余功能节点 -- 流程 1、 将 提供其余节点页面 复制 pages/zhongzhuan 入库(点击左侧菜单入库) ----- Action (查询入库任务列表)---- instore_list.jsp ---- 办理任务 ---- instore_complete.jsp(提交form) --- Action (办理任务流转到下一个节点) 修改function 数据表 入库 task_findInStoreTask.action 出库 task_findOutStoreTask.action 签收 task_findReceiveInfoTask.action 2、 查询任务列表 TaskAction // 业务方法 ----- 查询入库环节 的个人任务 public String findInStoreTask(){ // 登陆用户 User user = (User) getSession().getAttribute("user"); List<ZhongZhuanInfo> zhongZhuanInfoList = myTaskService.findTransferTask(user,"入库"); // 值栈 ActionContext.getContext().put("zhongZhuanInfoList", zhongZhuanInfoList); return "findInStoreTaskSUCCESS"; } // 业务方法 ----- 查询出库环节 的个人任务 public String findOutStoreTask(){ // 登陆用户 User user = (User) getSession().getAttribute("user"); List<ZhongZhuanInfo> zhongZhuanInfoList = myTaskService.findTransferTask(user,"出库"); // 值栈 ActionContext.getContext().put("zhongZhuanInfoList", zhongZhuanInfoList); return "findOutStoreTaskSUCCESS"; } // 业务方法 ----- 查询签收环节 的个人任务 public String findReceiveInfoTask(){ // 登陆用户 User user = (User) getSession().getAttribute("user"); List<ZhongZhuanInfo> zhongZhuanInfoList = myTaskService.findTransferTask(user,"配送签收"); // 值栈 ActionContext.getContext().put("zhongZhuanInfoList", zhongZhuanInfoList); return "findReceiveInfoTaskSUCCESS"; } 3、 配置struts.xml结果页面 <result name="findInStoreTaskSUCCESS">/WEB-INF/pages/zhongzhuan/instore_list.jsp</result> <result name="findOutStoreTaskSUCCESS">/WEB-INF/pages/zhongzhuan/outstore_list.jsp</result> <result name="findReceiveInfoTaskSUCCESS">/WEB-INF/pages/zhongzhuan/receiveinfo_list.jsp</result> 4、 Action办理对应节点任务 // 业务方法 ---- 办理入库任务 public String instorecomplete(){ InStore inStore = new InStore(); inStore.setInfo(info); inStore.setUpdateTime(new Date()); // 调用业务层 myTaskService.completeInStore(taskId, inStore); return "instorecompleteSUCCESS"; } // 业务方法 ---- 办理出库任务 public String outstorecomplete(){ OutStore outStore = new OutStore(); outStore.setInfo(info); outStore.setUpdateTime(new Date()); // 调用业务层 myTaskService.completeOutStore(taskId, outStore); return "outstorecompleteSUCCESS"; } // 业务方法 ---- 办理签收任务 public String receiveinfocomplete(){ ReceiveGoodsInfo receiveGoodsInfo = new ReceiveGoodsInfo(); receiveGoodsInfo.setInfo(info); receiveGoodsInfo.setUpdateTime(new Date()); // 调用业务层 myTaskService.completeReceiveGoodsInfo(taskId, receiveGoodsInfo); return "receiveinfocompleteSUCCESS"; } 5、 struts.xml 跳转回任务列表 <result name="instorecompleteSUCCESS" type="redirectAction">task_findInStoreTask</result> <result name="outstorecompleteSUCCESS" type="redirectAction">task_findOutStoreTask</result> <result name="receiveinfocompleteSUCCESS" type="redirectAction">task_findReceiveInfoTask</result> 在JBPM在流程结束时,发生异常 org.springframework.dao.DataIntegrityViolationException: could not delete: [org.jbpm.pvm.internal.model.ExecutionImpl#50001]; SQL [delete from JBPM4_EXECUTION where DBID_=? and DBVERSION_=?]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not delete: [org.jbpm.pvm.internal.model.ExecutionImpl#50001] 解决: hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect ======================================================================================================================== 三、 流程实例 查看管理 1、 查询实例功能 查询系统正在运行实例 ExecutionService.createProcessInstanceQuery() 查询系统已经完成的实例 HistoryService.createHistoryProcessInstanceQuery() json/admin/json { id:1005, pId:100, name:"查看正在运行的任务", t:"",page:"page_workflow_processinstance.action"} 改为 { id:1005, pId:100, name:"查看正在运行的任务", t:"",page:"processinstance_list.action"} 2、 每个运行流程实例,关联流程变量 ZhongZhuanInfo 包含所有流程信息 根据流程实例id 查询ZhongZhuanInfo数据 (ZhongZhuanInfo) processEngine.getExecutionService().getVariable(pid, "zhongZhuanInfo"); 服务器返回 中转信息 result = zhongZhuanInfo.toString(); <result name="showInfoSUCCESS" type="json"> <param name="root">result</param> </result> 在页面抓取中转信息,回显 $.post("${pageContext.request.cotnextPath}/processinstance_showInfo.action", {"pid": pid}, function(data){ $.messager.alert("流程实例信息", data, "info"); }); 3、 实例运行的流程图查看 需要在流程图上面 标记每个 流程运行到哪个任务节点 第一步: 为每条实例记录,添加查看流程图函数 <a href="#" class="easyui-linkbutton" onclick="showPng('<s:property value="#processInstance.id"/>');">查看流程图</a> function showPng(pid){ alert("查看" + pid + "对应流程图"); } 第二步: 流程图查看Action 可以复用 ---- processdefinition_showpng.action?deploymentId= & resourceName= RepositoryService.getResourceAsStream(java.lang.String deploymentId, java.lang.String resourceName) * resourceName 可以通过 deploymentId 动态获得 RepositoryService repositoryService = processEngine.getRepositoryService(); ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).uniqueResult(); resourceName = processDefinition.getImageResourceName(); 第三步: 点击查看流程图, 获得活动节点坐标 通过 window.showModalDialog() 查看流程图页面 (弹出窗口,显示页面 url地址不能修改 ) function showPng(pid){ //alert("查看" + pid + "对应流程图"); window.showModalDialog("${pageContext.request.contextPath}/processinstance_findactivityinfo.action?pid="+pid); } 查看某一个具体活动节点坐标 String processDefinitionId = "test-1"; // 流程定义的id String activityName = "start1"; // 活动的名称 ActivityCoordinates c = processEngine.getRepositoryService() .getActivityCoordinates(processDefinitionId, activityName); 查看当前流程实例活动所有节点名称 ProcessInstance的 java.util.Set<java.lang.String> findActiveActivityNames() @Override public List<ActivityCoordinates> findActivityCoordinates(String pid) { // 1、 根据流程实例id 获得所有活动节点名称 ProcessInstance processInstance = processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(pid).uniqueResult(); Set<String> activityNames = processInstance.findActiveActivityNames(); // 2、一个活动节点 --- 对应一个坐标对象 List<ActivityCoordinates> activityCoordinates = new ArrayList<ActivityCoordinates>(); for(String activityName: activityNames){ // 获得每一个活动节点名称 String processDefinitionId = processInstance.getProcessDefinitionId();//流程定义id ActivityCoordinates activityCoordinate = processEngine.getRepositoryService().getActivityCoordinates(processDefinitionId, activityName); activityCoordinates.add(activityCoordinate); } return activityCoordinates; } ============ 为了在下一个页面 可以显示流程图, 根据实例id 查询 发布id @Override public String findDeploymentIdByProcessInstanceId(String pid) { ProcessInstance processInstance = processEngine.getExecutionService().createProcessInstanceQuery().processInstanceId(pid).uniqueResult(); ProcessDefinition processDefinition = processEngine.getRepositoryService().createProcessDefinitionQuery().processDefinitionId(processInstance.getProcessDefinitionId()).uniqueResult(); return processDefinition.getDeploymentId(); } <img src="${pageContext.request.contextPath }/processdefinition_showpng.action?deploymentId=<s:property value="#deploymentId"/>" style="position: absolute;top: 0;left: 0"/> <s:iterator value="#activityCoordinates" var="activityCoordinate"> <div style="width: <s:property value="#activityCoordinate.width"/>px; height: <s:property value="#activityCoordinate.height"/>px; left: <s:property value="#activityCoordinate.x"/>px; top: <s:property value="#activityCoordinate.y"/>px; position: absolute; border-color: red; border-style: solid; border-width: 1px;"> </s:iterator>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值