Open Source, Open Future!
  menu
107 文章
ღゝ◡╹)ノ❤️

mybatis 置顶!

重要接口

SqlSessionFactoryBuilder

用于创建 SqlSessionFactory对象。

SqlSessionFactory

用于创建 SqlSession对象,每一个数据库最好只对应一个 SqlSessionFactory,以避免过多消耗。

类图如下:

image.png

SqlSession

代表一次会话,相当于 JDBC中的 Connection
作用:

  • 获取映射器,让映射器通过命名空间和方法名称找到对应的SQL,发送给数据库后返回结果;
  • 直接通过命名信息去执行SQL返回结果(ibatis版本的方式)。

类图如下:

image.png

Mapper

映射器是由java接口和XML文件(或注解)共同组成的。

Executor

执行器,用来调度 StatementHandlerParameterHandlerResultHandler等来执行对应的 SQL

类图如下:

image.png

说明
SimpleExecutor简单执行器
ReuseExecutor重用预处理语句的执行器
BatchExecutor针对批量操作的执行器
CachingExecutor若开启了缓存,则对以上三种执行器做封装

配置方式:

    <settings>
        <setting name="defaultExecutorType" value="SIMPLE"/>
    </settings>

defaultExecutorType默认 SIMPLE,可取值如下:

对应类
SIMPLESimpleExecutor
REUSEReuseExecutor
BATCHBatchExecutor
StatementHandler

数据库会话器,执行 CRUD等操作。

类图如下:

image.png

说明
SimpleStatementHandler对应 JDBCStatement
执行简单的 SQL
PreparedStatementHandler对应 JDBCPreparedStatement
执行预编译的 SQL
CallableStatementHandler对应 JDBCCallableStatement
执行存储过程
RoutingStatementHandler负责创建和调用以上三种会话器

配置方式:

<select id="queryById" resultType="user" statementType="PREPARED">
    SELECT `id`,`name`,`age` FROM `user` WHERE `id` = #{id}
</select>

statementType默认 PREPARED,可取值如下:

对应类
STATEMENTSimpleExecutor
PREPAREDPreparedStatementHandler
CALLABLECallableStatementHandler

主要方法:

方法名说明
prepareSQL预编译
parameterize设置参数
query执行查询
update执行更新
ParameterHandler

参数处理器,调用类型处理器 TypeHandler做类型转换。

类图如下:

image.png

ResultHandler

结果处理器,调用类型处理器 TypeHandler做类型转换。

类图如下:

image.png

Configuration
SqlSource
BoundSql
MappedStatement

示例

User.java

package mncode.entity;

@Data
@Alias("user")
public class User {

    private Integer id;
    private String name;
    private Integer age;
}

UserMapper.java

package mncode.mapper;

public interface UserMapper {
    User queryById(Integer id);
}

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="mncode.entity"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mncode.mapper.UserMapper">
    <select id="queryById" resultType="user">
        SELECT `id`,`name`,`age` FROM `user` WHERE `id` = #{id}
    </select>
</mapper>

测试代码:

    @Test
    public void test1() throws Exception {
        SqlSession sqlSession = null;
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            sqlSession = sqlSessionFactory.openSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = userMapper.queryById(1);
            System.out.println(user);
        } finally {
            sqlSession.close();
        }
    }

原理

使用流程,大概如下:

  1. 使用 SqlSessionFactoryBuilder创建 SqlSessionFactory
  2. 使用 SqlSessionFactory创建 SqlSession
  3. 使用 SqlSession获取映射器;
  4. 通过映射器做 CRUD操作;
  5. 操作完成后,关闭 SqlSession
创建SqlSessionFactory
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

主要逻辑在 build方法中,时序图如下:

image.png

说明:

  • 2步以 configuration为根节点解析 xml文件,返回 XNode对象;
  • 4步对 XNode对象做进一步处理,parseConfiguration源码如下:
  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  • 4步会对每个标签做处理,并把处理结果封装进 Configuration类型的对象中;
  • 6build源码如下:
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

代码很简单,创建 DefaultSqlSessionFactory对象,并传入 Configuration对象的引用。

Mapper文件处理

上一小节第 4步会对每个标签做处理,先暂时只关注对 <mappers>标签的处理:

完整源码
    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
            for (XNode child : parent.getChildren()) {
                if ("package".equals(child.getName())) {
                    String mapperPackage = child.getStringAttribute("name");
                    configuration.addMappers(mapperPackage);
                } else {
                    String resource = child.getStringAttribute("resource");
                    String url = child.getStringAttribute("url");
                    String mapperClass = child.getStringAttribute("class");
                    if (resource != null && url == null && mapperClass == null) {
                        ErrorContext.instance().resource(resource);
                        InputStream inputStream = Resources.getResourceAsStream(resource);
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if (resource == null && url != null && mapperClass == null) {
                        ErrorContext.instance().resource(url);
                        InputStream inputStream = Resources.getUrlAsStream(url);
                        XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                        mapperParser.parse();
                    } else if (resource == null && url == null && mapperClass != null) {
                        Class<?> mapperInterface = Resources.classForName(mapperClass);
                        configuration.addMapper(mapperInterface);
                    } else {
                        throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                    }
                }
            }
        }
    }

截取需要关注的代码如下:

// 处理 mappers 标签下的所有 mapper 标签
for (XNode child : parent.getChildren()) {
    // 获取<mapper>标签的 resource属性
    String resource = child.getStringAttribute("resource");
    // 加载对应的文件
    InputStream inputStream = Resources.getResourceAsStream(resource);
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    // 解析
    mapperParser.parse();
}

parse源码如下:

    public void parse() {
        // 判断文件是否已经加载过
        if (!configuration.isResourceLoaded(resource)) {
            // 解析
            configurationElement(parser.evalNode("/mapper"));
            // 解析成功后,放入loadedResources集合中,下次就不需要再解析了
            configuration.addLoadedResource(resource);
            bindMapperForNamespace();
        }
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }

configurationElement源码如下:

  private void configurationElement(XNode context) {
    try {
      // 获取命名空间
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // 对 <cache-ref> 节点处理
      cacheRefElement(context.evalNode("cache-ref"));
      // 对 <cache> 节点处理
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 对 <parameterMap> 节点处理
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 对 <sql> 节点处理
      sqlElement(context.evalNodes("/mapper/sql"));
      // 对 select|insert|update|delete 4种节点处理
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

暂时只关注对 <select>节点的处理,对应代码如下:

源码
  private void buildStatementFromContext(List<XNode> list) {

    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

调用 parseStatementNode做解析,时序图如下:

image.png

说明:

  • 1:获取节点的各个属性值(如 idparameterTyperesultMap等);
  • 2:先创建建造器(MappedStatement.Builder类型);
  • 3:再使用建造器的 build方法创建 MappedStatement,内部封装了第 1步解析出的所有信息;
  • 5:数据保存到 mappedStatements中,mappedStatements结构如下:
  protected final Map<String, MappedStatement> mappedStatements;

key《 命名空间 + "." + id 》,例如:mncode.mapper.UserMapper.queryById
valueMappedStatement对象。

创建 SqlSession
sqlSession = sqlSessionFactory.openSession();

openSession会调用 openSessionFromDataSource,时序图如下:

image.png

说明:

  • 1:根据配置文件的配置生成 TransactionFactory
  • 2 ~ 3:创建 Transaction对象;
  • 4 ~ 5:创建 Executor对象;
  • 最后创建 DefaultSqlSession对象并返回;
获取映射器
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

getMapper时序图如下:

image.png

MapperRegistry类的 getMapper方法源码如下:

    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

入参 typemncode.entity.User.UserMapper(自定义的 Mapper接口);
先从 knownMappers缓存中获取对应的 mapperProxyFactoryknownMappers结构如下:

Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>()

然后调用 MapperProxyFactorynewInstance方法:

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

这里使用JDK的动态代理机制生成代理对象(动态代理详解

自动生成的代理类,内容如下:

代码
package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import mncode.entity.User;
import mncode.mapper.UserMapper;

public final class $Proxy2 extends Proxy implements UserMapper {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy2(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final User queryById(Integer var1) throws  {
        try {
            return (User)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("mncode.mapper.UserMapper").getMethod("queryById", Class.forName("java.lang.Integer"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

可以看到代理类继承了 Proxy类,实现了 UserMapper接口。

查询数据
User user = userMapper.queryById(1);

queryById方法的代码如下:

    public final User queryById(Integer var1) throws  {
        try {
            return (User)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

调用的 invoke方法在 MapperProxy中:

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

最后调用 MapperMethod类的 execute方法,源码如下:

execute源码
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

这里 CRUD不同的操作对应不同的代码片段,先暂时只关注查询相关的代码:

result = sqlSession.selectOne(command.getName(), param);

调用的是 DefaultSqlSession类的 selectOne方法:

selectOne源码
  @Override
  public <T> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

说明:
先从 mappedStatements缓存中根据 statement获取 MappedStatement对象;statement为接口名+方法名(如:mncode.mapper.UserMapper.queryById);而 MappedStatement对象 是在前面的 Mapper 文件处理》小节中放入到缓存中的;mybatis通过这种机制将 mapper接口与配置文件中的语句关联了起来;最后会执行 CachingExecutor类的 query方法。

query源码如下:

query源码
  @Override

  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);

  }



  @Override

  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {

    flushCacheIfRequired(ms);

    return delegate.queryCursor(ms, parameter, rowBounds);

  }



  @Override

  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)

      throws SQLException {

    Cache cache = ms.getCache();

    if (cache != null) {

      flushCacheIfRequired(ms);

      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

紧接着把请求转发给 SimpleExecutorquery,时序图如下:

image.png

说明:

  • 3:创建会话器 RoutingStatementHandler
  • 6:获取数据库连接 Connection
  • 7:预编译 SQL
  • 9:设置参数;
  • 10:执行查询;

其中第 7步,RoutingStatementHandler会把请求委派给 PreparedStatementHandler来处理,时序图如下:

image.png

说明:

  • 3:调用 JDBCAPI创建 preparedStatement对象;

最后执行查询,RoutingStatementHandler会把请求委派给 PreparedStatementHandler来处理,时序图如下:

image.png

说明:

  • 2:调用 JDBCAPI执行查询;
  • 3:委派结果处理器 DefaultResultSetHandler对查询结果做处理;
  • 5:调用 JDBCAPI获取结果集;
  • 7:结果集转换为 List

插件

原理

Mybatis的插件相当于是个拦截器。使用了动态代理模式和责任链模式。

  1. 需要实现以下接口:
public interface Interceptor {
  // 拦截后要执行的代码
  Object intercept(Invocation invocation) throws Throwable;
  // 利用动态代理返回代理对象
  Object plugin(Object target);
  // 参数设置
  void setProperties(Properties properties);
}
  1. 注解说明:

    • @Intercepts:标记这是个拦截器
    • @Signature:拦截点
      • type:要拦截的类
      • method:要拦截的方法
      • args:方法参数类型
  2. Mybatis支持被拦截的方法如下:

方法
Executorquery
update
flushStatements
commit
rollback
getTransaction
close
isClosed
ParameterHandlergetParameterObject
setParameters
ResultSetHandlerhandleResultSets
handleOutputParameters
StatementHandlerprepare
parameterize
batch
update
query
自定义

插件实现类:

package mncode.plugin;

@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class FirstPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("第一个插件...");
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 修改sql语句
        BoundSql boundSql = statementHandler.getBoundSql();
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, boundSql.getSql() + " limit 1");
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

mybatis的配置文件中添加:

    <plugins>
        <plugin interceptor="mncode.plugin.FirstPlugin"/>
    </plugins>

测试:

    @Test
    public void test1() throws Exception {
        SqlSession sqlSession = null;
        try {
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            sqlSession = sqlSessionFactory.openSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = userMapper.queryById(1);
            System.out.println(user);
        } finally {
            sqlSession.close();
        }
    }

输出:

11:24:09.264 [main] INFO  mncode.plugin.FirstPlugin - 第一个插件...
11:24:09.264 [main] DEBUG mncode.mapper.UserMapper.queryById - ==>  Preparing: SELECT `id`,`name`,`age` FROM `user` WHERE `id` = ? limit 1 
11:24:09.295 [main] DEBUG mncode.mapper.UserMapper.queryById - ==> Parameters: 1(Integer)
11:24:09.342 [main] DEBUG mncode.mapper.UserMapper.queryById - <==      Total: 1
User(id=1, name=Jone, age=25)