龍華哪有做網(wǎng)站設(shè)計網(wǎng)站關(guān)鍵詞優(yōu)化排名推薦
介紹?
j?????MyBatis-Plus?(opens new window)(簡稱 MP)是一個?MyBatis?(opens new window)的增強工具,在 MyBatis 的基礎(chǔ)上只做增強不做改變,為簡化開發(fā)、提高效率而生。
官網(wǎng):MyBatis-Plus (baomidou.com)
1.引入MybatisPlus的起步依賴
MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且實現(xiàn)了自動裝配效果。
因此我們可以用MybatisPlus的starter代替Mybatis的starter:
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency>
2.定義Mapper
?
為了簡化單表CRUD,MybatisPlus提供了一個基礎(chǔ)的BaseMapper
接口,其中已經(jīng)實現(xiàn)了單表的CRUD:
因此我們自定義的Mapper只要實現(xiàn)了這個BaseMapper
,就無需自己實現(xiàn)單表CRUD了。 修改mp-demo中的com.itheima.mp.mapper
包下的UserMapper
接口,讓其繼承BaseMapper
:
代碼如下:
package com.itheima.mp.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;public interface UserMapper extends BaseMapper<User> {
}
MybatisPlus就是根據(jù)PO實體的信息來推斷出表的信息,從而生成SQL的。默認(rèn)情況下:
-
MybatisPlus會把PO實體的類名駝峰轉(zhuǎn)下劃線作為表名
-
MybatisPlus會把PO實體的所有變量名駝峰轉(zhuǎn)下劃線作為表的字段名,并根據(jù)變量類型推斷字段類型
-
MybatisPlus會把名為id的字段作為主鍵
但很多情況下,默認(rèn)的實現(xiàn)與實際場景不符,因此MybatisPlus提供了一些注解便于我們聲明表信息。
常見注解
@TableName
說明:
-
描述:表名注解,標(biāo)識實體類對應(yīng)的表
-
使用位置:實體類
示例:
@TableName("user")
public class User {private Long id;private String name;
}
TableName注解除了指定表名以外,還可以指定很多其它屬性:
屬性 | 類型 | 必須指定 | 默認(rèn)值 | 描述 |
value | String | 否 | "" | 表名 |
schema | String | 否 | "" | schema |
keepGlobalPrefix | boolean | 否 | false | 是否保持使用全局的 tablePrefix 的值(當(dāng)全局 tablePrefix 生效時) |
resultMap | String | 否 | "" | xml 中 resultMap 的 id(用于滿足特定類型的實體類對象綁定) |
autoResultMap | boolean | 否 | false | 是否自動構(gòu)建 resultMap 并使用(如果設(shè)置 resultMap 則不會進(jìn)行 resultMap 的自動構(gòu)建與注入) |
excludeProperty | String[] | 否 | {} | 需要排除的屬性名 @since 3.3.1 |
@TableId
說明:
-
描述:主鍵注解,標(biāo)識實體類中的主鍵字段
-
使用位置:實體類的主鍵字段
示例:
@TableName("user")
public class User {@TableIdprivate Long id;private String name;
}
TableId
注解支持兩個屬性:
屬性 | 類型 | 必須指定 | 默認(rèn)值 | 描述 |
---|---|---|---|---|
value | String | 否 | "" | 表名 |
type | Enum | 否 | IdType.NONE | 指定主鍵類型 |
IdType
支持的類型有:
值 | 描述 |
---|---|
AUTO | 數(shù)據(jù)庫 ID 自增 |
NONE | 無狀態(tài),該類型為未設(shè)置主鍵類型(注解里等于跟隨全局,全局里約等于 INPUT) |
INPUT | insert 前自行 set 主鍵值 |
ASSIGN_ID | 分配 ID(主鍵類型為 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默認(rèn)實現(xiàn)類為DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配 UUID,主鍵類型為 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默認(rèn) default 方法) |
| 分布式全局唯一 ID 長整型類型(please use ASSIGN_ID) |
| 32 位 UUID 字符串(please use ASSIGN_UUID) |
| 分布式全局唯一 ID 字符串類型(please use ASSIGN_ID) |
這里比較常見的有三種:
-
AUTO
:利用數(shù)據(jù)庫的id自增長 -
INPUT
:手動生成id -
ASSIGN_ID
:雪花算法生成Long
類型的全局唯一id,這是默認(rèn)的ID策略
@TableField
說明:
描述:普通字段注解
示例:
@TableName("user")
public class User {@TableIdprivate Long id;private String name;private Integer age;@TableField("isMarried")private Boolean isMarried;@TableField("concat")private String concat;
}
一般情況下我們并不需要給字段添加@TableField
注解,一些特殊情況除外:
-
成員變量名與數(shù)據(jù)庫字段名不一致
-
成員變量是以
isXXX
命名,按照JavaBean
的規(guī)范,MybatisPlus
識別字段時會把is
去除,這就導(dǎo)致與數(shù)據(jù)庫不符。 -
成員變量名與數(shù)據(jù)庫一致,但是與數(shù)據(jù)庫的關(guān)鍵字沖突。使用
@TableField
注解給字段名添加轉(zhuǎn)義字符:``
支持的其它屬性如下:
屬性 | 類型 | 必填 | 默認(rèn)值 | 描述 |
value | String | 否 | "" | 數(shù)據(jù)庫字段名 |
exist | boolean | 否 | true | 是否為數(shù)據(jù)庫表字段 |
condition | String | 否 | "" | 字段 where 實體查詢比較條件,有值設(shè)置則按設(shè)置的值為準(zhǔn),沒有則為默認(rèn)全局的 %s=#{%s},參考(opens new window) |
update | String | 否 | "" | 字段 update set 部分注入,例如:當(dāng)在version字段上注解update="%s+1" 表示更新時會 set version=version+1 (該屬性優(yōu)先級高于 el 屬性) |
insertStrategy | Enum | 否 | FieldStrategy.DEFAULT | 舉例:NOT_NULL insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>) |
updateStrategy | Enum | 否 | FieldStrategy.DEFAULT | 舉例:IGNORED update table_a set column=#{columnProperty} |
whereStrategy | Enum | 否 | FieldStrategy.DEFAULT | 舉例:NOT_EMPTY where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if> |
fill | Enum | 否 | FieldFill.DEFAULT | 字段自動填充策略 |
select | boolean | 否 | true | 是否進(jìn)行 select 查詢 |
keepGlobalFormat | boolean | 否 | false | 是否保持使用全局的 format 進(jìn)行處理 |
jdbcType | JdbcType | 否 | JdbcType.UNDEFINED | JDBC 類型 (該默認(rèn)值不代表會按照該值生效) |
typeHandler | TypeHander | 否 | 類型處理器 (該默認(rèn)值不代表會按照該值生效) | |
numericScale | String | 否 | "" | 指定小數(shù)點后保留的位數(shù) |
MybatisPlus是如何獲取實現(xiàn)CRUD的數(shù)據(jù)庫表信息的?
默認(rèn)以類名駝峰轉(zhuǎn)下劃線作為表名
默認(rèn)把名為id的字段作為主鍵
默認(rèn)把變量名駝峰轉(zhuǎn)下劃線作為表的字段名MybatisPlus的常用注解有哪些?@TableName:指定表名稱及全局配置@Tableld:指定id字段及相關(guān)配置@TableField:指定普通字段及相關(guān)配置
IdType的常見類型有哪些?AUTO、ASSIGN ID、INPUT使用@TableField的常見場景是?
成員變量名與數(shù)據(jù)庫字段名不一致
成員變量名以is開頭,且是布爾值
成員變量名與數(shù)據(jù)庫關(guān)鍵字沖突
成員變量不是數(shù)據(jù)庫字段?
常見配置
MybatisPlus也支持基于yaml文件的自定義配置,詳見官方文檔:
使用配置 | MyBatis-Plus (baomidou.com)
大多數(shù)的配置都有默認(rèn)值,因此我們都無需配置。但還有一些是沒有默認(rèn)值的,例如:
-
實體類的別名掃描包
-
全局id類型
mybatis-plus:type-aliases-package: com.itheima.mp.domain.poglobal-config:db-config:id-type: auto # 全局id類型為自增長
需要注意的是,MyBatisPlus也支持手寫SQL的,而mapper文件的讀取地址可以自己配置:
mybatis-plus:mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,當(dāng)前這個是默認(rèn)值。
可以看到默認(rèn)值是classpath*:/mapper/**/*.xml
,也就是說我們只要把mapper.xml文件放置這個目錄下就一定會被加載。
核心功能
剛才的案例中都是以id為條件的簡單CRUD,一些復(fù)雜條件的SQL語句就要用到一些更高級的功能了。
1.條件構(gòu)造器
除了新增以外,修改、刪除、查詢的SQL語句都需要指定where條件。因此BaseMapper中提供的相關(guān)方法除了以id
作為where
條件以外,還支持更加復(fù)雜的where
條件。
參數(shù)中的Wrapper
就是條件構(gòu)造的抽象類,其下有很多默認(rèn)實現(xiàn),繼承關(guān)系如圖:?
?
Wrapper
的子類AbstractWrapper
提供了where中包含的所有條件構(gòu)造方法:?
而QueryWrapper在AbstractWrapper的基礎(chǔ)上拓展了一個select方法,允許指定查詢字段:
而UpdateWrapper在AbstractWrapper的基礎(chǔ)上拓展了一個set方法,允許指定SQL中的SET部分:
QueryWrapper
無論是修改、刪除、查詢,都可以使用QueryWrapper來構(gòu)建查詢條件。接下來看一些例子: 查詢:查詢出名字中帶o
的,存款大于等于1000元的人。代碼如下:
@Test
void testQueryWrapper() {// 1.構(gòu)建查詢條件 where name like "%o%" AND balance >= 1000QueryWrapper<User> wrapper = new QueryWrapper<User>().select("id", "username", "info", "balance").like("username", "o").ge("balance", 1000);// 2.查詢數(shù)據(jù)List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}
?更新:更新用戶名為jack的用戶的余額為2000,代碼如下:
@Test
void testUpdateByQueryWrapper() {// 1.構(gòu)建查詢條件 where name = "Jack"QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "Jack");// 2.更新數(shù)據(jù),user中非null字段都會作為set語句User user = new User();user.setBalance(2000);userMapper.update(user, wrapper);
}
UpdateWrapper
基于BaseMapper中的update方法更新時只能直接賦值,對于一些復(fù)雜的需求就難以實現(xiàn)。 例如:更新id為1,2,4
的用戶的余額,扣200,對應(yīng)的SQL應(yīng)該是:
UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)
SET的賦值結(jié)果是基于字段現(xiàn)有值的,這個時候就要利用UpdateWrapper中的setSql功能了:?
@Test
void testUpdateWrapper() {List<Long> ids = List.of(1L, 2L, 4L);// 1.生成SQLUpdateWrapper<User> wrapper = new UpdateWrapper<User>().setSql("balance = balance - 200") // SET balance = balance - 200.in("id", ids); // WHERE id in (1, 2, 4)// 2.更新,注意第一個參數(shù)可以給null,也就是不填更新字段和數(shù)據(jù),// 而是基于UpdateWrapper中的setSQL來更新userMapper.update(null, wrapper);
}
LambdaQueryWrapper
無論是QueryWrapper還是UpdateWrapper在構(gòu)造條件的時候都需要寫死字段名稱,會出現(xiàn)字符串魔法值
。這在編程規(guī)范中顯然是不推薦的。 那怎么樣才能不寫字段名,又能知道字段名呢?
其中一種辦法是基于變量的gettter
方法結(jié)合反射技術(shù)。因此我們只要將條件對應(yīng)的字段的getter
方法傳遞給MybatisPlus,它就能計算出對應(yīng)的變量名了。而傳遞方法可以使用JDK8中的方法引用
和Lambda
表達(dá)式。 因此MybatisPlus又提供了一套基于Lambda的Wrapper,包含兩個:
-
LambdaQueryWrapper
-
LambdaUpdateWrapper
分別對應(yīng)QueryWrapper和UpdateWrapper
其使用方式如下:
@Test
void testLambdaQueryWrapper() {// 1.構(gòu)建條件 WHERE username LIKE "%o%" AND balance >= 1000QueryWrapper<User> wrapper = new QueryWrapper<>();wrapper.lambda().select(User::getId, User::getUsername, User::getInfo, User::getBalance).like(User::getUsername, "o").ge(User::getBalance, 1000);// 2.查詢List<User> users = userMapper.selectList(wrapper);users.forEach(System.out::println);
}
2.自定義SQL
在演示UpdateWrapper的案例中,我們在代碼中編寫了更新的SQL語句:
這種寫法在某些企業(yè)也是不允許的,因為SQL語句最好都維護(hù)在持久層,而不是業(yè)務(wù)層。就當(dāng)前案例來說,由于條件是in語句,只能將SQL寫在Mapper.xml文件,利用foreach來生成動態(tài)SQL。 這實在是太麻煩了。假如查詢條件更復(fù)雜,動態(tài)SQL的編寫也會更加復(fù)雜。
所以,MybatisPlus提供了自定義SQL功能,可以讓我們利用Wrapper生成查詢條件,再結(jié)合Mapper.xml編寫SQL
基本用法
以當(dāng)前案例來說,我們可以這樣寫:
@Test
void testCustomWrapper() {// 1.準(zhǔn)備自定義查詢條件List<Long> ids = List.of(1L, 2L, 4L);QueryWrapper<User> wrapper = new QueryWrapper<User>().in("id", ids);// 2.調(diào)用mapper的自定義方法,直接傳遞WrapperuserMapper.deductBalanceByIds(200, wrapper);
}
然后在UserMapper中自定義SQL:
package com.itheima.mp.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;public interface UserMapper extends BaseMapper<User> {@Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")void deductBalanceByIds(@Param("money") int money, @Param("ew") QueryWrapper<User> wrapper);
}
?這樣就省去了編寫復(fù)雜查詢條件的煩惱了。
多表關(guān)聯(lián)
理論上來講MyBatisPlus是不支持多表查詢的,不過我們可以利用Wrapper中自定義條件結(jié)合自定義SQL來實現(xiàn)多表查詢的效果。 例如,我們要查詢出所有收貨地址在北京的并且用戶id在1、2、4之中的用戶 要是自己基于mybatis實現(xiàn)SQL,大概是這樣的:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT *FROM user uINNER JOIN address a ON u.id = a.user_idWHERE u.id<foreach collection="ids" separator="," item="id" open="IN (" close=")">#{id}</foreach>AND a.city = #{city}</select>
可以看出其中最復(fù)雜的就是WHERE條件的編寫,如果業(yè)務(wù)復(fù)雜一些,這里的SQL會更變態(tài)。
但是基于自定義SQL結(jié)合Wrapper的玩法,我們就可以利用Wrapper來構(gòu)建查詢條件,然后手寫SELECT及FROM部分,實現(xiàn)多表查詢。
查詢條件這樣來構(gòu)建:
@Test
void testCustomJoinWrapper() {// 1.準(zhǔn)備自定義查詢條件QueryWrapper<User> wrapper = new QueryWrapper<User>().in("u.id", List.of(1L, 2L, 4L)).eq("a.city", "北京");// 2.調(diào)用mapper的自定義方法List<User> users = userMapper.queryUserByWrapper(wrapper);users.forEach(System.out::println);
}
然后在UserMapper中自定義方法:
@Select("SELECT u.* FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}")
List<User> queryUserByWrapper(@Param("ew")QueryWrapper<User> wrapper);
當(dāng)然,也可以在UserMapper.xml
中寫SQL:
<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">SELECT * FROM user u INNER JOIN address a ON u.id = a.user_id ${ew.customSqlSegment}
</select>
Service接口
MybatisPlus不僅提供了BaseMapper,還提供了通用的Service接口及默認(rèn)實現(xiàn),封裝了一些常用的service模板方法。 通用接口為IService
,默認(rèn)實現(xiàn)為ServiceImpl
,其中封裝的方法可以分為以下幾類:
-
save
:新增 -
remove
:刪除 -
update
:更新 -
get
:查詢單個結(jié)果 -
list
:查詢集合結(jié)果 -
count
:計數(shù) -
page
:分頁查詢
CRUD
我們先倆看下基本的CRUD接口。 新增:
-
save
是新增單個元素 -
saveBatch
是批量新增 -
saveOrUpdate
是根據(jù)id判斷,如果數(shù)據(jù)存在就更新,不存在則新增 -
saveOrUpdateBatch
是批量的新增或修改
?刪除:
-
removeById
:根據(jù)id刪除 -
removeByIds
:根據(jù)id批量刪除 -
removeByMap
:根據(jù)Map中的鍵值對為條件刪除 -
remove(Wrapper<T>)
:根據(jù)Wrapper條件刪除 -
~~removeBatchByIds~~
:暫不支持
修改:
-
updateById
:根據(jù)id修改 -
update(Wrapper<T>)
:根據(jù)UpdateWrapper
修改,Wrapper
中包含set
和where
部分 -
update(T,Wrapper<T>)
:按照T
內(nèi)的數(shù)據(jù)修改與Wrapper
匹配到的數(shù)據(jù) -
updateBatchById
:根據(jù)id批量修改
Get:
?
-
getById
:根據(jù)id查詢1條數(shù)據(jù) -
getOne(Wrapper<T>)
:根據(jù)Wrapper
查詢1條數(shù)據(jù) -
getBaseMapper
:獲取Service
內(nèi)的BaseMapper
實現(xiàn),某些時候需要直接調(diào)用Mapper
內(nèi)的自定義SQL
時可以用這個方法獲取到Mapper
List:
-
listByIds
:根據(jù)id批量查詢 -
list(Wrapper<T>)
:根據(jù)Wrapper條件查詢多條數(shù)據(jù) -
list()
:查詢所有
Count:
-
count()
:統(tǒng)計所有數(shù)量 -
count(Wrapper<T>)
:統(tǒng)計符合Wrapper
條件的數(shù)據(jù)數(shù)量
getBaseMapper: 當(dāng)我們在service中要調(diào)用Mapper中自定義SQL時,就必須獲取service對應(yīng)的Mapper,就可以通過這個方法:
?
基本用法
由于Service
中經(jīng)常需要定義與業(yè)務(wù)有關(guān)的自定義方法,因此我們不能直接使用IService
,而是自定義Service
接口,然后繼承IService
以拓展方法。同時,讓自定義的Service實現(xiàn)類
繼承ServiceImpl
,這樣就不用自己實現(xiàn)IService
中的接口了。
首先,定義IUserService
,繼承IService
:
package com.itheima.mp.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {// 拓展自定義方法
}
然后,編寫UserServiceImpl
類,繼承ServiceImpl
,實現(xiàn)UserService
:?
package com.itheima.mp.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.po.service.IUserService;
import com.itheima.mp.mapper.UserMapper;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements IUserService {
}
項目結(jié)構(gòu)如下:?
?
接下來,我們快速實現(xiàn)下面4個接口:
編號 | 接口 | 請求方式 | 請求路徑 | 請求參數(shù) | 返回值 |
---|---|---|---|---|---|
1 | 新增用戶 | POST | /users | 用戶表單實體 | 無 |
2 | 刪除用戶 | DELETE | /users/{id} | 用戶id | 無 |
3 | 根據(jù)id查詢用戶 | GET | /users/{id} | 用戶id | 用戶VO |
4 | 根據(jù)id批量查詢 | GET | /users | 用戶id集合 | 用戶VO集合 |
首先,我們在項目中引入幾個依賴:
<!--swagger-->
<dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-openapi2-spring-boot-starter</artifactId><version>4.1.0</version>
</dependency>
<!--web-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
然后需要配置swagger信息:
knife4j:enable: trueopenapi:title: 用戶管理接口文檔description: "用戶管理接口文檔"email: zhanghuyi@itcast.cnconcat: 虎哥url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.itheima.mp.controller
然后,接口需要兩個實體:
-
UserFormDTO:代表新增時的用戶表單
-
UserVO:代表查詢的返回結(jié)果
首先是UserFormDTO:
package com.itheima.mp.domain.dto;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶表單實體")
public class UserFormDTO {@ApiModelProperty("id")private Long id;@ApiModelProperty("用戶名")private String username;@ApiModelProperty("密碼")private String password;@ApiModelProperty("注冊手機號")private String phone;@ApiModelProperty("詳細(xì)信息,JSON風(fēng)格")private String info;@ApiModelProperty("賬戶余額")private Integer balance;
}
然后是UserVO:
package com.itheima.mp.domain.vo;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶VO實體")
public class UserVO {@ApiModelProperty("用戶id")private Long id;@ApiModelProperty("用戶名")private String username;@ApiModelProperty("詳細(xì)信息")private String info;@ApiModelProperty("使用狀態(tài)(1正常 2凍結(jié))")private Integer status;@ApiModelProperty("賬戶余額")private Integer balance;
}
?最后,按照Restful風(fēng)格編寫Controller接口方法:
package com.itheima.mp.controller;import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;import java.util.List;@Api(tags = "用戶管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {private final IUserService userService;@PostMapping@ApiOperation("新增用戶")public void saveUser(@RequestBody UserFormDTO userFormDTO){// 1.轉(zhuǎn)換DTO為POUser user = BeanUtil.copyProperties(userFormDTO, User.class);// 2.新增userService.save(user);}@DeleteMapping("/{id}")@ApiOperation("刪除用戶")public void removeUserById(@PathVariable("id") Long userId){userService.removeById(userId);}@GetMapping("/{id}")@ApiOperation("根據(jù)id查詢用戶")public UserVO queryUserById(@PathVariable("id") Long userId){// 1.查詢用戶User user = userService.getById(userId);// 2.處理voreturn BeanUtil.copyProperties(user, UserVO.class);}@GetMapping@ApiOperation("根據(jù)id集合查詢用戶")public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids){// 1.查詢用戶List<User> users = userService.listByIds(ids);// 2.處理voreturn BeanUtil.copyToList(users, UserVO.class);}
}
可以看到上述接口都直接在controller即可實現(xiàn),無需編寫任何service代碼,非常方便。
不過,一些帶有業(yè)務(wù)邏輯的接口則需要在service中自定義實現(xiàn)了。例如下面的需求:
-
根據(jù)id扣減用戶余額
這看起來是個簡單修改功能,只要修改用戶余額即可。但這個業(yè)務(wù)包含一些業(yè)務(wù)邏輯處理:
-
判斷用戶狀態(tài)是否正常
-
判斷用戶余額是否充足
這些業(yè)務(wù)邏輯都要在service層來做,另外更新余額需要自定義SQL,要在mapper中來實現(xiàn)。因此,我們除了要編寫controller以外,具體的業(yè)務(wù)還要在service和mapper中編寫。
首先在UserController中定義一個方法:
@PutMapping("{id}/deduction/{money}")
@ApiOperation("扣減用戶余額")
public void deductBalance(@PathVariable("id") Long id, @PathVariable("money")Integer money){userService.deductBalance(id, money);
}
然后是UserService接口:
package com.itheima.mp.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;public interface IUserService extends IService<User> {void deductBalance(Long id, Integer money);
}
最后是UserServiceImpl實現(xiàn)類:?
package com.itheima.mp.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Overridepublic void deductBalance(Long id, Integer money) {// 1.查詢用戶User user = getById(id);// 2.判斷用戶狀態(tài)if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶狀態(tài)異常");}// 3.判斷用戶余額if (user.getBalance() < money) {throw new RuntimeException("用戶余額不足");}// 4.扣減余額baseMapper.deductMoneyById(id, money);}
}
最后是mapper:
@Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
void deductMoneyById(@Param("id") Long id, @Param("money") Integer money);
Lambda
IService中還提供了Lambda功能來簡化我們的復(fù)雜查詢及更新功能。我們通過兩個案例來學(xué)習(xí)一下。
案例一:實現(xiàn)一個根據(jù)復(fù)雜條件查詢用戶的接口,查詢條件如下:
-
name:用戶名關(guān)鍵字,可以為空
-
status:用戶狀態(tài),可以為空
-
minBalance:最小余額,可以為空
-
maxBalance:最大余額,可以為空
可以理解成一個用戶的后臺管理界面,管理員可以自己選擇條件來篩選用戶,因此上述條件不一定存在,需要做判斷。
我們首先需要定義一個查詢條件實體,UserQuery實體:
package com.itheima.mp.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶查詢條件實體")
public class UserQuery {@ApiModelProperty("用戶名關(guān)鍵字")private String name;@ApiModelProperty("用戶狀態(tài):1-正常,2-凍結(jié)")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
?接下來我們在UserController中定義一個controller方法:
@GetMapping("/list")
@ApiOperation("根據(jù)id集合查詢用戶")
public List<UserVO> queryUsers(UserQuery query){// 1.組織條件String username = query.getName();Integer status = query.getStatus();Integer minBalance = query.getMinBalance();Integer maxBalance = query.getMaxBalance();LambdaQueryWrapper<User> wrapper = new QueryWrapper<User>().lambda().like(username != null, User::getUsername, username).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance);// 2.查詢用戶List<User> users = userService.list(wrapper);// 3.處理voreturn BeanUtil.copyToList(users, UserVO.class);
}
在組織查詢條件的時候,我們加入了 username != null
這樣的參數(shù),意思就是當(dāng)條件成立時才會添加這個查詢條件,類似Mybatis的mapper.xml文件中的<if>
標(biāo)簽。這樣就實現(xiàn)了動態(tài)查詢條件效果了。
不過,上述條件構(gòu)建的代碼太麻煩了。 因此Service中對LambdaQueryWrapper
和LambdaUpdateWrapper
的用法進(jìn)一步做了簡化。我們無需自己通過new
的方式來創(chuàng)建Wrapper
,而是直接調(diào)用lambdaQuery
和lambdaUpdate
方法:
基于Lambda查詢:
@GetMapping("/list")
@ApiOperation("根據(jù)id集合查詢用戶")
public List<UserVO> queryUsers(UserQuery query){// 1.組織條件String username = query.getName();Integer status = query.getStatus();Integer minBalance = query.getMinBalance();Integer maxBalance = query.getMaxBalance();// 2.查詢用戶List<User> users = userService.lambdaQuery().like(username != null, User::getUsername, username).eq(status != null, User::getStatus, status).ge(minBalance != null, User::getBalance, minBalance).le(maxBalance != null, User::getBalance, maxBalance).list();// 3.處理voreturn BeanUtil.copyToList(users, UserVO.class);
}
可以發(fā)現(xiàn)lambdaQuery方法中除了可以構(gòu)建條件,還需要在鏈?zhǔn)骄幊痰淖詈筇砑右粋€list()
,這是在告訴MP我們的調(diào)用結(jié)果需要是一個list集合。這里不僅可以用list()
,可選的方法有:
-
.one()
:最多1個結(jié)果 -
.list()
:返回集合結(jié)果 -
.count()
:返回計數(shù)結(jié)果
MybatisPlus會根據(jù)鏈?zhǔn)骄幊痰淖詈笠粋€方法來判斷最終的返回結(jié)果。
與lambdaQuery方法類似,IService中的lambdaUpdate方法可以非常方便的實現(xiàn)復(fù)雜更新業(yè)務(wù)。
例如下面的需求:
需求:改造根據(jù)id修改用戶余額的接口,要求如下
如果扣減后余額為0,則將用戶status修改為凍結(jié)狀態(tài)(2)
也就是說我們在扣減用戶余額時,需要對用戶剩余余額做出判斷,如果發(fā)現(xiàn)剩余余額為0,則應(yīng)該將status修改為2,這就是說update語句的set部分是動態(tài)的。
實現(xiàn)如下:
@Override
@Transactional
public void deductBalance(Long id, Integer money) {// 1.查詢用戶User user = getById(id);// 2.校驗用戶狀態(tài)if (user == null || user.getStatus() == 2) {throw new RuntimeException("用戶狀態(tài)異常!");}// 3.校驗余額是否充足if (user.getBalance() < money) {throw new RuntimeException("用戶余額不足!");}// 4.扣減余額 update tb_user set balance = balance - ?int remainBalance = user.getBalance() - money;lambdaUpdate().set(User::getBalance, remainBalance) // 更新余額.set(remainBalance == 0, User::getStatus, 2) // 動態(tài)判斷,是否更新status.eq(User::getId, id).eq(User::getBalance, user.getBalance()) // 樂觀鎖.update();
}
批量新增
IService中的批量新增功能使用起來非常方便,但有一點注意事項,我們先來測試一下。 首先我們測試逐條插入數(shù)據(jù):
@Test
void testSaveOneByOne() {long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {userService.save(buildUser(i));}long e = System.currentTimeMillis();System.out.println("耗時:" + (e - b));
}private User buildUser(int i) {User user = new User();user.setUsername("user_" + i);user.setPassword("123");user.setPhone("" + (18688190000L + i));user.setBalance(2000);user.setInfo("{\"age\": 24, \"intro\": \"英文老師\", \"gender\": \"female\"}");user.setCreateTime(LocalDateTime.now());user.setUpdateTime(user.getCreateTime());return user;
}
執(zhí)行結(jié)果如下:?
可以看到速度非常慢。
然后再試試MybatisPlus的批處理:
@Test
void testSaveBatch() {// 準(zhǔn)備10萬條數(shù)據(jù)List<User> list = new ArrayList<>(1000);long b = System.currentTimeMillis();for (int i = 1; i <= 100000; i++) {list.add(buildUser(i));// 每1000條批量插入一次if (i % 1000 == 0) {userService.saveBatch(list);list.clear();}}long e = System.currentTimeMillis();System.out.println("耗時:" + (e - b));
}
執(zhí)行最終耗時如下:?
?
可以看到使用了批處理以后,比逐條新增效率提高了10倍左右,性能還是不錯的。
不過,我們簡單查看一下MybatisPlus
源碼:
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
// ...SqlHelper
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {Assert.isFalse(batchSize < 1, "batchSize must not be less than one");return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {int size = list.size();int idxLimit = Math.min(batchSize, size);int i = 1;for (E element : list) {consumer.accept(sqlSession, element);if (i == idxLimit) {sqlSession.flushStatements();idxLimit = Math.min(idxLimit + batchSize, size);}i++;}});
}
?可以發(fā)現(xiàn)其實MybatisPlus
的批處理是基于PrepareStatement
的預(yù)編譯模式,然后批量提交,最終在數(shù)據(jù)庫執(zhí)行時還是會有多條insert語句,逐條插入數(shù)據(jù)。SQL類似這樣:
Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01
而如果想要得到最佳性能,最好是將多條SQL合并為一條,像這樣:?
INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);
該怎么做呢?
MySQL的客戶端連接參數(shù)中有這樣的一個參數(shù):rewriteBatchedStatements
。顧名思義,就是重寫批處理的statement
語句。參考文檔:
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements
這個參數(shù)的默認(rèn)值是false,我們需要修改連接參數(shù),將其配置為true
修改項目中的application.yml文件,在jdbc的url后面添加參數(shù)&rewriteBatchedStatements=true
:
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=truedriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: MySQL123
再次測試插入10萬條數(shù)據(jù),可以發(fā)現(xiàn)速度有非常明顯的提升:?
在ClientPreparedStatement
的executeBatchInternal
中,有判斷rewriteBatchedStatements
值是否為true并重寫SQL的功能:
最終,SQL被重寫了:
擴(kuò)展功能
1.代碼生成
在使用MybatisPlus以后,基礎(chǔ)的Mapper
、Service
、PO
代碼相對固定,重復(fù)編寫也比較麻煩。因此MybatisPlus官方提供了代碼生成器根據(jù)數(shù)據(jù)庫表結(jié)構(gòu)生成PO
、Mapper
、Service
等相關(guān)代碼。只不過代碼生成器同樣要編碼使用,也很麻煩。
這里推薦大家使用一款MybatisPlus
的插件,它可以基于圖形化界面完成MybatisPlus
的代碼生成,非常簡單。
安裝插件
在Idea
的plugins市場中搜索并安裝MyBatisPlus
插件:
?
然后重啟你的Idea即可使用。
使用
剛好數(shù)據(jù)庫中還有一張address表尚未生成對應(yīng)的實體和mapper等基礎(chǔ)代碼。我們利用插件生成一下。 首先需要配置數(shù)據(jù)庫地址,在Idea頂部菜單中,找到other
,選擇Config Database
:
在彈出的窗口中填寫數(shù)據(jù)庫連接的基本信息:
點擊OK保存。
然后再次點擊Idea頂部菜單中的other,然后選擇Code Generator
:
?
在彈出的表單中填寫信息:?
最終,代碼自動生成到指定的位置了:?
2.靜態(tài)工具
有的時候Service之間也會相互調(diào)用,為了避免出現(xiàn)循環(huán)依賴問題,MybatisPlus提供一個靜態(tài)工具類:Db
,其中的一些靜態(tài)方法與IService
中方法簽名基本一致,也可以幫助我們實現(xiàn)CRUD功能:
?
示例:
@Test
void testDbGet() {User user = Db.getById(1L, User.class);System.out.println(user);
}@Test
void testDbList() {// 利用Db實現(xiàn)復(fù)雜條件查詢List<User> list = Db.lambdaQuery(User.class).like(User::getUsername, "o").ge(User::getBalance, 1000).list();list.forEach(System.out::println);
}@Test
void testDbUpdate() {Db.lambdaUpdate(User.class).set(User::getBalance, 2000).eq(User::getUsername, "Rose");
}
需求:改造根據(jù)id用戶查詢的接口,查詢用戶的同時返回用戶收貨地址列表
首先,我們要添加一個收貨地址的VO對象:
package com.itheima.mp.domain.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("縣/區(qū)")private String town;@ApiModelProperty("手機")private String mobile;@ApiModelProperty("詳細(xì)地址")private String street;@ApiModelProperty("聯(lián)系人")private String contact;@ApiModelProperty("是否是默認(rèn) 1默認(rèn) 0否")private Boolean isDefault;@ApiModelProperty("備注")private String notes;
}
?接下來,修改UserController中根據(jù)id查詢用戶的業(yè)務(wù)接口:
@GetMapping("/{id}")
@ApiOperation("根據(jù)id查詢用戶")
public UserVO queryUserById(@PathVariable("id") Long userId){// 基于自定義service方法查詢return userService.queryUserAndAddressById(userId);
}
由于查詢業(yè)務(wù)復(fù)雜,所以要在service層來實現(xiàn)。首先在IUserService中定義方法:?
package com.itheima.mp.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;public interface IUserService extends IService<User> {void deduct(Long id, Integer money);UserVO queryUserAndAddressById(Long userId);
}
然后,在UserServiceImpl中實現(xiàn)該方法:
@Override
public UserVO queryUserAndAddressById(Long userId) {// 1.查詢用戶User user = getById(userId);if (user == null) {return null;}// 2.查詢收貨地址List<Address> addresses = Db.lambdaQuery(Address.class).eq(Address::getUserId, userId).list();// 3.處理voUserVO userVO = BeanUtil.copyProperties(user, UserVO.class);userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));return userVO;
}
在查詢地址時,我們采用了Db的靜態(tài)方法,因此避免了注入AddressService,減少了循環(huán)依賴的風(fēng)險。
再來實現(xiàn)一個功能:
-
根據(jù)id批量查詢用戶,并查詢出用戶對應(yīng)的所有地址
3.邏輯刪除
對于一些比較重要的數(shù)據(jù),我們往往會采用邏輯刪除的方案,即:
-
在表中添加一個字段標(biāo)記數(shù)據(jù)是否被刪除
-
當(dāng)刪除數(shù)據(jù)時把標(biāo)記置為true
-
查詢時過濾掉標(biāo)記為true的數(shù)據(jù)
一旦采用了邏輯刪除,所有的查詢和刪除邏輯都要跟著變化,非常麻煩。
為了解決這個問題,MybatisPlus就添加了對邏輯刪除的支持。
注意,只有MybatisPlus生成的SQL語句才支持自動的邏輯刪除,自定義SQL需要自己手動處理邏輯刪除。
例如,我們給address
表添加一個邏輯刪除字段:
alter table address add deleted bit default b'0' null comment '邏輯刪除';
然后給Address
實體添加deleted
字段:?
接下來,我們要在application.yml
中配置邏輯刪除字段:?
mybatis-plus:global-config:db-config:logic-delete-field: deleted # 全局邏輯刪除的實體字段名(since 3.3.0,配置后可以忽略不配置步驟2)logic-delete-value: 1 # 邏輯已刪除值(默認(rèn)為 1)logic-not-delete-value: 0 # 邏輯未刪除值(默認(rèn)為 0)
測試: 首先,我們執(zhí)行一個刪除操作:?
@Test
void testDeleteByLogic() {// 刪除方法與以前沒有區(qū)別addressService.removeById(59L);
}
方法與普通刪除一模一樣,但是底層的SQL邏輯變了:
查詢一下試試:?
@Test
void testQuery() {List<Address> list = addressService.list();list.forEach(System.out::println);
}
會發(fā)現(xiàn)id為59的確實沒有查詢出來,而且SQL中也對邏輯刪除字段做了判斷:?
?
綜上, 開啟了邏輯刪除功能以后,我們就可以像普通刪除一樣做CRUD,基本不用考慮代碼邏輯問題。還是非常方便的。
注意: 邏輯刪除本身也有自己的問題,比如:
-
會導(dǎo)致數(shù)據(jù)庫表垃圾數(shù)據(jù)越來越多,從而影響查詢效率
-
SQL中全都需要對邏輯刪除字段做判斷,影響查詢效率
因此,我不太推薦采用邏輯刪除功能,如果數(shù)據(jù)不能刪除,可以采用把數(shù)據(jù)遷移到其它表的辦法。
4.通用枚舉
User類中有一個用戶狀態(tài)字段:
這種字段我們一般會定義一個枚舉,做業(yè)務(wù)判斷的時候就可以直接基于枚舉做比較。但是我們數(shù)據(jù)庫采用的是int
類型,對應(yīng)的PO也是Integer
。因此業(yè)務(wù)操作時必須手動把枚舉
與Integer
轉(zhuǎn)換,非常麻煩。
因此,MybatisPlus提供了一個處理枚舉的類型轉(zhuǎn)換器,可以幫我們把枚舉類型與數(shù)據(jù)庫類型自動轉(zhuǎn)換。
定義枚舉
我們定義一個用戶狀態(tài)的枚舉:
?
代碼如下:
package com.itheima.mp.enums;import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;@Getter
public enum UserStatus {NORMAL(1, "正常"),FREEZE(2, "凍結(jié)");private final int value;private final String desc;UserStatus(int value, String desc) {this.value = value;this.desc = desc;}
}
然后把User
類中的status
字段改為UserStatus
類型:
要讓MybatisPlus
處理枚舉與數(shù)據(jù)庫類型自動轉(zhuǎn)換,我們必須告訴MybatisPlus
,枚舉中的哪個字段的值作為數(shù)據(jù)庫值。 MybatisPlus
提供了@EnumValue
注解來標(biāo)記枚舉屬性:?
配置枚舉處理器
在application.yaml文件中添加配置:
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
測試
@Test
void testService() {List<User> list = userService.list();list.forEach(System.out::println);
}
最終,查詢出的User
類的status
字段會是枚舉類型:?
?
?同時,為了使頁面查詢結(jié)果也是枚舉格式,我們需要修改UserVO中的status屬性:
并且,在UserStatus枚舉中通過@JsonValue
注解標(biāo)記JSON序列化時展示的字段:?
最后,在頁面查詢,結(jié)果如下:?
5.JSON類型處理器
數(shù)據(jù)庫的user表中有一個info
字段,是JSON類型:
?
格式像這樣:?
{"age": 20, "intro": "佛系青年", "gender": "male"}
而目前User
實體類中卻是String
類型:
這樣一來,我們要讀取info中的屬性時就非常不方便。如果要方便獲取,info的類型最好是一個Map
或者實體類。
而一旦我們把info
改為對象
類型,就需要在寫入數(shù)據(jù)庫時手動轉(zhuǎn)為String
,再讀取數(shù)據(jù)庫時,手動轉(zhuǎn)換為對象
,這會非常麻煩。
因此MybatisPlus提供了很多特殊類型字段的類型處理器,解決特殊字段類型與數(shù)據(jù)庫類型轉(zhuǎn)換的問題。例如處理JSON就可以使用JacksonTypeHandler
處理器。
接下來,我們就來看看這個處理器該如何使用。
定義實體
首先,我們定義一個單獨實體類來與info字段的屬性匹配:
代碼如下:
package com.itheima.mp.domain.po;import lombok.Data;@Data
public class UserInfo {private Integer age;private String intro;private String gender;
}
使用類型處理器
接下來,將User類的info字段修改為UserInfo類型,并聲明類型處理器:
測試可以發(fā)現(xiàn),所有數(shù)據(jù)都正確封裝到UserInfo當(dāng)中了:?
同時,為了讓頁面返回的結(jié)果也以對象格式返回,我們要修改UserVO中的info字段:
此時,在頁面查詢結(jié)果如下:?
插件功能
MybatisPlus提供了很多的插件功能,進(jìn)一步拓展其功能。目前已有的插件有:
-
PaginationInnerInterceptor
:自動分頁 -
TenantLineInnerInterceptor
:多租戶 -
DynamicTableNameInnerInterceptor
:動態(tài)表名 -
OptimisticLockerInnerInterceptor
:樂觀鎖 -
IllegalSQLInnerInterceptor
:sql 性能規(guī)范 -
BlockAttackInnerInterceptor
:防止全表更新與刪除
注意: 使用多個分頁插件的時候需要注意插件定義順序,建議使用順序如下:
-
多租戶,動態(tài)表名
-
分頁,樂觀鎖
-
sql 性能規(guī)范,防止全表更新與刪除
這里我們以分頁插件為里來學(xué)習(xí)插件的用法。
分頁插件
在未引入分頁插件的情況下,MybatisPlus
是不支持分頁功能的,IService
和BaseMapper
中的分頁方法都無法正常起效。 所以,我們必須配置分頁插件。
配置分頁插件
在項目中新建一個配置類:
其代碼如下:?
package com.itheima.mp.config;import com.baomidou.mybatisplus.annotation.DbType;
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() {// 初始化核心插件MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分頁插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}
分頁API
編寫一個分頁查詢的測試:
@Test
void testPageQuery() {// 1.分頁查詢,new Page()的兩個參數(shù)分別是:頁碼、每頁大小Page<User> p = userService.page(new Page<>(2, 2));// 2.總條數(shù)System.out.println("total = " + p.getTotal());// 3.總頁數(shù)System.out.println("pages = " + p.getPages());// 4.數(shù)據(jù)List<User> records = p.getRecords();records.forEach(System.out::println);
}
運行的SQL如下:?
這里用到了分頁參數(shù),Page,即可以支持分頁參數(shù),也可以支持排序參數(shù)。常見的API如下:?
int pageNo = 1, pageSize = 5;
// 分頁參數(shù)
Page<User> page = Page.of(pageNo, pageSize);
// 排序參數(shù), 通過OrderItem來指定
page.addOrder(new OrderItem("balance", false));userService.page(page);
通用分頁實體
現(xiàn)在要實現(xiàn)一個用戶分頁查詢的接口,接口規(guī)范如下:
參數(shù) | 說明 |
請求方式 | GET |
請求路徑 | /users/page |
請求參數(shù) |
|
返回值 |
|
特殊說明 |
|
這里需要定義3個實體:
-
UserQuery
:分頁查詢條件的實體,包含分頁、排序參數(shù)、過濾條件 -
PageDTO
:分頁結(jié)果實體,包含總條數(shù)、總頁數(shù)、當(dāng)前頁數(shù)據(jù) -
UserVO
:用戶頁面視圖實體
實體
由于UserQuery之前已經(jīng)定義過了,并且其中已經(jīng)包含了過濾條件,具體代碼如下:
package com.itheima.mp.domain.query;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@ApiModel(description = "用戶查詢條件實體")
public class UserQuery {@ApiModelProperty("用戶名關(guān)鍵字")private String name;@ApiModelProperty("用戶狀態(tài):1-正常,2-凍結(jié)")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
?其中缺少的僅僅是分頁條件,而分頁條件不僅僅用戶分頁查詢需要,以后其它業(yè)務(wù)也都有分頁查詢的需求。因此建議將分頁查詢條件單獨定義為一個PageQuery
實體:
PageQuery
是前端提交的查詢參數(shù),一般包含四個屬性:
-
pageNo
:頁碼 -
pageSize
:每頁數(shù)據(jù)條數(shù) -
sortBy
:排序字段 -
isAsc
:是否升序
@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("用戶名關(guān)鍵字")private String name;@ApiModelProperty("用戶狀態(tài):1-正常,2-凍結(jié)")private Integer status;@ApiModelProperty("余額最小值")private Integer minBalance;@ApiModelProperty("余額最大值")private Integer maxBalance;
}
?返回值的用戶實體沿用之前定一個UserVO
實體:
?最后,則是分頁實體PageDTO:
?
代碼如下:?
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 = "分頁結(jié)果")
public class PageDTO<T> {@ApiModelProperty("總條數(shù)")private Long total;@ApiModelProperty("總頁數(shù)")private Long pages;@ApiModelProperty("集合")private List<T> list;
}
開發(fā)接口
我們在UserController
中定義分頁查詢用戶的接口:
package com.itheima.mp.controller;import com.itheima.mp.domain.dto.PageDTO;
import com.itheima.mp.domain.query.PageQuery;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {private final UserService userService;@GetMapping("/page")public PageDTO<UserVO> queryUsersPage(UserQuery query){return userService.queryUsersPage(query);}// 。。。 略
}
然后在IUserService
中創(chuàng)建queryUsersPage
方法:
PageDTO<UserVO> queryUsersPage(PageQuery query);
?接下來,在UserServiceImpl中實現(xiàn)該方法:
@Override
public PageDTO<UserVO> queryUsersPage(PageQuery query) {// 1.構(gòu)建條件// 1.1.分頁條件Page<User> page = Page.of(query.getPageNo(), query.getPageSize());// 1.2.排序條件if (query.getSortBy() != null) {page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));}else{// 默認(rèn)按照更新時間排序page.addOrder(new OrderItem("update_time", false));}// 2.查詢page(page);// 3.數(shù)據(jù)非空校驗List<User> records = page.getRecords();if (records == null || records.size() <= 0) {// 無數(shù)據(jù),返回空結(jié)果return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList());}// 4.有數(shù)據(jù),轉(zhuǎn)換List<UserVO> list = BeanUtil.copyToList(records, UserVO.class);// 5.封裝返回return new PageDTO<UserVO>(page.getTotal(), page.getPages(), list);
}
啟動項目,在頁面查看:?
改造PageQuery實體
在剛才的代碼中,從PageQuery
到MybatisPlus
的Page
之間轉(zhuǎn)換的過程還是比較麻煩的。
我們完全可以在PageQuery
這個實體中定義一個工具方法,簡化開發(fā)。 像這樣:
package com.itheima.mp.domain.query;import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;@Data
public class PageQuery {private Integer pageNo;private Integer pageSize;private String sortBy;private Boolean isAsc;public <T> Page<T> toMpPage(OrderItem ... orders){// 1.分頁條件Page<T> p = Page.of(pageNo, pageSize);// 2.排序條件// 2.1.先看前端有沒有傳排序字段if (sortBy != null) {p.addOrder(new OrderItem(sortBy, isAsc));return p;}// 2.2.再看有沒有手動指定排序字段if(orders != null){p.addOrder(orders);}return p;}public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){return this.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);}
}
?這樣我們在開發(fā)也時就可以省去對從PageQuery
到Page
的的轉(zhuǎn)換:
// 1.構(gòu)建條件
Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();
改造PageDTO實體
在查詢出分頁結(jié)果后,數(shù)據(jù)的非空校驗,數(shù)據(jù)的vo轉(zhuǎn)換都是模板代碼,編寫起來很麻煩。
我們完全可以將其封裝到PageDTO的工具方法中,簡化整個過程:
package com.itheima.mp.domain.dto;import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {private Long total;private Long pages;private List<V> list;/*** 返回空分頁結(jié)果* @param p MybatisPlus的分頁結(jié)果* @param <V> 目標(biāo)VO類型* @param <P> 原始PO類型* @return VO的分頁對象*/public static <V, P> PageDTO<V> empty(Page<P> p){return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());}/*** 將MybatisPlus分頁結(jié)果轉(zhuǎn)為 VO分頁結(jié)果* @param p MybatisPlus的分頁結(jié)果* @param voClass 目標(biāo)VO類型的字節(jié)碼* @param <V> 目標(biāo)VO類型* @param <P> 原始PO類型* @return VO的分頁對象*/public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {// 1.非空校驗List<P> records = p.getRecords();if (records == null || records.size() <= 0) {// 無數(shù)據(jù),返回空結(jié)果return empty(p);}// 2.數(shù)據(jù)轉(zhuǎn)換List<V> vos = BeanUtil.copyToList(records, voClass);// 3.封裝返回return new PageDTO<>(p.getTotal(), p.getPages(), vos);}/*** 將MybatisPlus分頁結(jié)果轉(zhuǎn)為 VO分頁結(jié)果,允許用戶自定義PO到VO的轉(zhuǎn)換方式* @param p MybatisPlus的分頁結(jié)果* @param convertor PO到VO的轉(zhuǎn)換函數(shù)* @param <V> 目標(biāo)VO類型* @param <P> 原始PO類型* @return VO的分頁對象*/public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {// 1.非空校驗List<P> records = p.getRecords();if (records == null || records.size() <= 0) {// 無數(shù)據(jù),返回空結(jié)果return empty(p);}// 2.數(shù)據(jù)轉(zhuǎn)換List<V> vos = records.stream().map(convertor).collect(Collectors.toList());// 3.封裝返回return new PageDTO<>(p.getTotal(), p.getPages(), vos);}
}
?最終,業(yè)務(wù)層的代碼可以簡化為:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {// 1.構(gòu)建條件Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();// 2.查詢page(page);// 3.封裝返回return PageDTO.of(page, UserVO.class);
}
如果是希望自定義PO到VO的轉(zhuǎn)換過程,可以這樣做:
@Override
public PageDTO<UserVO> queryUserByPage(PageQuery query) {// 1.構(gòu)建條件Page<User> page = query.toMpPageDefaultSortByCreateTimeDesc();// 2.查詢page(page);// 3.封裝返回return PageDTO.of(page, user -> {// 拷貝屬性到VOUserVO vo = BeanUtil.copyProperties(user, UserVO.class);// 用戶名脫敏String username = vo.getUsername();vo.setUsername(username.substring(0, username.length() - 2) + "**");return vo;});
}
?最終查詢的結(jié)果如下: