1. 程式人生 > 其它 >MyBatis官方文件——SQL語句構建及日誌部分

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。由於 WHEREHAVING都會自動使用 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 類將原樣插入 LIMITOFFSETOFFSET 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的良好設計。

我們可以根據實際專案的需要,選擇合適的日誌進行配置