对于不是使用spring管理的项目,咱们就本身建立对象使用:大概的思路就是①建立dao接口②实现该接口,而且编写逻辑:java
Dao:web
public interface StudentDao { public List<Student> findAll(); }
由于是测试,因此咱们就只定义了一个查询全部记录的方法,下面是实现:spring
public class StudentDaoImpl implements StudentDao{ @Override @SuppressWarnings("unchecked") public List<Student> findAll() { JPAConnection jc=new JPAConnection("jpa-1"); EntityManager em=jc.getEntityManager(); String sql="select s from Student s"; Query query=em.createQuery(sql); List<Student> students=query.getResultList(); if(students==null){ students=Lists.newArrayList(); } jc.destory(); return students; } }
,由于建立EntityManager会重复不少的代码:因此我就将这些东西给抽出来了sql
public class JPAConnection { private EntityManagerFactory entityManagerFactory; private EntityManager entityManager; private EntityTransaction entityTransaction; public JPAConnection(){ } public JPAConnection(String unitName) { this.entityManagerFactory=Persistence.createEntityManagerFactory(unitName); this.entityManager=entityManagerFactory.createEntityManager(); this.entityTransaction=entityManager.getTransaction(); entityTransaction.begin(); } /** Getter And Setter **/ public void destory(){ if(this.entityTransaction.isActive()){ this.entityTransaction.commit(); } if(this.entityManager.isOpen()){ this.entityManager.close(); } if(this.entityManagerFactory.isOpen()){ this.entityManagerFactory.close(); } } }
上面虽然是简单的实现了查询的功能,可是对于真正实际的项目仍是远远不够的,对于真是项目中,还要考虑事务等等,这里的代码只是简单的给个jpa实现的思路。数组
Spring管理的项目:框架
对于上面的代码,以为最多余的就是建立EntityManager这部分了,因此固然会使用spring的特性减小这些建立的代码了,同时操做事务也变得很简单了,只要添加一些注解就能够了:dom
下面是spring的配置文件:ide
<context:component-scan base-package="com.hotusm.common"/> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManager"/> </bean> <bean id="entityManager" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"/>
以后代码就变成这样了:post
@Repository("userDao") public class UserDaoImpl implements UserDao{ @PersistenceContext private EntityManager em; @Transactional @Override @SuppressWarnings("unchecked") public List<User> findAll() { String sql="select o from User o"; Query query=em.createQuery(sql); List<User> users=query.getResultList(); if(users==null){ users=Lists.newArrayList(); } return users; } }
是否是超级爽快?,若是是web项目,在使用的时候,在service中直接注入进去就能够直接使用,同时使用注解@Transactional便可进行事务操做了。测试
下面总结一下使用 Spring Data JPA 进行持久层开发大体须要的三个步骤:
1.声明持久层的接口,该接口继承 Repository,Repository 是一个标记型接口,它不包含任何方法,固然若是有须要,Spring Data 也提供了若干 Repository 子接口,其中定义了一些经常使用的增删改查,以及分页相关的方法。
2.在接口中声明须要的业务方法。Spring Data 将根据给定的策略(具体策略稍后讲解)来为其生成实现代码。
3.在 Spring 配置文件中增长一行声明,让 Spring 为声明的接口建立代理对象。配置了 <jpa:repositories> 后,Spring 初始化容器时将会扫描 base-package 指定的包目录及其子目录,为继承 Repository 或其子接口的接口建立代理对象,并将代理对象注册为 Spring Bean,业务层即可以经过 Spring 自动封装的特性来直接使用该对象。
此外,<jpa:repository> 还提供了一些属性和子标签,便于作更细粒度的控制。能够在 <jpa:repository> 内部使用 <context:include-filter>、<context:exclude-filter> 来过滤掉一些不但愿被扫描到的接口。具体的使用方法见 Spring参考文档。
应该继承哪一个接口?
前面提到,持久层接口继承 Repository 并非惟一选择。Repository 接口是 Spring Data 的一个核心接口,它不提供任何方法,开发者须要在本身定义的接口中声明须要的方法。与继承 Repository 等价的一种方式,就是在持久层接口上使用 @RepositoryDefinition 注解,并为其指定 domainClass 和 idClass 属性。以下两种方式是彻底等价的:
清单 15. 两种等价的继承接口方式示例
public interface UserDao extends Repository<AccountInfo, Long> { …… } @RepositoryDefinition(domainClass = AccountInfo.class, idClass = Long.class) public interface UserDao { …… }
若是持久层接口较多,且每个接口都须要声明类似的增删改查方法,直接继承 Repository 就显得有些啰嗦,这时能够继承 CrudRepository,它会自动为域对象建立增删改查方法,供业务层直接使用。开发者只是多写了 "Crud" 四个字母,即刻便为域对象提供了开箱即用的十个增删改查方法。
可是,使用 CrudRepository 也有反作用,它可能暴露了你不但愿暴露给业务层的方法。好比某些接口你只但愿提供增长的操做而不但愿提供删除的方法。针对这种状况,开发者只能退回到 Repository 接口,而后到 CrudRepository 中把但愿保留的方法声明复制到自定义的接口中便可。
分页查询和排序是持久层经常使用的功能,Spring Data 为此提供了 PagingAndSortingRepository 接口,它继承自 CrudRepository 接口,在 CrudRepository 基础上新增了两个与分页有关的方法。可是,咱们不多会将自定义的持久层接口直接继承自 PagingAndSortingRepository,而是在继承 Repository 或 CrudRepository 的基础上,在本身声明的方法参数列表最后增长一个 Pageable 或 Sort 类型的参数,用于指定分页或排序信息便可,这比直接使用 PagingAndSortingRepository 提供了更大的灵活性。
JpaRepository 是继承自 PagingAndSortingRepository 的针对 JPA 技术提供的接口,它在父接口的基础上,提供了其余一些方法,好比 flush(),saveAndFlush(),deleteInBatch() 等。若是有这样的需求,则能够继承该接口。
上述四个接口,开发者到底该如何选择?其实依据很简单,根据具体的业务需求,选择其中之一。笔者建议在一般状况下优先选择 Repository 接口。由于 Repository 接口已经能知足平常需求,其余接口能作到的在 Repository 中也能作到,彼此之间并不存在功能强弱的问题。只是 Repository 须要显示声明须要的方法,而其余则可能已经提供了相关的方法,不须要再显式声明,但若是对 Spring Data JPA 不熟悉,别人在检视代码或者接手相关代码时会有疑惑,他们不明白为何明明在持久层接口中声明了三个方法,而在业务层使用该接口时,却发现有七八个方法可用,从这个角度而言,应该优先考虑使用 Repository 接口。
前面提到,Spring Data JPA 在后台为持久层接口建立代理对象时,会解析方法名字,并实现相应的功能。除了经过方法名字之外,它还能够经过以下两种方式指定查询语句:
Spring Data JPA 能够访问 JPA 命名查询语句。开发者只须要在定义命名查询语句时,为其指定一个符合给定格式的名字,Spring Data JPA 便会在建立代理对象时,使用该命名查询语句来实现其功能。
开发者还能够直接在声明的方法上面使用 @Query 注解,并提供一个查询语句做为参数,Spring Data JPA 在建立代理对象时,便以提供的查询语句来实现其功能。
下面咱们分别讲述三种建立查询的方式。
经过解析方法名建立查询
经过前面的例子,读者基本上对解析方法名建立查询的方式有了一个大体的了解,这也是 Spring Data JPA 吸引开发者的一个很重要的因素。该功能其实并不是 Spring Data JPA 独创,而是源自一个开源的 JPA 框架 Hades,该框架的做者 Oliver Gierke 自己又是 Spring Data JPA 项目的 Leader,因此把 Hades 的优点引入到 Spring Data JPA 也就是瓜熟蒂落的了。
框架在进行方法名解析时,会先把方法名多余的前缀截取掉,好比 find、findBy、read、readBy、get、getBy,而后对剩下部分进行解析。而且若是方法的最后一个参数是 Sort 或者 Pageable 类型,也会提取相关的信息,以便按规则进行排序或者分页查询。
在建立查询时,咱们经过在方法名中使用属性名称来表达,好比 findByUserAddressZip ()。框架在解析该方法时,首先剔除 findBy,而后对剩下的属性进行解析,详细规则以下(此处假设该方法针对的域对象为 AccountInfo 类型):
先判断 userAddressZip (根据 POJO 规范,首字母变为小写,下同)是否为 AccountInfo 的一个属性,若是是,则表示根据该属性进行查询;若是没有该属性,继续第二步;
从右往左截取第一个大写字母开头的字符串(此处为 Zip),而后检查剩下的字符串是否为 AccountInfo 的一个属性,若是是,则表示根据该属性进行查询;若是没有该属性,则重复第二步,继续从右往左截取;最后假设 user 为 AccountInfo 的一个属性;
接着处理剩下部分( AddressZip ),先判断 user 所对应的类型是否有 addressZip 属性,若是有,则表示该方法最终是根据 "AccountInfo.user.addressZip" 的取值进行查询;不然继续按照步骤 2 的规则从右往左截取,最终表示根据 "AccountInfo.user.address.zip" 的值进行查询。
可能会存在一种特殊状况,好比 AccountInfo 包含一个 user 的属性,也有一个 userAddress 属性,此时会存在混淆。读者能够明确在属性之间加上 "_" 以显式表达意图,好比 "findByUser_AddressZip()" 或者 "findByUserAddress_Zip()"。
在查询时,一般须要同时根据多个属性进行查询,且查询的条件也格式各样(大于某个值、在某个范围等等),Spring Data JPA 为此提供了一些表达条件查询的关键字,大体以下:
And --- 等价于 SQL 中的 and 关键字,好比 findByUsernameAndPassword(String user, Striang pwd);
Or --- 等价于 SQL 中的 or 关键字,好比 findByUsernameOrAddress(String user, String addr);
Between --- 等价于 SQL 中的 between 关键字,好比 findBySalaryBetween(int max, int min);
LessThan --- 等价于 SQL 中的 "<",好比 findBySalaryLessThan(int max);
GreaterThan --- 等价于 SQL 中的">",好比 findBySalaryGreaterThan(int min);
IsNull --- 等价于 SQL 中的 "is null",好比 findByUsernameIsNull();
IsNotNull --- 等价于 SQL 中的 "is not null",好比 findByUsernameIsNotNull();
NotNull --- 与 IsNotNull 等价;
Like --- 等价于 SQL 中的 "like",好比 findByUsernameLike(String user);
NotLike --- 等价于 SQL 中的 "not like",好比 findByUsernameNotLike(String user);
OrderBy --- 等价于 SQL 中的 "order by",好比 findByUsernameOrderBySalaryAsc(String user);
Not --- 等价于 SQL 中的 "! =",好比 findByUsernameNot(String user);
In --- 等价于 SQL 中的 "in",好比 findByUsernameIn(Collection<String> userList) ,方法的参数能够是 Collection 类型,也能够是数组或者不定长参数;
NotIn --- 等价于 SQL 中的 "not in",好比 findByUsernameNotIn(Collection<String> userList) ,方法的参数能够是 Collection 类型,也能够是数组或者不定长参数;
使用 @Query 建立查询
@Query 注解的使用很是简单,只需在声明的方法上面标注该注解,同时提供一个 JP QL 查询语句便可,以下所示:
清单 16. 使用 @Query 提供自定义查询语句示例
public interface UserDao extends Repository<AccountInfo, Long> { @Query("select a from AccountInfo a where a.accountId = ?1") public AccountInfo findByAccountId(Long accountId); @Query("select a from AccountInfo a where a.balance > ?1") public Page<AccountInfo> findByBalanceGreaterThan( Integer balance,Pageable pageable); }
不少开发者在建立 JP QL 时喜欢使用命名参数来代替位置编号,@Query 也对此提供了支持。JP QL 语句中经过": 变量"的格式来指定参数,同时在方法的参数前面使用 @Param 将方法参数与 JP QL 中的命名参数对应,示例以下:
清单 17. @Query 支持命名参数示例
public interface UserDao extends Repository<AccountInfo, Long> { public AccountInfo save(AccountInfo accountInfo); @Query("from AccountInfo a where a.accountId = :id") public AccountInfo findByAccountId(@Param("id")Long accountId); @Query("from AccountInfo a where a.balance > :balance") public Page<AccountInfo> findByBalanceGreaterThan( @Param("balance")Integer balance,Pageable pageable); }
此外,开发者也能够经过使用 @Query 来执行一个更新操做,为此,咱们须要在使用 @Query 的同时,用 @Modifying 来将该操做标识为修改查询,这样框架最终会生成一个更新的操做,而非查询。以下所示:
清单 18. 使用 @Modifying 将查询标识为修改查询
@Modifying @Query("update AccountInfo a set a.salary = ?1 where a.salary < ?2") public int increaseSalary(int after, int before);
经过调用 JPA 命名查询语句建立查询
命名查询是 JPA 提供的一种将查询语句从方法体中独立出来,以供多个方法共用的功能。Spring Data JPA 对命名查询也提供了很好的支持。用户只须要按照 JPA 规范在 orm.xml 文件或者在代码中使用 @NamedQuery(或 @NamedNativeQuery)定义好查询语句,惟一要作的就是为该语句命名时,须要知足”DomainClass.methodName()”的命名规则。假设定义了以下接口:
清单 19. 使用 JPA 命名查询时,声明接口及方法时不须要什么特殊处理
public interface UserDao extends Repository<AccountInfo, Long> { ...... public List<AccountInfo> findTop5(); }
若是但愿为 findTop5() 建立命名查询,并与之关联,咱们只须要在适当的位置定义命名查询语句,并将其命名为 "AccountInfo.findTop5",框架在建立代理类的过程当中,解析到该方法时,优先查找名为 "AccountInfo.findTop5" 的命名查询定义,若是没有找到,则尝试解析方法名,根据方法名字建立查询。
建立查询的顺序
Spring Data JPA 在为接口建立代理对象时,若是发现同时存在多种上述状况可用,它该优先采用哪一种策略呢?为此,<jpa:repositories> 提供了 query-lookup-strategy 属性,用以指定查找的顺序。它有以下三个取值:
create --- 经过解析方法名字来建立查询。即便有符合的命名查询,或者方法经过 @Query 指定的查询语句,都将会被忽略。
create-if-not-found --- 若是方法经过 @Query 指定了查询语句,则使用该语句实现查询;若是没有,则查找是否认义了符合条件的命名查询,若是找到,则使用该命名查询;若是二者都没有找到,则经过解析方法名字来建立查询。这是 query-lookup-strategy 属性的默认值。
use-declared-query --- 若是方法经过 @Query 指定了查询语句,则使用该语句实现查询;若是没有,则查找是否认义了符合条件的命名查询,若是找到,则使用该命名查询;若是二者都没有找到,则抛出异常。
Spring Data JPA 对事务的支持
默认状况下,Spring Data JPA 实现的方法都是使用事务的。针对查询类型的方法,其等价于 @Transactional(readOnly=true);增删改类型的方法,等价于 @Transactional。能够看出,除了将查询的方法设为只读事务外,其余事务属性均采用默认值。
若是用户以为有必要,能够在接口方法上使用 @Transactional 显式指定事务属性,该值覆盖 Spring Data JPA 提供的默认值。同时,开发者也能够在业务层方法上使用 @Transactional 指定事务属性,这主要针对一个业务层方法屡次调用持久层方法的状况。持久层的事务会根据设置的事务传播行为来决定是挂起业务层事务仍是加入业务层的事务。具体 @Transactional 的使用,请参考 Spring的参考文档。
为接口中的部分方法提供自定义实现
有些时候,开发者可能须要在某些方法中作一些特殊的处理,此时自动生成的代理对象不能彻底知足要求。为了享受 Spring Data JPA 带给咱们的便利,同时又可以为部分方法提供自定义实现,咱们能够采用以下的方法:
将须要开发者手动实现的方法从持久层接口(假设为 AccountDao )中抽取出来,独立成一个新的接口(假设为 AccountDaoPlus ),并让 AccountDao 继承 AccountDaoPlus;
为 AccountDaoPlus 提供自定义实现(假设为 AccountDaoPlusImpl );
将 AccountDaoPlusImpl 配置为 Spring Bean;
在 <jpa:repositories> 中按清单 19 的方式进行配置。
清单 20. 指定自定义实现类
<jpa:repositories base-package="footmark.springdata.jpa.dao"> <jpa:repository id="accountDao" repository-impl-ref=" accountDaoPlus " /> </jpa:repositories> <bean id="accountDaoPlus" class="......."/>
此外,<jpa:repositories > 提供了一个 repository-impl-postfix 属性,用以指定实现类的后缀。假设作了以下配置:清单 21. 设置自动查找时默认的自定义实现类命名规则 <jpa:repositories base-package="footmark.springdata.jpa.dao" repository-impl-postfix="Impl"/>则在框架扫描到 AccountDao 接口时,它将尝试在相同的包目录下查找 AccountDaoImpl.java,若是找到,便将其中的实现方法做为最终生成的代理类中相应方法的实现。