App下載

JDBC 查詢?nèi)罩居涗涀兊酶菀椎姆桨竻R總

難以再遇你 2021-09-13 10:41:44 瀏覽數(shù) (2316)
反饋

在大多數(shù)情況下,JDBC?PreparedStatement?使執(zhí)行數(shù)據(jù)庫(kù)查詢變得更加容易,并且可以顯著提高你的整體應(yīng)用程序性能。但是?PreparedStatement?當(dāng)涉及到記錄查詢語(yǔ)句時(shí),該接口就不夠用了。盡管 ?aPreparedStatement?的優(yōu)勢(shì)在于其可變性,但一個(gè)好的日志條目必須準(zhǔn)確描述發(fā)送到數(shù)據(jù)庫(kù)的 SQL 在所有參數(shù)占位符已被實(shí)際參數(shù)值替換后的外觀。盡管有多種方法可以解決這個(gè)難題,但沒(méi)有一種方法可以輕松大規(guī)模實(shí)現(xiàn),而且大多數(shù)方法都會(huì)使你的代碼變得混亂。

在本文中,你將學(xué)習(xí)如何擴(kuò)展 JDBC?PreparedStatement?接口以進(jìn)行查詢?nèi)罩居涗洝?雖然LoggableStatement?類實(shí)現(xiàn)的?PreparedStatement?接口,但增加了在適合于記錄的格式獲得查詢字符串的方法。使用?LoggableStatement?該類既可以減少日志代碼中的錯(cuò)誤發(fā)生率,又可以隨著時(shí)間的推移生成更整潔、更易于管理的代碼。

請(qǐng)注意,本文假設(shè)你之前有使用 JDBC 和?PreparedStatement?類的經(jīng)驗(yàn)。

典型的查詢記錄解決方案

清單 1 說(shuō)明了?PreparedStatement?在進(jìn)行數(shù)據(jù)庫(kù)查詢時(shí)通常如何使用 ?a?(盡管省略了初始化和錯(cuò)誤處理)。我們將使用SQL查詢SELECT我們的例子在這篇文章中,但討論也同樣適用于其他類型的SQL語(yǔ)句如DELETE,UPDATE和INSERT。

清單 1. 一個(gè)典型的 SQL 數(shù)據(jù)庫(kù)查詢
String sql = "select foo, bar from foobar where foo < ? and bar = ?";
    String fooValue = new Long(99);
    String barValue = "christmas";

    Connection conn = dataSource.getConnection();
    PreparedStatement pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1,fooValue);
    pstmt.setString(2,barValue);

    ResultSet rs = pstmt.executeQuery();

    // parse result...

清單 1 中查詢的良好日志條目可能如下所示:

Executing query: select foo,bar from foobar where foo < 99 and
bar='christmas'

下面是該條目的日志記錄代碼外觀的一個(gè)示例。請(qǐng)注意,清單 1 中的問(wèn)號(hào)已替換為每個(gè)參數(shù)的值。

System.out.println("Executing query: select foo, bar from foobar where foo
< "+fooValue+" and bar = '+barValue+"'")

更好的方法是創(chuàng)建一個(gè)方法,我們稱之為?replaceFirstQuestionMark?,它接受查詢字符串并用參數(shù)值替換問(wèn)號(hào),如清單 2 所示。使用這種方法消除了創(chuàng)建重復(fù)字符串來(lái)描述的需要SQL 語(yǔ)句。

清單 2. 使用 replaceFirstQuestionMark 進(jìn)行字符串替換
// listing 1 goes here

     sql = replaceFirstQuestionMark(sql, fooValue);
     sql = replaceFirstQuestionMark(sql, barValue);
     System.out.println("Executing query: "+sql);

雖然易于實(shí)施,但這些解決方案都不是理想的。問(wèn)題是,每當(dāng)對(duì) SQL 模板進(jìn)行更改時(shí),日志記錄代碼也必須更改。在某些時(shí)候你會(huì)犯錯(cuò)誤幾乎是不可避免的。查詢將被更改,但您將忘記更新日志記錄代碼,并且你最終會(huì)得到與發(fā)送到數(shù)據(jù)庫(kù)的查詢不匹配的日志條目——這是調(diào)試噩夢(mèng)。

我們真正需要的是一個(gè)設(shè)計(jì),讓我們使用的每個(gè)參數(shù)變量(?fooValue?和?barValue?在我們的例子)只有一次。我們想要一種方法,它可以讓我們獲取參數(shù)占位符替換為實(shí)際值的查詢字符串。因?yàn)?java.sql.PreparedStatement?沒(méi)有這樣的方法,我們必須自己實(shí)現(xiàn)一個(gè)。

自定義解決方案

我們的自定義實(shí)現(xiàn)?PreparedStatement?將充當(dāng) JDBC 驅(qū)動(dòng)程序提供的“真實(shí)語(yǔ)句”的包裝器。包裝器語(yǔ)句會(huì)將所有方法調(diào)用(例如,?setLong(int, long)?和?setString(int,String))?轉(zhuǎn)發(fā)到“真實(shí)語(yǔ)句”。在這樣做之前,它將保存相關(guān)的參數(shù)值,以便它們可用于生成日志輸出。

清單 3 顯示了?LoggableStatement?類是如何實(shí)現(xiàn)的?java.sql.PreparedStatement?,以及它是如何使用 JDBC 連接和 SQL 模板作為輸入來(lái)構(gòu)建的。

清單 3. LoggableStatement 實(shí)現(xiàn) java.sql.PreparedStatement
public class LoggableStatement implements java.sql.PreparedStatement {

     // used for storing parameter values needed
      // for producing log
     private ArrayList parameterValues;

     // the query string with question marks as
     // parameter placeholders
     private String sqlTemplate;

     // a statement created from a real database
     // connection
     private PreparedStatement wrappedStatement;

    public LoggableStatement(Connection connection, String sql)
      throws SQLException {
      // use connection to make a prepared statement
      wrappedStatement = connection.prepareStatement(sql);
      sqlTemplate = sql;
      parameterValues = new ArrayList();
    }
     }

LoggableStatement 如何工作

清單 4 說(shuō)明了LoggableStatement?如何添加對(duì)?saveQueryParamValue()?方法的調(diào)用,以及如何在方法?setLong?和? setString?的 ?real語(yǔ)句?上調(diào)用相應(yīng)的方法。?saveQueryParamValue()?調(diào)用以類似的方式添加到用于參數(shù)設(shè)置的所有方法(例如?setChar?,?setLong?、?setRef?、 和?setObj?)。清單 4 還展示了在??不調(diào)用?saveQueryParamValue()?的情況下如何包裝方法?executeQuery?,因?yàn)樗皇恰皡?shù)設(shè)置”方法。

清單 4. LoggableStatement 方法
public void setLong(int parameterIndex, long x)
         throws java.sql.SQLException {
      wrappedStatement.setLong(parameterIndex, x);
      saveQueryParamValue(parameterIndex, new Long(x));
   }

   public void setString(int parameterIndex, String x)
       throws java.sql.SQLException {
      wrappedStatement.setString(parameterIndex, x);
      saveQueryParamValue(parameterIndex, x);
   }

  public ResultSet executeQuery() throws java.sql.SQLException {
     return wrappedStatement.executeQuery();
   }

?saveQueryParamValue()?方法如清單 5 所示。它將每個(gè)參數(shù)值轉(zhuǎn)換為一種?String?表示形式,并將其保存以供該?getQueryString?方法以后使用。默認(rèn)情況下,對(duì)象將?String?使用其?toString?方法轉(zhuǎn)換為 ?a? ,但如果對(duì)象是 ?aString?或 ?a Date?,它將用單引號(hào) ?(")? 括起來(lái)。?getQueryString()?方法允許你從日志中復(fù)制大多數(shù)查詢并將它們粘貼到交互式 SQL 處理器中進(jìn)行測(cè)試和調(diào)試,而無(wú)需修改。你可以根據(jù)需要修改該方法以轉(zhuǎn)換其他類的參數(shù)值。

清單 5. saveQueryParamValue() 方法
private void saveQueryParamValue(int position, Object obj) {
      String strValue;
      if (obj instanceof String || obj instanceof Date) {
           // if we have a String, include '' in the saved value
           strValue = "'" + obj + "'";
      } else {
           if (obj == null) {
                // convert null to the string null
                 strValue = "null";
           } else {
                // unknown object (includes all Numbers), just call toString
                strValue = obj.toString();
           }
      }
      // if we are setting a position larger than current size of
      // parameterValues, first make it larger
      while (position >= parameterValues.size()) {
           parameterValues.add(null);
      }
      // save the parameter
      parameterValues.set(position, strValue);
 }

當(dāng)使用標(biāo)準(zhǔn)方法設(shè)置所有參數(shù)時(shí),我們只需調(diào)用我們的?getQueryString()?方法?LoggableStatement?來(lái)獲取查詢字符串。所有問(wèn)號(hào)都將替換為實(shí)際參數(shù)值,這些值已準(zhǔn)備好輸出到我們選擇的日志記錄目的地。

使用 LoggableStatement

清單 6 顯示了如何將清單 1 和 2 中的代碼更改為使用?LoggableStatement?。?在我們的應(yīng)用程序代碼引入?LoggableStatement解決了重復(fù)參數(shù)變量的問(wèn)題。當(dāng)對(duì)SQL模板進(jìn)行更改時(shí),我們只需要更新?PreparedStatement?參數(shù)設(shè)置調(diào)用(例如,添加?pstmt.setString(3,"new-param-value")?)。更改將反映在日志輸出中,無(wú)需對(duì)日志代碼進(jìn)行任何手動(dòng)更新。

清單 6. 工作中的 LoggableStatement
String sql = "select foo, bar from foobar where foo < ? and bar = ?";
    long fooValue = 99;
    String barValue = "christmas";

    Connection conn = dataSource.getConnection();
    PreparedStatement pstmt;

    if(logEnabled) // use a switch to toggle logging.
        pstmt = new LoggableStatement(conn,sql);
    else
        pstmt = conn.prepareStatement(sql);

    pstmt.setLong(1,fooValue);
    pstmt.setString(2,barValue);

    if(logEnabled)
       System.out.println("Executing query: "+
         ((LoggableStatement)pstmt).getQueryString());

    ResultSet rs = pstmt.executeQuery();

結(jié)論

通過(guò)本文中概述的簡(jiǎn)單步驟,你可以擴(kuò)展?用于查詢?nèi)罩居涗浀?JDBCPreparedStatement?接口。我們?cè)谶@里使用的技術(shù)可以被認(rèn)為是“包裝擴(kuò)展”或裝飾器設(shè)計(jì)模式的一個(gè)實(shí)例。在必須擴(kuò)展 API 但不能選擇子類化的情況下,通過(guò)包裝進(jìn)行擴(kuò)展非常有用。


0 人點(diǎn)贊