前言
1:在流程上,通过 DefaultSqlSession#selectOne 方法调用执行器,并通过预处理语句处理器 PreparedStatementHandler 执行参数设置和结果查询。
2:那么这个流程中我们所处理的参数信息,也就是每个 SQL 执行时,那些?号 需要被替换的地方,目前是通过硬编码的方式进行处理的。而这就是本章节需要解决的问题,如果只是硬编码完成参数设置,那么对于所有哪些不同类型的参数就没法进行操作了。
设计
参数的处理也就是通常我们使用 JDBC 直接操作数据库时,所使用 ps.setXxx(i, parameter); 设置的各类参数。那么在自动化解析 XML 中 SQL 拆分出所有的参数类型后,则应该根据不同的参数进行不同的类型设置,也就;Long 调用 ps.setLong、String 调用 ps.setString 所以这里需要使用策略模式,在解析 SQL 时按照不同的执行策略,封装进去类型处理器(也就是是实现 TypeHandler 接口的过程)
MapperMethod
public class MapperMethod {
public Object execute(SqlSession sqlSession, Object[] args) {
Object result = null;
switch (command.getType()) {
case SELECT:
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
break;
default:
throw new RuntimeException("Unknown execution method for: " + command.getName());
}
return result;
}
/**
* 方法签名
*/
public static class MethodSignature {
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
// 如果没参数
return null;
} else if (paramCount == 1) {
return args[params.keySet().iterator().next().intValue()];
} else {
// 否则,返回一个ParamMap,修改参数名,参数名就是其位置
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
// 1.先加一个#{0},#{1},#{2}...参数
param.put(entry.getValue(), args[entry.getKey().intValue()]);
// ...
}
return param;
}
}
}
}
在映射器方法中 MapperMethod#execute 将原来的直接将参数 args 传递给 SqlSession#selectOne 方法,调整为转换后再传递对象。
参数策略处理器
在 Mybatis 的源码包中,有一个 type 包,这个包下所提供的就是一套参数的处理策略集合。它通过定义类型处理器接口、由抽象模板实现并定义标准流程,定提取抽象方法交给子类实现,这些子类就是各个类型处理器的具体实现。
public interface TypeHandler<T> {
/**
* 设置参数
*/
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
}
模板模式
public abstract class BaseTypeHandler<T> implements TypeHandler<T> {
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
// 定义抽象方法,由子类实现不同类型的属性设置
setNonNullParameter(ps, i, parameter, jdbcType);
}
protected abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
}
子类实现
/**
* @description Long类型处理器
*/
public class LongTypeHandler extends BaseTypeHandler<Long> {
@Override
protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException {
ps.setLong(i, parameter);
}
}
/**
* @description String类型处理器
*/
public class StringTypeHandler extends BaseTypeHandler<String>{
@Override
protected void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter);
}
}
这里的接口实现举了个例子,分别是;LongTypeHandler、StringTypeHandler,在 Mybatis 源码中还有很多其他类型,这里我们暂时不需要实现那么多,只要清楚这个处理过程和编码方式即可。
类型注册机
public final class TypeHandlerRegistry {
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>();
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();
public TypeHandlerRegistry() {
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
}
//...
}
里在构造函数中,新增加了 LongTypeHandler、StringTypeHandler 两种类型的注册器。
参数构建
相对于前面章节所完成的内容,这个章节需要对 SqlSourceBuilder 源码构建器中,创建参数映射 ParameterMapping 需要添加参数处理器的内容。
// 构建参数映射
private ParameterMapping buildParameterMapping(String content) {
// 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
Map<String, String> propertiesMap = new ParameterExpression(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (property != null) {
MetaClass metaClass = MetaClass.forClass(parameterType);
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
} else {
propertyType = Object.class;
}
logger.info("构建参数映射 property:{} propertyType:{}", property, propertyType);
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
return builder.build();
}
参数使用
参数构建完成后,就可以在 DefaultSqlSession#selectOne 调用时设置参数使用了。那么这里的链路关系;Executor#query - > SimpleExecutor#doQuery -> StatementHandler#parameterize -> PreparedStatementHandler#parameterize -> ParameterHandler#setParameters 到了 ParameterHandler#setParameters 就可以看到了根据参数的不同处理器循环设置参数。
public class DefaultParameterHandler implements ParameterHandler {
@Override
public void setParameters(PreparedStatement ps) throws SQLException {
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (null != parameterMappings) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
String propertyName = parameterMapping.getProperty();
Object value;
if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
// 通过 MetaObject.getValue 反射取得值设进去
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
JdbcType jdbcType = parameterMapping.getJdbcType();
// 设置参数
logger.info("根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:{}", JSON.toJSONString(value));
TypeHandler typeHandler = parameterMapping.getTypeHandler();
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
测试
配置数据源
<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://127.0.0.1:3306/mybatis?useUnicode=true"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
1:通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
2:在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证.
配置Mapper
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
</select>
<select id="queryUserInfo" parameterType="cn.bugstack.mybatis.test.po.User" resultType="cn.bugstack.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id} and userId = #{userId}
</select>
单元测试
@Before
public void init() throws IOException {
// 1. 从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
sqlSession = sqlSessionFactory.openSession();
}
@Test
public void test_queryUserInfoById() {
// 1. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 2. 测试验证:基本参数
User user = userDao.queryUserInfoById(1L);
logger.info("测试结果:{}", JSON.toJSONString(user));
}
对象类型参数
@Test
public void test_queryUserInfo() {
// 1. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 2. 测试验证:对象参数
User user = userDao.queryUserInfo(new User(1L, "10001"));
logger.info("测试结果:{}", JSON.toJSONString(user));
}
总结
们算是把一个 ORM 框架的基本流程串联起来了,不要硬编码也能完成简单 SQL 的处理。读者伙伴可以仔细阅读下当前框架中,所包含的分包结构。比如:构建、绑定、映射、反射、执行、类型、事务、数据源等等,尝试画一画他们的链接关系,这样会让你更加清晰现在的代码解耦结构。
好了到这里就结束了手写mybatis之细化XML语句构建器,完善静态SQL解析的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;