JPA使用总结

到了现阶段,JPA 的常用方法已经大部分掌握了,平时只需要使用 JPA 的 ORM 全自动管理,不需要使用一对多映射,配合 JdbcTemplate 就能很好的实现大部分业务开发

一、对象状态

临时状态刚创建出来,与 EntityManager 没关系
托管状态JavaBean 与 Entitymanager 发生关系后被持久化,修改属性之后 JPA 会自动同步到数据库
持久化状态flush 之后就短暂进入持久化状态,提交事务之后变为游离态
游离态事务提交之后,此时修改属性就没用了,如果 new 的对象,设置的 id 是数据库已存在的 ID,那也是游离态

二、ORM 全自动管理

强烈推荐使用的原因之一,表管理操作都可以在代码中实现,实体类即库表,配合 ide 的 jpa 插件很方便使用
不需要再直接使用数据库连接工具进行管理

  • 可以进行索引管理和字段类型管理,包括字段注释,很方便
  • 使用 hibernate 的**@SQLDelete@Where**注解可以实现逻辑删除:JPQL 查询中会忽略已删除的数据;如果需要使用被删除的数据,编写 nativeQuery sql 即可
  • 使用 innodb 引擎:在配置中加入 spring.jpa.properties.hibernate.dialect.storage_engine=innodb
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
@Entity(name = "templateHeader")
@Table(name = "templateHeader", indexes = {@Index(columnList = "templateId")})
@SQLDelete(sql = "update templateHeader set deleted = 1 where id = ?")
@Where(clause = "deleted = 0")
public class TemplateHeader extends BaseJpaEntity implements Comparable<TemplateHeader> {
/**
* 逻辑删除标志位:0 未删除,1 已删除
* 给deleted赋默认值0,在业务操作中即可忽略该字段
*/
@JsonIgnore
@Column(columnDefinition = "int default 0 comment '逻辑删除标志位:0-未删除, 1-已删除'")
private Integer deleted = 0;
/**
* 名字
*/
@Column(columnDefinition = "varchar(255) comment '名字'")
private String name;
/**
* 是否是数据域的表头:true/false:是/否
*/
@Column(columnDefinition = "bit comment '是否是数据域的表头:true/false:是/否'")
private Boolean isData;
/**
* 模板id
*/
@Column(columnDefinition = "bigint comment '模板id'")
private Long templateId;
/**
* 排序,越大越在前面
*/
@Column(columnDefinition = "int comment '排序,越大越在前面'")
private Integer sort;

@Override
public int compareTo(@NotNull TemplateHeader o) {
return this.sort - o.sort;
}
}

三、关键字查询

页面上搜索框需要对多个字段进行关键字模糊查询。其实搜索最好使用 ES。但我司业务比较轻,就没有使用 ES

  • 在这里需要对工单号,参会专家,参会人员,客户单位,保修厂家,会议主题进行关键字搜索,是 or 搜索,还有其他的条件搜索,组合起来为一个多条件分页搜素
  • 使用两个断言数组,一个存储 and 查询,一个存储关键字 or 查询即可。最后根据条件进行拼装。
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
 public PageUtil<Appointment> list(AppointPageReq req) {
Sort sort = Sort.by(Sort.Direction.DESC, "startTime", "updateTime");
Pageable pageable = PageUtil.getPageRequest(req, sort);
String keyword = req.getKeyword();
LocalDate start = req.getStart();
LocalDate end = req.getEnd();
LocalDateTime now = LocalDateTime.now();
Specification<Appointment> specification = (root, query, builder) -> {
List<Predicate> andPre = new ArrayList<>();
List<Predicate> orPre = new ArrayList<>();
//关键字查询:工单号、参会专家、参会人员、客户单位、报修厂家、会议主题
boolean hasOr = StrUtil.isNotBlank(StrUtil.trim(keyword));
if (hasOr) {
String blurStr = SqlUtil.getBlurStr(keyword);
orPre.add(builder.like(root.get("workOrderNum"), blurStr));
orPre.add(builder.like(root.get("customerUnit"), blurStr));
orPre.add(builder.like(root.get("repairFactory"), blurStr));
orPre.add(builder.like(root.get("theme"), blurStr));
//查出其他参会人员和会议的关联id列表
List<Long> idListByNameLike = appointParticipantRepo.findIdListByNameLike(blurStr);
if (CollUtil.isNotEmpty(idListByNameLike)) {
orPre.add(root.get("id").in(idListByNameLike));
}
//查出参会专家的关联id列表
List<Long> idListByExpertNameLike = expertRepo.findIdListByNameLike(blurStr);
if (CollUtil.isNotEmpty(idListByExpertNameLike)) {
orPre.add(root.get("id").in(idListByExpertNameLike));
}
}
if (Objects.nonNull(start)) {
if (Objects.isNull(end)) {//为空就查今天及以后的
andPre.add(builder.greaterThan(root.get("startTime"), start.atTime(LocalTime.MIN)));
} else {//否则查
andPre.add(builder.between(root.get("startTime"), start.atTime(LocalTime.MIN), end.atTime(LocalTime.MAX)));
}
}
Predicate[] a = new Predicate[andPre.size()];
Predicate[] o = new Predicate[orPre.size()];
// 如果有关键字查询,就有or查询,否则全部是and查询
if (hasOr) {
return query.where(builder.and(andPre.toArray(a)), builder.or(orPre.toArray(o))).getRestriction();
} else {
return builder.and(andPre.toArray(a));
}
};
//return ..........其他业务
}

四、传参及查询结果的接收

以前使用 mybatis 的时会使用@Param 进行参数绑定, 在使用 JPA 的过程中每台注意,后来发现其实也是有类似注解的,这样会使代码更具有可读性,不必每次使用?1 进行参数的指定

一个实体有可能只需要其中的部分字段,这时可以使用 JPQL 的语法使用对应的 DTO 进行查询结果的接收
在 DTO 中定义好有参构造器即可,注意需要使用 DTO 的权限定类名进行接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface AppointExpertRefRepo extends BaseJpaRepo<AppointExpertRef> {
/**
* 通过专家id查询某个时间段内所有会议的开始时间
*
* @param expertId
* @param startTime
* @return
*/
@Query(value ="select new cn.hzncc.dao.bean.dto.AppointTime(a.startTime,a.endTime,a.theme)
from appointment a left join appointExpertRef ae on ae.appointId = a.id "
+ "where ae.expertId = :expertId and a.startTime > :startTime")
List<AppointTime> findAppointByExpertIdAndStartTime(@Param("expertId") Long expertId, @Param("startTime") LocalDateTime startTime);
}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class AppointTime {
private LocalDateTime startTime;
private LocalDateTime endTime;
private String theme;
}

五、基于注解的审计元数据

该部分部分来源为JPA 中文文档
很多时候需要自动填充的数据,如创建时间,更新时间,创建人,更新人

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
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseJpaEntity implements Serializable {

private static final long serialVersionUID = 733899366518016549L;
/**
* 主键id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;


//不指定columnDefinition会默认使用datetime(6),导致报错
@CreatedDate
@Column(columnDefinition = "datetime comment '创建时间'")
protected LocalDateTime createTime;


@LastModifiedDate
@Column(columnDefinition = "datetime comment '更新时间'")
protected LocalDateTime updateTime;

@CreatedBy
protected User createUser;


@@LastModifiedBy
protected User updateUser;

}

AuditorAware

  • 如果你使用 @CreatedBy 或 @LastModifiedBy,审计基础设施需要以某种方式知道当前的 principal。为此,我们提供了一个 AuditorAwareSPI 接口,你必须实现这个接口来告诉基础设施谁是与应用程序交互的当前用户或系统。泛型 T 定义了用 @CreatedBy 或 @LastModifiedBy 注解的属性必须是什么类型。 下面的例子显示了一个使用 Spring Security 的 Authentication 对象的接口实现
1
2
3
4
5
6
7
8
9
10
11
12
class SpringSecurityAuditorAware implements AuditorAware<User> {

@Override
public Optional<User> getCurrentAuditor() {

return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}

ReactiveAuditorAware

当使用响应式基础设施时,你可能想利用上下文(Context)信息来提供 @CreatedBy 或 @LastModifiedBy 信息。我们提供了一个 ReactiveAuditorAwareSPI 接口,你必须实现这个接口来告诉基础设施谁是当前与应用程序交互的用户或系统。泛型 T 定义了用 @CreatedBy 或 @LastModifiedBy 注释的属性必须是什么类型。

下面的例子显示了一个接口的实现,它使用了 Spring Security 的 Authentication 对象。
Example 124. 基于 Spring Security 的 ReactiveAuditorAware 的实现

1
2
3
4
5
6
7
8
9
10
11
12
class SpringSecurityAuditorAware implements ReactiveAuditorAware<User> {

@Override
public Mono<User> getCurrentAuditor() {

return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(User.class::cast);
}
}

该实现访问由 Spring Security 提供的 Authentication 对象,并查找你在 UserDetailsService 实现中创建的自定义 UserDetails 实例。我们在这里假设你是通过 UserDetails 实现来暴露 domain 用户的,但根据找到的 Authentication,你也可以从任何地方查到它。
还有一个方便的基类,AbstractAuditable,你可以扩展它以避免手动实现接口方法。这样做会增加你的 domain 类与 Spring Data 的耦合度,这可能是你想避免的事情。通常情况下,基于注解的定义审计元数据的方式更受欢迎,因为它的侵入性更小,也更灵活。

六、批处理

JPA 默认的 saveAllAndFlush 速度太慢,一般在性能要求不高的场景下使用;当有大量数据需要存储时,速度会特别慢,因此进行优化。我在平时工作中一般会使用 jdbcTemplate 进行管理

  • 配置文件的变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 在数据库链接url中加入该参数
rewriteBatchedStatements=true


# application.yml配置
spring:
jpa:
open-in-view: false
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
properties:
hibernate:
dialect:
storage_engine: innodb
format_sql: true
jdbc:
batch_size: 30
batch_versioned_data: true
  • 具体使用
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
/**
* jdbcTemplate使用
* 特殊资源需要统一管理,一般都是批量操作
*/
@Service
@RequiredArgsConstructor
public class JdbcTemplateManager {
private final JdbcTemplate jdbcTemplate;

/**
* insert操作会将sql整合为:insert into table xxx(a,b,c) values (1,1,1),(2,2,2)...(3,3,3);的形式,速度会好很多
*
* @param formDataList
*/
public void addFormDataList(List<FormData> formDataList) {
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
jdbcTemplate.batchUpdate("insert into formData (stepId,headerId,archivesId,createTime,updateTime) values (?,?,?,?,?)", formDataList, 50, (ps, f) -> {
ps.setLong(1, f.getStepId());
ps.setLong(2, f.getHeaderId());
ps.setLong(3, f.getArchivesId());
ps.setTimestamp(4, now);
ps.setTimestamp(5, now);
});
}

/**
* update操作不会将sql整理为一个,只是 update xxx set a=1 where id = 2;.....;update xxx set a=a where id = 2222;
* 但速度也好了很多
*
* @param modelTemplateList
*/
public void updateModelTemplate(List<ModelTemplate> modelTemplateList) {
Timestamp now = Timestamp.valueOf(LocalDateTime.now());
jdbcTemplate.batchUpdate("update modelTemplate set templateIdListStr = ?, updateTime = ? where id = ?", modelTemplateList, 50, (ps, m) -> {
ps.setString(1, m.getTemplateIdListStr());
ps.setTimestamp(2, now);
ps.setLong(3, m.getId());
});
}
}
  • 经过测试之后,也确实证实了 jdbcTemplate 速度的优异性
  • 测试结果为:
1
2
3
4
5
6
7
8
StopWatch '': running time = 814550800 ns
---------------------------------------------
ns % Task name
---------------------------------------------
029008200 004% jdbcTemplate batch1
029116900 004% jdbcTemplate batch2
655802100 081% jpa saveAllAndFlush
100623600 012% mybatisPlus batch
  • 测试程序如下
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
@Slf4j
@SpringBootTest
class JdbcTemplateManagerTest {
@Autowired
JdbcTemplateManager jdbcTemplateManager;
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
PersonMapper personMapper;
@Autowired
PersonRepo personRepo;
@Test
void addPersons() {
int n = 99;
List<Object[]> list = IntStream.rangeClosed(0, n).mapToObj(i -> new Object[]{"name", i + 10, "hubei" + i, i / 2 == 0 ? "唱跳rap" : "篮球"}).collect(Collectors.toList());
List<Person> jdbcList = IntStream.rangeClosed(0, n).mapToObj(i -> new Person().setName("jdbcTemplate").setAge(i + 10).setAddress("hubei" + i).setGoodAt(i / 2 == 0 ? "唱跳rap" : "篮球")).collect(Collectors.toList());
List<Person> jdbcList2 = IntStream.rangeClosed(0, n).mapToObj(i -> new Person().setName("jdbcTemplate2").setAge(i + 10).setAddress("hubei" + i).setGoodAt(i / 2 == 0 ? "唱跳rap" : "篮球")).collect(Collectors.toList());
List<Person> mbpList = IntStream.rangeClosed(0, n).mapToObj(i -> new Person().setName("mbp").setAge(i + 10).setAddress("hubei" + i).setGoodAt(i / 2 == 0 ? "唱跳rap" : "篮球")).collect(Collectors.toList());
jdbcTemplate.execute("truncate table person");
StopWatch watch = new StopWatch();

watch.start("jdbcTemplate batch1");
int[] ints = jdbcTemplateManager.addPersons(list);
watch.stop();

watch.start("jdbcTemplate batch2");
int[][] ints2 = jdbcTemplateManager.addPersons2(jdbcList);
watch.stop();

watch.start("jpa saveAllAndFlush");
List<Person> flush = personRepo.saveAllAndFlush(jdbcList2);
watch.stop();

//mybatis批量插入插件
watch.start("mybatisPlus batch");
Integer batchSomeColumn = personMapper.insertBatchSomeColumn(mbpList);
watch.stop();

System.out.println(watch.prettyPrint());
}
}

@Service
@RequiredArgsConstructor
public class JdbcTemplateManager {
private final JdbcTemplate jdbcTemplate;

public int[] addPersons(List<Object[]> args) {
return jdbcTemplate.batchUpdate("insert into person (name,age,address,goodAt) values (?,?,?,?)", args);
}

public int[][] addPersons2(List<Person> personList) {
return jdbcTemplate.batchUpdate("insert into person (name,age,address,goodAt) values (?,?,?,?)", personList, 50, (ps, p) -> {
ps.setString(1, p.getName());
ps.setInt(2, p.getAge());
ps.setString(3, p.getAddress());
ps.setString(4, p.getGoodAt());
});
}
}

@Table(name = "person")
@Entity(name = "person")
@Data
@TableName("person")
public class Person extends BaseEntity {
private String name;
private Integer age;
private String address;
private String goodAt;

}

七、杂想

  • 总的来说个人还是更喜欢使用 JPA 的,在编写 JPQL 的时候如果又字段不对,IDEA 也会进行对应的标红提示,基本做到了类型安全。
  • 在开发中,简单的 sql 使用 jpa 内置方法,声明符合规则的方法即可,较复杂的使用 JPQL 查询也可以解决很多问题。更复杂的可以使用 Specification 构建查询规则,或者使用 jdbcTemplate 完全手写 sql 也很不错
  • 希望开发中能早日用上文本块,这样 sql 换行也就更美观了
  • 面对太复杂的动态 sql 查询,jpa 没有好的解决办法,可以考虑引入 mybatis,限制只做单纯的查询操作,增删改操作全部使用 jpa 完成,这样也能可以做非常好的弥补。
  • 也使用过 mybatis-plus,在 service 中使用 lambda wrapper,会让代码非常丑陋,dao 层的逻辑侵入到 service 中,非常不利于维护,后面将 wrapper 代码放到 mapper 的 default 方法解决了此问题;IService 也不是很好的实现,虽然可以抛弃不用;api 变动频繁,关于自动填充部分的接口变化太快,虽然官方文档有说明,但还是不太合理
  • querydsl:jpa 可以很方便的引入 querydsl,曾经也使用过,可以编写类型安全的复杂查询,但是也做不到条件查询,而且代码可读性也不高,最终放弃

JPA使用总结
https://polarisink.github.io/20230220/yuque/JPA使用总结/
作者
Areis
发布于
2023年2月20日
许可协议