Hibernate技术

来源:互联网 发布:14总决赛詹姆斯数据 编辑:程序博客网 时间:2024/04/17 05:28
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. 修改包名 
Hibernate程序性能优化的考虑要点
本文依照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-writeread-write(比较普遍的形式,效率一般)transactional(JTA中,且支持的缓存产品较少)
  d) 分布式缓存:c)的配置一样,只是缓存产品的选用不同,在目前的HIBERNATE中可供选择的不多,oscache, jboss cache,目前的大多数项目,对它们的用于集群的使用(特别是关键交易系统)都持保守态度。在集群环境中,只利用数据库级的缓存是最安全的。
  5延迟加载
  a) 实体延迟加载:通过使用动态代理实现
  b) 集合延迟加载:通过实现自有的SET/LISTHIBERNATE提供了这方面的支持
  c) 属性延迟加载:
  6方法选用
  a) 完成同样一件事,HIBERNATE提供了可供选择的一些方式,但具体使用什么方式,可能用性能/代码都会有影响。显示,一次返回十万条记录(List/Set/Bag/Map)进行处理,很可能导致内存不够的问题,而如果用基于游标(ScrollableResults)Iterator的结果集,则不存在这样的问题。
  b) Sessionload/get方法,前者会使用二级缓存,而后者则不使用。
  c) Querylist/iterator,如果去仔细研究一下它们,你可能会发现很多有意思的情况,二者主要区别(如果使用了Spring,在HibernateTemplate中对应find,iterator方法):
  i. list只能利用查询缓存(但在交易系统中查询缓存作用不大),无法利用二级缓存中的单个实体,但list查出的对象会写入二级缓存,但它一般只生成较少的执行SQL语句,很多情况就是一条(无关联)
  ii. iterator则可以利用二级缓存,对于一条查询语句,它会先从数据库中找出所有符合条件的记录的ID,再通过ID去缓存找,对于缓存中没有的记录,再构造语句从数据库中查出,因此很容易知道,如果缓存中没有任何符合条件的记录,使用iterator会产生N+1SQL语句(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找出了所有ACCOUNTID,再进行删除,这主要是为了维护二级缓存,这样效率肯定高不了,在后续的版本中增加了bulk delete/update,但这也无法解决缓存的维护问题。也就是说,由于有了二级缓存的维护问题,HIBERNATE的批量操作效率并不尽如人意!
  从前面许多要点可以看出,很多时候我们是在效率与安全/准确性上找一个平衡点,无论如何,优化都不是一个纯技术的问题,你应该对你的应用和业务特征有足够的了解,一般的,优化方案应在架构设计期就基本确定,否则可能导致没必要的返工,致使项目延期,而作为架构师和项目经理,还要面对开发人员可能的抱怨,必竟,我们对用户需求更改的控制力不大,但技术/架构风险是应该在初期意识到并制定好相关的对策。
  还有一点要注意,应用层的缓存只是锦上添花,永远不要把它当救命稻草,应用的根基(数据库设计,算法,高效的操作语句,恰当API的选择等)才是最重要的。