1分钟,快速入门MyBatis

hey, 大家好啊.这是哪儿来着?

1分钟,快速入门MyBatis

前几天写过一篇《如何同时使用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.classnull),
                        SqlCommandType.SELECT)
                        .resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", Integer.classCollections.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.classnull),
                SqlCommandType.SELECT)
                .resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", HashMap.classCollections.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.classnull),
                SqlCommandType.SELECT)
                .resultMaps(Lists.newArrayList(new ResultMap.Builder(configuration, "", A.classCollections.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)]

这里的逻辑简单, 就是传递两个参数, 返回他们的加减乘除结果, 不过代码变化比较大:

  1. 有了参数
  2. 返回类型用了一个类
  3. 返回的结果是多行, 这里用了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, 都是交给SqlSourceBuilderparse出来, 得到的结果, 是SqlSource接口的实现.

在这里, SqlSource 就是一种根据sql 和参数, 进行变化,返回 BoundSql 的接口. 官方实现里是这几个:

1分钟,快速入门MyBatis

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.classCollections.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 aInteger 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方式来做. 下边这个, 就是一个奇怪的, 用springjdbcTemplate执行mybatis生成出来的sql的执行器插件.


@Intercepts({
//        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.classmethod "query", args = {MappedStatement.classObject.classRowBounds.classResultHandler.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做个简介. 有机会话看看别的什么具体内容比较有趣, 再详细介绍下.1分钟,快速入门MyBatis

对了, 你知道么?常用框架里,mybatis是代码量最小的, 只有2万行. 而这2万行, 核心功能可能4000行就拿下了.但真正做到好用易用,则需要剩下的代码. 这是一件非常符合二八原则的事. mybatis当年还叫ibatis时候,他的包名,就是 sqlmap.

如果对mybatis的史前故事感兴趣话, 也可以参考下以前写的这篇 《猫猫狗狗和MyBatis有什么关系》


1分钟,快速入门MyBatis


索尔走了

原文始发于微信公众号(K字的研究):1分钟,快速入门MyBatis

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/29683.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!