MyBatis官方文件——SQL語句構建及日誌部分
文章目錄
SQL 語句構建器
1、問題
Java 程式設計師面對的最痛苦的事情之一就是在 Java 程式碼中嵌入 SQL 語句。這通常是因為需要動態生成 SQL 語句,不然我們可以將它們放到外部檔案或者儲存過程中。如你所見,MyBatis 在 XML 對映中具備強大的 SQL 動態生成能力。但有時,我們還是需要在 Java 程式碼裡構建 SQL 語句。此時,MyBatis 有另外一個特性可以幫到你,讓你從處理典型問題中解放出來,比如加號、引號、換行、格式化問題、嵌入條件的逗號管理及 AND 連線。確實,在 Java 程式碼中動態生成 SQL 程式碼真的就是一場噩夢。例如:
String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
"P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
"FROM PERSON P, 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 AND P.FIRST_NAME like ?) " +
"OR (P.LAST_NAME like ?) " +
"GROUP BY P.ID " +
"HAVING (P.LAST_NAME like ?) " +
"OR (P.FIRST_NAME like ?) " +
"ORDER BY P.ID, P.FULL_NAME";
2、解決方案
MyBatis 3 提供了方便的工具類來幫助解決此問題。藉助 SQL 類,我們只需要簡單地建立一個例項,並呼叫它的方法即可生成 SQL 語句。讓我們來用 SQL 類重寫上面的例子:
private String selectPersonSql() {
return 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();
}
這個例子有什麼特別之處嗎?仔細看一下你會發現,你不用擔心可能會重複出現的 “AND” 關鍵字,或者要做出用 “WHERE” 拼接還是 “AND” 拼接還是不用拼接的選擇。SQL 類已經為你處理了哪裡應該插入 “WHERE”、哪裡應該使用 “AND” 的問題,並幫你完成所有的字串拼接工作。
批註:
該場景用於需要在 Java 程式碼裡構建 SQL 語句,當然我目前還沒有接觸過,但是這個道理卻十分好明白。當我們需要使用Java程式碼來構建SQL語句的時候,如果使用傳統的方式來構建,在編寫SQL上會遇到諸多的程式碼編寫上的不順利,基於此,MyBatis給出了自己的解決方案,即提供一個工具類來解決我們遇到的諸多的不順。
使用MyBatis的工具類以後,雖然我們編寫的SQL相關的程式碼沒有太大的區別,但對SQL片段與SQL片段之間有關連線的部分程式碼卻進行改良。即用MyBatis工具類中的連線方式進行連線,能夠有效的避免常規的用Java程式碼構建SQL語句時出現的連線問題。
3、SQL 類
這裡有一些示例:
// 匿名內部類風格
public String deletePersonSql() {
return new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
}
// Builder / Fluent 風格
public String insertPersonSql() {
String sql = new SQL()
.INSERT_INTO("PERSON")
.VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
.VALUES("LAST_NAME", "#{lastName}")
.toString();
return sql;
}
// 動態條件(注意引數需要使用 final 修飾,以便匿名內部類對它們進行訪問)
public String selectPersonLike(final String id, final String firstName, final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID like #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
public String deletePersonSql() {
return new SQL() {{
DELETE_FROM("PERSON");
WHERE("ID = #{id}");
}}.toString();
}
public String insertPersonSql() {
return new SQL() {{
INSERT_INTO("PERSON");
VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
VALUES("LAST_NAME", "#{lastName}");
}}.toString();
}
public String updatePersonSql() {
return new SQL() {{
UPDATE("PERSON");
SET("FIRST_NAME = #{firstName}");
WHERE("ID = #{id}");
}}.toString();
}
方法 | 描述 |
---|---|
SELECT(String) SELECT(String…) | 開始新的或追加到已有的 SELECT 子句。可以被多次呼叫,引數會被追加到 SELECT 子句。 引數通常使用逗號分隔的列名和別名列表,但也可以是資料庫驅動程式接受的任意引數。 |
SELECT_DISTINCT(String) SELECT_DISTINCT(String…) | 開始新的或追加到已有的 SELECT 子句,並新增 DISTINCT 關鍵字到生成的查詢中。可以被多次呼叫,引數會被追加到 SELECT 子句。 引數通常使用逗號分隔的列名和別名列表,但也可以是資料庫驅動程式接受的任意引數。 |
FROM(String) FROM(String…) | 開始新的或追加到已有的 FROM 子句。可以被多次呼叫,引數會被追加到 FROM 子句。 引數通常是一個表名或別名,也可以是資料庫驅動程式接受的任意引數。 |
JOIN(String) JOIN(String…) INNER_JOIN(String) INNER_JOIN(String…) LEFT_OUTER_JOIN(String) LEFT_OUTER_JOIN(String…) RIGHT_OUTER_JOIN(String) RIGHT_OUTER_JOIN(String…) | 基於呼叫的方法,新增新的合適型別的 JOIN 子句。 引數可以包含一個由列和連線條件構成的標準連線。 |
WHERE(String) WHERE(String…) | 插入新的 WHERE 子句條件,並使用 AND 拼接。可以被多次呼叫,對於每一次呼叫產生的新條件,會使用 AND 拼接起來。要使用 OR 分隔,請使用 OR() 。 |
OR() | 使用 OR 來分隔當前的 WHERE 子句條件。 可以被多次呼叫,但在一行中多次呼叫會生成錯誤的 SQL 。 |
AND() | 使用 AND 來分隔當前的 WHERE 子句條件。 可以被多次呼叫,但在一行中多次呼叫會生成錯誤的 SQL 。由於 WHERE 和 HAVING 都會自動使用 AND 拼接, 因此這個方法並不常用,只是為了完整性才被定義出來。 |
GROUP_BY(String) GROUP_BY(String…) | 追加新的 GROUP BY 子句,使用逗號拼接。可以被多次呼叫,每次呼叫都會使用逗號將新的條件拼接起來。 |
HAVING(String) HAVING(String…) | 追加新的 HAVING 子句。使用 AND 拼接。可以被多次呼叫,每次呼叫都使用AND 來拼接新的條件。要使用 OR 分隔,請使用 OR() 。 |
ORDER_BY(String) ORDER_BY(String…) | 追加新的 ORDER BY 子句,使用逗號拼接。可以多次被呼叫,每次呼叫會使用逗號拼接新的條件。 |
LIMIT(String) LIMIT(int) | 追加新的 LIMIT 子句。 僅在 SELECT()、UPDATE()、DELETE() 時有效。 當在 SELECT() 中使用時,應該配合 OFFSET() 使用。(於 3.5.2 引入) |
OFFSET(String) OFFSET(long) | 追加新的 OFFSET 子句。 僅在 SELECT() 時有效。 當在 SELECT() 時使用時,應該配合 LIMIT() 使用。(於 3.5.2 引入) |
OFFSET_ROWS(String) OFFSET_ROWS(long) | 追加新的 OFFSET n ROWS 子句。 僅在 SELECT() 時有效。 該方法應該配合 FETCH_FIRST_ROWS_ONLY() 使用。(於 3.5.2 加入) |
FETCH_FIRST_ROWS_ONLY(String) FETCH_FIRST_ROWS_ONLY(int) | 追加新的 FETCH FIRST n ROWS ONLY 子句。 僅在 SELECT() 時有效。 該方法應該配合 OFFSET_ROWS() 使用。(於 3.5.2 加入) |
DELETE_FROM(String) | 開始新的 delete 語句,並指定刪除表的表名。通常它後面都會跟著一個 WHERE 子句! |
INSERT_INTO(String) | 開始新的 insert 語句,並指定插入資料表的表名。後面應該會跟著一個或多個 VALUES() 呼叫,或 INTO_COLUMNS() 和 INTO_VALUES() 呼叫。 |
SET(String) SET(String…) | 對 update 語句追加 “set” 屬性的列表 |
UPDATE(String) | 開始新的 update 語句,並指定更新表的表名。後面都會跟著一個或多個 SET() 呼叫,通常也會有一個 WHERE() 呼叫。 |
VALUES(String, String) | 追加資料值到 insert 語句中。第一個引數是資料插入的列名,第二個引數則是資料值。 |
INTO_COLUMNS(String…) | 追加插入列子句到 insert 語句中。應與 INTO_VALUES() 一同使用。 |
INTO_VALUES(String…) | 追加插入值子句到 insert 語句中。應與 INTO_COLUMNS() 一同使用。 |
ADD_ROW() | 新增新的一行資料,以便執行批量插入。(於 3.5.2 引入) |
提示 注意,SQL 類將原樣插入 LIMIT
、OFFSET
、OFFSET n ROWS
以及 FETCH FIRST n ROWS ONLY
子句。換句話說,類庫不會為不支援這些子句的資料庫執行任何轉換。 因此,使用者應該要了解目標資料庫是否支援這些子句。如果目標資料庫不支援這些子句,產生的 SQL 可能會引起執行錯誤。
從版本 3.4.2 開始,你可以像下面這樣使用可變長度引數:
public String selectPersonSql() {
return new SQL()
.SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
.FROM("PERSON P", "ACCOUNT A")
.INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
.WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
.ORDER_BY("P.ID", "P.FULL_NAME")
.toString();
}
public String insertPersonSql() {
return new SQL()
.INSERT_INTO("PERSON")
.INTO_COLUMNS("ID", "FULL_NAME")
.INTO_VALUES("#{id}", "#{fullName}")
.toString();
}
public String updatePersonSql() {
return new SQL()
.UPDATE("PERSON")
.SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
.WHERE("ID = #{id}")
.toString();
}
從版本 3.5.2 開始,你可以像下面這樣構建批量插入語句:
public String insertPersonsSql() {
// INSERT INTO PERSON (ID, FULL_NAME)
// VALUES (#{mainPerson.id}, #{mainPerson.fullName}) , (#{subPerson.id}, #{subPerson.fullName})
return new SQL()
.INSERT_INTO("PERSON")
.INTO_COLUMNS("ID", "FULL_NAME")
.INTO_VALUES("#{mainPerson.id}", "#{mainPerson.fullName}")
.ADD_ROW()
.INTO_VALUES("#{subPerson.id}", "#{subPerson.fullName}")
.toString();
}
從版本 3.5.2 開始,你可以像下面這樣構建限制返回結果數的 SELECT 語句,:
public String selectPersonsWithOffsetLimitSql() {
// SELECT id, name FROM PERSON
// LIMIT #{limit} OFFSET #{offset}
return new SQL()
.SELECT("id", "name")
.FROM("PERSON")
.LIMIT("#{limit}")
.OFFSET("#{offset}")
.toString();
}
public String selectPersonsWithFetchFirstSql() {
// SELECT id, name FROM PERSON
// OFFSET #{offset} ROWS FETCH FIRST #{limit} ROWS ONLY
return new SQL()
.SELECT("id", "name")
.FROM("PERSON")
.OFFSET_ROWS("#{offset}")
.FETCH_FIRST_ROWS_ONLY("#{limit}")
.toString();
}
4、SqlBuilder 和 SelectBuilder (已經廢棄)
在版本 3.2 之前,我們的實現方式不太一樣,我們利用 ThreadLocal 變數來掩蓋一些對 Java DSL 不太友好的語言限制。現在,現代 SQL 構建框架使用的構建器和匿名內部類思想已被人們所熟知。因此,我們廢棄了基於這種實現方式的 SelectBuilder 和 SqlBuilder 類。
下面的方法僅僅適用於廢棄的 SqlBuilder 和 SelectBuilder 類。
方法 | 描述 |
---|---|
BEGIN() RESET() | 這些方法清空 SelectBuilder 類的 ThreadLocal 狀態,並準備好構建一個新的語句。開始新的語句時,BEGIN() 是最名副其實的(可讀性最好的)。但如果由於一些原因(比如程式邏輯在某些條件下需要一個完全不同的語句),在執行過程中要重置語句構建狀態,就很適合使用 RESET() 。 |
SQL() | 該方法返回生成的 SQL() 並重置 SelectBuilder 狀態(等價於呼叫了 BEGIN() 或 RESET() )。因此,該方法只能被呼叫一次! |
SelectBuilder 和 SqlBuilder 類並不神奇,但最好還是知道它們的工作原理。 SelectBuilder 以及 SqlBuilder 藉助靜態匯入和 ThreadLocal 變數實現了對插入條件友好的簡潔語法。要使用它們,只需要靜態匯入這個類的方法即可,就像這樣(只能使用其中的一條,不能同時使用):
import static org.apache.ibatis.jdbc.SelectBuilder.*;
import static org.apache.ibatis.jdbc.SqlBuilder.*;
然後就可以像下面這樣建立一些方法:
/* 已被廢棄 */
public String selectBlogsSql() {
BEGIN(); // 重置 ThreadLocal 狀態變數
SELECT("*");
FROM("BLOG");
return SQL();
}
/* 已被廢棄 */
private String selectPersonSql() {
BEGIN(); // 重置 ThreadLocal 狀態變數
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");
return SQL();
}
批註:
該方式已經廢棄,但是還是有必要了解一下它的原理。
在使用Java類構建SQL語句的時候,我們需要使用BEGIN或者RESET方法來初始化對應的狀態。
日誌
Mybatis 通過使用內建的日誌工廠提供日誌功能。內建日誌工廠將會把日誌工作委託給下面的實現之一:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
MyBatis 內建日誌工廠會基於執行時檢測資訊選擇日誌委託實現。它會(按上面羅列的順序)使用第一個查詢到的實現。當沒有找到這些實現時,將會禁用日誌功能。
不少應用伺服器(如 Tomcat 和 WebShpere)的類路徑中已經包含 Commons Logging。注意,在這種配置環境下,MyBatis 會把 Commons Logging 作為日誌工具。這就意味著在諸如 WebSphere 的環境中,由於提供了 Commons Logging 的私有實現,你的 Log4J 配置將被忽略。這個時候你就會感覺很鬱悶:看起來 MyBatis 將你的 Log4J 配置忽略掉了(其實是因為在這種配置環境下,MyBatis 使用了 Commons Logging 作為日誌實現)。如果你的應用部署在一個類路徑已經包含 Commons Logging 的環境中,而你又想使用其它日誌實現,你可以通過在 MyBatis 配置檔案 mybatis-config.xml 裡面新增一項 setting 來選擇其它日誌實現。
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>
可選的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是實現了 org.apache.ibatis.logging.Log
介面,且構造方法以字串為引數的類完全限定名。
你也可以呼叫以下任一方法來選擇日誌實現:
org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
org.apache.ibatis.logging.LogFactory.useLog4JLogging();
org.apache.ibatis.logging.LogFactory.useJdkLogging();
org.apache.ibatis.logging.LogFactory.useCommonsLogging();
org.apache.ibatis.logging.LogFactory.useStdOutLogging();
你應該在呼叫其它 MyBatis 方法之前呼叫以上的某個方法。另外,僅當執行時類路徑中存在該日誌實現時,日誌實現的切換才會生效。如果你的環境中並不存在 Log4J,你卻試圖呼叫了相應的方法,MyBatis 就會忽略這一切換請求,並將以預設的查詢順序決定使用的日誌實現。
關於 SLF4J、Apache Commons Logging、Apache Log4J 和 JDK Logging 的 API 介紹不在本文件介紹範圍內。不過,下面的例子可以作為一個快速入門。有關這些日誌框架的更多資訊,可以參考以下連結:
1、日誌配置
你可以通過在包、對映類的全限定名、名稱空間或全限定語句名上開啟日誌功能,來檢視 MyBatis 的日誌語句。
再次提醒,具體配置步驟取決於日誌實現。接下來我們會以 Log4J 作為示範。配置日誌功能非常簡單:新增一個或多個配置檔案(如 log4j.properties),有時還需要新增 jar 包(如 log4j.jar)。下面的例子將使用 Log4J 來配置完整的日誌服務。一共兩個步驟:
步驟 1:新增 Log4J 的 jar 包
由於我們使用的是 Log4J,我們要確保它的 jar 包可以被應用使用。為此,需要將 jar 包新增到應用的類路徑中。Log4J 的 jar 包可以在上面的連結中下載。
對於 web 應用或企業級應用,你可以將 log4j.jar
新增到 WEB-INF/lib
目錄下;對於獨立應用,可以將它新增到 JVM 的 -classpath
啟動引數中。
步驟 2:配置 Log4J
配置 Log4J 比較簡單。假設你需要記錄這個對映器的日誌:
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}
在應用的類路徑中建立一個名為 log4j.properties
的檔案,檔案的具體內容如下:
# 全域性日誌配置
log4j.rootLogger=ERROR, stdout
# MyBatis 日誌配置
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# 控制檯輸出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
上述配置將使 Log4J 詳細列印 org.mybatis.example.BlogMapper
的日誌,對於應用的其它部分,只打印錯誤資訊。
為了實現更細粒度的日誌輸出,你也可以只打印特定語句的日誌。以下配置將只打印語句 selectBlog
的日誌:
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
或者,你也可以列印一組對映器的日誌,只需要開啟對映器所在的包的日誌功能即可:
log4j.logger.org.mybatis.example=TRACE
某些查詢可能會返回龐大的結果集。這時,你可能只想檢視 SQL 語句,而忽略返回的結果集。為此,SQL 語句將會在 DEBUG 日誌級別下記錄(JDK 日誌則為 FINE)。返回的結果集則會在 TRACE 日誌級別下記錄(JDK 日誌則為 FINER)。因此,只要將日誌級別調整為 DEBUG 即可:
log4j.logger.org.mybatis.example=DEBUG
但如果你要為下面的對映器 XML 檔案列印日誌,又該怎麼辦呢?
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
這時,你可以通過開啟名稱空間的日誌功能來對整個 XML 記錄日誌:
log4j.logger.org.mybatis.example.BlogMapper=TRACE
而要記錄具體語句的日誌,可以這樣做:
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
你應該會發現,為對映器和 XML 檔案開啟日誌功能的語句毫無差別。
提示 如果你使用的是 SLF4J 或 Log4j 2,MyBatis 會設定 tag 為 MYBATIS。
配置檔案 log4j.properties
的餘下內容用來配置輸出器(appender),這一內容已經超出本文件的範圍。關於 Log4J 的更多內容,可以參考上面的 Log4J 網站。或者,你也可以簡單地做個實驗,看看不同的配置會產生怎樣的效果。
批註:
日誌檔案在專案中是一個十分重要的點,就我個人而言,定位錯誤全靠它。日誌檔案的配置並不難,只需要進行幾個簡單的配置就好了,當然這得得益於MyBatis的良好設計。
我們可以根據實際專案的需要,選擇合適的日誌進行配置