Spring对Hibernate和JPA的支持

a、延迟加载(Lazy loading)

b、预先抓取(Eager fetching)

c、级联(Cascading)

一些可用的框架提供了如上这样的服务,这些服务的通用名称是对象/关系映射(object-relational mapping,ORM)。在持久层使用ORM工具,可以节省数千行的代码和大量的开发时间。ORM工具能够把你的注意力从容易出错的SQL代码转向如何实现应用程序的真正需求。Spring对多个持久化框架都提供了支持,包括Hibernate、iBATIS、Java数据对象(Java Data Objects,JDO)以及Java持久化API(Java Persistence API,JPA)。与Spring对JDBC的支持那样,Spring对ORM框架的支持提供了与这些框架的集成点以及一些附加的服务:

1、支持集成Spring声明式事务;

2、透明的异常处理;

3、线程安全的、轻量级的模板类;

4、DAO支持类;

5、资源管理;

在本章中,我们将会看到Spring如何与最常用的两种ORM方案集成:Hibernate和JPA。同时还会通过Spring Data JPA了解一下Spring Data项目。借助这种方式,我们不仅可以学习到如何借助Spring Data JPA移除JPA Repository中的样板式代码,还能为下一章的如何将Spring Data用于无模式的存储打下基础。

一、Spring集成Hibernate,不用Template和HibernateDaoSupport了,而是直接使用session了!

使用Hibernate所需要的主要接口是org.hibernate.Session。通过Hibernate的Session接口,应用程序的Repository能够满足所有的持久化需求。获取Hibernate Session对象的标准方式是借助于Hibernate SessionFactory接口的实现类。除了一些其他的任务,SessionFactory主要负责Hibernate Session的打开、关闭以及管理。Spring提供了三个Session工厂bean供我们选择:

1、org.springframework.orm.hibernate3.LocalSessionFactoryBean;

2、org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;

3、org.springframework.orm.hibernate4.LocalSessionFactoryBean;

这些Session工厂bean都是Spring FactoryBean接口的实现,它们会产生一个HibernateSessionFactory。这样的话,就能在应用的Spring上下文中,与其他的bean一起配置Hibernate Session工厂。

如果你使用Hibernate4的话推荐使用org.springframework.orm.hibernate4.LocalSessionFactoryBean。它是hibernate3里面的LocalSessionFactoryBean与AnnotationSessionFactoryBean的结合体,能够支持基于XML的映射和基于注解的映射。如下的代码展现了如何对它进行配置,使其支持基于注解的映射:

Spring对Hibernate和JPA的支持

 

用packagesToScan属性告诉Spring扫描一个或多个包以查找域类,这些类通过注解的方式表明要使用Hibernate进行持久化,这些类可以使用的注解包括JPA的@Entity或@MappedSuperclass以及Hibernate的@Entity。

 

在Spring和Hibernate的早期岁月中,我们用的是HibernateTemplate,HibernateTemplate能够保证每个事务使用同一个Session。但是这种方式的弊端在于我们的Repository实现会直接与Spring耦合。现在的最佳实践是不再使用HibernateTemplate,而是使用上下文Session(Contextual session)。通过这种方式,会直接将Hibernate SessionFactory装配到Repository中,并使用它来获取Session,如下面的程序清单所示:

Spring对Hibernate和JPA的支持

 

我们通过@Inject注解让Spring自动将一个SessionFactory注入到HibernateSpitterRepository的sessionFactory属性中。接下来,在currentSession()方法中,我们使用这个SessionFactory来获取当前事务的Session。

 

重点:我们在类上使用了@Repository注解,这会为我们做两件事情

1、@Repository是Spring的另一种构造性注解,它能够像其他注解一样被Spring的组件扫描所扫描到。这样就不必明确声明HibernateSpitterRepository bean了,只要这个Repository类在组件扫描所涵盖的包中即可。

2、下面讲。。。!

让我们回想一下模板类,它有一项任务就是捕获平台相关的异常,然后使用Spring统一非检查型异常的形式重新抛出。如果我们使用Hibernate上下文Session而不是Hibernate模板的话,那异常转换会怎么处理呢?为了给不使用模板的Hibernate Repository添加异常转换功能,我们只需在Spring应用上下文中添加一个PersistenceExceptionTranslationPostProcessor bean(在配置文件里面申明即可)。PersistenceExceptionTranslationPostProcessor是一个bean 后置处理器(beanpost-processor),它会在所有拥有@Repository注解的类上添加一个通知器(advisor),这样就会捕获任何平台相关的异常并以Spring非检查型数据访问异常的形式重新抛出。

好了,到这里为止,我们没有依赖Spring的Hibernate版本的Repository已经完成了。

 

二、使用JPA的第一步,配置实体管理器工厂,类似于LocalSessionFactoryBean那一类的东西,用来生成EntityManager:

在Spring中使用JPA的第一步是要在Spring应用上下文中将实体管理器工厂(entity manager factory)按照bean的形式来进行配置。

简单来讲,基于JPA的应用程序需要使用EntityManagerFactory的实现类来获取EntityManager实例。JPA定义了两种类型的实体管理器:

1、应用程序管理类型(Application-managed):当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器适合于不运行在Java EE容器中的独立应用程序。

2、容器管理类型(Container-managed):实体管理器由Java EE创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或JNDI来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适用于Java EE容器,在这种情况下会希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

两种的最主要的区别在EntityManager的创建和管理方式。

应用程序管理类型的EntityManager是由EntityManagerFactory创建的。

应用程序管理类型EntityManagerFactory是通过PersistenceProvider的createEntityManagerFactory()方法得到的。

容器管理类型的Entity ManagerFactory是通过PersistenceProvider的createContainerEntityManager Factory()方法获得的。

不管你希望使用哪种EntityManagerFactory,Spring都会负责管理EntityManager。

 

这两种实体管理器工厂分别由对应的Spring工厂Bean创建:

1、LocalEntityManagerFactoryBean生成应用程序管理类型的EntityManagerFactory;

2、LocalContainerEntityManagerFactoryBean生成容器管理类型的;

需要说明的是,选择应用程序管理类型的还是容器管理类型的EntityManagerFactory,对于基于Spring的应用程序来讲是完全透明的。

应用程序管理类型和容器管理类型的实体管理器工厂之间唯一值得关注的区别是在Spring应用上下文中如何进行配置。

 

这里讲下哈,配置应用程序管理类型的JPA我没看懂,由于实际操作中也不用这个,索性就不看这种了,来,我们直接看如何配置使用容器管理类型的JPA:

如下的@Bean注解方法声明了在Spring中如何使用用LocalContainerEntityManagerFactoryBean来来配置容器管理类型的JPA:

Spring对Hibernate和JPA的支持

在这个配置中,LocalContainerEntityManagerFactoryBean会扫描com.habuma.spittr.domain包,查找带有@Entity注解的类。

jpaVendorAdapter属性用于指明所使用的是哪一个厂商的JPA实现。Spring提供了多个JPA厂商适配器:

1、EclipseLinkJpaVendorAdapter

2、HibernateJpaVendorAdapter

3、OpenJpaVendorAdapter

4、TopLinkJpaVendorAdapter(在Spring 3.1版本中,已经将其废弃了)

在本例中,我们使用Hibernate作为JPA实现,所以将其配置为HibernateJpaVendorAdapter(厂商适配器):

Spring对Hibernate和JPA的支持

有多个属性需要设置到厂商适配器上,但是最重要的是database属性,在上面我们设置了要使用的数据库是Hypersonic。这个属性支持的其他值如表11.1所示。

Spring对Hibernate和JPA的支持

Spring对Hibernate和JPA的支持

 

三、使用实体管理器工厂注入的方式实现简单的JPA持久化方案(类似Spring中使用Hibernate注入SessionFactory的方式):

正如Spring对其他持久化方案的集成一样,Spring对JPA集成也提供了JpaTemplate模板以及对应的支持类JpaDaoSupport。但是,为了实现更纯粹的JPA方式,基于模板的JPA已经被弃用了。

纯粹的JPA方式远胜于基于模板的JPA,如下程序清单中的JpaSpitterRepository展现了如何开发不使用Spring JpaTemplate的JPA Repository:

Spring对Hibernate和JPA的支持

需要注意的是EntityManagerFactory属性,它使用了@PersistenceUnit注解,因此,Spring会将EntityManagerFactory注入到Repository之中。有了EntityManagerFactory之后,JpaSpitterRepository的方法就能使用它来创建EntityManager了,然后EntityManager可以针对数据库执行操作。

在JpaSpitterRepository中,唯一的问题在于每个方法都会调用createEntityManager()。除了引入易出错的重复代码以外,这还意味着每次调用Repository的方法时,都会创建一个新的EntityManager。这种复杂性源于事务。如果我们能够预先准备好EntityManager,那会不会更加方便呢?

这里的问题在于EntityManager并不是线程安全的,一般来讲并不适合注入到像Repository这样共享的单例bean中。但是,这并不意味着我们没有办法要求注入EntityManager。如下的程序清单展现了如何借助@PersistentContext注解为JpaSpitterRepository设置EntityManager:

Spring对Hibernate和JPA的支持

在这个新版本的JpaSpitterRepository中,直接为其设置了EntityManager,这样的话,在每个方法中就没有必要再通过EntityManagerFactory创建EntityManager了。尽管这种方式非常便利,但是你可能会担心注入的EntityManager会有线程安全性的问题。

这里的真相是@PersistenceContext并不会真正注入EntityManager——至少,精确来讲不是这样的。它没有将真正的EntityManager设置给Repository,而是给了它一个EntityManager的代理。真正的EntityManager是与当前事务相关联的那一个,如果不存在这样的EntityManager的话,就会创建一个新的。这样的话,我们就能始终以线程安全的方式使用实体管理器。

另外,还需要了解@PersistenceUnit和@PersistenceContext并不是Spring的注解,它们是由JPA规范提供的。为了让Spring理解这些注解,并注入EntityManager Factory或EntityManager,我们必须要配置Spring的PersistenceAnnotationBeanPostProcessor。如果你已经使用了<context:annotation-config>或<context:component-scan>,那么你就不必再担心了,因为这些配置元素会自动注册PersistenceAnnotationBeanPostProcessorbean。否则的话,我们需要显式地注册这个bean

Spring对Hibernate和JPA的支持

你可能也注意到了JpaSpitterRepository使用了@Repository和@Transactional注解。@Transactional表明这个Repository中的持久化方法是在事务上下文中执行的。对于@Repository注解,它的作用与开发Hibernate上下文Session版本的Repository时是一致

的。由于没有使用模板类来处理异常,所以我们需要为Repository添加@Repository注解,这样PersistenceExceptionTranslationPostProcessor就会知道要将这个bean产生的异常转换成Spring的统一数据访问异常

既然提到了PersistenceExceptionTranslationPostProcessor,要记住的是我们需要将其作为一个bean装配到Spring中,就像我们在Hibernate样例中所做的那样:

Spring对Hibernate和JPA的支持

提醒一下,不管对于JPA还是Hibernate,异常转换都不是强制要求的。如果你希望在Repository中抛出特定的JPA或Hibernate异常,只需将PersistenceExceptionTranslationPostProcessor省略掉即可,这样原来的异常就会正常地处理。但是,如果使用了Spring的异常转换,你会将所有的数据访问异常置于Spring的体系之下,这样以后切换持久化机制的话会更容易

 

四、借助Spring Data实现自动化的JPA Repository,只编写Repository接口,不需要我们自己写实现:

Spring Data JPA能够终结这种样板式的愚蠢行为。我们不再需要一遍遍地编写相同的Repository实现,Spring Data能够让我们只编写Repository接口就可以了。根本就不再需要实现类了。

例如,看一下SpitterRepository接口(借助Spring Data,以接口定义的方式创建Repository):

Spring对Hibernate和JPA的支持

编写Spring Data JPA Repository的关键在于要从一组接口中挑选一个进行扩展。这里,SpitterRepository扩展了Spring Data JPA的JpaRepository(稍后,我会介绍几个其他的接口)。通过这种方式,JpaRepository进行了参数化,所以它就能知道这是一个用来持久化Spitter对象的Repository,并且Spitter的ID类型为Long。另外,它还会继承18个执行持久化操作的通用方法,如保存Spitter、删除Spitter以及根据ID查询Spitter,这个接口不用写实现直接就能用哦,哈哈!

我们根本不需要编写SpitterRepository的任何实现类,相反,我们让Spring Data来为我们做这件事请。我们所需要做的就是对它提出要求。

为了要求Spring Data创建SpitterRepository的实现,我们需要在Spring配置中添加一个元素。如下的程序清单展现了在XML配置中启用Spring Data JPA所需要添加的内容

Spring对Hibernate和JPA的支持

<jpa:repositories>元素掌握了Spring Data JPA的所有魔力。就像<context:component-scan>元素一样,<jpa:repositories>元素也需要指定一个要进行扫描的base-package。不过,<context:component-scan>会扫描包(及其子包)来查找带有@Component注解的类,而<jpa:repositories>会扫描它的基础包来查找扩展自Spring Data JPA Repository接口的所有接口。如果发现了扩展自Repository的接口,它会自动生成(在应用启动的时候)这个接口的实现。

如果要使用Java配置的话,那就不需要使用<jpa:repositories>元素了,而是要在Java配置类上添加@EnableJpaRepositories注解。如下就是一个Java配置类,它使用了@EnableJpaRepositories注解,并且会扫描com.habuma.spittr.db包:

Spring对Hibernate和JPA的支持

让我们回到SpitterRepository接口,它扩展自JpaRepository,而JpaRepository又扩展自Repository标记接口(虽然是间接的)。因此,SpitterRepository就传递性地扩展了Repository接口,也就是Repository扫描时所要查找的接口。当Spring Data找到它后,就会创建SpitterRepository的实现类,其中包含了继承自JpaRepository、PagingAndSortingRepository和CrudRepository的18个方法。很重要的一点在于Repository的实现类是在应用启动的时候生成的,也就是Spring的应用上下文创建的时候。它并不是在构建时通过代码生成技术产生的,也不是接口方法调用时才创建的。

很漂亮的技术,对吧?

Spring Data JPA很棒的一点在于它能为Spitter对象提供18个便利的方法来进行通用的JPA操作,而无需你编写任何持久化代码。如果需求超过了这18个,那么下面介绍为Repository添加自定义方法的实现方式:

例如:

Spring对Hibernate和JPA的支持

当创建Repository实现的时候,Spring Data会检查Repository接口的所有方法,解析方法的名称,并基于被持久化的对象来试图推测方法的目的。本质上,Spring Data定义了一组小型的领域特定语言(domain-specific language ,DSL),在这里,持久化的细节都是通过Repository方法的签名来描述的。

Spring Data能够知道这个方法是要查找Spitter的,因为我们使用Spitter对JpaRepository进行了参数化。方法名findByUsername确定该方法需要根据username属性相匹配来查找Spitter,而username是作为参数传递到方法中来的。另外,因为在方法签名中定义了该方法要返回一个Spitter对象,而不是一个集合,因此它只会查找一个username属性匹配的Spitter。

findByUsername()方法非常简单,但是Spring Data也能处理更加有意思的方法名称。Repository方法是由一个动词、一个可选的主题(Subject)、关键词By以及一个断言所组成。在findByUsername()这个样例中,动词是find,断言是Username,主题并没有指定,暗含的主题是Spitter。

Spring Data允许在方法名中使用四种动词:get、read、find和count。其中,动词get、read和find是同义的,这三个动词对应的Repository方法都会查询数据并返回对象。而动词count则会返回匹配对象的数量,而不是对象本身。

下面给一个典型示例:

Spring对Hibernate和JPA的支持

Repository方法的主题是可选的。它可写可不写,效果一样。对于大部分场景来说,主题会被省略掉。要查询的对象类型是通过如何参数化JpaRepository接口来确定的,而不是方法名称中的主题。

在省略主题的时候,有一种例外情况。如果主题的名称以Distinct开头的话,那么在生成查询的时候会确保所返回结果集中不包含重复记录。

断言指定了限制结果集的属性。断言中,会有一个或多个限制结果的条件。每个条件必须引用一个属性,并且还可以指定一种比较操作。如果省略比较操作符的话,那么这暗指是一种相等比较操作。不过,我们也可以选择其他的比较操作,包括如下的种类:

Spring对Hibernate和JPA的支持

要对比的属性值就是方法的参数。

要处理String类型的属性时,条件中可能还会包含IgnoringCase或IgnoresCase,这样在执行对比的时候就会不再考虑字符是大写还是小写。例如,要在firstname和lastname属性上忽略大小写,那么可以将方法签名改成如下的形式:

Spring对Hibernate和JPA的支持

需要注意,IgnoringCase和IgnoresCase是同义的,你可以随意挑选一个最合适的。作为IgnoringCase/IgnoresCase的替代方案,我们还可以在所有条件的后面添加AllIgnoringCase或AllIgnoresCase,这样它就会忽略所有条件的大小写。

注意,参数的名称是无关紧要的,但是它们的顺序必须要与方法名称中的操作符相匹配

最后,我们还可以在方法名称的结尾处添加OrderBy,实现结果集排序。如果要根据多个属性排序的话,只需将其依序添加到OrderBy中即可。

不过,Spring Data这个小型的DSL依旧有其局限性,有时候通过方法名称表达预期的查询很烦琐,甚至无法实现。如果遇到这种情形的话,Spring Data能够让我们通过@Query注解来解决问题。,如下讲解使用@Query实现自定义查询:

例如:

Spring对Hibernate和JPA的支持

这个方法并不符合Spring Data的方法命名约定。当Spring Data试图生成这个方法的实现时,无法将方法名的内容与Spitter元模型进行匹配,因此会抛出异常。

如果所需的数据无法通过方法名称进行恰当地描述,那么我们可以使用@Query注解,为Spring Data提供要执行的查询。

Spring对Hibernate和JPA的支持

我们依然不需要编写findAllGmailSpitters()方法的实现,只需提供查询即可,让Spring Data JPA知道如何实现这个方法。

可以看到,当使用方法命名约定很难表达预期的查询时,@Query注解能够发挥作用。如果按照命名约定,方法的名称特别长的时候,也可以使用这个注解。

对于Spring Data JPA的接口来说,@Query是一种添加自定义查询的便利方式。但是,它仅限于单个JPA查询。如果我们需要更为复杂的功能,无法在一个简单的查询中处理的话,该怎么办呢?

 

有些时候,我们需要Repository所提供的功能是无法用Spring Data的方法命名约定来描述的,甚至无法用@Query注解设置查询来实现。尽管Spring Data JPA非常棒,但是它依然有其局限性,可能需要我们按照传统的方式来编写Repository方法:也就是直接使用EntityManager。当遇到这种情况的时候,我们是不是要放弃Spring Data JPA,重新按照11.2.2小节中的方式来编写Repository呢?

简单来说,是这样的。如果你需要做的事情无法通过Spring Data JPA来实现,那就必须要在一个比Spring Data JPA更低的层级上使用JPA。好消息是我们没有必要完全放弃Spring DataJPA。我们只需在必须使用较低层级JPA的方法上,才使用这种传统的方式即可,而对于Spring Data JPA知道该如何处理的功能,我们依然可以通过它来实现。当Spring Data JPA为Repository接口生成实现的时候,它还会查找名字与接口相同,并且添加了Impl后缀的一个类。如果这个类存在的话,Spring Data JPA将会把它的方法与Spring DataJPA所生成的方法合并在一起。对于SpitterRepository接口而言,要查找的类名为SpitterRepositoryImpl。例如:

Spring对Hibernate和JPA的支持

注意,SpitterRepositoryImpl并没有实现SpitterRepository接口。Spring Data JPA负责实现这个接口。SpitterRepositoryImpl(将它与Spring Data的Repository关联起来的是它的名字)实现了SpitterSweeper接口,它如下所示:

Spring对Hibernate和JPA的支持

我们还需要确保eliteSweep()方法会被声明在SpitterRepository接口中。要实现这一点,避免代码重复的简单方式就是修改SpitterRepository,让它扩展SpitterSweeper:

Spring对Hibernate和JPA的支持

 

 

@EnableJpaRepositories注解的repositoryBaseClass属性的详解:

正常情况下我们理解的Repository层,只要我们自定义的Repository例如UserRepository实现JpaRepository相关类似Spring boot提供的基础的接口,那么我们自己就不需要实现自定义接口的实现类,jpa会自动帮助我们生成自定义接口的实现类。

这里面包含两部分数据,一部分是从JpaRepository继承过来的所有的jpa提供的基础的方法(基础的增删改查等方法),一部分是我们在自定义接口UserRepository里面自定义的方法。这两部分数据不是说都是在服务启动的时候jpa才帮助我们自动生成

实现,对于自定义接口UserRepository里面我们自定义的方法这个jpa是提前预知不到的,它只能在服务启动的时候再帮助我生成实现,而对于从jpa里面基础接口里面继承过来的那些方法(好像是18个)这些方法是已经固定了的,所以说针对这些

方法jpa是可以提前定义实现类的,jpa提供的针对这些方法的默认实现类是SimpleJpaRepository(也就是说@EnableJpaRepositories注解的repositoryBaseClass参数的默认值是SimpleJpaRepository.class)。而针对从jpa里面基础

接口里继承过来的那些方法(好像是18个)其实我们是可以自定义实现的,也就是说我们可以不用jpa提供的默认的SimpleJpaRepository里面的实现而是自己重新定义实现,只要我们针对这些方法自己定义了实现,那么jpa默认就不会

再使用它提供的实现SimpleJpaRepository而是转而使用我们自己定义的实现类。具体如何在配置中指定我们自定义的这个实现类,配置方式是在@EnableJpaRepositories注解的repositoryBaseClass属性上指定该实现类,例如我们

自定义的实现类名称是BaseJpaRespositoryImpl,那么可以配置成:

@EnableJpaRepositories(repositoryBaseClass = BaseJpaRespositoryImpl.class)

 

这样的话,默认从jpa基础接口里继承过来的那些方法(好像是18个)的实现就从SimpleJpaRepository变成了我们自己定义的实现BaseJpaRespositoryImpl,针对BaseJpaRespositoryImpl里有的实现就用此实现,如果这里没有的

还是会回到父类SimpleJpaRepository里去找实现。“父类有的,子类可以对其进行修改”,这就是这里的原理。

而针对我们自定义的Repository例如UserRepository里面的方法的实现这个肯定是在服务启动的时候jpa帮助我们自动去生成。

 

自己定义的实现类的好处是:

可以屏蔽例如deleteAll()、deleteAllInBatch()这种高风险方法;

可以修改save()方法的逻辑,jpa默认提供的这个方法的逻辑其实是save或update的;

可以在自己指定的那个实现类里自己重新定义jpa提供的基础方法的实现。