ฉันจะรับ SQL ของ PreparedStatement ได้อย่างไร


160

ฉันมีวิธี Java ทั่วไปที่มีลายเซ็นวิธีการดังต่อไปนี้:

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

มันเปิดการเชื่อมต่อสร้างการPreparedStatementใช้คำสั่ง sql และพารามิเตอร์ในqueryParamsอาร์เรย์ความยาวตัวแปรเรียกใช้แคชResultSet(ในCachedRowSetImpl), ปิดการเชื่อมต่อและส่งกลับชุดผลแคช

ฉันมีข้อยกเว้นการจัดการในวิธีการที่บันทึกข้อผิดพลาด ฉันบันทึกคำสั่ง sql เป็นส่วนหนึ่งของบันทึกเนื่องจากมีประโยชน์มากสำหรับการแก้ไขข้อบกพร่อง ปัญหาของฉันคือการบันทึกตัวแปร String จะsqlบันทึกคำสั่งเทมเพลตด้วยหรือไม่แทนค่าจริง ฉันต้องการบันทึกคำสั่งจริงที่ดำเนินการ (หรือพยายามดำเนินการ)

ดังนั้น ... มีวิธีที่จะได้รับคำสั่ง SQL ที่เกิดขึ้นจริงที่จะดำเนินการโดยใดPreparedStatement? ( โดยไม่ต้องสร้างมันด้วยตัวเองถ้าฉันไม่สามารถหาวิธีเข้าถึงPreparedStatement'sSQL ได้ฉันอาจจะสร้างมันด้วยตัวเองในcatches)


1
หากคุณกำลังเขียนรหัส JDBC ตรงผมขออยากแนะนำให้มองหาที่ Apache Commons-dbutils commons.apache.org/dbutils มันลดความซับซ้อนของรหัส JDBC อย่างมาก
Ken Liu

คำตอบ:


173

ใช้คำสั่งที่เตรียมไว้ไม่มี "แบบสอบถาม SQL":

  • คุณมีคำสั่งที่มีตัวยึดตำแหน่ง
    • มันถูกส่งไปยังเซิร์ฟเวอร์ฐานข้อมูล
    • และเตรียมไว้ที่นั่น
    • ซึ่งหมายความว่าคำสั่ง SQL คือ "วิเคราะห์" แยกวิเคราะห์บางโครงสร้างข้อมูลที่เป็นตัวแทนมันถูกเตรียมไว้ในหน่วยความจำ
  • และจากนั้นคุณมีตัวแปรที่ถูกผูกไว้
    • ซึ่งถูกส่งไปยังเซิร์ฟเวอร์
    • และคำสั่งที่เตรียมไว้จะถูกดำเนินการ - ทำงานกับข้อมูลเหล่านั้น

แต่ไม่มีการสร้างแบบสอบถาม SQL จริงขึ้นมาใหม่ - ไม่ได้อยู่บนฝั่ง Java หรือบนฐานข้อมูล

ดังนั้นจึงไม่มีวิธีรับ SQL ของคำสั่งที่เตรียมไว้ - เนื่องจากไม่มี SQL ดังกล่าว


สำหรับวัตถุประสงค์ในการแก้ไขข้อบกพร่องการแก้ปัญหามีดังนี้:

  • Ouput โค้ดของข้อความสั่งพร้อมด้วยตัวยึดตำแหน่งและรายการข้อมูล
  • หรือเพื่อ "สร้าง" แบบสอบถาม SQL บางคำ "ด้วยมือ"

22
แม้ว่านี่จะเป็นจริงตามหน้าที่ แต่ก็ไม่มีอะไรที่ป้องกันยูทิลิตี้โค้ดจากการสร้างคำสั่งที่ไม่ได้เตรียมไว้ให้ใหม่ ตัวอย่างเช่นใน log4jdbc: "ในเอาต์พุตที่บันทึกไว้สำหรับคำสั่งที่เตรียมไว้อาร์กิวเมนต์ bind จะถูกแทรกลงในเอาต์พุต SQL โดยอัตโนมัติสิ่งนี้ช่วยปรับปรุงความสามารถในการอ่านและการดีบักในหลายกรณีอย่างมาก" มีประโยชน์มากสำหรับการแก้ไขข้อบกพร่องตราบใดที่คุณทราบว่าไม่ใช่วิธีที่คำสั่งนั้นดำเนินการโดยเซิร์ฟเวอร์ DB
ดาวฤกษ์

6
นอกจากนี้ยังขึ้นอยู่กับการดำเนินการ ใน MySQL - อย่างน้อยรุ่นที่ฉันใช้ไม่กี่ปีที่ผ่านมา - ไดรเวอร์ JDBC สร้างแบบสอบถาม SQL ทั่วไปจากแม่แบบและตัวแปรผูก ฉันเดาว่าเวอร์ชันของ MySQL ไม่รองรับงบที่เตรียมไว้ดังนั้นพวกเขาจึงใช้มันในไดร์เวอร์ JDBC
Jay

@sidereal: นั่นคือสิ่งที่ฉันหมายถึงโดย"สร้างแบบสอบถามด้วยมือ" ; แต่คุณบอกว่ามันดีกว่าฉัน ;; @Jay: เรามี mecanism แบบเดียวกับที่ใช้ใน PHP (ข้อความจริงที่เตรียมไว้เมื่อได้รับการสนับสนุน, คำสั่ง pseudo ที่จัดทำขึ้นสำหรับไดรเวอร์ฐานข้อมูลที่ไม่สนับสนุน)
Pascal MARTIN

6
หากคุณกำลังใช้ java.sql.PreparedStatement .ToString ง่าย () บน PreparedStatement จะรวมถึงการสร้าง SQL ฉันได้รับการตรวจสอบใน 1.8.0_60
Pr0n

5
@Preston สำหรับ Oracle DB PreparedStatement # toString () ไม่แสดง SQL ดังนั้นฉันคิดว่ามันขึ้นอยู่กับไดรเวอร์ DB JDBC
Mike Argyriou

57

มันเป็นที่กำหนดโดยไม่มีที่ไหนในสัญญา JDBC API แต่ถ้าคุณโชคดีไดรเวอร์ JDBC ในคำถามอาจจะกลับมาของ SQL PreparedStatement#toString()สมบูรณ์โดยเพียงแค่โทร กล่าวคือ

System.out.println(preparedStatement);

อย่างน้อย MySQL 5.x และ PostgreSQL 8.x ไดรเวอร์ JDBC รองรับ อย่างไรก็ตามไดรเวอร์ JDBC อื่น ๆ ส่วนใหญ่ไม่รองรับ หากคุณมีหนึ่งดังกล่าวแล้วทางออกที่ดีที่สุดของคุณใช้Log4jdbcหรือP6Spy

หรือคุณสามารถเขียนฟังก์ชันทั่วไปซึ่งรับค่าConnectionสตริง SQL และค่าคำสั่งและส่งกลับค่า a PreparedStatementหลังจากบันทึกสตริง SQL และค่า ตัวอย่างกำหนดการ:

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

และใช้มันเป็น

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

อีกทางเลือกหนึ่งคือการใช้งานแบบกำหนดเองPreparedStatementซึ่ง wraps (ประดับ) ของจริง PreparedStatementในการก่อสร้างและแทนที่วิธีการทั้งหมดเพื่อที่จะเรียกวิธีการของจริง PreparedStatementและรวบรวมค่าในsetXXX()วิธีการทั้งหมดและสร้างสตริง SQL "ที่แท้จริง" เมื่อใดก็ตามexecuteXXX()วิธีการที่เรียกว่า (ค่อนข้างทำงาน แต่ IDE ที่สุดให้ autogenerators สำหรับวิธีการมัณฑนากร, Eclipse ไม่) ในที่สุดก็ใช้มันแทน นั่นเป็นสิ่งที่ P6Spy และผู้ให้การสนับสนุนได้ทำไว้แล้วภายใต้หน้ากาก


มันคล้ายกับวิธีที่ฉันใช้ (วิธี PreparStatement ของคุณ) คำถามของฉันไม่ใช่วิธีการ - คำถามของฉันคือวิธีการบันทึกคำสั่ง sql ฉันรู้ว่าฉันสามารถทำได้logger.debug(sql + " " + Arrays.asList(values))- ฉันกำลังมองหาวิธีการบันทึกคำสั่ง sql ด้วยพารามิเตอร์ที่รวมอยู่ในนั้น โดยไม่ต้องวนตัวเองและเปลี่ยนเครื่องหมายคำถาม
froadie

จากนั้นตรงไปที่ย่อหน้าสุดท้ายของคำตอบของฉันหรือดูที่ P6Spy พวกเขาทำ "น่ารังเกียจ" บ่วงและแทนที่การทำงานสำหรับคุณ;)
BalusC

ลิงก์ไปที่ P6Spy เสียแล้ว
Stephen P

@BalusC ฉันเป็นมือใหม่กับ JDBC ฉันมีข้อสงสัย หากคุณเขียนฟังก์ชั่นทั่วไปเช่นนั้นมันจะสร้างPreparedStatementทุกครั้ง นั่นจะไม่ใช่วิธีที่มีประสิทธิภาพเพราะจุดรวมของPreparedStatementการสร้างครั้งเดียวและนำกลับมาใช้ใหม่ทุกที่หรือไม่
Bhushan

นอกจากนี้ยังใช้งานได้กับไดรฟ์เวอร์ jdbc voltdb เพื่อรับข้อความค้นหา SQL แบบเต็มสำหรับคำสั่งที่เตรียม
k0pernikus

37

ฉันใช้ Java 8, ไดรเวอร์ JDBC กับ MySQL connector v. 5.1.31

ฉันอาจได้รับสตริง SQL จริงโดยใช้วิธีนี้:

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

ดังนั้นมันจึงกลับมาเป็นแบบนี้:

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

นี่ควรมี upvotes ทั้งหมดเนื่องจากเป็นสิ่งที่ OP ต้องการ
HuckIt

มีฟังก์ชั่นที่คล้ายกันสำหรับไดรเวอร์ Postgres หรือไม่?
davidwessman

จะได้รับมันได้Apache Derbyอย่างไร
Gunasekar

ไม่สามารถใช้งานได้แล้วโยนClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement ไม่สามารถส่งไปยัง com.mysql.jdbc.JDBC4PreparedStatement
Ercan

1
@ErcanDuman คำตอบของฉันไม่สากลมันครอบคลุม Java 8 และ MySQL JDBC driver เท่านั้น
userlond

21

หากคุณกำลังดำเนินการค้นหาและคาดหวังว่าการResultSet(คุณอยู่ในสถานการณ์นี้อย่างน้อย) แล้วคุณก็สามารถเรียกResultSet's getStatement()ชอบโดย:

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

ตัวแปรจะมีคำสั่งที่ใช้ในการสร้างexecutedQueryResultSet

ตอนนี้ฉันรู้ว่าคำถามนี้ค่อนข้างเก่า แต่ฉันหวังว่านี่จะช่วยใครซักคน ..


6
@Elad Stern มันพิมพ์, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b แทนที่จะพิมพ์คำสั่ง sql ที่ถูกเรียกใช้! กรุณาแนะนำเรา!
AVA

@AVA คุณใช้ toString () หรือไม่
Elad Stern

ใช้ @EladStern toString () แล้ว!
AVA

@AVA ฉันไม่แน่ใจ แต่อาจต้องทำกับไดรเวอร์ jdbc ของคุณ ฉันใช้ mysql-connector-5 เรียบร้อยแล้ว
Elad Stern

3
rs.getStatement () เพียงแค่คืนค่าอ็อบเจกต์ statement ดังนั้นจึงลงไปว่าไดรเวอร์ที่คุณใช้ใช้เป็น. toString () ซึ่งกำหนดว่าคุณจะได้รับ SQL คืนหรือไม่
Daz

3

ฉันได้แยก sql ของฉันจาก PreparedStatement โดยใช้ PrepStatement.toString () ในกรณีของฉัน toString () ส่งคืนสตริงเช่นนี้:

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

ตอนนี้ฉันได้สร้างเมธอด (Java 8) ซึ่งใช้ regex เพื่อดึงทั้งเคียวรีและค่าและใส่ลงในแผนที่:

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

วิธีนี้จะส่งคืนแผนที่ที่เรามีคู่ค่าคีย์:

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

ตอนนี้ - ถ้าคุณต้องการค่าเป็นรายการคุณสามารถใช้:

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

หาก readyStatement.toString () ของคุณแตกต่างจากในกรณีของฉันมันเป็นเพียงเรื่องของ "การปรับ" regex


2

การใช้PostgreSQL 9.6.xกับไดร์เวอร์ Java อย่างเป็นทางการ42.2.4:

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

จะแสดง SQL พร้อม?แทนที่แล้วซึ่งเป็นสิ่งที่ฉันกำลังมองหา เพิ่งเพิ่มคำตอบนี้เพื่อให้ครอบคลุมกรณี postgres

ฉันไม่เคยคิดเลยว่ามันจะง่าย


1

ฉันใช้รหัสต่อไปนี้สำหรับการพิมพ์ SQL จาก PreparStatement

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

Code Snippet เพื่อแปลง SQL PreparedStaments ด้วยรายการอาร์กิวเมนต์ มันใช้งานได้สำหรับฉัน

  /**
         * 
         * 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

ดึกมาก :) แต่คุณสามารถรับ SQL ดั้งเดิมจาก OraclePreparedStatementWrapper by

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

1
เมื่อฉันพยายามใช้ wrapper มันบอกว่า: oracle.jdbc.driver.OraclePreparedStatementWrapperไม่เป็นสาธารณะใน oracle.jdbc.driver ไม่สามารถเข้าถึงได้จากแพ็คเกจภายนอก คุณใช้คลาสนั้นอย่างไร
สเปกตรัม

0

หากคุณกำลังใช้ MySQL คุณสามารถเข้าสู่ระบบโดยใช้แบบสอบถามบันทึกแบบสอบถามของ MySQL ฉันไม่ทราบว่าผู้ขายรายอื่นมีคุณสมบัตินี้หรือไม่ แต่พวกเขามีโอกาสหรือไม่


-1

ฟังก์ชั่นเพียง:

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

มันก็ดีเหมือนกันสำหรับการเตรียมรัฐ


นี่เป็นเพียง.toString()บรรทัดสองสามบรรทัดที่จะหลอกผู้ใช้ที่ไม่มีประสบการณ์และได้รับคำตอบทุกวัยแล้ว
Pere

-1

ฉันใช้ Oralce 11g และไม่สามารถจัดการรับ SQL ขั้นสุดท้ายจาก PreparedStatement ได้ หลังจากอ่านคำตอบ @Pascal MARTIN ฉันเข้าใจว่าทำไม

ฉันเพิ่งละทิ้งความคิดในการใช้ PreparedStatement และใช้ตัวจัดรูปแบบข้อความธรรมดาที่เหมาะสมกับความต้องการของฉัน นี่คือตัวอย่างของฉัน:

//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);

คุณคิด sqlInParam สามารถสร้างแบบไดนามิกในวงวน (สำหรับในขณะที่) ฉันเพิ่งทำมันง่าย ๆ ธรรมดาถึงจุดของการใช้คลาส MessageFormat เพื่อทำหน้าที่เป็นแม่แบบสตริงสำหรับแบบสอบถาม SQL


4
การเรียงลำดับนี้ทำให้เกิดเหตุผลทั้งหมดสำหรับการใช้ข้อความที่เตรียมไว้เช่นการหลีกเลี่ยงการฉีด sql และปรับปรุงประสิทธิภาพ
ticktock

1
ฉันเห็นด้วยกับคุณ 100% ฉันควรทำให้ชัดเจนว่าฉันทำรหัสนี้จะถูกดำเนินการสองสามครั้งมากที่สุดสำหรับการรวมข้อมูลขนาดใหญ่จำนวนมากและต้องการวิธีที่รวดเร็วเพื่อให้มีการบันทึกผลลัพธ์บางอย่างโดยไม่ต้องใช้ทั้ง log4j enchilada ซึ่งจะ overkill สำหรับสิ่งที่ ฉันต้องการ. สิ่งนี้ไม่ควรใช้กับรหัสการผลิต :-)
Diego Tercero

สิ่งนี้ใช้ได้กับฉันด้วยไดรเวอร์ Oracle (แต่ไม่รวมพารามิเตอร์):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj

-4

ในการทำเช่นนี้คุณต้องมีการเชื่อมต่อ JDBC และ / หรือไดรเวอร์ที่รองรับการบันทึก sql ในระดับต่ำ

ดูที่log4jdbc


3
ลองดูที่ log4jdbc แล้วอะไรนะ? คุณใช้มันอย่างไร? คุณไปที่ไซต์นั้นและดูการท่องเที่ยวแบบสุ่มเกี่ยวกับโครงการโดยไม่มีตัวอย่างที่ชัดเจนเกี่ยวกับวิธีการใช้เทคโนโลยีจริง
Hooli
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.