首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

擴展 PreparedStatement 實現 JDBC 查詢日誌記錄

     編輯:Java 相關知識

在 Java 資料庫開發中,PreparedStatement 不僅能有效防止 SQL 注入,還能透過預編譯顯著提升查詢效能。然而,當需要記錄實際執行的 SQL 語句時,標準介面卻顯得力不從心:它只保留帶佔位符(?)的模板,無法直接輸出替換真實參數值後的完整語句。這對除錯與效能分析帶來極大不便。

傳統日誌方案的缺陷

最直觀的做法是在程式碼中手動拼裝 SQL 字串:

String sql = "select foo, bar from foobar where foo < ? and bar = ?";
long fooValue = 99;
String barValue = "christmas";
System.out.println("Executing query: select foo, bar from foobar where foo < " + fooValue + " and bar = '" + barValue + "'");

這種方式存在明顯缺點:

  • 參數值與 SQL 模板分離,維護成本高
  • 修改查詢條件時極易忘記同步更新日誌代碼
  • 產生的日誌可能與實際發送給資料庫的語句不一致,導致除錯誤導

LoggableStatement 封裝器設計

本文採用 Decorator 模式,實作一個包裝真實 PreparedStatement 的 LoggableStatement 類別。核心思路:

  1. 建構子接收 Connection 與 SQL 模板,建立真實 PreparedStatement 並保存模板
  2. 覆寫所有 setXxx() 方法:先委派給真實物件,再將參數值(轉為字串)存入內部列表
  3. 提供 getQueryString() 方法,將模板中的 ? 依序替換為已保存的參數值,生成可直接執行的完整 SQL

關鍵程式碼片段

public class LoggableStatement implements PreparedStatement {
    private final PreparedStatement wrappedStatement;
    private final String sqlTemplate;
    private final List<Object> parameterValues = new ArrayList<>();

    public LoggableStatement(Connection conn, String sql) throws SQLException {
        this.wrappedStatement = conn.prepareStatement(sql);
        this.sqlTemplate = sql;
    }

    @Override
    public void setLong(int parameterIndex, long x) throws SQLException {
        wrappedStatement.setLong(parameterIndex, x);
        saveParameter(parameterIndex, x);
    }

    @Override
    public void setString(int parameterIndex, String x) throws SQLException {
        wrappedStatement.setString(parameterIndex, x);
        saveParameter(parameterIndex, x);
    }

    // 其他 setXxx 方法同理...

    private void saveParameter(int index, Object value) {
        String strValue;
        if (value instanceof String || value instanceof java.util.Date) {
            strValue = "'" + value + "'";
        } else if (value == null) {
            strValue = "null";
        } else {
            strValue = value.toString();
        }
        // 確保列表足夠大
        while (index >= parameterValues.size()) parameterValues.add(null);
        parameterValues.set(index, strValue);
    }

    public String getQueryString() {
        String result = sqlTemplate;
        for (Object val : parameterValues) {
            if (val != null) result = result.replaceFirst("\\?", val.toString());
        }
        return result;
    }
    // 其他介面方法直接委派給 wrappedStatement...
}

實際應用範例

Connection conn = dataSource.getConnection();
PreparedStatement pstmt;
if (logEnabled) {
    pstmt = new LoggableStatement(conn, sql);
} else {
    pstmt = conn.prepareStatement(sql);
}
pstmt.setLong(1, 99);
pstmt.setString(2, "christmas");

if (logEnabled) {
    System.out.println("Executing query: " + ((LoggableStatement)pstmt).getQueryString());
}
ResultSet rs = pstmt.executeQuery();

優勢總結

  • 零侵入式日誌:業務代碼無需重複參數變數,修改 SQL 時自動同步日誌輸出
  • 參數處理統一:字串加引號、日期格式化、null 處理集中在 saveParameter,易於擴展
  • 除錯友善:輸出的 SQL 可直接複製到資料庫客戶端執行驗證

這個輕量級封裝展示了「以組合代替繼承」的經典應用,不僅解決了 JDBC 日誌痛點,也為類似的介面增強提供了範本。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved