手写mybatis之细化XML语句构建器,完善静态SQL解析

时间:2024-10-10 07:14:59

前言


实现到本章节前,关于 Mybatis ORM 框架的大部分核心结构已经逐步体现出来了,包括;解析、绑定、映射、事务、执行、数据源等。但随着更多功能的逐步完善,我们需要对模块内的实现进行细化处理,而不单单只是完成功能逻辑。这就有点像把 CRUD 使用设计原则进行拆分解耦,满足代码的易维护和可扩展性。而这里我们首先着手要处理的就是关于 XML 解析的问题,把之前粗糙的实现进行细化,满足我们对解析时一些参数的整合和处理。
在这里插入图片描述
对于 XML 信息的读取,各个功能模块的流程上应该符合单一职责,而每一个具体的实现又得具备迪米特法则,这样实现出来的功能才能具有良好的扩展性。通常这类代码也会看着很干净 那么基于这样的诉求,我们则需要给解析过程中,所属解析的不同内容,按照各自的职责类进行拆解和串联调用。
在这里插入图片描述
解耦映射解析
提供单独的 XML 映射构建器 XMLMapperBuilder 类,把关于 Mapper 内的 SQL 进行解析处理。提供了这个类以后,就可以把这个类的操作放到 XML 配置构建器,XMLConfigBuilder#mapperElement 中进行使用了。

public class XMLMapperBuilder extends BaseBuilder {

    /**
     * 解析
     */
    public void parse() throws Exception {
        // 如果当前资源没有加载过再加载,防止重复加载
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(element);
            // 标记一下,已经加载过了
            configuration.addLoadedResource(resource);
            // 绑定映射器到namespace
            configuration.addMapper(Resources.classForName(currentNamespace));
        }
    }

    // 配置mapper元素
    // <mapper namespace="org.mybatis.example.BlogMapper">
    //   <select id="selectBlog" parameterType="int" resultType="Blog">
    //    select * from Blog where id = #{id}
    //   </select>
    // </mapper>
    private void configurationElement(Element element) {
        // 1.配置namespace
        currentNamespace = element.attributeValue("namespace");
        if (currentNamespace.equals("")) {
            throw new RuntimeException("Mapper's namespace cannot be empty");
        }

        // 2.配置select|insert|update|delete
        buildStatementFromContext(element.elements("select"));
    }

    // 配置select|insert|update|delete
    private void buildStatementFromContext(List<Element> list) {
        for (Element element : list) {
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, element, currentNamespace);
            statementParser.parseStatementNode();
        }
    }

}

在 XMLMapperBuilder#parse 的解析中,主要体现在资源解析判断、Mapper解析和绑定映射器到;

1:configuration.isResourceLoaded 资源判断避免重复解析,做了个记录。
2:configuration.addMapper 绑定映射器主要是把 namespace 3:cn.bugstack.mybatis.test.dao.IUserDao 绑定到 Mapper 上。也就是注册到映射器注册机里。
4:configurationElement 方法调用的 buildStatementFromContext,重在处理 XML 语句构建器,下文中单独讲解。

public class XMLConfigBuilder extends BaseBuilder {

    /*
     * <mappers>
     *	 <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
     *	 <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
     *	 <mapper resource="org/mybatis/builder/PostMapper.xml"/>
     * </mappers>
     */
    private void mapperElement(Element mappers) throws Exception {
        List<Element> mapperList = mappers.elements("mapper");
        for (Element e : mapperList) {
            String resource = e.attributeValue("resource");
            InputStream inputStream = Resources.getResourceAsStream(resource);

            // 在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource);
            mapperParser.parse();
        }
    }

}

语句构建器
XMLStatementBuilder 语句构建器主要解析 XML 中 select|insert|update|delete 中的语句,当前我们先以 select 解析为案例。

public class XMLStatementBuilder extends BaseBuilder {

    //解析语句(select|insert|update|delete)
    //<select
    //  id="selectPerson"
    //  parameterType="int"
    //  parameterMap="deprecated"
    //  resultType="hashmap"
    //  resultMap="personResultMap"
    //  flushCache="false"
    //  useCache="true"
    //  timeout="10000"
    //  fetchSize="256"
    //  statementType="PREPARED"
    //  resultSetType="FORWARD_ONLY">
    //  SELECT * FROM PERSON WHERE ID = #{id}
    //</select>
    public void parseStatementNode() {
        String id = element.attributeValue("id");
        // 参数类型
        String parameterType = element.attributeValue("parameterType");
        Class<?> parameterTypeClass = resolveAlias(parameterType);
        // 结果类型
        String resultType = element.attributeValue("resultType");
        Class<?> resultTypeClass = resolveAlias(resultType);
        // 获取命令类型(select|insert|update|delete)
        String nodeName = element.getName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

        // 获取默认语言驱动器
        Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();
        LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);

        SqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);

        MappedStatement mappedStatement = new MappedStatement.Builder(configuration, currentNamespace + "." + id, sqlCommandType, sqlSource, resultTypeClass).build();

        // 添加解析 SQL
        configuration.addMappedStatement(mappedStatement);
    }

}

脚本语言驱动
在 XMLStatementBuilder#parseStatementNode 语句构建器的解析中,可以看到这么一块,获取默认语言驱动器并解析SQL的操作。其实这部分就是 XML 脚步语言驱动器所实现的功能,在 XMLScriptBuilder 中处理静态SQL和动态SQL。
定义接口

public interface LanguageDriver {

    SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType);

}

XML语言驱动器实现

public class XMLLanguageDriver implements LanguageDriver {

    @Override
    public SqlSource createSqlSource(Configuration configuration, Element script, Class<?> parameterType) {
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
    }

}

XML脚本构建器解析

public class XMLScriptBuilder extends BaseBuilder {

    public SqlSource parseScriptNode() {
        List<SqlNode> contents = parseDynamicTags(element);
        MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
        return new RawSqlSource(configuration, rootSqlNode, parameterType);
    }

    List<SqlNode> parseDynamicTags(Element element) {
        List<SqlNode> contents = new ArrayList<>();
        // element.getText 拿到 SQL
        String data = element.getText();
        contents.add(new StaticTextSqlNode(data));
        return contents;
    }

}

SQL源码构建器

public class SqlSourceBuilder extends BaseBuilder {

    private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

    public SqlSourceBuilder(Configuration configuration) {
        super(configuration);
    }

    public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        String sql = parser.parse(originalSql);
        // 返回静态 SQL
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
    }

    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
       
        @Override
        public String handleToken(String content) {
            parameterMappings.add(buildParameterMapping(content));
            return "?";
        }

        // 构建参数映射
        private ParameterMapping buildParameterMapping(String content) {
            // 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR}
            Map<String, String> propertiesMap = new ParameterExpression(content);
            String property = propertiesMap.get("property");
            Class<?> propertyType = parameterType;
            ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
            return builder.build();
        }

    }
    
}

DefaultSqlSession 调用调整
因为以上整个设计和实现,调整了解析过程,以及细化了 SQL 的创建。那么在 MappedStatement 映射语句中,则使用 SqlSource 替换了 BoundSql,所以在 DefaultSqlSession 中也会有相应的调整。

public class DefaultSqlSession implements SqlSession {

    private Configuration configuration;
    private Executor executor;

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        List<T> list = executor.query(ms, parameter, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
        return list.get(0);
    }

}

测试
配置数据源

<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>

单元测试

@Test
public void test() throws IOException {
    // 1. 从SqlSessionFactory中获取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
   
    // 2. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 测试验证
    User user = userDao.queryUserInfoById(1L);
    logger.info("测试结果:{}", JSON.toJSONString(user));
}

总结
1:我们就像是去把原本 CRUD 的代码,通过设计原则进行拆分和解耦,运用不用的类来承担不同的职责,完整整个功能的实现。这包括;映射构建器、语句构建器、源码构建器的综合使用,以及对应的引用;脚本语言驱动和脚本构建器解析,处理我们的 XML 中的 SQL 语句。
2:通过这样的重构代码,也能让我们对平常的业务开发中的大片面向过程的流程代码有所感悟,当你可以细分拆解职责功能到不同的类中去以后,你的代码会更加的清晰并易于维护。

好了到这里就结束了手写mybatis之细化XML语句构建器,完善静态SQL解析的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;