这个方法需要大家注意一下,如果 Configuration 的 databaseId 不为空,sqlElement 方法会被调用了两次。第一次传入具体的 databaseId,用于解析带有 databaseId 属性,且属性值与此相等的 <sql> 节点。第二次传入的 databaseId 为空,用于解析未配置 databaseId 属性的 <sql> 节点。这里是个小细节,大家注意一下就好。我们继续往下分析。
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception { for (XNode context : list) { // 获取 id 和 databaseId 属性 String databaseId = context.getStringAttribute("databaseId"); String id = context.getStringAttribute("id"); // id = currentNamespace + "." + id id = builderAssistant.applyCurrentNamespace(id, false); // 检测当前 databaseId 和 requiredDatabaseId 是否一致 if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { // 将 <id, XNode> 键值对缓存到 sqlFragments 中 sqlFragments.put(id, context); } } }这个方法逻辑比较简单,首先是获取 <sql> 节点的 id 和 databaseId 属性,然后为 id 属性值拼接命名空间。最后,通过检测当前 databaseId 和 requiredDatabaseId 是否一致,来决定保存还是忽略当前的 <sql> 节点。下面,我们来看一下 databaseId 的匹配逻辑是怎样的。
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { if (requiredDatabaseId != null) { // 当前 databaseId 和目标 databaseId 不一致时,返回 false if (!requiredDatabaseId.equals(databaseId)) { return false; } } else { // 如果目标 databaseId 为空,但当前 databaseId 不为空。两者不一致,返回 false if (databaseId != null) { return false; } /* * 如果当前 <sql> 节点的 id 与之前的 <sql> 节点重复,且先前节点 * databaseId 不为空。则忽略当前节点,并返回 false */ if (this.sqlFragments.containsKey(id)) { XNode context = this.sqlFragments.get(id); if (context.getStringAttribute("databaseId") != null) { return false; } } } return true; }下面总结一下 databaseId 的匹配规则。
databaseId 与 requiredDatabaseId 不一致,即失配,返回 false
当前节点与之前的节点出现 id 重复的情况,若之前的 <sql> 节点 databaseId 属性不为空,返回 false
若以上两条规则均匹配失败,此时返回 true
在上面三条匹配规则中,第二条规则稍微难理解一点。这里简单分析一下,考虑下面这种配置。
<!-- databaseId 不为空 --> <sql databaseId="mysql"> article </sql> <!-- databaseId 为空 --> <sql> article </sql>在上面配置中,两个 <sql> 节点的 id 属性值相同,databaseId 属性不一致。假设 configuration.databaseId = mysql,第一次调用 sqlElement 方法,第一个 <sql> 节点对应的 XNode 会被放入到 sqlFragments 中。第二次调用 sqlElement 方法时,requiredDatabaseId 参数为空。由于 sqlFragments 中已包含了一个 id 节点,且该节点的 databaseId 不为空,此时匹配逻辑返回 false,第二个节点不会被保存到 sqlFragments。
上面的分析内容涉及到了 databaseId,关于 databaseId 的用途,这里简单介绍一下。databaseId 用于标明数据库厂商的身份,不同厂商有自己的 SQL 方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在 <sql> 节点中有什么用呢?这个问题也不难回答。<sql> 节点用于保存 SQL 语句片段,如果 SQL 语句片段中包含方言的话,那么该 <sql> 节点只能被同一 databaseId 的查询语句或更新语句引用。关于 databaseId,这里就介绍这么多。
好了,本节内容先到这里。继续往下分析。
2.1.5 解析 SQL 语句节点前面分析了 <cache>、<cache-ref>、<resultMap> 以及 <sql> 节点,从这一节开始,我们要分析映射文件中剩余的几个节点,分别是 <select>、<insert>、<update> 以及 <delete> 等。这几个节点中存储的是相同的内容,都是 SQL 语句,所以这几个节点的解析过程也是相同的。在进行代码分析之前,这里需要特别说明一下:为了避免和 <sql> 节点混淆,同时也为了描述方便,这里把 <select>、<insert>、<update> 以及 <delete> 等节点统称为 SQL 语句节点。好了,下面开始本节的分析。
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { // 调用重载方法构建 Statement buildStatementFromContext(list, configuration.getDatabaseId()); } // 调用重载方法构建 Statement,requiredDatabaseId 参数为空 buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { // 创建 Statement 建造类 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { /* * 解析 Statement 节点,并将解析结果存储到 * configuration 的 mappedStatements 集合中 */ statementParser.parseStatementNode(); } catch (IncompleteElementException e) { // 解析失败,将解析器放入 configuration 的 incompleteStatements 集合中 configuration.addIncompleteStatement(statementParser); } } }