一分钟进入mybatis的世界

2022-06-20 20:13:25 浏览数 (1)

mybatis是一款优秀的持久层框架, 通过配置mybatis-config.xml和mapper.xml就可以轻松完成ORM工作. 在当前流行JavaCodeConfig的情况下, 这些配置项又是如何应用配置的呢? 它的执行过程又是怎样的呢? 带着这两个问题, 一起看下mybatis的真面目.

一. mybatis的配置与使用

1.1

字符集

首先我们看下这个全局配置文件, 它是mybatis的核心配置,包括数据源, setting, 自定义类型转化, 以及mapper文件等等.

这个配置文件命名是不限定的, 一般会被命名为mybatis-config.xml

代码语言:javascript复制
<configuration>
    <settings>
        <setting name="logImpl" value="LOG4J"/>
    </settings>
     <typeAliases>
        <package name="com.coderworld968.model"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC" />
            <dataSource type="UNPOOLED">
                <property name="driver" value="org.h2.Driver"/>
                <property name="url" value="jdbc:h2:mem:voice;INIT=RUNSCRIPT FROM 'classpath:/h2/schema-h2.sql'"/>
                <property name="username" value="sa"/>
                <property name="password" value="sa"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/CountryMapper.xml"/>
    </mappers>
</configuration>

全局配置有了, 再看下mapper.xml中的SQL.

代码语言:javascript复制
<mapper namespace="com.coderworld968.mapper">
 <select id="selectAll" resultType="Country">
  select id,countryname,countrycode from country
 </select>
</mapper>

mybatis的本质是简化SQL处理, 那有了配置文件, 再看如何使用? 使用步骤非常简单, 创建sqlSessionFactory和根据mapper.xml中的sql ID执行对应SQL.

(1) 读取全局配置文件,创建sqlSessionFactory

代码语言:javascript复制
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

(2) 执行SQL

sqlSessionFactory会创建SqlSession对象, 并调用mapper.xml中的SQL ID执行对应SQL.

代码语言:javascript复制
SqlSession sqlSession = sqlSessionFactory.openSession();
List<Country> countryList = sqlSession.selectList("selectAll");

二. mybatis的配置解析

在上述创建sqlSessionFactory时, 跟进builder()方法就会发现逻辑很简单,就是解析mybatis-config.xml配置文件, 并最终映射成一个Configuration对象.

代码语言:javascript复制
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
// ...
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
// ...
}

那我们完全可以抛开XML配置文件, 直接创建Configuration对象.

代码语言:javascript复制
public static void init(){
try {
    UnpooledDataSource dataSource = new UnpooledDataSource(
            "org.h2.Driver",
            "jdbc:h2:mem:voice;INIT=RUNSCRIPT FROM 'classpath:/h2/schema-h2.sql'",
            "sa",
            "sa");
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("Java", transactionFactory, dataSource);

    Configuration configuration = new Configuration(environment);
    configuration.getTypeAliasRegistry().registerAliases("com.coderworld968.model");
    configuration.setLogImpl(Log4jImpl.class);

    InputStream inputStream = Resources.getResourceAsStream("mapper/CountryMapper.xml");
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "mapper/CountryMapper.xml", configuration.getSqlFragments());
                mapperParser.parse();

    sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
} catch (Exception ignore) {
    ignore.printStackTrace();
}
}

Mapper的注册方式除了示例中解析xml配置文件外,还有包扫描, 指定类等方式, 非常利于mybatis的扩展使用.

代码语言:javascript复制
public void addMappers(String packageName, Class<?> superType)
public void addMappers(String packageName)
public <T> void addMapper(Class<T> type)

通过这种无配置文件, 纯代码配置的方式也就能明白spring或者spring boot是如何封装mybatis的了.

与示例代码中有TypeAliasRegistry处理类型别名(TypeAlias)类似的,也会有MapperRegistry,TypeHandlerRegistry,LanguageDriverRegistry,InterceptorChain对相应的功能做处理.

稍稍扩展下mybatis中对常见数据类型的别名(TypeAlias)处理方式是在类的构造方法中完成的. 类似的, TypeHandler处理也是在构造方法中处理的.

代码语言:javascript复制
public TypeAliasRegistry() {
  registerAlias("string", String.class);
  registerAlias("byte", Byte.class);
  registerAlias("long", Long.class);
  // ...
}

三. mybatis的SQL执行

3.1

MappedStatement对象

在解析配置文件(或JavaCodeConfig)中的mapper部分时, 不仅会记录SQL本身, 还会结合Configuration配置项生成对应的MappedStatement对象. 创建的MappedStatement对象会存储到Configuration.mappedStatements集合中.

代码语言:javascript复制
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")

mappedStatement在存储时, KEY会根据mapper配置分为两种:namespace SQLID 和 SQLID. 这也是示例中执行查询时, 没有指定namespace也可以找到对应sql的原因了.

代码语言:javascript复制
sqlSession.selectList("selectAll")

3.2

SqlSession

SqlSession是从sqlSessionFactory中创建的,主要用来处理SQL执行, 事务等功能. SqlSession中主要包含四个对象:

(1)Executor: 调度执行StatementHandler、ParameterHandler、ResultHandler执行相应的SQL语句;

(2)StatementHandler: 使用数据库中Statement(PrepareStatement)执行操作,即底层是封装好了的prepareStatement;

(3)ParameterHandler: 处理SQL参数;

(4)ResultHandler: 结果集ResultSet封装处理返回;

3.3

SQL执行

下面结合源码一起看下sqlSession.selectList("selectAll")的执行流程

(1) Configuration创建SqlSession, 其中包含封装了拦截器(interceptorChain)的Executor. 拦截器的实现原理可以点这里. 如果是开启了缓存, 还会再封装成CachingExecutor.

代码语言:javascript复制
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// ...
  executor = new SimpleExecutor(this, transaction);
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

(2) 从Configuration中获得MappedStatement对象,并运行执行器(CachingExecutor)执行query()操作

代码语言:javascript复制
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
// ...
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
// ...
}

(3) 解析SQL和参数, 继续调用query()方法

代码语言:javascript复制
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

(4) 经过多级调用, 会执行SimpleExecutor.doQuery(), 生成StatementHandler对象, 并封装java.sql.Statement对象

代码语言:javascript复制
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
// ...
  Configuration configuration = ms.getConfiguration();
  StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
  stmt = prepareStatement(handler, ms.getStatementLog());
  return handler.query(stmt, resultHandler);
// ...
}

(5) StatementHandler对象是通过Configuration生成的RoutingStatementHandler类型, 内部根据MappedStatement.getStatementType()会选择具体不同的handler. 本例中用到的是PreparedStatementHandler.

代码语言:javascript复制
public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  switch (ms.getStatementType()) {
    case STATEMENT:
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: "   ms.getStatementType());
  }
}

(6) PreparedStatementHandler执行query()操作, 并根据resultSetHandler封装SQL执行结果集.

代码语言:javascript复制
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  return resultSetHandler.handleResultSets(ps);
}

以上就是mybatis的针对一个SQL的主要处理流程.

总结

通过上述流程,大家对mybatis也有了一定的了解, 由于篇幅原因, 文中并没有提到mapper接口相关的动态代理处理, 后续会完善动态代理的详细介绍及使用.

0 人点赞