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