MyBatis-Plus是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。MyBatis-Plus提供了通用的mapper和service,可以在不编写任何SQL语句的情况下,快速的实现对单表的CRUD、批量、逻辑州除、分页等操作。本文从MyBatis- Plus的特性及使用,到MyBatis-Pus所提供的优秀的插件,以及多数据源的配置都有详细的讲解。
本文主要以MysQL数据库为案例,使用ldea作为IDE,使用Maven作为构建工具,使用Spring Boot展示MyBatis-Plus的各个功能,所以需要有MyBatis和Spring Boot的基础。
https://baomidou.com
官网地址:https://baomidou.com/pages/24112f/#%E7%89%B9%E6%80%A7
任何能使用 MyBatis 进行 CRUD, 并且支持标准 SQL 的数据库,具体支持情况如下,如果不在下列表查看分页部分教程 PR 您的支持。
第一步:Scan Entity扫描实体,根据反射技术对实体类中的属性进行抽取;
第二步:分析表与实体类中的关系,以及表字段与实体属性之间的关系;
第三步:根据当前调用的方法来生成对应的SQL语句,再将生成的SQL语句注入到MyBatis的容器中
IDE:idea
JDK:JDK8
构建工具:Maven
数据库:MySQL:8.0.29
SpringBoot:2.7.7
MyBatis-Plus:3.5.1
-- 创建MyBatis-Plus测试数据库
CREATE DATABASE `mybatis_plus`;
USE `mybatis_plus`;
-- 创建测试user表
DROP TABLE IF EXISTS `user`;
CREATE TABLE IF NOT EXISTS `user`(
`id` BIGINT(20) NOT NULL COMMENT '主键',
`name` VARCHAR(30) DEFAULT NULL COMMENT'姓名',
`age` INT(11) DEFAULT NULL COMMENT '年龄',
`email` VARCHAR(50) DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY(`id`)
);
-- 添加测试数据
INSERT INTO `user` (id,name,age,email) VALUES
(1,'Jone',18,'[email protected]'),
(2,'Jack',20,'[email protected]'),
(3,'Tom',28,'[email protected]'),
(4,'Sandy',21,'[email protected]'),
(5,'Billie',24,'[email protected]');
MyBatis-Plus在数据插入时,默认会使用雪花算法来生成id,所以采用bigint类型
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.7version>
<relativePath/>
parent>
<groupId>com.xiugroupId>
<artifactId>mybatis-plus-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>mybatis-plus-demoname>
<description>mybatis-plus-demodescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
MyBatis-Plus的依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.1version>
dependency>
** 注意:**
1、驱动类spring.datasource.driver-class-name:
SpringBoot2.0(内置jdbc5驱动),驱动类使用:
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
SpringBoot2.1及以上(内置jdbc8驱动),驱动类使用:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
若配置与版本不对应,会有警告信息!!!
2、连接地址spring.datasource.url
MySQL5.7版本的url:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=UTF-8&useSSL=false
MySQL8.0版本的url:
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimeZone=GMT%2B&characterEncoding=UTF-8&useSSL=false
否则运行时,会报错:java.sql.SQLException:The server time zone value…
当前MySQL驱动为8版本的且SpringBoot为2.7.7
#端口
server.port=8080
#数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_plus?serverTimeZone=GMT%2B&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=19960327xiu
MyBatis中有ORM的概念,对象关系映射,表中字段要与实体进行映射。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 4916508803893379409L;
/**
* 主键
*/
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 邮箱
*/
private String email;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.User;
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
public interface UserMapper extends BaseMapper<User> {
}
说明:
BaseMapper是由MyBatis-Plus提供的通用的Mapper,包含了对单表的各种CRUD操作
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.xiu.mybatisplusdemo.mapper")
@SpringBootApplication
public class MybatisPlusDemoApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisPlusDemoApplication.class, args);
}
}
说明:
@MapperScan注解是用来扫描,对应包下的所有Mapper接口, 所动态生成的代理类交给IOC容器进行管理
报错原因:
@MapperScan注解是用来扫描,对应包下的所有Mapper接口, 所动态生成的代理类交给IOC容器进行管理。并不是将接口交给IOC容器进行管理,IOC容器中只能存在类所对应的Bean,而不能存在接口所对应的Bean,所以是将UserMapper动态生成的代理类放进IOC容器中,IDE在编译时, 认为UserMapper无法自动装配,但是,运行时不会有问题。
若强迫症,不希望看到红色波浪线,可以在UserMapper的接口上添加一个注解:@Repository,标识为持久层组件。
即可解决。
import com.xiu.mybatisplusdemo.mapper.UserMapper;
import com.xiu.mybatisplusdemo.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
/**
* 测试查询列表
* 有条件:构造queryWrapper
* 无条件传入:null
*/
@Test
public void testQueryList() {
//null表示没有条件,若有条件,需要构造queryWrapper
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
}
之前我们需要在接口中写方法,并在对应的映射文件中编写SQL语句,但是现在我们仅需要使用MyBatis-plus为我们提供的方法即可。
测试结果:
测试成功!
当前我们只是根据extends BaseMapper设置了User的范型,即找到了对应的user表。
将user表中的字段赋值给了User类中对应的属性。
BaseMapper中存在各种各样的方法:
当前我们只看到了MyBatis-Plus为我们提供的方法,即结果,如果要查看实际操作所生成的SQL语句,需要添加日志。
在application.properties中添加配置即可。
#日志配置
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
再次执行测试用例
然而,我们当前并没有指明我们要操作的表,以及表中的字段和实体类中属性的对应关系。
因为,当前我们的表名与类名刚好一致,表中的字段与属性刚好一一对应,完全匹配。
该图中显示:先去扫描实体,然后反射抽取实体类中属性,再分析表及表字段,再生成SQL注入容器。
所以,我们当前要操作的表及表字段由实体类及属性来决定。
源码:
/**
* Mapper 继承该接口后,无需编写 mapper.xml 文件,即可获得CRUD功能
* 这个 Mapper 支持 id 泛型
*
* @author hubin
* @since 2016-01-23
*/
public interface BaseMapper<T> extends Mapper<T> {
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据实体(ID)删除
*
* @param entity 实体对象
* @since 3.4.4
*/
int deleteById(T entity);
/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/**
* 根据 entity 条件,删除记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 删除(根据ID或实体 批量删除)
*
* @param idList 主键ID列表或实体列表(不能为 null 以及 empty)
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<?> idList);
/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);
/**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T selectById(Serializable id);
/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
/**
* 查询(根据 columnMap 条件)
*
* @param columnMap 表字段 map 对象
*/
List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/**
* 根据 entity 条件,查询一条记录
* 查询一条记录,例如 qw.last("limit 1") 限制取一条记录, 注意:多条数据会报异常
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) {
List<T> ts = this.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(ts)) {
if (ts.size() != 1) {
throw ExceptionUtils.mpe("One record is expected, but the query result is multiple records");
}
return ts.get(0);
}
return null;
}
/**
* 根据 Wrapper 条件,判断是否存在记录
*
* @param queryWrapper 实体对象封装操作类
* @return
*/
default boolean exists(Wrapper<T> queryWrapper) {
Long count = this.selectCount(queryWrapper);
return null != count && count > 0;
}
/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录
* 注意: 只返回第一个字段的值
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
<P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
/**
* 根据 Wrapper 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件
* @param queryWrapper 实体对象封装操作类
*/
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}
/**
* 测试新增功能
*/
@Test
public void testInsert() {
User user = new User(null, "zhangsan", 20, "[email protected]");
int count = userMapper.insert(user);
//获取自动生成的id
System.out.println("id=" + user.getId());
Assert.assertEquals(1, count);
}
测试结果:
数据信息:
使用断言,添加了pom
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
/**
* 测试根据id进行删除
*/
@Test
public void testDeleteById() {
//DELETE FROM user WHERE id=?
int count = userMapper.deleteById(1606668345558142977L);
Assert.assertEquals(1, count);
}
测试结果:
/**
* 根据map集合中设置的条件进行删除
* map中是条件
*/
@Test
public void testDeleteByMap() {
Map<String, Object> map = new HashMap<>();
map.put("name", "zhangsan");
map.put("age", 20);
//DELETE FROM user WHERE name = ? AND age = ?
int count = userMapper.deleteByMap(map);
Assert.assertEquals(1, count);
}
测试结果:
/**
* 测试通过多个id进行批量删除方法
*/
@Test
public void testDeleteBatchIds() {
List<Long> idList = Arrays.asList(1606685497249914881L, 1606685517403557890L, 1606685581836427266L);
int count = userMapper.deleteBatchIds(idList);
Assert.assertEquals(idList.size(), count);
}
测试结果:
/**
* 测试根据id进行修改
*/
@Test
public void testUpdateById() {
User user = new User();
user.setId(1606686812017520641L);
user.setName("test");
user.setEmail("[email protected]");
//UPDATE user SET name=?, email=? WHERE id=?
int count = userMapper.updateById(user);
Assert.assertEquals(1, count);
}
测试结果:
/**
* 测试根据id进行查询
*/
@Test
public void testSelectById() {
Long id = 1L;
//SELECT id,name,age,email FROM user WHERE id=?
User user = userMapper.selectById(id);
System.out.println(user);
Assert.assertEquals(user.getId(), id);
}
测试结果:
/**
* 测试根据id列表进行查询
*/
@Test
public void testSelectBatchIds() {
List<Long> idList = Arrays.asList(1L, 2L, 3L);
//SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? )
List<User> userList = userMapper.selectBatchIds(idList);
userList.forEach(System.out::println);
Assert.assertEquals(idList.size(), userList.size());
}
测试结果:
/**
* 测试根据map进行查询
*/
@Test
public void testSelectByMap() {
Map<String, Object> map = new HashMap<>();
map.put("name", "Jack");
map.put("age", 20);
//SELECT id,name,age,email FROM user WHERE name = ? AND age = ?
List<User> userList = userMapper.selectByMap(map);
userList.forEach(System.out::println);
}
测试结果:
/**
* 测试查询列表
*/
@Test
public void testSelectList() {
//SELECT id,name,age,email FROM user
List<User> userList = userMapper.selectList(null);
userList.forEach(System.out::println);
}
测试结果:
注意:若没有条件,则queryWrapper参数传null,删除和修改也可这样操作,但是,一般不会修改或删除全部数据。
MyBatis只是对我们原来的MyBatis功能进行增强,而不会影响原本的功能。
若BaseMapper提供的功能,无法满足我们的需求,我们可通过编写映射文件来实现。
我们可以配置mybatis-plus.config-location属性的值,来指定mapper文件位置。
但是,mybatis-plus.config-location有默认配置:classpath*:/mapper/**/*.xml
也就是资源目录下的mapper文件夹内,可以里面再创建多层文件目录。
UserMapper接口:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.Map;
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
/**
* 根据id查询map
*
* @param id
* @return
*/
Map<String, Object> selectMapById(@Param("id") Long id);
}
映射配置文件:
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiu.mybatisplusdemo.mapper.UserMapper">
<select id="selectMapById" resultType="java.util.Map">
SELECT
id,
name,
age,
email
FROM
`user`
WHERE
id=#{id}
select>
map>
单测:
/**
* 测试自定义方法:根据id查询Map
*/
@Test
public void testSelectMapById() {
Long id = 1L;
//SELECT id, name, age, email FROM `user` WHERE id=?
Map<String, Object> map = userMapper.selectMapById(id);
System.out.println(map);
}
测试结果:
官网地址:https://baomidou.com/pages/49cc81/#service-crud-%E6%8E%A5%E5%8F%A3
说明:
- 通用 Service CRUD 封装IService(opens new window)接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
- 泛型 T 为任意实体对象
- 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
- 对象 Wrapper 为 条件构造器
MyBatis-Plus中有一个接口IService和其实现类ServiceImpl,封装了常见的业务逻辑。
IService
saveOrUpdate的操作区别:
如果没有id,则为新增,如果有id,则说明是修改。
IService:
public interface IService<T> {
//T是实体对象
}
ServiceImpl:
/**
* IService 实现类( 泛型:M 是 mapper 对象,T 是实体 )
*
* @author hubin
* @since 2018-06-23
*/
@SuppressWarnings("unchecked")
public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {
//M:自己写的Mapper
//T:实体对象
}
注意:
ServiceImpl大概率不能满足我们的需求,不建议使用,我们可以自己编写接口,然后继承IService
当前UserServiceImpl实现了UserService,但是UserService接口继承了IService,所以需要重写所有的IService中的方法,但实际并不需要,所以UserServiceImpl只需要再即成MyBatis-Plus提供的ServiceImpl即可。
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
//UserMapper是我们要操作的Mapper
//User是要操作的实体类
}
如此,我们便可以使用通用的功能,也可以使用我们自定义的功能。
/**
* 测试查询总count数,不带条件
*/
@Test
public void testGetCount() {
//SELECT COUNT( * ) FROM user
long count = userService.count();
System.out.println(count);
}
测试结果:
BaseMapper中并未提供批量添加的方法,但是IService中提供了saveBatch方法,但是该方法,也是通过单个添加,循环来实现。
/**
* 测试批量添加的功能
*/
@Test
public void testInsertMore() {
List<User> entityList = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
User user = new User();
user.setName("test" + i);
user.setAge(10 + i);
entityList.add(user);
}
// INSERT INTO user ( id, name, age ) VALUES ( ?, ?, ? )
boolean res = userService.saveBatch(entityList);
Assert.assertEquals(true, res);
}
测试结果:
通过单个添加,循环实现的批量添加。
之前,我们并未指定表名,便可进行操作,说明操作的表是由BaseMapper设置的范型来决定的。
如果,我们的表名与我们的实体类的类名不一致,在操作时就会报错:
如果我们将数据库中的表名从user改为t_user,再次执行测试用例:
我们可以通过在实体类上添加@TableName来指定数据库中的表名
@TableName("t_user")
public class User implements Serializable {
...
}
再次执行测试用例查看 :
但是,如果我们每张表名前全部都有一样的前缀就会很繁琐。
我们可以通过配置MyBatis-Plus的全局配置,指定所有实体类表名默认的前缀。
#全局配置实体类对应表名的统一前缀
mybatis-plus.global-config.db-config.table-prefix=t_
MyBatis-Plus默认会将id作为主键,但是如果我们的实体属性是uid,表字段也是uid,这样去执行方法时,会报错,因为,MyBatis-Plus默认是将id作为主键的。如果uid会指定为主键,那么其他属性是不是也会指定为主键。
我们可以通过@TableId 将这个属性所对应的字段指定为主键。
示例:
@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
private static final long serialVersionUID = 4916508803893379409L;
/**
* 主键
* TableId是将这个属性所对应的字段指定为主键
*/
@TableId
private Long id;
...
}
@TableId注解源码:
/**
* 表主键标识
*
* @author hubin
* @since 2016-01-23
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
public @interface TableId {
/**
* 字段名(该值可无)
*/
String value() default "";
/**
* 主键类型
* {@link IdType}
*/
IdType type() default IdType.NONE;
}
若我们的实体类中的主键属性与表字段不一致,实体类中主键是id,表字段是uid,则无法形成对应。此时执行方法也会报错。
当我们去操作id这个属性时,对应的字段也是id,表中没有对应的字段就会报错。
我们可通过@TableId注解的value属性来指定表中表示主键的字段名。
@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
private static final long serialVersionUID = 4916508803893379409L;
/**
* 主键
* TableId是将这个属性所对应的字段指定为主键
* value来指定表中对应的主键字段
*/
@TableId(value = "id")
private Long id;
...
}
@TableId的value属性时用于指定主键的字段
如果只设置value的属性,则可以省略value=,因为默认设置的就是value属性
@TableId(“id”) 即可。
表示主键生成的策略,MyBatis-Plus中id默认生成主键的策略是雪花算法。
如果我们希望主键是自增的而不是通过雪花算法生成,首先需要设置数据库中的主键字段是主键自动自增。
-- 清空表数据
TRUNCATE TABLE t_user;
-- 指定表主键自增
ALTER TABLE t_user MODIFY id BIGINT AUTO_INCREMENT;
我们之前的主键是雪花算法生成然后再赋值给id字段,我们使用主键自增并没有生成再赋值,而是直接自增生成。
@TableName("t_user") //指定数据库中的表名
public class User implements Serializable {
private static final long serialVersionUID = 4916508803893379409L;
/**
* 主键
* TableId是将这个属性所对应的字段指定为主键
* value来指定表中对应的主键字段
* type指定主键生成策略
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
...
}
再次测试:
此时,id并不是先通过雪花算法生成再插入数据库的,而是通过数据库主键自增生成的。
type的其他选项:
/**
* 生成ID类型枚举类
*
* @author hubin
* @since 2015-11-10
*/
@Getter
public enum IdType {
/**
* 数据库ID自增
* 该类型请确保数据库设置了 ID自增 否则无效
*/
AUTO(0),
/**
* 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
*/
NONE(1),
/**
* 用户输入ID
* 该类型可以通过自己注册自动填充插件进行填充
*/
INPUT(2),
/* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
/**
* 分配ID (主键类型为number或string),
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
*
* @since 3.3.0
*/
ASSIGN_ID(3),
/**
* 分配UUID (主键类型为 string)
* 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
*/
ASSIGN_UUID(4);
private final int key;
IdType(int key) {
this.key = key;
}
}
说明:
默认的是ASSIGN_ID:基于雪花算法生成,与数据库id是否设置自增无关
AUTO:使用数据库的自增,该类型必须确保数据库设置了主键自增,否则无效。
因为id作为主键,是非空的,如果不设置主键自增,我们在执行时,id没有值,就会报错。
如果我们只是设置了value=“id”,并没有指定type。
我们在添加时,设置了id=100L,此时是否还会按照type的默认雪花算法生成id。测试结果是并不会根据雪花算法生成id,而是根据设置的值,进行赋值。
如果我们想统一主键的生成策略,实现方式:
配置全局配置主键生成策略:
#全局统一配置主键生成策略
mybatis-plus.global-config.db-config.id-type=auto
配置的属性值与@TableId注解的值选项IdType类一致。
总结:
上述主键策略默认生成是通过雪花算法生成的。
随着数据规模增长,需要合适的方案去应对逐渐增长的访问压力和数据量。
数据库拓展:业务分库、主从复制(读写分离)、数据库分表。
在不同业务数据分散到不同的数据库服务器中,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如:淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定无法满足性能要求,此时就需要对单表数据进行拆分。
单表数据拆分的两种方式:垂直分表和水平分表。
示意图:
垂直分表适合将表中某些不常用旦古了大量空间的列拆分出去。
例如,前面示意因中的nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用户的时候,主要是用 age 和sex两个宇段进行查询,而 nicknamne 和description 两个字段主要用于展示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外一张表中,这样在查询 age 和sex 时,就能带来一定的性能提升。
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。
但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶顽或者隐患
水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理
◎以最常见的用户 ID 为例,可以按照 1000000 的范国大小进行分段,1-999999 放到表1中,1000000-
1999999 放到表2中,以此类推。
②复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。
③优点:可以随着数据的增加平滑地扩 充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。
④缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有1条,而另外
段实际存储的数据量有1000 万条。
◎同样以用户 1D 为例,假如我们一开始就规划了 10 个数据库表,可以简单地用 user _id % 10的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID为 10086 的用户放到编号为 6 的子表中。
②复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。
③优点:表分布比较均匀。
④缺点:扩充新的表很麻烦,所有数据都要重分布,
优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率极高。
之前@TableId注解主要是用来处理主键的映射关系,用来设置主键,以及主键的生成策略。
若其他属性与数据库表字段名不一致。
情况一:
实体类属性:userName 小驼峰命名
表字段:user_name 下划线命名
此时并不需要额外配置,MyBatis-Plus默认配置下划线转驼峰。
情况二:
实体类属性:name
表字段:user_name 下划线命名
此时,属性与字段名不匹配,且不满足下划线转驼峰。运行会报错:
我们可以通过@TableField注解来指定属性所对应的数据库中的表字段。
示例:
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
@TableName("t_user") //指定数据库中的表名
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 4916508803893379409L;
/**
* 主键
* TableId是将这个属性所对应的字段指定为主键
* value来指定表中对应的主键字段
* type指定主键生成策略
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 姓名
* TableField用来指定数据库中对应的字段名
*/
@TableField("name")
private String name;
...
}
注意:
@TableId与@TableField区别:
@TableId设置的是主键相关的配置关系
@TableField设置的是普通非主键的配置关系
比如:is_deleted字段表示该数据是否删除,0表示未删除,1表示已删除。其实本质上就是修改操作。
在表中新添加一个新的字段:is_deleted
-- 添加逻辑删除字段is_deleted
ALTER TABLE t_user ADD is_deleted INT(2) DEFAULT 0 COMMENT '逻辑删除,0未被删除,1已删除';
实体类中添加对应属性:
/**
* 是否被删除
*/
@TableField("is_deleted")
@TableLogic //表示逻辑删除字段
private Integer isDeleted;
测试:
其本质就是修改操作。
数据库表数据:
再次执行查询操作:
对应的SQL添加了条件,is_deleted=0。
结果并没有被逻辑删除的数据。
如果,表名与属性名不一致时,我们使用@TableField或者@TableId注解,MyBatis-Plus会通过别名的形式,形成映射关系:
我们当前的删除操作会变成修改操作,查询操作会变成查询未被逻辑删除的数据,而不需要手动去指定。
条件构造器就是用来封装我们当前的条件的。增删改查的SQL语句中,删除、修改、查询是可以添加条件的,我们可以通过Wrapper来构造条件。
示例:
/**
* 查询条件:
* 用户名包含a,
* 年龄在20-30之间,
* email不为null
*/
@Test
public void testSelectList() {
//范型指定为实体类类型
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like("name", "a").between("age", 20, 30).isNotNull("email");
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
示例:
/**
* 查询条件:
* 查询用户信息,按照年龄的降序排序,若年龄相同,则按照id升序排序
*/
@Test
public void testSelectList02() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 ORDER BY age DESC,id ASC
wrapper.orderByDesc("age").orderByAsc("id");
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
删除此次使用的是QueryWrapper条件构造器
示例:
/**
* 删除条件:
* 删除email为null的user
*/
@Test
public void testDel() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.isNull("email");
//UPDATE t_user SET is_deleted=1 WHERE is_deleted=0 AND (email IS NULL)
int delete = userMapper.delete(wrapper);
System.out.println("res = " + delete);
}
结果:
示例:
/**
* 修改条件:
* 将年龄大于20并且用户名中包含a或者邮箱为null的用户信息修改
*/
@Test
public void testUpdate() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.gt("age", 20).like("name", "a").or().isNull("email");
User user = new User();
user.setAge(44);
user.setEmail("[email protected]");
//UPDATE t_user SET age=?, email=? WHERE is_deleted=0 AND (age > ? AND name LIKE ? OR email IS NULL)
int update = userMapper.update(user, wrapper);
System.out.println("res = " + update);
}
结果:
备注:
update方法,两种用法:
用法一使用QueryWrapper:参数一为要修改的字段,参数二为条件。
用法二使用UpdateWrapper:参数一为null,参数二为条件+要修改的值。
举例:将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
当前需求中,括号中的条件为优先判断条件。
当我们使用了and或者or方法,如果使用了lambda表达式,则lambda优先执行。
示例:
/**
* 修改条件:
* 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
*/
@Test
public void testUpdate2() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
//wrapper.gt("age", 20).like("name", "a").or().isNull("email");
wrapper.like("name", "a").and(x -> x.gt("age", 20).or().isNull("email"));
User user = new User();
user.setName("aaa");
user.setEmail("[email protected]");
//UPDATE t_user SET name=?, email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
int update = userMapper.update(user, wrapper);
System.out.println("res = " + update);
}
结果:
如果我们只需要查看部分字段即可,而不需要每次都返回全部字段。
示例:
/**
* 只返回部分字段
*/
@Test
public void testSelect() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.select("name", "age", "email");
//SELECT name,age,email FROM t_user WHERE is_deleted=0
List<Map<String, Object>> maps = userMapper.selectMaps(wrapper);
maps.forEach(System.out::println);
}
结果:
示例:
SQL语句:
USE `mybatis_plus`;
SELECT id FROM t_user WHERE id<=100;
SELECT * FROM t_user WHERE id IN (SELECT id FROM t_user WHERE id<=100);
代码:
/**
* 子查询
*/
@Test
public void testChildSelect() {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.inSql("id", "SELECT id FROM t_user WHERE id<=100");
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (id IN (SELECT id FROM t_user WHERE id<=100))
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
UpdateWrapper不仅仅可以设置修改的条件,还可以设置修改的字段
示例:
/**
* 根据updateWrapper来修改用户信息
* 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
*/
@Test
public void testUpdateByUpdateWrapper() {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.like("name", "a").and(x -> x.gt("age", 20).or().isNull("email"));
updateWrapper.set("name", "111").set("email", "[email protected]");
//UPDATE t_user SET name=?,email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
int res = userMapper.update(null, updateWrapper);
System.out.println("res = " + res);
}
结果:
示例:
/**
* 模拟实际开发中的条件
*/
@Test
public void testQuery() {
String name = "";
Integer ageBegin = 10;
Integer ageEnd = 30;
QueryWrapper<User> wrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(name)) {
wrapper.like("name", name);
}
if (ageBegin != null) {
wrapper.ge("age", ageBegin);
}
if (ageEnd != null) {
wrapper.le("age", ageEnd);
}
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
当前存在的问题,每个条件都需要手动去判断、组装,很繁琐。
示例:
/**
* 根据condition条件查询
*/
@Test
public void testQueryByCondition() {
String name = "";
Integer ageBegin = 10;
Integer ageEnd = 30;
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), "name", name);
wrapper.ge(ageBegin != null, "age", ageBegin);
wrapper.le(ageEnd != null, "age", ageEnd);
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
当前存在的另外一个问题:我们在字符串内手动写字段名,写错了,字符串也不会有任何提示。
我们要访问的字段直接访问实体类对应的属性即可,无需手动去写字段名了,避免出错。
示例:
/**
* 根据LambdaQueryWrapper条件查询
*/
@Test
public void testQueryByLambda() {
String name = "";
Integer ageBegin = 10;
Integer ageEnd = 30;
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotBlank(name), User::getName, name);
wrapper.ge(ageBegin != null, User::getAge, ageBegin);
wrapper.le(ageEnd != null, User::getAge, ageEnd);
//SELECT id,name,age,email,is_deleted FROM t_user WHERE is_deleted=0 AND (age >= ? AND age <= ?)
List<User> userList = userMapper.selectList(wrapper);
userList.forEach(System.out::println);
}
结果:
示例:
/**
* 根据LambdaUpdateWrapper来修改用户信息
* 将用户名中包含a并且(年龄大于20或邮箱为null)的用户信息修改。
*/
@Test
public void testUpdateByLambdaUpdateWrapper() {
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.like(User::getName, "a").and(x -> x.gt(User::getAge, 20).or().isNull(User::getEmail));
updateWrapper.set(User::getName, "111").set(User::getEmail, "[email protected]");
//UPDATE t_user SET name=?,email=? WHERE is_deleted=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
int res = userMapper.update(null, updateWrapper);
System.out.println("res = " + res);
}
结果:
MyBatis-Plus自带分页插件,只需要简单配置。
因为是在查询功能进行拦截,然后再进行操作。
配置Bean:
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;
/**
* @author zhangzengxiu
* @date 2023/1/2
*/
@Configuration
public class MyBatisPlusConfig {
/**
* 分页插件
*
* @return
*/
@Bean
public MybatisPlusInterceptor getMybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
示例:
/**
* 测试分页插件
*/
@Test
public void testPagePlugin() {
Page<User> page = new Page<>(2, 2);
userMapper.selectPage(page, null);
System.out.println("记录:" + page.getRecords());
System.out.println("总页数" + page.getPages());
System.out.println("总记录数" + page.getTotal());
System.out.println("是否有上一页" + page.hasPrevious());
System.out.println("是否有下一页" + page.hasNext());
}
结果:
自定义查询:
接口:
/**
* 通过年龄查询用户信息并分页
*
* @param page MyBatis-Plus提供的分页对象,必须放在第一个参数位置
* @param age
* @return
*/
Page<User> selectUserByPage(@Param("page") Page<User> page, @Param("age") Integer age);
映射配置文件:
<select id="selectUserByPage" resultType="com.xiu.mybatisplusdemo.pojo.User">
SELECT
id,
name,
age,
email
FROM
`t_user`
WHERE
age > #{age}
select>
注意:
1、返回值必须是:Page
2、第一个参数必须是:Page
结果:
乐观锁实现流程:
数据库表中添加字段version,
去除记录时,获取当前version
SELECT id,name,price,version FROM product WHERE id = 1;
更新时,version+1,如果version的版本与之前取出来的版本不对应,则更新失败
UPDATE product SET price=price+50 ,version=version+1 WHERE id = 1 AND version=1;
表:
USE `mybatis_plus`;
DROP TABLE IF EXISTS t_product;
CREATE TABLE IF NOT EXISTS t_product(
id BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
`name` VARCHAR(30) DEFAULT NULL COMMENT '商品名',
price INT(11) DEFAULT 0 COMMENT '价格',
version INT(11) DEFAULT 0 COMMENT '乐观锁'
);
-- 添加测试数据
INSERT INTO t_product (name,price) VALUES ("Mac",100);
代码:
接口:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xiu.mybatisplusdemo.pojo.Product;
import org.springframework.stereotype.Repository;
/**
* @author zhangzengxiu
* @date 2023/1/2
*/
@Repository
public interface ProductMapper extends BaseMapper<Product> {
}
实体:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* t_product表对应的实体
*
* @author zhangzengxiu
* @date 2023/1/2
*/
@TableName("t_product")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
/**
* 主键
*/
@TableId(type = IdType.AUTO, value = "id")
private Long id;
/**
* 商品名
*/
@TableField("name")
private String name;
/**
* 价格
*/
@TableField("price")
private Integer price;
/**
* 版本号
*/
@TableField("version")
private Integer version;
}
单测:
@Autowired
private ProductMapper productMapper;
@Test
public void testUpdate() {
Product productA = productMapper.selectById(1);
System.out.println("A查询到的价格 = " + productA.getPrice());
Product productB = productMapper.selectById(1);
System.out.println("B查询到的价格 = " + productB.getPrice());
//A将价格在原本基础上+50
productA.setPrice(productA.getPrice() + 50);
productMapper.updateById(productA);
//B将价格在原本基础上-30
productB.setPrice(productB.getPrice() - 30);
productMapper.updateById(productB);
Product productBoss = productMapper.selectById(1);
System.out.println("Boss查询到的价格 = " + productBoss.getPrice());
}
结果:
此时结果是70,与我们的预期不符。
修改实体类:
@TableName("t_product")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
...
/**
* 版本号
*/
@TableField("version")
@Version
private Integer version;
}
@Version 注解标明是一个乐观锁版本号字段。
配置乐观锁插件:
/**
* @return
*/
@Bean
public MybatisPlusInterceptor getMybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
再次执行:
此时查询到的价格为150
数据库数据:
当前存在的问题:A操作执行完成,但是B并未执行成功,以至于最终结果不满足预期。
添加重试逻辑代码:
@Autowired
private ProductMapper productMapper;
@Test
public void testUpdate() {
Product productA = productMapper.selectById(1);
System.out.println("A查询到的价格 = " + productA.getPrice());
Product productB = productMapper.selectById(1);
System.out.println("B查询到的价格 = " + productB.getPrice());
//A将价格在原本基础上+50
productA.setPrice(productA.getPrice() + 50);
productMapper.updateById(productA);
//B将价格在原本基础上-30
productB.setPrice(productB.getPrice() - 30);
int res = productMapper.updateById(productB);
if (res == 0) {
//重试
Product productNew = productMapper.selectById(1);
productNew.setPrice(productNew.getPrice() - 30);
productMapper.updateById(productNew);
}
Product productBoss = productMapper.selectById(1);
System.out.println("Boss查询到的价格 = " + productBoss.getPrice());
}
执行流程:
表添加性别字段:
USE `mybatis_plus`;
-- 添加性别字段
ALTER TABLE t_user ADD gender INT(1) COMMENT '性别:1男2女';
实体:
/**
* @author zhangzengxiu
* @date 2022/12/24
*/
@TableName("t_user") //指定数据库中的表名
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
...
/**
* 性别字段
*/
@TableField("gender")
private GenderEnum gender;
}
性别枚举:
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author zhangzengxiu
* @date 2023/1/2
*/
@Getter
@AllArgsConstructor
public enum GenderEnum {
UNKNOWN(0, "未知"),
MALE(1, "男"),
FEMALE(2, "女");
/**
* 性别
*/
private Integer gender;
/**
* 性别名称
*/
private String genderName;
}
单测:
/**
* @author zhangzengxiu
* @date 2023/1/2
*/
@SpringBootTest
public class MyBatisPlusEnumTest {
@Autowired
private UserMapper userMapper;
@Test
public void test() {
User user = new User();
user.setName("admin");
user.setAge(33);
user.setGender(GenderEnum.MALE);
int res = userMapper.insert(user);
Assert.assertEquals(1, res);
}
}
执行单测:
报错:
修改性别枚举:添加注解@EnumValue 标识要存到数据库中的值
@Getter
@AllArgsConstructor
public enum GenderEnum {
...
/**
* 性别
*/
@EnumValue
private Integer gender;
}
添加配置:
#扫描通用枚举包
mybatis-plus.type-enums-package=com.xiu.mybatisplusdemo.enums
再次执行:
实际存到数据库的值也是对应的值。
即可。
场景:一些固定的值, Java中可以通过枚举类来实现这个功能,将指定的值添加到数据库中,我们需要通过@EnumValue注解配合包扫描来找到对应的枚举。
略。