一、关于MybatisPlus
MyBatis-Plus 是基于 MyBatis 的一款优秀的ORM(对象关系映射)框架,它在原有 MyBatis 功能上进行了封装和扩展,并提供了一些强大的增强功能,方便开发人员更加高效地开发数据访问层。
相比于 MyBatis,MyBatis-Plus改变和优化了以下几点:
MyBatis-Plus 提供了方便易用的代码生成器,可以快速生成包括实体类、Mapper接口、XML 文件等各个层次的代码,并支持多种常见数据库的自动驱动。通过这个生成器,开发人员可以少写很多重复的代码。
MyBatis-Plus 引入了 EntityWrapper 和 QueryWrapper 两个查询构造器,可以方便地进行复杂的 SQL 查询组装。支持链式调用,可以动态拼装 where、join、group by、order by 等 SQL 片段,大大提高了查询语句的可读性和灵活性。
MyBatis-Plus 提供了分页插件 PageHelper,可以方便地实现分页需求。可以通过简单的配置,在查询语句中添加 LIMIT 子句,并返回分页结果,避免了手工编写复杂的分页逻辑。
MyBatis-Plus 提供了自动填充功能,可以在插入或更新时自动填充一些公用字段,如创建人、创建时间等。通过配置全局处理器,可以方便地实现自动填充的逻辑。
MyBatis-Plus 提供了乐观锁插件,可以方便地实现基于版本号的乐观锁功能。使用简单,只需要在实体类中增加一个 version 字段,并配置相应的乐观锁插件即可。
MyBatis-Plus 在优化性能方面也做了很多工作,如缓存管理、批量操作、避免 N+1 查询等。同时,MyBatis-Plus 还提供了 SQL 语句分析器,可以分析查询语句的性能瓶颈,并给出优化建议。
总的来说,MybatisPlus是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
MybatisPlus关于通用查询能力的实现,有一个比较关键的接口BaseMapper,其中定义了表结构与数据实体之间的常用的方法:
public interface BaseMapper<T> extends Mapper<T> {
/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);
/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);
/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
/**
* 根据 entity 条件,删除记录
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
//省略其他通用方法
}
然而,通过定义的持久化操作接口继承了BaseMapper之后,就能直接使用常用的数据操作方法:
@Repository
public interface ActivityMapper extends BaseMapper<ActivityDO> {
}
在业务类中注入自己定义的Mapper,然后就能直接使用常用的insert、selectById和updateById等等方法了,然而我们并没有自己实现相关的sql.
@Service
public class MyBatisPlusTest {
@Autowired
ActivityMapper activityMapper;
public ActivityDO testSelect(Long id) {
return activityMapper.selectById(id);
}
}
当然,我们自己没有做相关实现,不代表框架没有做相关实现,我们通过定义数据实体类后,在应用启动时框架会解析相关属性,并且会帮我们生成接口代理以及通用方法的相关实现。
我们以官网的starter版本做分析,以mybatis-plus-boot-starter:3.5.1为例。
自动装配配置文件中指定了自动装配类MybatisPlusAutoConfiguration,我们看一下其对于通用方法注入能力支撑的关键配置:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
//...省略...
@Override
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(mybatisPlusPropertiesCustomizers)) {
mybatisPlusPropertiesCustomizers.forEach(i -> i.customize(properties));
}
checkConfigFileExists();
}
private void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
Assert.state(resource.exists(),
"Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
}
}
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
//...省略...
// TODO 此处必为非 NULL
GlobalConfig globalConfig = this.properties.getGlobalConfig();
// TODO 注入填充器
this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
// TODO 注入主键生成器
this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
// TODO 注入sql注入器
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
// TODO 注入ID生成器
this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
// TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
}
该自动装配类中有很多其他配置,此处不展开,重点看一下SqlSessionFactory的bean定义,在我们使用mybatis的时候直接使用的是SqlSessionFactory类型的bean,此处用mybatisPlus自己定义的MybatisSqlSessionFactoryBean替换了原有的bean。
从类继承关系可以看出,MybatisSqlSessionFactoryBean本质上是一个FactoryBean,并且拥有InitializingBean和ApplicationListener接口的能力,对于FactoryBean类型的bean在实例化的时候会调用其getObject方法获取,在初始化的时候会调用其实现的afterPropertiesSet方法:
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
SqlRunner.DEFAULT.close();
this.sqlSessionFactory = buildSqlSessionFactory();
}
MybatisSqlSessionFactoryBean本身持有SqlSessionFactory,此处做的事情是初始化SqlSessionFactory,在调用getObject方法的时候返回SqlSessionFactory:
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
继续看buildSqlSessionFactory方法:
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
//...省略...
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
//...省略...
final SqlSessionFactory sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(targetConfiguration);
// TODO SqlRunner
SqlHelper.FACTORY = sqlSessionFactory;
return sqlSessionFactory;
}
这里会解析Mapper配置文件和接口,调用XMLMapperBuilder的parse方法:
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
this.configurationElement(this.parser.evalNode("/mapper"));
this.configuration.addLoadedResource(this.resource);
this.bindMapperForNamespace();
}
//... 省略 ...
}
接着会调用私有bindMapperForNamespace方法:
private void bindMapperForNamespace() {
String namespace = this.builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException var4) {
}
if (boundType != null && !this.configuration.hasMapper(boundType)) {
this.configuration.addLoadedResource("namespace:" + namespace);
this.configuration.addMapper(boundType);
}
}
}
此处的configuration是MybatisPlus重写过的MybatisConfiguration,接着会调用其addMapper方法:
public class MybatisConfiguration extends Configuration {
@Override
public <T> void addMapper(Class<T> type) {
mybatisMapperRegistry.addMapper(type);
}
}
这里会调用MybatisPlus自己实现的MybatisMapperRegistry添加Mapper接口。
MybatisMapperRegistry集成自MapperRegistry类,重写了getMapper、addMapper等相关方法,前边有聊到调用MybatisMapperRegistry的addMapper方法添加Mapper接口,看一下实现:
@Override
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (hasMapper(type)) {
// TODO 如果之前注入 直接返回
return;
}
boolean loadCompleted = false;
try {
// TODO 这里也换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
knownMappers.put(type, new MybatisMapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// TODO 这里也换成 MybatisMapperAnnotationBuilder 而不是 MapperAnnotationBuilder
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
对比MapperRegistry的addMapper方法,此处的MapperAnnotationBuilder换成了MybatisPlus自己的MybatisMapperAnnotationBuilder实现:
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
if (this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
this.knownMappers.put(type, new MapperProxyFactory(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(this.config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
this.knownMappers.remove(type);
}
}
}
}
继续看MybatisMapperAnnotationBuilder的parse方法实现:
@Override
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
//...省略 ...
InterceptorIgnoreHelper.InterceptorIgnoreCache cache = 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(cache, 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();
}
先执行自定义Mapper接口本身定义的一些方法解析和sql绑定,然后检查是否继承了Mapper接口,如果是执行解析拦截:
void parserInjector() {
GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
}
此处根据Configuration获取SqlInjector,默认获取到的是DefaultSqlInjector,然后调用inspectInject方法进行通用方法与sql绑定。
DefaultSqlInjector是一个ISqlInjector,并持有抽象类AbstractSqlInjector相关能力,前边parserInjector方法会调用DefaultSqlInjector的inspectInject方法,实际会调用到抽象类AbstractSqlInjector的inspectInject方法:
@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);
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);
}
}
}
该方法先调用TableInfoHelper#initTableInfo方法获取表的相关描述信息,然后调用子类DefaultSqlInjector的getMethodList实现获取需要绑定的方法列表:
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
.add(new Insert())
.add(new Delete())
.add(new DeleteByMap())
.add(new Update())
.add(new SelectByMap())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectMapsPage())
.add(new SelectObjs())
.add(new SelectList())
.add(new SelectPage());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}
从这里可以看出,框架对通用方法做了抽象定义,与BaseMapper中的方法一一对应,继续看inspectInject,获取到方法列表后,遍历并调用inject方法进行sql与方法绑定。
通用方法抽象出来的类都继承了AbstractMethod类并实现了injectMappedStatement方法,我们以SelectById为例进行分析:
public class SelectById extends AbstractMethod {
public SelectById() {
super(SqlMethod.SELECT_BY_ID.getMethod());
}
public SelectById(String name) {
super(name);
}
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
SqlSource sqlSource = new RawSqlSource(configuration, String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(), tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true)), Object.class);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
}
然后会调用到AbstractMethod的addMappedStatement方法添加 MappedStatement 到 Mybatis容器:
protected MappedStatement addMappedStatement(Class<?> mapperClass, String id, SqlSource sqlSource,
SqlCommandType sqlCommandType, Class<?> parameterType,
String resultMap, Class<?> resultType, KeyGenerator keyGenerator,
String keyProperty, String keyColumn) {
String statementName = mapperClass.getName() + DOT + id;
if (hasMappedStatement(statementName)) {
logger.warn(LEFT_SQ_BRACKET + statementName + "] Has been loaded by XML or SqlProvider or Mybatis's Annotation, so ignoring this injection for [" + getClass() + RIGHT_SQ_BRACKET);
return null;
}
/* 缓存逻辑处理 */
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
return builderAssistant.addMappedStatement(id, sqlSource, StatementType.PREPARED, sqlCommandType,
null, null, null, parameterType, resultMap, resultType,
null, !isSelect, isSelect, false, keyGenerator, keyProperty, keyColumn,
configuration.getDatabaseId(), languageDriver, null);
}
最终会调用Mybatis的MapperBuilderAssistant工具类将MappedStatement添加到Mybatis容器并与方法签名进行绑定。
这样我们定义的Mapper在经过上述步骤后就变成了一个完整的bean供业务调用了,当然这里忽略了资源加载、数据连接处理等动作,这些和Mybatis也没有大的结构上的变更,就通用方法注入而言,整个链路大致如下:
对于MybatisPlus提供的BaseMapper 是一个通用的 Mapper 接口,主要用于解决数据访问层的常见操作,提供了一系列常用的数据库操作方法,可以大大简化开发人员编写 CRUD(增删改查)操作的工作量。
在体验到便利的同时,我们也应该用结构化思维去考虑一下这样的设计以及在常见的框架和设计模式中的使用,以Mybatis这种中间件为例,他们提供的是一种通用的或者模板化的能力,我们可以根据自己的诉求自己定一些能力板块,交给模板去运作形成业务个性化能力。
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!