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,這部分內容就分享到這~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容