一、一个简单的Demo
开始之前先创建一张表,搭建一个简单的工程。
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`sex` int(255) DEFAULT NULL,
`schoolName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '易哥', 'yeecode@sample.com', '18', '0', 'Sunny School');
INSERT INTO `user` VALUES ('2', '莉莉', 'lili@sample.com', '15', '1', 'Garden School');
INSERT INTO `user` VALUES ('3', '杰克', 'jack@sample.com', '25', '0', 'Sunny School');
INSERT INTO `user` VALUES ('4', '张大壮', 'zdazhaung@sample.com', '16', '0', 'Garden School');
INSERT INTO `user` VALUES ('5', '王小壮', 'wxiaozhuang@sample.com', '27', '0', 'Sunny School');
INSERT INTO `user` VALUES ('6', '露西', 'lucy@sample.com', '14', '1', 'Garden School');
INSERT INTO `user` VALUES ('7', '李二壮', 'lerzhuang@sample.com', '9', '0', 'Sunny School');
1、实体类
package com.gitee.mycode.mybatisdemo.entity;
import lombok.Data;
/**
* @program: mybatisdemo
* @description: 用户实体
* @author: Mr.Hu
* @create: 2022-05-31 21:19
*/
@Data
public class User {
private Integer id;
private String name;
private String email;
private Integer age;
private Integer sex;
private String schoolName;
}
2、配置文件
<?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="com.gitee.mycode.mybatisdemo.entity"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
3、映射接口文件
package com.gitee.mycode.mybatisdemo.mapper;
import com.gitee.mycode.mybatisdemo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @program: mybatisdemo
* @description:
* @author: Mr.Hu
* @create: 2022-05-31 21:22
*/
@Mapper
public interface UserMapper {
List<User> queryUserBySchoolName( User user);
}
4、映射文件
<?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="com.gitee.mycode.mybatisdemo.mapper.UserMapper">
<select id="queryUserBySchoolName" resultType="com.gitee.mycode.mybatisdemo.entity.User">
select * from user
<if test="schoolName!=null">
where schoolName=#{schoolName}
</if>
</select>
</mapper>
5、核心逻辑
package com.gitee.mycode.mybatisdemo;
import com.gitee.mycode.mybatisdemo.entity.User;
import com.gitee.mycode.mybatisdemo.mapper.UserMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@SpringBootApplication
public class MybatisdemoApplication {
public static void main(String[] args) {
//第一阶段:mybatis初始化
String resource = "mybatis-config.xml";
//得到配置文件输入流
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//第二阶段:数据读写阶段
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
User userParam = new User();
userParam.setSchoolName("Sunny School");
List<User> users = userMapper.queryUserBySchoolName(userParam);
for (User user : users) {
System.out.println("userName:"+user.getName()+";email:"+user.getEmail());
}
}
}
}
最终项目结构和运行结果如下
二、初始化阶段追踪
从上面demo的核心逻辑可以看出整个运行过程分为两个阶段,初始化阶段和数据读取阶段。初始化阶段的最核心代码为:
inputStream = Resources.getResourceAsStream(resource);
和SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
1、获取InputStream
通过Resources.getResourceAsStream
追踪会发现ClassLoaderWrapper类有这样一段代码
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
ClassLoader[] var3 = classLoader;
int var4 = classLoader.length;
for(int var5 = 0; var5 < var4; ++var5) {
ClassLoader cl = var3[var5];
if (null != cl) {
InputStream returnValue = cl.getResourceAsStream(resource);
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
getResourcecAsSream方法会调用传入的每一个类加载器的getResourceAsStream方法来尝试获取配置文件的输入流。在尝试过程中如果获取失败的话,会在传入的地址前加上“/”再获取一次。只要尝试成功,则表明成功加载了指定的资源,会将所获得的输入流返回。
2、构建SqlSessionFactory对象
通过对SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
build方法的追踪可以发现
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
//创建XMLConfigBuilder对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
//通过XMLConfigBuilder对象生成出一个Congfiguration对象之后,传入build方法,构建SqlSessionFactory
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
;
}
}
return var5;
}
2.1构建Configuration对象
进入parser.parse()方法可以看到下面这段代码
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
this.loadCustomLogImpl(settings);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
有没有觉得代码里的这些字符串很眼熟?没错,就是我们demo中配置文件里的那些标签名。可想而知,parse方法作用就是解析配置文件并返回Configuration实例。因此Configuration类中保存了配置文件的所有设置消息,也保存了映射文件的信息。可见,Configuration类是一个非常重要的类。
2.2、构建SqlSessionFactory对象
执行完parse方法之后,接下来就是将返回的Configuration对象传入build方法,源码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
至此,SqlSessionFactory对象创建成功!
三、数据读写阶段追踪
接着,我们来到核心逻辑的第二阶段。
//第二阶段:数据读写阶段
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper userMapper = session.getMapper(UserMapper.class);
User userParam = new User();
userParam.setSchoolName("Sunny School");
List<User> users = userMapper.queryUserBySchoolName(userParam);
for (User user : users) {
System.out.println("userName:"+user.getName()+";email:"+user.getEmail());
}
}
1、获得SqlSession
我们追踪sqlSessionFactory.openSession()
可以在DefaultSqlFactory的openSessionFromDataSource方法找到下面这段代码,这是生成SqlSession的核心源码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
从这段代码可以看出,Configuration对象中存储的设置消息被用来创建各种对象。包括事务工厂TransactionFactory、执行器Executor
以及默认的DefaultSqlSession。进入DefaultSqlSession后可以看到大量的增删改查、提交、回滚等方法。从DefaultSqlSession返回之后,SqlSession session = sqlSessionFactory.openSession()
这段代码执行完毕。
2、映射接口文件与映射文件的绑定
下一步就是执行主方法中的UserMapper userMapper = session.getMapper(UserMapper.class);
这段代码,跟进getMappe方法会发现MapperRegistry类中的getMapper方法如下
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
上述代码中,getMapper方法通过映射接口信息从所有已经解析的映射文件找到对应的映射文件,然后根据该映射文件组建并返回接口的一个实现对象。
3、映射接口的代理
那么mapperProxyFactory.newInstance(sqlSession)
返回的对象到底是什么呢?继续追踪代码
protected T newInstance(MapperProxy<T> mapperProxy) {
return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
}
可见这里返回的是一个动态代理对象,因此我们找到MapperProxy类的invoke方法并在其中打上断点。
所以主方法List<User> users = userMapper.queryUserBySchoolName(userParam)
会执行上图的代码。接着会触发MapperMethod对象的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;
}
该方法中mybatis根据不同的数据库操作进行了响应的方法处理。当前demo是进行数据库的查询操作,会触发result = executeForMany(sqlSession, args);
语句,executeForMany方法源码如下:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
//下一步入口
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
在该方法中mybatis开始通过SqlSession对象的selectList方法展开后续的查询工作。跟到这里,mybatis已经完成了为映射接口注入实现的过程。于是,对映射接口中的抽象方法调用转变成了数据查询操作。
4、SQL语句的查找
进入result = sqlSession.selectList(command.getName(), param, rowBounds)
,源码如下:
@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();
}
}
configuration.getMappedStatement(statement)
语句将要执行的MappedStatement对象从Configuration对象存储的映射文件信息中找了出来
每个MappedStatement对象对应了我们设置的一个数据库操作节点,它主要定义了数据库操作语句、输入/输出参数等信息。
5、查询结果缓存
点进executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER)
可以发现query方法为抽象方法,该方法有BaseExecutor和CachingExecutor两种实现方式,在query抽象方法上打上断点后重新运行项目,会发现断点自动跳进了CachingExecutor类,下面是CachingExecutor类的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> 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);
}
根据以上代码可以看出,mybatis会查看当前操作是否命中缓存。如果是,则从缓存中获取数据结果;否则,便通过delegate调用query方法。
BoundSql是经过层层转化后去除if、where等标签的SQL语句,而CacheKey是本次查询操作计算出来的缓存键。
6、数据库查询
再次进入delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql)
发现它同样是一个抽象方法,再次采用上面的操作,在该方法上打一断点后,发现这次是进入了BaseExecutor类,下面来看看BaseExecutor类的query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//下一步入口
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
这个方法逻辑有些复杂,重点看list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql)
这行核心代码,继续跟进去查看:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//下一步入口
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
通过代码可以看出,mybatis先在缓存中放入一个占位符,然后调用doQuery方法执行实际查询操作。最后又把缓存中的占位符替换成真正的查询结果。
doQuery是BaseExcutor的抽象方法,在该抽象方法上打上断点后发现,跳到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);
stmt = prepareStatement(handler, ms.getStatementLog());
//下一步入口
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
上述方法生成了一个java.sql.Statement类的对象stmt,Statement类可以执行静态SQL语句并返回结果。
程序先获取到一个StatementHandler对象之后,然后再将查询操作交给StatementHandler对象执行。
StatementHandler对象是一个语句处理器类,其中封装了很多语句操作方法,暂不细说。
接着走到handler.query(stmt, resultHandler)
这一行,最后经过多次跳转,程序执行到了PreparedStatementHandler类的query方法
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
这里ps.execute()
真正执行了SQL语句,然后把执行结果交给ResultHandler处理。下图是debug模式下最后查询出来的数据库信息
数据库查询结果在PreparedStatement对象中藏的比较深,为h>statement>results。数据库指端信息在columnDefinition变量中,数据记录信息在rowData变量中
7、处理结果集
查询到的结果并没有直接返回,而是交给了ResultHandler处理,继续跟进handler.query(stmt, resultHandler)
,其源码如下:
@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);
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
ResultMap resultMap = resultMaps.get(resultSetCount);
handleResultSet(rsw, resultMap, multipleResults, null);
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);
}
在上述方法中查询出来的结果被遍历后放入multipleResults集合并返回,该集合中存储的就是这次查询期望的结果LIst<User>,至于mybatis是如何将数据库输出的记录转化为对象列表的具体过程比较长,这里就不展开了。主要的就是一下三个方法。
- DefaultResultSetHandler.createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix):该方法创建了输出结果对象,也就是demo中的User对象
- DefaultResultSetHandler.applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix):在自动属性映射功能开启的情况下,该方法将数据记录的赋给输出结果对象
- DefaultResultSetHandler.applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix):该方法按照用户的映射设置,给输出结果对象的属性赋值
四、总结
在整个数据库操作阶段,mybatis完成的工作可以分为以下几步:
- 根据配置文件位置,获取他的输入流
- 从配置文件跟节点开始,逐层解析配置文件,也包括相关的映射文件。解析过程中不断将解析结果放入Configuration对象
- 以配置好的Configuration对象为参数,获取一个SqlSessionFactory对象
- 建立连接数据库的SqlSession
- 查找当前映射接口中抽象方法对应的数据库操作节点,根据节点生成接口的实现
- 接口的实现拦截映射接口中抽象方法的调用,并将其转化为数据库查询操作
- 对数据库节点中的操作语句进行多次处理,最终得到标准的SQL语句
- 尝试从缓存中查找结果,如果找到则返回;如果找不到则继续从数据库中查找
- 从数据库查询结果
处理结果集
- 建立输出对象
- 根据输出结果对输出对象的属性赋值
- 在缓存中记录查询结果
- 返回查询结果
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。