让我们考虑以下多行SQL字符串:
UPDATE "public"."office"
SET ("address_first", "address_second", "phone") =
(SELECT "public"."employee"."first_name",
"public"."employee"."last_name", ?
FROM "public"."employee"
WHERE "public"."employee"."job_title" = ?
众所周知,在JDK 8之前,我们可以以多种方式将这段SQL包装成Java字符串(字符串字面量)。
在JDK 8之前
可能最常见的方法是使用众所周知的"+"运算符进行直接连接。这样,我们得到如下的多行字符串表示:
String sql =
"UPDATE \"public\".\"office\"\n"
+ "SET (\"address_first\", \"address_second\", \"phone\") =\n"
+ " (SELECT \"public\".\"employee\".\"first_name\",\n"
+ " \"public\".\"employee\".\"last_name\", ?\n"
+ " FROM \"public\".\"employee\"\n"
+ " WHERE \"public\".\"employee\".\"job_title\" = ?";
编译器应该(并且通常是)足够智能,将"+"操作内部转换为StringBuilder/StringBuffer实例,并使用append()方法来构建最终的字符串。但是,我们可以直接使用StringBuilder(非线程安全)或StringBuffer(线程安全),如以下示例所示:
StringBuilder sql = new StringBuilder();
sql.append("UPDATE \"public\".\"office\"\n")
.append("SET ...\n")
.append(" (SELECT...\n")
// ... 省略其他部分
另一种方法(可能不如前两种流行)是使用String.concat()方法。这是一个不可变操作,基本上将给定的字符串附加到当前字符串的末尾。最后,它返回新的组合字符串。尝试附加null值将导致NullPointerException(在前面的两个示例中,我们可以附加null值而不会引发任何异常)。通过链接concat()调用,我们可以像以下示例一样表示多行字符串:
String sql = "UPDATE \"public\".\"office\"\n"
.concat("SET...\n")
.concat(" (SELECT...\n")
// ... 省略其他部分
进一步来说,我们有String.format()方法。只需使用%s格式说明符,我们就可以在多行字符串中连接多个字符串(包括null值),如下所示:
String sql = String.format("%s%s%s%s%s%s",
"UPDATE \"public\".\"office\"\n",
"SET ...\n",
" (SELECT ...\n",
// ... 省略其他部分
虽然这些方法如今仍然很流行,但让我们看看JDK 8在这个话题上有什么要说的。
从JDK 8开始
从JDK 8开始,我们可以使用String.join()方法来表示多行字符串。此方法也专门用于字符串连接,它允许我们的示例具有清晰的可读性。如何实现?此方法将分隔符作为第一个参数,并在要连接的字符串之间使用此分隔符。因此,如果我们认为\n是我们的行分隔符,那么它只需要指定一次,如下所示:
String sql = String.join("\n",
"UPDATE \"public\".\"office\"",
"SET (\"address_first\", \"address_second\", \"phone\") =",
" (SELECT \"public\".\"employee\".\"first_name\",",
" \"public\".\"employee\".\"last_name\", ?",
" FROM \"public\".\"employee\"",
" WHERE \"public\".\"employee\".\"job_title\" = ?;");
除了String.join()方法外,JDK 8还提供了java.util.StringJoiner。StringJoiner支持分隔符(如String.join())但也支持前缀和后缀。表达我们的多行SQL字符串不需要前缀/后缀;因此分隔符仍然是我们最喜欢的功能:
StringJoiner sql = new StringJoiner("\n");
sql.add("UPDATE \"public\".\"office\"")
.add("SET (\"address_first\", ..., \"phone\") =")
.add(" (SELECT \"public\".\"employee\".\"first_name\",")
// ... 省略其他部分
最后,谈到JDK 8就不能不提它的强大的Stream API。更具体地说,我们对Collectors.joining()收集器感兴趣。这个收集器的工作方式与String.join()相同,在我们的例子中,它看起来像这样:
String sql = Stream.of(
"UPDATE \"public\".\"office\"",
"SET (\"address_first\", \"address_second\", \"phone\") =",
" (SELECT \"public\".\"employee\".\"first_name\",",
" \"public\".\"employee\".\"last_name\", ?",
" FROM \"public\".\"employee\"",
" WHERE \"public\".\"employee\".\"job_title\" = ?;")
.collect(Collectors.joining(String.valueOf("\n")));
所有前面的示例都有一个共同的缺点。最重要的是,这些示例中没有一个是真正的多行字符串字面量,每行之间的转义字符和额外引号严重影响了可读性。幸运的是,从JDK 13(作为未来预览)到JDK 15(作为最终功能),新的文本块已成为表示多行字符串字面量的标准。让我们看看如何实现。
引入文本块(JDK 13/15)
JDK 13(JEP 355)引入了一个预览功能,旨在为多行字符串字面量提供支持。在JDK 15(JEP 378)的两个版本中,文本块功能已成为最终且永久可用的功能。但是,让我们快速看看文本块如何塑造我们的多行SQL字符串:
String sql="""
UPDATE "public"."office"
SET ("address_first", "address_second", "phone") =
(SELECT "public"."employee"."first_name",
"public"."employee"."last_name", ?
FROM "public"."employee"
WHERE "public"."employee"."job_title" = ?""";
这太棒了,对吧?!我们立即注意到SQL的可读性已经恢复,我们没有混淆分隔符、行终止符和连接。文本块简洁、易于更新且易于理解。在SQL字符串中额外的代码足迹为零,Java编译器将尽最大努力以尽可能可预测的方式创建字符串。以下是嵌入JSON信息的另一个示例:
String json = """
{
"widget": {
"debug": "on",
"window": {
"title": "Sample Widget 1",
"name": "back_window"
},
// ... 省略其他部分
}""";
那么,如何用文本块表示HTML呢?
String html = """
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>John</td>
<td>Smith</td>
<!-- 这里有一个错误,缺少年龄单元格 -->
</tr>
</table>"""; // 注意这里修正了原始代码中的错误,应该是</table>而不是<table>
挂接文本块语法
文本块的语法相当简单。没有花哨的东西,也没有复杂的事情,只需要记住两个方面:
1. 文本块必须以"""(即三个双引号)和换行符开始。我们称此构造为开放分隔符。
2. 文本块必须以"""(即三个双引号)结束。"""可以单独放在一行(作为新行)或放在文本的最后一行末尾(如我们的示例所示)。我们称此构造为关闭分隔符。但是,这两种方法之间存在语义差异(在下一个问题中详细讨论)。
在此上下文中,以下示例在语法上是正确的:
String tb = """
I'm a text block""";
String tb = """
I'm a text block
""";
// ... 其他正确示例
另一方面,以下示例是不正确的,并会导致编译器错误:
String tb = """I'm a text block"""; // 错误:缺少换行符
// ... 其他错误示例
但是,请考虑以下最佳实践。
通过查看前面的代码片段,我们可以为文本块塑造一个最佳实践:仅在您有多行字符串时使用文本块;如果字符串适合单行代码(如前面的代码片段所示),则使用普通字符串字面量,因为使用文本块不会增加任何显著价值。
在捆绑的代码中,您可以在SQL、JSON和HTML的片段上实践本问题中的所有示例。
对于第三方库支持,请考虑:Apache Commons, StringUtils.join(), 和 Guava Joiner.on()。
接下来,让我们专注于处理文本块分隔符。