Как я могу получить SQL PreparedStatement?


160

У меня есть общий метод Java со следующей сигнатурой метода:

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

Он открывает соединение, создает с PreparedStatementпомощью оператора sql и параметров в queryParamsмассиве переменной длины, запускает его, кэширует ResultSet(в a CachedRowSetImpl), закрывает соединение и возвращает кэшированный набор результатов.

У меня есть обработка исключений в методе, который регистрирует ошибки. Я записываю SQL-оператор как часть журнала, так как он очень полезен для отладки. Моя проблема заключается в том, что запись в переменную String sqlзаписывает оператор шаблона с? Вместо фактических значений. Я хочу записать фактический оператор, который был выполнен (или попытался выполнить).

Итак ... Есть ли способ получить фактический оператор SQL, который будет выполняться PreparedStatement? ( Не создавая его сам. Если я не смогу найти способ получить доступ к PreparedStatement'sSQL, я, вероятно, в конечном итоге сам создам его в своем catch.)


1
Если вы пишете простой код JDBC, я настоятельно рекомендую взглянуть на Apache commons-dbutils commons.apache.org/dbutils . Это значительно упрощает код JDBC.
Кен Лю

Ответы:


173

Используя подготовленные операторы, «SQL-запрос» отсутствует:

  • У вас есть заявление, содержащее заполнители
    • отправлено на сервер БД
    • и подготовил там
    • это означает, что оператор SQL «анализируется», анализируется, некоторая структура данных, представляющая его, подготавливается в памяти
  • И тогда у вас есть связанные переменные
    • которые отправляются на сервер
    • и подготовленное заявление выполняется - работает над этими данными

Но реальный реальный запрос SQL не перестраивается - ни на стороне Java, ни на стороне базы данных.

Таким образом, нет никакого способа получить подготовленный оператор SQL - так как нет такого SQL.


В целях отладки решения могут быть следующими:

  • Ouput код заявления, с заполнителями и списком данных
  • Или «построить» какой-нибудь SQL-запрос «вручную».

22
Хотя это функционально верно, ничто не мешает служебному коду восстановить эквивалентный неподготовленный оператор. Например, в log4jdbc: «В записанном выводе для подготовленных операторов аргументы связывания автоматически вставляются в вывод SQL. Это значительно улучшает удобочитаемость и отладку во многих случаях». Очень полезно для отладки, если вы знаете, что это не то, как оператор фактически выполняется сервером БД.
звездный

6
Это также зависит от реализации. В MySQL - по крайней мере, в той версии, которую я использовал несколько лет назад - драйвер JDBC фактически строил обычный запрос SQL из шаблона и переменных связывания. Я полагаю, что версия MySQL изначально не поддерживала подготовленные операторы, поэтому они реализовали их в драйвере JDBC.
Jay

@sidereal: это то, что я имел в виду под « составить запрос вручную» ; но ты сказал это лучше меня ;;; @Jay: у нас такой же механизм в PHP (реально подготовленные операторы, когда они поддерживаются; псевдо-подготовленные операторы для драйверов баз данных, которые их не поддерживают)
Паскаль МАРТИН

6
Если вы используете java.sql.PreparedStatement, то простой метод .toString () для подготовленного состояния будет включать в себя сгенерированный SQL, который я проверял в 1.8.0_60
Pr0n

5
@Preston Для БД Oracle PreparedStatement # toString () не показывает SQL. Поэтому я думаю, это зависит от драйвера JDBC БД.
Майк Аргириу

57

Это нигде не определено в контракте API JDBC, но, если вам повезет, рассматриваемый драйвер JDBC может вернуть полный SQL, просто вызвав вызов PreparedStatement#toString(). Т.е.

System.out.println(preparedStatement);

По крайней мере, драйверы MySQL 5.x и PostgreSQL 8.x JDBC поддерживают его. Однако большинство других драйверов JDBC не поддерживают его. Если у вас есть такой, то лучше всего использовать Log4jdbc или P6Spy .

Кроме того, вы также можете написать универсальную функцию, которая принимает Connectionстроку SQL и значения оператора и возвращает 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интерфейса, который оборачивает (украшает) реальное PreparedStatement в конструкцию и переопределяет все методы так, что он вызывает методы реального PreparedStatement и собирает значения во всех setXXX()методах и лениво создает «фактическую» строку SQL всякий раз, когда один из эти executeXXX()методы называют (вполне рабочей, но большинство IDE обеспечивают автогенераторы методов декоратора, Eclipse делает). Наконец, просто используйте это вместо. Это также в основном то, что P6Spy и супруги уже делают под капотами.


Это похоже на метод, который я использую (ваш метод prepareStatement). Мой вопрос не в том, как это сделать, а в том, как войти в SQL-оператор. Я знаю, что я могу сделать logger.debug(sql + " " + Arrays.asList(values))- я ищу способ войти в оператор SQL с параметрами, уже интегрированными в него. Без зацикливания себя и замены знаков вопроса.
2010 года

Затем перейдите к последнему абзацу моего ответа или посмотрите на P6Spy. Они делают «
грязные» циклы

Ссылка на P6Spy теперь не работает.
Стивен П.

@BalusC Я новичок в JDBC. У меня есть одно сомнение. Если вы напишите обобщенную функцию, то она будет создаваться PreparedStatementкаждый раз. Не будет ли это не очень эффективным способом, потому что весь смысл в PreparedStatementтом, чтобы создать их один раз и использовать их повсеместно?
Бхушан,

Это также работает с драйвером voltdb jdbc, чтобы получить полный SQL-запрос для подготовленного оператора.
k0pernikus

37

Я использую Java 8, драйвер JDBC с MySQL коннектором 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;

Это должно иметь все положительные отзывы, поскольку это именно то, что ищет ОП.
HuckIt

Есть ли аналогичная функция для Postgres-драйвера?
Дэвидвессман

Как получить это для Apache Derby?
Gunasekar

Это не работает и выдает ClassCastException : java.lang.ClassCastException: oracle.jdbc.driver.T4CPreparedStatement не может быть приведен к com.mysql.jdbc.JDBC4PreparedStatement
Ercan

1
@ErcanDuman, мой ответ не универсален, он охватывает только драйверы Java 8 и MySQL JDBC.
userlond

21

Если вы выполнение запроса и ожидали ResultSet(вы в этом случае, по крайней мере) , то вы можете просто позвонить ResultSet«S getStatement()выглядеть примерно так:

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

Переменная executedQueryбудет содержать оператор, который использовался для создания ResultSet.

Теперь я понимаю, что этот вопрос довольно старый, но я надеюсь, что это поможет кому-то ..


6
@Elad Stern Он печатает, oracle.jdbc.driver.OraclePreparedStatementWrapper@1b9ce4b вместо печати выполненного оператора sql! Пожалуйста, ведите нас!
AVA

@AVA, ты использовал toString ()?
Elad Stern

@EladStern toString () используется!
AVA

@AVA, ну, я не уверен, но это может быть связано с твоим драйвером jdbc. Я успешно использовал mysql-connector-5.
Элад Стерн

3
rs.getStatement () просто возвращает объект оператора, поэтому все зависит от того, реализует ли используемый вами драйвер .toString (), который определяет, вернетесь ли вы к SQL
Daz

3

Я извлек свой sql из PreparedStatement с помощью prepareStatement.toString (). В моем случае toString () возвращает String так:

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

Теперь я создал метод (Java 8), который использует регулярные выражения для извлечения запроса и значений и помещает их в карту:

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 () отличается от того, что в моем случае, это просто вопрос «корректировки» регулярного выражения.


2

Использование PostgreSQL 9.6.x с официальным драйвером Java 42.2.4:

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

Покажет SQL с ?уже замененным, что я и искал. Просто добавил этот ответ, чтобы покрыть дело postgres.

Я бы никогда не подумал, что это может быть так просто.


1

Я реализовал следующий код для печати SQL из PrepareStatement

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

Фрагмент кода для преобразования 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 по

((OraclePreparedStatementWrapper) preparedStatement).getOriginalSql();

1
Когда я пытаюсь использовать оболочку, он говорит: 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 можно динамически построить в цикле (for, while). Я просто упростил задачу использования класса MessageFormat, который служит в качестве формирователя шаблонов строк для SQL-запроса.


4
Этот тип взрывает всю причину использования подготовленных операторов, таких как предотвращение SQL-инъекций и повышение производительности.
тиканье

1
Я согласен с вами на 100%. Я должен был дать понять, что я сделал этот код для выполнения не более пары раз для массовой интеграции больших объемов данных и остро нуждался в быстром способе получить некоторый вывод журнала, не вдаваясь в целую энчиладу log4j, которая была бы излишней из-за чего. Мне было нужно. Это не должно входить в производственный код :-)
Диего Терсеро

Это работает для меня с драйвером Oracle (но не включает в себя параметры):((OraclePreparedStatementWrapper) myPreparedStatement).getOriginalSql()
latj

-4

Для этого вам нужно JDBC-соединение и / или драйвер, который поддерживает ведение журнала sql на низком уровне.

Взгляните на log4jdbc


3
Взгляните на log4jdbc и что дальше? Как Вы этим пользуетесь? Вы заходите на этот сайт и видите случайный бродящий по проекту без четкого примера того, как на самом деле использовать технологию.
Хооли
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.