使用JDBC API開發過項目的讀者應該知道,當我們需要使用Statement對象執行SQL時,SQL語句會嵌入Java代碼中。SQL語句比較複雜時,我們可能會在代碼中對SQL語句進行拼接,查詢條件不固定時,還需要根據不同條件拼接不同的SQL語句,拼接語句時不要忘記添加必要的空格,還要注意去掉清單最後一個列名的逗号。這個過程對于開發人員來說簡直就是一場噩夢,而且代碼可維護性級低,例如:
String orgSql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON\n" +
"FROM PERSON P, ACCOUNT A\n" +
"INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID\n" +
"INNER JOIN COMPANY C on D.COMPANY_ID = C.ID\n" +
"WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) \n" +
"OR (P.LAST_NAME like ?)\n" +
"GROUP BY P.ID\n" +
"HAVING (P.LAST_NAME like ?) \n" +
"OR (P.FIRST_NAME like ?)\n" +
"ORDER BY P.ID, P.FULL_NAME";
為了解決這個問題,MyBatis中提供了一個SQL工具類。使用這個工具類,我們可以很友善地在Java代碼中動态建構SQL語句。上面的語句如果使用SQL工具類來建構,就會簡單很多。下面是使用MyBatis中的SQL工具類動态建構SQL語句的案例
String newSql = new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
FROM("PERSON P");
FROM("ACCOUNT A");
INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
WHERE("P.ID = A.ID");
WHERE("P.FIRST_NAME like ?");
OR();
WHERE("P.LAST_NAME like ?");
GROUP_BY("P.ID");
HAVING("P.LAST_NAME like ?");
OR();
HAVING("P.FIRST_NAME like ?");
ORDER_BY("P.ID");
ORDER_BY("P.FULL_NAME");
}}.toString();
如上面的代碼所示,建立了一個匿名的SQL類的子類,在匿名子類的初始化代碼塊中,調用SELECT()、FROM()等方法建構SQL語句,這種方式能夠很好地避免字元串拼接過程中缺少空格或者偶然間重複出現的AND關鍵字導緻的SQL語句不正确。
除了SELECT語句外,SQL工具類也可以用作建構UPDATE、INSERT等語句。下面是SQL工具類的一些使用案例:
@Test
public void testInsertSql() {
String insertSql = new SQL().
INSERT_INTO("PERSON").
VALUES("ID, FIRST_NAME", "#{id}, #{firstName}").
VALUES("LAST_NAME", "#{lastName}").toString();
System.out.println(insertSql);
}
@Test
public void testDeleteSql() {
String deleteSql = new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
System.out.println(deleteSql);
}
@Test
public void testUpdateSql() {
String updateSql = new SQL() {{
UPDATE("PERSON");
SET("FIRST_NAME = #{firstName}");
WHERE("ID = #{id}");
}}.toString();
System.out.println(updateSql);
}
使用SQL工具類的另一個好處是可以很友善地在Java代碼中根據條件動态地拼接SQL語句,例如:
public String selectPerson(final String id, final String firstName, final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD");
SELECT("P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID = #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME = #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME = #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
SQL工具類中提供的所有方法及作用可參考表的内容。
方法 | 描述 |
SELECT(String) SELECT(String...) | 開始一個SELECT子句或将内容追加到SELECT子句。方法可以被多次調用,參數也會添加到 SELECT 子句。參數通常是使用逗号分隔的列名或列的别名清單,也可以是資料庫驅動程式接收的任意關鍵字 |
SELECT DISTINCT(String) SELECT DISTINCT(String...) | 開始一個SELECT子句或将内容追加到 SELECT子句。同時可以插入DISTINCT 關鍵字到SELECT 語句中。方法可以被多次調用,參數也會添加到SELECT子句。參數通常使用逗号分隔的列名或者列的别名清單,也可以是資料庫驅動程式接收的任意關鍵字 |
FROM(String) FROM(String...) | 開始或插入 FROM子句。方法可以被多次調用,參數會添加到 FROM子句。參數通常是表名或别名,也可以是資料庫驅動程式接收的任意關鍵字 |
JOIN(String) JOIN(String...) INNERJOIN(String) INNERJOIN(String...) LEFT OUTER JOIN(String) LEFT OUTER JOIN(String...) RIGHT OUTERJOIN(String) RIGHT OUTER JOIN(String....) | 根據不同的方法添加對應類型的JOIN子句,例如INNERJOINO方法添加INNER JOIN 子句,LEFT OUTER JOINO方法添加 LEFT JOIN子句。參數可以包含由列命和JOINON條件組合成的标準JOIN |
WHERE(String) WHERE(String...) | 插入新的 WHERE 子句條件,并通過AND 關鍵字連接配接。方法可以多次被調用,每次都由AND 來連接配接新條件。使用 OR0方法可以追加 OR關鍵字 |
OR() | 使用OR來分隔目前的 WHERE 子句條件。可以被多次調用,但在一行中多次調用可能生成錯誤的 SQL語句 |
AND() | 使用AND 來分隔目前的 WHERE 子句條件。可以被多次調用,但在一行中多次調用可能會生成錯誤的 SQL 語句。這個方法使用較少,因為WHEREO和HAVINGO方法都會自動追加 AND,隻有必要時才會額外調用AND0方法 |
GROUP BY(String) GROUP BY(String...) | 插入新的 GROUP BY 子句,通過逗号連接配接。方法可以被多次調用每次都會使用逗号連接配接新的條件 |
HAVING(String) HAVING(String...) | 插入新的 HAVING子句條件。由AND 關鍵字連接配接。方法可以被多次調用,每次都由 AND 來連接配接新的條件 |
ORDER BY(String) ORDER BY(String...) | 插入新的 ORDER BY 子句元素,由逗号連接配接。可以多次被調用,每次都由逗号連接配接新的條件 |
DELETE FROM(String) | 開始一個DELETE 語句并指定表名。通常它後面都會跟着 WHERE語句 |
INSERT INTO(String) | 開始一個 INSERT語句并指定表名,後面都會跟着一個或者多個VALUES0,或者INTO COLUMNSO和INTO VALUES() |
SET(String) SET(String...) | 針對UPDATE語句,插入SET子句中 |
UPDATE(String) | 開始一個UPDATE 語句并指定需要更新的表名。後面都會跟着一個或者多個 SETO方法,通常會有一個或多個 WHEREO方法 |
VALUES(String,String) | 插入 INSERT 語句中,第一個參數是要插入的列名,第二個參數則是該列的值 |
INTO COLUMNS(String...) | 追加字段到INSERT子句中,該方法必須和INTOVALUESO聯合使用 |
INTO VALUES(String...) | 追加字段值到INSERT子句中,該方法必須和INTOCOLUMNSO方法聯合使用 |
在學習完SQL工具類的使用後,接下來我們簡單地了解一下SQL工具類的實作源碼。SQL繼承至AbstractSQL類,隻重寫了該類的getSelf()方法,代碼如下:
public class SQL extends AbstractSQL<SQL> {
@Override
public SQL getSelf() {
return this;
}
}
所有的功能由AbstractSQL類完成,AbstractSQL類中維護了一個SQLStatement内部類的執行個體和一系列前面提到過的構造SQL語句的方法,例如SELECT()、UPDATE()等方法。AbstractSQL類的部分代碼如下:
private static final String AND = ") \nAND (";
private static final String OR = ") \nOR (";
private final SQLStatement sql = new SQLStatement();
public abstract T getSelf();
public T UPDATE(String table) {
sql().statementType = SQLStatement.StatementType.UPDATE;
sql().tables.add(table);
return getSelf();
}
public T SET(String sets) {
sql().sets.add(sets);
return getSelf();
}
SQLStatement内部類用于描述一個SQL語句,該類中通過StatementType确定SQL語句的類型。SQLStatement類中還維護了一系列的ArrayList屬性,當調用SELECT()、UPDATE()等方法時,這些方法的參數内容會記錄在這些ArrayList對象中,SQLStatement類中的屬性如下:
private static class SQLStatement {
public enum StatementType {
DELETE, INSERT, SELECT, UPDATE
}
StatementType statementType;
List<String> sets = new ArrayList<>();
List<String> select = new ArrayList<>();
List<String> tables = new ArrayList<>();
List<String> join = new ArrayList<>();
List<String> innerJoin = new ArrayList<>();
List<String> outerJoin = new ArrayList<>();
List<String> leftOuterJoin = new ArrayList<>();
List<String> rightOuterJoin = new ArrayList<>();
List<String> where = new ArrayList<>();
List<String> having = new ArrayList<>();
List<String> groupBy = new ArrayList<>();
List<String> orderBy = new ArrayList<>();
List<String> lastList = new ArrayList<>();
List<String> columns = new ArrayList<>();
List<String> values = new ArrayList<>();
boolean distinct;
public SQLStatement() {
// Prevent Synthetic Access
}
private void sqlClause(SafeAppendable builder, String keyword, List<String> parts, String open, String close,
String conjunction) {
if (!parts.isEmpty()) {
if (!builder.isEmpty()) {
builder.append("\n");
}
builder.append(keyword);
builder.append(" ");
builder.append(open);
String last = "________";
for (int i = 0, n = parts.size(); i < n; i++) {
String part = parts.get(i);
if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
builder.append(conjunction);
}
builder.append(part);
last = part;
}
builder.append(close);
}
}
private String selectSQL(SafeAppendable builder) {
if (distinct) {
sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");
} else {
sqlClause(builder, "SELECT", select, "", "", ", ");
}
sqlClause(builder, "FROM", tables, "", "", ", ");
joins(builder);
sqlClause(builder, "WHERE", where, "(", ")", " AND ");
sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");
sqlClause(builder, "HAVING", having, "(", ")", " AND ");
sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");
return builder.toString();
}
private void joins(SafeAppendable builder) {
sqlClause(builder, "JOIN", join, "", "", "\nJOIN ");
sqlClause(builder, "INNER JOIN", innerJoin, "", "", "\nINNER JOIN ");
sqlClause(builder, "OUTER JOIN", outerJoin, "", "", "\nOUTER JOIN ");
sqlClause(builder, "LEFT OUTER JOIN", leftOuterJoin, "", "", "\nLEFT OUTER JOIN ");
sqlClause(builder, "RIGHT OUTER JOIN", rightOuterJoin, "", "", "\nRIGHT OUTER JOIN ");
}
private String insertSQL(SafeAppendable builder) {
sqlClause(builder, "INSERT INTO", tables, "", "", "");
sqlClause(builder, "", columns, "(", ")", ", ");
sqlClause(builder, "VALUES", values, "(", ")", ", ");
return builder.toString();
}
private String deleteSQL(SafeAppendable builder) {
sqlClause(builder, "DELETE FROM", tables, "", "", "");
sqlClause(builder, "WHERE", where, "(", ")", " AND ");
return builder.toString();
}
private String updateSQL(SafeAppendable builder) {
sqlClause(builder, "UPDATE", tables, "", "", "");
joins(builder);
sqlClause(builder, "SET", sets, "", "", ", ");
sqlClause(builder, "WHERE", where, "(", ")", " AND ");
return builder.toString();
}
public String sql(Appendable a) {
SafeAppendable builder = new SafeAppendable(a);
if (statementType == null) {
return null;
}
String answer;
switch (statementType) {
case DELETE:
answer = deleteSQL(builder);
break;
case INSERT:
answer = insertSQL(builder);
break;
case SELECT:
answer = selectSQL(builder);
break;
case UPDATE:
answer = updateSQL(builder);
break;
default:
answer = null;
}
return answer;
}
}
AbstrastSQL類重寫了toString()方法,該方法中會調用SQLStatement對象的sql()方法生成SQL字元串,代碼如下:
public String toString() {
StringBuilder sb = new StringBuilder();
sql().sql(sb);
return sb.toString();
}
sql(Appendable a)方法中會判斷SQL語句的類型,以UPDATE語句為例,會調用SQLStatement對象的updateSql()方法生成UPDATE語句。updateSql()方法代碼如下:
private String updateSQL(SafeAppendable builder) {
sqlClause(builder, "UPDATE", tables, "", "", "");// update語句
joins(builder); // join語句
sqlClause(builder, "SET", sets, "", "", ", "); // set語句
sqlClause(builder, "WHERE", where, "(", ")", " AND ");// where 語句
return builder.toString();
}
如上面的代碼所示,updateSql()方法中,最終會調用sqlCalause()方法完成SQL語句的拼接。sqlCalause()方法實作代碼如下:
/**
* SQL語句拼接
* @param builder SQL 字元串建構對象
* @param keyword SOL 關鍵甯
* @param parts SOL關鍵字子句内容
* @param open SQL 關鍵字後開始字元
* @param close SOL 關鍵字後結束字元Cparam
* @param conjunction SQL連接配接關鍵字,通常為AND或OR
*/
private void sqlClause(SafeAppendable builder, String keyword, List<String> parts, String open, String close,
String conjunction) {
if (!parts.isEmpty()) {
if (!builder.isEmpty()) {
builder.append("\n");
}
//拼接 SQL 關鍵字
builder.append(keyword);
builder.append(" ");
//拼接關鍵字後開始字元
builder.append(open);
String last = "________";
for (int i = 0, n = parts.size(); i < n; i++) {
String part = parts.get(i);
//如果SQL 關鍵字對應的子句内容不為OR或AND,則追加連接配接關鍵字
if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
builder.append(conjunction);
}
//追加子句内容
builder.append(part);
//追加關鍵字後結束字元
last = part;
}
builder.append(close);
}
}