MybatisPlus BaseMapper 实现对数据库增删改查源码
作者:myboy 发布时间:2024-01-26 10:03:57
MybatisPlus 是一款在 Mybatis 基础上进行的增强 orm 框架,可以实现不写 sql 就完成数据库相关的操作。普通的 mapper 接口通过继承 BaseMapper 接口,即可获得增强,如下所示:
public interface UserMapper extends BaseMapper<User> {
}
接下来就对其源码一探究竟,看看他到底是如何实现的
环境搭建
1、使用 h2 数据库,方便测试,导入相关依赖
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.1'
implementation 'com.baomidou:mybatis-plus-boot-starter:3.5.3.1'
implementation 'org.projectlombok:lombok:1.18.24'
implementation 'com.h2database:h2:1.4.200'
}
2、springboot 配置文件
spring:
datasource:
driver-class-name: org.h2.Driver
username: root
password: test
sql:
init:
schema-locations: classpath:db/schema-h2.sql
data-locations: classpath:db/data-h2.sql
3、resources 目录下新建 db 目录,创建 sql 文件
schema-h2.sql
DROP TABLE IF EXISTS demo_user;
CREATE TABLE demo_user
(
id int primary key,
name varchar,
age int,
email varchar
);
data-h2.sql
DELETE
FROM demo_user;
INSERT INTO demo_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');
4、编写 mapper 文件
public interface UserMapper extends BaseMapper<User> {
}
5、启动测试
@MapperScan("org.example.mapper")
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Main.class, args);
UserMapper userMapper = context.getBean(UserMapper.class);
System.out.println(userMapper.selectList(null));
}
}
结果如下
[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)]
从 @MapperScan 入手
@MapperScan 注解的作用是扫描指定 mapper 接口所在的包,并生成接口的代理对象,注入到 ioc 容器中,接口定义如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
}
可以看到 Import 了个 MapperScannerRegistrar,点进去看看这个类做了什么
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
// 注册一个 beanDefinition
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// 注册MapperScannerConfigurer的BeanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// ......
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
这个 importRegister 注册了一个 MapperScannerConfigurer,这个类是个 BeanDefinitionRegistryPostProcessor,核心逻辑就是在这个类中,即扫描指定 mapper 接口所在的包,并生成接口的代理对象,注入到 ioc 容器中,查看该类的 postProcessBeanDefinitionRegistry() 方法
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 设置一些scanner参数
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
// ......
// 扫描mapper接口
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
进入父类 scan 方法,发现核心方法是子类的 doScan(), 来到 MapperScannerConfigurer.doScan()
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 拿到扫描到的 beanDefinition
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
// 处理 mapper beanDefinition
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
核心在 processBeanDefinitions(beanDefinitions) 中
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
// 设置该BeanDefinition的beanClass是 MapperFactoryBean
definition.setBeanClass(this.mapperFactoryBeanClass);
// ......
// 设置该MapperFactoryBean 中的 sqlSessionTemplateBeanName
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
}
// ......
}
}
通过这一系列源码,可以知道,@MapperScan 指定的包在 MapperScannerConfigurer 被扫描成 BeanDefinition, 并且修改了 BeanDefinition 的 beanClass 属性为 MapperFactory,这样 spring 实例化 UserMapper 单例 bean 时,会生成对应的 MapperFactory
看看这个 MapperFactory 是什么鬼
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
}
这个类是个 FactoryBean,那么它的 getObject() 方法就是调用 sqlSessionTemplate 的 getMapper() 方法获取代理对象,关于这个 getMapper() 方法的解析,可以参考我之前写的《Mybatis 通过接口实现 sql 执行原理解析》
到这里,MapperFactory 生成的 bean 被放到了 ioc 容器中,结束了吗?我们忽略了 MapperFactory 的父类 SqlSessionDaoSupport,下面一节来看看这个父类 SqlSessionDaoSupport 做了什么
SqlSessionDaoSupport
这个类看名字是给 Dao 做支持的,Dao 指的就是那个 mapper 接口,做什么支持?其实给就是给 BaseMapper 里定义的方法生成对应的 Statemnet,注册到 MybatisMapperRegistry 中,这样调用 BaseMapper 方法时,代理类就会从 MybatisMapperRegistry 中找到 Statemnet,这样可以取出 sql 执行了,来看源码,其他都是抽象方法,只有一个初始化方法
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// 让子类处理
checkDaoConfig();
// Let concrete implementations initialize themselves.
try {
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException("Initialization of DAO failed", ex);
}
}
调用了抽象方法,子类实现了 checkDaoConfig(),来看下 MapperFactoryBean.checkDaoConfig()
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 解析这个 mapper 方法
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
看到 configuration.addMapper(this.mapperInterface) 方法,相信看过 mybatis 源码的小伙伴们已经知道要干什么了吧。就是解析这个 mapper 类方法,找到对应的 sql,并封装成 statemnet,下面看看这个 configuration.addMapper(this.mapperInterface) 的实现逻辑吧
MybatisConfiguration.addMapper()
因为是 MybatisPlus,所以源码内部的 Configuration 类是 MybatisConfiguration,查看他的 addMapper() 方法源码
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
再进入 mybatisMapperRegistry.addMapper(type) 源码
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
}
boolean loadCompleted = false;
try {
// TODO 注册mapper类对应的代理工厂类,用于生成代理对象
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
// 解析mapper类,生成 statement
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
进入 parse() 方法查看
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper.xml
loadXmlResource();
configuration.addLoadedResource(resource);
String mapperName = type.getName();
assistant.setCurrentNamespace(mapperName);
// 解析缓存
parseCache();
parseCacheRef();
IgnoreStrategy ignoreStrategy = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// TODO 加入 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(ignoreStrategy, mapperName, method);
parseStatement(method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql
try {
// https://github.com/baomidou/mybatis-plus/issues/3038
if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
parserInjector();
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new InjectorResolver(this));
}
}
parsePendingMethods();
}
关注最后注释,注入 CRUD 动态 SQL,其实就是给 BaseMapper 里的方法创建对应的 Statement,查看内部逻辑:
void parserInjector() {
// DefaultSqlInjector.inspectInject();
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
这里先获取到默认的 Sql 注入器 DefaultSqlInjector,再调用其 inspectInject() 方法注入 sql
@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
Class<?> modelClass = ReflectionKit.getSuperClassGenericType(mapperClass, Mapper.class, 0);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
// 根据实体类,根据注解解析出表的信息
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
// 拿到所有的AbstractMethod实现类
List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
if (CollectionUtils.isNotEmpty(methodList)) {
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
mapperRegistryCache.add(className);
}
}
}
这里面的 AbstractMethod 的实现类有很多,如下
可以说,BaseMapper 中每个方法都有一个对应的 AbstractMethod 实现类,以 selectList() 为例,可以找到 SelectList 类
在下面循环注入的地方:methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo))
, 进入 AbstractMethod.inject() 方法
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
子类实现了 injectMappedStatement 方法,还是以 SelectList 为例
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// selectList sql 模版
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
// 格式化sql
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo), sqlOrderBy(tableInfo), sqlComment());
// 封装成 sqlSource
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 注册 mapperStatement
return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, tableInfo);
}
其中 sqlSelectColumns(tableInfo, true) 方法是构造出 select 的所有列名,并加上动态sql标签
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,age,email</otherwise>
</choose>
其中 sqlWhereEntityWrapper(true, tableInfo) 方法是构造出 where 后面的条件语句,并加上动态sql标签
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null"> AND name=#{ew.entity.name}</if>
<if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if>
最后 format 后的 sql 语句是
<script>
<if test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</if> SELECT <choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,age,email</otherwise>
</choose> FROM demo_user
<if test="ew != null">
<where>
<if test="ew.entity != null">
<if test="ew.entity.id != null">id=#{ew.entity.id}</if>
<if test="ew.entity['name'] != null"> AND name=#{ew.entity.name}</if>
<if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
<if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal"> AND</if> ${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if> <if test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</if>
</script>
最后是把 sql 封装成了 SqlSource,并构造 MapperStatement 存入 configuration.mappedStatements 中,后面 mapper 调用 selectList 方法时,会从 mappedStatements 中找到对应的 statement,并取出 sql 语句执行,就能拿到数据了
小结
到此,MybatisPlus BaseMapper 实现对数据库增删改查源码解析完毕,相信通过源码的阅读能对 mybatisPlus 有更深的了解
来源:https://juejin.cn/post/7193715829614542905


猜你喜欢
- 我们一般在Excel里面是使用数据连接属性里面写sql语句,或者vba里面利用ado组件执行sql语句。新版的Excel里面带上了Power
- 成果库修改: 要求主题列表随成果类型改变而改变  
- 本文主要通过实例介绍了scrapy框架的使用,分享了两个例子,爬豆瓣文本例程 douban 和图片例程 douban_imgs ,具体如下。
- 本文介绍在Anaconda环境下,安装Python中栅格、矢量等地理数据处理库GDAL的方法。  需要注
- 函数内省(function introspection)除了__doc__属性, 函数对象还有很多属性,对于下面的函数,可以使用dir()查
- jupyter中显示的DataFrame过长时会自动换行(print()显示方式)或自动省略(单元格最后一行直接显示),在一些情况下看上去不
- Python 中的可变和不可变对象一、文字描述可变和不可变对象在 Python 中,一切皆为对象Python 中不存在值传递,一切传递的都是
- 为了自定义一个模板标签,你需要告诉Django当遇到你的标签时怎样进行这个过程。当Django编译一个模板时,它将原始模板分成一个个 节点
- Jinja是组成Flask的模板引擎。可能你还不太了解它是干嘛的,但你对下面这些百分号和大括号肯定不陌生:{% block body %}
- 本文将介绍如何使用 Vue 和第三方组件库 Element UI 实现轮播图功能。我们将从以下几个方面进行讲解:安装 Element UI创
- 一、背景起源于一个问题:怎样找到字符串中出现次数最多的字符其实使用max函数就能很轻松的解决这个问题:代码:str1 = "AAA
- 前言ppi-cpi 剪刀差大家可能都听说过,通过这个指标可以了解当前的经济运行状况,小编为了学习 python 的图形绘制,通过爬
- 使用python进行图片处理,现在需要读出图片的任意一块区域,并将其转化为一维数组,方便后续卷积操作的使用。 下面使用两种方法进行处理:co
- 本文主要介绍了OpenCV实现背景分离(证件照背景替换),具有一定的参考价值,感兴趣的可以了解一下实现原理图像背景分离是常见的图像处理方法之
- 文件名称:ByVal.aspByRef.asp具体代码:<%Sub TestMain()Dim A : A=5Call TestBy(
- URLURL 是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有
- 本文介绍基于Python语言,按照一定命名规则批量修改多个文件的文件名的方法。已知现有一个文件夹,其中包括班级所有同学上交的作业文件,每人一
- 本文实例为大家分享了js实现简单贪吃蛇效果的具体代码,供大家参考,具体内容如下上代码之前,先给大家看一下效果:是不是想说:我能这样玩一天…话
- 破解百度翻译翻译是一件麻烦的事情,如果可以写一个爬虫程序直接爬取百度翻译的翻译结果就好了,可当我打开百度翻译的页面,输入要翻译的词时突然发现
- 1.以前的方法如果是要获得程序运行的当前目录所在位置,那么可以使用os模块的os.getcwd()函数。如果是要获得当前执行的脚本的所在目录