MyBatis-Plus是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。使用MyBatis-Plus能够极大地提升开发效率。

img

IService和BaseMapper辨析

查看MyBatis-Plus的文档,我们可以发现,在其内部存在着两种CRUD操作接口,Iservice和BaseMapper,如果只是用增删改查,会发现除了方法名称不同外,两者的功能是一致的。那如何在开发中进行合理的选择?

我们先来看一下官网的描述:

Service CRUD 接口

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper条件构造器

Mapper CRUD接口

  • 通用 CRUD 封装BaseMapper接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper条件构造器

MyBatis-Plus 提供两种包含预定义增删改查操作的接口和一个类:

  • BaseMapper 接口:针对dao/mapper层的方法封装 CRUD
  • IService<T>接口:针对业务逻辑层的封装需要指定Dao层类和对应的实体类 是在BaseMapper基础上的加强
  • ServiceImpl<M extends BaseMapper<T>, T> 类:针对业务逻辑层的实现

我们一般开发的模板代码如下:

1
2
3
4
5
6
7
8
9
10
@Repository
public class ArticleDao extends ServiceImpl<ArticleMapper, ArticleDO> {
@Resource
private ArticleMapper articleMapper;
}

public interface ArticleMapper extends BaseMapper<ArticleDO> {
}

mapper/ArticleMapper.xml

我们来看一下继承结构:

image-20240327171219061

是不是感觉很奇怪,我们明明只写Mapper就能够实现CRUD的,而继承了ServiceImpl,依旧实现了IService接口,同时依赖BaseMapper,有点混乱了。

我们来看一下这两个接口中的方法:

ServiceImpl

可以发现,IServiceBaseMapper的扩充,不但包含了所有基本方法,还加入了很多批处理功能。另外,也有一些差异:

  • 方法命名差异:比如,插入数据:在BaseMapper使用 insert,在IService使用 save ;
  • IService 提供了批处理操作的方法,而 BaseMapper 没有提供相应的批处理操作;
  • IService 依赖于 Spring 容器,而 BaseMapper 不依赖;
  • 如果既要使用批处理操作,又要添加自己的数据库操作,那就必须两个接口一起用。

可以这样认为:

Mapper简化了单表的SQL操作步骤(CRUD),而Serivce实现的了更加丰富的CRUD,加入了很多批处理功能。但是必须依赖Mapper,因此需要编写Mapper接口。

以下是通义千问的回答:

在MyBatis Plus框架中,IService接口和BaseMapper接口分别服务于不同的层次和目的。

BaseMapper

  • BaseMapper是一个抽象的接口,它定义了一系列基本的CRUD操作方法,这些方法对应于数据库的基本操作,如查询、插入、更新和删除等。
  • BaseMapper并不直接依赖Spring容器,因为它只定义了数据库操作的契约,具体的数据库交互逻辑是由MyBatis框架通过XML映射文件或者注解的方式来实现的。
  • MyBatis的Mapper接口可以直接由MyBatis框架处理,无需Spring容器进行实例化和依赖注入,Mapper接口的实现是由MyBatis在运行时动态生成的代理对象。

IService

  • IService是MyBatis Plus提供的一个服务层接口,它扩展了BaseMapper的基础CRUD功能,通常包含一些业务逻辑相关的操作,例如事务处理、组合查询以及其他领域逻辑等。
  • IService接口的设计意图是将其服务类对接到Spring容器中,以便利用Spring的依赖注入机制管理服务类的生命周期,以及注入其他的依赖,如事务管理器、DAO层对象(也就是实现了BaseMapper接口的对象)等。
  • 默认实现com.baomidou.mybatisplus.extension.service.impl.ServiceImpl通过继承BaseMapper,并在Spring容器中注册,这样@Service注解标注的ServiceImpl类就能够被Spring容器管理,从而具备了依赖注入的能力。

总结来说,**BaseMapper关注于数据库操作的具体接口定义,而IService则是更高一层的服务接口,它依赖Spring容器来获取依赖关系并参与到Spring框架所管理的整个应用上下文中。通过这样的设计,开发者可以在服务层实现复杂的业务逻辑,并利用Spring容器进行统一的依赖管理和生命周期管理。**

关于IService和BaseMapper的一些说明

以下内容转载自https://cloud.tencent.com/developer/article/2362497

Service CRUD 接口

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行、 remove 删除 、list 查询集合 、page 分页 前缀命名方式区分 Mapper 层避免混淆
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,就创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
  • 对象 Wrapper 为条件构造器
1
2
3
4
5
6
/**
* 订单 服务类
*/
public interface IOrderService extends IService<Order> {
// 无需编写任何方法,继承 IService 即可使用通用的 CRUD 方法
}

IOrderService 接口扩展了 MyBatis-Plus 提供的 IService<Order> 接口,表示它定义了与 Order 实体相关的业务逻辑方法。IService<Order> 接口是 MyBatis-Plus 的一部分,提供了一组通用的服务方法,包括常见的 CRUD(创建、读取、更新、删除)操作。

Mapper CRUD 接口

  • 通用 CRUD 封装BaseMapper接口,为 Mybatis-Plus 启动时自动解析实体表关系映射转换为 Mybatis 内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 Serializable 为任意类型主键 Mybatis-Plus 不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 Wrapper 为条件构造器
1
2
3
4
5
6
/**
* 订单 Mapper 接口
*/
public interface OrderMapper extends BaseMapper<Order> {
// 无需编写任何方法,继承 BaseMapper 即可使用通用的 CRUD 方法
}

OrderMapper 接口继承了 MyBatis-Plus 提供的 BaseMapper<Order> 接口,这意味着它会继承一系列通用的数据库操作方法,包括常见的查询、插入、更新、删除等 CRUD 操作。这样的设计遵循了 MyBatis-Plus 的规范,使得开发者无需手动实现这些通用的数据库操作,而是可以直接在 OrderMapper 接口中使用这些方法。

ServiceImpl<M extends BaseMapper<T>, T> 类中,M 是mapper对象,T 是实体。

ServiceImpl 是 MyBatis-Plus 提供的通用 Service 实现类。它已经实现了 IService 接口,包含了通用的 CRUD 方法的实现。在你的业务 Service 实现类中,可以直接继承 ServiceImpl,从而获得这些通用的数据库操作方法。

1
2
3
4
5
6
7
8
9
/**
* 订单 服务实现类
*/
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
implements IOrderService {
// 无需额外编写方法,继承 ServiceImpl 即可使用通用的 CRUD 方法
}

OrderServiceImplIOrderService接口的实现类,通过继承 ServiceImpl<OrderMapper, Order>,该类直接继承了 MyBatis-Plus 提供的默认 Service 实现,并指定了泛型参数为 OrderMapperOrder。因此,OrderServiceImpl 中无需额外编写方法,即可直接使用 ServiceImpl 中提供的通用的 CRUD 方法。

再根据项目看一下对应的继承关系图

img

既然ServiceImpl类也实现了IService接口,那么如果UserServiceImpl直接继承ServiceImpl类不就行了吗?为何还要自定义一个继承了IService接口的IUserService接口?

这是因为Spring自动注入要求是以接口为标准,在Controller里注入的Service要是一个接口才符合Spring的规范(当然注入类也行)!

Mapper和IService的使用场景

使用 Mapper的场景:

  1. Mapper 主要用于执行数据库的 CRUD 操作,包括查询、插入、更新和删除等底层数据库访问操作。
  2. 如果你有一些定制化的 SQL 需求,或者需要使用 MyBatis 的 XML 映射文件,那么直接使用 Mapper 可能更合适。你可以在 Mapper 接口中定义自己的 SQL 方法,并在 XML 文件中编写相应的 SQL 语句。
  3. 底层数据库访问: 如果你的操作更偏向于底层的数据库访问,例如需要直接操作数据库中的某个字段,或者使用一些特殊的 SQL 查询,那么直接使用 Mapper 会更直观和方便。使用 IService的场景:
  4. IService 主要用于定义业务逻辑层的接口,包括业务相关的操作方法。它提供了一些通用的业务逻辑方法,如保存、查询、更新等,更适用于业务操作。
  5. 如果你的操作涉及到事务,IService 提供了一些事务控制的方法,例如 saveOrUpdate,适合在业务逻辑层进行事务控制。
  6. IService 更抽象,更适用于高层次的业务操作。它对业务逻辑进行了封装,使得业务代码更清晰,易于维护。

组合使用:

在项目的一般开发流程中,先定义Mapper接口和对应的XML文件实现对数据库的操作,然后在Service层中注入Mapper接口的实例,并调用Mapper的方法来实现业务逻辑,提供更高层次的抽象和封装。

因此在项目开发中,通常会同时使用 MapperIService,将数据访问层和业务逻辑层分离。Mapper 用于处理底层数据库访问,而 IService 用于封装业务逻辑。这种组合使用的方式能够更好地利用 MyBatis-Plus 提供的功能,使代码结构更清晰,同时也便于单元测试和维护。

总结:

IService简直是BaseMapper的大扩充,不但包含了所有基本方法,还加入了很多批处理功能。

关于改进 IService 和 ServiceImpl 的建议

大家看了上面的分析,会不会还有点凌乱?Mybatis-Plus明明是一个Dao层的工具,为什么要提供 IService 呢?。应该是MP想让大家在写ServiceImpl的时候,直接继承MP的,如果项目的CRUD很简单,就不需要写DAO层代码了。有人在Github上提了issue https://github.com/baomidou/mybatis-plus/issues/5764,我个人认为是比较有道理的。

image-20240327195237107

image-20240327195251285

MyBatis-Plus的基本使用

为了避免和Service混淆的问题,我们在项目中如下命名ArticleDao,依然视为Dao层,而不是service层!

1
2
3
4
5
6
7
8
9
10
11
@Repository
public class ArticleDao extends ServiceImpl<ArticleMapper, ArticleDO> {
//如果用到就注入
@Resource
private ArticleMapper articleMapper;
}

public interface ArticleMapper extends BaseMapper<ArticleDO> {
}

mapper/ArticleMapper.xml

代码结构如下:

image-20240328100639652

DAO层提供的方法

以ArticleDAO为例,其他类似,不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//查询文章详情(最新版本)
public ArticleDTO queryArticleDetail(Long articleId);
//保存文章正文(设置version=1)
public Long saveArticleContent(Long articleId, String content);
//更新文章正文(update=true表示更新最后一条记录,false表示新插入一条记录)
public void updateArticleContent(Long articleId, String content, boolean update);
//文章列表查询(作者本人可以查看草稿、审核、上线文章,其他用户只能看到上线的文章)
public List<ArticleDO> listArticlesByUserId(Long userId, PageParam pageParam);
//根据categoryId查询文章列表
public List<ArticleDO> listArticlesByCategoryId(Long categoryId, PageParam pageParam);
//查询给定categoryId的文章数
public Long countArticleByCategoryId(Long categoryId);
//按照分类统计文章的数量
public Map<Long, Long> countArticleByCategoryId();
//根据指定的key搜索,返回文章列表
public List<ArticleDO> listArticlesBySearchKey(String key, PageParam pageParam);
//通过关键词,从标题中找出相似的进行推荐,只返回主键 + 标题
public List<ArticleDO> listSimpleArticlesByBySearchKey(String key);
//阅读计数
public int incrReadCount(Long articleId);
//统计用户的文章数
public int countArticleByUser(Long userId);
//热门文章推荐,按照阅读量排序
public List<SimpleArticleDTO> listHotArticles(PageParam pageParam);
//作者的热门文章排序
public List<SimpleArticleDTO> listAuthorHotArticles(long userId, PageParam pageParam);
//用户的文档年归档
public List<YearArticleDTO> listYearArticleByUserId(Long userId);

如何分页

新建一个PageParam类,作为数据库分页参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
* 数据库分页参数
*/
@Data
public class PageParam {

public static final Long DEFAULT_PAGE_NUM = 1L;
public static final Long DEFAULT_PAGE_SIZE = 10L;

public static final Long TOP_PAGE_SIZE = 4L;


@ApiModelProperty("请求页数,从1开始计数")
private long pageNum;

@ApiModelProperty("请求页大小,默认为 10")
private long pageSize;
private long offset;
private long limit;

public static PageParam newPageInstance() {
return newPageInstance(DEFAULT_PAGE_NUM, DEFAULT_PAGE_SIZE);
}

public static PageParam newPageInstance(Integer pageNum, Integer pageSize) {
return newPageInstance(pageNum.longValue(), pageSize.longValue());
}

public static PageParam newPageInstance(Long pageNum, Long pageSize) {
if (pageNum == null || pageSize == null) {
return null;
}

final PageParam pageParam = new PageParam();
pageParam.pageNum = pageNum;
pageParam.pageSize = pageSize;

pageParam.offset = (pageNum - 1) * pageSize;
pageParam.limit = pageSize;

return pageParam;
}

public static String getLimitSql(PageParam pageParam) {
return String.format("limit %s,%s", pageParam.offset, pageParam.limit);
}

}

辅助方法:

1
2
3
4
5
6
7
8
9
10
// 传入页数和pageSize,做一些校验,非法则使用默认值,返回PageParam实例
public PageParam buildPageParam(Long page, Long size) {
if (page <= 0) {
page = PageParam.DEFAULT_PAGE_NUM;
}
if (size == null || size > PageParam.DEFAULT_PAGE_SIZE) {
size = PageParam.DEFAULT_PAGE_SIZE;
}
return PageParam.newPageInstance(page, size);
}

返回对象:PageVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
* @author LouZai
* @date 2022/9/17
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageVo<T> {

private List<T> list;

private long pageSize;

private long pageNum;

private long pageTotal;

private long total;

/**
* 构造方法,int参数,需去除
*
* @param list
* @param pageSize
* @param pageNum
* @param total
* @return
*/
@Deprecated
public PageVo(List<T> list, int pageSize, int pageNum, int total) {
this.list = list;
this.total = total;
this.pageSize = pageSize;
this.pageNum = pageNum;
this.pageTotal = (int) Math.ceil((double) total / pageSize);
}

/**
* 构造PageVO
*
* @param list
* @param pageSize
* @param pageNum
* @param total
* @return
*/
public PageVo(List<T> list, long pageSize, long pageNum, long total) {
this.list = list;
this.total = total;
this.pageSize = pageSize;
this.pageNum = pageNum;
this.pageTotal = (long) Math.ceil((double) total / pageSize);
}

/**
* 创建PageVO
*
* @param list
* @param pageSize
* @param pageNum
* @param total
* @return PageVo<T>
*/
@Deprecated
public static <T> PageVo<T> build(List<T> list, int pageSize, int pageNum, int total) {
return new PageVo<>(list, pageSize, pageNum, total);
}

/**
* 创建PageVO
*
* @param list
* @param pageSize
* @param pageNum
* @param total
* @return PageVo<T>
*/
public static <T> PageVo<T> build(List<T> list, long pageSize, long pageNum, long total) {
return new PageVo<>(list, pageSize, pageNum, total);
}
}

返回的结果对象格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
public class ResVo<T> implements Serializable {
private static final long serialVersionUID = -510306209659393854L;
@ApiModelProperty(value = "返回结果说明", required = true)
private Status status;

@ApiModelProperty(value = "返回的实体结果", required = true)
private T result;


public ResVo() {
}

public ResVo(Status status) {
this.status = status;
}

public ResVo(T t) {
status = Status.newStatus(StatusEnum.SUCCESS);
this.result = t;
}

public static <T> ResVo<T> ok(T t) {
return new ResVo<T>(t);
}

@SuppressWarnings("unchecked")
public static <T> ResVo<T> fail(StatusEnum status, Object... args) {
return new ResVo<>(Status.newStatus(status, args));
}

public static <T> ResVo<T> fail(Status status) {
return new ResVo<>(status);
}
}

下面以获取tag列表为例:

调用入口:

1
2
3
4
5
6
7
8
9
10
//controller层
/**
* 查询所有的标签
*/
@GetMapping(path = "tag/list")
public ResVo<PageVo<TagDTO>> queryTags(@RequestParam(name = "key", required = false) String key,@RequestParam(name = "pageNumber", required = false, defaultValue = "1") Integer pageNumber,@RequestParam(name = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
PageVo<TagDTO> tagDTOPageVo = tagService.queryTags(key, PageParam.newPageInstance(pageNumber, pageSize));
//返回结果
return ResVo.ok(tagDTOPageVo);
}

调用service层:

1
2
3
4
5
6
7
//service层
public PageVo<TagDTO> queryTags(String key, PageParam pageParam) {
List<TagDTO> tagDTOS = tagDao.listOnlineTag(key, pageParam);
Integer totalCount = tagDao.countOnlineTag(key);
//构建PageVo
return PageVo.build(tagDTOS, pageParam.getPageSize(), pageParam.getPageNum(), totalCount);
}

调用DAO层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//DAO层
/**
* 获取已上线 Tags 列表(分页)
*/
public List<TagDTO> listOnlineTag(String key, PageParam pageParam) {
LambdaQueryWrapper<TagDO> query = Wrappers.lambdaQuery();
query.eq(TagDO::getStatus, PushStatusEnum.ONLINE.getCode())
.eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode())
.and(StringUtils.isNotBlank(key), v -> v.like(TagDO::getTagName, key))
.orderByDesc(TagDO::getId);
if (pageParam != null) {
//这里拼写sql语句的时候,会用到pageParam
query.last(PageParam.getLimitSql(pageParam));
}
List<TagDO> list = baseMapper.selectList(query);
return ArticleConverter.toDtoList(list);
}

public Integer countOnlineTag(String key) {
return lambdaQuery()
.eq(TagDO::getStatus, PushStatusEnum.ONLINE.getCode())
.eq(TagDO::getDeleted, YesOrNoEnum.NO.getCode())
.and(!StringUtils.isEmpty(key), v -> v.like(TagDO::getTagName, key))
.count()
.intValue();
}

参考

  1. https://juejin.cn/post/6844904096898482189
  2. https://cloud.tencent.com/developer/article/2362497
  3. https://github.com/baomidou/mybatis-plus/issues/5764
  4. https://blog.csdn.net/weixin_42516475/article/details/130115388
  5. https://baomidou.com/