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()
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();
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
清空内部缓存
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进行批量导入,绕过缓存机制。
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);
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(); // 清除内部缓存的全部数据,及时释放出占用的内存
}
}
}
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事务处理边界
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” 事务隔离等级。
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. 修改包名
在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. 修改包名
本文依照
HIBERNATE
帮助文档,一些网络书籍及项目经验整理而成,只提供要点和思路,具体做法可以留言探讨,或是找一些更详细更有针对性的资料。
初用
HIBERNATE
的人也许都遇到过性能问题,实现同一功能,用
HIBERNATE
与用
JDBC
性能相差十几倍很正常,如果不及早调整,很可能影响整个项目的进度。
大体上,对于
HIBERNATE
性能调优的主要考虑点如下
:
Ø
数据库设计调整
Ø HQL
优化
Ø API
的正确使用
(
如根据不同的业务类型选用不同的集合及查询
API)
Ø
主配置参数
(
日志,查询缓存,
fetch_size, batch_size
等
)
Ø
映射文件优化
(ID
生成策略,二级缓存,延迟加载,关联优化
)
Ø
一级缓存的管理
Ø
针对二级缓存,还有许多特有的策略
Ø
事务控制策略。
1
、
数据库设计
a)
降低关联的复杂性
b)
尽量不使用联合主键
c) ID
的生成机制,不同的数据库所提供的机制并不完全一样
d)
适当的冗余数据,不过分追求高范式
2
、
HQL
优化
HQL
如果抛开它同
HIBERNATE
本身一些缓存机制的关联,
HQL
的优化技巧同普通的
SQL
优化技巧一样,可以很容易在网上找到一些经验之谈。
3
、
主配置
a)
查询缓存,同下面讲的缓存不太一样,它是针对
HQL
语句的缓存,即完全一样的语句再次执行时可以利用缓存数据。但是,查询缓存在一个交易系统
(
数据变更频繁,查询条件相同的机率并不大
)
中可能会起反作用
:
它会白白耗费大量的系统资源但却难以派上用场。
b) fetch_size
,同
JDBC
的相关参数作用类似,参数并不是越大越好,而应根据业务特征去设置
c) batch_size
同上。
d)
生产系统中,切记要关掉
SQL
语句打印。
4
、
缓存
a)
数据库级缓存
:
这级缓存是最高效和安全的,但不同的数据库可管理的层次并不一样,比如,在
ORACLE
中,可以在建表时指定将整个表置于缓存当中。
b) SESSION
缓存
:
在一个
HIBERNATE SESSION
有效,这级缓存的可干预性不强,大多于
HIBERNATE
自动管理,但它提供清除缓存的方法,这在大批量增加
/
更新操作是有效的。比如,同时增加十万条记录,按常规方式进行,很可能会发现
OutofMemeroy
的异常,这时可能需要手动清除这一级缓存
:Session.evict
以及
Session.clear
c)
应用缓存
:
在一个
SESSIONFACTORY
中有效,因此也是优化的重中之重,因此,各类策略也考虑的较多,在将数据放入这一级缓存之前,需要考虑一些前提条件
:
i.
数据不会被第三方修改
(
比如,是否有另一个应用也在修改这些数据
?)
ii.
数据不会太大
iii.
数据不会频繁更新
(
否则使用
CACHE
可能适得其反
)
iv.
数据会被频繁查询
v.
数据不是关键数据
(
如涉及钱,安全等方面的问题
)
。
缓存有几种形式,可以在映射文件中配置
:read-only(
只读,适用于很少变更的静态数据
/
历史数据
)
,
nonstrict-read-write
,
read-write(
比较普遍的形式,效率一般
)
,
transactional(JTA
中,且支持的缓存产品较少
)
d)
分布式缓存
:
同
c)
的配置一样,只是缓存产品的选用不同,在目前的
HIBERNATE
中可供选择的不多,
oscache, jboss cache
,目前的大多数项目,对它们的用于集群的使用
(
特别是关键交易系统
)
都持保守态度。在集群环境中,只利用数据库级的缓存是最安全的。
5
、
延迟加载
a)
实体延迟加载
:
通过使用动态代理实现
b)
集合延迟加载
:
通过实现自有的
SET/LIST
,
HIBERNATE
提供了这方面的支持
c)
属性延迟加载
:
6
、
方法选用
a)
完成同样一件事,
HIBERNATE
提供了可供选择的一些方式,但具体使用什么方式,可能用性能
/
代码都会有影响。显示,一次返回十万条记录
(List/Set/Bag/Map
等
)
进行处理,很可能导致内存不够的问题,而如果用基于游标
(ScrollableResults)
或
Iterator
的结果集,则不存在这样的问题。
b) Session
的
load/get
方法,前者会使用二级缓存,而后者则不使用。
c) Query
和
list/iterator
,如果去仔细研究一下它们,你可能会发现很多有意思的情况,二者主要区别
(
如果使用了
Spring
,在
HibernateTemplate
中对应
find,iterator
方法
):
i. list
只能利用查询缓存
(
但在交易系统中查询缓存作用不大
)
,无法利用二级缓存中的单个实体,但
list
查出的对象会写入二级缓存,但它一般只生成较少的执行
SQL
语句,很多情况就是一条
(
无关联
)
。
ii. iterator
则可以利用二级缓存,对于一条查询语句,它会先从数据库中找出所有符合条件的记录的
ID
,再通过
ID
去缓存找,对于缓存中没有的记录,再构造语句从数据库中查出,因此很容易知道,如果缓存中没有任何符合条件的记录,使用
iterator
会产生
N+1
条
SQL
语句
(N
为符合条件的记录数
)
iii.
通过
iterator
,配合缓存管理
API
,在海量数据查询中可以很好的解决内存问题,如
:
while(it.hasNext()){
YouObject object = (YouObject)it.next();
session.evict(youObject);
sessionFactory.evice(YouObject.class, youObject.getId());
}
如果用
list
方法,很可能就出
OutofMemory
错误了。
iv.
通过上面的说明,我想你应该知道如何去使用这两个方法了。
7
、
集合的选用
在
HIBERNATE 3.1
文档的
“19.5. Understanding Collection performance”
中有详细的说明。
8
、
事务控制
事务方面对性能有影响的主要包括
:
事务方式的选用,事务隔离级别以及锁的选用
a)
事务方式选用
:
如果不涉及多个事务管理器事务的话,不需要使用
JTA
,只有
JDBC
的事务控制就可以。
b)
事务隔离级别
:
参见标准的
SQL
事务隔离级别
c)
锁的选用
:
悲观锁
(
一般由具体的事务管理器实现
)
,对于长事务效率低,但安全。乐观锁
(
一般在应用级别实现
)
,如在
HIBERNATE
中可以定义
VERSION
字段,显然,如果有多个应用操作数据,且这些应用不是用同一种乐观锁机制,则乐观锁会失效。因此,针对不同的数据应有不同的策略,同前面许多情况一样,很多时候我们是在效率与安全
/
准确性上找一个平衡点,无论如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解。
9
、
批量操作
即使是使用
JDBC
,在进行大批数据更新时,
BATCH
与不使用
BATCH
有效率上也有很大的差别。我们可以通过设置
batch_size
来让其支持批量操作。
举个例子,要批量删除某表中的对象,如
“delete Account”
,打出来的语句,会发现
HIBERNATE
找出了所有
ACCOUNT
的
ID
,再进行删除,这主要是为了维护二级缓存,这样效率肯定高不了,在后续的版本中增加了
bulk delete/update
,但这也无法解决缓存的维护问题。也就是说,由于有了二级缓存的维护问题,
HIBERNATE
的批量操作效率并不尽如人意
!
从前面许多要点可以看出,很多时候我们是在效率与安全
/
准确性上找一个平衡点,无论如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解,一般的,优化方案应在架构设计期就基本确定,否则可能导致没必要的返工,致使项目延期,而作为架构师和项目经理,还要面对开发人员可能的抱怨,必竟,我们对用户需求更改的控制力不大,但技术
/
架构风险是应该在初期意识到并制定好相关的对策。
还有一点要注意,应用层的缓存只是锦上添花,永远不要把它当救命稻草,应用的根基
(
数据库设计,算法,高效的操作语句,恰当
API
的选择等
)
才是最重要的。