前期准备
mybatis需要添加的依赖
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
mapStruct需要添加的依赖
<!--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>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
同时这边还添加了lombok插件方便开发
<!--lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
开始
集成mybatis(以下集成过程中,以获取验证码实现为例)
1、准备一个captcha类,在数据库中存储不同的验证码类型,以便以后根据业务需求切换不同的验证码类型,sql语句如下:
DROP TABLE IF EXISTS `captcha`;
CREATE TABLE `captcha` (
`id` bigint(20) NOT NULL COMMENT 'id',
`type` int(10) NOT NULL COMMENT '验证码类型',
`font_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字体名字',
`font_style` int(10) NULL DEFAULT NULL COMMENT '字体风格',
`font_size` int(10) NULL DEFAULT NULL COMMENT '字体大小',
`width` int(10) NULL DEFAULT NULL COMMENT '宽度',
`height` int(10) NULL DEFAULT NULL COMMENT '高度',
`len` int(10) NULL DEFAULT NULL COMMENT '位数',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
类结构如下
@Data
public class Captcha implements Serializable {
/** id */
private Long id;
/** 验证码类型 */
@NotNull
private Integer type;
/** 字体名字 */
private String fontName;
/** 字体风格 */
private Integer fontStyle;
/** 字体大小 */
private Integer fontSize;
/** 宽度 */
private Integer width;
/** 高度 */
private Integer height;
/** 位数 */
private Integer len;
public void copy(Captcha source){
BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true));
}
}
2、项目结构如下:
3、为了使项目启动,能将所有的IxxxMapper接口注入到容器中,选择在启动类xxxApplication中添加@MapperScan注解,value值为IxxxMapper接口所在包目录
项目启动时,因为启动类上添加了@MapperScan注解,所以会自动到该注解指定的包下扫描所有的mapper接口,并注入到ioc容器中,这是第①步,是接口当然要有实现类,所以这里第②步resources下mapper路径下的ICaptchaMapper.xml就相当于接口的实现类(这样解释易于理解),这里就是mybatis相比于Spring Data JPA与Hibernate这些ORM框架不一样的地方。
通过以下配置,可以将Mapper接口和Xml文件联系起来
在application.yml配置文件中配置
mybatis:
mapper-locations: classpath:mapper/*.xml
具体底层是怎么建立联系的?百度、谷歌~
https://blog.csdn.net/a745233700/article/details/89308762
https://juejin.cn/post/6990554478533410853
3、ICaptchaMapper.java接口代码,主要关注findById这个方法
public interface ICaptchaMapper {
Captcha findById(long id);
List<Captcha> findAllCaptchas();
int addCaptcha(Captcha captcha);
Captcha findCaptchaByTypeAndFontName(Captcha captcha);
Captcha findUserByIds(CaptchaDto dto);
}
对应的ICaptchaMapper.xml内容如下(包含一些用法解释)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace:填写映射当前的Mapper接口,所有的增删改查的参数和返回值类型,就可以直接填写缩写,不区分大小写,直接通过方法名去找类型-->
<mapper namespace="com.youxi.chenmuke.mapper.ICaptchaMapper">
<!-- sql:里面可以写入一个共同的sql代码,用于提取重复的代码。
要使用该代码的时候就直接使用<include>标签,id:为提取的sql代码,取一个id,起标识作用-->
<sql id="select">
select * from captcha
</sql>
<!--
public Captcha findById(int id);
id:填写在XxxMapper接口中的方法名
parameterType:填写参数的类型
resultType:填写方法中返回值的类型,不用写全路径,不区分大小写,因为用了free mybatis plugins 插件这里用了全路径
-->
<select id="findById" parameterType="long" resultType="com.youxi.chenmuke.entity.Captcha">
<!--
include:用于加载提取公共的sql语句,与<sql>标签对应
refid:填写<sql>标签中的id属性
-->
<include refid="select"></include>
where id = #{id}
</select>
<!-- resultMap属性:与resultMap标签一起使用,填写resultMap标签中定义的id属性 -->
<select id="findAllCaptchas" resultMap="captchas">
select * from orders
</select>
<!-- resultMap标签:用于自定义封装结果
type:最终结果还是封装到实体类中,type就是指定封装到哪一个类中
id:与<select>标签中的resultMap中的属性一致,一定要唯一
<id>:该标签是指定主键封装到实体类中的哪一个属性(可以省略)
<result>:该标签是其他的列封装到实体类中,一般只需填写实体类中的属性与表中列不同的项即可
property:填写实体类中的属性,column:填写表中的列名
-->
<resultMap type="com.youxi.chenmuke.entity.Captcha" id="captchas">
<id property="id" column="id"/>
<result property="type" column="type"/>
<result property="fontName" column="font_name"/>
<result property="fontStyle" column="font_style"/>
<result property="fontSize" column="font_size"/>
<result property="width" column="width"/>
<result property="height" column="height"/>
<result property="len" column="len"/>
</resultMap>
<!--
public void addCaptcha(Captcha captcha);
insert:用于执行添加语句;
update:执行更新语句
delete:执行删除语句
-->
<insert id="addCaptcha" parameterType="com.youxi.chenmuke.entity.Captcha">
<!--
selectKey配置主键信息的标签
keyColumn:对应数据库表中的主键列
keyProperty:对应实体类中的属性
after:代表执行下面代码之前,先执行当前里面的代码
-->
<selectKey keyColumn="id" keyProperty="id" order="AFTER" resultType="int">
select LAST_INSERT_ID()
</selectKey>
insert into captcha
(`type`,font_name,font_style,font_size,width,height,len)
values(#{type},#{font_name},#{font_style},#{font_size},#{width},#{height},#{len})
</insert>
<!-- public List<Captcha> findCaptchaByTypeAndFontName(Captcha captcha); -->
<select id="findCaptchaByTypeAndFontName"
parameterType="com.youxi.chenmuke.entity.Captcha"
resultType="com.youxi.chenmuke.entity.Captcha">
<!--select * from captcha where 1=1 -->
<include refid="select"></include>
<!-- where标签:一个where条件语句,通常和<if>标签混合使用 -->
<where>
<!--
if标签:执行一个判断语句,成立才会执行标签体内的sql语句
test:写上条件判断语句
注意:这里每一个if前面都尽量加上and,如果你是第一个条件,框架会自动帮你把and截取,如果是第二个if就不能省略and
-->
<if test="type != null and type != ''">
and `type` = #{type}
</if>
<if test="fontName != null and fontName != ''">
and font_name like '%${fontName}%'
</if>
</where>
</select>
<!-- public List<Captcha> findUserByIds(CaptchaDto dto); -->
<!-- QueryVo:是一个实体包装类,通常用于封装实体类之外的一些属性-->
<select id="findUserByIds"
parameterType="com.youxi.chenmuke.dto.CaptchaDto"
resultType="com.youxi.chenmuke.entity.Captcha">
<include refid="select"></include>
<where>
<!-- foreach:循环语句,通常多用于参数是集合时,需要对参数进行遍历出来,再进行赋值查询
collection:参数类型中的集合、数组的名字,例:下面的ids就是QueryVo这个类中的list集合的名字
item:为遍历该集合起一个变量名,遍历出来的每一个字,都赋值到这个item中
open:在sql语句前面添加的sql片段
close:在sql语句后面添加的sql片段
separator:指定遍历元素之前用什么分隔符
-->
<foreach collection="ids" item="id" open="id in(" close=")" separator=",">
#{id}
</foreach>
</where>
</select>
</mapper>
4、建立CaptchaServiceImpl服务层用来将ICaptchaMapper接口注入并使用
@Service
public class CaptchaServiceImpl implements ICaptchaService {
@Autowired
private ICaptchaMapper captchaMapper;
@Override
public Captcha findById(Long id) {
Captcha captcha = captchaMapper.findById(id);
return captcha;
}
}
这样就能成功利用mybatis,在xml文件中自定义sql语句从数据库DO返回到service层处理~
使用mapStruct将PO==>DTO
一般在系统中,有最经典的分层:
数据存储层、业务逻辑层、展示层
数据存储层,我们使用PO来抽象一个业务实体;在业务逻辑层,我们使用DTO来表示数据传输对象;到了展示层,我们又把对象封装成VO来与前端进行交互
以下是对DTO、VO、BO、PO、DO、POJO概念的解释
POJO的定义是无规则简单的对象,在日常的代码分层中pojo会被分为VO、BO、 PO、 DTO
VO (view object/value object)表示层对象
1、前端展示的数据,在接口数据返回给前端的时候需要转成VO
2、个人理解使用场景,接口层服务中,将DTO转成VO,返回给前台
B0(bussines object)业务层对象
1、主要在服务内部使用的业务对象
2、可以包含多个对象,可以用于对象的聚合操作
3、个人理解使用场景,在服务层服务中,由DTO转成BO然后进行业务处理后,转成DTO返回到接口层
PO(persistent object)持久对象
1、出现位置为数据库数据,用来存储数据库提取的数据
2、只存储数据,不包含数据操作
3、个人理解使用场景,在数据库层中,获取的数据库数据存储到PO中,然后转为DTO返回到服务层中
DTO(Data Transfer Object)数据传输对象
1、在服务间的调用中,传输的数据对象
2、个人理解,DTO是可以存在于各层服务中(接口、服务、数据库等等)服务间的交互使用DTO来解耦
DO(domain object)领域实体对象
DO 现在主要有两个版本:
①阿里巴巴的开发手册中的定义,DO( Data Object)这个等同于上面的PO
②DDD(Domain-Driven Design)领域驱动设计中,DO(Domain Object)这个等同于上面的BO
参考文档:
https://juejin.cn/post/6952848675924082718
https://juejin.cn/post/6844904046097072141
https://zhuanlan.zhihu.com/p/264675395
在实际使用场景中,如果需要严格划分各层级间使用的数据区别,且在各层服务间的交互需要使用DTO,如果用下面这种写法:
userDTO.setName(userPO.getName());
userDTO.setAge(userPO.getAge());
使用getter/setter的话,会产生很多的冗余代码,而且属性较多的时候会生成比较多了getter/setter,所以我们引入了mapStruct的使用
MapStruct的使用(以下集成过程中,以获取用户信息实现为例)
MapStruct(mapstruct.org/ )是一种代码生成器,它极大地简化了基于"约定优于配置"方法的Java bean类型之间映射的实现。生成的映射代码使用纯方法调用,因此快速、类型安全且易于理解。
约定优于配置,也称作按约定编程,是一种软件设计范式,旨在减少软件开发人员需做决定的数量,获得简单的好处,而又不失灵活性。
前面我们已经添加了mapstruct依赖,现在直接开始使用吧~
用户对象类 User (PO)
@Getter
@Setter
public class User implements Serializable {
@NotNull(groups = Update.class)
private Long id;
@NotBlank
private String username;
/** 用户昵称 */
@NotBlank
private String nickName;
/** 性别 */
private String sex;
@NotBlank
@Email
private String email;
@NotBlank
private String phone;
@NotNull
private Boolean enabled;
private String password;
private Date lastPasswordResetTime;
/**
* 一对一
*/
private UserAvatar userAvatar;
/**
* 多对多
*/
private Set<Role> roles;
/**
* 一对一
*/
private Job job;
/**
* 一对一
*/
private Dept dept;
private Timestamp createTime;
public @interface Update {}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return Objects.equals(id, user.id) &&
Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(id, username);
}
}
层级间用户传输类 UserDto (DTO)
@Getter
@Setter
public class UserDto implements Serializable {
@ApiModelProperty(hidden = true)
private Long id;
private String username;
private String nickName;
private String sex;
private String avatar;
private String email;
private String phone;
private Boolean enabled;
@JsonIgnore
private String password;
private Date lastPasswordResetTime;
@ApiModelProperty(hidden = true)
private Set<Role> roles;
@ApiModelProperty(hidden = true)
private Job job;
private Dept dept;
private Long jobId;
private Long deptId;
private Timestamp createTime;
}
UserService层代码
public UserDto findByName(String userName) {
User user;
if(ValidationUtil.isEmail(userName)){
user = userMapper.findByEmail(userName);
} else {
user = userMapper.findByUsername(userName);
}
if (user == null) {
throw new EntityNotFoundException(User.class, "name", userName);
} else {
return userConvert.toDto(user);
}
}
执行UserService层的findByName方法后,需要返回UserDto 对象,如果User 转 UserDto的过程每个类都手写,重复的动作很麻烦,所以这里使用mapStruct在编译时动态生成转换类的代码。
首先需要准备一个转换接口,大部分转换方法都是一样的,所以可以写一个父类,然后每个对应类的接口继承那个父类,有特定的转换再重写或者子类中自定义就好,代码如下:
父类BaseConvert.java
public interface BaseConvert<D,E> {
/**
* DTO转Entity
*/
E toEntity(D dto);
/**
* Entity转DTO
*/
D toDto(E entity);
/**
* DTO集合转Entity集合
*/
List<E> toEntity(List<D> dtoList);
/**
* Entity集合转DTO集合
*/
List<D> toDto(List<E> entityList);
}
子类UserConvert基础BaseConvert
@Component
@Mapper(componentModel = "spring")
public interface UserConvert extends BaseConvert<UserDto, User> {
@Mapping(source = "user.userAvatar.realName",target = "avatar")
@Mapping(source = "user.dept.id",target = "deptId")
@Mapping(source = "user.job.id",target = "jobId")
UserDto toDto(User user);
}
上面有几个注意点:
1、需要在UserConvert类上加上@Mapper注解,这个注解属于package org.mapstruct包,具体一些参数的详细解释去谷歌哦,給父类泛型传具体的类型
2、低版本的@Mapping是无法重复注解的,意味着没有@Repeatable这个注解,版本高一点才支持,比如
mapStruct版本为1.2.0.Final的时候,对应的注解@Mapping代码如下:
所以就会报错
mapStruct版本为1.3.1.Final的时候,对应的注解@Mapping代码如下:
发现加上了@Repeatable注解,就没有问题啦~
3、既然是PO => DTO 的转换,也就是User ===> UserDto ,意味着有一个转换的初始源,也有一个转换的目标源,所以初始源就是User,目标源就是UserDto
@Mapping(source = "user.userAvatar.realName",target = "avatar")
UserDto toDto(User user);
这段代码中,@Mapping里的参数source = "user.userAvatar.realName",target = "avatar",意思就是在生成动态转换代码的时候,需要将toDto方法里的那个user对象里的属性userAvatar(这个是个对象),再获取userAvatar这个对象里的string类型的realName属性,赋值给UserDto对象里string类型的avatar属性,也就是如下代码:
userDto.setAvatar( userUserAvatarRealName( user ) );
userUserAvatarRealName方法如下:
private String userUserAvatarRealName(User user) {
if ( user == null ) {
return null;
}
UserAvatar userAvatar = user.getUserAvatar();
if ( userAvatar == null ) {
return null;
}
String realName = userAvatar.getRealName();
if ( realName == null ) {
return null;
}
return realName;
}
如果不写这段@Mapping(source = "user.userAvatar.realName",target = "avatar"),在UserDto里avatar这个属性名在User里面找不到,User的属性是userAvatar,所以就没法赋值。
这样操作下来,编译后就动态生成代码啦
注意我红框框出来的地方,因为我刚才手痒编辑了一下,提示动态生成的源文件没法被编辑,编辑带来的代码改变会在代码重新生成的时候丢失~
OK,这部分内容就分享到这~