The previous article introduced the JDBC to implement SQL query . This article uses a simple MyBatis query example to explore the code flow of MyBatis executing SQL query.
This article is based on MyBatis 3.5.7.
1. Examples of use
Engineering structure:
Simple query example:
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init() {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void selectAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
List<Student> students = sqlSession.selectList("selectAll");
for (int i = 0; i < students.size(); i++) {
System.out.println(students.get(i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
2. Source code analysis
2.1 Parsing the configuration file
2.1.1 Parse mybatis-config.xml
An example of MyBatis XML configuration file is as follows. For more instructions, official document of
<?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>
<!-- MyBatis XML 配置说明 https://mybatis.org/mybatis-3/zh/configuration.html -->
<!-- 引入其他配置文件 -->
<properties resource="jdbc.properties"/>
<settings>
<!-- 设置一级缓存的范围 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 开启缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 开启驼峰式命名,数据库的列名能够映射到去除下划线驼峰命名后的字段名-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找 -->
<setting name="logImpl" value="LOG4J"/>
</settings>
<!-- 类型别名可为 Java 类型设置一个缩写名字。它仅用于 XML 配置,意在降低冗余的全限定类名书写 -->
<typeAliases>
<!-- 指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean。在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名 -->
<package name="com.sumkor.entity"/>
</typeAliases>
<plugins>
<plugin interceptor="com.sumkor.plugin.StatementInterceptor"/>
<plugin interceptor="com.sumkor.plugin.PageInterceptor"/>
</plugins>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<!-- POOLED 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 使用 package 标签,需要把 xml 映射文件和 Mapper 接口文件放在同一个目录,而且必须同名 -->
<package name="com.sumkor.mapper"/>
<!-- <mapper resource="com/sumkor/mapper/StudentMapper.xml"/> -->
</mappers>
</configuration>
Parse the mybatis-config.xml file through SqlSessionFactoryBuilder:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
Here, the mybatis-config.xml configuration file will be parsed into an org.apache.ibatis.session.Configuration object, which is an important configuration class.
org.apache.ibatis.session.SqlSessionFactoryBuilder#build
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
Parse each node in mybatis-config.xml.
org.apache.ibatis.builder.xml.XMLConfigBuilder#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);
}
}
After parsing the Configuration object, you can use it to construct a SqlSessionFactory object.
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(org.apache.ibatis.session.Configuration)
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
2.1.2 Parsing the SQL mapping file
An example of the SQL mapping file com\sumkor\mapper\StudentMapper.xml is as follows, for more instructions, official document
<mapper namespace="com.sumkor.mapper.StudentMapper">
<!-- 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明字段的 jdbcType 类型,以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型 -->
<resultMap id="BaseResultMap" type="com.sumkor.entity.Student">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="phone" jdbcType="VARCHAR" property="phone"/>
<result column="email" jdbcType="VARCHAR" property="email"/>
<result column="sex" jdbcType="TINYINT" property="sex"/>
<result column="locked" jdbcType="TINYINT" property="locked"/>
<result column="gmt_created" jdbcType="TIMESTAMP" property="gmtCreated"/>
<result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified"/>
<!-- 支持的 jdbcType 见 org.apache.ibatis.type.JdbcType 或者 https://mybatis.org/mybatis-3/zh/sqlmap-xml.html -->
</resultMap>
<sql id="base_column_list">
id, name, phone, email, sex, locked, gmt_created, gmt_modified
</sql>
<select id="selectAll" resultMap="BaseResultMap">
select
<include refid="base_column_list"/>
from student
</select>
</mapper>
When parsing the mybatis-config.xml file, parse the mappers tag to find the corresponding SQL mapping file.
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
org.apache.ibatis.session.Configuration#addMappers
org.apache.ibatis.binding.MapperRegistry#addMapper
org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
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|update|delete|insert>
node in the SQL mapping file will be parsed as a MappedStatement object.
org.apache.ibatis.mapping.MappedStatement
// 该对象表示 Mapper.xml 中的一条 SQL 信息,相关标签见 org\apache\ibatis\builder\xml\mybatis-3-mapper.dtd
public final class MappedStatement {
private String resource;
private Configuration configuration;
private String id;
private Integer fetchSize; // 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。默认值为未设置(unset)(依赖驱动)。
private Integer timeout; // 驱动程序等待数据库返回请求结果的秒数,超时将会抛出异常
private StatementType statementType; // 参数可选值为 STATEMENT、PREPARED 或 CALLABLE,这会让 MyBatis 分别使用 Statement、PreparedStatement 或 CallableStatement 与数据库交互,默认值为 PREPARED
private ResultSetType resultSetType; // 参数可选值为 FORWARD_ONLY、SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE,用于设置从结果集读取数据时,读指针能否上下移动。例如,只需要顺序读取,可设置为 FORWARD_ONLY,便于释放已读内容所占的内存
private SqlSource sqlSource; // sql 语句
private Cache cache; // 二级缓存,若无配置则为空
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired; // 对应 xml 中的 flushCache 属性。将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
private boolean useCache; // 对应 xml 中的 useCache 属性。将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
private boolean resultOrdered; // 这个设置仅针对嵌套结果 select 语句。默认值:false。
private SqlCommandType sqlCommandType; // sql 语句的类型,如 select、update、delete、insert
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
After the MappedStatement object is generated, it will be registered in the Configuration object:
org.apache.ibatis.builder.MapperBuilderAssistant#addMappedStatement
org.apache.ibatis.session.Configuration#addMappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
.conflictMessageProducer((savedValue, targetValue) ->
". please check " + savedValue.getResource() + " and " + targetValue.getResource());
public void addMappedStatement(MappedStatement ms) {
mappedStatements.put(ms.getId(), ms);
}
The mappedStatements property in the Configuration object is a StrictMap type, which is an internal class of MyBatis.
As you can see, registering with MappedStatement#id here will register a short name (eg: selectAll) and a long name (eg: com.sumkor.mapper.StudentMapper.selectAll) at the same time.
org.apache.ibatis.session.Configuration.StrictMap#put
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value); // 以短名称进行注册
} else {
super.put(shortKey, (V) new Ambiguity(shortKey)); // 短名称注册出现冲突
}
}
return super.put(key, value); // 以全名称注册
}
2.2 Start a session
SqlSession sqlSession = sqlSessionFactory.openSession()
The official description of SqlSession:
SqlSession provides all the methods needed to execute SQL commands in the database. The mapped SQL statement can be executed directly through the SqlSession instance.
Each thread should have its own instance of SqlSession. The instance of SqlSession is not thread-safe, so it cannot be shared, so its best scope is the request or method scope.
In a web application, every time an HTTP request is received, a SqlSession can be opened, and after a response is returned, it can be closed.
Construct an instance of SqlSession, the code is as follows. The default autoCommit is false.
Note that each SqlSession session has its own Executor and Transaction instances.
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 通过事务工厂,实例化 Transaction 事务对象
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 实例化 Executor 执行器对象,通过它来执行 SQL,支持插件扩展
final Executor executor = configuration.newExecutor(tx, execType);
// 构建 SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
By default, SimpleExecutor is used as the executor and as the dispatch center of MyBatis.
org.apache.ibatis.session.Configuration#newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
}
// 该类型的执行器会复用预处理语句。
else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
}
// 该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 使用插件来包装 executor
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
2.3 Execute SQL
List<Student> students = sqlSession.selectList("selectAll");
- The fully qualified name (such as "com.sumkor.mapper.StudentMapper.selectAll) will be used directly for search and use.
- Short names (such as "selectAll") can also be used as a single reference if they are globally unique. If it is not unique, there are two or more identical names (such as "com.foo.selectAll" and "com.bar.selectAll"), then the "short name is not unique" error will be generated when used. In this case, the fully qualified name must be used.
Here, the corresponding MappedStatement object is selectAll
from the Configuration object through the string 06130e6ff787e9.
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement); // 根据 SQL id 从配置类中获取 SQL 解析后的对象
return executor.query(ms, wrapCollection(parameter), rowBounds, handler); // 执行 SQL
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
2.3.1 Executor#query
Before querying the database, the MyBatis cache will be queried. Here only focus on the part of the database query:
org.apache.ibatis.executor.CachingExecutor#query
org.apache.ibatis.executor.BaseExecutor#query
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
org.apache.ibatis.executor.SimpleExecutor#doQuery
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 创建 StatementHandler,支持插件扩展
stmt = prepareStatement(handler, ms.getStatementLog()); // 获取数据库连接对象 Connection,以构造 Statement 对象
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
The main function of the Executor object is to create and use StatementHandler to access the database, and store the query results in the cache (if the cache is configured).
2.3.2 Executor#prepareStatement
After the Executor creates the StatementHandler object, it uses StatementHandler#prepare to construct the Statement object.
org.apache.ibatis.executor.SimpleExecutor#prepareStatement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog); // 获取数据库连接对象
stmt = handler.prepare(connection, transaction.getTimeout()); // 使用 Connection 对象构造 Statement 对象
handler.parameterize(stmt); // 其中会使用 ParameterHandler 设置参数,支持插件扩展
return stmt;
}
org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
org.apache.ibatis.executor.statement.BaseStatementHandler#instantiateStatement
org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
protected Statement instantiateStatement(Connection connection) throws SQLException {
// ...
String sql = boundSql.getSql();
return connection.prepareStatement(sql);
}
At the JDBC level, a Statement object is created through Connection, and finally a ClientPreparedStatement instance is obtained.
java.sql.Connection#prepareStatement(java.lang.String, int, int)
com.mysql.cj.jdbc.ConnectionImpl#prepareStatement
com.mysql.cj.jdbc.ConnectionImpl#clientPrepareStatement(java.lang.String, int, int, boolean)
com.mysql.cj.jdbc.ClientPreparedStatement#getInstance(com.mysql.cj.jdbc.JdbcConnection, java.lang.String, java.lang.String)
protected static ClientPreparedStatement getInstance(JdbcConnection conn, String sql, String db) throws SQLException {
return new ClientPreparedStatement(conn, sql, db);
}
2.3.3 StatementHandler#query
After obtaining the Statement object, use the StatementHandler#query method to execute the SQL statement.
org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.executor.statement.RoutingStatementHandler#query
org.apache.ibatis.executor.statement.PreparedStatementHandler#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
At the JDBC level, SQL is executed by the PreparedStatement object in the MySQL driver.
org.apache.ibatis.logging.jdbc.PreparedStatementLogger#invoke
com.mysql.cj.jdbc.ClientPreparedStatement#execute
2.4 Result mapping
After executing SQL through PreparedStatement in StatementHandler#query, read the response to MySQL server.
org.apache.ibatis.executor.SimpleExecutor#doQuery
org.apache.ibatis.executor.statement.RoutingStatementHandler#query
org.apache.ibatis.executor.statement.PreparedStatementHandler#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
The binary data read from the database through JDBC is encapsulated in the ResultSet object.
The data in the ResultSet needs to be converted into the ResultMap object configured in the SQL mapping file.
The ResultMap configuration in this example is as follows. For more configuration instructions, official document of
<mapper namespace="com.sumkor.mapper.StudentMapper">
<!-- 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以必须在参数和结果映射中指明字段的 jdbcType 类型,以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型 -->
<resultMap id="BaseResultMap" type="com.sumkor.entity.Student">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="phone" jdbcType="VARCHAR" property="phone"/>
<result column="email" jdbcType="VARCHAR" property="email"/>
<result column="sex" jdbcType="TINYINT" property="sex"/>
<result column="locked" jdbcType="TINYINT" property="locked"/>
<result column="gmt_created" jdbcType="TIMESTAMP" property="gmtCreated"/>
<result column="gmt_modified" jdbcType="TIMESTAMP" property="gmtModified"/>
<!-- 支持的 jdbcType 见 org.apache.ibatis.type.JdbcType 或者 https://mybatis.org/mybatis-3/zh/sqlmap-xml.html -->
</resultMap>
</mapper>
Code flow:
- The response data ResultSet object read from the database.
- Get the ResultMap object configured in the SQL mapping file, and generate the corresponding Java entity object.
- Use the data in the ResultSet object to assign values to the properties of the Java entity objects.
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSets
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
ResultSetWrapper rsw = getFirstResultSet(stmt); // 从数据库读取的响应数据 ResultSet 对象
List<ResultMap> resultMaps = mappedStatement.getResultMaps(); // 配置在 SQL 映射文件的 ResultMap 对象
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null); // 生成 Java 实体对象并进行属性赋值,存储在 multipleResults
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets(); // (在使用存储过程的情况下)如果配置了多结果集,则进一步处理
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
It is worth noting that not all binary data in the ResultSet needs to be converted into Java entity objects.
- In the case of using RowBounds, it means that the general paging function provided by MyBatis is used. You only need to take the data from the offset to the limit range of the ResultSet for object mapping.
- When using Discriminator, you need to perform object mapping based on the conditions of the discriminator.
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleResultSet
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValues
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
// 在查询数据库时,如果没有 limit 语句,则 ResultSet 中会包含所有满足条件的数据
ResultSet resultSet = rsw.getResultSet();
// 把 offset 之前的数据都 skip 掉
skipRows(resultSet, rowBounds);
// 判断数据是否超过了 limit
while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
// 如果配置了鉴别器 Discriminator,则对查询的结果进行分支处理
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
// 将 ResultSet 中的一行数据转换为 ResultMap 的一个对象并赋值
Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
// 将结果存储到 ResultHandler 之中
storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
For the description of Discriminator and multiple result sets in , please refer to 16130e6ff78d7a official document-XML mapping
2.4.1 Data Object Mapping
- Obtain Java entity objects according to ResultMap.
- Assign values to properties of Java entity objects.
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 获取 Java 实体类的实例,属性均为空
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue); // 反射工具类 MetaObject
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; // 利用 MetaObject 为 Java 实体的属性赋值
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
2.4.2 Obtaining Java entity objects based on ResultMap
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createResultObject
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
throws SQLException {
final Class<?> resultType = resultMap.getType(); // Java 实体类型
final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory); // 反射工具类 MetaClass
final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
if (hasTypeHandlerForResultObject(rsw, resultType)) {
return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
} else if (!constructorMappings.isEmpty()) {
return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
return objectFactory.create(resultType); // 创建 Java 实体类的实例
} else if (shouldApplyAutomaticMappings(resultMap, false)) {
return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
}
throw new ExecutorException("Do not know how to create an instance of " + resultType);
}
2.4.3 Assigning values to the properties of Java entity objects
Read the value of each field from the ResultSet and assign it to the properties of the Java entity.
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#applyPropertyMappings
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) { // 遍历所有字段
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); // 表字段名,例如:gmt_created
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); // 获取字段的值
// issue #541 make property optional
final String property = propertyMapping.getProperty(); // 实体属性名,例如:gmtCreated
if (property == null) {
continue;
} else if (value == DEFERRED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(property, value); // 为 Java 实体对象的字段赋值
}
}
}
return foundValues;
}
As for how to read the value of a field from the ResultSet, TypeHandler is used here to achieve the mapping and conversion between the java data type and the jdbc data type.
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getPropertyMappingValue
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
if (propertyMapping.getNestedQueryId() != null) {
return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
} else if (propertyMapping.getResultSet() != null) {
addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
return DEFERRED;
} else {
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); // 获取字段映射所需的 TypeHandler
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix); // 获取数据库表的字段名
return typeHandler.getResult(rs, column); // 进行 java 数据类型和 jdbc 数据类型之间的映射和转换
}
}
Take IntegerTypeHandler as an example, read the data corresponding to the field name columnName from the ResultSet, and the data type of the field is int.
org.apache.ibatis.type.IntegerTypeHandler#getNullableResult
@Override
public Integer getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int result = rs.getInt(columnName);
return result == 0 && rs.wasNull() ? null : result;
}
3. Summary
The process of MyBatis executing SQL:
- Load configuration: Parse the MyBatis XML configuration file into a Configuration object, in which the SQL statement in the SQL mapping file is parsed into a MappedStatement object and stored in the Configuration object.
- SQL parsing: When receiving a request through SQLSession, find the corresponding MappedStatement object according to the incoming SQL id, which contains the parsed SQL statement.
- SQL execution: Scheduled by Executor, using StatementHandler to initiate queries to the database, the bottom layer is to query the database using JDBC's Statement#execute method, and the query result is a ResultSet object.
- Result mapping: The query result is converted according to the mapping configuration, which can be converted into HashMap, JavaBean or basic data type, and the final result is returned.
Author: Sumkor
Link: https://segmentfault.com/a/1190000040610536
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。