Spring Data JPA学习笔记

Spring Data学习笔记

最近看了这个教程学习SpringData技术。老师开头演示了传统的JDBC使用方法,非常的繁琐,需要在逻辑代码中进行配置。每一次操作数据库都要通过建立连接、执行查询、释放资源三部重复的代码,还要写一个JDBCUtil工具类来整合数据库连接和关闭的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* JDBC工具类:
* 1) 获取Connection
* 2) 释放资源
*/
public class JDBCUtil {
/**
* 获取Connection
* @return 所获得到的JDBC的Connection
*/
public static Connection getConnection() throws Exception {

/**
* 不建议大家把配置硬编码到代码中
*
* 最佳实践:配置性的建议写到配置文件中
*/
// String url = "jdbc:mysql:///spring_data";
// String user = "root";
// String password = "root";
// String driverClass = "com.mysql.jdbc.Driver";

InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
properties.load(inputStream);


String url = properties.getProperty("jdbc.url");
String user = properties.getProperty("jdbc.user");
String password = properties.getProperty("jdbc.password");
String driverClass = properties.getProperty("jdbc.driverClass");

Class.forName(driverClass);
Connection connection = DriverManager.getConnection(url, user, password);
return connection;
}

/**
* 释放DB相关的资源
* @param resultSet
* @param statement
* @param connection
*/
public static void release(ResultSet resultSet,
Statement statement, Connection connection){

if(resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

if(statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}


if(connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

}



}

为了避免这样繁琐的配置操作,Spring框架内置的 JdbcTemplate模板来了,它将数据的配置独立到了.xml文件中,和逻辑代码独立开来,很大程度简化了代码,助力开发:

  • 添加Maven依赖(搜索Maven Repository,在Maven仓库中找到需要的依赖)
    这里需要添加两个依赖:spring-jdbc&spring-context?
  • 配置beans.xml:DataSource&JdbcTemplate
  • 开发spring jdbc版本的query和save方法
  • 写Test Case

在Spring中,我们将beans.xml配置文件放在Resources文件夹下,这里截取部分主要内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
<property name="url" value="jdbc:mysql:///spring_data"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="studentDAO" class="com.imooc.dao.StudentDAOSpringJdbcImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
</beans>

上面是视频中的代码,由于各种更新,我在《精通Spring 4.x》中学习的下面的代码也是可行的,原理逻辑是一样的,自己对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 扫描类包,将标注Spring注解的类自动转化Bean,同时完成Bean的注入 -->
<context:component-scan base-package="com.smart.dao"/>
<context:component-scan base-package="com.smart.service"/>

<!-- 配置数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/sampledb"
p:username="root"
p:password="123456"/>

<!-- 配置Jdbc模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource"/>

<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>

总结弊端

  • DAO has many many code
  • DAOImpl has many duplicate code
  • Develop the page and other functions对于开发分页和开发其他方法工作量很大

Spring Data JPA

PS:这部分的总结参考这里
SpringData JPA只是SpringData中的一个子模块
JPA是一套标准接口,而Hibernate是JPA的实现
SpringData JPA 底层默认实现是使用Hibernate

(Hibernate是什么呢?Hibernate是一种Java语言下的对象关系映射(ORM)解决方案。是一种ORM框架,全称为 Object_Relative DateBase-Mapping,在Java对象与关系数据库之间建立某种映射,以实现直接存取Java对象!通俗点说,就是Hibernate能将一个领域对象绑定到数据库的一张表上,无论你是想新建表还是CRUD这张表的数据,你只需要对这个类进行操作就可以了,而不需要直接去操作数据库,教程中的实例就是雇员: 先开发实体类===>自动生成对应的数据表)

开发环境搭建

首先我们要写配置文件,也就是POM和resources里面的.xml文件。这部分我踩了个坑,我根据教程写好POM配置以后,并没有办法测试SpringDataTest。报错信息第一句是:

1
java.sql.SQLException: Unknown system variable 'query_cache_size'

感谢isxuran给我提供的解决方案,原因是mysql-connecter-java的版本过低,很显然是数据库驱动程序与数据库版本不对应。所以我修改了mysql-connector-java的依赖版本,由教程中的5.1.38改成了5.1.6。为什么选择5.1.6呢,我只是在maven repository里网上找了一个使用率较高的进行尝试,没有报错,问题解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.ting</groupId>
<artifactId>springdataPratice</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>


<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
</dependency>

<!--spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

<!--spring data jpa-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.8.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.6.Final</version>
</dependency>

</dependencies>


</project>

beans.xml中主要配置了数据源dataSource、Hibernate会用到的EntityManagerFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!--1 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring_data"/>
</bean>

<!--2 配置EntityManagerFactory-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
</property>
<property name="packagesToScan" value="com.ting"/>

<property name="jpaProperties">
<props>
<!--命名策略-->
<prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
<!--使用的方言,这里我们用的是Mysql-->
<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
<!--查询时是否显示sql语句-->
<prop key="hibernate.show_sql">true</prop>
<!--是否格式化,这里选择格式化的好处是控制台输出的Hibernate语句会分行清晰的显示,否则会一行到底看不清楚,下文附对比图-->
<prop key="hibernate.format_sql">true</prop>
<!--自动创建领域对象对应的数据表-->
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>

</bean>

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

<!--4 配置支持注解的事务-->
<tx:annotation-driven transaction-manager="transactionManager"/>

<!--5 配置spring data-->
<jpa:repositories base-package="com.ting" entity-manager-factory-ref="entityManagerFactory"/>

<!--能够自动扫描指定包中的文件-->
<context:component-scan base-package="com.ting"/>

</beans>

Hibernate格式化与不格式化对比图(绿色框内的是格式化后的输出):

接着我们只需要写好domain object和测试类,进行单元测试即可:

单元测试通过后我们会发现数据库中已经新建了一个employee表:

然后我们再修改一下beans.xml中的事务管理配置、实现repository接口即可。

Spring Data JPA进阶

  1. Repository类的定义

    1
    2
    public interface Repository<T, ID extends Serializable> {
    }
    1
    2
    3
    4
    5
    6
    7
    import com.ting.domain.Employee;
    import org.springframework.data.repository.Repository;

    public interface EmployeeRepository extends Repository<Employee,Integer> {

    public Employee findByName(String name);
    }

    1) Repository是一个空接口,这种接口称为标记接口
    没有包含方法声明的接口
    2) 如果我们定义的接口EmployeeRepository继承了核心的接口Repository,那么就是告诉了Spring来管理我们的接口。假如我们没有继承的会,运行就会报错:

    1
    org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.ting.repository.EmployeeRepository' available

    这说明你没有继承核心接口,那么Spring就找不到你定义的接口。
    另外我们也可以用注释来代替继承语句,作用是一样的:

    1
    2
    3
    4
    5
    6
    7
    8
    import com.ting.domain.Employee;
    import org.springframework.data.repository.RepositoryDefinition;

    @RepositoryDefinition(domainClass = Employee.class,idClass = Integer.class)
    public interface EmployeeRepository {

    public Employee findByName(String name);
    }
  2. Repository子类接口:CrudRepository,JpaRepository,PagingAndSortingRepository,JpaSpecificationExecutor

在IDEA中crtl+t/h点击相应的类查看继承关系

  • CrudRepository:继承Repository,实现了CRUD相关操作
  • PagingAndSortingRepository:继承CrudRepository,实现了分页排序的相关方法
  • JpaRepository:继承PagingAndSortingRepository,实现JPA规范相关的方法
  1. 使用Repository接口查询方法定义规则和使用

1
2
3
4
5
6
7
8
9
10
11
// where name like ?% and age <?
public List<Employee> findByNameStartingWithAndAgeLessThan(String name, Integer age);

// where name like %? and age <?
public List<Employee> findByNameEndingWithAndAgeLessThan(String name, Integer age);

// where name in (?,?....) or age <?
public List<Employee> findByNameInOrAgeLessThan(List<String> names, Integer age);

// where name in (?,?....) and age <?
public List<Employee> findByNameInAndAgeLessThan(List<String> names, Integer age);

弊端:
1)方法名会比较长: 约定大于配置
2)对于一些复杂的查询,是很难实现
因此,对于这种情况下还是要写SQL语句简单得多。所以又引入了@Query注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Query("select o from Employee o where id=(select max(id) from Employee t1)")
public Employee getEmployeeByMaxId();

@Query("select o from Employee o where o.name=?1 and o.age=?2")
public List<Employee> queryParams1(String name, Integer age);

@Query("select o from Employee o where o.name=:name and o.age=:age")
public List<Employee> queryParams2(@Param("name")String name, @Param("age")Integer age);

@Query("select o from Employee o where o.name like %?1%")
public List<Employee> queryLike1(String name);

@Query("select o from Employee o where o.name like %:name%")
public List<Employee> queryLike2(@Param("name")String name);

//nativeQuery = true 打开原生态查询
@Query(nativeQuery = true, value = "select count(1) from employee")
public long getCount();

对于修改数据,需要增加Modify注解、并且一定要在事务的管理下才能修改数据,读取不需要事务

1
2
3
@Modifying
@Query("update Employee o set o.age = :age where o.id = :id")
public void update(@Param("id")Integer id, @Param("age")Integer age);

补充知识:【JPQL】–JPQL和SQL的比较

然后在service层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ting.services;

import com.ting.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service //告诉Spring这是Service层
public class EmployeeService {

@Autowired //注入,注意注入的前提是在beans.xml中定义了<context:component-scan base-package="com.ting"/>上下文扫描路径
private EmployeeRepository employeeRepository;

@Transactional //告诉Spring要添加事务管理,否则无法执行修改数据库的操作
public void update(Integer id,Integer age) {
employeeRepository.update(id,age);
}
}

事务在Spring data中的使用:
1)事务一般是在Service层
2)@Query、 @Modifying、@Transactional的综合使用

  1. 使用CrudRepository接口:
    这个接口继承源代码所提供的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <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();

    使用这个接口我们可以实现对数据库的批量操作CRUD

  2. 使用分页PagingAndSortingRepository接口:

EmployeePagingAndSortingRepositoryTest.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.ting.services;

import com.ting.domain.Employee;
import com.ting.repository.EmployeeCrudRepository;
import com.ting.repository.EmployeePagingAndSortingRepository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

public class EmployeePagingAndSortingRepositoryTest {
private ApplicationContext ctx = null;
private EmployeePagingAndSortingRepository employeePagingAndSortingRepository = null;

@Before
public void setup() {
ctx = new ClassPathXmlApplicationContext("beans.xml");
employeePagingAndSortingRepository = ctx.getBean(EmployeePagingAndSortingRepository.class);
System.out.println("setup");
}

@After
public void tearDown() {
ctx = null;
System.out.println("tearDown");
}

@Test
public void testPage() {
//注意page参数index从0开始
Pageable pageable = new PageRequest(0,5);
Page<Employee> page = employeePagingAndSortingRepository.findAll(pageable);

System.out.println("查询的总页数" + page.getTotalPages());
System.out.println("查询的总记录数" + page.getTotalElements());
System.out.println("当前第几页" + (page.getNumber()+1));
System.out.println("当前页面的集合" + page.getContent().toString());
System.out.println("当前页面的记录数" + page.getNumberOfElements());
}
}
  1. 使用JpaRepository接口
    接口源代码所提供的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAll(Iterable<ID> ids);
    <S extends T> List<S> save(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
  2. 补充一个JpaSpecificationExecutor接口
    使用时同时extends JpaRepository<Employee,Integer>,JpaSpecificationExecutor<Employee>
    Specification封装了JPA Criteria查询条件,可以操作分页、排序、条件查询.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Test
    public void testQuery() {
    Sort.Order order = new Sort.Order(Sort.Direction.ASC,"id");
    Sort sort = new Sort(order);

    Pageable pageable = new PageRequest(1,5,sort);


    /**
    * Root:我们要查询的类型(Employee)
    * query:添加查询条件
    * cb:构建Predicate
    */
    Specification<Employee> specification = new Specification<Employee>() {
    @Override
    public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    //root(employee(age))
    Path path = root.get("age");
    return cb.gt(path,50); //gt:greater than
    }
    };

    Page<Employee> page = employeeJpaSpecificationRepository.findAll(specification,pageable);
    System.out.println("查询的总页数" + page.getTotalPages());
    System.out.println("查询的总记录数" + page.getTotalElements());
    System.out.println("当前第几页" + (page.getNumber()+1));
    System.out.println("当前页面的集合" + page.getContent().toString());
    System.out.println("当前页面的记录数" + page.getNumberOfElements());

    }