SpringBoot集成SpringDataJPA项目实操
前言
该说的在《SpringBoot集成Mybatis项目实操》一文中都讲了,本文只是将 Mybatis 换成了 Spring Data JPA,带大家将整个项目跑起来。
本文将实现 SpringBoot+ Spring Data JPA 的项目搭建,项目特色是针对 JPA 专门写了一套动态链式查询工具类,进而实现一对多查询和连表查询。
不说废话了,我们直接进入主题。数据库
本项目采用的是 MySQL 数据库,版本为 8.x,建表语句如下:CREATE TABLE `customer` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `age` int(11) DEFAULT NULL, `address` varchar(100) DEFAULT NULL, `created_date` timestamp NULL DEFAULT NULL, `last_modified_date` timestamp NULL DEFAULT NULL, `del_flag` int(2) NOT NULL DEFAULT "0", `create_user_name` varchar(50) DEFAULT NULL, `last_modified_name` varchar(50) DEFAULT NULL, `version` int(11) NOT NULL DEFAULT "0", PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="父用户"; CREATE TABLE `subUser` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(20) DEFAULT NULL, `customer_id` varchar(36) NOT NULL, `address` varchar(100) DEFAULT NULL, `created_date` timestamp NULL DEFAULT NULL, `last_modified_date` timestamp NULL DEFAULT NULL, `del_flag` int(2) NOT NULL DEFAULT "0", `create_user_name` varchar(50) DEFAULT NULL, `last_modified_name` varchar(50) DEFAULT NULL, `version` int(11) NOT NULL DEFAULT "0", PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT="子用户"; 复制代码搭建SpringBoot项目
使用 IDEA 新建一个 Maven 项目,叫做 jpa-springboot。
一些共用的基础代码可以参考上篇文章,这里不做重复介绍,会介绍一些 JPA 相关的代码。引入依赖 org.springframework.boot spring-boot-starter-parent 2.6.3 1.8 1.2.73 5.5.1 8.0.19 2.1.4 4.1.5 1.4.2.Final 1.18.20 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-validation com.alibaba fastjson ${fastjson.version} cn.hutool hutool-all ${hutool.version} org.projectlombok lombok ${org.projectlombok.version} true org.springframework.boot spring-boot-starter-test test mysql mysql-connector-java ${mysql.version} runtime org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.version} org.springframework.boot spring-boot-starter-data-jpa org.springdoc springdoc-openapi-ui 1.6.9 com.alibaba druid-spring-boot-starter 1.1.18 org.mapstruct mapstruct ${org.mapstruct.version} org.mapstruct mapstruct-processor ${org.mapstruct.version} org.springframework.boot spring-boot-maven-plugin 复制代码
有些依赖不一定是最新版本,而且你看到这篇文章时,可能已经发布了新版本,到时候可以先模仿着将项目跑起来后,再根据自己的需求来升级各项依赖,有问题咱再解决问题。分页处理
某些业务场景是需要分页查询和排序功能的,所以我们需要考虑前端如何传递参数给后端,后端如何进行分页查询或者是排序查询。JPA 分页查询使用的是 Spring 自带的 Pageable。
分页基础类public class SimplePageInfo { private Integer pageNum = 1; private Integer pageSize = 10; public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } } 复制代码
排序包装类@Getter @Setter public class OrderInfo { private boolean asc = true; private String column; } 复制代码
分页且排序包装类@Getter @Setter @EqualsAndHashCode(callSuper = true) public class PageSortInfo extends SimplePageInfo { @Schema(name = "排序信息") private List orderInfos; public String parseSort() { if (CollectionUtils.isEmpty(orderInfos)) { return null; } StringBuilder sb = new StringBuilder(); for (OrderInfo orderInfo : orderInfos) { sb.append(orderInfo.getColumn()).append(" "); sb.append(orderInfo.isAsc() ? " ASC," : " DESC,"); } sb.deleteCharAt(sb.length() - 1); return sb.toString(); } } 复制代码
前端分页查询的请求体对象@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder public class CustomerQueryPageDTO { @JsonUnwrapped private PageSortInfo pageSortInfo; } 复制代码
服务层分页查询Pageable pageable = SpecificationBuilder.getPageable(dto.getPageSortInfo()); Page customerPage = customerRepository.findAll(pageable); 复制代码
关于 PageHelper 的使用这里就不多做介绍了。
我们得到的分页查询结果是 Page 对象,可以直接使用,也可以根据需要进行修改,比如下面这个文件:@Getter @Setter public class PageResult { /** * 总条数 */ private Long total; /** * 总页数 */ private Integer pageCount; /** * 每页数量 */ private Integer pageSize; /** * 当前页码 */ private Integer pageNum; /** * 分页数据 */ private List data; /** * 处理Jpa分页结果,Jpa页码默认从0开始,所以返回结果加1 */ public static PageResult ok(org.springframework.data.domain.Page page) { PageResult result = new PageResult(); result.setPageCount(page.getTotalPages()); result.setPageNum(page.getNumber() + 1); result.setPageSize(page.getSize()); result.setTotal(page.getTotalElements()); result.setData(page.getContent()); return result; } } 复制代码JPA基础实体类
作为其他实体类的父类,封装了所有的公共字段,包括逻辑删除标志,版本号,创建人和修改人信息。到底是否需要那么多字段,结合实际情况,这里的示例代码比较全,其中@LogicDelete 和@Version 是 Mybatis 特有的注解,@CreatedBy、@CreatedDate 是Springframework 自带的注解,如果我们需要新建人和修改人姓名,则需要自定义注解。@MappedSuperclass @EntityListeners(AuditingEntityListener.class) @Getter @EqualsAndHashCode(of = "id") @SuperBuilder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public class BaseDomain implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Schema(name = "创建人姓名") @CreatedBy @Column(name = "create_user_name") private String createUserName; @CreatedDate private LocalDateTime createdDate; @LastModifiedBy @Schema(name = "修改人姓名") @Column(name = "last_modified_name") private String lastModifiedName; @LastModifiedDate private LocalDateTime lastModifiedDate; @Schema(name = "") @Column(name = "del_flag") private Integer delFlag = 0; @Schema(name = "版本号") @Version @Column(name = "version") private Integer version; } 复制代码
可以发现,相较于 Mybatis 和 MybatisPlus 少了两个字段,分别是 createUserCode 和 lastModifiedCode,因为 Spring 提供的注解只有 CreatedBy 和 LastModifiedBy,我们可以基于 Spring Data 的 AuditorAware 审计功能来给这两个注解标识的字段赋值。JPA审计
简单介绍一下审计功能:即由谁在什么时候创建或修改实体。Spring Data 提供了在实体类的属性上增加@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate 注解,并配置相应的配置项,即可实现审计功能,有系统自动记录createdBy 、CreatedDate 、lastModifiedBy 、lastModifiedDate四个属性的值。
所以如果在 AuditorAware 实现类中根据@CreatedBy 拿到的值去数据库中查询 userCode 信息,也不是不可以,这里项目比较简单,所以就不过多介绍了。@Configuration public class JpaAutoConfiguration implements AuditorAware { @Override public Optional getCurrentAuditor() { SecurityContext ctx = SecurityContextHolder.getContext(); Object principal = ctx.getAuthentication().getPrincipal(); if (principal.getClass().isAssignableFrom(String.class)) { return Optional.of((String) principal); } else { return Optional.empty(); } } } 复制代码
因为我们没有使用 SpringSecurity 来配置 token 信息,所以这里获取的 principal 是默认值,值为 anonymousUser。动态链式查询
利用 JPA 的Specification接口和元模型就实现动态查询,相较于这篇文章,本文实现借鉴了 Mybatis 中使用的 ExampleBuilder,个人觉得效果更佳。
1、查询关系匹配枚举public enum MatchCondition { /** * equal-相等,notEqual-不等于,like-模糊匹配,notLike-, gt-大于,ge-大于等于,lt-小于,le-小于等于 */ EQUAL, NOT_EQUAL, LIKE, NOT_LIKE, GT, GE, LT, LE, IN, NOT_IN, BETWEEN, NOT_BETWEEN } 复制代码
2、查询条件连接符枚举public enum Operator { AND, OR } 复制代码
3、查询条件注解@Target({ElementType.FIELD, ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.RUNTIME) public @interface QueryCondition { /** * 数据库中字段名,默认为空字符串,则Query类中的字段要与数据库中字段一致 */ String column() default ""; /** * @see MatchCondition */ MatchCondition func() default MatchCondition.EQUAL; } 复制代码
4、连接符枚举public enum Connector { ON, WHERE } 复制代码
5、查询条件包装类//理论上会有多个QueryParam对象,当Connector是where时,joinName和joinType为null,当Connector为on时,根据joinName的不同,会生成不同的QueryParam对象,joinType包括INNER、LEFT @Data @NoArgsConstructor @AllArgsConstructor @Builder public class QueryParam { private Connector connector; private List queryItems; private String joinName; private JoinType joinType; } @Data @NoArgsConstructor @AllArgsConstructor @Builder public class QueryItem { private String fieldName; private Object fieldValue; private MatchCondition matchCondition; // between使用 private Object startValue; private Object endValue; // in查询 private Iterable