1. 簡介
官網(wǎng):https://baomidou.com/pages/24112f/
簡稱為MP,是一個Mybatis的增強工具,在Mybatis的基礎上只做增強不做改變,為簡化開發(fā),提高效率而生。
對比mybatis,MP減少了sql的書寫,之前的mybatis是需要將sql語句寫在xml文件中,涉及的操作比較繁瑣,而MP基本不用書寫簡單SQL,開發(fā)起來比較方便,而且MP擁有的特性也很受用。
2. 特性
-
無侵入
只做增強不做改變,引入它不會對現(xiàn)有工程產(chǎn)生影響,如絲般順滑
-
損耗小
啟動即會自動注入基本CRUD,性能基本無損耗,直接面向?qū)ο蟛僮?/p>
-
強大的CRUD操作
內(nèi)置通用Mapper,通用Service,僅僅通過少量配置即可實現(xiàn)單表大部分CRUD操作,更有強大的條件構造器,滿足各類使用需求
-
支持Lambda形式調(diào)用
通過Lambda表達式,方便的編寫各類查詢條件,無需擔心字段寫錯
-
支持主鍵自動生成
支持多達4種主鍵策略(內(nèi)含分布式唯一ID生成器- Sequence),可自由配置,完美解決主鍵問題
-
支持ActiveRecord模式
支持ActiveRecord形式調(diào)用,實體類只需繼承Model類即可進行強大的CRUD操作
-
支持自定義全局通用操作
支持全局通用方法注入
-
內(nèi)置代碼生成器
采用代碼或者Maven插件可快速生成Mapper,Model,Service,Controller層代碼,支持模板引擎,更有超多自定義配置
-
內(nèi)置分頁插件
基于Mybatis物理分頁,開發(fā)者無需關心具體操作,配置好插件后,寫分頁等同于普通List查詢
-
分頁插件支持多種數(shù)據(jù)庫
支持MySQL,Oracle,DB2,H2,HSQL,SQLite,Postgre,SQLServer等多種數(shù)據(jù)庫
-
內(nèi)置性能分析插件
可輸出SQL語句以及其執(zhí)行時間,建議開發(fā)測試時啟用該功能,能快速揪出慢查詢
-
內(nèi)置全局攔截插件
提供全表delete,update操作只能分析阻斷,也可以自定義攔截規(guī)則,預防誤操作
3. 快速開始
-
首先創(chuàng)建數(shù)據(jù)表,假設有一張用戶表user
CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主鍵ID', `name` varchar(32) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年齡', `email` varchar(32) DEFAULT NULL COMMENT '郵箱', PRIMARY KEY(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;這里id字段的類型為bigint,是因為mybatis默認的主鍵是雪花算法計算出來的,比較長超出范圍,需要用bigint
-
插入一些預置的數(shù)據(jù)
INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com'); -
創(chuàng)建Springboot工程,引入依賴
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> -
編寫配置(數(shù)據(jù)庫使用MySQL,數(shù)據(jù)源使用Druid)
spring: # 配置數(shù)據(jù)源信息 datasource: # 配置數(shù)據(jù)源類型 type: com.alibaba.druid.pool.DruidDataSource # 配置連接數(shù)據(jù)庫信息 driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf-8&useSSL=false username: root password: root這里有個要注意的:
-
驅(qū)動類(
driver-class-name)的值- Springboot 2.0或使用的數(shù)據(jù)庫是5.X版本,
driver-class-name的值為:com.mysql.jdbc.Driver - Springboot 2.1及以上或數(shù)據(jù)庫是8.x版本,
driver-class-name的值為:com.mysql.cj.jdbc.Driver
- Springboot 2.0或使用的數(shù)據(jù)庫是5.X版本,
-
連接地址(
url)的值-
mysql5.x版本的url
jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf8&useSSL=false -
mysql8.x版本的url
jdbc:mysql://localhost:3306/mybatisplus?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false
mysql8多了一個時區(qū)的設置
-
-
-
添加實體類User
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private Integer age; private String email; }這里的字段類型,盡量用包裝類。
在插入數(shù)據(jù)時,由于ID是自動生成的,在設置數(shù)據(jù)時,如果用了基本數(shù)據(jù)類型long,就得設置為0,就會變成了插入數(shù)據(jù)的ID了,而包裝類,可以設置為null。(個人理解)
-
添加對應UserMapper
public interface UserMapper extends BaseMapper<User> { }這里UserMapper繼承了BaseMapper,這個BaseMapper是MybatisPlus提供的模板Mapper,其中包含了基本的CRUD方法,泛型就是要操作的實體類型。
Mapper 繼承該接口后,無需編寫 mapper.xml 文件,即可獲得CRUD功能
-
啟動類加上
MapperScan@SpringBootApplication @MapperScan("com.zzy.mybatisplus_test.mapper") public class MybatisplusTestApplication { public static void main(String[] args) { SpringApplication.run(MybatisplusTestApplication.class, args); } } -
編寫測試方法
@Autowired private UserMapper userMapper; @Test public void testSelectList() { userMapper.selectList(null).forEach(System.out::println); }UserMapper中的selectList方法需要一個參數(shù),是MP內(nèi)置的條件封裝器,這里填寫null,就是指不需要條件,選擇所有
-
測試結果
控制臺會輸出前面插入的數(shù)據(jù)
User(id=1, name=Jone, age=18, email=test1@baomidou.com) User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=Tom, age=28, email=test3@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com) User(id=5, name=Billie, age=24, email=test5@baomidou.com)
4. MP的CRUD操作
在操作之前,我們可以配置MP的日志輸出,能在控制臺顯示MP執(zhí)行的SQL語句,方便Debug與學習。
# 配置Mybatis日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.1 使用BaseMapper
在MP中,基本的CRUD在內(nèi)置的BaseMapper中都已經(jīng)得到了實現(xiàn)了,我們可以直接使用。
-
Insert
/** * 插入一條記錄 * * @param entity 實體對象 */ int insert(T entity);@Test public void testInsert() { User user = new User(null, "zhangsan", 23, "zhangsan@qq.com"); int result = userMapper.insert(user); System.out.println("result: " + result); System.out.println("用戶id為:" + user.getId()); }輸出數(shù)據(jù):
==> Preparing: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
==> Parameters: 1546431457684246529(Long), zhangsan(String), 23(Integer), zhangsan@qq.com(String)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2fd9fb34]
result: 1
用戶id為:1546431457684246529用戶id是MP默認使用雪花算法生成的。
-
Delete
// 根據(jù) entity 條件,刪除記錄 // wrapper: 實體對象封裝操作類,可以為null int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper); // 刪除(根據(jù)ID 批量刪除) // idList:主鍵ID列表,不能為null以及empty int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList); // 根據(jù) ID 刪除 int deleteById(Serializable id); // 根據(jù) columnMap 條件,刪除記錄 // columnMap: 表字段map對象 int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);// 通過Id刪除記錄 @Test public void testDeleteById() { int result = userMapper.deleteById(1546431457684246529L); System.out.println("result = " + result); } // 輸出結果 ==> Preparing: DELETE FROM user WHERE id=? ==> Parameters: 1546431457684246529(Long) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@687389a6] result = 1// 根據(jù)ID批量刪除 @Test public void testDeleteByBatchId() { List<Long> list = Arrays.asList(1L, 2L, 3L); int result = userMapper.deleteBatchIds(list); System.out.println("result = " + result); } // 輸出結果 ==> Preparing: DELETE FROM user WHERE id IN ( ? , ? , ? ) ==> Parameters: 1(Long), 2(Long), 3(Long) <== Updates: 3 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4ffced4e] result = 3// 通過map條件刪除 @Test public void testDeleteByMap() { Map<String, Object> map = new HashMap<>(); map.put("age", 24); map.put("name", "Billie"); int result = userMapper.deleteByMap(map); System.out.println("result = " + result); } // 輸出結果 ==> Preparing: DELETE FROM user WHERE name = ? AND age = ? ==> Parameters: Billie(String), 24(Integer) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6094de13] result = 1 -
Update
// 根據(jù) whereWrapper 條件,更新記錄 int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper<T> whereWrapper); // 根據(jù) ID 修改 int updateById(@Param(Constants.ENTITY) T entity);前面演示了刪除的方法,把數(shù)據(jù)幾乎都刪光了,這里重新將原始數(shù)據(jù)添加到數(shù)據(jù)庫,再測試。
// 通過Id修改 @Test public void testUpdateById() { User user = new User(3L, "zhangsan", 20, null); int result = userMapper.updateById(user); System.out.println("result = " + result); } // 輸出結果 ==> Preparing: UPDATE user SET name=?, age=? WHERE id=? ==> Parameters: zhangsan(String), 20(Integer), 3(Long) <== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@bbb6f0] result = 1 // 注意,email設置為null,在執(zhí)行時并沒有去修改email的值的。 -
Select
// 根據(jù) ID 查詢 T selectById(Serializable id); // 根據(jù) entity 條件,查詢一條記錄 T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查詢(根據(jù)ID 批量查詢) List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList); // 根據(jù) entity 條件,查詢?nèi)坑涗?List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 查詢(根據(jù) columnMap 條件) List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap); // 根據(jù) Wrapper 條件,查詢?nèi)坑涗?List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根據(jù) Wrapper 條件,查詢?nèi)坑涗?。注意?只返回第一個字段的值 List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根據(jù) entity 條件,查詢?nèi)坑涗洠ú⒎摚?IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根據(jù) Wrapper 條件,查詢?nèi)坑涗洠ú⒎摚?IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper); // 根據(jù) Wrapper 條件,查詢總記錄數(shù) Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);// 根據(jù)Id查詢 @Test public void testSelectById() { User user = userMapper.selectById(2L); System.out.println("user = " + user); } // 輸出結果 ==> Preparing: SELECT id,name,age,email FROM user WHERE id=? ==> Parameters: 2(Long) <== Columns: id, name, age, email <== Row: 2, Jack, 20, test2@baomidou.com <== Total: 1 user = User(id=2, name=Jack, age=20, email=test2@baomidou.com)// 根據(jù)多個Id批量查詢 @Test public void testSelectByBatchId() { List<Long> list = Arrays.asList(4L, 5L); List<User> users = userMapper.selectBatchIds(list); System.out.println("users = " + users); } // 輸出結果 ==> Preparing: SELECT id,name,age,email FROM user WHERE id IN ( ? , ? ) ==> Parameters: 4(Long), 5(Long) <== Columns: id, name, age, email <== Row: 4, Sandy, 21, test4@baomidou.com <== Row: 5, Billie, 24, test5@baomidou.com <== Total: 2 users = [User(id=4, name=Sandy, age=21, email=test4@baomidou.com), User(id=5, name=Billie, age=24, email=test5@baomidou.com)]// 通過map條件查詢 @Test public void testSelectByMap() { Map<String, Object> map = new HashMap<>(); map.put("age", 21); List<User> users = userMapper.selectByMap(map); System.out.println(users); } // 輸出結果 ==> Preparing: SELECT id,name,age,email FROM user WHERE age = ? ==> Parameters: 21(Integer) <== Columns: id, name, age, email <== Row: 4, Sandy, 21, test4@baomidou.com <== Total: 1 [User(id=4, name=Sandy, age=21, email=test4@baomidou.com)]
4.2 使用通用Service
說明:
- 通用Service CRUD封裝了
IService接口,進一步封裝CRUD采用get 查詢單行,remove 刪除,list 查詢集合,page 分頁前綴命名的方式區(qū)分Mapper層,避免混淆 - 泛型T為任意實體對象
- 建議如果存在自定義通用Service方法的可能,請創(chuàng)建自己的
IBaseService繼承MP提供的基類
-
IService
MP中有一個接口
IService和其實現(xiàn)類ServiceImpl,封裝了常見的業(yè)務層邏輯(具體翻看源碼)Save
// 插入一條記錄(選擇字段,策略插入) boolean save(T entity); // 插入(批量) boolean saveBatch(Collection<T> entityList); // 插入(批量) boolean saveBatch(Collection<T> entityList, int batchSize);SaveOrUpdate
// TableId 注解存在更新記錄,否插入一條記錄 boolean saveOrUpdate(T entity); // 根據(jù)updateWrapper嘗試更新,否繼續(xù)執(zhí)行saveOrUpdate(T)方法 boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper); // 批量修改插入 boolean saveOrUpdateBatch(Collection<T> entityList); // 批量修改插入 boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);Remove
// 根據(jù) entity 條件,刪除記錄 boolean remove(Wrapper<T> queryWrapper); // 根據(jù) ID 刪除 boolean removeById(Serializable id); // 根據(jù) columnMap 條件,刪除記錄 boolean removeByMap(Map<String, Object> columnMap); // 刪除(根據(jù)ID 批量刪除) boolean removeByIds(Collection<? extends Serializable> idList);Update
// 根據(jù) UpdateWrapper 條件,更新記錄 需要設置sqlset boolean update(Wrapper<T> updateWrapper); // 根據(jù) whereWrapper 條件,更新記錄 boolean update(T updateEntity, Wrapper<T> whereWrapper); // 根據(jù) ID 選擇修改 boolean updateById(T entity); // 根據(jù)ID 批量更新 boolean updateBatchById(Collection<T> entityList); // 根據(jù)ID 批量更新 boolean updateBatchById(Collection<T> entityList, int batchSize);Get
// 根據(jù) ID 查詢 T getById(Serializable id); // 根據(jù) Wrapper,查詢一條記錄。結果集,如果是多個會拋出異常,隨機取一條加上限制條件 wrapper.last("LIMIT 1") T getOne(Wrapper<T> queryWrapper); // 根據(jù) Wrapper,查詢一條記錄 T getOne(Wrapper<T> queryWrapper, boolean throwEx); // 根據(jù) Wrapper,查詢一條記錄 Map<String, Object> getMap(Wrapper<T> queryWrapper); // 根據(jù) Wrapper,查詢一條記錄 <V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);List
// 查詢所有 List<T> list(); // 查詢列表 List<T> list(Wrapper<T> queryWrapper); // 查詢(根據(jù)ID 批量查詢) Collection<T> listByIds(Collection<? extends Serializable> idList); // 查詢(根據(jù) columnMap 條件) Collection<T> listByMap(Map<String, Object> columnMap); // 查詢所有列表 List<Map<String, Object>> listMaps(); // 查詢列表 List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper); // 查詢?nèi)坑涗?List<Object> listObjs(); // 查詢?nèi)坑涗?<V> List<V> listObjs(Function<? super Object, V> mapper); // 根據(jù) Wrapper 條件,查詢?nèi)坑涗?List<Object> listObjs(Wrapper<T> queryWrapper); // 根據(jù) Wrapper 條件,查詢?nèi)坑涗?<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);Page
// 無條件分頁查詢 IPage<T> page(IPage<T> page); // 條件分頁查詢 IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper); // 無條件分頁查詢 IPage<Map<String, Object>> pageMaps(IPage<T> page); // 條件分頁查詢 IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper); -
創(chuàng)建Service接口和實現(xiàn)類
public interface UserService extends IService<User> { } @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } -
測試查詢記錄數(shù)
@Test public void testGetCount() { long count = userService.count(); System.out.println("count = " + count); } -
測試批量插入
@Test public void testSaveBatch() { // SQL 長度有限制,海量數(shù)據(jù)插入單條SQL無法實行 // 因此MP將批量插入放在了通用Service中實現(xiàn),而不是通用Mapper ArrayList<User> users = new ArrayList<>(); for (int i = 0; i < 5; i++ ){ User user = new User(); user.setName("abc" + 1); user.setAge(20); users.add(user); } // INSERT INTO t_user1 ( name, age ) VALUES ( ?, ? ) userService.saveBatch(users); }
4.3 條件構造器
在BaseMapper類中的方法,大多數(shù)方法中都有帶`Wrapper`類型的形參,這個`Wrapper`就是條件構造器,能夠針對SQL語句設置不同的條件。**如果沒有條件,則可以將該形參賦值為null,即為查詢(刪除/修改)所有數(shù)據(jù)**
Wrapper的關系樹:

-
Wrapper:條件構造抽閑類,最頂端父類-
AbstractWrapper:用于查詢條件封裝,生成sql的where條件-
QueryWrapper:查詢條件封裝 -
UpdateWrapper: 更新條件封裝 -
AbstractLambdaWrapper:使用Lambda語法的條件封裝-
LambdaQueryWrapper:用于Lambda語法使用的查詢Wrapper -
LambdaUpdateWrapper:用于Lambda語法使用的更新Wrapper
-
-
-
-
QueryWrapper
-
組裝查詢條件
@Test public void test01() { // 查詢用戶名包含a,年齡在20-30之間,并且郵箱不為null的用戶信息 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的sql:SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) wrapper.like("name", "a") .between("age", 20, 30) .isNotNull("email"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 輸出結果 User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=zhangsan, age=20, email=test3@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com) -
組裝排序條件
@Test public void test02() { // 按年齡降序查詢用戶,如果年齡相同則按id升序排列 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的sql:SELECT id,name,age,email FROM user ORDER BY age DESC,id ASC wrapper.orderByDesc("age") .orderByAsc("id"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 輸出結果 User(id=5, name=Billie, age=24, email=test5@baomidou.com) User(id=4, name=Sandy, age=21, email=test4@baomidou.com) User(id=2, name=Jack, age=20, email=test2@baomidou.com) User(id=3, name=zhangsan, age=20, email=test3@baomidou.com) User(id=1, name=Jone, age=18, email=test1@baomidou.com) -
組裝刪除條件
@Test public void test03() { // 刪除email為空的用戶 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的sql:DELETE FROM user WHERE (email IS NULL) wrapper.isNull("email"); int result = userMapper.delete(wrapper); System.out.println("result = " + result); } // 輸出結果 result = 0,因為沒有用戶email是null的 -
條件的優(yōu)先級
@Test public void test04() { // 將(年齡大于20并且用戶名中包含a)或者郵箱為null的用戶信息修改 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的sql:UPDATE user SET age=?, email=? WHERE (age >= ? AND name LIKE ? OR email IS NULL) wrapper.ge("age", 20) .like("name", "a") .or() .isNull("email"); User user = new User(); user.setAge(33); user.setEmail("user@qq.com"); int update = userMapper.update(user, wrapper); System.out.println("update = " + update); } // 輸出結果 update = 3@Test public void test05() { // 將用戶名中包含a并且(年齡大于20或者郵箱為null)的用戶信息修改 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的SQL:UPDATE user SET age=?, email=? WHERE (name LIKE ? AND (age >= ? OR email IS NULL)) wrapper.like("name", "a") .and(i -> i.ge("age", 20).or().isNull("email")); User user = new User(); user.setAge(10); user.setEmail("abc@qq.com"); int update = userMapper.update(user, wrapper); System.out.println("update = " + update); }主動調(diào)用
or表示接著的下一個方法不是用and連接(默認是使用and連接的) -
組裝select子句
@Test public void test06() { // 查詢用戶信息的name,age字段 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構造的sql:SELECT name,age FROM user wrapper.select("name", "age"); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } // 輸出結果 User(id=null, name=Jone, age=18, email=null) User(id=null, name=Jack, age=10, email=null) User(id=null, name=zhangsan, age=10, email=null) User(id=null, name=Sandy, age=10, email=null) User(id=null, name=Billie, age=24, email=null) -
實現(xiàn)子查詢
@Test public void test07() { // 查詢id小于等于3的用戶信息 QueryWrapper<User> wrapper = new QueryWrapper<>(); // 構建的sql:SELECT id,name,age,email FROM user WHERE (id IN (select id from user where id <= 3)) wrapper.inSql("id", "select id from user where id <= 3"); List<User> list = userMapper.selectList(wrapper); list.forEach(System.out::println); } // 輸出結果 User(id=1, name=Jone, age=18, email=test1@baomidou.com) User(id=2, name=Jack, age=10, email=abc@qq.com) User(id=3, name=zhangsan, age=10, email=abc@qq.com)這里只是測試效果,實際應該不會這樣select id
-
-
LambdaQueryWrapper
上面使用
QueryWrapper中,我們查詢時使用的是數(shù)據(jù)庫列名字符串,容易出現(xiàn)寫錯或與類的屬性名不一致的導致寫錯,MP提供了LambdaQueryWrapper,可以通過獲取Lambda數(shù)據(jù)庫列名。@Test public void test08() { // 查詢用戶名包含a,年齡在10-20之間,并且郵箱不為null的用戶信息 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // SELECT id,name,age,email FROM user WHERE (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL) wrapper.like(User::getName, "a") .between(User::getAge, 10, 20) .isNotNull(User::getEmail); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); }使用LambdaQueryWrapper后,不用直接寫列名字符串形式,可以通過類獲取
-
UpdateWrapper
前面的測試例子中也有用QueryWrapper實現(xiàn)update的,使用
UpdateWrapper可以使用set方法直接設置,不需要創(chuàng)建User對象,如果創(chuàng)建了User對象,并且設置屬性,傳遞給了update方法,那么設置列會加入到sql中。@Test public void test09() { // 將(年齡大于20或郵箱為null)并且用戶名中包含有a的用戶信息修改 UpdateWrapper<User> updateWrapper = new UpdateWrapper<>(); // UPDATE user SET age=?,email=? WHERE (name LIKE ? AND (age >= ? OR email IS NULL)) updateWrapper.set("age", 18) .set("email", "user@qq.com") .like("name", "a") .and(i -> i.ge("age", 20).or().isNull("email")); int update = userMapper.update(null, updateWrapper); System.out.println("update = " + update); } -
LambdaUpdateWrapper
將上面的測試例子用Lambda的方式來編寫
@Test public void test09() { // 將(年齡大于20或郵箱為null)并且用戶名中包含有a的用戶信息修改 LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.set(User::getAge, 38) .set(User::getEmail, "user@qq.com") .like(User::getName, "a") .and(i -> i.ge(User::getAge, 10).or().isNull(User::getEmail)); User user = new User(); user.setName("Billiea"); int update = userMapper.update(user, updateWrapper); System.out.println("update = " + update); } -
總結
-
QueryWrapper、QueryChainWrapper只能指定需要的數(shù)據(jù)庫列名 -
LambdaQueryWrapper、LambdaQueryChainWrapper可以通過Lambda獲取數(shù)據(jù)庫列名 -
QueryWrapper、LambdaQueryWrapper不能使用鏈式查詢的方式,必須借助BaseMapper來執(zhí)行 -
QueryChainWrapper、LambdaQueryChainWrapper可以使用鏈式查詢的方式,如list(),one()
-
4.4 condition
在真正開發(fā)過程中,組裝條件時,有些數(shù)據(jù)是來源于用戶輸入的,是可選的,因此在組裝條件之前是需要先判斷條件是否成立的,成立才組裝條件到SQL執(zhí)行。
MP在條件構造器的方法中提供了帶condition參數(shù)的方法
未使用帶condition參數(shù)的方法時的操作:
@Test
public void test10() {
// 定義查詢條件,有可能為null,假設輸入數(shù)據(jù)如下
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
queryWrapper.like(User::getName, "a");
}
if (ageBegin != null) {
queryWrapper.ge(User::getAge, ageBegin);
}
if (ageEnd != null) {
queryWrapper.le(User::getAge, ageEnd);
}
// SELECT id,name,age,email FROM user WHERE (age >= ? AND age <= ?)
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
使用帶condition參數(shù)的方法:
@Test
public void test11() {
// 定義查詢條件,有可能為null,假設輸入數(shù)據(jù)如下
String name = null;
Integer ageBegin = 10;
Integer ageEnd = 24;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(name), User::getName, name)
.ge(ageBegin != null, User::getAge, ageBegin)
.le(ageEnd != null, User::getAge, ageEnd);
List<User> users = userMapper.selectList(queryWrapper);
users.forEach(System.out::println);
}
5. 常用注解
-
@TableName在以上的測試中,我們使用MP進行CRUD時,并沒有指定要操作的表,只是在UserMapper接口繼承BaseMapper時,設置了泛型為User,但實際操作的表是User表。
因此可以得出結論,MP在確定操作的表時,是由BaseMapper的泛型決定的,即實體類型決定,而且默認操作的表名與實體類的類名一致。
@TableName的作用是標識實體類對應的表,作用在實體類上。可以做如下測試:將數(shù)據(jù)庫的表user改名為t_user,運行前面的測試程序,報錯如下
org.springframework.jdbc.BadSqlGrammarException: ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist ### The error may exist in com/zzy/mybatisplus_test/mapper/UserMapper.java (best guess) ### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: SELECT id,name,age,email FROM user WHERE (age >= ? AND age <= ?) ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist ; bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'mybatisplus.user' doesn't exist解決方法:因為當前實體類為User,數(shù)據(jù)庫表為t_user,二者名字不匹配,因此報錯,可以在User類上添加注解
@TableName("t_user")標識User類與表t_user匹配@Data @NoArgsConstructor @AllArgsConstructor @TableName("t_user") public class User { private Long id; private String name; private Integer age; private String email; }上面我們只是改了一個表,如果所有的實體類對應的表都有固定的前綴,比如t_,我們總不能手動一個個的添加@TableName注解吧,那得累死
為了解決這種情況,我們可以通過MP全局配置,為實體類所對應的表名設置默認的前綴:
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默認前綴 table-prefix: t_ -
@TableId經(jīng)過以上測試,MP在實現(xiàn)CRUD時,會默認將id作為主鍵列,并在插入數(shù)據(jù)時,默認基于雪花算法的策略生成id
問題:如果實體類和表中表示主鍵的不是id,而是其他字段,比如uid,那MP還會自動識別uid為主鍵列嗎?
測試:將實體類中屬性id改為uid,表中字段id也改為uid,測試添加功能
### SQL: INSERT INTO t_user1 ( name, age, email ) VALUES ( ?, ?, ? ) ### Cause: java.sql.SQLException: Field 'uid' doesn't have a default value ; Field 'uid' doesn't have a default value; nested exception is java.sql.SQLException: Field 'uid' doesn't have a default value解決方法:通過
@TableId將uid屬性標識為主鍵public class User { @TableId private Long uid; private String name; private Integer age; private String email; }若實體類中主鍵對應的屬性為id,而表中表示主鍵的字段為uid,此時如果只在屬性上添加注解
@TableId,則會拋出異常Unknown column 'id' in 'field list',也就是MP仍然會將id作為表的主鍵操作,而表中表示主鍵的是字段uid。此時就需要通過@TableId的value屬性了,指定表中主鍵字段:
@TableId("uid")或@TableId(value="uid")@TableId的type屬性
常用的主鍵策略:
-
IdType.ASSIGN_ID(默認)基于雪花算法的策略生成數(shù)據(jù)id,與數(shù)據(jù)庫id是否設置自增無關
-
IdType.AUTO使用數(shù)據(jù)庫的自增策略,注意,該類型請確保數(shù)據(jù)庫設置了id自增,否則無效
配置全局主鍵策略:
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默認前綴 table-prefix: t_ # 配置MP的主鍵策略 id-type: auto -
-
@TableField經(jīng)過以上的測試,我們可以發(fā)現(xiàn),MP在執(zhí)行SQL語句時,要保證實體類中的屬性名和表中的字段名一致
如果實體類中的屬性名和字段名不一致,會產(chǎn)生什么問題?
-
如果實體類中屬性使用的是駝峰命名風格,而表中字段使用的是下劃線命名風格,比如實體類屬性為userName,表中字段為user_name.
這種情況MP會自動將下劃線命名的風格轉(zhuǎn)化為駝峰命名風格
-
如果實體類中的屬性和表中的字段不滿足第一個情況,比如實體類屬性是name,表中字段為username
這種情況需要在實體類上添加注解
@TableField("username")設置屬性對應的字段名
-
-
@TableLogic邏輯刪除:假刪除,就是新增一個字段用來表示刪除狀態(tài),在數(shù)據(jù)庫中仍舊可以看到這條記錄,不是真正的刪除,但是對于MP來說像是刪除了的,無法使用CRUD再操作這條數(shù)據(jù)。
實現(xiàn)邏輯刪除:
- 數(shù)據(jù)庫中創(chuàng)建邏輯刪除狀態(tài)列,設置默認值為0
- 實體類中添加邏輯刪除屬性,添加注解@TableLogic
6.插件
-
分頁插件
MP自帶分頁插件,只需要簡單配置就可以實現(xiàn)分頁功能了。
-
添加配置類
@Configuration @MapperScan("com.zzy.mybatisplus_test.mapper") // 可以將主類中Mapper的注解移到這里 public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 數(shù)據(jù)類型為DbType.MYSQL mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return mybatisPlusInterceptor; } } -
測試
@Test public void testPage() { // 設置分頁參數(shù),通過分頁參數(shù)來確定數(shù)據(jù) Page<User> page = new Page<User>(1, 3); userMapper.selectPage(page, null); // 獲取分頁數(shù)據(jù) List<User> list = page.getRecords(); list.forEach(System.out::println); System.out.println("當前頁: " + page.getCurrent()); System.out.println("每頁顯示的條數(shù): " + page.getSize()); System.out.println("總記錄數(shù): " + page.getTotal()); System.out.println("總頁數(shù): " + page.getPages()); System.out.println("是否有上一頁:" + page.hasPrevious()); System.out.println("是否有下一頁: " + page.hasNext()); }
使用XML自定義分頁
-
UserMapper中定義接口方法
/** * 根據(jù)年齡查詢用戶列表,分頁顯示 * @param page 分頁對象,xml中可以從里面進行取值,傳遞參數(shù)Page即自動分頁,必須放在第一位 * @param age 年齡 * @return */ Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age); -
UserMapper.xml中編寫SQL
<?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.zzy.mybatisplus_test.mapper.UserMapper"> <sql id="BaseColumns">uid, name, age, email</sql> <select id="selectPageVo" resultType="User"> select <include refid="BaseColumns"/> from t_user1 where age > #{age} </select> </mapper> -
測試
@Test public void testSelectPageVo() { // 設置分頁參數(shù) Page<User> page = new Page<>(1, 3); userMapper.selectPageVo(page, 20); // 獲取分頁數(shù)據(jù) List<User> list = page.getRecords(); list.forEach(System.out::println); System.out.println("當前頁: " + page.getCurrent()); System.out.println("每頁顯示的條數(shù): " + page.getSize()); System.out.println("總記錄數(shù): " + page.getTotal()); System.out.println("總頁數(shù): " + page.getPages()); System.out.println("是否有上一頁:" + page.hasPrevious()); System.out.println("是否有下一頁: " + page.hasNext()); }
-
-
樂觀鎖插件
悲觀鎖與樂觀鎖的理解可以看看這個文章:https://my.oschina.net/hanchao/blog/3057429
-
樂觀鎖實現(xiàn)方式:(當要更新一條記錄的時候,希望這條記錄沒有被別人更新)
取出記錄時,獲取當前version
更新時,帶上這個version
執(zhí)行更新時,set version = newVersion where version = oldVersion
如果version不對,就更新失敗
-
模擬沖突場景
有一件商品成本80元,售價100元,老板先讓小李將商品價格提高50元,小李沒有及時操作,一小時后,老板覺得商品售價150元太高,就又讓小王去將價格降低30元。
老板的想法是100 -> 150 -> 120
但是這時候小李和小王同時操作系統(tǒng),取出的商品價格都是100元,然后小李加了50元,將150存到了數(shù)據(jù)庫;小王減掉30元,將70存入數(shù)據(jù)庫.
如果沒有鎖,小李的操作就會被小王覆蓋了,最終可能價格變成了70元,導致老板虧錢
新增一張商品表
CREATE TABLE t_product ( id BIGINT(20) NOT NULL COMMENT '主鍵ID', NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名稱', price INT(11) DEFAULT 0 COMMENT '價格', VERSION INT(11) DEFAULT 0 COMMENT '樂觀鎖版本號', PRIMARY KEY (id) ); # 添加數(shù)據(jù) INSERT INTO t_product (id, NAME, price) VALUES (1, '筆記本', 100);測試(未使用樂觀鎖):
@Test public void testConCurrentUpdate() { // 1. 小李 Product p1 = productMapper.selectById(1L); System.out.println("小李取出的價格:" + p1.getPrice()); // 2. 小王 Product p2 = productMapper.selectById(1L); System.out.println("小王取出的價格: " + p2.getPrice()); // 3. 小李將價格增加50元,存入了數(shù)據(jù)庫 p1.setPrice(p1.getPrice() + 50); int ret1 = productMapper.updateById(p1); System.out.println("小李修改結果:" + ret1); // 4. 小王將價格減了30元,存入了數(shù)據(jù)庫 p2.setPrice(p2.getPrice() - 30); int ret2 = productMapper.updateById(p2); System.out.println("小王修改結果:" + ret2); // 最后的結果 Product p3 = productMapper.selectById(1L); System.out.println("最后的價格是:" + p3.getPrice()); } // 最終輸出的價格變?yōu)?0 -
樂觀鎖配置:
-
配置插件
@Configuration @MapperScan("com.zzy.mybatisplus_test.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 數(shù)據(jù)類型為DbType.MYSQL mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 樂觀鎖插件 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return mybatisPlusInterceptor; } } -
實體類字段加上
@Version注解模擬沖突:
@Data @NoArgsConstructor @AllArgsConstructor public class Product { private Integer id; private String name; private Integer price; @Version private Integer version; }
-
-
測試(使用樂觀鎖后的測試)
@Test public void testConcurrentVesionUpdate() { // 小李取數(shù)據(jù) Product p1 = productMapper.selectById(1L); // 小王取數(shù)據(jù) Product p2 = productMapper.selectById(1L); // 小李修改 + 50 p1.setPrice(p1.getPrice() + 50); int result = productMapper.updateById(p1); System.out.println("小李修改的結果:" + result); // 小王修改 - 30 p2.setPrice(p2.getPrice() - 30); int ret = productMapper.updateById(p2); System.out.println("小王修改的結果:" + ret); if (ret == 0) { // 失敗重試,重新獲取version并更新 p2 = productMapper.selectById(1L); p2.setPrice(p2.getPrice() - 30); ret = productMapper.updateById(p2); } System.out.println("小王修改重試的結果:" + ret); // 老板看價格 Product p3 = productMapper.selectById(1L); System.out.println("老板看價格:" + p3.getPrice()); }
7.通用枚舉
表中有些字段值是固定的,比如性別,只有男或女,此時我們可以用MP的通用枚舉來實現(xiàn)
-
數(shù)據(jù)庫表增加一個字段sex
enum.png -
創(chuàng)建通用枚舉類型
@Getter public enum SexEnum { MALE(1, "男"), FEMALE(2, "女"); @EnumValue private Integer sex; private String sexName; SexEnum(Integer sex, String sexName) { this.sex = sex; this.sexName = sexName; } } -
配置掃描通用枚舉
# 配置Mybatis日志 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 配置MP操作表的默認前綴 table-prefix: t_ # 配置MP的主鍵策略 id-type: auto type-aliases-package: com.zzy.mybatisplus_test.pojo # 配置掃描通用枚舉 type-enums-package: com.zzy.mybatisplus_test.enums -
測試
@Test public void testSexEnum() { User user = new User(); user.setName("Enum"); user.setAge(20); // 設置性別信息為枚舉項,會將@EnumValue注解所標注的屬性值存儲到數(shù)據(jù)庫 user.setSex(SexEnum.FEMALE); userMapper.insert(user); }
8.多數(shù)據(jù)源
適用于多種場景:純粹多庫,讀寫分離,一主多從,混合模式等
接下來模擬一個純粹多庫的場景:
創(chuàng)建兩個庫,分別為:mybatisplus(上面的庫不動)與mybatisplus_1(新建),將mybatis_plus庫的t_product表移動到mybatisplus_1庫,這樣每個庫一張表,通過一個測試用例分別獲取用戶數(shù)據(jù)與商品數(shù)據(jù),如果獲取到說明多庫模擬成功。
-
創(chuàng)建數(shù)據(jù)庫及表
CREATE DATABASE `mybatisplus_1`; use `mybatisplus_1`; CREATE TABLE t_product ( id BIGINT(20) NOT NULL COMMENT '主鍵ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名稱', price INT(11) DEFAULT 0 COMMENT '價格', version INT(11) DEFAULT 0 COMMENT '樂觀鎖版本號', PRIMARY KEY (id) ); INSERT INTO t_product (id, NAME, price) VALUES (1, '筆記本', 100); # 刪除mybatisplus庫的t_product表 use mybatisplus; DROP TABLE IF EXISTS t_product; -
引入依賴
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.1</version> </dependency> -
配置多數(shù)據(jù)源
將之前的數(shù)據(jù)庫連接配置注釋掉,添加新的配置
spring: # 配置數(shù)據(jù)源信息 datasource: dynamic: # 設置默認的數(shù)據(jù)源或者數(shù)據(jù)源組,默認值即為master primary: master # 嚴格匹配數(shù)據(jù)源,默認false.true未匹配到指定數(shù)據(jù)源時拋異常,false使用默認數(shù)據(jù)源 strict: false datasource: master: url: jdbc:mysql://localhost:3306/mybatisplus?characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.jdbc.Driver username: root password: root slave_1: url: jdbc:mysql://localhost:3306/mybatisplus_1?characterEncoding=utf-8&useSSL=false driver-class-name: com.mysql.jdbc.Driver username: root password: root -
創(chuàng)建用戶Service
在Service上添加注解
@DS,指定數(shù)據(jù)源public interface UserService extends IService<User> { } @Service @DS("master") public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } -
創(chuàng)建商品Service
public interface ProductService extends IService<Product> { } @DS("slave_1") @Service public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService { } -
測試
@Test public void testDynamicDataSource() { // User user = userService.getById(1L); User user = userMapper.selectById(2L); System.out.println(user); System.out.println(productService.getById(1L)); }注意:
這里在測試時候遇到了一點bug,使用Mybatis-plus 3.5.2版本的時候,從數(shù)據(jù)庫查詢得到的枚舉不會轉(zhuǎn)換,一直報錯:
org.springframework.jdbc.UncategorizedSQLException: Error attempting to get column 'sex' from result set. Cause: java.sql.SQLException: Error
; uncategorized SQLException; SQL state [null]; error code [0]; Error; nested exception is java.sql.SQLException: Error一直查找問題,最后查看了這個issue:https://github.com/baomidou/mybatis-plus/issues/4338,使用3.4.3,3.5.1,3.5.3版本都報錯,只有將mybatis-plus版本退回3.4.2才可以正常運行。
