Bagaimana saya bisa mendapatkan SQL dari PreparedStatement?

160

Saya memiliki metode Java umum dengan metode tanda tangan berikut:

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

Ini membuka koneksi, membangun PreparedStatementmenggunakan pernyataan sql dan parameter dalam queryParamsarray panjang variabel, menjalankannya, cache ResultSet(dalam a CachedRowSetImpl), menutup koneksi, dan mengembalikan set hasil cache.

Saya memiliki pengecualian dalam penanganan metode yang mencatat kesalahan. Saya mencatat pernyataan sql sebagai bagian dari log karena ini sangat membantu untuk debugging. Masalah saya adalah bahwa log variabel String sqllog pernyataan template dengan? S bukan nilai yang sebenarnya. Saya ingin mencatat pernyataan aktual yang dieksekusi (atau mencoba dieksekusi).

Jadi ... Apakah ada cara untuk mendapatkan pernyataan SQL aktual yang akan dijalankan oleh PreparedStatement? ( Tanpa membangunnya sendiri. Jika saya tidak dapat menemukan cara untuk mengakses PreparedStatement'sSQL, saya mungkin akan berakhir membangunnya sendiri di catches saya .)

froadie
sumber
1
Jika Anda menulis kode JDBC langsung, saya sangat merekomendasikan melihat Apache commons-dbutils commons.apache.org/dbutils . Ini menyederhanakan kode JDBC.
Ken Liu

Jawaban:

173

Menggunakan pernyataan yang disiapkan, tidak ada "permintaan SQL":

  • Anda memiliki pernyataan, berisi placeholder
    • dikirim ke server DB
    • dan disiapkan di sana
    • yang berarti pernyataan SQL "dianalisis", diurai, beberapa struktur data yang mewakili itu disiapkan dalam memori
  • Dan, kemudian, Anda memiliki variabel terikat
    • yang dikirim ke server
    • dan pernyataan yang disiapkan dijalankan - bekerja pada data tersebut

Tetapi tidak ada pembangunan kembali permintaan SQL nyata yang sebenarnya - baik di sisi Jawa, maupun di sisi database.

Jadi, tidak ada cara untuk mendapatkan pernyataan SQL yang disiapkan - karena tidak ada SQL seperti itu.


Untuk tujuan debugging, solusinya adalah:

  • Ouput kode pernyataan, dengan penampung dan daftar data
  • Atau untuk "membangun" beberapa permintaan SQL "dengan tangan".
Pascal MARTIN
sumber
22
Meskipun ini benar secara fungsional, tidak ada yang mencegah kode utilitas merekonstruksi pernyataan yang tidak siap yang setara. Misalnya, dalam log4jdbc: "Dalam output yang dicatat, untuk pernyataan yang disiapkan, argumen bind secara otomatis dimasukkan ke dalam output SQL. Ini sangat meningkatkan keterbacaan dan debugging untuk banyak kasus." Sangat berguna untuk debugging, selama Anda sadar bahwa itu bukan bagaimana pernyataan itu sebenarnya dieksekusi oleh server DB.
sidereal
6
Ini juga tergantung pada implementasinya. Di MySQL - setidaknya versi yang saya gunakan beberapa tahun lalu - driver JDBC sebenarnya membangun kueri SQL konvensional dari template dan variabel bind. Saya kira versi MySQL tidak mendukung pernyataan yang disiapkan secara asli, sehingga mereka mengimplementasikannya dalam driver JDBC.
Jay
@sereal: itulah yang saya maksud dengan "membangun kueri dengan tangan" ; tetapi Anda mengatakannya lebih baik dari saya ;;; @ Jay: kami memiliki jenis mekanisme yang sama di PHP (pernyataan nyata yang disiapkan ketika didukung; pernyataan semu yang disiapkan untuk driver basis data yang tidak mendukung mereka)
Pascal MARTIN
6
Jika Anda menggunakan java.sql.PreparedStatement, .toString () yang sederhana di readyStatement akan menyertakan SQL yang dihasilkan, saya telah memverifikasi ini di 1.8.0_60
Pr0n
5
@ Preston Untuk Oracle DB the PreparedStatement # toString () tidak menampilkan SQL. Karena itu saya kira itu tergantung dari driver DB JDBC.
Mike Argyriou
57

Ini tidak didefinisikan dalam kontrak JDBC API, tetapi jika Anda beruntung, driver JDBC yang dimaksud dapat mengembalikan SQL lengkap hanya dengan menelepon PreparedStatement#toString(). Yaitu

System.out.println(preparedStatement);

Setidaknya driver MySQL 5.x dan PostgreSQL 8.x JDBC mendukungnya. Namun, sebagian besar driver JDBC lainnya tidak mendukungnya. Jika Anda memilikinya, maka taruhan terbaik Anda adalah menggunakan Log4jdbc atau P6Spy .

Atau, Anda juga dapat menulis fungsi generik yang mengambil Connection, string SQL dan nilai pernyataan dan mengembalikan a PreparedStatementsetelah masuk string SQL dan nilai-nilai. Contoh kickoff:

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

dan gunakan sebagai

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

Alternatif lain adalah menerapkan kebiasaan PreparedStatementyang membungkus (menghiasi) yang nyata PreparedStatement pada konstruksi dan menimpa semua metode sehingga memanggil metode yang nyata PreparedStatement dan mengumpulkan nilai-nilai dalam semua setXXX()metode dan dengan malas membangun string SQL "aktual" setiap kali salah satu dari yang executeXXX()metode disebut (cukup bekerja, tapi yang paling IDE menyediakan autogenerators untuk metode dekorator, Eclipse tidak). Akhirnya gunakan saja sebagai gantinya. Itu juga pada dasarnya apa yang P6Spy dan selir sudah lakukan di bawah tenda.

BalusC
sumber
Itu mirip dengan metode yang saya gunakan (metode prepstatement Anda). Pertanyaan saya bukan bagaimana melakukannya - pertanyaan saya adalah bagaimana cara mencatat pernyataan sql. Saya tahu bahwa saya bisa melakukannya logger.debug(sql + " " + Arrays.asList(values))- Saya sedang mencari cara untuk mencatat pernyataan sql dengan parameter yang sudah terintegrasi ke dalamnya. Tanpa mengulangi dan mengganti tanda tanya.
froadie
Kemudian menuju ke paragraf terakhir dari jawaban saya atau lihat P6Spy. Mereka melakukan perulangan "jahat" dan mengganti pekerjaan untuk Anda;)
BalusC
Tautan ke P6Spy sekarang rusak.
Stephen P
@BalusC Saya pemula untuk JDBC. Saya punya satu keraguan. Jika Anda menulis fungsi generik seperti itu, maka itu akan dibuat PreparedStatementsetiap waktu. Bukankah itu cara yang tidak terlalu efisien karena seluruh intinya PreparedStatementadalah membuat mereka sekali dan menggunakannya kembali di mana-mana?
Bhushan
Ini juga berfungsi pada driver jdbc voltdb untuk mendapatkan kueri sql lengkap untuk pernyataan yang disiapkan.
k0pernikus
37

Saya menggunakan Java 8, JDBC driver dengan konektor MySQL v. 5.1.31.

Saya mungkin mendapatkan string SQL nyata menggunakan metode ini:

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

Jadi mengembalikan sesuatu seperti ini:

INSERT INTO oc_manufacturer SET manufacturer_id = 23, name = 'Google', sort_order=0;
userlond
sumber
Ini harus memiliki semua upvotes karena persis apa yang dicari OP.
HuckIt
Apakah ada fungsi yang serupa untuk driver Postgres?
davidwessman
Bagaimana cara mendapatkannya Apache Derby?
Gunasekar
Itu tidak bekerja dan melempar ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement tidak dapat dilemparkan ke com.mysql.jdbc.JDBC4PreparedStatement
Ercan
1
@ErcanDuman, jawaban saya tidak universal, hanya mencakup driver Java 8 dan MySQL JDBC.
userlond
21

Jika Anda mengeksekusi query dan mengharapkan ResultSet(Anda berada di skenario ini, setidaknya) maka Anda hanya dapat memanggil ResultSet's getStatement()seperti:

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

Variabel executedQueryakan berisi pernyataan yang digunakan untuk membuat ResultSet.

Sekarang, saya menyadari pertanyaan ini sudah cukup lama, tetapi saya harap ini membantu seseorang ..

Elad Stern
sumber
6
@Elad Stern Mencetak, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b alih-alih mencetak pernyataan sql yang dieksekusi! Tolong bimbing kami!
AVA
@ AVA, apakah Anda menggunakan toString ()?
Elad Stern
@EladStern toString () digunakan!
AVA
@ AVA, baik saya tidak yakin tetapi mungkin ada hubungannya dengan driver jdbc Anda. Saya telah berhasil menggunakan mysql-connector-5.
Elad Stern
3
rs.getStatement () baru saja mengembalikan objek pernyataan, jadi tergantung apakah driver yang Anda gunakan mengimplementasikan .toString () yang menentukan apakah Anda akan mendapatkan kembali SQL
Daz
3

Saya telah mengekstrak sql saya dari PreparedStatement menggunakan readyStatement.toString () Dalam kasus saya toString () mengembalikan String seperti ini:

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

Sekarang saya telah membuat metode (Java 8), yang menggunakan regex untuk mengekstraksi kueri dan nilai dan memasukkannya ke dalam peta:

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

Metode ini mengembalikan peta tempat kami memiliki pasangan nilai kunci:

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

Sekarang - jika Anda ingin nilai sebagai daftar, Anda cukup menggunakan:

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

Jika readyStatement.toString () Anda berbeda dari pada kasus saya, itu hanya masalah "menyesuaikan" regex.

RichardK
sumber
2

Menggunakan PostgreSQL 9.6.x dengan driver Java resmi 42.2.4:

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

Akan menampilkan SQL dengan yang ?sudah diganti, yang adalah apa yang saya cari. Baru saja menambahkan jawaban ini untuk membahas kasus postgres.

Saya tidak akan pernah berpikir itu bisa begitu sederhana.

Christophe Roussy
sumber
1

Saya menerapkan kode berikut untuk mencetak SQL dari ReadyStatement

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[" +  "] ...");
            }

    }

sumber
1

Cuplikan Kode untuk mengonversi SQL PreparedStaments dengan daftar argumen. Ini bekerja untuk saya

  /**
         * 
         * 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;
        }
Harsh Maheswari
sumber
1

Sangat terlambat :) tetapi Anda bisa mendapatkan SQL asli dari OraclePreparedStatementWrapper oleh

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();
pengguna497087
sumber
1
Ketika saya mencoba menggunakan pembungkus itu tertulis: oracle.jdbc.driver.OraclePreparedStatementWrappertidak umum di oracle.jdbc.driver. Tidak dapat diakses dari paket luar. Bagaimana Anda menggunakan Kelas itu?
spektrum
0

Jika Anda menggunakan MySQL, Anda dapat mencatat kueri menggunakan log kueri MySQL . Saya tidak tahu apakah vendor lain menyediakan fitur ini, tetapi kemungkinan mereka melakukannya.

Ionuț G. Stan
sumber
-1

Cukup berfungsi:

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

Tidak apa-apa juga untuk pernyataan yang disiapkan.

Hobson
sumber
Ini hanyalah .toString()dengan beberapa baris tambahan untuk mengelabui pengguna yang tidak ahli, dan sudah dijawab berabad-abad yang lalu.
Pere
-1

Saya menggunakan Oralce 11g dan tidak bisa mendapatkan SQL final dari PreparedStatement. Setelah membaca @Pascal, jawaban MARTIN saya mengerti mengapa.

Saya baru saja meninggalkan ide menggunakan PreparedStatement dan menggunakan formatter teks sederhana yang sesuai dengan kebutuhan saya. Inilah contoh saya:

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

Anda mengetahui sqlInParam dapat dibangun secara dinamis dalam (untuk, sementara) loop saya hanya membuatnya sederhana untuk sampai ke titik menggunakan kelas MessageFormat untuk berfungsi sebagai formater template string untuk query SQL.

Diego Tercero
sumber
4
Semacam ini meledakkan seluruh alasan untuk menggunakan pernyataan yang disiapkan, seperti menghindari injeksi sql dan peningkatan kinerja.
ticktock
1
Saya setuju dengan Anda 100%. Saya seharusnya menjelaskan bahwa saya membuat kode ini untuk dieksekusi beberapa kali paling banyak untuk integrasi data massal dan sangat membutuhkan cara cepat untuk memiliki beberapa log output tanpa masuk ke seluruh log4j enchilada yang akan membutuhkan banyak untuk apa Saya memerlukannya. Ini tidak boleh masuk ke dalam kode produksi :-)
Diego Tercero
Ini berfungsi untuk saya dengan driver Oracle (tetapi tidak termasuk parameter):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj
-4

Untuk melakukan ini, Anda memerlukan Koneksi JDBC dan / atau driver yang mendukung logging sql di tingkat rendah.

Lihatlah log4jdbc

yg berkenaan dgn bintang
sumber
3
Lihatlah log4jdbc dan lalu apa? Bagaimana kamu menggunakannya? Anda pergi ke situs itu dan melihat bertele-tele tentang proyek tanpa contoh yang jelas tentang bagaimana sebenarnya menggunakan teknologi.
Hooli