hey, 大家好啊.这是哪儿来着?
前几天写过一篇《如何同时使用5种方式操作数据库》, 里面提到过 mybatis
. 今天来深入一点点.如果想读mybatis源码不知道从哪里下手的话, 可以试试我这个陈述顺序.
SQLSession
平时mybatis
大多都是用类似下面的代码来使用的:
mapper.selectByBalabala()
但本质上, 这里只是一个代理. 实际执行中, 还是要交给SQLSession
来执行.
sqlSession.selectList("mapper.selectByBalabala")
mybatis的核心逻辑, 就是一个大号的HashMap<String,SQL>
执行SQL, 就是按名字去查询sql并执行.
接口形式的mapper
,只是用来生成类名+方法名
的sqlId
. 最后, 都要走到SQLSession
的字符串接口上去.
知道这一点以后, 我们就有了很多灵活的操作空间, 比如,动态,从配置文件里添加一部分sql进去.
Configuration
大号map的存放位置, 是Configuration
, 可以直接从SQLSessionFactory
里获取他.
@Slf4j
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SpringBootTest
class AppTest {
@Autowired
SqlSessionFactory sqlSessionFactory;
@BeforeAll
void contextLoads() {
final Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addMappedStatement(
new MappedStatement.Builder(
configuration,
"value",
new SqlSourceBuilder(configuration).parse("select 1 as a", Void.class, null),
SqlCommandType.SELECT)
.resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", Integer.class, Collections.emptyList()).build()))
.build());
}
@Test
void test() {
try (final SqlSession sqlSession = sqlSessionFactory.openSession()) {
final Object value = sqlSession.selectOne("value");
log.info("single value: {}", value);
// single value: 1
}
}
}
这段代码, 就是最简单的一个动态添加sql语句的方式. 他加一个叫value
的sqlselect 1 as a
, 返回类型是Integer
.
ResultType
前面语句里,返回值是只取了1, as a
这部分的a
,被忽略掉了.接下来我们还是这个sql,但是换个返回, 让他返回Map
格式:{"a":1}
.
控制这部分特性的, 就是ResultType
.
configuration.addMappedStatement(
new MappedStatement.Builder(
configuration,
"map",
new SqlSourceBuilder(configuration).parse("select 1 as a", Void.class, null),
SqlCommandType.SELECT)
.resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", HashMap.class, Collections.emptyList()).build()))
.build());
final Object map = sqlSession.selectOne("map");
log.info("result as map: {}", map);
//result as map: {a=1}
偷懒起见, 这里只放了新加的代码,添加到前面代码里即可, 返回接口就变了.
resultMaps
这部分, 是用来控制sql执行完以后的Jdbc结果怎么映射到具体的类的,可以写的非常复杂.不过一般情况下, 可以简写.
参数
前面的例子,把参数都省略掉了, 都是无参的sql. 下面给个有参数的:
@Data
public static class A {
Integer a;
Integer b;
}
configuration.addMappedStatement(
new MappedStatement.Builder(
configuration,
"m4",
new SqlSourceBuilder(configuration).parse("select #{a}+#{b} as a,#{a}-#{b} as b union select #{a}*#{b} as a,#{a}/#{b} as b", Void.class, null),
SqlCommandType.SELECT)
.resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", A.class, Collections.emptyList()).build()))
.build());
final Object m4 = sqlSession.selectList("m4", Map.of("a", 1, "b", 2));
log.info("result m4: {}", m4);
//result m4: [AppTest.A(a=3, b=-1), AppTest.A(a=2, b=0)]
这里的逻辑简单, 就是传递两个参数, 返回他们的加减乘除结果, 不过代码变化比较大:
-
有了参数 -
返回类型用了一个类 -
返回的结果是多行, 这里用了 selectList
, 继续用selectOne
遇到多行时候会报错
mybatis的传参还是比较简单的, 可以假装他把所有的参数打包成了一个Map
, 用#{}
引用他们即可(细节上很乱,不提了,免得迷路).
不过这里提一点, 因为老版本的Java,编译过程中,会把变量名扔掉, mybatis早年版本是要求用@Param
注解的.如果用的Java8版本,编译过程中可以要求maven插件
保留参数.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>8</source>
<target>8</target>
<compilerArgument>-parameters</compilerArgument>
</configuration>
</plugin>
SqlSource
前面的SQL, 都是交给SqlSourceBuilder
来parse
出来, 得到的结果, 是SqlSource
接口的实现.
在这里, SqlSource 就是一种根据sql 和参数, 进行变化,返回 BoundSql
的接口. 官方实现里是这几个:
DynamicSqlSource VS RawSqlSource
mybatis里的sql map, 并不是一对一的. 一个SQLID具体对应什么SQL语句, 可能会随着参数变化,也就是平时常用的动态sql功能.
如果在xml
里编写的SQL有动态属性, 或者@Select("<script> xxxx</script>")
之类的写法, 会产生前者动态sql源, 否则用原生sql源.
ProviderSqlSource
这个是注解方式常用的数据源, 如果写成@SelectProvider()
格式接口,可以传递一个Java类给mybatis做回调, 会用这个执行Java来生成SQL.
StaticSqlSource
静态sql源是最简单的, 不对sql做任何处理, 看构造器就能明白.
public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
this.sql = sql;
}
当然, 不局限于这些, 我们可以随意添加实现, 比如下面这个sql, 双休日调用会抛异常.
configuration.addMappedStatement(
new MappedStatement.Builder(
configuration,
"work",
parameterObject -> {
final boolean weekend = LocalDate.now().getDayOfWeek().getValue() > DayOfWeek.FRIDAY.getValue();
if (weekend) throw new RuntimeException();
return new BoundSql(configuration, String.format("select 'work'"), Collections.emptyList(), parameterObject);
},
SqlCommandType.SELECT)
.resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", String.class, Collections.emptyList()).build()))
.build());
不过, 一般来说默认的几个够用了.
接口, 还是接口
前面用的都没有使用Mapper
接口, 现在来讨论下Mapper
接口.
@Mapper
public interface TestMapper {
@Select("SELECT 1")
Integer one();
@Select("<script> select #{a} <if test='b!=1'>+ #{b}</if> </script>")
Integer add(Integer a, Integer b);
@SelectProvider(type = DoubleOrDivSqlProvider.class)
Integer doubleOrDivide(Integer a, Integer b);
class DoubleOrDivSqlProvider {
public String provideSql(ProviderContext context, Map<String, Integer> param) {
final Integer b = param.get("b");
return b % 2 == 0 ? "SELECT #{a} * 2" : "SELECT #{a} /2";
}
}
}
addMappedStatement
还是属于比较低的接口, 再高一点, 可以直接调用configuration.addMapper
. 启动过程中, 这个接口会被解析, 然后生成对应的sqlId
,MappedStatement
,配上相应的SqlSource.addMapper
方法会调用别的类来解析参数,最终走到addMs
上去.
-
第一个是 StaticSqlSource
-
第二个是 DynamicSqlSource
-
第三个是 ProviderSqlSource
最后, 组装好的信息会放到Map
里. 想看细节, 可以在这里org.apache.ibatis.session.Configuration#addMappedStatement
, 打断点验证下.
LanguageDriver
这里有一个小问题, sql的核心, SqlSource
创建过程是自动的. 如果想深度干预各种SqlSource
的生成, 有相应的扩展机制: LanguageDriver
.
@Lang(KLanguageDriver.class)
@SelectProvider(SimpleSelectProvider.class)
K k();
class KLanguageDriver implements LanguageDriver {
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return null;
}
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
return null;
}
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
return null;
}
}
看到这接口里的几个写法, 其实就很明显, 我们可以在这里对SqlSource进行干预, 还是挺好玩的.
Executor
mybatis作为一个sql框架, 执行过程用的接口, 还是要提一下的,主要用的是Executor
接口.
Executor
接口, 有多种实现, 常用以SimpleExecutor
为最多, 其他还有BatchExecutor
,CachingExecutor
等.
这个执行器也可以自定义, 使用自己的执行功能, 不过需要用plugin
方式来做. 下边这个, 就是一个奇怪的, 用spring
的jdbcTemplate
执行mybatis生成出来的sql的执行器插件.
@Intercepts({
// @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class MybatisFullSqlLog implements Interceptor {
@Autowired
JdbcTemplate jdbcTemplate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object arg = args[1];
BoundSql boundSql = ms.getBoundSql(arg);
String sql = boundSql.getSql();
final ArrayList<Object> params = new ArrayList<>();
if (arg != null) {
if (ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(arg.getClass())) {
params.add(arg);
} else {
MetaObject parameterObject = ms.getConfiguration().newMetaObject(boundSql.getParameterObject());
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
Object value = null;
if (parameterObject.hasGetter(mapping.getProperty()))
value = parameterObject.getValue(mapping.getProperty());
else if (boundSql.hasAdditionalParameter(mapping.getProperty())) {
value = boundSql.getAdditionalParameter(mapping.getProperty());
}
params.add(value);
}
}
}
Class<?> returnType = ms.getResultMaps().get(0).getType();
final List<?> objects = jdbcTemplate.queryForList(sql, returnType, params.toArray());
return objects;
}
}
这插件就是一种aop, spring里学足够了,犯不上在这地方学.我试了下, 凑合能跑.
后话
今天就到这里, 大致对mybatis做个简介. 有机会话看看别的什么具体内容比较有趣, 再详细介绍下.
对了, 你知道么?常用框架里,mybatis是代码量最小的, 只有2万行. 而这2万行, 核心功能可能4000行就拿下了.但真正做到好用易用,则需要剩下的代码. 这是一件非常符合二八原则的事. mybatis当年还叫ibatis时候,他的包名,就是 sqlmap.
如果对mybatis的史前故事感兴趣话, 也可以参考下以前写的这篇 《猫猫狗狗和MyBatis有什么关系》。
索尔走了
原文始发于微信公众号(K字的研究):1分钟,快速入门MyBatis
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/29683.html