MyBatis-plus 創業公司首選利器!
大家好,我是Tom哥
Mybatis-plus是一款Mybatis增強工具,用於簡化開發,提高效率。下文使用縮寫 mp 來簡化表示 mybatis-plus ,本文主要介紹mp搭配SpringBoot的使用。
注:本文使用的mp版本是當前最新的3.4.2,早期版本的差異請自行查閱文件
官方網站:baomidou.com/
快速入門
-
建立一個SpringBoot專案
-
匯入依賴
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-plus</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project> -
配置資料庫
# application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #開啟SQL語句列印 -
建立一個實體類
package com.example.mp.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
} -
建立一個mapper介面
package com.example.mp.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User;
public interface UserMapper extends BaseMapper<User> { } -
在SpringBoot啟動類上配置mapper介面的掃描路徑
package com.example.mp;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.mp.mappers")
public class MybatisPlusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusApplication.class, args);
}
} -
在資料庫中建立表
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主鍵',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年齡',
email VARCHAR(50) DEFAULT NULL COMMENT '郵箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直屬上級id',
create_time DATETIME DEFAULT NULL COMMENT '建立時間',
CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user (id)
) ENGINE=INNODB CHARSET=UTF8;
INSERT INTO user (id, name, age ,email, manager_id, create_time) VALUES
(1, '大BOSS', 40, '[email protected]', NULL, '2021-03-22 09:48:00'),
(2, '李經理', 40, '[email protected]', 1, '2021-01-22 09:48:00'),
(3, '黃主管', 40, '[email protected]', 2, '2021-01-22 09:48:00'),
(4, '吳組長', 40, '[email protected]', 2, '2021-02-22 09:48:00'),
(5, '小菜', 40, '[email protected]', 2, '2021-02-22 09:48:00') -
編寫一個SpringBoot測試類
package com.example.mp;
import com.example.mp.mappers.UserMapper;
import com.example.mp.po.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleTest {
@Autowired
private UserMapper mapper;
@Test
public void testSelect() {
List<User> list = mapper.selectList(null);
assertEquals(5, list.size());
list.forEach(System.out::println);
}
}
準備工作完成
資料庫情況如下
專案目錄如下
執行測試類
可以看到,針對單表的基本CRUD操作,只需要建立好實體類,並建立一個繼承自 BaseMapper
的介面即可,可謂非常簡潔。
並且,我們注意到, User
類中的 managerId
, createTime
屬性,自動和資料庫表中的 manager_id
, create_time
對應了起來,這是因為mp自動做了資料庫下劃線命名,到Java類的駝峰命名之間的轉化。
核心功能
註解
mp一共提供了8個註解,這些註解是用在Java的實體類上面的。
-
@TableName
註解在類上,指定類和資料庫表的對映關係。 實體類的類名(轉成小寫後)和資料庫表名相同時 ,可以不指定該註解。
-
@TableId
註解在實體類的某一欄位上, 表示這個欄位對應資料庫表的主鍵 。當主鍵名為id時(表中列名為id,實體類中欄位名為id),無需使用該註解顯式指定主鍵,mp會自動關聯。若類的欄位名和表的列名不一致,可用
value
屬性指定表的列名。另,這個註解有個重要的屬性type
,用於指定主鍵策略,參見主鍵策略小節 -
@TableField
註解在某一欄位上,指定Java實體類的欄位和資料庫表的列的對映關係。這個註解有如下幾個應用場景。
-
排除非表字段
若Java實體類中某個欄位,不對應表中的任何列,它只是用於儲存一些額外的,或組裝後的資料,則可以設定
exist
屬性為false
,這樣在對實體物件進行插入時,會忽略這個欄位。排除非表字段也可以通過其他方式完成,如使用static
或transient
關鍵字,但個人覺得不是很合理,不做贅述 -
欄位驗證策略
通過
insertStrategy
,updateStrategy
,whereStrategy
屬性進行配置,可以控制在實體物件進行插入,更新,或作為WHERE條件時,物件中的欄位要如何組裝到SQL語句中。參見配置小節 -
欄位填充策略
通過
fill
屬性指定,欄位為空時會進行自動填充 -
@Version
樂觀鎖註解,參見樂觀鎖小節
-
@EnumValue
註解在列舉欄位上
-
@TableLogic
邏輯刪除,參見邏輯刪除小節
-
KeySequence
序列主鍵策略(
oracle
) -
InterceptorIgnore
外掛過濾規則
CRUD介面
mp封裝了一些最基礎的CRUD方法,只需要直接繼承mp提供的介面,無需編寫任何SQL,即可食用。mp提供了兩套介面,分別是Mapper CRUD介面和Service CRUD介面。並且mp還提供了條件構造器 Wrapper
,可以方便地組裝SQL語句中的WHERE條件,參見條件構造器小節
Mapper CRUD介面
只需定義好實體類,然後建立一個介面,繼承mp提供的 BaseMapper
,即可食用。mp會在mybatis啟動時,自動解析實體類和表的對映關係,並注入帶有通用CRUD方法的mapper。 BaseMapper
裡提供的方法,部分列舉如下:
-
insert(T entity)
插入一條記錄 -
deleteById(Serializable id)
根據主鍵id刪除一條記錄 -
delete(Wrapper<T> wrapper)
根據條件構造器wrapper進行刪除 -
selectById(Serializable id)
根據主鍵id進行查詢 -
selectBatchIds(Collection idList)
根據主鍵id進行批量查詢 -
selectByMap(Map<String,Object> map)
根據map中指定的列名和列值進行 等值匹配 查詢 -
selectMaps(Wrapper<T> wrapper)
根據 wrapper 條件,查詢記錄,將查詢結果封裝為一個Map,Map的key為結果的列,value為值 -
selectList(Wrapper<T> wrapper)
根據條件構造器wrapper
進行查詢 -
update(T entity, Wrapper<T> wrapper)
根據條件構造器wrapper
進行更新 -
updateById(T entity)
-
...
簡單的食用示例如前文快速入門小節,下面講解幾個比較特別的方法
selectMaps
BaseMapper
介面還提供了一個 selectMaps
方法,這個方法會將查詢結果封裝為一個Map,Map的key為結果的列,value為值
該方法的使用場景如下:
-
只查部分列
當某個表的列特別多,而SELECT的時候只需要選取個別列,查詢出的結果也沒必要封裝成Java實體類物件時(只查部分列時,封裝成實體後,實體物件中的很多屬性會是null),則可以用
selectMaps
,獲取到指定的列後,再自行進行處理即可比如
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id","name","email").likeRight("name","黃");
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
} -
進行資料統計
比如
// 按照直屬上級進行分組,查詢每組的平均年齡,最大年齡,最小年齡
/**
select avg(age) avg_age ,min(age) min_age, max(age) max_age from user group by manager_id having sum(age) < 500;
**/
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("manager_id", "avg(age) avg_age", "min(age) min_age", "max(age) max_age")
.groupBy("manager_id").having("sum(age) < {0}", 500);
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
selectObjs
只會返回第一個欄位(第一列)的值,其他欄位會被捨棄
比如
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("id", "name").like("name", "黃");
List<Object> objects = userMapper.selectObjs(wrapper);
objects.forEach(System.out::println);
}
得到的結果,只封裝了第一列的id
selectCount
查詢滿足條件的總數,注意,使用這個方法,不能呼叫 QueryWrapper
的 select
方法設定要查詢的列了。這個方法會自動新增 select count(1)
比如
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "黃");
Integer count = userMapper.selectCount(wrapper);
System.out.println(count);
}
Service CRUD 介面
另外一套CRUD是Service層的,只需要編寫一個介面,繼承 IService
,並建立一個介面實現類,即可食用。(這個介面提供的CRUD方法,和Mapper介面提供的功能大同小異,
比較明顯的區別在於 IService
支援了更多的批量化操作
,如 saveBatch
, saveOrUpdateBatch
等方法。
食用示例如下
-
首先,新建一個介面,繼承
IService
package com.example.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.mp.po.User;
public interface UserService extends IService<User> {
} -
建立這個介面的實現類,並繼承
ServiceImpl
,最後打上@Service
註解,註冊到Spring容器中,即可食用package com.example.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.mp.mappers.UserMapper;
import com.example.mp.po.User;
import com.example.mp.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } -
測試程式碼
package com.example.mp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.mp.po.User;
import com.example.mp.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetOne() {
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery();
wrapper.gt(User::getAge, 28);
User one = userService.getOne(wrapper, false); // 第二引數指定為false,使得在查到了多行記錄時,不丟擲異常,而返回第一條記錄
System.out.println(one);
}
}
另, IService
也支援鏈式呼叫,程式碼寫起來非常簡潔,查詢示例如下
@Test public void testChain() { List<User> list = userService.lambdaQuery() .gt(User::getAge, 39) .likeRight(User::getName, "王") .list(); list.forEach(System.out::println); }
更新示例如下
@Test public void testChain() { userService.lambdaUpdate() .gt(User::getAge, 39) .likeRight(User::getName, "王") .set(User::getEmail, "[email protected]") .update(); }複製程式碼
刪除示例如下
@Test public void testChain() { userService.lambdaUpdate() .like(User::getName, "青蛙") .remove(); }複製程式碼
條件構造器
mp讓我覺得極其方便的一點在於其提供了強大的條件構造器 Wrapper
,可以非常方便的構造WHERE條件。條件構造器主要涉及到3個類, AbstractWrapper
。 QueryWrapper
, UpdateWrapper
,它們的類關係如下
在 AbstractWrapper
中提供了非常多的方法用於構建WHERE條件,而 QueryWrapper
針對 SELECT
語句,提供了 select()
方法,可自定義需要查詢的列,而 UpdateWrapper
針對 UPDATE
語句,提供了 set()
方法,用於構造 set
語句。條件構造器也支援lambda表示式,寫起來非常舒爽。
下面對 AbstractWrapper
中用於構建SQL語句中的WHERE條件的方法進行部分列舉
-
eq
:equals,等於 -
allEq
:all equals,全等於 -
ne
:not equals,不等於 -
gt
:greater than ,大於>
-
ge
:greater than or equals,大於等於≥
-
lt
:less than,小於<
-
le
:less than or equals,小於等於≤
-
between
:相當於SQL中的BETWEEN -
notBetween
-
like like("name","黃") name like '%黃%'
-
likeRight likeRight("name","黃") name like '黃%'
-
likeLeft likeLeft("name","黃") name like '%黃'
-
notLike notLike("name","黃") name not like '%黃%'
-
isNull
-
isNotNull
-
in
-
and
:SQL連線符AND -
or
:SQL連線符OR -
apply
:用於拼接SQL,該方法可用於資料庫函式,並可以動態傳參 -
.......
使用示例
下面通過一些具體的案例來練習條件構造器的使用。(使用前文建立的 user
表)
// 案例先展示需要完成的SQL語句,後展示Wrapper的寫法
// 1. 名字中包含佳,且年齡小於25
// SELECT * FROM user WHERE name like '%佳%' AND age < 25
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "佳").lt("age", 25);
List<User> users = userMapper.selectList(wrapper);
// 下面展示SQL時,僅展示WHERE條件;展示程式碼時, 僅展示Wrapper構建部分
// 2. 姓名為黃姓,且年齡大於等於20,小於等於40,且email欄位不為空
// name like '黃%' AND age BETWEEN 20 AND 40 AND email is not null
wrapper.likeRight("name","黃").between("age", 20, 40).isNotNull("email");
// 3. 姓名為黃姓,或者年齡大於等於40,按照年齡降序排列,年齡相同則按照id升序排列
// name like '黃%' OR age >= 40 order by age desc, id asc
wrapper.likeRight("name","黃").or().ge("age",40).orderByDesc("age").orderByAsc("id");
// 4.建立日期為2021年3月22日,並且直屬上級的名字為李姓
// date_format(create_time,'%Y-%m-%d') = '2021-03-22' AND manager_id IN (SELECT id FROM user WHERE name like '李%')
wrapper.apply("date_format(create_time, '%Y-%m-%d') = {0}", "2021-03-22") // 建議採用{index}這種方式動態傳參, 可防止SQL注入
.inSql("manager_id", "SELECT id FROM user WHERE name like '李%'");
// 上面的apply, 也可以直接使用下面這種方式做字串拼接,但當這個日期是一個外部引數時,這種方式有SQL注入的風險
wrapper.apply("date_format(create_time, '%Y-%m-%d') = '2021-03-22'");
// 5. 名字為王姓,並且(年齡小於40,或者郵箱不為空)
// name like '王%' AND (age < 40 OR email is not null)
wrapper.likeRight("name", "王").and(q -> q.lt("age", 40).or().isNotNull("email"));
// 6. 名字為王姓,或者(年齡小於40並且年齡大於20並且郵箱不為空)
// name like '王%' OR (age < 40 AND age > 20 AND email is not null)
wrapper.likeRight("name", "王").or(
q -> q.lt("age",40)
.gt("age",20)
.isNotNull("email")
);
// 7. (年齡小於40或者郵箱不為空) 並且名字為王姓
// (age < 40 OR email is not null) AND name like '王%'
wrapper.nested(q -> q.lt("age", 40).or().isNotNull("email"))
.likeRight("name", "王");
// 8. 年齡為30,31,34,35
// age IN (30,31,34,35)
wrapper.in("age", Arrays.asList(30,31,34,35));
// 或
wrapper.inSql("age","30,31,34,35");
// 9. 年齡為30,31,34,35, 返回滿足條件的第一條記錄
// age IN (30,31,34,35) LIMIT 1
wrapper.in("age", Arrays.asList(30,31,34,35)).last("LIMIT 1");
// 10. 只選出id, name 列 (QueryWrapper 特有)
// SELECT id, name FROM user;
wrapper.select("id", "name");
// 11. 選出id, name, age, email, 等同於排除 manager_id 和 create_time
// 當列特別多, 而只需要排除個別列時, 採用上面的方式可能需要寫很多個列, 可以採用過載的select方法,指定需要排除的列
wrapper.select(User.class, info -> {
String columnName = info.getColumn();
return !"create_time".equals(columnName) && !"manager_id".equals(columnName);
});
Condition
條件構造器的諸多方法中,均可以指定一個 boolean
型別的引數 condition
,用來決定該條件是否加入最後生成的WHERE語句中,比如
String name = "黃"; // 假設name變數是一個外部傳入的引數
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 僅當 StringUtils.hasText(name) 為 true 時, 會拼接這個like語句到WHERE中
// 其實就是對下面程式碼的簡化
if (StringUtils.hasText(name)) {
wrapper.like("name", name);
}
實體物件作為條件
呼叫建構函式建立一個 Wrapper
物件時,可以傳入一個實體物件。後續使用這個 Wrapper
時,會以實體物件中的非空屬性,構建WHERE條件(預設構建 等值匹配
的WHERE條件,這個行為可以通過實體類裡各個欄位上的 @TableField
註解中的 condition
屬性進行改變)
示例如下
@Test
public void test3() {
User user = new User();
user.setName("黃主管");
user.setAge(28);
QueryWrapper<User> wrapper = new QueryWrapper<>(user);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
執行結果如下。可以看到,是根據實體物件中的非空屬性,進行了 等值匹配查詢 。
若希望針對某些屬性,改變 等值匹配
的行為,則可以在實體類中用 @TableField
註解進行配置,示例如下
package com.example.mp.po;
import com.baomidou.mybatisplus.annotation.SqlCondition;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
@TableField(condition = SqlCondition.LIKE) // 配置該欄位使用like進行拼接
private String name;
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
}
執行下面的測試程式碼
@Test
public void test3() {
User user = new User();
user.setName("黃");
QueryWrapper<User> wrapper = new QueryWrapper<>(user);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
從下圖得到的結果來看,對於實體物件中的 name
欄位,採用了 like
進行拼接
@TableField
中配置的 condition
屬性實則是一個字串, SqlCondition
類中預定義了一些字串以供選擇
package com.baomidou.mybatisplus.annotation;
public class SqlCondition {
//下面的字串中, %s 是佔位符, 第一個 %s 是列名, 第二個 %s 是列的值
public static final String EQUAL = "%s=#{%s}";
public static final String NOT_EQUAL = "%s<>#{%s}";
public static final String LIKE = "%s LIKE CONCAT('%%',#{%s},'%%')";
public static final String LIKE_LEFT = "%s LIKE CONCAT('%%',#{%s})";
public static final String LIKE_RIGHT = "%s LIKE CONCAT(#{%s},'%%')";
}
SqlCondition
中提供的配置比較有限,當我們需要 <
或 >
等拼接方式,則需要自己定義。比如
package com.example.mp.po;
import com.baomidou.mybatisplus.annotation.SqlCondition;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User {
private Long id;
@TableField(condition = SqlCondition.LIKE)
private String name;
@TableField(condition = "%s > #{%s}") // 這裡相當於大於, 其中 > 是字元實體
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
}
測試如下
@Test public void test3() { User user = new User(); user.setName("黃"); user.setAge(30); QueryWrapper<User> wrapper = new QueryWrapper<>(user); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }複製程式碼
從下圖得到的結果,可以看出, name
屬性是用 like
拼接的,而 age
屬性是用 >
拼接的
allEq方法
allEq方法傳入一個 map
,用來做等值匹配
@Test public void test3() { QueryWrapper<User> wrapper = new QueryWrapper<>(); Map<String, Object> param = new HashMap<>(); param.put("age", 40); param.put("name", "黃飛飛"); wrapper.allEq(param); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }複製程式碼
當allEq方法傳入的Map中有value為 null
的元素時,預設會設定為 is null
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 40);
param.put("name", null);
wrapper.allEq(param);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
若想忽略map中value為 null
的元素,可以在呼叫allEq時,設定引數 boolean null2IsNull
為 false
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 40);
param.put("name", null);
wrapper.allEq(param, false);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
若想要在執行allEq時,過濾掉Map中的某些元素,可以呼叫allEq的過載方法 allEq(BiPredicate<R, V> filter, Map<R, V> params)
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
Map<String, Object> param = new HashMap<>();
param.put("age", 40);
param.put("name", "黃飛飛");
wrapper.allEq((k,v) -> !"name".equals(k), param); // 過濾掉map中key為name的元素
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
lambda條件構造器
lambda條件構造器,支援lambda表示式,可以不必像普通條件構造器一樣,以字串形式指定列名,它可以直接以實體類的 方法引用 來指定列。示例如下
@Test
public void testLambda() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, "黃").lt(User::getAge, 30);
List<User> users = userMapper.selectList(wrapper);
users.forEach(System.out::println);
}
像普通的條件構造器,列名是用字串的形式指定,無法在編譯期進行列名合法性的檢查,這就不如lambda條件構造器來的優雅。
另外,還有個 鏈式lambda條件構造器 ,使用示例如下
@Test
public void testLambda() {
LambdaQueryChainWrapper<User> chainWrapper = new LambdaQueryChainWrapper<>(userMapper);
List<User> users = chainWrapper.like(User::getName, "黃").gt(User::getAge, 30).list();
users.forEach(System.out::println);
}
更新操作
上面介紹的都是查詢操作,現在來講更新和刪除操作。
BaseMapper
中提供了2個更新方法
-
updateById(T entity)
根據入參
entity
的id
(主鍵)進行更新,對於entity
中非空的屬性,會出現在UPDATE語句的SET後面,即entity
中非空的屬性,會被更新到資料庫,示例如下@RunWith(SpringRunner.class)
@SpringBootTest
public class UpdateTest {
@Autowired
private UserMapper userMapper;
@Test
public void testUpdate() {
User user = new User();
user.setId(2L);
user.setAge(18);
userMapper.updateById(user);
}
} -
update(T entity, Wrapper<T> wrapper)
根據實體
entity
和條件構造器wrapper
進行更新,示例如下@Test public void testUpdate2() { User user = new User(); user.setName("王三蛋"); LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(); wrapper.between(User::getAge, 26,31).likeRight(User::getName,"吳"); userMapper.update(user, wrapper); }複製程式碼
額外演示一下,把實體物件傳入
Wrapper
,即用實體物件構造WHERE條件的案例@Test
public void testUpdate3() {
User whereUser = new User();
whereUser.setAge(40);
whereUser.setName("王");
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>(whereUser);
User user = new User();
user.setEmail("[email protected]");
user.setManagerId(10L);
userMapper.update(user, wrapper);
}注意到我們的User類中,對
name
屬性和age
屬性進行了如下的設定@Data
public class User {
private Long id;
@TableField(condition = SqlCondition.LIKE)
private String name;
@TableField(condition = "%s > #{%s}")
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
}執行結果
再額外演示一下,鏈式lambda條件構造器的使用
@Test
public void testUpdate5() {
LambdaUpdateChainWrapper<User> wrapper = new LambdaUpdateChainWrapper<>(userMapper);
wrapper.likeRight(User::getEmail, "share")
.like(User::getName, "飛飛")
.set(User::getEmail, "[email protected]")
.update();
}
反思
由於 BaseMapper
提供的2個更新方法都是傳入一個實體物件去執行更新,這 在需要更新的列比較多時還好
,若想要更新的只有那麼一列,或者兩列,則建立一個實體物件就顯得有點麻煩。針對這種情況, UpdateWrapper
提供有 set
方法,可以手動拼接SQL中的SET語句,此時可以不必傳入實體物件,示例如下
@Test
public void testUpdate4() {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.likeRight(User::getEmail, "share").set(User::getManagerId, 9L);
userMapper.update(null, wrapper);
}
刪除操作
BaseMapper
一共提供瞭如下幾個用於刪除的方法
-
deleteById
根據主鍵id進行刪除 -
deleteBatchIds
根據主鍵id進行批量刪除 -
deleteByMap
根據Map進行刪除(Map中的key為列名,value為值,根據列和值進行等值匹配) -
delete(Wrapper<T> wrapper)
根據條件構造器Wrapper
進行刪除
與前面查詢和更新的操作大同小異,不做贅述
自定義SQL
當mp提供的方法還不能滿足需求時,則可以自定義SQL。
原生mybatis
示例如下
-
註解方式
package com.example.mp.mappers;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.mp.po.User;import org.apache.ibatis.annotations.Select;import java.util.List;/** * @Author yogurtzzz * @Date 2021/3/18 11:21 **/public interface UserMapper extends BaseMapper<User> { @Select("select * from user") List<User> selectRaw();}複製程式碼
-
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"><mapper namespace="com.example.mp.mappers.UserMapper"> <select id="selectRaw" resultType="com.example.mp.po.User"> SELECT * FROM user </select></mapper>複製程式碼package com.example.mp.mappers;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.mp.po.User;import org.apache.ibatis.annotations.Select;import java.util.List;public interface UserMapper extends BaseMapper<User> { List<User> selectRaw();}複製程式碼
使用xml時, 若xml檔案與mapper介面檔案不在同一目錄下
,則需要在 application.yml
中配置mapper.xml的存放路徑
mybatis-plus: mapper-locations: /mappers/*複製程式碼
若有多個地方存放mapper,則用陣列形式進行配置
mybatis-plus: mapper-locations: - /mappers/* - /com/example/mp/*複製程式碼
測試程式碼如下
@Test public void testCustomRawSql() { List<User> users = userMapper.selectRaw(); users.forEach(System.out::println); }複製程式碼
結果
mybatis-plus
也可以使用mp提供的Wrapper條件構造器,來自定義SQL
示例如下
-
註解方式
package com.example.mp.mappers;import com.baomidou.mybatisplus.core.conditions.Wrapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.baomidou.mybatisplus.core.toolkit.Constants;import com.example.mp.po.User;import org.apache.ibatis.annotations.Param;import org.apache.ibatis.annotations.Select;import java.util.List;public interface UserMapper extends BaseMapper<User> { // SQL中不寫WHERE關鍵字,且固定使用${ew.customSqlSegment} @Select("select * from user ${ew.customSqlSegment}") List<User> findAll(@Param(Constants.WRAPPER)Wrapper<User> wrapper);}複製程式碼
-
xml方式
package com.example.mp.mappers;import com.baomidou.mybatisplus.core.conditions.Wrapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.mp.po.User;import java.util.List;public interface UserMapper extends BaseMapper<User> { List<User> findAll(Wrapper<User> wrapper);}複製程式碼<!-- UserMapper.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"><mapper namespace="com.example.mp.mappers.UserMapper"> <select id="findAll" resultType="com.example.mp.po.User"> SELECT * FROM user ${ew.customSqlSegment} </select></mapper>複製程式碼
分頁查詢
BaseMapper
中提供了2個方法進行分頁查詢,分別是 selectPage
和 selectMapsPage
,前者會將查詢的結果封裝成Java實體物件,後者會封裝成 Map<String,Object>
。分頁查詢的食用示例如下
-
建立mp的分頁攔截器,註冊到Spring容器中
package com.example.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;@Configurationpublic class MybatisPlusConfig { /** 新版mp ** / @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } /** 舊版mp 用 PaginationInterceptor ** /}複製程式碼
-
執行分頁查詢
@Test
public void testPage() {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getAge, 28);
// 設定分頁資訊, 查第3頁, 每頁2條資料
Page<User> page = new Page<>(3, 2);
// 執行分頁查詢
Page<User> userPage = userMapper.selectPage(page, wrapper);
System.out.println("總記錄數 = " + userPage.getTotal());
System.out.println("總頁數 = " + userPage.getPages());
System.out.println("當前頁碼 = " + userPage.getCurrent());
// 獲取分頁查詢結果
List<User> records = userPage.getRecords();
records.forEach(System.out::println);
} -
結果
-
其他
-
注意到,分頁查詢總共發出了2次SQL,一次查總記錄數,一次查具體資料。 若希望不查總記錄數,僅查分頁結果 。可以通過
Page
的過載建構函式,指定isSearchCount
為false
即可public Page(long current, long size, boolean isSearchCount)
-
在實際開發中,可能遇到 多表聯查 的場景,此時
BaseMapper
中提供的單表分頁查詢的方法無法滿足需求,需要 自定義SQL ,示例如下(使用單表查詢的SQL進行演示,實際進行多表聯查時,修改SQL語句即可)
-
在mapper介面中定義一個函式,接收一個Page物件為引數,並編寫自定義SQL
// 這裡採用純註解方式。當然,若SQL比較複雜,建議還是採用XML的方式
@Select("SELECT * FROM user ${ew.customSqlSegment}")
Page<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper); -
執行查詢
@Test public void testPage2() { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.ge(User::getAge, 28).likeRight(User::getName, "王"); Page<User> page = new Page<>(3,2); Page<User> userPage = userMapper.selectUserPage(page, wrapper); System.out.println("總記錄數 = " + userPage.getTotal()); System.out.println("總頁數 = " + userPage.getPages()); userPage.getRecords().forEach(System.out::println); }複製程式碼
-
結果
AR模式
ActiveRecord模式,通過操作實體物件,直接操作資料庫表。與ORM有點類似。
示例如下
-
讓實體類
User
繼承自Model
package com.example.mp.po;
import com.baomidou.mybatisplus.annotation.SqlCondition;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = false)
@Data
public class User extends Model<User> {
private Long id;
@TableField(condition = SqlCondition.LIKE)
private String name;
@TableField(condition = "%s > #{%s}")
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
} -
直接呼叫實體物件上的方法
@Test
public void insertAr() {
User user = new User();
user.setId(15L);
user.setName("我是AR豬");
user.setAge(1);
user.setEmail("[email protected]");
user.setManagerId(1L);
boolean success = user.insert(); // 插入
System.out.println(success);
} -
結果
其他示例
// 查詢
@Test
public void selectAr() {
User user = new User();
user.setId(15L);
User result = user.selectById();
System.out.println(result);
}
// 更新
@Test
public void updateAr() {
User user = new User();
user.setId(15L);
user.setName("王全蛋");
user.updateById();
}
//刪除
@Test
public void deleteAr() {
User user = new User();
user.setId(15L);
user.deleteById();
}
主鍵策略
在定義實體類時,用 @TableId
指定主鍵,而其 type
屬性,可以指定主鍵策略。
mp支援多種主鍵策略,預設的策略是基於雪花演算法的自增id。全部主鍵策略定義在了列舉類 IdType
中, IdType
有如下的取值
-
AUTO
資料庫ID自增, 依賴於資料庫 。在插入操作生成SQL語句時,不會插入主鍵這一列
-
NONE
未設定主鍵型別。若在程式碼中沒有手動設定主鍵,則會根據 主鍵的全域性策略 自動生成(預設的主鍵全域性策略是基於雪花演算法的自增ID)
-
INPUT
需要手動設定主鍵,若不設定。插入操作生成SQL語句時,主鍵這一列的值會是
null
。oracle的序列主鍵需要使用這種方式 -
ASSIGN_ID
當沒有手動設定主鍵,即實體類中的主鍵屬性為空時,才會自動填充,使用雪花演算法
-
ASSIGN_UUID
當實體類的主鍵屬性為空時,才會自動填充,使用UUID
-
....(還有幾種是已過時的,就不再列舉)
可以針對每個實體類,使用 @TableId
註解指定該實體類的主鍵策略,這可以理解為 區域性策略
。若希望對所有的實體類,都採用同一種主鍵策略,挨個在每個實體類上進行配置,則太麻煩了,此時可以用主鍵的 全域性策略
。只需要在 application.yml
進行配置即可。比如,配置了全域性採用自增主鍵策略
> 推薦下自己做的 Spring Cloud 的實戰專案:
>
> <https://github.com/YunaiV/onemall>
# application.yml
mybatis-plus:
global-config:
db-config:
id-type: auto
下面對不同主鍵策略的行為進行演示
-
AUTO
在
User
上對id
屬性加上註解,然後將MYSQL的user
表修改其主鍵為自增。@EqualsAndHashCode(callSuper = false)
@Data
public class User extends Model<User> {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(condition = SqlCondition.LIKE)
private String name;
@TableField(condition = "%s > #{%s}")
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
}測試
@Test
public void testAuto() {
User user = new User();
user.setName("我是青蛙呱呱");
user.setAge(99);
user.setEmail("[email protected]");
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
System.out.println(user.getId());
}結果
可以看到,程式碼中沒有設定主鍵ID,發出的SQL語句中也沒有設定主鍵ID,並且插入結束後,主鍵ID會被寫回到實體物件。
-
NONE
在MYSQL的
user
表中,去掉主鍵自增。然後修改User
類(若不配置@TableId
註解,預設主鍵策略也是NONE
)@TableId(type = IdType.NONE)
private Long id;插入時,若實體類的主鍵ID有值,則使用之;若主鍵ID為空,則使用主鍵全域性策略,來生成一個ID。
-
其餘的策略類似,不贅述
小結
AUTO
依賴於資料庫的自增主鍵,插入時,實體物件無需設定主鍵,插入成功後,主鍵會被寫回實體物件。
INPUT`完全依賴於使用者輸入。實體物件中主鍵ID是什麼,插入到資料庫時就設定什麼。若有值便設定值,若為`null`則設定`null
其餘的幾個策略,都是在實體物件中主鍵ID為空時,才會自動生成。
NONE
會跟隨全域性策略, ASSIGN_ID
採用雪花演算法, ASSIGN_UUID
採用UUID
全域性配置,在 application.yml
中進行即可;針對單個實體類的區域性配置,使用 @TableId
即可。對於某個實體類,若它有區域性主鍵策略,則採用之,否則,跟隨全域性策略。
配置
mybatis plus有許多可配置項,可在 application.yml
中進行配置,如上面的全域性主鍵策略。下面列舉部分配置項
基本配置
-
configLocation
:若有單獨的mybatis配置,用這個註解指定mybatis的配置檔案(mybatis的全域性配置檔案) -
mapperLocations
:mybatis mapper所對應的xml檔案的位置 -
typeAliasesPackage
:mybatis的別名包掃描路徑 -
.....
進階配置
-
mapUnderscoreToCamelCase
:是否開啟自動駝峰命名規則對映。(預設開啟) -
dbTpe
:資料庫型別。一般不用配,會根據資料庫連線url自動識別 -
fieldStrategy
:(已過時)欄位驗證策略。 該配置項在最新版的mp文件中已經找不到了 ,被細分成了insertStrategy
,updateStrategy
,selectStrategy
。預設值是NOT_NULL
,即對於實體物件中非空的欄位,才會組裝到最終的SQL語句中。有如下幾種可選配置
這個配置項,可在
application.yml
中進行 全域性配置 ,也可以在某一實體類中,對某一欄位用@TableField
註解進行 區域性配置這個欄位驗證策略有什麼用呢?在UPDATE操作中能夠體現出來,若用一個
User
物件執行UPDATE操作,我們希望只對User
物件中非空的屬性,更新到資料庫中,其他屬性不做更新,則NOT_NULL
可以滿足需求。而若updateStrategy
配置為IGNORED
,則不會進行非空判斷,會將實體物件中的全部屬性如實組裝到SQL中,這樣,執行UPDATE時,可能就將一些不想更新的欄位,設定為了NULL
。 -
IGNORED NULL NULL
-
NOT_NULL NULL NULL
-
NOT_EMPTY
:非空校驗。當有欄位是字串型別時,只組裝非空字串;對其他型別的欄位,等同於NOT_NULL
-
NEVER
:不加入SQL。所有欄位不加入到SQL語句 -
tablePrefix
:新增表名字首比如
mybatis-plus
global-config:
db-config:
table-prefix: xx_然後將MYSQL中的表做一下修改。但Java實體類保持不變(仍然為
User
)。測試
@Test
public void test3() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "黃");
Integer count = userMapper.selectCount(wrapper);
System.out.println(count);
}可以看到拼接出來的SQL,在表名前面添加了字首
完整的配置可以參考mp的官網 ==> https://baomidou.com/config/#mapperlocations
程式碼生成器
mp提供一個生成器,可快速生成Entity實體類,Mapper介面,Service,Controller等全套程式碼。
示例如下
public class GeneratorTest {
@Test
public void generate() {
AutoGenerator generator = new AutoGenerator();
// 全域性配置
GlobalConfig config = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// 設定輸出到的目錄
config.setOutputDir(projectPath + "/src/main/java");
config.setAuthor("yogurt");
// 生成結束後是否開啟資料夾
config.setOpen(false);
// 全域性配置新增到 generator 上
generator.setGlobalConfig(config);
// 資料來源配置
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai");
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("root");
// 資料來源配置新增到 generator
generator.setDataSource(dataSourceConfig);
// 包配置, 生成的程式碼放在哪個包下
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.example.mp.generator");
// 包配置新增到 generator
generator.setPackageInfo(packageConfig);
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
// 下劃線駝峰命名轉換
strategyConfig.setNaming(NamingStrategy.underline_to_camel);
strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
// 開啟lombok
strategyConfig.setEntityLombokModel(true);
// 開啟RestController
strategyConfig.setRestControllerStyle(true);
generator.setStrategy(strategyConfig);
generator.setTemplateEngine(new FreemarkerTemplateEngine());
// 開始生成
generator.execute();
}
}
執行後,可以看到生成了如下圖所示的全套程式碼
高階功能
高階功能的演示需要用到一張新的表 user2
DROP TABLE IF EXISTS user2;
CREATE TABLE user2 (
id BIGINT(20) PRIMARY KEY NOT NULL COMMENT '主鍵id',
name VARCHAR(30) DEFAULT NULL COMMENT '姓名',
age INT(11) DEFAULT NULL COMMENT '年齡',
email VARCHAR(50) DEFAULT NULL COMMENT '郵箱',
manager_id BIGINT(20) DEFAULT NULL COMMENT '直屬上級id',
create_time DATETIME DEFAULT NULL COMMENT '建立時間',
update_time DATETIME DEFAULT NULL COMMENT '修改時間',
version INT(11) DEFAULT '1' COMMENT '版本',
deleted INT(1) DEFAULT '0' COMMENT '邏輯刪除標識,0-未刪除,1-已刪除',
CONSTRAINT manager_fk FOREIGN KEY(manager_id) REFERENCES user2(id)
) ENGINE = INNODB CHARSET=UTF8;
INSERT INTO user2(id, name, age, email, manager_id, create_time)
VALUES
(1, '老闆', 40 ,'[email protected]' ,NULL, '2021-03-28 13:12:40'),
(2, '王狗蛋', 40 ,'[email protected]' ,1, '2021-03-28 13:12:40'),
(3, '王雞蛋', 40 ,'[email protected]' ,2, '2021-03-28 13:12:40'),
(4, '王鴨蛋', 40 ,'[email protected]' ,2, '2021-03-28 13:12:40'),
(5, '王豬蛋', 40 ,'[email protected]' ,2, '2021-03-28 13:12:40'),
(6, '王軟蛋', 40 ,'[email protected]' ,2, '2021-03-28 13:12:40'),
(7, '王鐵蛋', 40 ,'[email protected]' ,2, '2021-03-28 13:12:40')
並建立對應的實體類 User2
package com.example.mp.po;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class User2 {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer version;
private Integer deleted;
}
以及Mapper介面
package com.example.mp.mappers;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.mp.po.User2;
public interface User2Mapper extends BaseMapper<User2> { }
邏輯刪除
首先,為什麼要有邏輯刪除呢?直接刪掉不行嗎?當然可以,但日後若想要恢復,或者需要檢視這些資料,就做不到了。 邏輯刪除是為了方便資料恢復,和保護資料本身價值的一種方案 。
日常中,我們在電腦中刪除一個檔案後,也僅僅是把該檔案放入了回收站,日後若有需要還能進行檢視或恢復。當我們確定不再需要某個檔案,可以將其從回收站中徹底刪除。這也是類似的道理。
mp提供的邏輯刪除實現起來非常簡單
只需要在 application.yml
中進行邏輯刪除的相關配置即可
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全域性邏輯刪除的實體欄位名
logic-delete-value: 1 # 邏輯已刪除值(預設為1)
logic-not-delete-value: 0 # 邏輯未刪除值(預設為0)
# 若邏輯已刪除和未刪除的值和預設值一樣,則可以不配置這2項
測試程式碼
package com.example.mp;
import com.example.mp.mappers.User2Mapper;
import com.example.mp.po.User2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogicDeleteTest {
@Autowired
private User2Mapper mapper;
@Test
public void testLogicDel() {
int i = mapper.deleteById(6);
System.out.println("rowAffected = " + i);
}
}
結果
可以看到,發出的SQL不再是 DELETE
,而是 UPDATE
此時我們再執行一次 SELECT
@Test
public void testSelect() {
List<User2> users = mapper.selectList(null);
}
可以看到,發出的SQL語句,會自動在WHERE後面拼接邏輯未刪除的條件。查詢出來的結果中,沒有了id為6的王軟蛋。
若想要SELECT的列,不包括邏輯刪除的那一列,則可以在實體類中通過 @TableField
進行配置
@TableField(select = false)
private Integer deleted;
可以看到下圖的執行結果中,SELECT中已經不包含deleted這一列了
前面在 application.yml
中做的配置,是全域性的。通常來說,對於多個表,我們也會統一邏輯刪除欄位的名稱,統一邏輯已刪除和未刪除的值,所以全域性配置即可。當然,若要對某些表進行單獨配置,在實體類的對應欄位上使用 @TableLogic
即可
@TableLogic(value = "0", delval = "1")
private Integer deleted;
小結
開啟mp的邏輯刪除後,會對SQL產生如下的影響
-
INSERT語句:沒有影響
-
SELECT語句:追加WHERE條件,過濾掉已刪除的資料
-
UPDATE語句:追加WHERE條件,防止更新到已刪除的資料
-
DELETE語句:轉變為UPDATE語句
注意,上述的影響,只針對mp自動注入的SQL生效。如果是自己手動新增的自定義SQL,則不會生效。比如
public interface User2Mapper extends BaseMapper<User2> {
@Select("select * from user2")
List<User2> selectRaw();
}
呼叫這個 selectRaw
,則mp的邏輯刪除不會生效。
另,邏輯刪除可在 application.yml
中進行全域性配置,也可在實體類中用 @TableLogic
進行區域性配置。
自動填充
表中常常會有“新增時間”,“修改時間”,“操作人” 等欄位。比較原始的方式,是每次插入或更新時,手動進行設定。mp可以通過配置,對某些欄位進行自動填充,食用示例如下
-
在實體類中的某些欄位上,通過
@TableField
設定自動填充public class User2 {
private Long id;
private String name;
private Integer age;
private String email;
private Long managerId;
@TableField(fill = FieldFill.INSERT) // 插入時自動填充
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE) // 更新時自動填充
private LocalDateTime updateTime;
private Integer version;
private Integer deleted;
} -
實現自動填充處理器
package com.example.mp.component;import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;import org.apache.ibatis.reflection.MetaObject;import org.springframework.stereotype.Component;import java.time.LocalDateTime;@Component //需要註冊到Spring容器中public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { // 插入時自動填充 // 注意第二個引數要填寫實體類中的欄位名稱,而不是表的列名稱 strictFillStrategy(metaObject, "createTime", LocalDateTime::now); } @Override public void updateFill(MetaObject metaObject) { // 更新時自動填充 strictFillStrategy(metaObject, "updateTime", LocalDateTime::now); }}複製程式碼
測試
@Test public void test() { User2 user = new User2(); user.setId(8L); user.setName("王一蛋"); user.setAge(29); user.setEmail("[email protected]"); user.setManagerId(2L); mapper.insert(user); }複製程式碼
根據下圖結果,可以看到對createTime進行了自動填充
注意,自動填充僅在該欄位為空時會生效,若該欄位不為空,則直接使用已有的值。如下
@Test
public void test() {
User2 user = new User2();
user.setId(8L);
user.setName("王一蛋");
user.setAge(29);
user.setEmail("[email protected]");
user.setManagerId(2L);
user.setCreateTime(LocalDateTime.of(2000,1,1,8,0,0));
mapper.insert(user);
}
更新時的自動填充,測試如下
@Test
public void test() {
User2 user = new User2();
user.setId(8L);
user.setName("王一蛋");
user.setAge(99);
mapper.updateById(user);
}
樂觀鎖外掛
當出現併發操作時,需要確保各個使用者對資料的操作不產生衝突,此時需要一種併發控制手段。悲觀鎖的方法是,在對資料庫的一條記錄進行修改時,先直接加鎖(資料庫的鎖機制),鎖定這條資料,然後再進行操作;而樂觀鎖,正如其名,它先假設不存在衝突情況,而在實際進行資料操作時,再檢查是否衝突。樂觀鎖的一種通常實現是 版本號 ,在MySQL中也有名為MVCC的基於版本號的併發事務控制。
在讀多寫少的場景下,樂觀鎖比較適用,能夠減少加鎖操作導致的效能開銷,提高系統吞吐量。
在寫多讀少的場景下,悲觀鎖比較使用,否則會因為樂觀鎖不斷失敗重試,反而導致效能下降。
樂觀鎖的實現如下:
-
取出記錄時,獲取當前version
-
更新時,帶上這個version
-
執行更新時, set version = newVersion where version = oldVersion
-
如果oldVersion與資料庫中的version不一致,就更新失敗
這種思想和CAS(Compare And Swap)非常相似。
樂觀鎖的實現步驟如下
-
配置樂觀鎖外掛
package com.example.mp.config;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/** 3.4.0以後的mp版本,推薦用如下的配置方式 ** /
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/** 舊版mp可以採用如下方式。注意新舊版本中,新版的類,名稱帶有Inner, 舊版的不帶, 不要配錯了 ** /
/*
@Bean
public OptimisticLockerInterceptor opLocker() {
return new OptimisticLockerInterceptor();
}
*/
} -
在實體類中表示版本的欄位上添加註解
@Version
@Datapublic class User2 { private Long id; private String name; private Integer age; private String email; private Long managerId; private LocalDateTime createTime; private LocalDateTime updateTime; @Version private Integer version; private Integer deleted;}複製程式碼
測試程式碼
@Test public void testOpLocker() { int version = 1; // 假設這個version是先前查詢時獲得的 User2 user = new User2(); user.setId(8L); user.setEmail("[email protected]"); user.setVersion(version); int i = mapper.updateById(user); }複製程式碼
執行之前先看一下資料庫的情況
根據下圖執行結果,可以看到SQL語句中添加了version相關的操作
當UPDATE返回了1,表示影響行數為1,則更新成功。反之,由於WHERE後面的version與資料庫中的不一致,匹配不到任何記錄,則影響行數為0,表示更新失敗。更新成功後,新的version會被封裝回實體物件中。
實體類中version欄位,型別只支援int,long,Date,Timestamp,LocalDateTime
注意,樂觀鎖外掛僅支援 updateById(id)
與 update(entity, wrapper)
方法
注意:如果使用 wrapper
,則 wrapper
不能複用! 示例如下
@Test public void testOpLocker() { User2 user = new User2(); user.setId(8L); user.setVersion(1); user.setAge(2); // 第一次使用 LambdaQueryWrapper<User2> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User2::getName, "王一蛋"); mapper.update(user, wrapper); // 第二次複用 user.setAge(3); mapper.update(user, wrapper); }複製程式碼
可以看到在第二次複用 wrapper
時,拼接出的SQL中,後面WHERE語句中出現了2次version,是有問題的。
效能分析外掛
該外掛會輸出SQL語句的執行時間,以便做SQL語句的效能分析和調優。
注:3.2.0版本之後,mp自帶的效能分析外掛被官方移除了,而推薦食用第三方效能分析外掛
食用步驟
-
引入maven依賴
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency> -
修改
application.yml
spring:
datasource:
driver-class-name: com.p6spy.engine.spy.P6SpyDriver #換成p6spy的驅動
url: jdbc:p6spy:mysql://localhost:3306/yogurt?serverTimezone=Asia/Shanghai #url修改
username: root
password: root -
在
src/main/resources
資源目錄下新增spy.properties
#spy.properties
#3.2.1以上使用
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
# 真實JDBC driver , 多個以逗號分割,預設為空。由於上面設定了modulelist, 這裡可以不用設定driverlist
#driverlist=com.mysql.cj.jdbc.Driver
# 自定義日誌列印
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
#日誌輸出到控制檯
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
#若要日誌輸出到檔案, 把上面的appnder註釋掉, 或者採用下面的appender, 再新增logfile配置
#不配置appender時, 預設是往檔案進行輸出的
#appender=com.p6spy.engine.spy.appender.FileLogger
#logfile=log.log
# 設定 p6spy driver 代理
deregisterdrivers=true
# 取消JDBC URL字首
useprefix=true
# 配置記錄 Log 例外,可去掉的結果集有error,info,batch,debug,statement,commit,rollback,result,resultset.
excludecategories=info,debug,result,commit,resultset
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 是否開啟慢SQL記錄
outagedetection=true
# 慢SQL記錄標準 2 秒
outagedetectioninterval=2
# 執行時間設定, 只有超過這個執行時間的才進行記錄, 預設值0, 單位毫秒
executionThreshold=10
隨便執行一個測試用例,可以看到該SQL的執行時長被記錄了下來
多租戶SQL解析器
多租戶的概念:多個使用者共用一套系統,但他們的資料有需要相對的獨立,保持一定的隔離性。
多租戶的資料隔離一般有如下的方式:
-
不同租戶使用不同的資料庫伺服器
優點是:不同租戶有不同的獨立資料庫,有助於擴充套件,以及對不同租戶提供更好的個性化,出現故障時恢復資料較為簡單。
缺點是:增加了資料庫數量,購置成本,維護成本更高
-
不同租戶使用相同的資料庫伺服器,但使用不同的資料庫(不同的schema)
優點是購置和維護成本低了一些,缺點是資料恢復較為困難,因為不同租戶的資料都放在了一起
-
不同租戶使用相同的資料庫伺服器,使用相同的資料庫,共享資料表,在表中增加租戶id來做區分
優點是,購置和維護成本最低,支援使用者最多,缺點是隔離性最低,安全性最低
食用例項如下
新增多租戶攔截器配置。新增配置後,在執行CRUD的時候,會自動在SQL語句最後拼接租戶id的條件
package com.example.mp.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 返回租戶id的值, 這裡固定寫死為1
// 一般是從當前上下文中取出一個 租戶id
return new LongValue(1);
}
/**
** 通常會將表示租戶id的列名,需要排除租戶id的表等資訊,封裝到一個配置類中(如TenantConfig)
**/
@Override
public String getTenantIdColumn() {
// 返回表中的表示租戶id的列名
return "manager_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 表名不為 user2 的表, 不拼接多租戶條件
return !"user2".equals(tableName);
}
}));
// 如果用了分頁外掛注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分頁外掛必須設定 MybatisConfiguration#useDeprecatedExecutor = false
return interceptor;
}
}
測試程式碼
@Test
public void testTenant() {
LambdaQueryWrapper<User2> wrapper = new LambdaQueryWrapper<>();
wrapper.likeRight(User2::getName, "王")
.select(User2::getName, User2::getAge, User2::getEmail, User2::getManagerId);
user2Mapper.selectList(wrapper);
}
動態表名SQL解析器
當資料量特別大的時候,我們通常會採用分庫分表。這時,可能就會有多張表,其表結構相同,但表名不同。例如 order_1
, order_2
, order_3
,查詢時,我們可能需要動態設定要查的表名。mp提供了動態表名SQL解析器,食用示例如下
先在mysql中拷貝一下 user2
表
配置動態表名攔截器
package com.example.mp.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Random;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
HashMap<String, TableNameHandler> map = new HashMap<>();
// 對於user2表,進行動態表名設定
map.put("user2", (sql, tableName) -> {
String _ = "_";
int random = new Random().nextInt(2) + 1;
return tableName + _ + random; // 若返回null, 則不會進行動態表名替換, 還是會使用user2
});
dynamicTableNameInnerInterceptor.setTableNameHandlerMap(map);
interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
return interceptor;
}
}
測試
@Test
public void testDynamicTable() {
user2Mapper.selectList(null);
}
總結
-
條件構造器
AbstractWrapper
中提供了多個方法用於構造SQL語句中的WHERE條件,而其子類QueryWrapper
額外提供了select
方法,可以只選取特定的列,子類UpdateWrapper
額外提供了set
方法,用於設定SQL中的SET語句。除了普通的Wrapper
,還有基於lambda表示式的Wrapper
,如LambdaQueryWrapper
,LambdaUpdateWrapper
,它們在構造WHERE條件時,直接以 方法引用 來指定WHERE條件中的列,比普通Wrapper
通過字串來指定要更加優雅。另,還有 鏈式Wrapper ,如LambdaQueryChainWrapper
,它封裝了BaseMapper
,可以更方便地獲取結果。 -
條件構造器採用 鏈式呼叫 來拼接多個條件,條件之間預設以
AND
連線 -
當
AND
或OR
後面的條件需要被括號包裹時,將括號中的條件以lambda表示式形式,作為引數傳入and()
或or()
特別的,當
()
需要放在WHERE語句的最開頭時,可以使用nested()
方法 -
條件表示式時當需要傳入自定義的SQL語句,或者需要呼叫資料庫函式時,可用
apply()
方法進行SQL拼接 -
條件構造器中的各個方法可以通過一個
boolean
型別的變數condition
,來根據需要靈活拼接WHERE條件(僅當condition
為true
時會拼接SQL語句) -
使用lambda條件構造器,可以通過lambda表示式,直接使用實體類中的屬性進行條件構造,比普通的條件構造器更加優雅
-
若mp提供的方法不夠用,可以通過 自定義SQL (原生mybatis)的形式進行擴充套件開發
-
使用mp進行分頁查詢時,需要建立一個分頁攔截器(Interceptor),註冊到Spring容器中,隨後查詢時,通過傳入一個分頁物件(Page物件)進行查詢即可。單表查詢時,可以使用
BaseMapper
提供的selectPage
或selectMapsPage
方法。複雜場景下(如多表聯查),使用自定義SQL。 -
AR模式可以直接通過操作實體類來操作資料庫。讓實體類繼承自
Model
即可
來源:blog.csdn.net/vcj1009784814/article/details/115159687
關於我:Tom哥,前阿里P7技術專家,offer收割機,參加多次淘寶雙11大促活動。歡迎關注,我會持續輸出更多經典原創文章,為你晉級大廠助力
目前微信群已開放, 想進交流群 的小夥伴請新增Tom哥微信,暗號「 進群 」,嘮嗑聊天, 技術交流,圍觀朋友圈,人生打怪不再寂寞
花了兩週時間,我將 《我想去大廠》 (包含JAVA、MySQL、Redis、MQ佇列、網路、專案亮點等)整理成冊,PDF分享。回覆關鍵字 大廠 ,即可獲得百度盤地址,無套路領取!
加個關注不迷路
喜歡就點個"在看"唄^_^
- 位元組一面:Redis主節點宕機,如何處理?
- 5個基礎內功,程式設計師要從這入手!
- 一文搞懂 Sentinel 限流熔斷!
- 阿里一面:聊聊Java中執行緒的生命週期狀態
- Elasticsearch 億級資料效能優化!
- 你真的會面試嗎?
- 硬核的網路學習資料!再也不用擔心被面試官摩擦
- 秒殺系統架構!
- 開箱即用,40 個 SpringBoot 常用註解!
- 繁忙的工作之外,大佬都在幹這件事....
- 9大外掛,高效寫程式碼,老闆要給加薪
- 阿里一面: Spring 有哪些擴充套件點?
- SpringBoot 整合 Elasticsearch 實現海量級資料搜尋
- 一文詳解 JDK1.8 的 Lambda、Stream、LocalDateTime
- 新來的技術總監,指導大家落地DDD,那是一個服氣!
- 併發程式設計的三大核心問題
- 聽說你精通 MyBatis!我們切磋下
- 滿屏的 if-else,要怎麼優化?
- Api 介面優化的幾個技巧
- MyBatis-plus 創業公司首選利器!