《2021最新Java面试题全集-2021年第二版》不断更新完善!

    

第十二章 Hibernate

1:什么是 ORM 框架?

对象-关系映射(Object-Relational Mapping,简称ORM),是一种为了解决程序的面向对象模型与数据库的关系模型互不匹配问题的技术。

简单的说,ORM是通过使用描述对象和数据库之间映射的元数据(在Java中可以用XML或者是注解),将程序中的对象自动持久化到关系数据库中或者将关系数据库表中的行转换成Java对象,其本质上就是将数据从一种形式转换到另外一种形式。

对象和关系数据是业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。

 

2:为什么要使用 Hibernate

·       HibernateJDBC访问数据库的代码做了封装,大大简化了数据访问层繁琐的重复性代码。

·       Hibernate是一个基于JDBC的主流持久化框架,是一个优秀的ORM实现。他很大程度的简化DAO层的编码工作

·       Hibernate使用Java反射机制,而不是字节码增强程序来实现透明性。

·       Hibernate的性能非常好,因为它是个轻量级框架。映射的灵活性很出色。它支持各种关系数据库,从一对一到多对多的各种复杂关系。

 

3:JPA Hibernate 有什么区别?

·       JPA Java Persistence API,是Java EE 5的标准ORM接口,也是EJB3规范的一部分。

·       Hibernate,当今很流行的ORM框架,是JPA的一个实现,但是其功能是JPA的超集。

·       JPAHibernate之间的关系,可以简单的理解为JPA是标准接口,Hibernate是实现。那么Hibernate是如何实现与JPA的这种关系的呢。Hibernate主要是通过三个组件来实现的,及Hibernate-annotationHibernate-entitymanagerHibernate-core

·       Hibernate-annotationHibernate支持annotation方式配置的基础,它包括了标准的JPA annotation以及Hibernate自身特殊功能的annotation

·       Hibernate-coreHibernate的核心实现,提供了Hibernate所有的核心功能。

·       Hibernate-entitymanager实现了标准的JPA,可以把它看成Hibernate-coreJPA之间的适配器,它并不直接提供ORM的功能,而是对Hibernate-core进行封装,使得Hibernate符合JPA的规范。

 

4:阐述实体对象的状态以及转换关系

最新的Hibernate文档中为Hibernate对象定义了四种状态(原来是三种状态,面试的时候基本上问的也是三种状态),分别是:瞬时态(new, or transient)、持久态(managed, or persistent)、游离态(detached)和移除态(removed,以前Hibernate文档中定义的三种状态中没有移除态),就以前的Hibernate文档中移除态被视为是瞬时态。

https://img-blog.csdnimg.cn/20190403231622847.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2xpMTMyNTE2OTAyMQ==,size_16,color_FFFFFF,t_70

1)瞬时态:

new一个实体对象后,这个对象处于瞬时态,即这个对象只是一个保存临时数据的内存区域,如果没有变量引用这个对象,则会被JVM的垃圾回收机制回收。这个对象所保存的数据与数据库没有任何关系,除非通过Sessionsave()saveOrUpdate()persist()merge()方法把瞬时态对象与数据库关联,并把数据插入或者更新到数据库,这个对象才转换为持久态对象。

2)持久态:

持久态对象的实例在数据库中有对应的记录,并拥有一个持久化标识(ID)。对持久态对象进行delete操作后,数据库中对应的记录将被删除,那么持久态对象与数据库记录不再存在对应关系,持久态对象变成移除态(可以视为瞬时态)。持久态对象被修改变更后,不会马上同步到数据库,直到数据库事务提交。

3)游离态:

Session进行了close()clear()evict()flush()后,实体对象从持久态变成游离态,对象虽然拥有持久和与数据库对应记录一致的标识值,但是因为对象已经从会话中清除掉,对象不在持久化管理之内,所以处于游离态(也叫脱管态)。游离态的对象与临时状态对象是十分相似的,只是它还含有持久化标识。

 

5:如何理解Hibernate的延迟加载机制?在实际应用中,延迟加载与Session关闭的矛盾是如何处理的?

延迟加载就是在读取的时候不把数据加载进来,而是等到使用时再加载。Hibernate使用了虚拟代理机制实现延迟加载,我们使用Sessionload()方法加载数据或者一对多关联映射在使用延迟加载的情况下从一的一方加载多的一方,得到的都是虚拟代理,简单的说返回给用户的并不是实体本身,而是实体对象的代理。

代理对象在用户调用getter方法时才会去数据库加载数据。但加载数据就需要数据库连接。而当我们把会话关闭时,数据库连接就同时关闭了。

延迟加载与session关闭的矛盾一般可以这样处理:

1)关闭延迟加载特性。

这种方式操作起来比较简单,因为Hibernate的延迟加载特性是可以通过映射文件或者注解进行配置的,但这种解决方案存在明显的缺陷,系统中已经存在主外键关联,如果去掉延迟加载的话,每次查询的开销都会变得很大。

2)在session关闭之前先获取需要查询的数据,可以使用工具方法Hibernate.isInitialized()判断对象是否被加载,如果没有被加载则可以使用Hibernate.initialize()方法加载对象。

3)使用拦截器或过滤器延长Session的生命周期直到视图获得数据。Spring整合Hibernate提供的OpenSessionInViewFilterOpenSessionInViewInterceptor就是这种做法。

 

6:持久层设计要考虑的问题有哪些?

所谓"持久"就是将数据保存到可掉电式存储设备中以便今后使用,简单的说,就是将内存中的数据保存到关系型数据库、文件系统、消息队列等提供持久化支持的设备中。

持久层就是系统中专注于实现数据持久化的相对独立的层面。

持久层设计的目标包括:

1)数据存储逻辑的分离,提供抽象化的数据访问接口。

2)数据访问底层实现的分离,可以在不修改代码的情况下切换底层实现。

3)资源管理和调度的分离,在数据访问层实现统一的资源调度(如缓存机制)。

4)数据抽象,提供更面向对象的数据操作。

7:HibernateSessionFactory是线程安全的吗?Session是线程安全的吗(两个线程能够共享同一个Session吗)?

SessionFactory对应Hibernate的一个数据存储的概念,它是线程安全的,可以被多个线程并发访问。

SessionFactory一般只会在启动的时候构建。对于应用程序,最好将SessionFactory通过单例模式进行封装以便于访问。

Session是一个轻量级非线程安全的对象(线程间不能共享session),它表示与数据库进行交互的一个工作单元。

Session是由SessionFactory创建的,在任务完成之后它会被关闭。Session是持久层服务对外提供的主要接口。Session会延迟获取数据库连接(也就是在需要的时候才会获取)。

为了避免创建太多的session,可以使用ThreadLocalsession和当前线程绑定在一起,这样可以让同一个线程获得的总是同一个session

8:Hibernate 中如何在控制台查看打印的 sql 语句?

在配置中设置ShowSqltrue

9:Hibernate 有几种查询方式?

HQL:通过Hibernate提供的查询语言进行查询。Hibernate Query lanague

EJBQL(JPQL 1.0) :是EJB提供的查询语言

QBC(query by cretira):通过Cretira接口进行查询

QBE(query by Example):通过Example编程接口进行查询

SQL:直接使用native sql查询

10:Hibernate 实体类可以被定义为 final 吗?

可以将Hibernate的实体类定义为final类,但这种做法并不好。

因为Hibernate会使用代理模式在延迟关联的情况下提高性能,如果你把实体类定义成final类之后,因为 Java不允许对final类进行扩展,所以Hibernate就无法再使用代理了,如此一来就限制了使用可以提升性能的手段。

 

11: Hibernate 中使用 Integer int 做映射有什么区别?

Hibernate中,如果将OID定义为Integer类型,那么Hibernate就可以根据其值是否为null而判断一个对象是否是临时的,如果将OID定义为了int类型,还需要在hbm映射文件中设置其unsaved-value属性为0

 

12:Hibernate 是如何工作的?

Hibernate工作原理:

1)通过Configuration config = new Configuration().configure();//读取并解析hibernate.cfg.xml配置文件

2)由hibernate.cfg.xml中的<mapping resource="com/xx/User.hbm.xml"/>读取并解析映射信息

3)通过SessionFactory sf = config.buildSessionFactory();//创建SessionFactory

4Session session = sf.openSession();//打开Sesssion

5Transaction tx = session.beginTransaction();//创建并启动事务Transation

6persistent operate操作数据,持久化操作

7tx.commit();//提交事务

8)关闭Session

    关闭SesstionFactory

 

 

13: get() load()的区别?

get 方法和load方法都是根据对象id来取得一个对象(也就是一条记录):

1)从返回结果上对比:

load方式检索不到数据的话会抛出ObjectNotFoundException异常

get方法检索不到数据的话会返回null

2)从检索执行机制上对比:

get方法和find方法都是直接从数据库中检索。

load方法的执行则比较复杂:

首先查找sessionpersistent Context中是否有缓存,如果有则直接返回;

如果没有则判断是否是lazy,如果不是直接访问数据库检索,查到记录返回,查不到抛出异常;

如果是lazy则需要建立代理对象,对象的initialized属性为falsetarget属性为null,在访问获得的代理对象的属性时,检索数据库;

如果找到记录则把该记录的对象复制到代理对象的target上,并将initialized=true,如果找不到就抛出异常。

 

14:Sessionsave()update()merge()lock()saveOrUpdate()persist()方法分别是做什么的?有什么区别?

Hibernate的对象有三种状态:瞬时态(transient)、持久态(persistent)和游离态(detached)。

瞬时态的实例可以通过调用save()persist()或者saveOrUpdate()方法变成持久态;游离态的实例可以通过调用 update()saveOrUpdate()lock()或者replicate()变成持久态。

save()persist()将会引发SQLINSERT语句,而update()merge()会引发UPDATE语句。save()update()的区别在于一个是将瞬时态对象变成持久态,一个是将游离态对象变为持久态。

merge()方法可以完成save()update()方法的功能,它的意图是将新的状态合并到已有的持久化对象上或创建新的持久化对象。

对于persist()方法,按照官方文档的说明:① persist()方法把一个瞬时态的实例持久化,但是并不保证标识符被立刻填入到持久化实例中,标识符的填入可能被推迟到flush的时间;② persist()方法保证当它在一个事务外部被调用的时候并不触发一个INSERT语句,当需要封装一个长会话流程的时候,persist()方法是很有必要的;③ save()方法不保证第②条,它要返回标识符,所以它会立即执行INSERT语句,不管是在事务内部还是外部。

至于lock()方法和update()方法的区别,update()方法是把一个已经更改过的脱管状态的对象变成持久状态;lock()方法是把一个没有更改过的脱管状态的对象变成持久状态。

 

15:阐述Session加载实体对象的过程

Session加载实体对象的步骤是:

1Session在调用数据库查询功能之前,首先会在一级缓存中通过实体类型和主键进行查找,如果一级缓存查找命中且数据状态合法,则直接返回;

2)如果一级缓存没有命中,接下来Session会在当前NonExists记录(相当于一个查询黑名单,如果出现重复的无效查询可以迅速做出判断,从而提升性能)中进行查找,如果NonExists中存在同样的查询条件,则返回null

3)如果一级缓存查询失败则查询二级缓存,如果二级缓存命中则直接返回;

4)如果之前的查询都未命中,则发出SQL语句,如果查询未发现对应记录则将此次查询添加到SessionNonExists中加以记录,并返回null

5)根据映射配置和SQL语句得到ResultSet,并创建对应的实体对象;

6)将对象纳入Session(一级缓存)的管理;

7)如果有对应的拦截器,则执行拦截器的onLoad方法;

8)如果开启并设置了要使用二级缓存,则将数据对象纳入二级缓存;

9)返回数据对象。

 

16:Query接口的list方法和iterate方法有什么区别?

list()方法无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存;iterate()方法可以充分利用缓存,如果目标数据只读或者读取频繁,使用iterate()方法可以减少性能开销。

list()方法不会引起N+1查询问题,而iterate()方法可能引起N+1查询问题

 

 

17:说一下 Hibernate 的缓存机制?

Hibernate中的缓存分为一级缓存和二级缓存。

一级缓存就是 Session 级别的缓存,在事务范围内有效是,内置的不能被卸载。二级缓存是 SesionFactory级别的缓存,从应用启动到应用结束有效。是可选的,默认没有二级缓存,需要手动开启。保存数据库后,缓存在内存中保存一份,如果更新了数据库就要同步更新。

什么样的数据适合存放到第二级缓存中?

很少被修改的数据   帖子的最后回复时间

经常被查询的数据   电商的地点

不是很重要的数据,允许出现偶尔并发的数据

不会被并发访问的数据

常量数据

Hibernate的二级缓存默认是不支持分布式缓存的。可以使用Redis等来代替二级缓存

 

18:锁机制有什么用?简述Hibernate的悲观锁和乐观锁机制。

有些业务逻辑在执行过程中要求对数据进行排他性的访问,于是需要通过一些机制保证在此过程中数据被锁住不会被外界修改,这就是所谓的锁机制。

Hibernate支持悲观锁和乐观锁两种锁机制。悲观锁,顾名思义悲观的认为在数据处理过程中极有可能存在修改数据的并发事务(包括本系统的其他事务或来自外部系统的事务),于是将处理的数据设置为锁定状态。悲观锁必须依赖数据库本身的锁机制才能真正保证数据访问的排他性。

乐观锁,顾名思义,对并发事务持乐观态度(认为对数据的并发操作不会经常性的发生),通过更加宽松的锁机制来解决由于悲观锁排他性的数据访问对系统性能造成的严重影响。

最常见的乐观锁是通过数据版本标识来实现的,读取数据时获得数据的版本号,更新数据时将此版本号加1,然后和数据库表对应记录的当前版本号进行比较,如果提交的数据版本号大于数据库中此记录的当前版本号则更新数据,否则认为是过期数据无法更新。

Hibernate中通过Sessionget()load()方法从数据库中加载对象时可以通过参数指定使用悲观锁;而乐观锁可以通过给实体类加整型的版本字段再通过XML@Version注解进行配置。

使用乐观锁会增加了一个版本字段,很明显这需要额外的空间来存储这个版本字段,浪费了空间,但是乐观锁会让系统具有更好的并发性,这是对时间的节省。因此乐观锁也是典型的空间换时间的策略。

 

19: Hibernate getCurrentSession openSession 的区别是什么?

openSession是打开一个新的session对象,而且每次使用都是打开一个新的session,假如连续使用多次,则获得的session不是同一个对象,并且使用完需要调用close方法关闭session

getCurrentSession是获取当前上下文一个session对象,当第一次使用此方法时,会自动产生一个session对象,并且连续使用多次时,得到的session都是同一个对象,这就是与openSession的区别之一。

简单而言,getCurrentSession 就是:如果有已经使用的,用旧的,如果没有,建新的。

 

20:Hibernate实体类必须要有无参构造函数吗?为什么?

必须要有,因为Hibernate框架会调用这个默认构造方法来构造实例对象,即Class类的newInstance方法,这个方法就是通过调用默认构造方法来创建实例对象的。

如果你没有提供任何构造方法,虚拟机会自动提供默认构造方法(无参构造器),但是如果你提供了其他有参数的构造方法的话,虚拟机就不再为你提供默认构造方法,这时必须手动把无参构造器写在代码里,否则new Xxxx()是会报错的,所以默认的构造方法不是必须的,只在有多个构造方法时才是必须的,这里必须指的是必须手动写出来

 

21:谈一下你对继承映射的理解。

继承关系的映射策略有三种:

1)每个继承结构一张表(table per class hierarchy),不管多少个子类都用一张表。

2)每个子类一张表(table per subclass),公共信息放一张表,特有信息放单独的表。

3)每个具体类一张表(table per concrete class),有多少个子类就有多少张表。

第一种方式属于单表策略,其优点在于查询子类对象的时候无需表连接,查询速度快,适合多态查询;缺点是可能导致表很大。

后两种方式属于多表策略,其优点在于数据存储紧凑,其缺点是需要进行连接查询,不适合多态查询。

 

22:简述Hibernate常见优化策略。

这个问题应当挑自己使用过的优化策略回答,常用的有:

1)制定合理的缓存策略(二级缓存、查询缓存)。

2)采用合理的Session管理机制。

3)尽量使用延迟加载特性。

4)设定合理的批处理参数。

5)如果可以,选用UUID作为主键生成器。

6)如果可以,选用基于版本号的乐观锁替代悲观锁。

7)在开发过程中, 开启hibernate.show_sql选项查看生成的SQL,从而了解底层的状况;开发完成后关闭此选项。

8)考虑数据库本身的优化,合理的索引、恰当的数据分区策略等都会对持久层的性能带来可观的提升,但这些需要专业的DBA(数据库管理员)提供支持。

 

23:谈一谈Hibernate的一级缓存、二级缓存和查询缓存。

HibernateSession提供了一级缓存的功能,默认总是有效的,当应用程序保存持久化实体、修改持久化实体时,Session并不会立即把这种改变提交到数据库,而是缓存在当前的Session中,除非显示调用了Sessionflush()方法或通过close()方法关闭Session。通过一级缓存,可以减少程序与数据库的交互,从而提高数据库访问性能。

SessionFactory级别的二级缓存是全局性的,所有的Session可以共享这个二级缓存。不过二级缓存默认是关闭的,需要显示开启并指定需要使用哪种二级缓存实现类(可以使用第三方提供的实现)。一旦开启了二级缓存并设置了需要使用二级缓存的实体类,SessionFactory就会缓存访问过的该实体类的每个对象,除非缓存的数据超出了指定的缓存空间。

一级缓存和二级缓存都是对整个实体进行缓存,不会缓存普通属性,如果希望对普通属性进行缓存,可以使用查询缓存。查询缓存是将HQLSQL语句以及它们的查询结果作为键值对进行缓存,对于同样的查询可以直接从缓存中获取数据。查询缓存默认也是关闭的,需要显示开启。

 

24:HibernateDetachedCriteria类是做什么的?

DetachedCriteriaCriteria的用法基本上是一致的,但Criteria是由SessioncreateCriteria()方法创建的,也就意味着离开创建它的SessionCriteria就无法使用了。

DetachedCriteria不需要Session就可以创建(使用DetachedCriteria.forClass()方法创建),所以通常也称其为离线的Criteria,在需要进行查询操作的时候再和Session绑定(调用其getExecutableCriteria(Session)方法),这也就意味着一个DetachedCriteria可以在需要的时候和不同的Session进行绑定。

 

25:@OneToMany注解的mappedBy属性有什么作用?

@OneToMany用来配置一对多关联映射,但通常情况下,一对多关联映射都由多的一方来维护关联关系,例如学生和班级,应该在学生类中添加班级属性来维持学生和班级的关联关系(在数据库中是由学生表中的外键班级编号来维护学生表和班级表的多对一关系),如果要使用双向关联,在班级类中添加一个容器属性来存放学生,并使用@OneToMany注解进行映射,此时mappedBy属性就非常重要。如果使用XML进行配置,可以用<set>标签的inverse="true"设置来达到同样的效果。