SpringBoot2 | 第六篇:整合SpringDataJPA

上一篇介绍了Spring JdbcTemplate的使用,对比原始的JDBC而言,它更加的简洁。但随着表的增加,重复的CRUD工作让我们苦不堪言,这时候Spring Data Jpa的作用就体现出来了…..

[TOC]

JPA

​ JPA 是 Java Persistence API 的简称,中文名 Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。

​ Sun引入新的 JPA ORM 规范出于两个原因:其一,简化现有 Java EEJava SE 应用开发工作;其二,Sun希望整合 ORM 技术,实现天下归一。

​ Sun公司在JDK1.5的时候,吸收了HibernateTopLinkORM框架 的优点,提出了Java持久化规范:JPA;Hibernate在3.2的时候提供了JPA的实现,其余的JPA的供应商还有诸如OpenJPA、Toplink等;

SpringDataJPA

Spring Data JPA 是在JPA规范的基础下提供了Repository层的 实现,但是使用哪一款 ORM 需要你自己去决定;相比我们更为熟悉的Hibernate和MyBatis,Spring Data JPA 可以看做更高层次的抽象

优点

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

缺点

  • 学习成本较大,需要学习HQL如:@Query(value="from LogToIndex group by elkIndex")
  • 配置复杂,虽然SpringBoot简化的大量的配置,但关系映射多表查询配置依旧不容易
  • 性能较差,对比JdbcTemplateMybatis等ORM框架,它的性能无异于是最差的

环境/版本一览:

  • 开发工具:Intellij IDEA 2018.2.2
  • springboot: 2.0.5.RELEASE
  • jdk:1.8.0_171
  • maven:3.3.9

1、搭建

1538314105146

2、pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- spring-data-jpa 携带 jdbc, jdbc 携带 HikariCP -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

3、application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
# 基本属性
url: jdbc:mysql://localhost:3306/chapter6?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC&useSSL=false
username: root
password: 123456
jpa:
# 显示 sql
show-sql: true
# 数据库类型
database: mysql
# JPA 配置
hibernate:
ddl-auto: update
# 指定生成的表的引擎为InnoDB类型(默认是MyISAM,MyISAM不支持事务)
database-platform: org.hibernate.dialect.MySQL57InnoDBDialect

JPA 配置 ddl-auto 几种属性

  • create: 每次运行程序时,都会重新创建表,故而数据会丢失
  • create-drop: 每次运行程序时会先创建表结构,然后待程序结束时清空表
  • upadte: 每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)
  • validate: 运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错

4、sql

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

5、entity

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

import lombok.Data;
import lombok.experimental.Accessors;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.io.Serializable;

/**
* User 实体
* 自动生成数据表
* @author: Fatal
* @date: 2018/9/30 0030 21:36
*/
@Data
@Accessors(chain = true)
@Entity(name="user")
public class User implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 自增,如果没有`@GeneratedValue`,则需我们手动添加 id
private Long id;

private String username;

private String password;

private String email;

}

6、dto

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

import lombok.Data;
import lombok.experimental.Accessors;

/**
* DTO 数据传输对象
* @author: Fatal
* @date: 2019/6/26 0026 11:34
*/
@Data
@Accessors(chain = true)
public class UserDTO {

private Long id;

private String username;

}

7、utils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.fatal.convert;

import com.fatal.dto.UserDTO;
import com.fatal.entity.User;
import org.springframework.beans.BeanUtils;

/**
* 数据传输对象转换工具类
* @author: Fatal
* @date: 2019/6/26 0026 11:39
*/
public class ConvertUtil {

public static UserDTO convert(User user) {
UserDTO dto = new UserDTO();
BeanUtils.copyProperties(user, dto);
return dto;
}

}

8、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
package com.fatal.mapper;

import com.fatal.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

/**
* @author: Fatal
* @date: 2018/9/30 0030 21:42
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

// 实现`JpaRepository`后有好多方法可以用

/**
* 自定义分页查询
* @param pageable
* @return
*/
@Query(value = "SELECT * FROM USER",
countQuery = "SELECT count(*) FROM USER",
nativeQuery = true)
Page<User> findPage(Pageable pageable);

/**
* 动态拼装sql加模糊查询
* @param username 用户名
* @return
*/
@Query(value = "SELECT u.* FROM USER u WHERE if(:username != '', u.`username` LIKE CONCAT('%', :username, '%'), 1=1)", nativeQuery = true)
List<User> findPageWithLike(String username);
}

注:if(:username != '', u.username LIKE CONCAT('%', :username, '%'), 1=1)中判断!= 后面不能用 null,否则不起作用。

9、Test

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.fatal;

import com.fatal.convert.ConvertUtil;
import com.fatal.dto.UserDTO;
import com.fatal.entity.User;
import com.fatal.mapper.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.*;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class Chapter6ApplicationTests {

@Autowired
private UserRepository repository;

@Test
public void save() {
User user = new User().setUsername("米彩").setPassword("123").setEmail("123@qq.com");
repository.save(user);
log.info("[添加成功] - [{}]",user);
}

@Test
public void findAll() {
List<User> list = repository.findAll();
log.info("[查询所有] - [{}]", list);
}

@Test
public void findOne() {
User user = new User().setUsername("张三");
User one = repository.findOne(Example.of(user)).orElse(null);
log.info("[根据用户名查询成功] - [{}]", one==null?"无记录":one);
}

/**
* 测试分页查询
*/
@Test
public void findPage() {
Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Order.asc("username")));
Page<User> page = repository.findAll(pageable);
log.info("[分页+排序+查询所有] - [{}]", page.getContent());
}

/**
* 测试分页查询(内容替换)
*/
@Test
public void findPageWithConvertContent() {
Pageable pageable = PageRequest.of(0, 5, Sort.by(Sort.Order.asc("username")));
Page<User> page = repository.findAll(pageable);
// 内容转为DTO
Page<UserDTO> result = page.map(ConvertUtil::convert);
log.info("[分页+排序+查询所有(DTO)] - [{}]", result.getContent());
}

/**
* 测试自定义分页查询sql
*/
@Test
public void findPageWithCustomSql() {
Pageable pageable = PageRequest.of(0, 1, Sort.by(Sort.Order.asc("id")));
Page<User> page = repository.findPage(pageable);
log.info("[分页+排序+查询所有(自定义分页查询sql)] - [{}]", page.getContent());
}

/**
* 测试动态拼装sql加模糊查询
*/
@Test
public void findPageWithLike() {
String username1 = "米";
String username2 = "";
String username3 = null;
List<User> list1 = repository.findPageWithLike(username1);
List<User> list2 = repository.findPageWithLike(username2);
List<User> list3 = repository.findPageWithLike(username3);
System.out.println("list1: ");
list1.forEach(System.out::println);
System.out.println("list2: ");
list2.forEach(System.out::println);
System.out.println("list3: ");
list3.forEach(System.out::println);
}
}

测试

前三个就贴出来了。

数据库

1561626209032

1、测试分页查询

1561626273097

2、测试分页查询(内容替换)

1561626314961

3、测试自定义分页查询sql

1561626410094

4、测试动态拼装sql加模糊查询

1561626490697

笔记

1、GenerationType

1
2
3
4
5
6
7
8
9
public enum GenerationType {
TABLE,
SEQUENCE,
IDENTITY,
AUTO;

private GenerationType() {
}
}
  • TABLE:使用一个特定的数据库表格来保存主键。
  • SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。
  • IDENTITY:主键由数据库自动生成(主要是自动增长型)
  • AUTO:主键由程序控制。

2、JpaRepository 中的 T getOne(ID id)

这个方法使用要特别注意,查询结果自带懒加载效果的。

如果你在测试类中调用这个方法,那么你讲会遇到下面的错误 No Session

org.hibernate.LazyInitializationException: could not initialize proxy [com.fatal.entity.Goods#1] - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:169)
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:309)
    at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
    at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
    at com.fatal.entity.Goods$HibernateProxy$yA5wHloO.toString(Unknown Source)
    at java.lang.String.valueOf(String.java:2994)
    at java.io.PrintStream.println(PrintStream.java:821)
    at com.fatal.repository.GoodsRepositoryTest.findByIdTest(GoodsRepositoryTest.java:37)

即便你在application.yml中加了

1
2
3
# 注册OpenEntityManagerInViewInterceptor。在整个请求处理过程中将JPA EntityManager绑定到线程。
# 作用:扩大了 JPA Session 的范围,至视图。这部分后面jpa关联查询再详细介绍
spring.jpa.open-in-view: true

也无济于事。因为 junit 根本不需要加载拦截器(这只是我的猜想,因为 junit 不需要web,那需要拦截器干嘛)

参考资料

Spring Data JPA 官方文档

一起来学SpringBoot | 第六篇:整合SpringDataJpa

总结

SpringBoot的知识已经有前辈在我们之前探索了。比较喜欢的博主有:唐亚峰 | Battcn方志朋的专栏程序猿DD纯洁的微笑。对这门技术感兴趣的可以去他们的博客逛逛。谢谢他们的分享~~

以上文章是我用来学习的Demo,都是基于 SpringBoot2.x 版本。

源码地址: https://github.com/ynfatal/springboot2-learning/tree/master/chapter6

学习 唐亚峰 前辈的经验