mybatis-plus是一款Mybatis加強工具,用於簡化開發,提升效率。下文使用縮寫mp來簡化表示mybatis-plus,本文主要介紹mp搭配SpringBoot的使用。java
注:本文使用的mp版本是當前最新的3.4.2,早期版本的差別請自行查閱文檔mysql
官方網站:baomidou.com/算法
<!-- 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; }
package com.example.mp.mappers; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mp.po.User; public interface UserMapper extends BaseMapper<User> { }
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, 'boss@baomidou.com', NULL, '2021-03-22 09:48:00'), (2, '李經理', 40, 'boss@baomidou.com', 1, '2021-01-22 09:48:00'), (3, '黃主管', 40, 'boss@baomidou.com', 2, '2021-01-22 09:48:00'), (4, '吳組長', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00'), (5, '小菜', 40, 'boss@baomidou.com', 2, '2021-02-22 09:48:00')
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); } }
準備工做完成spring
數據庫狀況以下sql
項目目錄以下數據庫
運行測試類express
能夠看到,針對單表的基本CRUD操做,只須要建立好實體類,並建立一個繼承自BaseMapper
的接口便可,可謂很是簡潔。而且,咱們注意到,User
類中的managerId
,createTime
屬性,自動和數據庫表中的manager_id
,create_time
對應了起來,這是由於mp自動作了數據庫下劃線命名,到Java類的駝峯命名之間的轉化。apache
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
插件過濾規則
mp封裝了一些最基礎的CRUD方法,只須要直接繼承mp提供的接口,無需編寫任何SQL,便可食用。mp提供了兩套接口,分別是Mapper CRUD接口和Service CRUD接口。而且mp還提供了條件構造器Wrapper
,能夠方便地組裝SQL語句中的WHERE條件。
只需定義好實體類,而後建立一個接口,繼承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)
下面講解幾個比較特別的方法
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); }
只會返回第一個字段(第一列)的值,其餘字段會被捨棄
好比
@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
查詢知足條件的總數,注意,使用這個方法,不能調用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); } 複製代碼
另一套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, "w39@baomidou.com") .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中的BETWEENnotBetween
like
:模糊匹配。like("name","黃")
,至關於SQL的name like '%黃%'
likeRight
:模糊匹配右半邊。likeRight("name","黃")
,至關於SQL的name like '黃%'
likeLeft
:模糊匹配左半邊。likeLeft("name","黃")
,至關於SQL的name like '%黃'
notLike
:notLike("name","黃")
,至關於SQL的name not like '%黃%'
isNull
isNotNull
in
and
:SQL鏈接符ANDor
:SQL鏈接符ORapply
:用於拼接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); });
條件構造器的諸多方法中,都可以指定一個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方法傳入一個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表達式,能夠沒必要像普通條件構造器同樣,以字符串形式指定列名,它能夠直接以實體類的方法引用來指定列。示例以下
@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("share@baomidou.com"); 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, "ff@baomidou.com") .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
進行刪除與前面查詢和更新的操做大同小異,不作贅述
當mp提供的方法還不能知足需求時,則能夠自定義SQL。
示例以下
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 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); }
結果
也可使用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); }
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>
。分頁查詢的食用示例以下
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; @Configuration public 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); }
結果
其餘
Page
的重載構造函數,指定isSearchCount
爲false
便可public Page(long current, long size, boolean isSearchCount)
在實際開發中,可能遇到多表聯查的場景,此時BaseMapper
中提供的單表分頁查詢的方法沒法知足需求,須要自定義SQL,示例以下(使用單表查詢的SQL進行演示,實際進行多表聯查時,修改SQL語句便可)
// 這裏採用純註解方式。固然,若SQL比較複雜,建議仍是採用XML的方式 @Select("SELECT * FROM user ${ew.customSqlSegment}") Page<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
2. 執行查詢
@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); }
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("ar@baomidou.com"); 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
進行配置便可。好比,配置了全局採用自增主鍵策略
# 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("frog@baomidou.com"); 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語句中。
有以下幾種可選配置
IGNORED
:忽略校驗。即,不作校驗。實體對象中的所有字段,不管值是什麼,都如實地被組裝到SQL語句中(爲NULL
的字段在SQL語句中就組裝爲NULL
)。NOT_NULL
:非NULL
校驗。只會將非NULL
的字段組裝到SQL語句中NOT_EMPTY
:非空校驗。當有字段是字符串類型時,只組裝非空字符串;對其餘類型的字段,等同於NOT_NULL
NEVER
:不加入SQL。全部字段不加入到SQL語句這個配置項,可在application.yml
中進行全局配置,也能夠在某一實體類中,對某一字段用@TableField
註解進行局部配置
這個字段驗證策略有什麼用呢?在UPDATE操做中可以體現出來,若用一個User
對象執行UPDATE操做,咱們但願只對User
對象中非空的屬性,更新到數據庫中,其餘屬性不作更新,則NOT_NULL
能夠知足需求。而若updateStrategy
配置爲IGNORED
,則不會進行非空判斷,會將實體對象中的所有屬性如實組裝到SQL中,這樣,執行UPDATE時,可能就將一些不想更新的字段,設置爲了NULL
。
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提供一個生成器,可快速生成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 ,'boss@baomidou.com' ,NULL, '2021-03-28 13:12:40'), (2, '王狗蛋', 40 ,'gd@baomidou.com' ,1, '2021-03-28 13:12:40'), (3, '王雞蛋', 40 ,'jd@baomidou.com' ,2, '2021-03-28 13:12:40'), (4, '王鴨蛋', 40 ,'yd@baomidou.com' ,2, '2021-03-28 13:12:40'), (5, '王豬蛋', 40 ,'zd@baomidou.com' ,2, '2021-03-28 13:12:40'), (6, '王軟蛋', 40 ,'rd@baomidou.com' ,2, '2021-03-28 13:12:40'), (7, '王鐵蛋', 40 ,'td@baomidou.com' ,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產生以下的影響
注意,上述的影響,只針對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("yd@baomidou.com"); 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("yd@baomidou.com"); 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的基於版本號的併發事務控制。
在讀多寫少的場景下,樂觀鎖比較適用,可以減小加鎖操做致使的性能開銷,提升系統吞吐量。
在寫多讀少的場景下,悲觀鎖比較使用,不然會由於樂觀鎖不斷失敗重試,反而致使性能降低。
樂觀鎖的實現以下:
這種思想和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
@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; @Version private Integer version; private Integer deleted; }
測試代碼
@Test public void testOpLocker() { int version = 1; // 假設這個version是先前查詢時得到的 User2 user = new User2(); user.setId(8L); user.setEmail("version@baomidou.com"); 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自帶的性能分析插件被官方移除了,而推薦食用第三方性能分析插件
食用步驟
<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的執行時長被記錄了下來
多租戶的概念:多個用戶共用一套系統,但他們的數據有須要相對的獨立,保持必定的隔離性。
多租戶的數據隔離通常有以下的方式:
不一樣租戶使用不一樣的數據庫服務器
優勢是:不一樣租戶有不一樣的獨立數據庫,有助於擴展,以及對不一樣租戶提供更好的個性化,出現故障時恢復數據較爲簡單。
缺點是:增長了數據庫數量,購置成本,維護成本更高
不一樣租戶使用相同的數據庫服務器,但使用不一樣的數據庫(不一樣的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); }
當數據量特別大的時候,咱們一般會採用分庫分表。這時,可能就會有多張表,其表結構相同,但表名不一樣。例如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()
方法
apply()
方法進行SQL拼接boolean
類型的變量condition
,來根據須要靈活拼接WHERE條件(僅當condition
爲true
時會拼接SQL語句)BaseMapper
提供的selectPage
或selectMapsPage
方法。複雜場景下(如多表聯查),使用自定義SQL。Model
便可做者:yogurtzzz
連接: https://juejin.cn/post/696172...