java阴魂不散_EntityManager无法remove entity的问题

今天遇到一个奇怪的事情,利用EntityManager.remove(entity)方法删除一个entity时,删不掉,也不报错。后来经过多方查证,解决了这个问题。

ERD

0550849aea99b35befcc1bfdc33b29f9.png

Entity定义

数据

问题

按照多年SQL脚本操作数据的经验,直接从B表中删除记录b(id:2)是可行的。A表上不存在任何对B表的外键引用,所以可以直接删除B表上的数据,数据库管理系统不会不开心。但是,使用JPA中EntityManager的remove(entity)方法来删除b(id:2)时,问题发生了。remove根本删不掉b(id:2)记录,别说数据库中的记录了,连SQL语句都没有从JEE container中发出来。而且,更要命的是,没!有!报!错!

我做了一个替换方案,用JPQL语句直接删除B(id:2),结果成功了。呵呵,到此可不算完结,不然我也不用大费周章的把这件事情记录下来。在删除 B(id:2)之后,我又尝试保存对A所做的变更,这么一保存,又出问题了。JPA报错,说是B(id:2)找不到,我晕。这又是什么情 况,B(id:2)明明已经被我删掉了,怎么在persist A的时候JPA却要去检查一个已经被删掉的object?我确信在用JPQL删掉了B(id:2)后,我手动从A.bs集合中剔除了B(id:2),为啥 这个B(id:2)阴魂不散呢?

分析

在翻阅了一些文档后,我隐约意识到,问题应该与entity的几种状态(尤其是detached状态)以及O/R Mapping框架中的缓存有关。说白了,就是程序哪里产生数据不一致了。一般,之所以产生这种不一致问题可能与受管对象的状态、生命周期或是访问范围等有关。那么,代入JPA中考虑,对应的应该是Entity的生命周期或访问机制(缓存机制)。

继续深究发现,这个issue是由于多个方面综合作用下产生的。

首先,问题的最关键之处:A与B的bidirectional OneToMany(双向一对多关系)。

这其实很好理解,就像Java中的垃圾回收机制一样,被用到的Object不会被GC。同理,被引用的child,也就是这个B(id:2)啦,一直被A(id:1)引用着呢,JPA怎么会让你把他干掉?!。前面未曾提及,在删除B(id:2)之前,A(id:1)被JPA读取过。当我试图删除B(id:2)时A(id:1)应该还在JPA的缓存里待着。根据Entity上的annotation标注,A(id:1)应该同时保有B(id:1)、B(id:2)的引用(就是那个List bs集合中的两个元素)。JPA的remove出于某种保护,并不会让你把被引用的B(id:2)删掉。

当然,如果你执意要删除,那么可以用entityManager.createQuery("DELETE FROM B WHERE B.id=2").executeUpdate();来强行删除指定的数据库记录。因为createQuery().executeUpdate()会向DBMS发送指定的sql,如果有报错,异常会由DBMS通过底层JDBC报给JPA框架最终通过EntityManager冒出来。我就是用了这种方法强行把B(id:2)给干掉了。

接下来要说的,就是因素二:缓存与实际数据库不一致

看上面的那段标红的内容。是不是想到了什么?在删除B(id:2)之前,A(id:1)带着对B(id:1)和B(id:2)的引用一直待在缓存里。当B(id:2)被我用JPQL强行删除之后,并没有任何代码去更新缓存里的A(id:1),所以A(id:1)上应该还有B(id:2)的引用。接下来,要persist A(id:1)的改动。虽然我后来手动做了A.bs.remove(B(id:2))(从bs集合中剔除了B(id:2)的引用),但很遗憾,A(id:1)已经处于detached状态(即游离状态,姑且把已经处于游离状态的A(id:1)叫做a(id:1))。对一个已经处于游离状态的object进行的改动,不会映射到对应的Entity上,换句话说,不论我怎样操作a(id:1),在JPA缓存中的A(id:1)不会被更新。而且,戏剧性的一幕发生了,当我尝试着去persist一个游离对象a(id:1)时,JPA通过a(id:1).equals(A(id:1))的比较,认为a(id:1) == A(id:1),因为两个对象的id一样,hashcode一样,所以JPA从缓存中找到A(id:1),试图再次persist一遍,接下来的事情也就不用我说了,JPA报错,并提示我找不到B(id:2)。(什么?为什么会去找B(id:2)?哦,那是因为A上的Cascade定义,)

解决方案

我这里有两种解决方案:

1、以更新A为起始点,剔除B(id:2)后,persist A。由于A上设置的Cascade,在更新A的同时,JPA会级联删除B(id:2)

2、可以强行删除B(id:2),但在对A(id:1)进行任何操作前,先去fecth一下A(id:1),也就是强行刷新一下JPA的缓存。

我个人推荐第一种方案。

本文转自 rickqin 51CTO博客,原文链接:http://blog.51cto.com/rickqin/1766492

第一次写博客,那是因为我想与大家分享.Net世界.我原来是ASP程序员,与.Net结缘那是在两年多前.第一次接触它,就喜欢上了.哈哈 接着我给大家分享一下我在项目中用到的数据访问层,这个是我用微软网站上得到的DBHepler数据库访问层的一次改编,让它支持实体类和用表达 式的方式生成SQL,而且更关键的是,他是采用参数的方式传值的,这样就避免了SQL的注入啦.. 以下是这个项目的结构 [SuperDAL] ---DbHelper.cs(来自MSDN的数据访问层) ---EntityManager.cs(实体类管理) ---Expressions.cs(实体类表达式查询) ---Expression.cs(实体类表达式) ---OrderByExpressions.cs(排序表达式查询) ---OrderByExpression.cs(排序表达式) ---ObjectValues -------OrderBy.cs(排序枚举类) ---DBManager.cs(数据访问类管理) ---DbParams.cs(数据库访问类参数) ---DataTableExtensions.cs(这个就是顾名思义啦,DataTable传实体类) 在这里最主要介绍的就是EntityManager这个啦,使用方法如下: 有数据库DB的一张表Customs CREATE TABLE [Customs] ( [Name] [varchar] (20) , [Password] [varchar] (20) , [Email] [varchar] (50) , [Phone] [varchar] (11) NULL , [Old] [int] , [Birthday] [DateTime] ) 有个实体类Customs.cs,结构如下: public class Customs { public string Name {get;set;} public string Password {get;set;} public string Email {get;set;} public string Phone {get;set;} public int Old{get;set} public DateTime Brithday {get;set;} } 数据库表与实体Customs结构是一一对应的.有了实体类CUstoms,下面就可以操作实体类跟操作数据库一样的啦,我们新建一个实体类管理类 CustomsManager.cs public class CustomsManager:EntityManager { public Customs GetByName(string name) { //创建表达式Expressions Expressions exps=new Expressions(); //向表达式添加条件 exps.Eq("name",name); //根据条件查询返回实体类 return EM_GetUnique(exps); } public List SearchByName(string name) { //同样像上面一样建立表达式类 Expressions exps=new Expressions(); exps.Like("name",name);//当然,有年朋友会说如果我要姓为"陈"的,那有些人的名字带陈的怎么办,没关系,可以改为 exps.LeftLike ("name",name); //根据条件查询返回实体类 return EM_GetEntity(exps); } /// /// 登录 /// /// 用户名 /// 密码 public List Login(string name,string password) { Expressions exps=new Expressions(); exps.Eq("name",name); exps.Eq("password",password); return EM_GetEntity(exps); } /// /// 选择年龄大于指定岁数的,并按年龄从小到大排序 /// /// 年龄 public List SelectOlder(int old) { Expressions exps=new Expressions(); exps.Gt("old",old); exp.OrderBys.Add("old", OrderBy.Asc); return EM_GetEntity(exps); } /// /// 选择年龄大于小于指定岁数段的,并按年龄从小到大,姓名从字母升序排序 /// /// 年龄 public List SelectByOld(int oldStart,int oldend) { Expressions exps=new Expressions(); exps.Between("old",oldStart,oldEnd); exp.OrderBys.Add("old", OrderBy.Asc); exp.OrderBys.Add("name",OrderBy.Asc); return EM_GetEntity(exps); } #region 增删改 操作 /// /// 更新操作 /// /// 实体类 public int Update(Customs customs) { return EM_Save(customs);//返回更新的记录数,当然,一般成功执行就会返回1;也可以改上面为public void Update(Customs customs) } /// /// 删除操作 /// /// public int DeleteByName(string name) { Expressions exps=new Expressions(); exps.Eq("name",name); return EM_Delete(exps); } /// /// 删除操作 /// /// 实体类 public int Save(Customs custom) { return EM_Save(custom); } #endregion } 当然还有更多的也就不一一贴出来了 Expressions支持的表达式有 1. Eq (等于)[name=value] 2. NotEq (不等于)[not name = value] 3. Ge (大于等于)[name >=value] 4. Gt (大于)[name>value] 5. Le (小于等于)[name<=value] 6. Lt (小于)[name<value] 7. In (包括)[name in (value)] 8. NotIn (不包括)[not name in (value) 9. IsNotNull (不为NULL)[not name is null] 10. IsNull (等于NULL)[name is null] 11. Between (之间)[name between valueStart and valueEnd] 12. Like (模糊) [name like ‘%value%’ ] 13. LeftLike (左模糊) [name like ‘%value’] 14. RightLike (右模糊) [name like ‘value%’] 其它功能待与Net爱好者探讨啦,希望你有更好的思路
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值