【Java開源框架】持久層框架mybatis之SQL語句構建器類及日誌6

SQL語句構建器類

問題

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";

The Solution

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",還有所有的字符串鏈接。

SQL類

這裏給出一些示例:

// Anonymous inner class
public String deletePersonSql() {
  return new SQL() {{
    DELETE_FROM("PERSON");
    WHERE("ID = #{id}");
  }}.toString();
}

// Builder / Fluent style
public String insertPersonSql() {
  String sql = new SQL()
    .INSERT_INTO("PERSON")
    .VALUES("ID, FIRST_NAME", "#{id}, #{firstName}")
    .VALUES("LAST_NAME", "#{lastName}")
    .toString();
  return sql;
}

// With conditionals (note the final parameters, required for the anonymous inner class to access them)
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子句。 參數可以包含由列命和join on條件組合成標準的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子句元素, 由逗號連接。可以多次被調用,每次由逗號連接新的條件。
DELETE_FROM(String) 開始一個delete語句並指定需要從哪個表刪除的表名。通常它後面都會跟着WHERE語句!
INSERT_INTO(String) 開始一個insert語句並指定需要插入數據的表名。後面都會跟着一個或者多個VALUES() or INTO_COLUMNS() and INTO_VALUES()。
  • SET(String)
  • SET(String...)
針對update語句,插入到"set"列表中
UPDATE(String) 開始一個update語句並指定需要更新的表明。後面都會跟着一個或者多個SET(),通常也會有一個WHERE()。
VALUES(String, String) 插入到insert語句中。第一個參數是要插入的列名,第二個參數則是該列的值。
INTO_COLUMNS(String...) Appends columns phrase to an insert statement. This should be call INTO_VALUES() with together.
INTO_VALUES(String...) Appends values phrase to an insert statement. This should be call INTO_COLUMNS() with together.

Since version 3.4.2, you can use variable-length arguments as follows:

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();
}

SqlBuilder 和 SelectBuilder (已經廢棄)

在3.2版本之前,我們使用了一點不同的做法,通過實現ThreadLocal變量來掩蓋一些導致Java DSL麻煩的語言限制。但這種方式已經廢棄了,現代的框架都歡迎人們使用構建器類型和匿名內部類的想法。因此,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.*;

這就允許像下面這樣來創建方法:

/* DEPRECATED */
public String selectBlogsSql() {
  BEGIN(); // Clears ThreadLocal variable
  SELECT("*");
  FROM("BLOG");
  return SQL();
}
        
/* DEPRECATED */
private String selectPersonSql() {
  BEGIN(); // Clears ThreadLocal variable
  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();
}

日誌

Mybatis 的內置日誌工廠提供日誌功能,內置日誌工廠將日誌交給以下其中一種工具作代理:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

     

MyBatis 內置日誌工廠基於運行時自省機制選擇合適的日誌工具。它會使用第一個查找得到的工具(按上文列舉的順序查找)。如果一個都未找到,日誌功能就會被禁用。

     

不少應用服務器(如 Tomcat 和 WebShpere)的類路徑中已經包含 Commons Logging,所以在這種配置環境下的 MyBatis 會把它作爲日誌工具,記住這點非常重要。這將意味着,在諸如 WebSphere 的環境中,它提供了 Commons Logging 的私有實現,你的 Log4J 配置將被忽略。MyBatis 將你的 Log4J 配置忽略掉是相當令人鬱悶的(事實上,正是因爲在這種配置環境下,MyBatis 纔會選擇使用 Commons Logging 而不是 Log4J)。如果你的應用部署在一個類路徑已經包含 Commons Logging 的環境中,而你又想使用其它日誌工具,你可以通過在 MyBatis 配置文件 mybatis-config.xml 裏面添加一項 setting 來選擇別的日誌工具。

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>

     

logImpl 可選的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是實現了接口 org.apache.ibatis.logging.Log 的,且構造方法是以字符串爲參數的類的完全限定名。(譯者注:可以參考org.apache.ibatis.logging.slf4j.Slf4jImpl.java的實現)

     

你也可以調用如下任一方法來使用日誌工具:

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 方法之前調用它。另外,僅當運行時類路徑中存在該日誌工具時,調用與該日誌工具對應的方法纔會生效,否則 MyBatis 一概忽略。如你環境中並不存在 Log4J,你卻調用了相應的方法,MyBatis 就會忽略這一調用,轉而以默認的查找順序查找日誌工具。

     

關於 SLF4J、Apache Commons Logging、Apache Log4J 和 JDK Logging 的 API 介紹不在本文檔介紹範圍內。不過,下面的例子可以作爲一個快速入門。關於這些日誌框架的更多信息,可以參考以下鏈接:

     

日誌配置

       

你可以對包、映射類的全限定名、命名空間或全限定語句名開啓日誌功能來查看 MyBatis 的日誌語句。

       

再次說明下,具體怎麼做,由使用的日誌工具決定,這裏以 Log4J 爲例。配置日誌功能非常簡單:添加一個或多個配置文件(如 log4j.properties),有時需要添加 jar 包(如 log4j.jar)。下面的例子將使用 Log4J 來配置完整的日誌服務,共兩個步驟:

 

步驟 1:添加 Log4J 的 jar 包

       

因爲我們使用的是 Log4J,就要確保它的 jar 包在應用中是可用的。要啓用 Log4J,只要將 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 的文件,文件的具體內容如下:

# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
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 語句而不想記錄結果該怎麼辦?爲此,Mybatis 中 SQL 語句的日誌級別被設爲DEBUG(JDK 日誌設爲 FINE),結果的日誌級別爲 TRACE(JDK 日誌設爲 FINER)。所以,只要將日誌級別調整爲 DEBUG 即可達到目的:

log4j.logger.org.mybatis.example=DEBUG

       

要記錄日誌的是類似下面的映射器文件而不是映射器接口又該怎麼做呢?

<?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 將以 MYBATIS 這個值進行調用。

       

配置文件 log4j.properties 的餘下內容是針對日誌輸出源的,這一內容已經超出本文檔範圍。關於 Log4J 的更多內容,可以參考Log4J 的網站。不過,你也可以簡單地做做實驗,看看不同的配置會產生怎樣的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章