MyBatis-Plus 整理

紫邪情發表於2024-07-10

# 前言

程式碼生成器外掛選擇去這裡:https://www.cnblogs.com/zixq/p/16726534.html

相關外掛在那裡面已經提到了

# 上手

MyBatis-Plus 是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生

PS:要開啟官網需要將瀏覽器的廣告攔截外掛新增白名單,具體操作訪問官網即可看到

官網入手示例:https://www.baomidou.com/getting-started/

1、依賴

<!-- 這個starter包含對mybatis的自動裝配,完全可以替換掉Mybatis的starter -->

<!-- Spring Boot2 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter-test</artifactId>
    <version>3.5.7</version>
</dependency>


<!-- Spring Boot3 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version>
</dependency>

2、介面 extends BaseMapper<T>。該介面定義了單表CRUD的一些常用API

// 泛型 User 是與資料庫對應的實體類
public interface UserMapper extends BaseMapper<User> {
}

image-20240708220102171

Mybatis-Plus就是根據PO實體的資訊來推斷出表的資訊,從而生成SQL的。預設情況下:

  • MybatisPlus會把PO實體的類名駝峰轉下劃線作為表名

  • MybatisPlus會把PO實體的所有變數名駝峰轉下劃線作為表的欄位名,並根據變數型別推斷欄位型別

  • MybatisPlus會把名為id的欄位作為主鍵

但很多情況下,預設的實現與實際場景不符,因此MybatisPlus提供了一些註解便於我們宣告表資訊

3、然後就可以像呼叫自定義mapper介面方法一樣呼叫了

# 註解

更多註解看官網:https://www.baomidou.com/reference/annotation/

@TableName:用來指定表名

@TableId:用來指定表中的主鍵欄位資訊。IdType列舉如下

  • AUTO:資料庫自增長
  • INPUT:透過set方法自行輸入
  • ASSIGN_ID:分配 ID(預設方式),介面IdentifierGenerator的方法nextId來生成id,預設實現類為DefaultIdentifierGenerator雪花演算法

@TableField:用來指定表中的普通欄位資訊

使用@TableField的常見場景:

  • 成員變數名與資料庫欄位名不一致
  • 成員變數名以is開頭,且是布林值
  • 成員變數名與資料庫關鍵字衝突
  • 成員變數不是資料庫欄位

# 配置

相容MyBatis的配置

另外更多配置,檢視官網:https://www.baomidou.com/reference/

mybatis-plus:
	type-aliases-package: com.zixq.mp.domain.po # 別名掃描包
	mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml檔案地址,預設值
	configuration: map-underscore-to-camel-case: true # 是否開啟下劃線和駝峰的對映
	cache-enabled: false # 是否開啟二級快取
	global-config:
		db-config:
            id-type: assign_id # id為雪花演算法生成	這個優先順序沒有使用 @TableId 註解高
            update-strategy: not_null # 更新策略:只更新非空欄位
# 全域性邏輯刪除的實體欄位名(since 3.3.0,配置後可以忽略不配置步驟2)
            logic-delete-field: deleted
            logic-delete-value: 1 # 邏輯已刪除值(預設為 1)
            logic-not-delete-value: 0 # 邏輯未刪除值(預設為 0)

邏輯刪除進行上面配置後mp就可自動實現delete變update操作了,但邏輯刪除會佔用空間,影響效能,所以可採用刪除前將資料遷移到另一張表中

如果要表刪除標誌欄位沒統一的話,可以使用 @TableLogic 來指定

@TableLogic
private Boolean deleted;

# 條件構造器

1、QueryWrapper 和 LambdaQueryWrapper一般用來構建select、delete、update的where條件部分

2、UpdateWrapper 和 LambdaUpdateWrapper通常只有在set語句比較特殊才使用,如set的是 money = money - 1000

3、儘量使用LambdaQueryWrapper 和 LambdaUpdateWrapper,避免硬編碼

image-20240708233808472

BaseMapper包含的Wrapper構建起

image-20240708234326940

AbstractWrapper

image-20240708234424820

QueryWrapper

image-20240708234459412

UpdateWrapper

image-20240708234522243

# 示例

1、QueryWrapper根據指定欄位和條件查詢

SQL

SELECT id, username, info, balance
FROM `user`
WHERE username LIKE ? AND  balance >= ?;

mp構建:

// 1、構建查詢條件
Querywrapper<User> wrapper = new QueryWrapper<User()
    .select("id", "username", "info","balance")
    .like("username", "o")
    ·ge("balance", 1000);

// 2、查詢
List<User> users = userMapper.selectList(wrapper);



// 使用 LambdaWrapper 的方式
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User()
    .select(User::getId, User::getUsername, User::getInfo,User::getBalance)
    .like(User::getUsername, "o")
    ·ge(User::getBalance, 1000);

// 2、查詢
List<User> users = userMapper.selectList(wrapper);

2、QueryWrapper更新構建

SQL

UPDATE `user`
SET balance = 2000
WHERE username = "jack"

mp構建:

// 1、要更新的資料
User user = new User();
user.setBalance(2000);

// 2、更新的條件
QueryWrapper<User> wrapper = new Querywrapper<User>()
    .eq("username", "jack");

// 3、執行更新
userMapper.update(user, wrapper);

3、UpdateWrapper構建set的特殊情況

SQL:

UPDATE user
SET balance = balance - 200
WHERE id in (1, 2, 4)

mp構建:

UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
    .setSql("balance = balance - 200")
    .in("id", List.of(1L, 2L, 4L));

userMapper.update(null, wrapper) ;

# 自定義SQL

適用場景:要構建的SQL除了where條件之外的語句很複雜,那麼就讓mp幫我們構建where條件部分,其他部分由我們自己構建

上手

SQL:

UPDATE user
SET balance = balance - 200
WHERE id in (1, 2, 4)

mp構建:

UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
    .setSql("balance = balance - 200")
    .in("id", List.of(1L, 2L, 4L));

userMapper.update(null, wrapper) ;

setSql("balance = balance -2oo") 這部分應該在mapper層,而不是在業務層,所以需要進行傳遞,然後在mapper層進行SQL拼接

1、業務層

// 1、準備自定義查詢條件
List<Long> ids = List.of(1L, 2L, 4L);
QueryWrapper<User> wrapper = new QueryWrapper<User>()
    .in("id", ids);

// 2、呼叫mapper的自定義方法,直接傳遞Wrapper
userMapper.deductBalanceByIds(200, wrapper);

2、mapper層

public interface UserMapper extends BaseMapper<User> {
    
    /**
     * <p>
     * 		@Param("ew")	引數名必須是這個	也可以使用 @Param(Constants.WRAPPER)
     *		${ew.customSqlSegment}	是mp自動解析自定義SQL片段
     * </p>
     */
    @Select("UPDATE user SET balance = balance - #{money} ${ew.customSqlSegment}")
    void deductBalanceByIds(@Param("money") int money, 
                            @Param("ew") QueryWrapper<User> wrapper);
}

# 多表關聯

MyBatis構建的方式:

<select id="queryUserByIdAndAddr" resultType="com.itheima.mp.domain.po.User">
      SELECT *
      FROM user u
	      INNER JOIN address a ON u.id = a.user_id
      WHERE u.id
      <foreach collection="ids" separator="," item="id" open="IN (" close=")">
          #{id}
      </foreach>
      AND a.city = #{city}
  </select>

利用Wrapper來構建查詢條件,然後手寫SELECT及FROM部分,實現多表查詢

業務層:

// 1、準備自定義查詢條件
QueryWrapper<User> wrapper = new QueryWrapper<User>()
        .in("u.id", List.of(1L, 2L, 4L))
        .eq("a.city", "北京");

// 2、呼叫mapper的自定義方法
List<User> users = userMapper.queryUserByWrapper(wrapper);

mapper層:

@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);

# IService介面

MybatisPlus不僅提供了BaseMapper,還提供了通用的Service介面及預設實現,封裝了一些常用的service模板方法

通用介面為IService,預設實現為ServiceImpl

image-20240709010248578

# 上手

如果直接繼承 IService 介面,則需要實現裡面的方法,因此mp提供的一個預設實現 ServiceImp

image-20240709010435817

1、自定義Service介面

public interface IUserService extends IService<User> {}

2、自定義Service介面實現類:繼承mp的 ServiceImpl<M, T>

/**
 *	ServiceImpl<UserMapper, User>	UserMapper指定對應的mapper	User指定對應的實體類
 */
public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    implements IUserService {}

3、正常使用上面那些增刪改、單個查、多個查之類的API即可

# Lambda

IService中還提供了Lambda功能來簡化我們的複雜查詢(LambdaQuery)及更新功能(LambdaUpdate)

# LambdaQuery

根據複雜條件查詢使用者的介面,查詢條件如下:

  • name:使用者名稱關鍵字,可以為空
  • status:使用者狀態,可以為空
  • minBalance:最小余額,可以為空
  • maxBalance:最大餘額,可以為空
GetMapping("/list")
@ApiOperation("根據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();	// 告訴MP我們的呼叫結果需要的是一個list集合	上面是構建條件

    // 3、處理vo
    return BeanUtil.copyToList(users, UserVO.class);
}

UserQuery實體

@Data
@ApiModel(description = "使用者查詢條件實體")
public class UserQuery {
    @ApiModelProperty("使用者名稱關鍵字")
    private String name;
    @ApiModelProperty("使用者狀態:1-正常,2-凍結")
    private Integer status;
    @ApiModelProperty("餘額最小值")
    private Integer minBalance;
    @ApiModelProperty("餘額最大值")
    private Integer maxBalance;
}

除了list(),還可選的方法有:

  • one():最多1個結果
  • list():返回集合結果
  • count():返回計數結果

# LambdaUpdate

根據id修改使用者餘額,如果扣減後餘額為0,則將使用者status修改為凍結狀態2

public class UserServiceImpl extends ServiceImpl<UserMapper, User> 
    implements IUserService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deductBalance(Long id, Integer money) {
        
        // 1、查詢使用者
        User user = this.getById(id);
        // 2.校驗使用者狀態
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("使用者狀態異常!");
        }

        // 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) // 動態判斷,是否更新status
                .eq(User::getId, id)
                .eq(User::getBalance, user.getBalance()) // 樂觀鎖
                .update();	// 上面為構建條件,這一步才為真正去修改
    }
}

# saveBatch 批次新增 說明

YAML中datesource的url後新增引數 rewriteBatchedStatements=true

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;
}


@Test
void testSaveBatch() {
    // 準備10萬條資料
    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));
}

saveBatch() 原始碼

@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++;
        }
    });
}

可見Mybatis-Plus的批處理是基於PrepareStatement的預編譯模式,形成的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);

這就需要修改SQL的配置,新增 &rewriteBatchedStatements=true: 引數

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true

這樣配置之後,效能可以得到極大地提升

# Db 靜態工具類

官網示例:https://gitee.com/baomidou/mybatis-plus/blob/3.0/mybatis-plus/src/test/java/com/baomidou/mybatisplus/test/toolkit/DbTest.java

Db這個類裡面的靜態方法就是IService中的方法,實現方式有點區別(提供Class物件)

適用場景:A類中注入B類來進行呼叫問題。使用該靜態工具就不需要注入,直接使用Class就可進行呼叫了,從而減少迴圈依賴風險

image-20240709172544251

注意

  • 使用 Db Kit 前,需要確保專案中已注入對應實體的 BaseMapper。
  • 當引數為 Wrapper 時,需要在 Wrapper 中傳入 Entity 或者 EntityClass,以便尋找對應的 Mapper。
  • 不建議在迴圈中頻繁呼叫 Db Kit 的方法,如果是批次操作,建議先將資料構造好,然後使用 Db.saveBatch(資料) 等批次方法進行儲存

示例:

@Override
public UserVO queryUserAndAddressById(Long userId) {
    
    // 1、查詢使用者
    User user = getById(userId);
    if (user == null) {
        return null;
    }
    
    // 2、查詢收貨地址	不需要注入 AddressSerivce 了
    List<Address> addresses = Db.lambdaQuery(Address.class)
            .eq(Address::getUserId, userId)
            .list();
    
    // 3、處理vo
    UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
    userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
    
    return userVO;
}

# 列舉處理器

解決Java列舉型別和資料庫型別轉換

image-20240709204115909

@Data
public class UserEntity {
    // ......................
    
    // 0 正常	1禁用
    private Integer status;
}

上面這種和資料庫int型別轉換方便,但是不符合編碼,因為0和1得手動輸入,程式碼多了很麻煩且容易弄錯甚至混亂不堪(不信邪的可以去看若依專案:RuoYi-Vue),因此我們需要統一狀態,即列舉

@Getter
public enum UserStatusEnum {
    NORMAL(1, "正常"),
    FREEZE(2, "凍結")
    ;
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

實體類

@Data
public class UserEntity {
    // ......................
    
    // 0 正常	1禁用
    private UserStatusEnum status;
}

但此時實體是 UserStatusEnum 而資料庫是int,涉及型別轉換,這就需要mp來做了

1、YAML配置

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

2、@EnumValue 標記列舉中哪個值為資料庫欄位值

@Getter
public enum UserStatusEnum {
    NORMAL(0, "正常"),
    FREEZE(1, "凍結")

    ;
    
    @EnumValue
    private final int value;
    @JsonValue	// Jackson的,返回前端的值是正常或凍結 而不是 NORMAL 這種	SpringMVC底層使用的是 Jackson
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

# JSON型別處理器

官網:https://www.baomidou.com/guides/type-handler/

資料庫中欄位是JSON格式,和Java實體類對應欄位進行適配,方便操作JSON格式

mp提供的JSON處理器預設是JacksonHandler,也推薦使用它,因為安全

image-20240710000948018

Java定義JSON對應的類

@Data
public class Userlnfo {
    privateIntegerage;
    privateString intro;
    privateString gender;
}

Java實體類

@Data@TableName(value="user", autoResultMap = true)	// 操作1
public class User{
    private Long id;
    private String username;
    @TableField(typeHandler = JacksonTypeHandler.class)	// 操作2
    private String UserInfoinfo

image-20240710000932372

# 分頁外掛

官網:https://www.baomidou.com/plugins/pagination/

# 上手

1、註冊外掛

@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 初始化核心外掛
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分頁外掛
        PaginationInnerInterceptor pgInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        // 設定分頁最大條數
        pgInterceptor.setMaxLimit(1000);
        // 註冊分頁外掛
        interceptor.addInnerInterceptor();
        
        return interceptor;
    }
}

使用分頁:

int pageNo = 1, pageSize = 2;

// 1、準備分頁條件
// 1.1、分頁條件
Page<User> page = Page.of(pageNo,pageSize);
// 1.2、排序條件	true 為升序 false 為降序
page.addorder(new OrderItem("balance", true));
page.addorder(new OrderItem("id", true));	// balance 相同下,以 id 排序

// 2、分頁查詢
Page<User> p = userservice.page(page);

// 3、解析
long total = p·getTotal();	// 總條數
System.out.println("total = " + total);
long pages = p.getPages();	// 總頁數
System.out.println("pages = " ++ pages);

List<User> users = p·getRecords();	// 分頁後的資料

# 企業級mp相關設計

直接去看這個專案:RuoYi企業級改造版:AgileBoot-Back-End-Basic

對應md文件

連結:MyBatis-Plus。檔案格式:md

相關文章