Spring实战(十一)-使用对象-关系映射持久化数据

本文基于《Spring实战(第4版)》所写。

随着应用程序变得越来越复杂,我们需要将对象的属性映射到数据库的列上,并且需要自动化生成语句和查询。此外,我们还需要一些更复杂的特性:

  • 延迟加载:允许我们只在需要的时候获取数据。
  • 预先抓取:借助于预先抓取,我们可以使用一个查询获取完整的关联对象。
  • 级联:有时,更改数据库中的表会同时修改其他表。

一些可用的框架提供了这样的服务,这些服务的通用名称是对象/关系映射(object-relational mapping,ORM)。

Spring对多个持久化框架都提供了支持,包括Hibernate、iBATIS、Java数据对象(Java Data Objects,JDO)以及Java持久化API(Java Persistence API, JPA)。与Spring对JDBC的支持那样,Spring对ORM框架的支持提供了与这些框架的集成点以及一些附加的服务:

  • 支持集成Spring声明式事物;
  • 透明的异常处理;
  • 线程安全的、轻量级的模板类;
  • DAO支持类;
  • 资源管理。

在Spring中集成Hibernate

声明Hibernate的Session工厂

使用Hibernate所需的主要接口是org.hibernate.Session。Session接口提供了基本的数据访问功能,如保存、更新、删除以及从数据库加载对象的功能。通过Hibernate的Session接口,应用程序的Repository能够满足所有的持久化需求。

获取Hibernate Session对象的标准方式是借助于Hibernate SessionFactory接口的实现类。除了一些其他的任务,SessionFactory主要负责Hibernate Session的打开、关闭以及管理。

在Spring中,我们要通过Spring的某一个Hibernate Session工厂bean来获取Hibernate SessionFactory。从3.1版本开始,Spring提供了三个Session工厂bean供我们选择:

  • org.springframework.orm.hibernate3. LocalSessionFactoryBean
  • org.springframework.orm.hibernate3.annotation. AnnotationSessionFactoryBean
  • org.springframework.orm.hibernate4. LocalSessionFactoryBean

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

如果使用Hibernate 3.2或更高版本(直到Hibernate 4.0,但不包含这个版本)并且使用XML定义映射的话,那么你需要定义Spring的org.springframework.orm.hibernate3包中的LocalSessionFactoryBean:

@Bean 
public LocalSessionFactoryBean sessionFactory(DataSource dataSource){
    LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
    sfb.setDataSource(dataSource);
    sfb.setMappingResources(new String[] { "Spitter.hbm.xml" });
    Properties props = new Properties();
    props.setProperties("dialect", "org.hibernate.dialect.H2Dialect");
    sfb.setHibernateProperties(props);
    return sfb;
}

在配置 LocalSessionFactoryBean时,我们使用了三个属性。属性dataSource装配了一个DataSource bean的引用。属性mappingResources列出了一个或多个的Hibernate映射文件,在这些文件中定义了应用程序的持久化策略。最后,hibernateProperties属性配置了Hibernate如何进行操作的细节。在本例中,我们配置Hibernate使用H2数据库并且要按照HeDialect来构建SQL。

如果你更倾向于使用注解的方式来定义持久化,并且你还没有使用Hibernate 4的话,那么需要使用AnnotationSessionFactoryBean来代替LocalSessionFactoryBean:

@Bean 
public AnnotationSessionFactoryBean sessionFactory(DataSource dataSource){
    AnnotationSessionFactoryBean sfb = new AnnotationSessionFactoryBean();
    sfb.setDataSource(dataSource);
    sfb.setPackagesToScan(new String[] { "com.habuma.spitter.domain" });
    Properties props = new Properties();
    props.setProperties("dialect", "org.hibernate.dialect.H2Dialect");
    sfb.setHibernateProperties(props);
    return sfb;
}

如果你使用Hibernate 4的话,那么就应用使用org.springframework.orm.hibernate4中的LocalSessionFactoryBean。尽管它与Hibernate 3包中的LocalSessionFactoryBean使用了相同的名称,但是Spring 3.1新引入的这个Session工厂类似于Hibernate 3中LocalSessionFactoryBean和AnnotationSessionFactoryBean的结合体。它有很多相同的属性,能够支持基于XML的映射和基于注解的映射。如下的代码展现了如何对它进行配置,使其支持基于注解的映射:

@Bean 
public LocalSessionFactoryBean sessionFactory(DataSource dataSource){
    LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
    sfb.setDataSource(dataSource);
    sfb.setPackagesToScan(new String[] { "com.habuma.spitter.domain" });
    Properties props = new Properties();
    props.setProperties("dialect", "org.hibernate.dialect.H2Dialect");
    sfb.setHibernateProperties(props);
    return sfb;
}

在这两个配置中,dataSource和hibernateProperties属性都声明了从哪里获取数据库连接以及要使用哪一种数据库。这里不再列出Hibernate配置文件,而是使用packagesToScan属性告诉Spring扫描一个或多个包以查找域类,这些类通过注解的方式表明要使用Hibernate进行持久化,这些类可以使用的注解包括JPA的@Entity或@MappedSuperclass以及Hibernate的@Entity。

如果愿意的话,还可以使用annotatedClasses属性来将应用程序中所有的持久化类以全限定名的方式明确列出:

sfb.setAnnotatedClassed(
      new Class<?> {  Spitter.class, Spittle.class }
);

annotatedClassed属性对于准确指定少量的域类是不错的选择。如果有很多的域类并且不想将其全部列出,有或者想自由地添加或移除域类而不像修改Spring配置的话,那使用packagesToScan属性是更合适的。

构建不依赖于Spring的Hibernate代码

在Spring和Hibernate的早起岁月中,编写Repository类将会涉及到使用Spring的HibernateTemplate。HibernateTemplate能够保证每个事务使用同一个Session。但是这种方式的弊端在于我们的Repository实现会直接与Spring耦合。

现在的最佳实践是不再使用HibernateTemplate,而是使用上下文Session(Contextual session)。通过这种方式,会直接将Hibernate SessionFactory装配到Repository中,并使用它来获取Session,如下面的程序清单所示。

@Repository
public class HibernateSpitterRepository implements SpitterRepository {
    private SessionFactory sessionFactory;
    
    @Autowired
    public HibernateSpitterRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    private Session currentSession() {
        return sessionFactory.getCurrentSession();
    }

    public long count(){
        return findAll().size();
    }

    public Spitter save(Spitter spitter) {
        Serializable id = currentSession().save(spitter);
        return new Spitter((Long) id,
                      spitter.getUsername(),
                      spitter.getPassword(),
                      spitter.getFirstName());
    }

    public Spitter findOne(long id) {
        return (Spitter) currentSession().get(Spitter.class, id);
    }

    public Spitter findByUsername(String username) {
        return (Spitter) currentSession()
                    .createCriteria(Spitter.class)
                    .add(Restrictions.eq("username", username))
                    .list().get(0);
    }

    public List<Spitter> findAll() {
        return (List<Spitter>) currentSession()
                     .createCriteria(Spitter.class).list();
    }
}

在以上程序清单中有几个需要注意的地方。首先,我们通过@Autowired注解让Spring自动将一个SessionFactory注入到HibernateSpitterRepository的sessionFactory属性中。接下来,在currentSession() 方法中,我们使用这个SessionFactory来获取当前事务的Session。

另外需要注意的是,我们在类上使用@Repository注解,这会为我们做两件事情。首先,@Repository是Spring的另一种构造性注解,它能够像其他注解一样被Spring的组件扫描所扫描到。这样就不必明确声明HibernateSpitterRepository bean了,只要这个Repository类在组件扫描所覆盖的包中即可。

除了帮助减少显示配置以外,@Repository还有另外一个用处。让我们会想一下模板类,它有一项任务就是捕获平台相关的异常,然后使用Spring统一非检查型异常的形式重新抛出。如果我们使用Hibernate上下文Session而不是Hibernate模板的话,为了给不使用模板的Hibernate Repository添加异常转换功能,我们只需在Spring应用上下文中添加一个PersistenceExceptionTranslationPostProcessor bean:

@Bean 
public BeanPostProcessor persistenceTranslation(){
    return new PersistenceExceptionTranslationPostProcessor();
}

PersistenceExceptionTranslationPostProcessor是一个bean后置处理器(bean post-processor),它会在所有拥有@Repository注解的类上添加一个通知器(advisor),这样就会捕获任何平台相关的异常并以Spring非检查型数据访问异常的形式重新抛出。

Spring与Java持久化API

JPA是基于POJO的持久化机制,它从Hibernate和Java数据对象(Java Data Object, JDO)上借鉴了很多理念并加入Java 5注解的特性。

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

配置实体管理器工厂

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

  • 应用程序管理类型(Application-managed):当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器更适合于不运行在Java EE容器中的独立应用程序。
  • 容器管理类型(Container-managed):实体管理器有Java EE创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或JNDI来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适用于Java EE容器,在这种情况下会希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

以上的两种实体管理器实现了同一个EntityManager接口。关键的区别不在于EntityManager本身,而是在于EntityManager的创建和管理方式。应用程序管理类型的EntityManager是由EntityManagerFactory创建的,而后者是通过PersistenceProvider的createEntityManagerFactory()方法得到的。与此相对,容器管理类型的EntityManagerFactory是通过PersistenceProvider的createContainerEntityManagerFactory()方法获得的。

如果使用的是应用程序管理类型的实体管理器,Spring承担了应用程序的角色以透明的方式处理EntityManager。在容器管理的场景下,Spring会担当容器的角色。

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

  • LocalEntityManagerFactoryBean生成应用程序管理类型的EntityManagerFactory;
  • LocalContainerEntityManagerFactoryBean生成容器管理类型的EntityManagerFactory。

需要说明的是,选择应用程序管理类型的还是容器管理类型EntityManager Factory, 对于基于Spring的应用程序来讲是完全透明的。当组合使用Spring和JPA时,处理EntityManagerFactory的复杂细节被隐藏了起来,数据访问代码只需关注它们的真正目标即可,也就是数据访问。

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

对于应用程序管理类型的实体管理器工厂来说,它绝大部分配置信息来源于一个名为persistence.xml的配置文件。这个文件必须位于类路径下的META-INF目录下。

persistence.xml的作用在于定义一个或多个持久化单元。持久化单元是同一个数据源下的一个或多个持久化类。简单来讲,persistence.xml列出了一个或多个的持久化类以及一些其他的配置如数据源和基于XML的配置文件。如下是一个典型的persistence.xml文件,它是用于Spittr应用程序的:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
      <persistence-unit name="spitterPU">
          <class>com.habuma.spittr.domain.Spitter</class>
          <class>com.habuma.spittr.domain.Spittle</class>
          <properties>
                <property name="toplink.jdbc.driver" value="org.hsqldb:hsql://localhost/spitter/spitter" />
                <property name="toplink.jdbc.url" value="jdbc:hsqldb:hsql://localhost/spitter/spitter" />
                <property name="toplink.jdbc.user" value="sa" />
                <property name="toplink.jdbc.password" value="" />
          </properties>
      </persistence-unit>
</persistence>

因为在persistence.xml文件中包含了大量的配置信息,所以在Spring中需要配置的就很少了。可以通过以下的@Bean注解方法在Spring中声明LocalEntityManagerFactoryBean:

@Bean
public LocalEntityManagerFactoryBean entityManagerFactoryBean(){
    LocalEntityManagerFactoryBean emfb = new LocalEntityManagerFactoryBean();
    emfb.setPersistenceUnitName("spitterPU");
    return emfb;
}

赋给persistenceUnitName属性的值就是persistence.xml中持久化单元的名称。

创建应用程序管理类型的EntityManagerFactory都是在persistence.xml中进行的,而这正是应用程序管理的本意。在应用程序管理的场景下(不考虑Spring时),完全由应用程序本身来负责获取EntityManagerFactory,这是通过JPA实现的PersistenceProvider做到的。如果每次请求EntityManagerFactory时都需要定义持久化单元,那代码将会迅速膨胀。通过将其配置在persistence.xml中,JPA就能够在这个特定的位置查找持久化单元定义了。

但借助于Spring对JPA的支持,我们不再需要直接处理PersistenceProvider了。因此,再将配置信息放在persistence.xml中就显得不那么明智了。实际上,这样做妨碍了我们在Spring中配置EntityManagerFactory(如果不是这样的话,我们可以提供一个Spring配置的数据源)。

鉴于以上的原因,让我们关注一下容器管理的JPA:

使用容器管理类型的JPA

容器管理的JPA采取了一个不同的方式。当运行在容器中时,可以使用容器(在我们的场景下是Spring)提供的信息来生成EntityManagerFactory。

你可以将数据源信息配置在Spring应用上下文中,而不是在persistence.xml中了。例如,如下的@Bean注解方法声明了在Spring中如何使用LocalContainerEntityManagerFactoryBean来配置容器管理类型的JPA:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
    LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
    emfb.setDataSource(dataSource);
    emfb.setJpaVendorAdapter(jpaVendorAdapter);
    return emfb;
}

这里,我们使用了Spring配置的数据源来设置dataSource属性。任何javax.sql.DataSource的实现都是可以的。尽管数据源还可以在persistence.xml中进行配置,但是这个属性指定的数据源具有更高的优先级。

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

  • EclipseLinkJpaVendorAdapter
  • HibernateJpaVendorAdapter
  • OpenJpaVendorAdapter
  • TopLinkJpaVendorAdapter(在Spring 3.1版本中,已经将其废弃了)

在本例中,我们使用Hibernate作为JPA实现,所以将其配置为HibernateJpaVendorAdapter:

@Bean
public JpaVendorAdapter jpaVendorAdapter(){
    HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
    adapter.setDatabase("HSQL");
    adapter.setShowSql(true);
    adapter.setGenerateDdl(false);
    adapter.setDatabasePlatform("org.hibernate.dialect.HSQLDialect");
    return adapter;
}

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

数据库平台 属性database的值
IBM DB2 DB2
Apache Derby DERBY
H2 H2
Hypersonic HSQL
Informix INFORMIX
Mysql MYSQL
Oracle ORACLE
PostgresQL POSTGRESQL
Microsoft SQL Server SQLSERVER
Sybase SYBASE

一些特定的动态持久化功能需要对持久化类按照指令(instrumentation)进行修改才能支持。在属性延迟加载(只在他们被实际访问时才从数据库中获取)的对象中,必须要包含知道如何查询未加载数据的代码。一些框架使用动态代理实现延迟加载,而有一些框架像JDO,则是在编译时执行类指令。

选择哪一种实体管理器工厂主要取决于如何使用它。persistence.xml文件的主要作用就在于识别持久化单元中的实体类。但是从Spring 3.1开始,我们能够在LocalContainerEntityManagerFactoryBean中直接设置packagesToScan属性:

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
    LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
    emfb.setDataSource(dataSource);
    emfb.setJpaVendorAdapter(jpaVendorAdapter);
    emfb.setPackagesToScan("com.habuma.spittr.domain");
    return emfb;
}

在这个配置中,LocalContainerEntityManagerFactoryBean会扫描com.habuma.spittr.domain包,查找带有@Entity注解的类。因此,没有必要在persistence.xml文件中进行声明了。同时,因为DataSource也是注入到LocalContainerEntityManagerFactoryBean中的,所以也没有必要在persistence.xml文件中配置数据库信息了。那么结论就是,persistence.xml文件完全没有必要存在了!

从JNDI获取实体管理器工厂

还有一件需要注意的事项,如果将Spring应用程序部署在应用服务器中,EntityManagerFactory可能已经创建好了并且位于JNDI中等待查询使用。在这种情况下,可以使用Spring jee命名空间下的<jee:jndi-lookup>元素来获取对EntityManagerFactory的引用:

<jee:jndi-lookup id="emf" jndi-name="persistence/spitterPU" />

我们也可以使用如下的Java配置来获取EntityManagerFactory:

@Bean
public JndiObjectFactoryBean entityManagerFactory() {
    JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
    jndiObjectFB.setJndiName("jdbc/SpittrDS");
    return jndiObjectFB;
}

尽管这种方法没有返回EntityManagerFactory,但是它的结果就是一个EntityManagerFactory bean。这是因为它所返回的JndiObjectFactoryBean是FactoryBean接口的实现,它能够创建EntityManagerFactory。

一旦得到EntityManagerFactory的对象,接下来就可以编写Repository了。

编写基于JPA的Repository

正如Spring对其他持久化方案的集成一样,Spring对JPA集成也提供了JpaTemplate模板以及对应的支持类JpaDapSupport。但是,为了实现更纯粹的JPA方式,基于模板的JPA已经被弃用了。这于我们在之前使用的Hibernate上下文 Session是很类似的。

鉴于纯粹的JPA方式远胜于基于模板的JPA,所以下面我们将重点关注如何构建不依赖Spring的JPA Repository。如下程序清单中JpaSpitterRepository展现了如何开发不使用Spring JpaTemplate的JPA Repository。

package com.habuma.spittr.persistence;
    import java.util.List;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.PersistenceUnit;
    import org.springframework.dao.DataAccessException;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;
    import com.habuma.spittr.domain.Spitter;
    import com.habuma.spittr.domain.Spittle;

    @Repository
    @Transactional
    public class JpaSitterRepository implements SpitterRepository {
        @PersistenceUnit
        private EntityManagerFactory emf;  // 注入EntityManagerFactory

        public void addSpitter(Spitter spitter) {
            // 创建并使用EntityManager
            emf.createEntityManager().persist(spitter);
        }

        public Spitter getSpitterById(long id) {
            return emf.createEntityManager().find(Spitter.class, id);
        }

        public void saveSpitter(Spitter spitter) {
            emf.createEntityManager().merge(spitter);
        }
     ...
    }

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

在JpaSpitterRepository中,唯一的问题在于每个方法都会调用createEntityManager()。除了引入易出错的重复代码以外,这还意味着每次调用Reposity的方法时,都会创建一个新的EntityManager。这种复杂性源于事务。

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

package com.habuma.spittr.persistence;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.habuma.spittr.domain.Spitter;
import com.habuma.spittr.domain.Spittle;

@Repository
@Transactional
public class JpaSitterRepository implements SpitterRepository {
    @PersistenceContext
    private EntityManager em;  // 注入 EntityManager
    
    public void addSpitter(Spitter spitter){
        em.persist(spitter);   // 使用EntityManager;
    }

    public Spitter getSpitterById(long id) {
        return em.find(Spitter.class, id);
    }
 
    public void saveSpitter(Spitter spitter) {
        em.merge(spitter);
    }
}

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

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

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

@Bean
public PersistenceAnnotationBeanPostProcessor paPostProcessor(){
    return new PersistenceAnnotationBeanPostPrecessor();
}

@Transactional注解表明这个Repository中的持久化方法是在事务上下文执行的。

对于@Repository注解,它的作用与开发Hibernate上下文Session版本的Repository时是一致的。由于没有使用模板类来处理异常,所以我们需要为Repository添加@Repository注解,这样PersistenceExceptionTranslationPostProcessor就会知道要将这个bean产生的异常转换为Spring的统一数据访问异常。

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

@Bean
public BeanPostProcessor persistenceTranslation() {
    return new PersistenceExceptionTranslationPostProcessor();
}

借助Spring Data实现自动化的JPA Repository

Spring Data JPA能够不再需要一遍遍地编写相同的Repository实现(比如entityManager.persist(spitter)这种样板式代码),只编写Repository接口就可以了。根本就不再需要实现类了。

例如,看一下SpitterRepository接口。

public interface SpitterRepository extends JpaRepository<Spitter, Long> {
}

此时,SpitterRepository看上去并没有什么作用。但是,它的功能远超出了表面上所看到的那样。

编写Spring Data JPA Repository的关键在于要从一组接口中挑选一个进行扩展。这里,SpitterRepository扩展了Spring Data JPA的JpaRepository。通过这种方式,JpaRepository进行了参数化,所以它就能知道这是一个用来持久化Spitter对象的Repository,并且Spitter的ID类型为Long。另外,它还会继承18个持久化操作的通用方法,如保存Spitter、删除Spitter以及根据ID查询Spitter。这18个持久化方法根本不需要编写SpitterRepository的任何实现类,相反,我们让Spring Data来为我们做这件事。我们所需要做的就是对它提出要求。

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

<?xml version="1.0" encode="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:jpa="http://www.springframework.org/schema/data/jpa"
      xsi:schemaLocation="http://www.springframework.org/schema/data/jpa
            http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">
      <jpa:repositories base-package="com.habuma.spittr.db" />
      ...
</beans>

<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包:

@Configuration
@EnableJpaRepositories(basePackages="com.habuma.spittr.db")
public class JpaConfiguration {
    ...
}

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

定义查询方法

现在,SpitterRepository需要完成的一项功能是根据给定的username查找Spitter对象。比如,我们将SpitterRepository接口修改为如下所示的样子:

public interface SpitterRepository extends JpaRepository<Spitter, Long> {
    Spitter findByUsername(String username);
}

这个新的findByUsername() 非常简单,但是足以满足我们的需求。而且,我们并不需要实现findByUsername()。方法签名已经告诉Spring Data JPA足够的信息来创建这个方法的实现了。

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

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

Repository方法是由一个动词、一个可选的主题(Subject)、关键词By以及一个断言所组成。在findByUsername()这个样例中,动词是find,断言是Username,注意并没有指定,暗含的主题是Spitter。

作为编写Repository方法名称的样例,我们参照名为readSpitterByFirstnameOrLastname()的方法,看一下方法中的各个部分是如何映射的。下图展现了这个方法是如何拆分的

Repository方法的命名遵循一种模式,有助于Spring Data生成针对数据库的查询

我们可以看到,这里的动词是read,与之前样例中的find有所差别。Spring Data允许在方法名中使用四种动词:get、read、find和count。其中,动词get、read和find是同义词,这个三个动词对应的Repository方法都会查询数据并返回。而动词count则会返回匹配对象的数量,而不是对象本身。

Repository方法的主题是可选的。它的主要目的是让你在命名方法的时候,有更多的灵活性。如果你更愿意将方法称为readSpitterByFirstnameOrLastname()而不是readByFirstnameOrLastname()的话,那么你尽可以这么做。

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

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

断言指定了限制结果集的属性。在readByFirstnameOrLastname()这个样例中,会通过firstname属性或lastname属性的值来限制结果。

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

  • IsAfter、After、IsGreaterThan、GreaterThan
  • IsGreaterThanEqual、GreaterThanEqual
  • IsBefore、Before、IsLessThan、LessThan
  • IsLessThanEqual、LessThanEqual
  • IsBetween、Between
  • IsNull、Null
  • IsNotNull、NotNull
  • IsStartingWith、StartingWith、StartsWith
  • IsContaining、Containing、Contains
  • IsLike、Like
  • IsTrue、True
  • IsFalse、False
  • Is、Equals
  • IsNot、Not

要对比的属性值就是方法的参数。完整的方法签名如下所示:

List<Spitter> readByFirstnameOrLastname(String first, String last);

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

List<Spitter> readByFirstnameIgnoringCaseOrLastnameIgnoringCase(String first, String last);

需要注意,IgnoringCase和IgnoresCase是同义的,可以随意挑选一个。

作为IgnoringCase/IgnoresCase的替代方案,我们还可以在所有条件的后面添加AllIgoringCase或AllIgoresCase,这样它就会忽略所有条件的大小写:

List<Spitter> readByFirstnameOrLastnameAllIgnoresCase(String first, String last);

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

最后,我们还可以在方法名称的结尾处添加OrderBy,实现结果集排序。例如,我们可以按照lastname属性升序排列结果集:

List<Spitter> readByFirstnameOrLastnameOrderByLastnameAsc(String first, String last);

如果要根据多个属性排序的话,只需将其依序添加到OrderBy中即可。例如,下面的样例中,首先会根据lastname升序排列,然后根据firstname属性降序排列:

List<Spitter> readByFirstnameOrLastnameOrderByLastnameAscFirstnameDesc(String first, String last);

可以看到,条件部分是通过And或者Or进行分割的。

如下展示出几个符合方法命名约定的方法签名:

  • List<Pet> findPetsByBreadIn(List<String> bead)
  • int countProductsByDiscountinuedTrue()
  • List<Order> findByShippingDateBetween(Date start, Date end)

声明自定义查询

假设我们想要创建一个Repository方法,用来查找E-mail地址是Gmail邮箱的Spitter。有一种方式就是定义一个findByEmailLike() 方法,然后每次想查找Gmail用户的时候就将“%gmail.com” 传递进来。不过更好的方案是定义一个更加便利的findAllGmailSpitters()方法,这样的话,就不用将Email地址的一部分传递进来了:

List<Spitter> findAllGmailSpitters()

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

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

@Query("select s from Spitter s where s.email like '%gmail.com' ")
List<Spitter> findAllGmailSpitters();

还有种情况,当使用方法命名约定很难表达预期的查询时,@Query注解能够发挥作用。如果按照命名约定,方法的名称特别长的时候,也可以使用这个注解。例如,考虑如下的查询:

List<Order>
    findByCustomerAddressZipCodeOrCustomerNameAndCustomerAddressState();

在这种情况下,最好使用一个较短的方法名,并使用@Query来指定该方法要如何查询数据库。

混合自定义的功能

如果你需要做的事情无法通过Spring Data JPA来实现,那就必须要在一个比Spring Data JPA更低的层级上使用JPA。我们只需在必须使用较低层级JPA的方法上,才使用这种传统的方式即可,而对于Spring Data JPA知道该如何处理的功能,我们依然可以通过它来实现。

当Spring Data JPA为Repository接口生成实现的时候,它还会查找名字与接口相同,并且添加了Impl后缀的一个类。如果这个类存在的话,Spring Data JPA将会把它的方法与Spring Data JPA所生成的方法合并在一起。对于SpitterRepository接口而言,要查找的类名为SpitterRepositoryImpl。

假设我们需要在SpitterRepository中添加一个方法,发表Spittler数量在10000及以上的Spitter将会更新为Elite状态。使用Spring Data JPA的方法命名约定或使用@Query均没有办法声明这样的方法。最为可行的方法是使用如下的eliteSweep()方法。

public class SpitterRepositoryImpl implements SpitterSweeper {

  @PersistenceContext
  private EntityManager em;

  public int eliteSweep(){
    String update = 
              "UPDATE Spitter spitter " + 
              "SET spitter.status = 'Elite' " +
              "WHERE spitter.status = 'Newbie' " + 
              "AND spitter.id IN ( " +
              "SELECT s FROM Spitter s WHERE (" + 
              " SELECT COUNT(spittles) FROM s.spittles spittles) > 10000 "  +
              ");";
    return em.createQuery(update).executeUpdate();
  }
}

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

public interface SpitterSweeper{
    int eliteSweep();
}

我们还需要确保eliteSweep()方法会被声明在SpitterRepository接口中。要实现这一点,就是修改SpitterRepository,让它扩展SpitterSweeper:

public interface SpitterRepository extends JpaRepository<Spitter, Long>,SpitterSweeper{
    ...
}

如前所述,Spring Data JPA将实现类与接口关联起来是基于接口的名称。但是Impl后缀只是默认的做法,如果你想使用其他后缀的话,只需在配置@EnableJpaRepositories的时候,设置repositoryImplementationPostfix属性即可:

@EnableJpaRepositories(basePackages="com.habuma.spittr.db",
                      repositoryImplementationPostfix="Helper")

如果在XML中使用<jap:repositories>元素来配置Spring Data JPA的话,我们可以借助repository-impl-postfix属性指定后缀:

<jap:repositories basePackages="com.habuma.spittr.db"
            repository-impl-postfix="Helper" />

我们将后缀设置称Helper,Spring Data JPA将会查找名为SpitterRepositoryHelper的类,用它来匹配SpitterRepository接口。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容