🔥 本文由 程序喵正在路上 原創,CSDN首發!
💖 系列專欄:Springcloud微服務
🌠 首發時間:2024年6月4日
🦋 歡迎關注🖱點贊👍收藏🌟留言🐾
目錄
- 擴展功能
- 代碼生成
- 靜態工具
- 邏輯刪除
- 枚舉處理器
- JSON處理器
- 插件功能
- 分頁插件
- 通用分頁實體
擴展功能
代碼生成
在使用 MybatisPlus 以后,基礎的 Mapper、Service、PO 代碼相對固定,重復編寫也比較麻煩。因此 MybatisPlus 官方提供了代碼生成器根據數據庫表結構生成 PO、Mapper、Service 等相關代碼。只不過代碼生成器同樣要編碼使用,也很麻煩。
所以這里推薦使用另外一款 MybatisPlus 的插件,它可以基于圖形化界面完成 MybatisPlus 的代碼生成,非常簡單。
安裝插件
在 Idea 的 plugins 市場中搜索并安裝 MyBatisPlus 插件:
然后重啟你的 Idea 即可使用。
使用
剛好數據庫中還有一張 address 表尚未生成對應的實體和 mapper 等基礎代碼,我們可以利用插件生成一下。
首先需要配置數據庫地址,在 Idea 頂部菜單中,找到 other,選擇 Config Database,然后填寫一些信息:
然后再次點擊 Idea 頂部菜單中的 other,然后選擇 Code Generator,填寫表單信息:
示例:
靜態工具
有的時候 Service 之間也會相互調用,為了避免出現循環依賴問題,MybatisPlus 提供一個靜態工具類:Db,其中的一些靜態方法與 IService 中方法簽名基本一致,也可以幫助我們實現 CRUD 功能:
下面,我們通過一些案例來學習使用靜態工具。
需求:
1、改造根據 id 查詢用戶的接口,查詢用戶的同時,查詢出用戶對應的所有地址
由于現在需要額外返回收貨地址,所以我們需要定義一個收貨地址的 VO:
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "收貨地址VO")
public class AddressVO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用戶ID")private Long userId;@ApiModelProperty("省")private String province;@ApiModelProperty("市")private String city;@ApiModelProperty("縣/區")private String town;@ApiModelProperty("手機")private String mobile;@ApiModelProperty("詳細地址")private String street;@ApiModelProperty("聯系人")private String contact;@ApiModelProperty("是否是默認 1默認 0否")private Boolean isDefault;@ApiModelProperty("備注")private String notes;
}
然后在 UserVO 中添加一個屬性:
修改 UserController 中根據 id 查詢用戶的業務接口,新建一個方法:
/*** 根據id查詢用戶** @param userId* @return*/
@GetMapping("/{id}")
@ApiOperation("根據id查詢用戶")
public UserVO queryUserById(@ApiParam("用戶id") @PathVariable("id") Long userId) {return userService.queryUserAndAddressById(userId);
}
IUserService 接口中聲明該方法:
/*** 根據id查詢用戶及其收貨地址** @param userId* @return*/
UserVO queryUserAndAddressById(Long userId);
UserServiceImpl 中實現方法:
/*** 根據id查詢用戶及其收貨地址** @param userId* @return*/
public UserVO queryUserAndAddressById(Long userId) {// 1.查詢用戶User user = getById(userId);if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶狀態異常!");}// 2.查詢收貨地址列表List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, userId).list();// 3.封裝數據UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);// 先判斷收貨地址列表是否為空if (CollUtil.isNotEmpty(addresses)) {userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));}// 4.返回數據return userVO;
}
測試:
2、改造根據 id 批量查詢用戶的接口,查詢用戶的同時,查詢出用戶對應的所有地址
UserController:
/*** 根據id集合查詢用戶** @param ids* @return*/
@GetMapping
@ApiOperation("根據id集合查詢用戶")
public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {return userService.queryUserAndAddressByIds(ids);
}
UserServiceImpl:
/*** 根據id集合查詢用戶及其收貨地址** @param ids* @return*/
public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {// 1.查詢用戶List<User> users = listByIds(ids);if (CollUtil.isEmpty(users)) {return Collections.emptyList(); // 返回空列表}// 2.查詢收貨地址List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, ids).list(); //根據用戶id查詢收貨地址List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class); //轉化地址VO//將用戶收貨地址分組處理,相同用戶的放入一個集合中Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);if (CollUtil.isNotEmpty(addressVOList)) {addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));}// 3.封裝數據List<UserVO> list = new ArrayList<>(users.size());for (User user : users) {UserVO vo = BeanUtil.copyProperties(user, UserVO.class); // 拷貝用戶信息vo.setAddresses(addressMap.get(user.getId())); // 設置用戶收貨地址list.add(vo);}return list;
}
測試:
邏輯刪除
邏輯刪除就是基于代碼邏輯模擬刪除效果,但并不會真正刪除數據庫中的數據。思路如下:
- 在表中添加一個字段標記數據是否被刪除
- 當刪除數據時把標記置為 1
- 查詢時只查詢標記為 0 的數據
一旦采用了邏輯刪除,所有的查詢和刪除邏輯都要跟著變化,非常麻煩。
為了解決這個問題,MybatisPlus 提供了邏輯刪除功能,無需改變方法調用的方式,而是在底層幫我們自動修改 CRUD 的語句。我們要做的就是在 application.yaml
文件中配置邏輯刪除的字段名稱和值即可:
在 Address 實體類中已經存在一個字段 deleted 用于邏輯刪除,所以我們不用再定義:
mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局邏輯刪除的實體字段名(since 3.3.0,配置后可以忽略不配置步驟2)logic-delete-value: 1 # 邏輯已刪除值(默認為 1, 可以不配置)logic-not-delete-value: 0 # 邏輯未刪除值(默認為 0, 可以不配置)
注意,只有 MybatisPlus 生成的 SQL 語句才支持自動的邏輯刪除,自定義 SQL 需要自己手動處理邏輯刪除。
寫一個測試類測試一下:
package com.itheima.mp.service;import com.itheima.mp.domain.po.Address;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest
class IAddressServiceTest {@Autowiredprivate IAddressService addressService;@Testvoid testDeleteByLogic() {// 刪除方法與以前沒有區別addressService.removeById(59L);}@Testvoid testQuery() {List<Address> list = addressService.list();list.forEach(System.out::println);}
}
先執行第一個測試方法,進行刪除,結果如下:
再執行第二個方法查詢一下,結果如下:
綜上, 開啟了邏輯刪除功能以后,我們就可以像普通刪除一樣做 CRUD,基本不用考慮代碼邏輯問題。還是非常方便的。
但是,邏輯刪除本身也有自己的問題,比如:
- 會導致數據庫表垃圾數據越來越多,影響查詢效率
- SQL 中全都需要對邏輯刪除字段做判斷,影響查詢效率
因此,還是不太推薦采用邏輯刪除功能,如果數據不能刪除,可以采用把數據遷移到其它表的辦法。
枚舉處理器
User 類中有一個用戶狀態字段:
像這種字段我們一般會定義一個枚舉,做業務判斷的時候就可以直接基于枚舉做比較。但是我們數據庫采用的是 int 類型,對應的PO也是 Integer。因此業務操作時必須手動把枚舉與 Integer 轉換,非常麻煩。
因此,MybatisPlus 提供了一個處理枚舉的類型轉換器,可以幫我們把枚舉類型與數據庫類型自動轉換。
定義枚舉
我們定義一個用戶狀態的枚舉:
package com.itheima.mp.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結");private final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
項目結構如下:
然后把 User 類中和 UserVO 中的 status 字段改為 UserStatus 類型。
將 UserServiceImpl 代碼中的 2
替換為 UserStatus.FREEZE
,顯得更專業一點。
要讓 MybatisPlus 處理枚舉與數據庫類型自動轉換,我們必須告訴 MybatisPlus,枚舉中的哪個字段的值作為數據庫值。
MybatisPlus 中提供了 @EnumValue
注解來標記枚舉屬性:
配置枚舉處理器
在 application.yaml 文件中添加配置:
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
然后我們測試一下查詢:
查詢成功,不過查詢出的 User 類的 status 字段是枚舉類型,可能不是很好理解。
我們在 UserStatus 枚舉中通過 @JsonValue
注解標記 JSON 序列化時要展示的字段,添加在 value 或者 desc 上都可以:
比如,我們添加在 desc 上,就會顯示 “正常” 或者 “凍結”:
JSON處理器
數據庫的 user 表中有一個 info 字段,是 JSON 類型:
而目前我們的 User 實體類中卻是 String 類型的:
這樣一來,我們要讀取 info 中的屬性時就非常不方便。如果要方便獲取,info 的類型最好是一個 Map 或者實體類。
而一旦我們把 info 改為對象類型,就需要在寫入數據庫時手動轉為 String,再讀取數據庫時,手動轉換為對象,這將會非常麻煩。
因此,MybatisPlus 為我們提供了很多特殊類型字段的類型處理器,解決特殊字段類型與數據庫類型轉換的問題。例如處理 JSON 就可以使用 JacksonTypeHandler
處理器。
接下來,我們就來看看這個處理器該如何使用。
定義實體
首先,我們在 po 下定義一個單獨實體類 UserInfo 來與 info 字段的屬性匹配:
package com.itheima.mp.domain.po;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class UserInfo {private Integer age;private String intro;private String gender;
}
使用類型處理器
接下來,將 User 類的 info 字段修改為 UserInfo 類型,并聲明類型處理器,同時開啟結果自動映射;UserVO 中的 info 字段也需要修改為 UserInfo 類型:
package com.itheima.mp.domain.po;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.itheima.mp.enums.UserStatus;
import lombok.Data;import java.time.LocalDateTime;@Data
@TableName(value = "user", autoResultMap = true) //開啟結果自動映射
public class User {@TableId(type = IdType.AUTO) //不指定的話,默認為隨機生成id,也就是第三種方式private Long id; //用戶idprivate String username; //用戶名private String password; //密碼private String phone; //注冊手機號@TableField(typeHandler = JacksonTypeHandler.class)private UserInfo info; //詳細信息private UserStatus status; //使用狀態(1正常 2凍結)private Integer balance; //賬戶余額private LocalDateTime createTime;//創建時間private LocalDateTime updateTime;//更新時間
}
將測試方法中關于設置詳細信息的代碼修改一下:
重啟服務,測試一下查詢接口,可以看到信息成功返回:
插件功能
MybatisPlus 提供了很多的插件功能,進一步拓展其功能。目前已有的插件有:
- PaginationInnerInterceptor:自動分頁
- TenantLineInnerInterceptor:多租戶
- DynamicTableNameInnerInterceptor:動態表名
- OptimisticLockerInnerInterceptor:樂觀鎖
- IllegalSQLInnerInterceptor:sql 性能規范
- BlockAttackInnerInterceptor:防止全表更新與刪除
最常用的是自動分頁插件。
分頁插件
在未引入分頁插件的情況下,MybatisPlus 是不支持分頁功能的,IService 和 BaseMapper 中的分頁方法都無法正常起效。所以,我們必須配置分頁插件。
在項目中新建一個配置類,項目結構如下:
package com.itheima.mp.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class MybatisConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {// 1.初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 2.創建分頁插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();paginationInnerInterceptor.setMaxLimit(1000L); //設置最大分頁限制// 3.添加分頁插件interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}
}
在 IUserServiceTest 中寫一個分頁查詢的測試方法:
@Test
void testPageQuery() {// 1.準備分頁條件int pageNum = 1, pageSize = 2; //頁碼、每頁大小Page<User> page = Page.of(pageNum, pageSize);// 2.排序條件page.addOrder(new OrderItem("balance", true)); // 先按余額升序排序page.addOrder(new OrderItem("id", true)); // 再按id升序排序// 3.分頁查詢Page<User> p = userService.page(page);// 4.解析數據long total = p.getTotal(); // 總條數System.out.println("total = " + total);long pages = p.getPages();// 總頁數System.out.println("pages = " + pages);List<User> users = p.getRecords(); // 當前頁碼的數據記錄users.forEach(System.out::println);
}
結果:
通用分頁實體
需求:遵循下面的接口規范,編寫一個 UserController 接口,實現 User 的分頁查詢。
定義實體
這里需要定義3個實體:
- UserQuery:分頁查詢條件的實體,包含分頁、排序參數、過濾條件
- PageDTO:分頁結果實體,包含總條數、總頁數、當前頁數據
- UserVO:用戶頁面視圖實體(已存在)
雖然 UserQuery 之前已經定義過了,并且其中已經包含了過濾條件,其中缺少的僅僅是分頁條件,但是分頁條件不僅僅是用戶分頁查詢需要,以后其它業務也都有分頁查詢的需求。因此建議將分頁查詢條件單獨定義為一個 PageQuery 實體:
PageQuery 是前端提交的查詢參數,一般包含四個屬性:
- pageNo:頁碼
- pageSize:每頁數據條數
- sortBy:排序字段
- isAsc:是否升序
package com.itheima.mp.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "分頁查詢實體")
public class PageQuery {@ApiModelProperty("頁碼")private Long pageNo;@ApiModelProperty("每頁數據條數")private Long pageSize;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc;
}
然后,讓我們的 UserQuery 繼承這個實體:
package com.itheima.mp.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;@EqualsAndHashCode(callSuper = true)
@Data
@ApiModel(description = "用戶查詢條件實體")
public class UserQuery extends PageQuery{@ApiModelProperty("用戶名關鍵字")private String name;@ApiModelProperty("用戶狀態:1-正常,2-凍結")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
最后,則是分頁實體 PageDTO,由于在其它微服務項目中可能也會使用到這個分頁實體,我們將其定為為 DTO:
package com.itheima.mp.domain.dto;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.List;@Data
@ApiModel(description = "分頁結果")
public class PageDTO<T> {@ApiModelProperty("總條數")private Long total;@ApiModelProperty("總頁數")private Long pages;@ApiModelProperty("集合")private List<T> list;
}
開發接口
在 UserController 中定義分頁查詢用戶的接口:
/*** 根據條件分頁查詢用戶** @param query* @return*/
@GetMapping("/page")
@ApiOperation("根據條件分頁查詢用戶接口")
public PageDTO<UserVO> queryUsersPage(UserQuery query) {return userService.queryUsersPage(query);
}
在 IUserService 中創建 queryUsersPage 方法:
/*** 根據條件分頁查詢用戶** @param query* @return*/
PageDTO<UserVO> queryUsersPage(UserQuery query);
在 UserServiceImpl 中實現該方法:
/*** 根據條件分頁查詢用戶** @param query* @return*/
public PageDTO<UserVO> queryUsersPage(UserQuery query) {String name = query.getName();Integer status = query.getStatus();// 構建分頁條件Page<User> page = Page.of(query.getPageNo(), query.getPageSize()); 3// 排序條件if (StrUtil.isNotBlank(query.getSortBy())) {// 不為空page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));} else {// 為空,默認按照更新時間排序page.addOrder(new OrderItem("update_time", query.getIsAsc()));}// 分頁查詢Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);//封裝VO結果PageDTO<UserVO> dto = new PageDTO<>();dto.setTotal(p.getTotal());dto.setPages(p.getPages());List<User> records = p.getRecords();if (CollUtil.isEmpty(records)) {// 為空,設置為空列表dto.setList(Collections.emptyList());} else {// 不為空dto.setList(BeanUtil.copyToList(records, UserVO.class));}return dto;
}
測試一下:
改造PageQuery實體
在上面的代碼中,從 PageQuery 到 MybatisPlus 的 Page 之間轉換的過程還是比較麻煩的。
我們完全可以在 PageQuery 這個實體中定義一個工具方法,來簡化開發。
package com.itheima.mp.domain.query;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "分頁查詢實體")
public class PageQuery {@ApiModelProperty("頁碼")private Long pageNo = 1L;@ApiModelProperty("每頁數據條數")private Long pageSize = 2L;@ApiModelProperty("排序字段")private String sortBy;@ApiModelProperty("是否升序")private Boolean isAsc = true;// 多個參數public <T> Page<T> toMpPage(OrderItem... orders) {// 分頁條件Page<T> page = Page.of(pageNo, pageSize);// 排序條件if (StrUtil.isNotBlank(sortBy)) {// 不為空page.addOrder(new OrderItem(sortBy, isAsc));} else if (orders != null) {// 為空page.addOrder(orders);}return page;}// 只有一個參數public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) {return toMpPage(new OrderItem(defaultSortBy, isAsc));}// 沒有參數,希望按照創建時間排序public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {return toMpPage("create_time", false);}// 沒有參數,希望按照更新時間排序public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {return toMpPage("update_time", false);}
}
這樣我們在開發也時就可以省去對從 PageQuery 到 Page 的的轉換:
// 構建分頁條件
Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();
改造PageDTO實體
同理,在查詢出分頁結果后,數據的非空校驗,數據的 vo 轉換都是模板代碼,編寫起來很麻煩。
我們完全可以將其封裝到 PageDTO 的工具方法中,簡化整個過程:
package com.itheima.mp.domain.dto;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;@Data
@ApiModel(description = "分頁結果")
public class PageDTO<T> {@ApiModelProperty("總條數")private Long total;@ApiModelProperty("總頁數")private Long pages;@ApiModelProperty("集合")private List<T> list;// 需要傳結果類型的字節碼public static <PO, VO> PageDTO<VO> of(Page<PO> p, Class<VO> clazz) {PageDTO<VO> dto = new PageDTO<>();dto.setTotal(p.getTotal()); // 總條數dto.setPages(p.getPages()); // 總頁數// 當前頁數據List<PO> records = p.getRecords();if (CollUtil.isEmpty(records)) {// 為空,設置為空里欸博愛dto.setList(Collections.emptyList());} else {// 不為空dto.setList(BeanUtil.copyToList(records, clazz));}return dto;}// 需要傳PO如何轉換為VO的方法public static <PO, VO> PageDTO<VO> of(Page<PO> p, Function<PO, VO> convertor) {PageDTO<VO> dto = new PageDTO<>();dto.setTotal(p.getTotal()); // 總條數dto.setPages(p.getPages()); // 總頁數// 當前頁數據List<PO> records = p.getRecords();if (CollUtil.isEmpty(records)) {// 為空,設置為空里欸博愛dto.setList(Collections.emptyList());} else {// 不為空dto.setList(records.stream().map(convertor).collect(Collectors.toList()));}return dto;}
}
最終,業務層的代碼可以簡化為:
public PageDTO<UserVO> queryUsersPage(UserQuery query) {String name = query.getName();Integer status = query.getStatus();// 構建分頁條件Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();// 分頁查詢Page<User> p = lambdaQuery().like(name != null, User::getUsername, name).eq(status != null, User::getStatus, status).page(page);return PageDTO.of(p, UserVO.class);
}
如果是希望自定義 PO 到 VO 的轉換過程,可以這樣做:
return PageDTO.of(p, user -> {// 1.拷貝基礎屬性UserVO vo = BeanUtil.copyProperties(user, UserVO.class);// 2.處理特殊邏輯:比如用戶名脫敏String username = vo.getUsername();vo.setUsername(username.substring(0, username.length() - 2) + "**");return vo;
});
測試一下:
在改進的過程中,我們都是在實體類中添加了對應的方法來簡化我們的開發,這也意味著這些方法和實體類耦合了。因為我們使用的是 MP,所以可以這樣寫,如果使用的不是 MP,我們可以考慮定義一些工具類來實現這些類型之間的轉換,以此讓方法與實體類解耦合。