Làm cách nào tôi có thể nhận được SQL của PreparedStatement?


160

Tôi có một phương thức Java chung với chữ ký phương thức sau:

private static ResultSet runSQLResultSet(String sql, Object... queryParams)

Nó mở một kết nối, xây dựng một PreparedStatementcâu lệnh sql và các tham số trong queryParamsmảng có độ dài thay đổi, chạy nó, lưu trữ ResultSet(trong a CachedRowSetImpl), đóng kết nối và trả về tập kết quả được lưu trữ.

Tôi có xử lý ngoại lệ trong phương pháp ghi lại lỗi. Tôi ghi nhật ký sql như một phần của nhật ký vì nó rất hữu ích để gỡ lỗi. Vấn đề của tôi là việc sqlghi nhật ký biến String ghi lại câu lệnh mẫu bằng? Thay vì giá trị thực. Tôi muốn ghi lại câu lệnh thực tế đã được thực thi (hoặc đã cố thực hiện).

Vậy ... Có cách nào để lấy câu lệnh SQL thực tế sẽ được chạy bởi a PreparedStatementkhông? ( Không tự mình xây dựng nó. Nếu tôi không thể tìm cách truy cập PreparedStatement'sSQL, có lẽ tôi sẽ tự mình xây dựng nó trong catches của mình .)


1
Nếu bạn đang viết mã JDBC thẳng, tôi khuyên bạn nên xem Apache commons-dbutils commons.apache.org/dbutils . Nó đơn giản hóa mã JDBC rất nhiều.
Ken Liu

Câu trả lời:


173

Sử dụng các câu lệnh đã chuẩn bị, không có "truy vấn SQL":

  • Bạn có một tuyên bố, có chứa giữ chỗ
    • nó được gửi đến máy chủ DB
    • và chuẩn bị ở đó
    • có nghĩa là câu lệnh SQL được "phân tích", phân tích cú pháp, một số cấu trúc dữ liệu đại diện cho nó được chuẩn bị trong bộ nhớ
  • Và, sau đó, bạn có các biến ràng buộc
    • được gửi đến máy chủ
    • và câu lệnh được chuẩn bị được thực thi - làm việc trên những dữ liệu đó

Nhưng không có cấu trúc lại của một truy vấn SQL thực tế - không phải ở phía Java, cũng như phía cơ sở dữ liệu.

Vì vậy, không có cách nào để có được SQL của câu lệnh đã chuẩn bị - vì không có SQL nào như vậy.


Đối với mục đích gỡ lỗi, các giải pháp là:

  • Ouput mã của tuyên bố, với các phần giữ chỗ và danh sách dữ liệu
  • Hoặc để "xây dựng" một số truy vấn SQL "bằng tay".

22
Mặc dù điều này đúng về mặt chức năng, nhưng không có gì ngăn cản mã tiện ích xây dựng lại một tuyên bố chưa chuẩn bị tương đương. Ví dụ: trong log4jdbc: "Trong đầu ra được ghi, đối với các câu lệnh đã chuẩn bị, các đối số liên kết được tự động chèn vào đầu ra SQL. Điều này cải thiện đáng kể khả năng đọc và gỡ lỗi cho nhiều trường hợp." Rất hữu ích để gỡ lỗi, miễn là bạn biết rằng đó không phải là cách câu lệnh thực sự được thực thi bởi máy chủ DB.
sidereal

6
Điều này cũng phụ thuộc vào việc thực hiện. Trong MySQL - ít nhất là phiên bản tôi đã sử dụng vài năm trước - trình điều khiển JDBC thực sự đã xây dựng một truy vấn SQL thông thường từ khuôn mẫu và các biến liên kết. Tôi đoán rằng phiên bản MySQL không hỗ trợ các câu lệnh được chuẩn bị nguyên bản, vì vậy chúng đã triển khai chúng trong trình điều khiển JDBC.
Jay

@sidereal: đó là ý của tôi khi "xây dựng truy vấn bằng tay" ; nhưng bạn nói nó tốt hơn tôi ;;; @Jay: chúng ta có cùng một loại mecanism trong PHP (các câu lệnh được chuẩn bị thực sự khi được hỗ trợ; các câu lệnh giả được chuẩn bị cho các trình điều khiển cơ sở dữ liệu không hỗ trợ chúng)
Pascal MARTIN

6
Nếu bạn đang sử dụng java.sql.PreparedStatement, một .toString () đơn giản trên yetStatement sẽ bao gồm SQL được tạo mà tôi đã xác minh điều này trong 1.8.0_60
Pr0n

5
@Preston Đối với Oracle DB, PreparedStatement # toString () không hiển thị SQL. Vì vậy, tôi đoán nó phụ thuộc vào trình điều khiển DBBC DB.
Mike Argyriou 23/2/2016

57

Không có gì được xác định trong hợp đồng API JDBC, nhưng nếu bạn may mắn, trình điều khiển JDBC được đề cập có thể trả về SQL hoàn chỉnh chỉ bằng cách gọi PreparedStatement#toString(). I E

System.out.println(preparedStatement);

Ít nhất các trình điều khiển JDBC MySQL 5.x và PostgreSQL 8.x hỗ trợ nó. Tuy nhiên, hầu hết các trình điều khiển JDBC khác không hỗ trợ nó. Nếu bạn có một cái như vậy, thì đặt cược tốt nhất của bạn là sử dụng Log4jdbc hoặc P6Spy .

Ngoài ra, bạn cũng có thể viết một hàm chung có một Connectionchuỗi SQL và các giá trị câu lệnh và trả về a PreparedStatementsau khi ghi chuỗi SQL và các giá trị. Ví dụ khởi động:

public static PreparedStatement prepareStatement(Connection connection, String sql, Object... values) throws SQLException {
    PreparedStatement preparedStatement = connection.prepareStatement(sql);
    for (int i = 0; i < values.length; i++) {
        preparedStatement.setObject(i + 1, values[i]);
    }
    logger.debug(sql + " " + Arrays.asList(values));
    return preparedStatement;
}

và sử dụng nó như

try {
    connection = database.getConnection();
    preparedStatement = prepareStatement(connection, SQL, values);
    resultSet = preparedStatement.executeQuery();
    // ...

Một cách khác là triển khai một tùy chỉnh PreparedStatementbao bọc (trang trí) thực tế PreparedStatement khi xây dựng và ghi đè tất cả các phương thức để nó gọi các phương thức của thực PreparedStatement và thu thập các giá trị trong tất cả các setXXX()phương thức và lười biếng xây dựng chuỗi SQL "thực tế" bất cứ khi nào một các executeXXX()phương thức được gọi (khá là công việc, nhưng hầu hết các IDE đều cung cấp trình phát tự động cho các phương thức trang trí, Eclipse thực hiện). Cuối cùng chỉ cần sử dụng nó để thay thế. Về cơ bản, đó cũng là những gì P6Spy và các phối ngẫu đã làm dưới mui xe.


Điều đó tương tự với phương pháp tôi đang sử dụng (phương thức Chuẩn bị của bạn). Câu hỏi của tôi không phải là làm thế nào để làm điều đó - câu hỏi của tôi là làm thế nào để ghi nhật ký sql. Tôi biết rằng tôi có thể làm logger.debug(sql + " " + Arrays.asList(values))- Tôi đang tìm cách để ghi nhật ký sql với các tham số đã được tích hợp vào nó. Không lặp lại chính mình và thay thế các dấu hỏi.
froadie

Sau đó đi đến đoạn cuối của câu trả lời của tôi hoặc nhìn vào P6Spy. Họ thực hiện công việc lặp lại "khó chịu" và thay thế công việc cho bạn;)
BalusC

Liên kết đến P6Spy hiện đã bị hỏng.
Stephen P

@BalusC Tôi là người mới tham gia JDBC. Tôi có một nghi ngờ. Nếu bạn viết hàm chung như thế, thì nó sẽ tạo ra PreparedStatementmỗi lần. Đó không phải là cách không hiệu quả vì toàn bộ quan điểm PreparedStatementlà tạo ra chúng một lần và sử dụng lại chúng ở mọi nơi?
Bhushan

Điều này cũng hoạt động trên trình điều khiển jdbc voltdb để có được truy vấn sql đầy đủ cho một tuyên bố đã chuẩn bị.
k0pernikus

37

Tôi đang sử dụng trình điều khiển Java 8, JDBC với trình kết nối MySQL v. 5.1.31.

Tôi có thể nhận được chuỗi SQL thực bằng phương thức này:

// 1. make connection somehow, it's conn variable
// 2. make prepered statement template
PreparedStatement stmt = conn.prepareStatement(
    "INSERT INTO oc_manufacturer" +
    " SET" +
    " manufacturer_id = ?," +
    " name = ?," +
    " sort_order=0;"
);
// 3. fill template
stmt.setInt(1, 23);
stmt.setString(2, 'Google');
// 4. print sql string
System.out.println(((JDBC4PreparedStatement)stmt).asSql());

Vì vậy, nó trả về smth như thế này:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;

Điều này nên có tất cả các upvote vì nó chính xác là những gì OP đang tìm kiếm.
HuckIt 8/8/2016

Có một chức năng tương tự cho trình điều khiển Postgres?
davidwessman

Làm thế nào để có được nó cho Apache Derby?
Gunasekar

Nó không hoạt động và ném ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement không thể được chuyển thành com.mysql.jdbc.JDBC4PreparedStatement
Ercan

1
@ErcanDuman, câu trả lời của tôi không phổ biến, nó chỉ bao gồm trình điều khiển JDBC của Java 8 và MySQL.
dùng

21

Nếu bạn đang thực hiện truy vấn và mong đợi một ResultSet(bạn đang ở trong tình huống này, ít nhất) sau đó bạn chỉ có thể gọi ResultSetgetStatement()như vậy:

ResultSet rs = pstmt.executeQuery();
String executedQuery = rs.getStatement().toString();

Biến executedQuerysẽ chứa câu lệnh được sử dụng để tạo ResultSet.

Bây giờ, tôi nhận ra câu hỏi này khá cũ, nhưng tôi hy vọng nó sẽ giúp được ai đó ..


6
@Elad Stern Nó in, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b thay vì in câu lệnh sql đã thực thi! Hãy hướng dẫn chúng tôi!
AVA

@AVA, bạn đã sử dụng toString () chưa?
Elad Stern

@EladStern toString () được sử dụng!
AVA

@AVA, tôi không chắc nhưng có thể phải làm với trình điều khiển jdbc của bạn. Tôi đã sử dụng mysql-Connector-5 thành công.
Elad Stern

3
rs.getStatement () chỉ trả về đối tượng câu lệnh, do đó, liệu trình điều khiển bạn đang sử dụng có thực hiện hay không .toString () sẽ xác định xem bạn có lấy lại SQL hay không
Daz

3

Tôi đã trích xuất sql của mình từ PreparedStatement bằng yetStatement.toString () Trong trường hợp của tôi, toString () trả về Chuỗi như thế này:

org.hsqldb.jdbc.JDBCPreparedStatement@7098b907[sql=[INSERT INTO 
TABLE_NAME(COLUMN_NAME, COLUMN_NAME, COLUMN_NAME) VALUES(?, ?, ?)],
parameters=[[value], [value], [value]]]

Bây giờ tôi đã tạo một phương thức (Java 8), đang sử dụng regex để trích xuất cả truy vấn và giá trị và đưa chúng vào bản đồ:

private Map<String, String> extractSql(PreparedStatement preparedStatement) {
    Map<String, String> extractedParameters = new HashMap<>();
    Pattern pattern = Pattern.compile(".*\\[sql=\\[(.*)],\\sparameters=\\[(.*)]].*");
    Matcher matcher = pattern.matcher(preparedStatement.toString());
    while (matcher.find()) {
      extractedParameters.put("query", matcher.group(1));
      extractedParameters.put("values", Stream.of(matcher.group(2).split(","))
          .map(line -> line.replaceAll("(\\[|])", ""))
          .collect(Collectors.joining(", ")));
    }
    return extractedParameters;
  }

Phương thức này trả về bản đồ nơi chúng ta có các cặp khóa-giá trị:

"query" -> "INSERT INTO TABLE_NAME(COLUMN_NAME, COLUMN_NAME, COLUMN_NAME) VALUES(?, ?, ?)"
"values" -> "value,  value,  value"

Bây giờ - nếu bạn muốn các giá trị dưới dạng danh sách, bạn chỉ cần sử dụng:

List<String> values = Stream.of(yourExtractedParametersMap.get("values").split(","))
    .collect(Collectors.toList());

Nếu chuẩn bịStatement.toString () của bạn khác với trường hợp của tôi thì đó chỉ là vấn đề "điều chỉnh" regex.


2

Sử dụng PostgreSQL 9.6.x với trình điều khiển Java chính thức 42.2.4:

...myPreparedStatement.execute...
myPreparedStatement.toString()

Sẽ hiển thị SQL với ? đã được thay thế, đó là những gì tôi đang tìm kiếm. Chỉ cần thêm câu trả lời này để bao gồm các trường hợp postgres.

Tôi sẽ không bao giờ nghĩ rằng nó có thể đơn giản như vậy.


1

Tôi đã triển khai mã sau đây để in SQL từ PrepStatement

public void printSqlStatement(PreparedStatement preparedStatement, String sql) throws SQLException{
        String[] sqlArrya= new String[preparedStatement.getParameterMetaData().getParameterCount()];
        try {
               Pattern pattern = Pattern.compile("\\?");
               Matcher matcher = pattern.matcher(sql);
               StringBuffer sb = new StringBuffer();
               int indx = 1;  // Parameter begin with index 1
               while (matcher.find()) {
             matcher.appendReplacement(sb,String.valueOf(sqlArrya[indx]));
               }
               matcher.appendTail(sb);
              System.out.println("Executing Query [" + sb.toString() + "] with Database[" + "] ...");
               } catch (Exception ex) {
                   System.out.println("Executing Query [" + sql + "] with Database[" +  "] ...");
            }

    }

1

Đoạn mã để chuyển đổi SQL PreparedStaments với danh sách các đối số. Nó làm việc cho tôi

  /**
         * 
         * formatQuery Utility function which will convert SQL
         * 
         * @param sql
         * @param arguments
         * @return
         */
        public static String formatQuery(final String sql, Object... arguments) {
            if (arguments != null && arguments.length <= 0) {
                return sql;
            }
            String query = sql;
            int count = 0;
            while (query.matches("(.*)\\?(.*)")) {
                query = query.replaceFirst("\\?", "{" + count + "}");
                count++;
            }
            String formatedString = java.text.MessageFormat.format(query, arguments);
            return formatedString;
        }

1

Rất muộn :) nhưng bạn có thể lấy SQL gốc từ OraclePreparedStatementWrapper bằng cách

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

1
Khi tôi cố gắng sử dụng trình bao bọc, nó báo: oracle.jdbc.driver.OraclePreparedStatementWrapperkhông công khai trong oracle.jdbc.driver. Không thể được truy cập từ gói bên ngoài. Bạn đang sử dụng Class đó như thế nào?
phổ


-1

Chức năng đơn giản:

public static String getSQL (Statement stmt){
    String tempSQL = stmt.toString();

    //please cut everything before sql from statement
    //javadb...: 
    int i1 = tempSQL.indexOf(":")+2;
    tempSQL = tempSQL.substring(i1);

    return tempSQL;
}

Nó cũng tốt cho chuẩn bị sẵn sàng.


Điều này chỉ đơn giản là .toString()có thêm một vài dòng để đánh lừa người dùng thiếu kinh nghiệm và đã được trả lời từ lâu.
Pere

-1

Tôi đang sử dụng Oralce 11g và không thể quản lý để có được SQL cuối cùng từ PreparedStatement. Sau khi đọc câu trả lời @Pascal MARTIN tôi hiểu tại sao.

Tôi vừa từ bỏ ý tưởng sử dụng PreparedStatement và sử dụng một trình định dạng văn bản đơn giản phù hợp với nhu cầu của tôi. Đây là ví dụ của tôi:

//I jump to the point after connexion has been made ...
java.sql.Statement stmt = cnx.createStatement();
String sqlTemplate = "SELECT * FROM Users WHERE Id IN ({0})";
String sqlInParam = "21,34,3434,32"; //some random ids
String sqlFinalSql = java.text.MesssageFormat(sqlTemplate,sqlInParam);
System.out.println("SQL : " + sqlFinalSql);
rsRes = stmt.executeQuery(sqlFinalSql);

Bạn tìm ra sqlInParam có thể được xây dựng linh hoạt trong một vòng lặp (for, while) Tôi chỉ đơn giản để đi đến điểm sử dụng lớp MessageFormat để phục vụ như một công cụ định dạng mẫu chuỗi cho truy vấn SQL.


4
Loại này làm nổ tung toàn bộ lý do sử dụng các báo cáo đã chuẩn bị, chẳng hạn như tránh tiêm sql và cải thiện hiệu suất.
ticktock

1
Tôi đồng ý với bạn 100%. Tôi nên nói rõ rằng tôi đã thực hiện mã này một vài lần để tích hợp dữ liệu số lượng lớn và rất cần một cách nhanh chóng để có một số đầu ra log mà không đi vào toàn bộ log4j enchilada mà sẽ bị quá mức cho những gì Tôi cần. Điều này không nên đi vào mã sản xuất :-)
Diego Tercero

Điều này hoạt động với tôi với trình điều khiển Oracle (nhưng không bao gồm các tham số):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj

-4

Để làm điều này, bạn cần có Trình kết nối và / hoặc trình điều khiển JDBC hỗ trợ ghi nhật ký sql ở mức thấp.

Hãy xem log4jdbc


3
Hãy xem log4jdbc và sau đó là gì? Làm thế nào để bạn sử dụng nó? Bạn vào trang web đó và thấy lan man ngẫu nhiên về dự án mà không có ví dụ rõ ràng về cách thực sự sử dụng công nghệ.
Hooli
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.