目录
背景
说一下我目前对数据持久层的看法:在后端开发中,接口开发是一个后端必备的技能,但是如何才能优雅,高效的去完成这个拧螺丝的CRUD呢?一般我们会使用MyBatis作为持久层,但是这个需要自己配置XML写映射和SQL语句,或者自己写映射注解和SQL语句,所以就诞生了MyBatis-plus,这个有效的减少繁琐的xml等一些的依赖配置,但是目前好像只适合用于单表的操作,多表操作还是需要自己手动做map映射,一个个手写SQL,相对还是挺麻烦的,不过手动配置也有它的好处,那就是自己操作的空间更大,操作也可以按自己思路去灵活多变,但缺点就是太麻烦了。然后一般我们也会使用Sping-Data-jpa去作为持久层就是开发,这个就相当于解放了双手,真的是大部分的数据操作都是可以无配置实现,并且做多表操作,级联操作也很方便,如果有需要自己写Sql配置的,也可以使用@Query注解方便的进行配置,可以说,jpa的开发效率确实很高,但缺点就是操作相对没有那么灵活,如果数据量庞大,还是需要自己去手动配置,有可能就会被硬性要求使用MyBatis。还有就是Sping-Data-jpa可以很方便的集成其他的一些数据工具,比如ElasticSearch等。
有下面的一个问题:
如果需要一个接口,可以根据传进来的不确定的多个参数动态的,条件的查询数据,这个需要怎么处理呢?
其实这个问题在之前的做项目中也有遇到过,处理方法是使用MyBatis的配置语句动态进行拼接,有一种if-else的感觉,当时做这个需求还是挺麻烦的。
所以需要一个更加优雅。更加高效的处理方法:我最近在项目中遇到过一个工具:QueryHelp。
QueryHelp介绍
QueryHelp其实只是一个工具类,这个工具类中对在真实开发中常用的查询方法进行了封装,在操作持节层的时候也是基于Spring-Data-Jpa的,只需要配置一个序列化的QueryCriteria 作为本次数据操作的条件,进行简单的配置,即可以做到一个接口可以进行多条件,模糊,精准,甚至多表连接后的数据进行条件查询。
先看一下效果吧:
这有一个非常精简的查询接口,但这个查询接口却具有很大的魔力
- 这是正常的查询全部。
- 这是多条件模糊查询
- 这是分页查询
当然参数既可以条件也可以加分页。
QueryHelp的使用步骤
在本次的项目demo中,使用到了上篇文章的Mapstruct,和上上篇文章的Jdk8新特性。
0. 目录结构如下
1. 建表
-- ----------------------------
-- Table structure for dept
-- ----------------------------
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of dept
-- ----------------------------
INSERT INTO `dept` VALUES (1, '软件部');
INSERT INTO `dept` VALUES (2, '产品部');
INSERT INTO `dept` VALUES (3, '测试部');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`did` int(11) NULL DEFAULT NULL,
`createtime` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP,
`updatetime` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (2, 'Jack', 20, 'test2@baomidou.com', 1, '2021-08-17 09:45:31', '2021-09-02 14:49:48');
INSERT INTO `user` VALUES (3, 'Tom', 28, 'test3@baomidou.com', 3, '2021-08-17 09:45:31', '2021-09-02 14:50:17');
INSERT INTO `user` VALUES (4, 'Sandy', 21, 'test4@baomidou.com', 2, '2021-08-17 09:45:31', '2021-09-02 14:49:48');
INSERT INTO `user` VALUES (5, 'Billie', 24, 'test5@qq.com', 2, '2021-08-17 09:45:31', '2021-09-02 14:49:49');
INSERT INTO `user` VALUES (13, '如我西沉', 21, '12055@qq.com', 1, '2021-08-17 10:04:54', '2021-09-02 14:49:53');
INSERT INTO `user` VALUES (14, 'Nick', 21, '12055@qq.com', 1, '2021-08-19 18:19:00', '2021-09-02 14:49:51');
2. 导入坐标依赖
<properties>
<java.version>1.8</java.version>
<mapstruct.version>1.3.1.Final</mapstruct.version>
<hutool.version>5.3.4</hutool.version>
<commons-pool2.version>2.5.0</commons-pool2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>3
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!-- spring boot 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--Spring boot Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring boot 集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--mapStruct依赖-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Apache的BeanUtils依赖 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>
3. 创建工具类注解(重点)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by tao.
* Date: 2021/9/2 10:59
* 描述: 条件查询注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {
//基本对象的属性名
String propName() default "";
//查询方式
Type type() default Type.EQUAL;
/**
* 连接查询的属性名,如User类中的dept
*/
String joinName() default "";
/**
* 默认左连接
*/
Join join() default Join.LEFT;
/**
* 多字段模糊搜索,仅支持String类型字段,多个用逗号隔开, 如@Query(blurry = "email,username")
*/
String blurry() default "";
enum Type {
//相等
EQUAL
//大于等于
, GREATER_THAN
//小于等于
, LESS_THAN
//中模糊查询
, INNER_LIKE
//左模糊查询
, LEFT_LIKE
//右模糊查询
, RIGHT_LIKE
//小于
, LESS_THAN_NQ
//包含
, IN
// 不等于
, NOT_EQUAL
// between
, BETWEEN
// 不为空
, NOT_NULL
// 为空
, IS_NULL
}
/**
* @author tao
* 适用于简单连接查询,复杂的请自定义该注解,或者使用sql查询
*/
enum Join {
LEFT, RIGHT, INNER
}
}
4. 创建QueryHelp工具类和PageUtil工具类(重点)
- QueryHelp
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.kt.mapstructdemo.annotation.Query;
import lombok.extern.slf4j.Slf4j;
import javax.persistence.criteria.*;
import java.lang.reflect.Field;
import java.util.*;
/**
* Created by tao.
* Date: 2021/9/2 11:15
* 描述:
*/
@Slf4j
@SuppressWarnings({"unchecked", "all"})
public class QueryHelp {
public static <R, Q> Predicate getPredicate(Root<R> root, Q query, CriteriaBuilder cb) {
List<Predicate> list = new ArrayList<>();
if (query == null) {
return cb.and(list.toArray(new Predicate[0]));
}
try {
List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
for (Field field : fields) {
boolean accessible = field.isAccessible();
// 设置对象的访问权限,保证对private的属性的访
field.setAccessible(true);
Query q = field.getAnnotation(Query.class);
if (q != null) {
String propName = q.propName();
String joinName = q.joinName();
String blurry = q.blurry();
String attributeName = isBlank(propName) ? field.getName() : propName;
Class<?> fieldType = field.getType();
Object val = field.get(query);
if (ObjectUtil.isNull(val) || "".equals(val)) {
continue;
}
Join join = null;
// 模糊多字段
if (ObjectUtil.isNotEmpty(blurry)) {
String[] blurrys = blurry.split(",");
List<Predicate> orPredicate = new ArrayList<>();
for (String s : blurrys) {
orPredicate.add(cb.like(root.get(s)
.as(String.class), "%" + val.toString() + "%"));
}
Predicate[] p = new Predicate[orPredicate.size()];
list.add(cb.or(orPredicate.toArray(p)));
continue;
}
if (ObjectUtil.isNotEmpty(joinName)) {
String[] joinNames = joinName.split(">");
for (String name : joinNames) {
switch (q.join()) {
case LEFT:
if (ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)) {
join = join.join(name, JoinType.LEFT);
} else {
join = root.join(name, JoinType.LEFT);
}
break;
case RIGHT:
if (ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)) {
join = join.join(name, JoinType.RIGHT);
} else {
join = root.join(name, JoinType.RIGHT);
}
break;
case INNER:
if (ObjectUtil.isNotNull(join) && ObjectUtil.isNotNull(val)) {
join = join.join(name, JoinType.INNER);
} else {
join = root.join(name, JoinType.INNER);
}
break;
default:
break;
}
}
}
switch (q.type()) {
case EQUAL:
list.add(cb.equal(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), val));
break;
case GREATER_THAN:
list.add(cb.greaterThanOrEqualTo(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN:
list.add(cb.lessThanOrEqualTo(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case LESS_THAN_NQ:
list.add(cb.lessThan(getExpression(attributeName, join, root)
.as((Class<? extends Comparable>) fieldType), (Comparable) val));
break;
case INNER_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), "%" + val.toString() + "%"));
break;
case LEFT_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), "%" + val.toString()));
break;
case RIGHT_LIKE:
list.add(cb.like(getExpression(attributeName, join, root)
.as(String.class), val.toString() + "%"));
break;
case IN:
if (CollUtil.isNotEmpty((Collection<Long>) val)) {
list.add(getExpression(attributeName, join, root).in((Collection<Long>) val));
}
break;
case NOT_EQUAL:
list.add(cb.notEqual(getExpression(attributeName, join, root), val));
break;
case NOT_NULL:
list.add(cb.isNotNull(getExpression(attributeName, join, root)));
break;
case IS_NULL:
list.add(cb.isNull(getExpression(attributeName, join, root)));
break;
case BETWEEN:
List<Object> between = new ArrayList<>((List<Object>) val);
list.add(cb.between(getExpression(attributeName, join, root).as((Class<? extends Comparable>) between.get(0).getClass()),
(Comparable) between.get(0), (Comparable) between.get(1)));
break;
default:
break;
}
}
field.setAccessible(accessible);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
int size = list.size();
return cb.and(list.toArray(new Predicate[size]));
}
@SuppressWarnings("unchecked")
private static <T, R> Expression<T> getExpression(String attributeName, Join join, Root<R> root) {
if (ObjectUtil.isNotEmpty(join)) {
return join.get(attributeName);
} else {
return root.get(attributeName);
}
}
private static boolean isBlank(final CharSequence cs) {
int strLen;
if (cs == null || (strLen = cs.length()) == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
public static List<Field> getAllFields(Class clazz, List<Field> fields) {
if (clazz != null) {
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
getAllFields(clazz.getSuperclass(), fields);
}
return fields;
}
}
- PageUtil
import org.springframework.data.domain.Page;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Created by tao.
* Date: 2021/9/2 11:13
* 描述: 分页工具
*/
public class PageUtil extends cn.hutool.core.util.PageUtil {
/**
* List 分页
*/
public static List toPage(int page, int size, List list) {
int fromIndex = page * size;
int toIndex = page * size + size;
if (fromIndex > list.size()) {
return new ArrayList();
} else if (toIndex >= list.size()) {
return list.subList(fromIndex, list.size());
} else {
return list.subList(fromIndex, toIndex);
}
}
/**
* Page 数据处理,预防redis反序列化报错
*/
public static Map<String, Object> toPage(Page page) {
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("content", page.getContent());
map.put("totalElements", page.getTotalElements());
return map;
}
/**
* 自定义分页
*/
public static Map<String, Object> toPage(Object object, Object totalElements) {
Map<String, Object> map = new LinkedHashMap<>(2);
map.put("content", object);
map.put("totalElements", totalElements);
return map;
}
}
5. 配置文件配置MySql
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ceshi?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
6. 配置两个实体类做一对多关联
User.java
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
/**
* Created by tao.
* Date: 2021/9/2 9:32
* 描述:
*/
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
@Table(name = "user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private Integer age;
@Column(name = "email")
private String email;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "did")
private Dept dept;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "createtime")
private Date createtime;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "updatetime")
private Date updatetime;
public User(String name, Integer age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
Dept.java
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Created by tao.
* Date: 2021/9/2 14:51
* 描述:
*/
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Data
@Table(name = "dept")
public class Dept implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "dept", fetch = FetchType.EAGER)
@JsonIgnore
private List<User> users = new ArrayList<>();
}
7. 配置Dao层
import cn.kt.mapstructdemo.domin.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
/**
* Created by tao.
* Date: 2021/9/2 10:31
* 描述:
*/
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
8. 配置Mapstruct
新建BaseMapper
import java.util.List;
/**
* Created by tao
* Date: 2021/9/2 9:32
* 描述:
*/
public interface BaseMapper<D, E> {
/**
* DTO转Entity
* @param dto /
* @return /
*/
E toEntity(D dto);
/**
* Entity转DTO
* @param entity /
* @return /
*/
D toDto(E entity);
/**
* DTO集合转Entity集合
* @param dtoList /
* @return /
*/
List <E> toEntity(List<D> dtoList);
/**
* Entity集合转DTO集合
* @param entityList /
* @return /
*/
List <D> toDto(List<E> entityList);
}
新建UserMapper.java
import cn.kt.mapstructdemo.base.BaseMapper;
import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.service.dto.UserDto;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
/**
* Created by tao.
* Date: 2021/9/2 11:04
* 描述:
*/
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper extends BaseMapper<UserDto, User> {
}
9. 配置查询条件QueryCriteria(重点)
新建 UserQueryCriteria.java
import cn.kt.mapstructdemo.annotation.Query;
import lombok.Data;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* Created by tao.
* Date: 2021/9/2 10:56
* 描述:
*/
@Data
public class UserQueryCriteria implements Serializable {
/*条件查询*/
@Query
private Long id;
/*模糊查询*/
@Query(type = Query.Type.INNER_LIKE)
private String name;
/*多字段模糊查询*/
@Query(blurry = "name,email")
private String blurry;
/*根据多表连接后的数据进行条件查询*/
@Query(propName = "id", type = Query.Type.IN, joinName = "dept")
private Set<Long> dids = new HashSet<>();
}
这个QueryCriteria的配置注解可以参考上面示例,也可以查看注解类Query,里面有很清楚详细的注释。
10. 配置Service层
UserService.java
import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.service.dto.UserDto;
import cn.kt.mapstructdemo.service.queryCriteria.UserQueryCriteria;
import org.springframework.data.domain.Pageable;
import java.util.Set;
/**
* Created by tao.
* Date: 2021/9/2 10:50
* 描述:
*/
public interface UserService {
/**
* 查询全部
*
* @return /
*/
UserDto findByAll();
/**
* 分页查询
*
* @return /
*/
Object findByAll(UserQueryCriteria criteria, Pageable pageable);
/**
* 根据ID查询
*
* @param id ID
* @return /
*/
UserDto findById(long id);
/**
* 新增用户
*
* @param user /
*/
void create(User user);
/**
* 编辑用户
*
* @param user /
*/
void update(User user);
/**
* 删除用户
*
* @param ids /
*/
void delete(Set<Long> ids);
/**
* 根据用户名查询
*
* @param userName /
* @return /
*/
UserDto findByName(String userName);
}
UserServiceImpl.java
import cn.kt.mapstructdemo.domin.User;
import cn.kt.mapstructdemo.repository.UserRepository;
import cn.kt.mapstructdemo.service.UserService;
import cn.kt.mapstructdemo.service.dto.UserDto;
import cn.kt.mapstructdemo.service.mapstruct.UserMapper;
import cn.kt.mapstructdemo.service.queryCriteria.UserQueryCriteria;
import cn.kt.mapstructdemo.utils.PageUtil;
import cn.kt.mapstructdemo.utils.QueryHelp;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.Set;
/**
* Created by tao.
* Date: 2021/9/2 10:54
* 描述:
*/
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Override
public Object findByAll(UserQueryCriteria criteria, Pageable pageable) {
Page<User> page = userRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryHelp.getPredicate(root, criteria, criteriaBuilder), pageable);
return PageUtil.toPage(page.map(userMapper::toDto));
}
@Override
public UserDto findByAll() {
return null;
}
@Override
public UserDto findById(long id) {
return null;
}
@Override
public void create(User user) {
}
@Override
public void update(User user) {
}
@Override
public void delete(Set<Long> ids) {
}
@Override
public UserDto findByName(String userName) {
return null;
}
}
11. 最后配置Controller层
UserController.java
import cn.kt.mapstructdemo.service.UserService;
import cn.kt.mapstructdemo.service.queryCriteria.UserQueryCriteria;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by tao.
* Date: 2021/9/2 13:15
* 描述:
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
/*
* 用户查询(分页)
*/
@GetMapping
public ResponseEntity<Object> query(UserQueryCriteria criteria, Pageable pageable) {
return new ResponseEntity<>(userService.findByAll(criteria, pageable), HttpStatus.OK);
}
}
12. 启动项目
启动项目就可以操作这个接口了:http://localhost:8080/api/users
小结
这个项目是自己从大项目中抽取出来的一些好用的工具,是完全自己总结、归纳的,这个期间包含了学习 Jdk8 新特性,总结对象拷贝,学习MapStruct,和学习QueryHelp。不过这些也只是开发中一些好用的工具吧,也可能就是因为这些优雅的代码,便捷的封装技术,和一些框架源码,才让大佬成为大佬的吧。路漫漫其修远兮啊,加油吧,,,
源码下载
链接:https://pan.baidu.com/s/1RL25QwUGzVKWObZlMowH3w
提取码:m13f