SpringBoot(四) Spring Data JPA

Spring Data JPA 让我们解脱了DAO层的操作,基本上所有CRUD都可以依赖于它来实现

Spring Data JPA介绍

了解JPA是什么?

JPA是Java Persistence API的简称,中文名: Java持久层API,是官方(Sun)在JDK5.0后提出的Java持久化规范。其目的是为了简化现有JAVA EE和JAVA SE应用开发工作,以及整合现有的ORM技术实现规范统一

Spring Data JPA

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套JPA应用框架,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!

优点

(1)丰富的API,简单操作无需编写额外的代码
(2)丰富的SQL日志输出

缺点

(1)学习成本较大,需要学习HQL
(2)配置复杂,虽然SpringBoot简化的大量的配置,关系映射多表查询配置依旧不容易
(3)性能较差,对比JdbcTemplate、Mybatis等ORM框架,它的性能无异于是最差的

学习并使用 Spring Data JPA 可以极大提高开发效率!

项目构建

pom.xml

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
<dependencies>
<!-- Spring JDBC 的依赖包,使用 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 将会自动获得HikariCP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Web模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MYSQL包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 内嵌Tomcat 容器,必要时注释scope属性 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- 测试包,当我们使用 mvn package 的时候该包并不会被打入,因为它的生命周期只在 test 之内-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
username: root
password: root

# JPA
jpa:
hibernate:
# 自动创建、更新、验证数据库表结构
ddl-auto: update # 第一次建表create 后面用update
# 显示sql 语句
show-sql: true
# 数据库类型
database: mysql

hibernate.ddl-auto 属性说明

属性 作用
create 每次加载Hibernate时都会删除上一次生成的表,然后重新生成新表,即使两次没有任何修改也会这样执行,这就导致每次启动都是一个新的数据库,也是导致数据丢失的重要原因
create-drop 每次加载Hibernate时都会生成表,但当SessionFactory关闭时,所生成的表将自动删除
update 最常用的属性值,第一次加载Hibernate时创建数据表(前提是需要先有数据库),以后加载HIbernate时只会根据model更新,即使model已经删除了某些属性,数据表也不会随之删除字段
validate 每次加载Hibernate时都会验证数据表结构,只会和已经存在的数据表进行比较,根据model修改表结构,但不会创建新表

脚本初始化

由于上面我们采用的是 spring.jpa.hibernate.ddl-auto=update 方式,因此这里可以跳过手动建表的操作

实体类

(1)JPA规范注解坐落在javax.persistence 包下
(2)@Id 注解一定不要引用错了,否则会报错
(3)@GeneratedValue(strategy = GenerationType.IDENTITY) 自增策略
(4)不需要映射的字段可以通过@Transient 注解排除掉

常见的几种自增策略

(1)TABLE: 使用一个特定的数据库表格来保存主键
(2)SEQUENCE: 根据底层数据库的序列来生成主键,条件是数据库支持序列。这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)。
(3)IDENTITY: 主键由数据库自动生成(主要是支持自动增长的数据库,如mysql)
(4)AUTO: 主键由程序控制,也是GenerationType的默认值。

简单示例

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
package com.ray.springboot204.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;

/**
* @author Ray
* @date 2018/7/22 0022
*/
@Data
@Entity(name = "t_user")
public class User implements Serializable {

private static final long serialVersionUID = 8655851615465363473L;

/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* 名字
*/
@Column(name = "username", length = 100)
private String username;

/**
* 密码
*/
@Column(name = "password", length = 50)
private String password;

/**
* TODO 忽略该字段的映射
*/
@Transient
private String email;


/**
* 构造器
*/
public User() {
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

public User(Long id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}

}

@Data : Lombok注解
@Entity :说明这个class是实体类,并且使用默认的orm规则,即class名即数据库表中表名,class字段名即表中的字段名
@Table来改变class名与数据库中表名的映射规则
@Column来改变class中字段名与db中表的字段名的映射规则

映射规则

(1) 实体类必须用 @javax.persistence.Entity 进行注解
(2) 必须使用 @javax.persistence.Id 来注解一个主键
(3) 实体类必须拥有一个 public 或者 protected 的无参构造函数,之外实体类还可以拥有其他的构造函数
(4) 实体类必须是一个顶级类(top-level class)。一个枚举(enum)或者一个接口(interface)不能被注解为一个实体
(5) 实体类不能是 final 类型的,也不能有 final 类型的方法
(6) 如果实体类的一个实例需要用传值的方式调用(例如,远程调用),则这个实体类必须实现(implements)java.io.Serializable 接口

Dao数据访问层

创建UserDao数据访问层接口,需要继承JpaRepository<T,K>,JpaRepository需要泛型接口参数,第一个泛型参数是实体对象的名称,第二个是主键类型。

@Query注解自定义SQL

只需要这样简单的配置,该UserDao就拥常用的CRUD功能,JpaRepository本身就包含了常用功能,剩下的查询我们按照规范写接口即可,所以开发中一般都会继承它,它功能多一点。

示例:

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
package com.ray.springboot204.dao;

import com.ray.springboot204.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
* @author Ray
* @date 2018/7/22 0022
* JpaRepository接口继承了PagingAndSortingRepository接口,所以开发中一般都会继承它,它功能多一点。
*/
@Repository
public interface UserDao extends JpaRepository<User, Long> {

/**
* 方法一:指定使用本地SQL查询
* nativeQuery = true才是表明了使用原生的sql,如果不配置,默认是false,则使用HQL查询方式
*/
// @Query(value = "select * from t_user where username = ? ", nativeQuery = true)
// List<User> finalAllByUsername(String username);

/**
* 方法二:索引参数方式
* 索引值从1开始,查询中'?x'的个数要和方法的参数个数一致,且顺序也要一致
*/
// @Query(value = "from t_user where username = ?1")
// List<User> finalAllByUsername(String username);

/**
* 方法三:命名参数方式(推荐使用这种方式)
* 用':参数名'的形式,必须在方法参数中使用@Param("参数名")注解,这样就可以不用按顺序来定义形参
*/
@Query(value = "from t_user where username = :username")
List<User> finalAllByUsername(@Param("username") String username);

/**
* 这就是Spring-data-jpa的一大特性:通过解析方法名创建查询
* @param username
* @return
*/
List<User> findByusername(String username);
}

注意:SpringDataJPA内有个save方法,这个方法不仅仅是用来添加数据使用,当我们传入主键的值时则是根据主键的值完成更新数据操作。

命名参数方式创建查询

在上例中,我们看到几个@Query函数,其中:

1
2
3
4
5
6
/**
* 方法三:命名参数方式(推荐使用这种方式)
* 用':参数名'的形式,在方法参数中使用@Param("参数名")注解,这样就可以不用按顺序来定义形参
*/
@Query(value = "from t_user where username = :username")
List<User> finalAllByUsername(@Param("username") String username);

您只需要编写JPQL语句,并通过类似:name 来映射@Param 指定的参数

通过解析方法名创建查询

在上例中,我们看到这个函数:

1
2
3
4
5
6
/**
* 这就是Spring-data-jpa的一大特性:通过解析方法名创建查询
* @param username
* @return
*/
List<User> findByusername(String username);

它实现了按username查询User实体,可以看到我们这里没有任何类SQL语句就完成了条件查询方法。这就是Spring-data-jpa的一大特性:通过解析方法名创建查询。

注意

findByusername方法名称中的 username 必须与我们实体类属性 username 保持一致。 (这也是jpa一个坑)

@Query配合@Modifying

可以看到我们的@Query注解好像只是用来查询的,但是如果配合@Modifying注解一共使用,则可以完成数据的删除、添加、更新操作。
下面我们来测试下自定义SQL完成删除数据的操作,

根据 id 字段删除数据

此处使用 命名参数 方式,注意 :id@Param

1
2
3
4
5
6
7
/**
* @Query 配合 @Modifying 使用
* 可实现增删改查,这里只演示删除操作
*/
@Modifying
@Query(value = "delete from t_user where id = :id")
int deleteByKey(@Param("id") Long id);

编写测试类添加deleteByKey方法

1
2
3
4
5
6
7
    @Test
public void contextLoads() {
...
userDao.deleteById(user.getId());
log.info("[删除主键 {} 成功] - [{}]", user.getId());
}
}

发生异常

1
org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query

如果抛出异常 TranscationRequiredException 意思就是你当前的操作给你抛出了需要事务异常,SpringDataJPA自定义SQL时需要在对应的接口或者调用接口的地方添加事务注解@Transactional,来开启事务自动化管理。

解决异常

在Dao层添加 @Transactional 注解,重启项目再来访问刚才的地址即可

1
2
3
4
5
@Repository
@Transactional
public interface UserDao extends JpaRepository<User, Long> {

}

完整项目测试程序

下面的几个操作中,我们自己编写了几个方法: finalAllByUsernamefindByusernamedeleteByKey,其它的都是继承自JpaRepository接口中的方法,更关键的是分页及排序是如此的简单实例化一个Pageable即可…

测试类

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
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class Springboot204ApplicationTests {

@Autowired
private UserDao userDao;

@Test
public void contextLoads() {
User user = userDao.save(new User("u1", "p1"));
log.info("[添加成功] - [{}]", user);
List<User> iuser = userDao.findByusername("u1");
log.info("[通过解析方法名创建查询] - [{}]", iuser);
List<User> users = userDao.finalAllByUsername("u1");
log.info("[条件查询] - [{}]", users);
Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("username")));
Page<User> page = userDao.findAll(pageable);
log.info("[分页+排序+查询所有] - [{}]", page.getContent());
userDao.findById(page.getContent().get(0).getId()).ifPresent(user1 -> log.info("[主键查询] - [{}]", user1));
User edit = userDao.save(new User(user.getId(), "修改后的ui", "修改后的p1"));
log.info("[修改成功] - [{}]", edit);
// userDao.deleteById(user.getId());
userDao.deleteByKey(user.getId());
log.info("[删除主键 {} 成功] - [{}]", user.getId());
}
}

控制台结果

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
Hibernate: create table t_user (id bigint not null auto_increment, password varchar(50), username varchar(100), primary key (id)) engine=MyISAM
2018-07-22 22:01:13.582 INFO 17112 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2018-07-22 22:01:14.443 INFO 17112 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-07-22 22:01:16.230 INFO 17112 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
2018-07-22 22:01:16.493 INFO 17112 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.web.context.support.GenericWebApplicationContext@169bb4dd: startup date [Sun Jul 22 22:01:07 CST 2018]; root of context hierarchy
2018-07-22 22:01:16.580 WARN 17112 --- [ main] aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2018-07-22 22:01:16.651 INFO 17112 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-07-22 22:01:16.653 INFO 17112 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-07-22 22:01:16.711 INFO 17112 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-07-22 22:01:16.711 INFO 17112 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-07-22 22:01:17.569 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : Started Springboot204ApplicationTests in 11.288 seconds (JVM running for 13.247)
Hibernate: insert into t_user (password, username) values (?, ?)
2018-07-22 22:01:18.046 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [添加成功] - [User(id=1, username=u1, password=p1, email=null)]
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from t_user user0_ where user0_.username=?
2018-07-22 22:01:18.186 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [通过解析方法名创建查询] - [[User(id=1, username=u1, password=p1, email=null)]]
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from t_user user0_ where user0_.username=?
2018-07-22 22:01:18.231 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [条件查询] - [[User(id=1, username=u1, password=p1, email=null)]]
Hibernate: select user0_.id as id1_0_, user0_.password as password2_0_, user0_.username as username3_0_ from t_user user0_ order by user0_.username desc limit ?
2018-07-22 22:01:18.252 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [分页+排序+查询所有] - [[User(id=1, username=u1, password=p1, email=null)]]
Hibernate: select user0_.id as id1_0_0_, user0_.password as password2_0_0_, user0_.username as username3_0_0_ from t_user user0_ where user0_.id=?
2018-07-22 22:01:18.265 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [主键查询] - [User(id=1, username=u1, password=p1, email=null)]
Hibernate: select user0_.id as id1_0_0_, user0_.password as password2_0_0_, user0_.username as username3_0_0_ from t_user user0_ where user0_.id=?
Hibernate: update t_user set password=?, username=? where id=?
2018-07-22 22:01:18.282 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [修改成功] - [User(id=1, username=修改后的ui, password=修改后的p1, email=null)]
Hibernate: select user0_.id as id1_0_0_, user0_.password as password2_0_0_, user0_.username as username3_0_0_ from t_user user0_ where user0_.id=?
Hibernate: delete from t_user where id=?
2018-07-22 22:01:18.300 INFO 17112 --- [ main] c.r.s.Springboot204ApplicationTests : [删除主键 1 成功] - [{}]

数据库

SpringData方法命名规则

-------------- 本文结束  感谢您的阅读 --------------