Hibernate常见的面试问题
Hibernate
1.Hibernate简介?
Hibernate是一个开源的对象关系映射框架,它对JDBC进行了非常轻量级的
对象封装,使得Java程序员可以随心所欲的使用面向对象编程思想来操纵数据库。
2.Hibernate工作原理?及为什么要用?
Hibernate是一个JDO工具。它的工作原理是通过文件把值对象和数据库表之间建立起一个映射关系,这样,我们只需要通过操作这些值对象和Hibernate
提供的一些基本类,就可以达到使用数据库的目的。
1)对JDBC操作提供封装,方便操作简化数据库访问的代码,使用面向对象的方式操作DAO;
2)简化DAO层的代码量ORM从关系型DB到面向对象(java)的转变;
3)在java代码中充斥着大量的sql语句不便于维护,但是ORM映射可以减少此类代码,便于维护;
3.Hibernate的优点和缺点?
⑴Hibernate的优点:
1).Hibernate使用Java反射机制,而不是字节码增强程序来实现透明性;
2).Hibernate的性能非常好,因为它是一个轻量级框架。映射的灵活性很出色;
3).它支持多种关系数据库,从一对一到多对多的各种复杂关系;
(2)Hibernate的缺点:
它限制您所使用的对象模型,如一个持久性类不能映射到多个表,其独有的
界面和可怜的市场份额也让人不安。
4.Hibernate的缓存机制?
Hibernate的缓存包括Session的缓存和SessionFactory的缓存,其中
SessionFactory的缓存又可以分为两类:内置缓存和外置缓存。Session的缓存是内置的,不能被卸载,也被称为Hibernate的第一级缓存。SessionFactory的内置缓存中存放了映射元数据和预定义SQL语句,SessionFactory的内置缓存是只读的,应用程序不能修改缓存中的映射元数据和预定义SQL语句,因此SessionFactory不需要进行内置缓存与映射文件的同步。SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插
件。SessionFactory的外置缓存也被称为Hibernate的第二级缓存。事务范围的缓存是持久化层的第一级缓存;进程范围和集群范围的缓存是持久化层的第二级缓存。Hibernate还为查询结果提供了一个查询缓存,它依赖于二级缓存。
持久化层可以提供多种范围的缓存。如果在事务范围的缓存中没有查到相应的数据,还可以到进程范围或集群范围的缓存内查询,如果还是没有查到,那么只有到数据库中查询。事务范围的缓存是持久化层的第一级缓存,通常它是必需的;进程范围或集群范围的缓存是持久化层的第二级缓存,通常是可选的。
什么样的数据适合放到二级缓存中?
1)很少被修改的数据;
2)不是很重要的数据;
3)不会被并发访问的数据;
4)参考数据。
不适合放到二级缓存中的数据?
1)经常被修改的数据;
2)绝对重要的数据,例如:财务数据;
3)共享数据。
其它:
·处于一级缓存中的对象永远不会过期,除非应用程序显式清空缓存或者清除
特定的对象必须提供数据
过期策略;
·只要应用程序通过Session接口执行保存、更新、删除、加载和查询数据库数据的操作,Hibernate就会启用第一级缓存,把数据库中的数据以对象的形式拷贝到缓存中,对于批量更新和批量删除操作,如果不希望启用第一级缓存,可以绕过Hibernate API,直接通过JDBC API来执行指操作。 用户可以在单个类或类的单个集合的粒度上配置第二级缓存。如果类的实例被经常读但很少被修
改,就可以考虑使用第二级缓存。只有为某个类或集合配置了第二级缓存,Hibernate在运行时才会把它的实例加入到第二级缓存中。
我的理解:
Hibernate中的一级缓存是出于Hibernate自身的需要而进行的一次数据缓存,可以在Hibernate中的一般操作中起到数据缓存的作用。而通常所说的缓存
技术,实际上对应的就是Hibernate中的二级缓存,只不过在Hibernate中,已经有了Session级别的一级缓存,所以称之为二级缓存。
缓存的管理:
一级缓存的管理
Session为应用程序提供了两个管理缓存的方法:
1>.Evict(Object obj):从缓存中清除参数指定的持久化对象;
2>.Clear():清空缓存中所有持久化对象。
二级缓存的管理:
Hibernate的二级缓存策略的一般过程如下:
1) 条件查询的时候,总是发出一条select * from table_name where …. (选择
所有字段)这样的SQL语句查询数据库,一次获得所有的数据对象。
2) 把获得的所有数据对象根据ID放入到第二级缓存中。
3) 当Hibernate根据ID访问数据对象的时候,首先从Session一级缓存中查;
查不到,如果配置了二级缓存,那么从二级缓存中查;查不到,再查询数据库,把结果按照ID放入到缓存。
4) 删除、更新、增加数据的时候,同时更新缓存。此外,Hibernate的二级缓存策略,只是针对于ID查询的缓存策略,对于条件查询则毫无作用。为此,
Hibernate提供了针对条件查询的Query Cache(查询缓存)。
常用的缓存插件:
Hibernate的二级缓存是一个插件,下面是几种常用的缓存插件:
· EhCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬
盘,对Hibernate的查询缓存提供了支持。
·OSCache:可作为进程范围的缓存,存放数据的物理介质可以是内存或硬
盘,提供了丰富的缓存数据过期策略,对Hibernate的查询缓存提供了支持。
·SwarmCache:可作为群集范围内的缓存,但不支持Hibernate的查询缓存。
·JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对Hibernate的查询缓存提供了支持。
5.Hibernate是如何实现延迟加载的?
Hibernate对象关系映射提供延迟的与非延迟的对象初始化。
非延迟加载在读取一个对象的时候会将与这个对象所有相关的其他对象一起读取出来。这有时会导致成百select语句在读取对象的时候执行。这个问题有时出现在使用双向关系的时候,经常会导致整个数据库都在初始化的阶段被读
出来了。
一个明显的解决方法是使用Hibernate提供的延迟加载机制。这种初始化策略只在一个对象调用它的一对多或多对多关系时才将关系对象读取出来。这个过程对开发者来说是透明的,而且只进行了很少的数据库操作请求,因此会得到比
较明显的性能提升。
这项技术的一个缺陷是延迟加载技术要求一个Hibernate会话要在对象使用的时候一直开着。这会成为通过使用DAO模式将持久层抽象出来时的一个主要问题。为了将持久化机制完全地抽象出来,所有的数据库逻辑,包括打开或关闭会话,都不能在应用层出现。最常见的是,一些实现了简单接口的DAO实现类将数据库逻辑完全封装起来了。一种快速但是笨拙的解决方法是放弃DAO模式,将数据库连接逻辑加到应用层中来。这可能对一些小的应用程序有效,但是在大的系
统中,这是一个严重的设计缺陷,妨碍了系统的可扩展性。
幸运的是,Spring框架为Hibernate延迟加载与DAO模式的整合提供了一种方便的解决方法。对那些不熟悉Spring与Hibernate集成使用的人,我不会在这里讨论过多的细节,但是我建议你去了解Hibernate与Spring集成的数据访问。以一个Web应用为例,Spring提供了OpenSessionInViewFilter和OpenSessionInViewInterceptor。我们可以随意选择一个类来实现相同的功能。两种方法唯一的不同就在于interceptor在Spring容器中运行并被配置在web应用的上下文中,而Filter在Spring之前运行并被配置在web.xml中。不管用哪个,他们都在请求将当前会话与当前(数据库)线程绑定时打开Hibernate会话。一旦已绑定到线程,这个打开了的Hibernate会话可以在DAO实现类中透明地使用。这个会话会为延迟加载数据库中值对象的视图保持打开状态。一旦这个逻辑视图完成了,Hibernate会话会在Filter的doFilter方法或者Interceptor的
postHandle方法中被关闭。
6.Hibernate有几种查询方式?
· Session的get()和load()查询;
· HQL查询;
· Query查询;
· Criteria查询;
· 本地SQL语句查询(Native SQl)。
7.Hibernate的性能优化?
⑴在运行的情况下使用最新版本的Hibernate发行版,如hibernate3中经过优
化的批量处理机制,代理机制、属性的延迟加载支持等;
⑵指定合理的缓存策略,通过系统压力测试得到最佳的缓存性能;
⑶采用合理的Session管理机制,避免无谓的数据库开销和临时对象的反复创建;
⑷尽量使用延迟加载特性,以避免系统资源的无谓消耗;
⑸设定合理的批处理参数(batch_size);
⑹如果可能,使用UUID作为主键生成器;
⑺如果可能,使用基于Version的乐观锁策略替代悲观锁;
⑻开发过程中,打开Hibernate的SQL日志文件(hibernate.show_sql),通过观
察Hibernate生成的SQL
语句进一步了解其实现原理,从而指定更好的实现策略;
⑼数据库本身的优化也起着至关重要的作用,合理的索引、缓存和数据分区策略
都会对持久层性能带来客观提升。
8.Hibernate中的update()和saveOrUpdate()的区别?
Hibernate中的对象状态有3种,分别为:
· 未被持久化的值对象(Value Object,VO);
· 已经被持久化的持久化对象(Persistent Object,PO);
· 曾经被持久化过,但现在已经和Session分离,以VO的身份在运行。
首先,我们来说说何时用update();
· 如果你的PO不需要跨Session,即打开一个Session之后,对其进行操作,之后关闭,再也不会用到了,此时不需要用update()方法,此时Hibernate会自动检测到PO已经被修改过,会向数据库发送一条Update语句;如下代码:
Foo foo=sess.load(Foo.class,id);;
foo.setXXX(xxx);
sess.commit();
·如果你的PO需要跨Session操作,在第一个Session关闭后,你还想把已经于第一个Session分离的PO当做VO来用,并将修改更新到数据库中,那么
此时就需要Update()方法;也就是说,当数据是持久化对象时,不需要使用update方法,当数据不是持久化状态的时候,必须使用update方法,告诉数据库要更新数据;然后,我们谈谈saveOrUpdate();
如果业务层传过来的是一个已经持久化过的PO对象,那么Hibernate就会根据这个方法更新该对象,如果传递过来的是一个VO对象,那么Hibernate就
会save这个对象。
9.Hibernate中的load()和get()的区别?
最重要的区别:如果未能发现符合条件的记录,Hibernate get方法返回null,而load方法会抛出一个ObjectNotFoundException。
1. 对于Hibernate get方法,Hibernate会确认一下该id对应的数据是否存在,首先在session缓存中查找,然后在二级缓存中查找,还没有就查询数据库,数据库中没有就返回null。
2. Hibernate load方法加载实体对象的时候,根据映射文件上类级别的lazy属性的配置(默认为true),分情况讨论:
(1)若为true,则首先在Session缓存中查找,看看该id对应的对象是否存在,不存在则使用延迟加载,返回实体的代理类对象(该代理类为 实体类的子
类,由CGLIB动态生成)。等到具体使用该对象(除获
取OID以外)的时候,再查询二级缓存和数据库,若仍没发现符合条件的记录,则会抛出一个ObjectNotFoundException。
(2)若为false,就跟Hibernate get方法查找顺序一样,只是最终若没发现
符合条件的记录,则会抛出一个ObjectNotFoundException。
Hibernate常用的接口和类
Configuration接口
|- Configuration接口的作用是对Hibernate进行配置,以及对它进行启动。在Hibernate的启动过程中,Configuration类的实例首先定位映射文档的位置,读取这些配置,然后创建一个SessionFactory对象。
SessionFactory 接口
|- 这里用到了一个设计模式――工厂模式,用户程序从工厂类SessionFactory中取得Session的实例.SessionFactory并不是轻量级的,一个项目通常只需要一个 SessionFactory就够了,但是当你的项目要操作多个数据库时,那你必须为每个数据库指定一个SessionFactory。
SessionFactory在Hibernate中实际起到了一个缓冲区的作用,它缓冲了Hibernate自动生成的SQL语句和一些其它的映射数据,还缓冲了一些将来有可能重复利用的数据。
Session接口
|- Session接口对于Hibernate 开发人员来说是一个最重要的接口。然而在Hibernate中,实例化的Session是一个轻量级的类,创建和销毁它都不会占用很多资源。这在实际项目中确实很重要,因为在客户程序中,可能会不断地创建以及销毁Session对象,如果Session的开销太大,会给系统带来不良影响。但值得注意的是 Session对象是非线程安全的,因此在你的设计中,最好是一个线程只创建一个Session对象。
Hibernate使用Session来存取,它是持久化管理器接口,代表与数据库之间的一次操作。然而为了操作持久化数据,首先要获得SessionFactory对象,SessionFactory负责一个数据库,是Session的工厂,对于一个XML配置文件,SessionFactory对象Configuration对象负责创建。这里的net.sf.hibernate.cfg.Configuration的一个实例代表了应用程序中所有的Java类到关系数据库的映射的集合,这些映射是从一些XML映射文件中编译得到的。可以得到一个 Configuration的实例,直接实例化即可,当所有的映射都被Configuration解析之后,应用程序为了得到Session实例,必须先得到它的工厂,这些工厂应该是应用程序的所有线程所共享的。当然Hibernate并不禁止程序实例化多个SessionFactory,这在使用不止一个数据库时候就很有用。
Transaction 接口
|- Transaction接口是一个可选的API,你可以选择不使用这个接口,取而代之的是Hibernate的设计者自己写的底层事务处理代码。 Transaction接口是对实际事务实现的一个抽象,这些实现包括JDBC的事务、JTA中的UserTransaction、甚至可以是CORBA 事务。之所以这样设计是能让开发者能够使用一个统一事务的操作界面,使得自己的项目可以在不同的环境和容器之间方便地移值。
Query和Criteria接口
|- Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。
Criteria接口与Query接口非常类似,它允许你创建并执行面向对象的标准化查询。值得注意的是Query接口也是轻量级的,它不能在Session之外使用。
Callback 接口
当一些有用的事件发生时――例如持久对象的载入、存储、删除时,Callback接口会通知Hibernate去接收一个通知消息。一般而言,Callback接口在用户程序中并不是必须的,但你要在你的项目中创建审计日志时,你可能会用到它。
Hibernate管理Session对象的方式有哪些?
尽管让程序自主管理Session对象的生命周期也是可行的,但是在实际Java
应用中,把管理Session对象的生命周期交给Hibernate,可以简化 Java应用的程序代码和软件架构。
Hibernate 自身提供了 3 种管理Session对象生命周期的方式:
(1)Session对象的生命周期与本地线程绑定。
(2)Session对象的生命周期与 JTA事务绑定。
(3)Hibernate委托程序管理 Session对象的生命周期。
它包含了业务逻辑代码,创建 SessionFactory 对象的代码、管理 Session 对
象的代码,以及执行数据库事务的代码。对于复杂的软件应用,需要细分 BusinessService类的功能,建立精粒度的对象模型。精粒度的对象模型把功能逐步分解为多个模块,然后分配给多个类来协作完成。精粒度的对象模型可以减少重复代码,而且能提高每个类的独立性,有利于软件的维护和可重用。
本节将进一步细分 BusinessService类的功能,并且由多个类来分担 BusinessService类的功能:
● HibernateUtil类:实用类,负责创建一个应用范围内的 SessionFactory
对象,它的 getSessionFactory()静态方法返回这个 SessionFactory 对象。它的getCurrentSession()静态方法返回当前的 Session对象。
● Monkey类:持久化类,代表猴子。
● MonkeyDAo. 类:封装了通过 Hibernate API来访问数据库,进行查询及
更新 Monkey对象的代码。不负责声明数据库事务。DAO(DataAccess Object)
表示数据访问对象。
● BusinessService 类:负责处理投票业务,通过 MonkeyDAO 类来查询及
更新 Monkey对象,数据库事务的声明由 BusinessService类来完成。
SessionFactory对象是重量级对象,在一个Java应用中,对于一个数据库存储
源,只需要创建一个代表该存储源的 SessionFactory对象,它被整个Java应
用共享。
MonkeyDAO 类封装了与 Monkey 对象有关的访问数据库的代码,参见例程
16-2。MonkeyDAO类通过 HibernateUtil类的 getCurrentSession()方法来得到
当前的Session对象。MonkeyDAO类的 getById()及 update()方法既不用管理 Session,也不用声明事务。
package my pack;
public c1ass MonkeyDAO (
pub1ic Monkey get By Id (long id) (
return (Mon key ) Hibernate Util . get CurrentS ession ( )
. get (Monkey . class , new Long (id) );
public void update (Monkey monkey) (
HibernateUtil. getCurrentSession()·saveOrUpdate(monkey)i)
Hibernate 到底如何管理 Session对 Java应用是透明的。无论是那种管理方
式,MonkeyDAO 类都不必自己创建 Session 对象,只需调用 HibernateUtil 类的getCurrentSession()方法,就能获得当前的 Session对象。而HibernateUtil类的getCurrentSession()方法实际上是调用 SessionFactory对象的 getCurrentSession()方法,来获得当前的 Session对象。由此可见,Hibernate 内部封装了管理 Session对象的生命周期的实现细节。当 Java 应用改变 Hibernate 的 Session 管理方式时,无须修改 Monkey 类、MonkeyDAO 类和 HibernateUtil 类的源代码,只需修改 Hibernate 的配置文件及
BusinessService 类就行了。
在 Hibernate 的配置文件中,hibernate.current-sessioncontext-class属性用于指定 Session管理方式,可选值包括:
●thread:Session对象的生命周期与本地线程绑定。
●jta:Session对象的生命周期与 JTA 事务绑定。
●managed:Hibernate委托程序来管理 Session对象的生命周期。
Hibernate中的Cache管理
一级Cache:
Session实现了第一级Hibernate Cache,它属于事务级数据缓冲。一旦事务结束,这个Cache也随之失效。一个Session的生命周期对应一个数据库事务或一个程序事务。
Session-cache保证了一个Session中两次请求同一个对象时,取得的对象是同一个JAVA实例,有时它可以避免不必要的数据冲突。
另外,它还能为另一些重要的性能提供保证:
1:在对一个对象进行自我循环引用时,不至于产生堆栈溢出。
2:当数据库事务结束时,对于同一个数据库行,不会产生数据冲突,因为对于数据库中的一行,最多只有一个对象来表示它。
3:一个事务中可能会有很多个处理单元,在每一个处理单元中做的操作都会立即被另外的处理单元得知。
我们不用刻意去打开Session-cache,它总是被打开并且不能被关闭。当使用save(),update()或saveOrUpdate()来保存数据更改,或通过load(),find(),list()等方法来得到对象时,对象就会被加入到Session-cache.
如果要同步很多数据对象,就需要有效地管理Cache,可以用Session的evict()方法从一级Cache中移除对象。
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
for(int i = 0 ; i <100000 ; i++){
Student stu = new Student();
session.save(stu);
}
tx.commit();
session.close();
在保存50000个或更多对象时,程序可能会抛出OutOfMemoryException异常,因为 Hibernate Cache在一级缓存了新加入的所有对象。内存溢出。要解决这全问题就需要把JDBC批处理数量设置为一个合理的数值(一般是10~20)。
在 Hibernate Cache的配置文件中可以加入以下属性:
<property name="hibernate.jdbc.batch_size"> 20 </property>
然后我们在程序中一定时刻就提交并更新Session的Hibernate Cache:
Session session = HibernateUtil.currentSession();
Transaction tx = session.beginTransaction();
for(int i = 0 ; i <100000 ; i++){
Student stu = new Student();
session.save(stu);
if(i%20 == 0) { //每保存完20个对象后,进行如下操作
session.flush();//这个会提交更新
session.clear();//清除Cache,释放内存
}
}
二级Cache
二级Cache是SessionFactory范围内的缓存,所有的Session共享同一个二级Cache.在二级Cache中保存持久性实例的散装形式的数据。二级Cache的内部如何实现并不重要,重要的是采用哪种正确的缓存策略,以及采用哪个Cache提供器。持久化不同的数据需要不同的 Cache策略,比如一些因素将影响到Cache策略的选择:数据的读/写比例,数据表是否能被其他的应用程序扬访问等。对于一些读/写比例高的数据可以打开它的缓存,允许这些数据进入二级缓存容器有利于系统性能的优化;而对于能被其它应用程序访问的数据对象,最好将此对象的二级Cache选项关闭。
设置Hibernate Cache的二级需要分两步进行:首先确认使用什么数据并发策略,然后配置缓存过期时间并设置Hibernate Cache提供器。
有4种内置的Hibernate数据并发冲突策略,代表数据库隔离级别:
1:事务(Transaction)仅在受管理的环境中可用。它保证可重读的事务隔离级别,可以对读/写比例高,很少更新的数据采用该策略。
2:读写(read-write)使用时间戳机制维护读写提交事务隔离级别。可以对读/写比例高,很少更新的数据采用该策略。
3:非严格读写(notstrict-read-write)不保证Cache和数据库之间的数据库的一致性。使用此策略时,应该设置足够的缓存过期时间,否则可能从缓存中读出脏数据。当一些数据极少改变,并且当这些数据和数据库有一部份不量影响不大时,可以使用此策略。
4:只读(read-only)当确保数据永不改变时,可以使用此策略。
我们确定了Hibernate Cache策略后,就要挑选一个高效的Cache提供器,它将作为插件被Hibernate调用。Hibernate允许使用下述几种缓存插件:EhCache:可以在JVM中作为一个简单进程范围内的缓存,它可以把缓存的数据放入内存或磁盘,并支持Hibernate中可选用的查询缓存。
OpenSymphony OSCache:和EhCache相似,并且提供了丰富的缓存过期策略。
◆SwarmCache:可作为集群范围的缓存,但不支持查询缓存。
◆JBossCache:可作为集群范围的缓冲,但不支持查询缓存。
在Hibernate中使用EhCache
EhCache是一个纯JAVA程序,可以在Hibernate中作为一个插件引入。在Hibernate中使用EhCache需要在Hibernate的配置文件中设置
<propery name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider
</property>
<ehcache>
<diskStore path="c:““cache"/> //设置cache.data文件存放位置
<defaultCache
maxElementsInMemory="10000" //缓存中允许创建的最大对象数
eternal="false" //缓存中对象是否为永久的
timeToIdleSeconds="120"//缓存数据钝化时间(即对象在它过期前的空闲时间)
timeToLiveSeconds="120"//缓存数据生存时间(即对象在它过期前的生存时间)
overflowToDisk="true"
/>
<cache name="Student" //用户自定义的Cache配置
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
</ehcache>
此外我们还需要在持久化类的映射文件中进行配置。例如,Group(班级)和Student(学生)是一对多的关系,它们对应的数据表分别是 t_group和t_student.现在要把Student类的数据进行二级缓存,这需要在二个映射文件中都对二级缓存进行配置。
在Group.hbm.xml中如下,在其<set></set>中添加
Hibernate延迟加载机制
延迟加载:
延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。在Hibernate中提供了对实体对象的延迟加载以及对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。下面我们就分别介绍这些种类的延迟加载的细节。
A、实体对象的延迟加载: 如果想对实体对象使用延迟加载,必须要在实体的映射配置文件中进行相应的配置,如下所示:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user” lazy=”true”>
……
</class>
</hibernate-mapping>
通过将class的lazy属性设置为true,来开启实体的延迟加载特性。如果我们运行下面的代码:
User user=(User)session.load(User.class,”1”);(1)
System.out.println(user.getName());(2)
当运行到(1)处时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到(2)处时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的 getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id=’1’;来查询数据,并构造目标对象,并且将它赋值到CGLIB$CALBACK_0.target属性中。
这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。
B、集合类型的延迟加载:在 Hibernate的延迟加载机制中,针对集合类型的应用,意义是最为重大的,因为这有可能使性能得到大幅度的提高,为此Hibernate进行了大量的努力,其中包括对JDK Collection的独立实现,我们在一对多关联中,定义的用来容纳关联对象的Set集合,并不是java.util.Set类型或其子类型,而是 net.sf.hibernate.collection.Set类型,通过使用自定义集合类的实现,Hibernate实现了集合类型的延迟加载。为了对集合类型使用延迟加载,我们必须如下配置我们的实体类的关于关联的部分:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
</class>
</hibernate-mapping>
通过将<set>元素的lazy属性设置为true来开启集合类型的延迟加载特性。我们看下面的代码:
User user=(User)session.load(User.class,”1”);
Collection addset=user.getAddresses(); (1)
Iterator it=addset.iterator(); (2)
while(it.hasNext()){
Address address=(Address)it.next();
System.out.println(address.getAddress());
}
当程序执行到(1)处时,这时并不会发起对关联数据的查询来加载关联数据,只有运行到(2)处时,真正的数据读取操作才会开始,这时Hibernate会根据缓存中符合条件的数据索引,来查找符合条件的实体对象。
这里我们引入了一个全新的概念——数据索引,下面我们首先将接一下什么是数据索引。在 Hibernate中对集合类型进行缓存时,是分两部分进行缓存的,首先缓存集合中所有实体的id列表,然后缓存实体对象,这些实体对象的id列表,就是所谓的数据索引。当查找数据索引时,如果没有找到对应的数据索引,这时就会一条select SQL的执行,获得符合条件的数据,并构造实体对象集合和数据索引,然后返回实体对象的集合,并且将实体对象和数据索引纳入Hibernate的缓存之中。另一方面,如果找到对应的数据索引,则从数据索引中取出 id列表,然后根据id在缓存中查找对应的实体,如果找到就从缓存中返回,如果没有找到,在发起select SQL查询。在这里我们看出了另外一个问题,这个问题可能会对性能产生影响,这就是集合类型的缓存策略。如果我们如下配置集合类型:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<cache usage=”read-only”/>
<key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
</class>
</hibernate-mapping>
这里我们应用了<cache usage=”read-only”/>配置,如果采用这种策略来配置集合类型,Hibernate将只会对数据索引进行缓存,而不会对集合中的实体对象进行缓存。如上配置我们运行下面的代码:
User user=(User)session.load(User.class,”1”);
Collection addset=user.getAddresses();
Iterator it=addset.iterator();
while(it.hasNext()){
Address address=(Address)it.next();
System.out.println(address.getAddress());
}
System.out.println(“Second query……”);
User user2=(User)session.load(User.class,”1”);
Collection it2=user2.getAddresses();
while(it2.hasNext()){
Address address2=(Address)it2.next();
System.out.println(address2.getAddress());
}
运行这段代码,会得到类似下面的输出:
Select * from user where id=’1’;
Select * from address where user_id=’1’;
Tianjin
Dalian
Second query……
Select * from address where id=’1’;
Select * from address where id=’2’;
Tianjin
Dalian
我们看到,当第二次执行查询时,执行了两条对address表的查询操作,为什么会这样?这是因为当第一次加载实体后,根据集合类型缓存策略的配置,只对集合数据索引进行了缓存,而并没有对集合中的实体对象进行缓存,所以在第二次再次加载实体时,Hibernate找到了对应实体的数据索引,但是根据数据索引,却无法在缓存中找到对应的实体,所以Hibernate根据找到的数据索引发起了两条select SQL的查询操作,这里造成了对性能的浪费,怎样才能避免这种情况呢?我们必须对集合类型中的实体也指定缓存策略,所以我们要如下对集合类型进行配置:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<cache usage=”read-write”/>
<key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
</class>
</hibernate-mapping>
此时Hibernate会对集合类型中的实体也进行缓存,如果根据这个配置再次运行上面的代码,将会得到类似如下的输出:
Select * from user where id=’1’;
Select * from address where user_id=’1’;
Tianjin
Dalian
Second query……
Tianjin
Dalian
这时将不会再有根据数据索引进行查询的SQL语句,因为此时可以直接从缓存中获得集合类型中存放的实体对象。
C、属性延迟加载:
在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在前面我们讲大数据对象读取时,在User对象中有一个resume字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。在Hibernate2中,我们只有通过我们前面讲过的面性能的粒度细分,来分解User类,来解决这个问题(请参照那一节的论述),但是在Hibernate3中,我们可以通过属性延迟加载机制,来使我们获得只有当我们真正需要操作这个字段时,才去读取这个字段数据的能力,为此我们必须如下配置我们的实体类:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user”>
……
<property name=”resume” type=”java.sql.Clob” column=”resume” lazy=”true”/>
</class>
</hibernate-mapping>
通过对<property>元素的lazy属性设置true来开启属性的延迟加载,在Hibernate3中为了实现属性的延迟加载,使用了类增强器来对实体类的Class文件进行强化处理,通过增强器的增强,将CGLIB的回调机制逻辑,加入实体类,这里我们可以看出属性的延迟加载,还是通过CGLIB来实现的。CGLIB是 Apache的一个开源工程,这个类库可以操纵java类的字节码,根据字节码来动态构造符合要求的类对象。根据上面的配置我们运行下面的代码:
String sql=”from User user where user.name=’zx’ ”;
Query query=session.createQuery(sql); (1)
List list=query.list();
for(int i=0;i<list.size();i++){
User user=(User)list.get(i);
System.out.println(user.getName());
System.out.println(user.getResume()); (2)
}
当执行到(1)处时,会生成类似如下的SQL语句:
Select id,age,name from user where name=’zx’;
这时Hibernate会检索User实体中所有非延迟加载属性对应的字段数据,当执行到(2)处时,会生成类似如下的SQL语句:
Select resume from user where id=’1’;
这时会发起对resume字段数据真正的读取操作。
web面试常问的一个问题选用ibatis和hibernate的区别
Hibernate简介
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的J2EE架构中取代CMP,完成数据持久化的重任。
一、基本功能
Hibernate作为数据持久化的中间件,足以让数据库在业务逻辑层开发中去冬眠。它通过可扩展标记语言(XML)实现了类和数据表之间的映射,使程序员在业务逻辑的开发中面向数据库而改为面向对象开发。使整个项目开发分工更加明确,提高了程序开发的效率。
configuration对象:
Configuration 类负责管理Hibernate 的配置信息。Hibernate 运行时需要
获取一些底层实现的基本信息,其中几个关键属性包括:
1.数据库URL
2.数据库用户
3.数据库用户密码
4.数据库JDBC驱动类
5.数据库dialect,用于对特定数据库提供支持,其中包含了针对特定数据库特性的实现,如Hibernate数据类型到特定数据库数据类型的映射等。
以上信息一般情况下由hibernate.cfg.xml或者hibernate.properties文件来配置,实现与不同数据库的连接。
Session对象:
Session是持久层操作的基础,相当于JDBC中的Connection:
实例通过SessionFactory实例构建:
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
之后我们就可以调用Session所提供的save、find、flush等方法完成持久层操作。因此Session对象也封装了所有对数据库的操作来实现Hibernate对数据库的操纵功能,如:
Save()方法实现增加和保存;
Delete()方法实现数据的删除;
Update()方法实现数据更新和修改;
Find()方法实现数据的检索;
Hibernate会根据不同的操作自动生成相应的SQL语句,从而实现了程序员对PO对象的操作转化为对数据库关系表的操作。
二、使用步骤
1.编写Hibernate配置文件
Hibernate配置文件有两种,分别是hibernate.cfg.xml文件和hibernate.properties,推荐使用hibernate.cfg.xml。
2.PO和映射文件
使用middlegen和hibernate-extensions从数据库导出PO的映射文件,并在hibernate.cfg.xml当中声明。
3.编写DAO
对每一张关系表编写一个DAO,提供一组增、删、改、查方法供业务逻辑对数据库操作使用。
更多的细节请大家参阅hibernate的网站获取详细的信息。并在各自的实践和开发中加深体会。
Ibatis简介
相对Hibernate和Apache OJB 等"一站式"ORM解决方案而言,ibatis 是一种"半自动化"的ORM实现。所谓"半自动",可能理解上有点生涩。纵观目前主流的ORM,无论Hibernate 还是Apache OJB,都对数据库结构提供了较为完整的封装,提供了从POJO 到数据库表的全套映射机制。程序员往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate或者OJB 提供的方法完成持久层操作。程序员甚至不需要对SQL 的熟练掌握,Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。
Ibatis最直接的好处就是不但为程序员提供了对象与关系数据库之间的映射,同时提供操作方法与SQL间的直接影射,设计者可以直接为一个方法指定一条SQL语句,从而取得更加准确的数据,同时为优化查询、连接查询提供了方便。
一、基本功能
作为又一个轻量级的ORM中间件,ibatis除了提供了对数据库基本的增、删、改、查外还提供了连接管理,缓存支持,线程支持,(分布式)事物管理等一套教为完整的数据库管理功能。
SqlMapClient对象是ibatis持久层操作的基础,相当于hibernate中的session,提供对SQL映射的方法。
insert()方法实现对插入SQL语句的映射;
delete()方法实现对删除SQL语句的映射;
update()方法实现对更新SQL语句的影射;
queryForList()、queryForMap()、queryForObject()、queryForPaginatedList()等方法提供了一组查询SQL语句的影射;
二、使用步骤
1.ibatis SQL Map 配置文件
文件中对所用数据库的连接做了基本配置,包括数据库驱动类型、用户名、密码,以及连接池的相关管理数据。
2.PO和映射文件
和hibernate一样,PO作为数据库关系表的影射,也需要响应的映射配置文件,可以手写,也可以借助hibernate的相关工具生成PO,不会影响PO在ibatis中的使用。与hibernate不同的是,ibatis的映射文件中没有对PO中每个属性做响应的描述,而是指定了一系列与PO有关的SQL相关操作,也体现了ibatis良好的灵活性与可扩展性。
3.编写DAO
在DAO中,可以使用SqlMapClient提供的方法来对应的指定对PO操作的SQL语句,从而使业务逻辑层的开发仍然是面向对象的操作。
选择Hibernate还是iBATIS都有它的道理:
Hibernate的特点:
Hibernate功能强大,数据库无关性好,O/R映射能力强,如果你对Hibernate相当精通,而且对Hibernate进行了适当的封装,那么你的项目整个持久层代码会相当简单,需要写的代码很少,开发速度很快,非常爽。以数据库字段一一对应映射得到的PO和Hibernte这种对象化映射得到的PO是截然不同的,本质区别在于这种PO是扁平化的,不像Hibernate映射的PO是可以表达立体的对象继承,聚合等等关系的,这将会直接影响到你的整个软件系统的设计思路。Hibernate对数据库结构提供了较为完整的封装,Hibernate的O/R Mapping实现了POJO 和数据库表之间的映射,以及SQL 的自动生成和执行。程序员往往只需定义好了POJO 到数据库表的映射关系,即可通过Hibernate 提供的方法完成持久层操作。程序员甚至不需要对SQL 的熟练掌握, Hibernate/OJB 会根据制定的存储逻辑,自动生成对应的SQL 并调用JDBC 接口加以执行。Hibernate的缺点就是学习门槛不低,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡取得平衡,以及怎样用好Hibernate方面需要你的经验和能力都很强才行,但是Hibernate现在已经是主流O/R Mapping框架,从文档的丰富性,产品的完善性,版本的开发速度都要强于iBATIS。
iBATIS的特点:
iBATIS入门简单,即学即用,提供了数据库查询的自动对象绑定功能,而且延续了很好的SQL使用经验,对于没有那么高的对象模型要求的项目来说,相当完美。iBATIS的缺点就是框架还是比较简陋,功能尚有缺失,虽然简化了数据绑定代码,但是整个底层数据库查询实际还是要自己写的,工作量也比较大,而且不太容易适应快速数据库修改。当系统属于二次开发,无法对数据库结构做到控制和修改,那iBATIS的灵活性将比Hibernate更适合。系统数据处理量巨大,性能要求极为苛刻,这往往意味着我们必须通过经过高度优化的SQL语句(或存储过程)才能达到系统性能设计指标。在这种情况下iBATIS会有更好的可控性和表现。
对于实际的开发进行的比较:
1. iBATIS需要手写sql语句,也可以生成一部分,Hibernate则基本上可以自动生成,偶尔会写一些Hql。同样的需求,iBATIS的工作量比Hibernate要大很多。类似的,如果涉及到数据库字段的修改,Hibernate修改的地方很少,而iBATIS要把那些sql mapping的地方一一修改。
2. iBatis 可以进行细粒度的优化
比如说我有一个表,这个表有几个或者几十个字段,我需要更新其中的一个字段,iBatis 很简单,执行一个sql UPDATE TABLE_A SET column_1=#column_1# WHERE id=#id# 但是用 Hibernate 的话就比较麻烦了,缺省的情况下 hibernate 会更新所有字段。当然我记得 hibernate 有一个选项可以控制只保存修改过的字段,但是我不太确定这个功能的负面效果。
例如:我需要列出一个表的部分内容,用 iBatis 的时候,这里面的好处是可以少从数据库读很多数据,节省流量SELECT ID, NAME FROM TABLE_WITH_A_LOT_OF_COLUMN WHERE ...一般情况下Hibernate 会把所有的字段都选出来。比如说有一个上面表有8个字段,其中有一两个比较大的字段,varchar(255)/text。上面的场景中我为什么要把他们也选出来呢?用hibernate 的话,你又不能把这两个不需要的字段设置为lazy load,因为还有很多地方需要一次把整个 domain object 加载出来。这个时候就能显现出ibatis 的好处了。如果我需要更新一条记录(一个对象),如果使用 hibernate,需要现把对象 select 出来,然后再做 update。这对数据库来说就是两条sql。而iBatis只需要一条update的sql就可以了。减少一次与数据库的交互,对于性能的提升是非常重要。
3. 开发方面:
开发效率上,我觉得两者应该差不多。可维护性方面,我觉得 iBatis 更好一些。因为 iBatis 的 sql 都保存到单独的文件中。而 Hibernate 在有些情况下可能会在 java 代码中保sql/hql。相对Hibernate“O/R”而言,iBATIS 是一种“Sql Mapping”的ORM实现。而iBATIS 的着力点,则在于POJO 与SQL之间的映射关系。也就是说,iBATIS并不会为程序员在运行期自动生成SQL 执行。具体的SQL 需要程序员编写,然后通过映射配置文件,将SQL所需的参数,以及返回的结果字段映射到指定POJO。使用iBATIS 提供的ORM机制,对业务逻辑实现人员而言,面对的是纯粹的Java对象,这一层与通过Hibernate 实现ORM 而言基本一致,而对于具体的数据操作,Hibernate会自动生成SQL 语句,而iBATIS 则要求开发者编写具体的SQL 语句。相对Hibernate而言,iBATIS 以SQL开发的工作量和数据库移植性上的让步,为系统设计提供了更大的自由空间。
4. 运行效率
在不考虑 cache 的情况下,iBatis 应该会比hibernate 快一些或者很多。
1.在数据库中条件查询速度很慢的时候,如何优化?
1).建索引
2).减少表之间的关联
3).优化sql,尽量让sql很快定位数据,不要让sql做全表查询,应该走索引,把数据量大的表排在前面
4).简化查询字段,没用的字段不要,已经对返回结果的控制,尽量返回少量数据
2.在Hibernate中进行多表查询,每个表中各取几个字段,也就是说查询出来的结果集并没有一个实体类与之对应,如何解决这个问题?
解决方案一,按照Object[]数据取出数据,然后自己组bean
解决方案二,对每个表的bean写构造函数,比如表一要查出field1,field2两个字段,那么有一个构造函数就是Bean(type1 filed1,type2 field2) ,然后在hql里面就可以直接生成这个bean了。具体怎么用请看相关文档,我说的不是很清楚。
session.load()和session.get()的区别
3. Session.load/get方法均可以根据指定的实体类和id从数据库读取记录,并返回与之对应的实体对象。其区别在于:
如果未能发现符合条件的记录,get方法返回null,而load方法会抛出一个ObjectNotFoundException。
Load方法可返回实体的代理类实例,而get方法永远直接返回实体类。
load方法可以充分利用内部缓存和二级缓存中的现有数据,而get方法则仅仅在内部缓存中进行数据查找,如没有发现对应数据,将越过二级缓存,直接调用SQL完成数据读取。
Session在加载实体对象时,将经过的过程:
首先,Hibernate中维持了两级缓存。第一级缓存由Session实例维护,其中保持了Session当前所有关联实体的数据,也称为内部缓存。而第二级缓存则存在于SessionFactory层次,由当前所有由本SessionFactory构造的Session实例共享。出于性能考虑,避免无谓的数据库访问,Session在调用数据库查询功能之前,会先在缓存中进行查询。首先在第一级缓存中,通过实体类型和id进行查找,如果第一级缓存查找命中,且数据状态合法,则直接返回。之后,Session会在当前“NonExists”记录中进行查找,如果“NonExists”记录中存在同样的查询条件,则返回null。“NonExists”记录了当前Session实例在之前所有查询操作中,未能查询到有效数据的查询条件(相当于一个查询黑名单列表)。如此一来,如果Session中一个无效的查询条件重复出现,即可迅速作出判断,从而获得最佳的性能表现。
对于load方法而言,如果内部缓存中未发现有效数据,则查询第二级缓存,如果第二级缓存命中,则返回。
如在缓存中未发现有效数据,则发起数据库查询操作(Select SQL),如经过查询未发现对应记录,则将此次查询的信息在“NonExists”中加以记录,并返回null。
根据映射配置和Select SQL得到的ResultSet,创建对应的数据对象。
将其数据对象纳入当前Session实体管理容器(一级缓存)。
执行Interceptor.onLoad方法(如果有对应的Interceptor)。
将数据对象纳入二级缓存。
如果数据对象实现了LifeCycle接口,则调用数据对象的onLoad方法。
返回数据对象。
Hibernate的主键生成机制
1) assigned
主键由外部程序负责生成,无需Hibernate参与。
2) hilo
通过hi/lo 算法实现的主键生成机制,需要额外的数据库表保存主键生成历史状态。
3) seqhilo
与hilo 类似,通过hi/lo 算法实现的主键生成机制,只是主键历史状态保存在Sequence中,适用于支持Sequence的数据库,如Oracle。
4) increment
主键按数值顺序递增。此方式的实现机制为在当前应用实例中维持一个变量,以保存着当前的最大值,之后每次需要生成主键的时候将此值加1作为主键。这种方式可能产生的问题是:如果当前有多个实例访问同一个数据库,那么由于各个实例各自维护主键状态,不同实例可能生成同样的主键,从而造成主键重复异常。因此,如果同一数据库有多个实例访问,此方式必须避免使用。
5) identity
采用数据库提供的主键生成机制。如DB2、SQL Server、MySQL中的主键生成机制。
6) sequence
采用数据库提供的sequence 机制生成主键。如Oralce 中的Sequence。
7) native
由Hibernate根据底层数据库自行判断采用identity、hilo、sequence其中一种作为主键生成方式。
8) uuid.hex
由Hibernate基于128 位唯一值产生算法生成16 进制数值(编码后以长度32 的字符串表示)作为主键。
9) uuid.string
与uuid.hex 类似,只是生成的主键未进行编码(长度16)。在某些数据库中可能出现问题(如PostgreSQL)。
10) foreign
使用外部表的字段作为主键。一般而言,利用uuid.hex方式生成主键将提供最好的性能和数据库平台适应性。
这10中生成OID标识符的方法,increment 比较常用,把标识符生成的权力交给Hibernate处理.但是当同时多个Hibernate应用操作同一个数据库,甚至同一张表的时候.就推荐使用identity 依赖底层数据库实现,但是数据库必须支持自动增长,当然针对不同的数据库选择不同的方法.如果你不能确定你使用的数据库具体支持什么的情况下.可以选择用native 让Hibernate来帮选择identity,sequence,或hilo.
另外由于常用的数据库,如Oracle、DB2、SQLServer、MySql 等,都提供了易用的主键生成机制(Auto-Increase 字段或者Sequence)。我们可以在数据库提供的主键生成机制上,采用generator-class=native的主键生成方式。
不过值得注意的是,一些数据库提供的主键生成机制在效率上未必最佳,大量并发insert数据时可能会引起表之间的互锁。数据库提供的主键生成机制,往往是通过在一个内部表中保存当前主键状态(如对于自增型主键而言,此内部表中就维护着当前的最大值和递增量),之后每次插入数据会读取这个最大值,然后加上递增量作为新记录的主键,之后再把这个新的最大值更新回内部表中,这样,一次Insert操作可能导致数据库内部多次表读写操作,同时伴随的还有数据的加锁解锁操作,这对性能产生了较大影响。因此,对于并发Insert要求较高的系统,推荐采用uuid.hex 作为主键生成机制。