sql节点用来定义一些可重用的 SQL 语句片段,比如表名,或表的列名等。在映射文件中,我们可以通过include节点引用sql节点定义的内容。

1
2
3
4
5
6
7
8
9
10
<sql id="table">
article
</sql>

<select id="findOne" resultType="Article">
SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
</select>
<update id="update" parameterType="Article">
UPDATE <include refid="table"/> SET title = #{title} WHERE id = #{id}
</update>

上面的配置比较常规,除了静态文本,sql节点还支持属性占位符${}

1
2
3
<sql id="table">
${table_prefix}_article
</sql>

sql 节点的解析过程

1
2
3
4
5
6
7
8
private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {
// 调用 sqlElement 解析 <sql> 节点
sqlElement(list, configuration.getDatabaseId());
}
// 再次调用 sqlElement,不同的是,这次调用,该方法的第二个参数为 null
sqlElement(list, null);
}

如果 Configuration的databaseId不为空,sqlElement方法会被调用了两次。第一次传入具体的 databaseId,用于解析带有databaseId属性,且属性值与此相等的sql节点。第二次传入的 databaseId 为空,用于解析未配置 databaseId 属性的sql节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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是否一致,来决定保存还是忽略当前的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private boolean databaseIdMatchesCurrent(
String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
// 当前 databaseId 和目标 databaseId 不一致时,返回 false
if (!requiredDatabaseId.equals(databaseId)) {
return false;
}
} else {
// 如果目标requiredDatabaseId为空,但当前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 的匹配规则。

  1. databaseId 与 requiredDatabaseId 不一致,即失配,返回 false
  2. 当前节点与之前的节点出现id重复的情况,若之前的sql节点databaseId属性不为空,返回 false
  3. 若以上两条规则均匹配失败,此时返回 true

第二条规则稍微难理解一点

1
2
3
4
5
6
7
8
9
<!-- databaseId 不为空 -->
<sql id="table" databaseId="mysql">
article
</sql>

<!-- databaseId 为空 -->
<sql id="table">
article
</sql>

两个sql节点的 id 属性值相同,databaseId属性不一致,且configuration.databaseId=mysql

  • 第一次调用sqlElement方法,第一个节点对应的 XNode会被放入到 sqlFragments 中。
  • 第二次调用 sqlElement 方法时,requiredDatabaseId 参数为空。
  • 由于 sqlFragments 中已包含了一个id节点,且该节点的databaseId不为空,此时匹配逻辑返回 false,第二个节点不会被保存到 sqlFragments。

databaseId用于标明数据库厂商的身份,不同厂商有自己的SQL方言,MyBatis可以根据 databaseId 执行不同SQL语句。

databaseId 在sql节点中有什么用呢?

sql节点用于保存 SQL 语句片段,如果SQL语句片段中包含方言的话,那么该sql节点只能被同一databaseId 的查询语句或更新语句引用