天天看点

spring-data-jpa应用详细总结

零、前言

前面jpa详解 中也说了spring-data-jpa 秉承spring的优良传统(简化java开发),在jpa的基础上进一步抽象简化,下面就说说spring是如果简化jpa的。

简化思路:

我们平时开发持久化层(dao)时,都需要写一个个接口,例如findByName(String name), findByAddress(String address),如果开发人员命名规范,我们即使不看实现内容,也能知道这个接口在干什么。

以此思路,spring-data-jpa的愿景:

开发人员写一个接口,按照规范命名接口名,然后就直接可以使用。

这就是spring-data-jpa设计人员的核心想法。

而要使用spring-data-jpa,需要下面几个步骤:

  1. 添加依赖
  2. 配置
  3. 继承Repository接口

一、添加依赖

<!-- spring test -->
	<dependency>  
	    <groupId>org.springframework</groupId>  
	    <artifactId>spring-test</artifactId>  
	    <version>${spring.version}</version>
	</dependency>
	
	<!-- spring data jpa -->
	<dependency>
		<groupId>org.springframework.data</groupId>
		<artifactId>spring-data-jpa</artifactId>
		<version>1.9.6.RELEASE</version>
	</dependency>
           

小结:

  1. 使用spring-data-jpa,需要引入spring-data-jpa依赖包
  2. 使用spring的测试框架可以方便的完成本文所需要的demo示例

二、配置applicationContext.xml

使用spring的框架,就需要配置spring的默认配置文件

<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />

<!--使用spring data jpa的配置,需要在beans中 增加对jpa命名空间的引用-->
<jpa:repositories 
		base-package="com.yc"
		entity-manager-factory-ref="entityManagerFactory"
		transaction-manager-ref="transactionManager">
</jpa:repositories>

<!--实体管理器配置-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
</bean>

<!--事务管理器配置-->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
	<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
           

三个关键配置的关系如下:

spring-data-jpa应用详细总结

小结:

  1. entityManagerFactory:配置负责管理数据库连接以及实体类管理。上面是最简化的配置,没有写实体管理器是如何关联数据库的,其实是默认去找classpath*:META-INF/persistence.xml这个文件。entityManagerFactory提供了灵活的配置属性,persistence.xml中的配置都可以写到这里。
  2. transactionManager:事务管理器。
  3. jpa:repositories是最关键的一个声明配置,有了这句配置,spring就会到指定目录下为继承spring-data-jpa接口的接口创建代理对象。

三、继承Repository接口

使用spring-data-jpa的方式就是让持久层(dao)接口继承Repository接口,以此来免去写实现类的麻烦。

dao接口示例代码如下:

public interface StudentDao extends Repository<Student, Integer> {
	
	List<Student> findByNameAndTel(String name, String tel);

}
           

小结:

  1. Repository接口需要使用泛型,第一个是该接口的域对象类型(也就是要查询的表对应的实体类),第二个是该域对象的主键类型。
  2. 在dao接口中声明方法,例如上面的findByNameAndTel,后面的参数要和方法名对应。
  3. 这样一个只有接口的dao层方法就可以使用了,实现的逻辑就是从Student表中按照name和tel查询所有的数据。

四、测试

以上面的StudentDao为例,测试代码如下:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration("/applicationContext.xml") 
public class SpringJpaTest {

	@Autowired 
    private StudentDao studentDao;
	
	@Test
	public void testMethod() {
		List<Student> list1 = studentDao.findByNameAndTel("xxx", "xxxx");
		System.out.println(list1.get(0).getId());
	}

}
           

小结:

  1. @ContextConfiguration表示加载配置文件
  2. @Autowired注入dao层接口类
  3. 和直接使用junit一样对@Test方法进行执行

五、详解

上面是spring-data-jpa主推的语法,但也不仅限于这一种写法,毕竟业务场景多样,这一种写法是无法完成复杂的业务逻辑的,下面详细说明,spring-data-jpa支持的几种书写方式。

自定义接口名

Repository接口中没有任何方法,数据库操作的方式就是直接写一个意图明确的规范接口名,下面是示例代码:

public interface StudentPepositoryDao extends Repository<Student, Integer> {

	/**
	 * 新增数据<p>
	 * 固定这么写,别想把save改成其他名字,框架还没智能到可以猜你的心思;<p>
	 * 返回值就是插入的对象类型(改成其他类型,虽然不影响插入数据,但会报格式转换错误)
	 * @param stu
	 * @return
	 */
	Student save(Student stu);
	
	/**
	 * 批量新增数据<p>
	 * 固定写法,不要把参数改成其他类型,如List<Student>也是会报错的
	 * @param stuList
	 * @return
	 */
	Iterable<Student> save(Iterable<Student> stuList);
	
	/**
	 * 根据id删除对象<p>
	 * 删除接口没有返回值
	 * @param id
	 */
	void delete(Integer id);
	
	/**
	 * 根据对象删除对象
	 * @param stu
	 */
	void delete(Student stu);
	
	/**
	 * 批量删除<p>
	 * 还是固定写法,不要把参数改成其他类型,如List<Student>也是会报错的
	 * @param stuList
	 */
	void delete(Iterable<Student> stuList);
	
	//错误写法
//	void deleteByName(String name);
	
	/**
	 * 查询并排序
	 * @param sort
	 * @return
	 */
	List<Student> findAll(Sort sort);
	
	/**
	 * 分页查询
	 * @param pageable
	 * @return
	 */
	Page<Student> findAll(Pageable pageable);
	
	/**
	 * 普通分页查询
	 * @param pageable
	 * @return
	 */
	Page<Student> findByName(String name, Pageable pageable);
	
}
           

测试代码:

@Autowired 
    private StudentPepositoryDao studentPepositoryDao;
	
	@Test
	public void testMethod() {
		List<Student> list1 = studentDao.findByNameAndTel("saveTest", "111");
		System.out.println(list1.get(0).getId());
	}
	
	@Test
	public void saveTest1() {
		Student sd = new Student();
		sd.setName("saveTest");
		sd.setTel("111");
		//神奇吧,只用一个空借口就能插入数据,连事务注解都可以不写
		Student saveEntity = studentPepositoryDao.save(sd);
		System.out.println("id=" + saveEntity.getId());
	}
	
	
	@Test
	public void deleteTest1() {
		studentPepositoryDao.delete(20);
	}
	
	/**
	 * 修改,用的还是save方法<p>
	 * 因为spring-data-jpa基于jpa,在jpa中就是通过修改托管态的值来达到同步修改数据库的目的。
	 * 所以这里也是同样的道理,不要试图new一个对象,然后自己添加一个id,然后去save
	 */
	@Test
	public void updateTest() {
		List<Student> list1 = studentDao.findByNameAndTel("saveTest2", "111-1");//托管态
		Student updateEntity = list1.get(0);
		updateEntity.setName("saveTest2-update");
		studentPepositoryDao.save(updateEntity);
		
	}
	
	/**
	 * 目的:根据字段名删除
	 * 这样的写法是会报错的,因为spring-data-jpa没有“内置”
	 */
	@Test
	public void deleteTest() {
//		studentPepositoryDao.deleteByName("saveTest2");
	}
	
	/**
	 * 简单排序查询
	 */
	@Test
	public void simpleSortFindTest() {
		Sort sort = new Sort(Sort.Direction.DESC,"id");
		List<Student> res = studentPepositoryDao.findAll(sort);
		for (Student std : res) {
			System.out.println(std.getId() + " -- " + std.getName());
		}
	}
           

小结:

使用Repository,有两种方法:

  1. 固定写法的方法,包括增(save)、删(delete)、改(save),查询所有(findAll),删除所有(deleteAll),其他没有列举的就是不能随便写的方法名
  2. 固定书写规则的方法,就是各种个性化的查询,根据规则写方法名,就可以自定义查询条件

使用扩展接口

spring-data-jpa还提供了一些其他接口可供使用。这些接口也都是继承自Repository这个核心接口。

CrudRepository:继承Repository,实现了一组CRUD相关的方法。

PagingAndSortingRepository:继承CrudRepository,实现了一组分页排序相关的方法。

JpaRepository:继承PagingAndSortingRepository,实现一组JPA规范相关的方法。

JpaSpecificationExecutor:比较特殊,不属于Repository体系,实现一组JPA Criteria查询相关的方法。

CrudRepository

查看CrudRepository接口源码,可以看到包含一些常用的增删改查方法:

<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
           

是不是很眼熟————就是前面我们使用Repository写的那些方法。

还有下面的PagingAndSortingRepository接口。

PagingAndSortingRepository

包含的方法:

//查询所有实体类并排序
Iterable<T> findAll(Sort sort);
//分页查询
Page<T> findAll(Pageable pageable);
           

小结:

有人可能有疑问:既然其他接口功能这么强大,Repository接口方法又是各种限制,方法名也不能乱写,是不是Repository接口就没有用处了? 当然不是,这里有一个接口安全的问题:如果有一张表的数据是不能修改的,对应dao层接口还是继承了CrudRepository,那么该表的数据就有被篡改的风险。所以,需要对外暴露多少接口方法,还是看情况而定。

Jpql

jpal是jpa中的一个重要概念,可以灵活的控制一些复杂语句。在spring-data-jpa中也可以使用,完成一些纯粹使用Repository接口无法完成的任务。比如 前面说到一个写法deleteByName,是会报错的,因为Repository无法领会我们的接口名的意义。Repository结合jpql就完成根据条件删除更新对象。

先看看基本写法

public interface StudentJpqlDao extends Repository<Student, Integer> {

	@Query(value = "select d from Student d where d.name=:name")
	List<Student> getStudentByName(@Param("name")String name);
           

测试代码

@Test
	public void simpleJpql() {
		List<Student> list = studentJpqlDao.getStudentByName("mary");
		for (Student std : list) {
			System.out.println(std.getId() + " -- " + std.getName());
		}
	}
           

删除、修改写法如下

/**
	 * 两个注解不能少:
	 * @Modifying
	 * @Transactional
	 * @param name
	 */
	@Transactional
	@Modifying
	@Query(value = "delete from Student d where d.name=:name")
	void deleteByName(@Param("name")String name);
	
	/**
	 * 删除、更新 两个注解不能少:
	 * @Modifying
	 * @Transactional
	 */
	@Transactional
	@Modifying
	@Query(value = "update Student d set d.name=:name where d.id=:id")
	void updateByName(@Param("id")Integer id, @Param("name")String name);
           

小结:

在实现Repository(或其子接口)中,使用注解@Query,在注解value属性中写正常的jpql语句。需要注意的动态绑定参数的两种方式:

  1. 序号型
@Query(value = "select d from Student d where d.name=?1 and d.tel=?2")
           

? + 数字

,表示参数列表里的 第一个参数、第二个参数

2. 参数名绑定

也就是上面实例所示的,使用

: + 参数名

。需要注意的是参数列表里对应的要用注解@Param(“参数名”)来配合使用,一般推荐这种写法,不容易因为改动参数而绑定错误。

3. 删除、修改需要增加注解@Modifying和@Transactional

4. 就像之前说jpql一样,jpql在spring-data-jpa里也无法完成新增功能

静态jpql

在讲jpa时也就说了,可以在实体类上用注解@NamedQuery写“全局通用”的查询方法,在spring-data-jpa这里也是一样的。

就像我们写程序不建议大量使用全局变量一样,这里也不建议用这种方式,因为会让实体类承载过多内容,可能因为命名冲突引起不易排查的bug。

创建查询顺序

既然有这么多规则,那么当我们写了一个接口时,spring-data-jpa是如何判断我们用的是什么规则。

Spring Data JPA 在为接口创建代理对象时,如果发现同时存在多种上述情况可用,它该优先采用哪种策略呢?为此,jpa:repositories 提供了 query-lookup-strategy 属性,用以指定查找的顺序。它有如下三个取值:

  1. create — 通过解析方法名字来创建查询。即使有符合的命名查询,或者方法通过 @Query 指定的查询语句,都将会被忽略。
  2. create-if-not-found — 如果方法通过 @Query 指定了查询语句,则使用该语句实现查询;如果没有,则查找是否定义了符合条件的命名查询,如果找到,则使用该命名查询;如果两者都没有找到,则通过解析方法名字来创建查询。这是 query-lookup-strategy 属性的默认值。
  3. use-declared-query — 如果方法通过 @Query 指定了查询语句,则使用该语句实现查询;如果没有,则查找是否定义了符合条件的命名查询,如果找到,则使用该命名查询;如果两者都没有找到,则抛出异常。

六、应用总结

和原生jpa相比,除了省去很多重复的代码工作量,spring-data-jpa的“面向接口编程”还具有代码集中易维护的优势,层级分明,一旦出现某个共性错误,集中改动也方便。不像原生jpa(或hibernate),servies层穿插各种数据库操作语句。所以个人建议大部分数据库操作使用spring-data-jpa,有特殊的复杂业务场景才去使用原生jpa方法(比如需要精细控制事务提交)。不到万不得已不去用原生sql,因为既然我们用orm框架就是获得了orm屏蔽数据库的优势,一旦使用了原生sql,就失去了这一大优势。 而且一个项目如果使用的技术过于杂,也增加了其他人的维护难度和学习成本。

七、示例代码下载

码云

八、参考链接

Hibernate和Spring Data JPA的区别

使用Spring Data JPA 简化JPA开发

一步步实现:JPA的基本增删改查CRUD(jpa基于hibernate)

JPA简单使用

易百教程

JPA和Hibernate的关系

JPA之常用 基本注解

JPA EntityManager的四个主要方法 ——persist,merge,refresh和remove

Spring Data Jpa 实体状态分析

Spring Transaction 使用

JPA和事务管理

事务属性之7种传播行为

JPA缓存

Spring中Transactional注解

JAP JPQL相关语法

详解JPA 2.0动态查询机制:Criteria API