Hibernate技术
不懂技术的人或者技术新手往往容易被“框架”二字所唬住,所谓框架是前人对相关问题处理方案的总结,将对某类问题最有价值的解决方式汇集在一起,形成框架。其它人使用时,仅仅只需要按照框架缔结者设定的规则以及调用的API,来完成对框架的使用。
学习hibernate,学习的主要是hibernate的使用规则,理解这个框架的思想。
1、Configuration、SessionFactory、Session三个类一个都不能少,通通都需要了解。所谓了解,其实是夸大了,不少人仅仅只是使用这三个类最简单的创建过程代码,但这已经足够应付绝大多数场景了。无论你使用的时hibernate.properties,还是hibernate.cfg.xml,抑或者你自定义了一个配置文件,确保自己配置的正确性。
2、必须学习hbm.xml文件的编写规则。新手可以依靠一些自动生成工具来完成对hbm.xml以及java文件的生成,但上手便这样,不利于学习。工具虽然方便,但是掩盖了生成时所应该知道的基本原理。
3、hibernate的三种查询方式:HQL、Criteria、Native SQL。每种技术有其应用的优势场景,技术不分优劣,只有最适合当前场景使用的。HQL是使用最频繁的,Criteria是完全OO的,当你需要使用特定数据库的特性时,Native SQL是首选。
4、关联关系。数据库中的多表关联本是平常事,但这个问题又恰恰是最容易让人晕头转向的地方。hibernate处理关联关系的精髓内容在hbm.xml中,学习时(1)注意关联行为的主被动方(2)弄清关联的对象所依据的字段。
5、优化。Hibernate在封装现有JDBC操作的同时,对数据库操作进行了默认的一些优化。而通过延迟加载与批量操作等相关参数设置,我们可以进一步对数据库操作的性能进行优化。
Hibernate学习总结
1 Hibernate概述
Hibernate是一个持久层框架,用来负责实现对象和关系型数据库的转换。2003年Hibernate 2发布,2005年Hibernate 3发布。现在已经成为最为流行的ORM(Object/Relational Mapper)框架。
2 Hibernate内容
2.1 Hibernate的三种状态
A. Transient(临时对象)
new出来的对象,此状态下的对象还没有与数据库的记录对应上。或者通过session.delete(obj)操作的obj对象将从Detached状态改变为Transient状态
B. Persistent(持久对象)
--Transient状态的对象使用Session的save()方法保存到数据库后,对象成为persistent状态,例:
DomesticCat fritz = new DomesticCat();
fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) session.save(fritz);
--使用Hibernate从数据库得到数据并封装为对象(例如使用get()、load()),则该对象为Persistent状态;
>>get()
User user = (User) session.get(User.class, new Long(userID));
>>load()
User user = (User) session.load(User.class, new Long(userID));
get()和load()的区别在于:当要查找的对象数据不存在时,load()方法就是直接抛出异常,而get()方法则返回null值
--Detached 状态对象重新和session关联后(通过update或lock方法)变成Persistent 状态,例:
>>update()
//user是session1关闭后留下的未关联对象
user.setPassword("secret");
Session session2 = sessionFactory.openSession();
Transaction tx = session2.beginTransaction();
session2.update(user); // 此时user从Detached状态转为Persistent状态(session和user关联)
user.setUsername("jonny");
tx.commit();
session2.close();
这种方式,关联前后做修改都不打紧,关联前后做的修改都会被更新到数据库;
比如关联前你修改了password,关联后修改了username,事务提交时执行的update语句会把password、username都更新
>>lock()
Session session2 = sessions.openSession();
Transaction tx = session2 .beginTransaction();
session2 .lock(user, LockMode.NONE);
user.setPassword("secret");
user.setLoginName("jonny");
tx.commit();
session2 .close();
这种方式,关联前后是否做修改很重要,关联前做的修改不会被更新到数据库,
比如关联前你修改了password,关联后修改了loginname,事务提交时执行的update语句只会把loginname更新到数据库
所以,确信未关联对象没有做过更改才能使用lock()
Ps: 如果将Session实例关闭,则Persistent状态的对象会成为Detached状态。
C. Detached(未关联对象)
Detached状态的对象,与数据库中的具体数据对应,但脱离Session实例的管理,例如:
在使用load()、get()方法查询到数据并封装为对象之后,将Session实例关闭,则对象由Persistent状态变为Detached状态。
Detached状态的对象之任何属性变动,不会对数据库中的数据造成任何的影响。
这种状态的对象相当于cache数据,因为他不和session关联,谁都可以用,任何session都可以用它,用完后再放到cache中。
2.2 VO和PO
将Transient状态和Detached状态的对象统称为VO(Value Object)对象,而将Persistent状态的对象称为PO(Persistence Object)对象。
我认为,可以简单的理解为当前和session关联的对象就是PO对象,否则为VO对象
Ps: 应该尽量避免将PO对象传入/传出除持久层外的其他层面(如在view层修改PO)。解决办法是构造一个新的VO,使其具备和PO相同的属性,在系统其他层面使用VO进行数据传输。(但在实际应用中这种方法增加了系统复杂性并且影响性能,所以很多系统设计还是倾向于可以在view层操纵PO的)
2.3 SessionFactory和Session
SessionFactory用来创建session对象,由于SessionFactory本身是单例实现,并且是线程安全的,所以在一个应用中只能创建一个SessionFactory实例。例:
Configuration config = new Configuration.configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Session对象用SessionFactory创建,创建后可以调用其save,get,delete,find方法对持久层数据进行操作。(其中,find()方法在Hibernate3中被取消,用Query或Criteria代替)最后,通过调用session.close()方法关闭session。
2.4 缓存
缓存种类:分为事务级缓存,应用级缓存和分布式缓存。但由于分布式缓存在同步过程中会占用大量带宽,严重影响系统性能,所以不建议使用。
2.4.1
一级缓存
一级缓存是Hibernate的内部缓存,属于事务级缓存。
它在SessionImpl中,以Map的形式实现。当要从Session中取得某个PO时,会判断此PO的id和ClassName在Map中是否已经存在,如果已经存在,则直接从缓存中取出。
有两种手动干预内部缓存的方法:
a. Session.evict
将某个特定对象从内部缓存中清楚
b. Session.clear
清空内部缓存
当批量插入数据时,会引发内存溢出,这就是由于内部缓存造成的。例如:
For(int i=0; i<1000000; i++){
For(int j=0; j<1000000; j++){
User user = new User();
user.setUserName(“gaosong”);
user.setPassword(“
123”
);
session.save(user);
}
}
在每次循环时,都会有一个新的对象被纳入内部缓存中,所以大批量的插入数据会导致内存溢出。解决办法有两种:a 定量清除内部缓存 b 使用JDBC进行批量导入,绕过缓存机制。
a 定量清除内部缓存
For(int i=0; i<1000000; i++){
For(int j=0; j<1000000; j++){
User user = new User();
user.setUserName(“gaosong”);
user.setPassword(“
123”
);
session.save(user);
if(j%50 == 0){
session.flush();
session.clear(); // 清除内部缓存的全部数据,及时释放出占用的内存
}
}
}
b 使用JDBC进行批量导入,绕过缓存机制
Transaction tx=session.beginTransaction(); //使用Hibernate事务处理边界Connection conn=session.connection();
PrepareStatement stmt=conn.prepareStatement(“insert into T_STUDENT(name) values(?)”);
for(int j=0;j++;j<200){
for(int i=0;i++;j<50) {
stmt.setString(1,”feifei”);
}
}
stmt.executeUpdate();
tx.commit(); //使用 Hibernate事务处理边界
ps:注意,这里使用Hibernate的事务处理机制处理Connection
2.4.2
二级缓存
2.4.2.1 概述
Hibernate的二级缓存涵盖了应用级缓存和分布式缓存领域。Hibernate默认使用EHCache作为二级缓存的实现。(二级缓存由第三方实现)
缓存后,可能出现问题主要有:
1. 脏读:一个事务读取了另一个并行事务未提交的数据。
2. 不可重复读:一个事务再次读取之前的数据时,得到的数据不一致,被另一个已提交的事务修改。
3. 幻想读:一个事务重新执行一个查询,返回的记录中包含了因为其他最近提交的事务而产生的新记录。
对应以上问题,Hibernate提供了四种内置的缓存同步策略:
1. read-only:只读。对于不会发生改变的数据,可使用只读型缓存。
2. nonstrict-read-write:如果程序对并发访问下的数据同步要求不是非常严格,且数据更新操作频率较低,可以采用本选项,获得较好的性能。
3. read-write:严格可读写缓存。基于时间戳判定机制,实现了“read committed”事务隔离等级。可用于对数据同步要求严格的情况,但不支持分布式缓存。这也是实际应用中使用最多的同步策略。
4. transactional:事务型缓存,发生异常的时候,缓存也能够回滚,必须运行在JTA事务环境中。实现了“Repeatable read” 事务隔离等级。
Ps: 读写缓存和不严格读写缓存在实现上的区别在于,读写缓存更新缓存的时候会把缓存里面的数据换成一个锁,其他事务如果去取相应的缓存数据,发现被锁住了,然后就直接取数据库查询。
在hibernate2.1的ehcache实现中,如果锁住部分缓存的事务发生了异常,那么缓存会一直被锁住,直到60秒后超时。
不严格读写缓存不锁定缓存中的数据。
2.4.2
.2 锁
为了避免脏数据的出现,Hibernate默认实现了乐观锁机制。原理是为表加一个version字段来控制。例如:version初始值为1,A和B两个事务同时修改表,A和B都读入了version=1。A修改完成后将version加1,version=2。B修改完后也将version加1,当试图提交时发现:当前version已经为2,而提交版本也为2,违反了“提交版本必须大于记录版本”的乐观锁策略,事务被回滚。因此避免了脏数据的出现的可能。
2.4.2.3 Class缓存
Class缓存是一个Map,其中key是ClassName,Value是一个id序列(其中的id就对应DB中的一条记录,也就是对应一个PO)。当调用session.iterate(…)方法时,将通过一条查询语句返回一个id序列,只要序列中的id在Map中存在,则取出Map中此id对应的POJO。
session.iterate(…)方法和session.find(…)方法的区别:session.find(…)方法并不读取ClassCache,它通过查询语句直接查询出结果数据,并将结果数据put进classCache;session.iterate(…)方法返回id序列,根据id读取ClassCache,如果没有命中在去DB中查询出对应数据。
当使用session.find(…)方法时,如果返回大量数据,会将很多POJO放入内存中,导致内存溢出,此时可以用limit方式限定返回数量。
Ps: 由于session.iterate(…)方法不能规避N+1的问题,所以基本不用此方法。
2.4.2.4 查询缓存
Hibernate查询缓存根据HQL生成的SQL作为key,对应一个查询出的id序列,如果两次查询的SQL相同,将命中查询缓存,根据id序列再到ClassCache中load出对应的POJO。但是,如果在两次查询中数据库表有改动(Update/Insert/Delete),则会将查询缓存中的数据清空。所以查询缓存只能应用于两次查询中DB表没有改变,并且是同样的SQL查询的情况。
这里有个问题,就是查询缓存机制可能会遇到N+1的问题。因为当从查询缓存中读出id序列后,将去ClassCache中找,如果此时ClassCache中没有id对应的POJO,则将向数据库中发送查询语句。所以一定要确保ClassCache中数据的生命周期要比QueryCache的长,(比如ClassCache的超时时间一定不能短于QueryCache设置的超时时间)
2.4.2.5 批量操作(Update/Delete)
在Hibernate3.0.5以前的版本和以后的有很大差异,这里分两部分祥解:
1. Hibernate2.x
在Hibernate2.x中,批量操作时是不推荐用Hibernate自己的update/delete方法的,这是因为Hibernate在批量更新/删除时必须维护ClassCache,这就导致出现N+1的情况影响性能,所以不可取。解决方法是使用JDBC的相应方法,绕开缓存。
2. Hibernate3.x
在Hibernate3.x中,加入了“bulk update/delete”的操作,使得可以使用Hibernate本身的批量更新功能。
“bulk update”只是简单的发送一条update语句,并不维护ClassCache。此时就会有脏数据出现的可能。(需要确认)
另一方面,“bulk delete”发送一条delete语句,并且到ClassCache中将delete对应的Class设置为无效,实际是清空了表对应的ClassCache。
2.4.3 session.save
session.save方法的执行顺序:
1. 在一级缓存中寻找POJO,如果命中则认为此对象执行过Insert方法,直接返回。
2. 如果实现了lifecycle接口,则调用onSave方法
3. 如果实现了Validatable接口,则调用validate方法
4. 调用拦截机的Interceptor.onSave方法
5. 构造Insert Sql,并且执行
6. 返回新插入记录的id给POJO
7. 将POJO放入一级缓存中(session.save不会将POJO放入二级缓存,是因为通过save保存的POJO在事务的剩余部分被修改的可能往往很高,频繁更改二级缓存得不偿失)
8. 处理级连关系
3 Hibernate3.2新特性
1. Hibernate Annotation
2. bulk update/delete(批量更新/删除)
3. 和lucene集成
4. 取消Hibernate2中的session.find方法,改为session.createQuery.list方法
5. 修改包名