Mybatis-Plus是一个以Mybatis为基础,目的是为了开发者更简单的使用Mybatis,简化开发,提高效率。意愿是成为Mybatis最好的搭档,就像魂斗罗中的1P、2P
特点(节选自官网)
第一个demo
首先我们需要在MySql(也可以是其他)中创建一张user测试表,表结构如下
id | name | age | gender |
---|---|---|---|
1 | Lil Lei | 20 | 1 |
2 | Han Meimei | 18 | 0 |
3 | Jim | 20 | 1 |
4 | LinTao | 21 | 1 |
5 | Lucy | 18 | 0 |
6 | Lily | 18 | 0 |
DDL 如下
CREATE TABLE `user` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(32) DEFAULT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`gender` int DEFAULT NULL COMMENT '性别 0-女; 1-男',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
接下来需要创建springboot项目,引入以下相关maven依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>29.0-jreversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
在entity包中创建entity
@Data
@ToString
public class User {
private Long id;
private String name;
private Byte age;
private Byte gender;
}
在mapper包中创建mapper,需要继承Mybatis-Plus的BaseMapper
@Repository
public interface IUserMapper extends BaseMapper<User> {
}
在SpringBoot启动类上标注mapper扫描路径
@SpringBootApplication
@MapperScan("com.beemo.mybatisplus.mapper")
public class MybatisplusApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisplusApplication.class, args);
}
}
编写测试方法
@SpringBootTest
class MybatisplusApplicationTests {
@Autowired
private IUserMapper userMapper;
@Test
void testDemo() {
List<User> users = Optional.ofNullable(userMapper.selectList(null)).orElse(Lists.newArrayList());
users.stream().forEach(System.out::println);
}
}
控制台输出
User(id=1, name=Li Lei, age=20, gender=1)
User(id=2, name=Han Meimei, age=18, gender=0)
User(id=3, name=Jim, age=20, gender=1)
User(id=4, name=Lin Tao, age=21, gender=1)
User(id=5, name=Lucy, age=18, gender=0)
User(id=6, name=Lily, age=18, gender=0)
我们的第一个demo就成功了
刚才的例子中,我们并不能观察到SQL信息,虽然查询结果符合预期,但是无法确认SQL是否是正确,使得我们调试非常不方便,此时可以添加以下配置,来显示SQL信息
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
在运行我们上述的demo,在控制台就可以看到执行的SQL信息了
该注解表示一个实体类对应的表名,如表名与实体名相同(或满足驼峰式命名规则对应)
举例,表名叫sys_admin,实体类叫Admin
@TableName("sys_admin")
public class Admin {
// ...
}
值 | 描述 |
---|---|
AUTO | 数据库ID自增 |
NONE | 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT) |
INPUT | insert前自行set主键值 |
ASSIGN_ID | 分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法) |
ASSIGN_UUID | 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法) |
同ASSIGN_ID | |
同ASSIGN_UUID | |
同ASSIGN_ID |
具体的类型还需根据程序来选择,如为微服务等,选择ASSIGN_ID;如为单机,那么选择自增就可以
举例:
@TableId(value = "u_id", type = IdType.ASSIGN_ID)
private Long id;
该字段同@TableId,但是为映射非主键字段,同时属性也比较丰富
value:指定数据库的列名
exists:标识该字段是否为数据库表列
fill:自动填充策略,见后续说明
select:是否为查询项。有时候一些信息我们不想每次都进行查询,比如创建时间,创建用户等,此时我们可以标注select为false
举例:
将数据库添加新列
ALTER TABLE `user` ADD COLUMN create_time DATETIME DEFAULT NULL COMMENT '创建时间';
更新创建时间
UPDATE `user` SET create_time = now();
在实体类中添加新字段
// 表示该字段不是数据库表字段
@TableField(exist = false)
private String genderName;
// 表示查询时不查询此字段
@TableField(select = false)
private LocalDateTime createTime;
修改测试方法
@Test
void testDemo() {
List<User> users = Optional.ofNullable(userMapper.selectList(null)).orElse(Lists.newArrayList());
users.stream().forEach(u -> {
u.setGenderName(convertGenderName(u.getGender()));
System.out.println(u);
});
}
private String convertGenderName(Byte gender) {
gender = Optional.ofNullable(gender).orElse((byte)-1);
switch (gender) {
case 0: return "女";
case 1: return "男";
default: return "未知";
}
}
在运行方法,观察控制台
可以看出,createTime字段为数据库对应列名,但是查询的列中并不包含该列,证明我们的设置select = false生效;genderName不为数据库字段,程序没有报错,证明我们的设置exist = false生效
上述例子中,我们需要手动去转换性别,比较麻烦,Mybatis-Plus支持枚举的映射
首先定义枚举类
public enum Gender {
MALE(1, "男"),
FEMALE(0, "女");
Gender(int value, String name) {
this.value = value;
this.name = name;
}
@EnumValue
private int value;
private String name;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public java.lang.String getName() {
return name;
}
public void setName(java.lang.String name) {
this.name = name;
}
}
要记得与数据库对应的字段需要使用@EnumValue
标注
然后将实体类的性别字段,声明为枚举类型
private Gender gender;
之后我们再进行查询
可以看到,查询时Mybatis-Plus已经帮我们做了自动映射,我们再试一下增加和修改操作是否也会自动映射
首先修改实体类,增加主键自增方式以及构造方法
修改测试类,增加方法
@Test
void insert() {
User user = new User();
user.setName("Poly");
user.setAge((byte)2);
user.setGender(Gender.MALE);
user.setCreateTime(LocalDateTime.now());
userMapper.insert(user);
}
@Test
void update() {
User user = new User();
user.setId(1L);
user.setGender(Gender.FEMALE);
userMapper.updateById(user);
}
有时候我们不想去在每次增加和修改的时候,去设置字段,比如创建时间,编辑时间,创建人,编辑人等,此时我们可以使用Mybatis-Plus的自动填充
在数据库增加字段update_time, create_user, update_user
ALTER TABLE `user`
ADD COLUMN create_user varchar(32) COMMENT '创建用户',
ADD COLUMN update_time datetime COMMENT '更新时间',
ADD COLUMN update_user varchar(32) COMMENT '更新用户';
在需要自动填充的字段上标注@Table(fill=xx)
其中可选值为
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT)
private String createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateUser;
配置自动填充策略
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// String user = xxUtil.getUser();
String user = "admin";
strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
strictInsertFill(metaObject, "createUser", String.class, user);
strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
strictInsertFill(metaObject, "updateUser", String.class, user);
}
@Override
public void updateFill(MetaObject metaObject) {
String user = "admin2";
strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
strictInsertFill(metaObject, "updateUser", String.class, user);
}
}
其中insertFill方法配置的是新增时需要填充的字段,updateFill方法配置的是修改时需要填充的字段
将测试创建方法的createTime赋值去掉,重新执行创建方法以及修改方法
有时候我们在删除时并不想真的将数据从数据库中删除掉,而是给一个标记,标注这条记录是被删除的。比如用户注销等,我们还需要保留用户的信息。在查询的时候,我们只需要查询未被删除的记录即可
首先需要在数据库增加删除标记
ALTER TABLE `user`
ADD COLUMN flag SMALLINT DEFAULT 0 COMMENT '删除标记 0-未删除; 1-已删除';
然后我们在实体类增加该字段,并且使用@TableLogic进行标注
@TableLogic
private Byte flag;
进行逻辑删除配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag #全局逻辑删除字段值
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
创建删除测试类
@Test
void remove() {
userMapper.deleteById(1L);
}
运行方法,查看控制台
可以发现这里并不是删除,而是标记为已删除状态,实际是修改操作
我们再执行查询方法,看控制台输出
可以看到在查询的时候,自动拼接了flag=0字段,即只查询未删除的
小结:
logic-delete-field: flag
,那么就不需要标注@TableLogic。但是如果字段标记为@TableLogic,那么即使不是flag字段,也将作为逻辑删除字段,即@TableLogic优先级高于全局配置在接触CRUD之前,我们先认识一下wrapper
wrapper是条件构造器,当我们进行查询、更新、删除操作时,会用到它
wrapper是一个抽象类,常用的子类有QueryWrapper
、UpdateWrapper
、LambdaQueryWrapper
、LambdaUpdateWrapper
使用方法:
/ 创建查询wrapper,并添加查询条件
User user = new User();
user.setName("Lucy");
Wrapper<User> wrapper1 = new QueryWrapper<>(user);
Wrapper<User> wrapper2 = Wrappers.query(user);
给wrapper传递的user对象,如果某个属性不为空,那么就会作为查询条件,上述例子中查询条件即为name=“lucy”
除了根据实体传递查询条件,我们还可以使用QueryWrapper中的方法来手动赋值查询条件
// 创建查询wrapper
QueryWrapper<User> wrapper1 = new QueryWrapper<>();
QueryWrapper<User> wrapper2 = Wrappers.query();
wrapper1.eq("name", "Lucy");
wrapper1.ge("age", 15);
wrapper1.orderByAsc("age");
其中eq
,ge
等常用方法含义如下
方法 | 描述 | 方法 | 描述 |
---|---|---|---|
eq | 相等 | ge | 大于 |
le | 小于 | in | 在…之中 |
like | 模糊查询 | order by | 排序 |
select | 查询指定列 |
其中select可以指定要查询的列,例如
QueryWrapper<User> wrapper2 = Wrappers.query();
wrapper2.select("name", "age");
LambdaQueryWrapper<User> wrapper1 = Wrappers.lambdaQuery();
wrapper1.eq(User::getName, "Lily");
我们需要在我们的mapper中继承BaseMapper,然后就可以使用封装好的方法了
@Repository
public interface IUserMapper extends BaseMapper<User> {
}
User user = new User();
userMapper.insert(user);
调用该方法会向数据库保存一条T对象对应的数据库记录
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
// 查询所有
userMapper.selectList(null);
// 带有条件查询列表
userMapper.selectList(wrapper.le(User::getAge, 18).eq(User::getGender, Gender.FEMALE));
// 查询单条记录,如果结果返回多条,则会抛出异常TooManyResultsException
userMapper.selectOne(wrapper.eq(User::getName, "Lin Tao"));
// 查询数量
userMapper.selectCount(wrapper.eq(User::getName, "Lin Tao"));
// 根据主键查询
userMapper.selectById(1);
// 根据主键集合查询列表
userMapper.selectBatchIds(Arrays.asList(1, 2, 3, 4));
// 根据主键修改,修改ID为1的记录,赋值age为19
User user = new User();
user.setId(1L);
user.setAge((byte)19);
userMapper.updateById(user);
LambdaUpdateWrapper<User> wrapper = Wrappers.lambdaUpdate();
// 此种写法会修改age为21,但是不会触发自动填充updateTime以及updateUser
// userMapper.update(null, wrapper.eq(User::getId, 1).set(User::getAge, 21));
userMapper.update(new User(), wrapper.eq(User::getId, 1).set(User::getAge, 21));
// 如果有@TableLogic或者全局配置逻辑删除,则会改为update方法
// 根据主键删除,逻辑删除时,不会触发自动填充
userMapper.deleteById(1);
// 根据主键集合删除,逻辑删除时,不会触发自动填充
userMapper.deleteBatchIds(Arrays.asList(1, 2, 3, 4));
// 根据wrapper删除,逻辑删除时,不会触发自动填充
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
userMapper.delete(wrapper.eq(User::getName, "Jim"));
// 根据map删除,逻辑删除时,不会触发自动填充
Map<String, Object> params = Maps.newHashMap();
params.put("name", "Han meimei");
userMapper.deleteByMap(params);
Mybatis-Plus在service中又为我们封装了一层CRUD
首先我们需要创建service以及实现类,并继承相应的接口/类IService
、ServiceImpl
public interface IUserService extends IService<User> {
}
@Service
@AllArgsConstructor
public class UserServiceImpl extends ServiceImpl<IUserMapper, User> implements IUserService {
}
在测试类注入service
@Autowired
private IUserService userService;
User user = new User();
// 保存单条
// 这里赋值ID不会生效
user.setId(1L);
userService.save(user);
// 批量保存
userService.saveBatch(Lists.newArrayList(user));
// 批量保存,一次insert插入n条
userService.saveBatch(Lists.newArrayList(new User(), new User(), new User(), new User()), 2);
// 如何设置id,则为修改,会首先查询一次;如果不设置id,则为新增
// user.setId(1L);
userService.saveOrUpdate(user);
// 查询所有
userService.list();
// 根据wrapper条件查询
userService.list(Wrappers.<User>lambdaQuery().eq(User::getName, "Li Lei"));
// 根据主键查询
userService.listByIds(Arrays.asList(1, 2, 3, 4));
// 根据主键修改
User user = new User();
user.setId(1L);
user.setAge((byte)17);
userService.updateById(user);
// 根据wrapper修改,不会触发自动填充
userService.update(Wrappers.<User>lambdaUpdate()
.eq(User::getId, 1).set(User::getAge, 16));
// 根据wrapper修改,user中的设置无效,触发自动填充
userService.update(user, Wrappers.<User>lambdaUpdate().set(User::getAge, 15));
// 根据wrapper删除,如果为逻辑删除,则会update,不会触发自动填充
userMapper.delete(Wrappers.<User>lambdaQuery().eq(User::getId, 1));
// 根据主键删除,如果为逻辑删除,则会update,不会触发自动填充
userMapper.deleteById(1);
// 根据主键删除,如果为逻辑删除,则会update,不会触发自动填充
userMapper.deleteBatchIds(Arrays.asList(1, 2, 3, 4));
// 根据map删除,如果为逻辑删除,则会update,不会触发自动填充
Map<String, Object> params = Maps.newHashMap();
params.put("id", 1);
userMapper.deleteByMap(params);
我们平时开发中使用最多的就是分页了,MyBatis-Plus为我们实现了分页组件,即在SQL中拼装LIMIT
使用分页首先必须要进行配置
@Component
public class MyConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}
之后我们就可以使用mapper或service中的分页API了
分页API中会使用到IPage
接口,我们会使用到他的实现类Page
// 常用构造方法
new Page(); // 默认一页显示10条,查询第一页
new Page(2, 20); // 查询第二页,一页显示20条
如需要不分页(比如导出EXCEL查询list需要调用分页查询),可以有以下两种方法
new Page<>(1, -1); // 不会进行LIMIT拼接
new Page<>(1, Long.MAX_VALUE); // 会拼接LIMIT,只不过LIMIT一个Long的最大值
mapper
IPage page = new Page<>(1, 20);
userMapper.selectPage(page, null);
IPage page = new Page<>(1, 20);
userService.page(page);
userService.page(page, Wrappers.<User>lambdaQuery().le(User::getAge, 18));