Hibernate持久化对象生命周期之实战探索

来源:互联网 发布:js获取application值 编辑:程序博客网 时间:2024/06/05 07:25

正所谓“工欲善其事,必先利其器”,我陈述过自己的从业经历,因为并非IT科班出身,刚入职有很多不懂,由于需要快速开发,所以在还未对Hibernate有仔细的认识前,我就兼任新老系统的非对称数据库数据迁移、同步和校验程序的开发。在开发实战中积累了一定的错误经验,然后返回去抽空仔细查看了技术书籍或文档,特此记录,以备后忘。 

         持久化对象的生命周期,是Hibernate应用中的一个关键概念。对生命周期的理解和掌握,对Hibernate的正确应用很有必要。……

         言归正传,Hibernate持久化对象生命周期有3种状态:Transient(瞬时状态),persistent(持久状态),Detached(游离状态)。定义这三类状态,其实是和Hibernate的session息息相关。所谓transient,即对象在内存中是自由存在的,与数据库中的记录无关。比下:

BookBo bo = new BookBo();bo.setCode("v10234");bo.setName("Torrent");

对象bo与session没有任何关联,就是一个普通的java对象,此时bo即处于transient状态。

如果执行下面代码

session.update(bo);

则bo对象处于由Hibernate框架所管理的状态,即实体对象被纳入Hibernate实体容器session中加以管理,此时该实体对象处于persistent状态,其变更将由Hibernate持久化到数据库中这句话非常重要,我曾经因为没有仔细理解这句话,犯过如下错误:

背景简介:BZ当时开发一个数据同步项目,数据同步的两边数据库设计是不对称的,所以需要进行逻辑转换,为了开发便捷就使用了Hibernate,通过Quartz控制同步周期。其中,在同步时,为了同步系统也对新系统数据进行表更操作,为避免风险,程序上控制新平台数据只提供查询——现在想想,更合理的方式应该是通过数据库控制,仅适用可查询账号(这个不完全满足需求,但是可以仅对需要特殊变更的表开发账户权限)。当然,也就因为当时这个疏忽设计,加上下面这个疏忽,哥们废了大半个下午用于恢复新平台的数据,这算是血的教训,特此一记!

         嗯,在介绍完detached状态后,哥们会表示惭愧,对Hibernate的该设计顶礼膜拜,稍后会华丽的展示一下Hibernate的这个状态管理有多么的高效!如果仔细理解persistent状态的对象会交由session管理,那么上面的严重问题就不难看出来了。本来程序设计宗旨,不希望新平台的数据被同步程序变动,可是,因为哥们在查询完之后,不是将实体集合抛出,从而令他们处于detached状态再做处理,而是直接在session未关闭前就对实体集合进行变动。就这么看似平淡的修改,导致的就是新老数据两边的不一致,因为新平台数据本身需要加密,就这么被哥们给解密,然后重新保存了。当时,哥们懵了,觉得这个Hibernate这个设计有点不可理喻,就此发誓,总有一天,要讨回这笔时间债回来!

 

游离状态(detached)

         处于persistent状态的对象,其对应的session实例被关闭之后,该对象就处于detached状态。处于detached状态的对象可以重新被新的session关联,重新回到persistent状态,如果没有被重新关联,个人感觉其实和transient的状态没有太大的区别——主要区别在对象的主键。

 

好的,介绍完detached状态,就介绍一下哥们是怎么逆袭的——如何巧妙的使用Hibernate的缓存(对象状态和缓存机制是相关的),可以提高代码效率。

 

同样是上述的同步程序,已经在线运行了数个月,但是因为新平台会有一定的业务变更,所以同步程序为了能够保证效率,往往也会做一定的调整。比如,这个版本碰到的,由于特殊原因,需要对管理的部分IP进行数据更新,该更新有新平台程序完成,每5分钟同步一次,每次同步数据量6000条左右。我的同步程序则是每分钟同步一次,每次会检测到上次同步时间到当前时间变更的记录(数据库有时间标志字段)。一般,平台上每分钟操作不会有太大的数据,包括关联关系,平均在50条以内。所以,为了方便查询同步报警的原因,同步程序中将Hibernate的sql打印出来,并且把绑定的参数也打印出来,具体配置如下:

Log4j.properties#sql日志log4j.appender.F-sql=org.apache.log4j.RollingFileAppenderlog4j.appender.F-sql.File=./logs/cnc-rmp-synchronous-sql.loglog4j.appender.F-sql.Append=truelog4j.appender.F-sql.Threshold=TRACElog4j.appender.F-sql.MaxFileSize=30MBlog4j.appender.F-sql.MaxBackupIndex=30log4j.appender.F-sql.layout=org.apache.log4j.PatternLayoutlog4j.appender.F-sql.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c %M%n[%-5p] %m%n#Hibernatelog4j.logger.org.hibernate.SQL = TRACElog4j.logger.org.hibernate.type.descriptor.sql.BasicBinder= TRACE  hibernate-old.cfg.xml<propertyname="show_sql">true</property><property name="format_sql">true</property>


原本配置的sql日志30M*30,将近1G大小,可以记录3到4天的sql日志。因为报警脚本是通过邮件发送通知的,所以基本上这个时长是绰绰有余的。但是,因为新的需求,每五分钟IP变更数据达到6000条,平均每分钟达到1200条有余,这样的结果是导致在没有修改之前的代码前,每次碰到这6000条同步数据,会导致同步耗时超过一分钟,并且会引起sql日志5分钟就撑满一个30M的文件,如下图


如此,sql日志就变得没有太大意义。因为30个文件只能记录下150分钟,不利于下班后的报警处理——使用平台的人可能在本人下班后还在使用,所以同步程序并不会关闭。哥们考虑到可以使用Hibernate的persistent状态下的对象交由session管理,如果没有更新,则不做数据库的更新。太棒了,代码修改如下:


上述getModifiedIpPos方法代码如下

private List<IpPo> getModifiedIpPos(List<String> ipUuids) {        String hql = "FROM IpPo p WHERE p.ipid IN (:uuids)";        Query query = session.createQuery(hql);        query.setParameterList("uuids", ipUuids, StandardBasicTypes.STRING);        List<IpPo> pos = query.list();        return pos;    }

还有convertIp()方法是将新平台Po转换为老平台数据库的Po,内部为不对称的表设计做业务处理,不做详细展示。

通过日志打印查看效果:

Hibernate:    /*FROM    IpPo pWHERE    p.ipid IN (        :uuids    ) */ select        ippo0_.ipid as ipid1_7_,        ippo0_.cip as cip2_7_,        ippo0_.cipv6HighOrder as cipv3_7_,        ippo0_.cipv6LowOrder as cipv4_7_,        ippo0_.id as id5_7_,        ippo0_.ip as ip6_7_,        ippo0_.ipsegid as ipsegid7_7_,        ippo0_.remark as remark8_7_,        ippo0_.useflag as useflag9_7_,        ippo0_.useobjectid as useobje10_7_,        ippo0_.usetype as usetype11_7_    from        RMM_IPTable ippo0_    where        ippo0_.ipid in (          ? , ? , ……………………………………………………? , ?        )Hibernate:    /* update        com.*.synchronous.po.olddb.ipsegment.IpPo */ update            RMM_IPTable        set            cip=?,            cipv6HighOrder=?,            cipv6LowOrder=?,            id=?,            ip=?,            ipsegid=?,            remark=?,            useflag=?,            useobjectid=?,            usetype=?        where            ipid=?Hibernate:    /* update        com.*.synchronous.po.olddb.ipsegment.IpPo */ update            RMM_IPTable        set            cip=?,            cipv6HighOrder=?,            cipv6LowOrder=?,            id=?,            ip=?,            ipsegid=?,            remark=?,            useflag=?,            useobjectid=?,            usetype=?        where            ipid=?

可以发现,只有变更的ipPo对象会产生Update sql,其他的并不会产生。不仅大大减少了sql语句的打印,也同时提高了代码效率。


(文件调整为50M,平均计算,每30M可以保存26分钟,节约了5倍的存储空间)

 

总结:

  磨刀不误砍柴工,开发软件有时候往往不是急于动手就有成效,相反,仔细了解需求,研磨技术细节,往往会更能保证开发的质量和效率。不过,技术细节的研磨,往往又来自于实战的经验和错误。所以,好好珍惜每一次error,记录下来,争取不再犯同样的错。

 

 

0 0
原创粉丝点击