SqlSession接口的作用就是提供给程序员更加便捷的调用代码(JDBC操作)。
我们模拟两个接口,单条查询selectOne()与多条查询selectList()
/**
* 表示一个sql会话,就是一次CRUD操作
*/
public interface SqlSession {
<T> T selectOne(String statementId, Object param);
<T> List<T> selectList(String statementId, Object param);
}
有了接口,我们就需要解决两个问题,一个是实现类怎么搞,另外一个是sqlsession会被调用很多次,而且它需要Configuration对象(通过构造方法来获取)。但是调用sqlSession的人,不需要关心Configuration对象的创建,同样也不需要持有这个对象。这个时候就可以考虑使用工厂模式来屏蔽SqlSession的构造细节。
/**
* SqlSession默认实现类
*/
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <T> T selectOne(String statementId, Object param) {
return null;
}
@Override
public <T> List<T> selectList(String statementId, Object param) {
return null;
}
}
public interface SqlSessionFactory {
SqlSession openSqlSession();
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
// 等待注入
private Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
super();
this.configuration = configuration;
}
@Override
public SqlSession openSqlSession() {
return new DefaultSqlSession(configuration);
}
}
此时即使我们已经能够通过SqlSessionFactory去创建SqlSession从而达到隐藏SqlSession构建的详细内容,但是对于SqlSessionFactory我们还是要去通过构造方法得到Configuration对象,其实没有完全达到我们的要求。因此,考虑采用构建者模式的方式去生成SqlSessionFactory.
程序开发人员可以得到MyBatis全局配置文件,所以能够将全局配置文件转换为InputStream或者Reader对象,我们的构造者类可以用InputStream或者Reader作为入参。
最后,构造者类中再提供直接使用Configuration创建SqlSessionFactory的方式,使得不同的build方法可以得到共同的结果。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(InputStream inputStream) {
// 获取Configuration对象
Document document = DocumentUtils.readDocument(inputStream);
XMLConfigParser configParser = new XMLConfigParser();
Configuration configuration = configParser.parse(document.getRootElement());
return build(configuration);
}
// 也可以通过另外的入参来创建SqlSessionFactory
public SqlSessionFactory build(Reader reader) {
return null;
}
private SqlSessionFactory build(Configuration configuration) {
return new DefaultSqlSessionFactory(configuration);
}
}
以上整体的类关系如图:
到此,我们就完成了sqlSession的创建,接下来集中于selectOne方法的实现。
在selectOne方法中,我们需要做的事情是:
因此,我们需要一个执行器接口,使用它的多个实现类来分别实现statement的操作。(此处可以考虑放到MappedStatement对象中,该对象中可以根据是否配置了二级缓存来确定创建的是哪个Executor。我们本次手写框架暂时不实现此功能)
public interface Executor {
/**
* 查询方法
* @param mappedStatement 获取sql语句和入参出参等信息
* @param configuration 获取数据源对象
* @param param 入参对象
* @return
*/
<T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param);
}
随后我们还需要两个Executor的实现类,分别实现二级缓存和一级缓存:
CachingExecutor
/**
* 处理二级缓存
*/
public class CachingExecutor implements Executor {
// 基本执行器
private Executor delegate;
public CachingExecutor(Executor delegate) {
super();
this.delegate = delegate;
}
@Override
public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param) {
// 从二级缓存中根据sql语句获取处理结果(由于已经被弃用,暂不实现)
// 如果有,则直接返回,如果没有则继续委托给基本执行器去执行
return delegate.query(mappedStatement, configuration, param);
}
}
对于基本执行器,核心是处理一级缓存和执行真正的查询方法。其中:
BaseExecutor
这里我们暂时还没有实现getBoundSql()的核心逻辑,我们放在最后实现。
/**
* 基本执行器,主要处理一级缓存
*/
public abstract class BaseExecutor implements Executor {
private Map<String, List<Object>> oneLevelCache = new HashMap<String, List<Object>>();
@Override
public <T> List<T> query(MappedStatement mappedStatement, Configuration configuration, Object param) {
// 根据方法入参,从mappedStatement中动态获取(动态处理#{})带有值的sql语句
String sql = mappedStatement.getSqlSource().getBoundSql(param).getSql();
// 从一级缓存去根据sql语句获取查询结果
List<Object> result = oneLevelCache.get(sql);
if (result != null) {
return (List<T>) result;
}
// 如果没有结果,则调用相应的处理器去处理
result = queryFromDataBase(mappedStatement, configuration, param);
// 回种到一级缓存中
oneLevelCache.put(sql, result);
return (List<T>) result;
}
// 真正的抽象查询方法
public abstract List<Object> queryFromDataBase(MappedStatement mappedStatement, Configuration configuration,
Object param);
}
以上的整体类关系如图:
最后,我们要进入真正的JDBC执行了,编写基本执行器的子类,来最终实现queryFromDataBase方法:
JDBC的主要操作在之前的测试代码中讲到过,简化下来为:
我们编写主要的执行顺序代码:
/**
* 普通执行JDBC程序
* @author JeffOsmond
*/
public class SimpleExecutor extends BaseExecutor {
@Override
public List<Object> queryFromDataBase(MappedStatement mappedStatement, Configuration configuration, Object param) {
List<Object> results = new ArrayList<Object>();
try {
// 获取连接
Connection connection = getConnection(configuration);
// 获取sql语句
BoundSql boundSql = getBoundSql(mappedStatement.getSqlSource(), param);
String statementType = mappedStatement.getStatementType();
// 可以使用mybatis的四大组件来优化
if ("prepared".equals(statementType)) {
// 创建Statement
PreparedStatement statement = createStatement(connection, boundSql.getSql());
// 设置参数
handleParameter(statement, boundSql, param);
// 执行Statement
ResultSet resultSet = statement.executeQuery();
// 处理结果
handleResult(resultSet, mappedStatement, results);
}
} catch (Exception e) {
e.printStackTrace();
}
return results;
}
}
接下来完善每一步的具体操作:
获取连接getConnection()
我们在配置文件读取的时候,将数据库连接信息已经封装到DataSource中,所以可以通过DataSource类的内置方法获取数据库连接:
public class SimpleExecutor extends BaseExecutor {
...
private Connection getConnection(Configuration configuration) throws Exception {
DataSource dataSource = configuration.getDataSource();
Connection connection = dataSource.getConnection();
return connection;
}
}
获取sql语句 getBoundSql()
一个Mapper.xml对应一个MappedStatement,一个Mapper.xml中的标签对应一个SqlSource,因此,我们可以通过SqlSource类获取到真正要执行的SQL语句:
public class SimpleExecutor extends BaseExecutor {
...
private BoundSql getBoundSql(SqlSource sqlSource, Object param) {
BoundSql boundSql = sqlSource.getBoundSql(param);
return boundSql;
}
}
创建Statement createStatement()
public class SimpleExecutor extends BaseExecutor {
...
private PreparedStatement createStatement(Connection connection, String sql) throws Exception {
PreparedStatement prepareStatement = connection.prepareStatement(sql);
return prepareStatement;
}
}
处理参数 handleParameter()
这里面要操作的就是将已经仅包含?的sql进行处理,把?替换成方法入参中的具体参数:
select username from user where id = ?
-> select username from user where id = 1
public class SimpleExecutor extends BaseExecutor {
...
private void handleParameter(PreparedStatement statement, BoundSql boundSql, Object param) throws Exception {
// 判断入参的类型,如果是简单类型,直接处理
if (param instanceof Integer) {
statement.setObject(1, Integer.parseInt(param.toString())); // 这里为了方便,直接采用setObject,而非setInt
} else if (param instanceof String) {
statement.setObject(1, param.toString());
} else {
// 如果是POJO类型,则根据参数信息里面的参数名称,去入参对象中获取对应的参数值
// 获取参数集合信息(#{}处理之后得到的参数信息)
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i = 0; i < parameterMappings.size(); i++) {
Object valueToUser = null;
ParameterMapping parameterMapping = parameterMappings.get(i);
// #{}中的参数名称,也应该和POJO类型中的属性名称一直
String name = parameterMapping.getName();
// 使用反射获取指定name的值
Class<?> clazz = param.getClass();
// 获取指定名称的属性对象
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
valueToUser = field.get(param);
statement.setObject(i + 1, valueToUser);
}
}
}
}
处理结果映射 handleResult()
我们通过JDBC执行sql语句,得到了ResultSet,接下来就需要讲ResultSet里面的数据映射成Mapper.xml文件中配置的resultTye类型。
public class SimpleExecutor extends BaseExecutor {
...
private void handleResult(ResultSet rs, MappedStatement mappedStatement, List<Object> results) throws Exception {
// 从结果集中一行一行的取数据
// 每一行数据,再一列一列的取数据(包括列的名称和列的值)
// 最终将获取到的每一列的值都映射到目标对象的指定属性中(列的名称和属性名称要一致)
Class<?> resultTypeClass = mappedStatement.getResultTypeClass();
while (rs.next()) {
// 要映射的结果目标对象
Object result = resultTypeClass.newInstance();
// 获取结果集的元数据(目的是取列的信息)
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
for (int i = 0; i < columnCount; i++) {
String columnName = metaData.getColumnName(i + 1);
Field field = resultTypeClass.getDeclaredField(columnName);
field.setAccessible(true);
field.set(result, rs.getObject(columnName));
}
results.add(result);
}
}
}
有了前面编写的Executor,我们就可以调用executor进行selectOne与selectList操作了:
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
public DefaultSqlSession(Configuration configuration) {
this.configuration = configuration;
}
@Override
public <T> T selectOne(String statementId, Object param) {
List<Object> list = this.selectList(statementId, param);
if (list == null || list.size() == 0) {
return null;
} else if (list.size() == 1) {
return (T) list.get(0);
} else {
throw new RuntimeException("只能返回一个对象");
}
}
@Override
public <T> List<T> selectList(String statementId, Object param) {
// 根据statementId获取MappedStatement对象
MappedStatement mappedStatement = configuration.getMappedStatementById(statementId);
// 调用二级缓存执行器执行查询 (二级缓存CachingExecutor -> 一级缓存BaseExecutor -> 基础执行器SimpleExecutor)
Executor executor = new CachingExecutor(new SimpleExecutor());
return executor.query(mappedStatement, configuration, param);
}
}
到此,我们已经完成了全部的sql语句的配置文件加载以及执行器执行流程。
getBoundSql()方法是SqlSource接口的方法。用于获取处理完的sql语句。SqlSource接口有两个主要的实现类:DynamicSqlSource、RawSqlSource.这两个类的处理逻辑都一样,只不过处理的时机不同。
对于整体的处理逻辑为:将DynamicSqlSource、RawSqlSource处理成StaticSqlSource.StaticSqlSource就是最终我们可以在jdbc程序中使用的sql+?参数映射。我们借助另外一个类来实现这种操作。
对于单独的处理逻辑为:
处理过程示意图:
SqlSourceParser
该类主要用于处理#{},处理手段与之前的${}类似
public class SqlSourceParser {
public SqlSource parse(String sqlText) {
ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler();
GenericTokenParser tokenParser = new GenericTokenParser("#{", "}", tokenHandler);
// tokenParser.parse(sqlText)参数是未处理的,返回值是已处理的(没有${}和#{})
String sql = tokenParser.parse(sqlText);
return new StaticSqlSource(sql, tokenHandler.getParameterMappings());
}
}
public class ParameterMappingTokenHandler implements TokenHandler {
private List<ParameterMapping> parameterMappings = new ArrayList<>();
// context是参数名称
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
ParameterMapping parameterMapping = new ParameterMapping(content);
return parameterMapping;
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
public void setParameterMappings(List<ParameterMapping> parameterMappings) {
this.parameterMappings = parameterMappings;
}
}
RawSqlSource
注意:之前的步骤里,我们在RawSqlSource中定义了sqlNode,现在将其去掉,变为SqlSource.
public class RawSqlSource implements SqlSource {
private SqlSource sqlSource;
public RawSqlSource(SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(null);
rootSqlNode.apply(context);
// 在这里要先对sql节点进行解析
SqlSourceParser sqlSourceParser = new SqlSourceParser();
sqlSource = sqlSourceParser.parse(context.getSql());
}
@Override
public BoundSql getBoundSql(Object param) {
// 从staticSqlSource中获取相应信息
return sqlSource.getBoundSql(param);
}
}
DynamicSqlSource
public class DynamicSqlSource implements SqlSource {
private SqlNode rootSqlNode;
public DynamicSqlSource(MixedSqlNode rootSqlNode) {
this.rootSqlNode = rootSqlNode;
}
/**
* 在sqlsession执行的时候,才调用该方法
*/
@Override
public BoundSql getBoundSql(Object param) {
//首先先调用SqlNode的处理,将动态标签和${}处理一下
DynamicContext context = new DynamicContext(param);
rootSqlNode.apply(context);
// 再调用SqlSourceParser来处理#{}
SqlSourceParser sqlSourceParser = new SqlSourceParser();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql());
return sqlSource.getBoundSql(param);
}
}
在前面配置文件加载的测试项目中添加:
po类:User
public class User {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// 省略get/set方法
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birthday=" + birthday +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}
dao接口:UserDao
public interface UserDao {
/**
* 根据用户Id查询用户信息
* @param param
* @return
*/
User queryUserById(User param);
}
dao实现类:UserDaoImpl
public class UserDaoImpl implements UserDao {
private SqlSessionFactory sqlSessionFactory;
public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public User queryUserById(User param) {
SqlSession sqlSession = sqlSessionFactory.openSqlSession();
return sqlSession.selectOne("test.findUserById",param);
}
}
全局配置文件:SqlMapConfig.xml
<configuration>
<environments default="dev">
<environment id="dev">
<dataSource type="DBCP">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://192.168.152.100:3306/ssm" />
<property name="username" value="root" />
<property name="password" value="root" />
dataSource>
environment>
environments>
<mappers>
<mapper resource="mapper/UserMapper.xml" />
mappers>
configuration>
UserMapper.xml
<mapper namespace="test">
<select id="findUserById"
parameterType="com.osmond.mybatis.po.User"
resultType="com.osmond.mybatis.po.User"
statementType="prepared">
SELECT * FROM user WHERE id = #{id} AND username like '%${username}'
<if test="username != null and username !='' ">
AND username like '%${username}'
<if test="username != null and username !=''">
AND 1=1
if>
if>
select>
mapper>
测试类:UserDaoTest
public class UserDaoTest {
@Test
public void testQueryUserById() {
String resource = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// SqlSessionFactory的创建可能有几种创建方式,但是我还是不想要知道SqlSessionFactory的构造细节
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
User param = new User();
param.setId(1);
User user = userDao.queryUserById(param);
System.out.println(user);
}
}