SpringBoot开发:集成mybatis,并使用mapStruct将PO==>DTO

前期准备

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;
image.png

类结构如下

@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、项目结构如下:


image.png

3、为了使项目启动,能将所有的IxxxMapper接口注入到容器中,选择在启动类xxxApplication中添加@MapperScan注解,value值为IxxxMapper接口所在包目录


1683795765871.png

项目启动时,因为启动类上添加了@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代码如下:


image.png

所以就会报错


image.png

mapStruct版本为1.3.1.Final的时候,对应的注解@Mapping代码如下:
image.png

发现加上了@Repeatable注解,就没有问题啦~
image.png

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,所以就没法赋值。
这样操作下来,编译后就动态生成代码啦


image.png

注意我红框框出来的地方,因为我刚才手痒编辑了一下,提示动态生成的源文件没法被编辑,编辑带来的代码改变会在代码重新生成的时候丢失~


image.png

OK,这部分内容就分享到这~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容