本篇为原创文章,如需转载,请标明原创地址。
我先写一个简单的例子来执行一条 sql 语句
mapper.xml
<mapper namespace="com.example.demo1.mybatis.ArticleMapper">
<select id="selectById" resultType="com.example.demo1.mybatis.Article" parameterType="java.lang.Long">
select
<include refid="baseColumns"/>
from article where 1= 1
and id = #{id}
</select>
<sql id="baseColumns">
id,title
</sql>
</mapper>
实体类
@Data
public class Article {
private Long id;
private String title;
}
测试类
public class MybatisTest {public static void main(String[] args) throws IOException {
// sqlSessionFactory 是一个复杂对象,通常创建一个复杂对象会使用建造器来构建,这里首先创建建造器
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// configuration 对象对应 mybatis 的 config 文件,为了测试简便,我这里直接创建 Configuration 对象而不通过 xml 解析获得
Configuration configuration = new Configuration();
configuration.setEnvironment(buildEnvironment());
// 解析一个 mapper.xml 为 MappedStatement 并加入到 configuration 中
InputStream inputStream = Resources.getResourceAsStream("mybatis/Article.xml");
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "mybatis/Article.xml", configuration.getSqlFragments());
mapperParser.parse();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(configuration);
// 创建一个 sqlSession, 这里使用的是简单工厂设计模式
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行最终的 sql,查询文章 id 为 1 的文章
Article article = sqlSession.selectOne("com.example.demo1.mybatis.ArticleMapper.selectById",1L);
// 打印文件的标题
System.out.println(article.getTitle());
// sqlSession 默认不会自动关闭,我们需要手动关闭
sqlSession.close();}
private static Environment buildEnvironment() {return new Environment.Builder("test")
.transactionFactory(getTransactionFactory())
.dataSource(getDataSource()).build();}
private static DataSource getDataSource() {
String url = "url";
String user = "user";
String password = "password";
Properties properties = new Properties();
properties.setProperty("url", url);
properties.setProperty("username", user);
properties.setProperty("password", password);
properties.setProperty("driver", "com.mysql.jdbc.Driver");
properties.setProperty("driver.encoding", "UTF-8");
PooledDataSourceFactory factory = new PooledDataSourceFactory();
factory.setProperties(properties);
DataSource dataSource = factory.getDataSource();
return dataSource;
}
private static TransactionFactory getTransactionFactory() {return new JdbcTransactionFactory();
}
分析 sqlSession.selectOne(“com.example.demo1.mybatis.ArticleMapper.selectById”,1L);
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.<T>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;}
}
通过查看源码,我们发现不管是查询一条数据还是查询多条数据都是执行的 selectList 方法, 查询一条的时候只要取 list 的第一条数据即可。
public <E> List<E> selectList(String statement, Object parameter) {return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
/*
根据 statement id 找到对应的 MappedStatement,而 statement id 对应的就是 mapper 的 namespace+crud 操作的 id
在本例中就是 com.example.demo1.mybatis.ArticleMapper.selectById
*/
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();}
}
使用执行器来执行查询操作,简单的执行器只会执行 sql,并将结果放入到一级缓存中,带二级缓存的执行器会增加一层缓存读写操作,这里先只讨论简单执行器的执行
========================================================================
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 得到绑定 sql
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建缓存 Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
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
StatementHandler 的作用主要有以下几个:1. 从 sqlSource 获取最终需要执行的 sql
2. 创建 jdbc 的 statement 对象
3. 给 statement 对象赋值
4. 执行 statement.execute 方法执行赋值的 sql
5. 通过 resultHandler 对 resultSet 结果集进行处理收集,获得最终的结果
6. 返回最终的结果
*/
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 通过 jdbc 连接创建一个全新的 prepareStatement,并对其进行赋值,对应上面步骤的 2,3
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行赋值后的 sql 并对结果进行处理收集,对应上面步骤的 4,5
return handler.<E>query(stmt, resultHandler);
} finally {
// 执行 statement.close 方法
closeStatement(stmt);
}
}
创建 statementHandler 对象
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
/* 创建一个路由 statementHandler
根据 statementType 进行路由,根据 jdbc 的基本知识我们知道常用的 statementType 有三种:1.STATEMENT 硬编码的语句,有 sql 注入风险
2.PREPARED 预编译 sql 的语句,一般情况下都使用这个
3.CALLABLE 执行存储过程的语句
通常我们使用的都是 preparedStatement
*/
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
/*
通过上一个步骤 statementHandler 已经被创建好,preparedStatement 也已被初始化
我们得到了一个 sql 为 select id ,title from article where id = ? 的 preparedStatement
想一想,如果我们的 sql 为 select id,title from article 返回多条记录,我们要分页的话怎么办?一种方法是我们在 mapper.xml 中在 sql 语句尾部手动添加 limit *,* 来进行分页 (以 mysql 举例)
另一种方法我们可以通过插件的方式来实现,像常用的 PageHelper 插件就是基于此来实现的分页功能。使用插件的好处是将分页功能和 sql 语句分离,达到去耦合的目的。这样我们切换数据库的时候 sql 语句并不需要改动。* */
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
/**
* 这个方法没有什么好分析的,就是执行 sql 语句,并对结果 resultSet 进行处理。* 默认的结果集处理器就是 DefaultResultSetHandler,其处理方案就是遍历 resultSet 集合,* 将所有的数据追加到 List<Article> 集合中去。*/
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.<E> handleResultSets(ps);
}
关于 MappedStament
该对象是 mapper.xml 在对象中的体现,是整个 mybatis 框架中最为核心的对象,我们也可以不必通过 xml 文件来构建该对象,可以直接通过编码方式构建,像最常用的简单的增删改查操作完全可以手动构建 mappedStatement 对象并加入到 mybatis 容器中,这样我们就不需要在 xml 文件中手写 CRUD 操作了,mybatis-plus 框架设计的思想就是鉴于此。
总结:
其实 Mybatis 执行一条 sql,底层还是用的最基本的 jdbc 操作,只不过将事物,数据源,参数的设置,结果的收集转换都封装了起来,让我们在开发中专注于 sql 本身,而忽略那些与业务不相关的步骤(结果对象的映射,开启事物,关闭事物等等操作),提高了项目的内聚性。