This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26067-jdbc-plain-placeholders in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6d32c3c8b01a3afad0b2001ebf3ff6bfe8fa0106 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Nov 5 21:29:14 2026 +0100 CAMEL-26067: camel-jdbc - plain ? placeholders take their values only from the CamelJdbcParameters header With useHeadersAsParameters=true and a query with only plain ? placeholders, the jdbc and spring-jdbc producers bound the values of all the message headers by position (or of the CamelJdbcParameters map when set). Which value went to which ? depended on the headers of the message and their order. The values of plain ? placeholders now come only from the CamelJdbcParameters header: a List in the order of the placeholders, or a Map whose values are taken in the order of its entries. When the header is not set, or is of another type, the exchange fails with a clear message. Named parameters work as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/components/jdbc.json | 6 +- .../camel/catalog/components/spring-jdbc.json | 6 +- .../apache/camel/catalog/docs/jdbc-component.adoc | 16 ++++++ .../org/apache/camel/component/jdbc/jdbc.json | 6 +- .../camel-jdbc/src/main/docs/jdbc-component.adoc | 16 ++++++ .../jdbc/DefaultJdbcPrepareStatementStrategy.java | 50 ++++++++++++----- .../apache/camel/component/jdbc/JdbcConstants.java | 8 ++- .../apache/camel/component/jdbc/JdbcEndpoint.java | 9 ++- .../component/jdbc/JdbcParameterizedQueryTest.java | 65 ++++++++++++++++++++-- .../camel/component/spring/jdbc/spring-jdbc.json | 6 +- .../component/spring/jdbc/SpringJdbcEndpoint.java | 5 +- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 +++++ .../endpoint/dsl/JdbcEndpointBuilderFactory.java | 16 ++++-- .../dsl/SpringJdbcEndpointBuilderFactory.java | 16 ++++-- 14 files changed, 189 insertions(+), 49 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json index e05623176303..1693491f416d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/jdbc.json @@ -33,14 +33,14 @@ "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is an UPDATE, query the update count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" }, "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is a SELECT and the outputType is SelectList, the row count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" }, "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The column names from the ResultSet as a java.util.Set type.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" }, - "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A java.util.Map which has the headers to be used if useHeadersAsParameters has been enabled.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" }, + "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The values of the query parameters when useHeadersAsParameters has been enabled: a java.util.Map with the values of the named parameters, or a java.util.List with the values of the positional placeholders, in order.", "constantName": [...] "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "Set its value to true to retrieve generated keys", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" }, "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Set it to specify the expected generated columns", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" }, "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of rows in the header that contains generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" }, "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Rows that contains the generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its placeholders come from the CamelJdbcParameters header, or else from the headers" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its named parameters (:?name) come from the CamelJdbcParameters header, or else from the headers, and the values of plain ? placeholders, in order, only from the CamelJdbcParameters header" }, "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", "value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": "outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ "iterator", "unchanged" ] } ], "description": "The rows of a select: a List of Maps of the column names and their values (beans of outputClass when set). SelectOne gives the Map of the one row, th [...] }, "properties": { @@ -53,7 +53,7 @@ "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": "Reset Auto Commit", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Camel will set the autoCommit on the JDBC connection to be false, commit the change after executed the statement and reset the autoCommit flag of the connection at the end, if the resetAutoCommit is true. If [...] "transacted": { "index": 7, "kind": "parameter", "displayName": "Transacted", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not used: the producer does not read this option", "autowired": false, "secret": false, "defaultValue": false, "description": "Whether transactions are in use. Deprecation note: Not used: the producer does not read this option" }, "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": "Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "To read BLOB columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read after the producer has closed the connection. This may be needed for certain databases such as Oracle where yo [...] - "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] + "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", "displayName": "Use JDBC4 Column Name And Label Semantics", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get the column name where as JDBC 3.0 uses both [...] "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean Row Mapper", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.component.jdbc.BeanRowMapper when using outputClass. The default implementation will lower case the row names and skip underscores, and dashes. For exampl [...] diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json index 905518eb3d89..e1c237003a65 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-jdbc.json @@ -33,14 +33,14 @@ "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is an UPDATE, query the update count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" }, "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is a SELECT and the outputType is SelectList, the row count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" }, "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The column names from the ResultSet as a java.util.Set type.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" }, - "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A java.util.Map which has the headers to be used if useHeadersAsParameters has been enabled.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" }, + "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The values of the query parameters when useHeadersAsParameters has been enabled: a java.util.Map with the values of the named parameters, or a java.util.List with the values of the positional placeholders, in order.", "constantName": [...] "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "Set its value to true to retrieve generated keys", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" }, "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Set it to specify the expected generated columns", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" }, "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of rows in the header that contains generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" }, "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Rows that contains the generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its placeholders come from the CamelJdbcParameters header, or else from the headers" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its named parameters (:?name) come from the CamelJdbcParameters header, or else from the headers, and the values of plain ? placeholders, in order, only from the CamelJdbcParameters header" }, "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", "value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": "outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ "iterator", "unchanged" ] } ], "description": "The rows of a select: a List of Maps of the column names and their values (beans of outputClass when set). SelectOne gives the Map of the one row, th [...] }, "properties": { @@ -53,7 +53,7 @@ "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": "Reset Auto Commit", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Camel will set the autoCommit on the JDBC connection to be false, commit the change after executed the statement and reset the autoCommit flag of the connection at the end, if the resetAutoCommit is true. If [...] "transacted": { "index": 7, "kind": "parameter", "displayName": "Transacted", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not used: the producer does not read this option", "autowired": false, "secret": false, "defaultValue": false, "description": "Deprecation note: Not used: the producer does not read this option" }, "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": "Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "To read BLOB columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read after the producer has closed the connection. This may be needed for certain databases such as Oracle where yo [...] - "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] + "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", "displayName": "Use JDBC4 Column Name And Label Semantics", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get the column name where as JDBC 3.0 uses both [...] "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean Row Mapper", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.component.jdbc.BeanRowMapper when using outputClass. The default implementation will lower case the row names and skip underscores, and dashes. For exampl [...] diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc index c055e8f3c8b2..5c691ac96fd9 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc @@ -159,6 +159,22 @@ YAML:: You can also store the header values in a `java.util.Map` and store the map on the headers with the key `CamelJdbcParameters`. +=== Using plain ? placeholders + +A query with plain `?` placeholders (and no named parameters) takes the values of the placeholders, in order, only from +the `CamelJdbcParameters` header, as a `java.util.List` (a `java.util.Map` works too, with its values taken in the order +of its entries, so use a `LinkedHashMap`). The other message headers are not used, and when the header is not set, the +exchange fails. + +._Java-only: inline Processor that sets the List of the values_ +[source,java] +---- +from("direct:projects") + .process(exchange -> exchange.getIn().setHeader(JdbcConstants.JDBC_PARAMETERS, List.of("ASF", 123))) + .setBody(constant("select * from projects where license = ? and id > ? order by id")) + .to("jdbc:myDataSource?useHeadersAsParameters=true"); +---- + == Examples In the following example, we set up the DataSource that camel-jdbc requires. diff --git a/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json b/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json index e05623176303..1693491f416d 100644 --- a/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json +++ b/components/camel-jdbc/src/generated/resources/META-INF/org/apache/camel/component/jdbc/jdbc.json @@ -33,14 +33,14 @@ "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is an UPDATE, query the update count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" }, "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is a SELECT and the outputType is SelectList, the row count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" }, "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The column names from the ResultSet as a java.util.Set type.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" }, - "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A java.util.Map which has the headers to be used if useHeadersAsParameters has been enabled.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" }, + "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The values of the query parameters when useHeadersAsParameters has been enabled: a java.util.Map with the values of the named parameters, or a java.util.List with the values of the positional placeholders, in order.", "constantName": [...] "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "Set its value to true to retrieve generated keys", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" }, "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Set it to specify the expected generated columns", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" }, "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of rows in the header that contains generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" }, "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Rows that contains the generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its placeholders come from the CamelJdbcParameters header, or else from the headers" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its named parameters (:?name) come from the CamelJdbcParameters header, or else from the headers, and the values of plain ? placeholders, in order, only from the CamelJdbcParameters header" }, "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", "value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": "outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ "iterator", "unchanged" ] } ], "description": "The rows of a select: a List of Maps of the column names and their values (beans of outputClass when set). SelectOne gives the Map of the one row, th [...] }, "properties": { @@ -53,7 +53,7 @@ "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": "Reset Auto Commit", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Camel will set the autoCommit on the JDBC connection to be false, commit the change after executed the statement and reset the autoCommit flag of the connection at the end, if the resetAutoCommit is true. If [...] "transacted": { "index": 7, "kind": "parameter", "displayName": "Transacted", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not used: the producer does not read this option", "autowired": false, "secret": false, "defaultValue": false, "description": "Whether transactions are in use. Deprecation note: Not used: the producer does not read this option" }, "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": "Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "To read BLOB columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read after the producer has closed the connection. This may be needed for certain databases such as Oracle where yo [...] - "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] + "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", "displayName": "Use JDBC4 Column Name And Label Semantics", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get the column name where as JDBC 3.0 uses both [...] "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean Row Mapper", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.component.jdbc.BeanRowMapper when using outputClass. The default implementation will lower case the row names and skip underscores, and dashes. For exampl [...] diff --git a/components/camel-jdbc/src/main/docs/jdbc-component.adoc b/components/camel-jdbc/src/main/docs/jdbc-component.adoc index c055e8f3c8b2..5c691ac96fd9 100644 --- a/components/camel-jdbc/src/main/docs/jdbc-component.adoc +++ b/components/camel-jdbc/src/main/docs/jdbc-component.adoc @@ -159,6 +159,22 @@ YAML:: You can also store the header values in a `java.util.Map` and store the map on the headers with the key `CamelJdbcParameters`. +=== Using plain ? placeholders + +A query with plain `?` placeholders (and no named parameters) takes the values of the placeholders, in order, only from +the `CamelJdbcParameters` header, as a `java.util.List` (a `java.util.Map` works too, with its values taken in the order +of its entries, so use a `LinkedHashMap`). The other message headers are not used, and when the header is not set, the +exchange fails. + +._Java-only: inline Processor that sets the List of the values_ +[source,java] +---- +from("direct:projects") + .process(exchange -> exchange.getIn().setHeader(JdbcConstants.JDBC_PARAMETERS, List.of("ASF", 123))) + .setBody(constant("select * from projects where license = ? and id > ? order by id")) + .to("jdbc:myDataSource?useHeadersAsParameters=true"); +---- + == Examples In the following example, we set up the DataSource that camel-jdbc requires. diff --git a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java index e494c44fbbb5..613a1eceadef 100644 --- a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java +++ b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/DefaultJdbcPrepareStatementStrategy.java @@ -18,6 +18,7 @@ package org.apache.camel.component.jdbc; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.regex.Matcher; @@ -55,19 +56,19 @@ public class DefaultJdbcPrepareStatementStrategy implements JdbcPrepareStatement final String query, final String preparedQuery, final int expectedParams, final Exchange exchange, final Object value) throws SQLException { - Map<?, ?> map = null; - if (exchange.getIn().hasHeaders()) { - if (exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS) != null) { - // header JDBC_PARAMETERS takes precedence over regular headers - map = exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS, Map.class); - } else { - map = exchange.getIn().getHeaders(); + if (hasNamedParameters(query)) { + Map<?, ?> map = null; + if (exchange.getIn().hasHeaders()) { + if (exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS) != null) { + // header JDBC_PARAMETERS takes precedence over regular headers + map = exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS, Map.class); + } else { + map = exchange.getIn().getHeaders(); + } } - } - final Map<?, ?> headerMap = map; + final Map<?, ?> headerMap = map; - if (hasNamedParameters(query)) { // create an iterator that returns the value in the named order try { @@ -118,9 +119,32 @@ public class DefaultJdbcPrepareStatementStrategy implements JdbcPrepareStatement } } else { - // just use a regular iterator - return exchange.getContext().getTypeConverter().convertTo(Iterator.class, - headerMap != null ? headerMap.values() : null); + // plain ? placeholders are bound by position, so the values are only taken from the JDBC_PARAMETERS header + // (and not from all the message headers, as their order does not relate to the query) + return createPositionalIterator(query, exchange); + } + } + + private Iterator<?> createPositionalIterator(String query, Exchange exchange) { + Object parameters = exchange.getIn().getHeader(JdbcConstants.JDBC_PARAMETERS); + if (parameters instanceof Map<?, ?> map) { + return map.values().iterator(); + } else if (parameters instanceof Iterable<?> iterable) { + return iterable.iterator(); + } else if (parameters instanceof Object[] array) { + return Arrays.asList(array).iterator(); + } else if (parameters == null) { + throw new RuntimeExchangeException( + "Cannot set the ? placeholders in query [" + query + "]: set their values, in order, as a List in the " + + JdbcConstants.JDBC_PARAMETERS + + " header, or use named parameters (:?name)", + exchange); + } else { + throw new RuntimeExchangeException( + "Cannot set the ? placeholders in query [" + query + "]: the " + JdbcConstants.JDBC_PARAMETERS + + " header must be a List or a Map with their values, in order, but was " + + parameters.getClass().getName(), + exchange); } } diff --git a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java index 786c9f1c7606..a672e020af7a 100644 --- a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java +++ b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcConstants.java @@ -35,9 +35,11 @@ public final class JdbcConstants { "type.", javaType = "Set<String>") public static final String JDBC_COLUMN_NAMES = "CamelJdbcColumnNames"; - @Metadata(label = "producer", description = "A `java.util.Map` which has the headers to be used if\n" + - "`useHeadersAsParameters` has been enabled.", - javaType = "Map") + @Metadata(label = "producer", + description = "The values of the query parameters when `useHeadersAsParameters` has been enabled: a\n" + + "`java.util.Map` with the values of the named parameters, or a `java.util.List` with the\n" + + "values of the positional placeholders, in order.", + javaType = "Object") public static final String JDBC_PARAMETERS = "CamelJdbcParameters"; /** diff --git a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java index 26545f4053d2..d5a146c7986a 100644 --- a/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java +++ b/components/camel-jdbc/src/main/java/org/apache/camel/component/jdbc/JdbcEndpoint.java @@ -42,8 +42,9 @@ import org.apache.camel.support.DefaultEndpoint; @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE }, description = "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List" + " is not a statement: set the SQL as the body. With useHeadersAsParameters," - + " the values of its placeholders come from the CamelJdbcParameters header, or" - + " else from the headers"), + + " the values of its named parameters (:?name) come from the" + + " CamelJdbcParameters header, or else from the headers, and the values of" + + " plain ? placeholders, in order, only from the CamelJdbcParameters header"), producerOut = @BodyType(value = { Carrier.LIST, Carrier.UNCHANGED }, byOption = { @BodyType.When(option = "outputType", value = "SelectOne", @@ -243,7 +244,9 @@ public class JdbcEndpoint extends DefaultEndpoint { /** * Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries - * with named placeholders, and use headers with the dynamic values for the query placeholders. + * with named placeholders, and use headers with the dynamic values for the query placeholders. A query with only + * positional placeholders (no named parameters) takes their values, in order, only from the CamelJdbcParameters + * header (a List). */ public void setUseHeadersAsParameters(boolean useHeadersAsParameters) { this.useHeadersAsParameters = useHeadersAsParameters; diff --git a/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java b/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java index e573f022392a..3614ea3c0685 100644 --- a/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java +++ b/components/camel-jdbc/src/test/java/org/apache/camel/component/jdbc/JdbcParameterizedQueryTest.java @@ -17,15 +17,20 @@ package org.apache.camel.component.jdbc; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.apache.camel.CamelExecutionException; +import org.apache.camel.RuntimeExchangeException; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; import org.junit.jupiter.api.Test; import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class JdbcParameterizedQueryTest extends AbstractJdbcTestSupport { @@ -34,13 +39,14 @@ public class JdbcParameterizedQueryTest extends AbstractJdbcTestSupport { MockEndpoint mock = getMockEndpoint("mock:result"); mock.expectedMessageCount(1); - // The linkedHashMap values has different order in JDK7 and JDK8 - // so I had to reduce the parameters size - Map<String, Object> jdbcParams = new HashMap<>(); - jdbcParams.put("name", "jstrachan"); + Map<String, Object> headers = new LinkedHashMap<>(); + // other headers are not bound to the ? placeholders + headers.put("foo", "cust2"); + headers.put("bar", "nsandhu"); + headers.put(JdbcConstants.JDBC_PARAMETERS, List.of("cust1", "jstrachan")); - template.sendBodyAndHeaders("direct:start", "select * from customer where id = 'cust1' and name = ? order by ID", - jdbcParams); + template.sendBodyAndHeaders("direct:start", "select * from customer where id = ? and name = ? order by ID", + headers); MockEndpoint.assertIsSatisfied(context); @@ -50,6 +56,53 @@ public class JdbcParameterizedQueryTest extends AbstractJdbcTestSupport { assertEquals("jstrachan", row.get("NAME")); } + @Test + public void testParameterizedQueryNoNamesMap() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + // the values of a map are bound in the order of its entries + Map<String, Object> jdbcParams = new LinkedHashMap<>(); + jdbcParams.put("id", "cust3"); + jdbcParams.put("name", "willem"); + + Map<String, Object> headers = new LinkedHashMap<>(); + headers.put("foo", "cust2"); + headers.put(JdbcConstants.JDBC_PARAMETERS, jdbcParams); + + template.sendBodyAndHeaders("direct:start", "select * from customer where id = ? and name = ? order by ID", + headers); + + MockEndpoint.assertIsSatisfied(context); + + List<?> received = assertIsInstanceOf(List.class, mock.getReceivedExchanges().get(0).getIn().getBody()); + assertEquals(1, received.size()); + Map<?, ?> row = assertIsInstanceOf(Map.class, received.get(0)); + assertEquals("willem", row.get("NAME")); + } + + @Test + public void testParameterizedQueryNoNamesNotFromHeaders() { + Map<String, Object> headers = new LinkedHashMap<>(); + headers.put("id", "cust1"); + headers.put("name", "jstrachan"); + + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.sendBodyAndHeaders("direct:start", + "select * from customer where id = ? and name = ? order by ID", headers)); + RuntimeExchangeException cause = assertIsInstanceOf(RuntimeExchangeException.class, e.getCause()); + assertTrue(cause.getMessage().contains("as a List in the CamelJdbcParameters header"), cause.getMessage()); + } + + @Test + public void testParameterizedQueryNoNamesInvalidParameters() { + CamelExecutionException e = assertThrows(CamelExecutionException.class, + () -> template.sendBodyAndHeader("direct:start", + "select * from customer where id = ? order by ID", JdbcConstants.JDBC_PARAMETERS, "cust1")); + RuntimeExchangeException cause = assertIsInstanceOf(RuntimeExchangeException.class, e.getCause()); + assertTrue(cause.getMessage().contains("must be a List or a Map"), cause.getMessage()); + } + @Test public void testParameterizedQuery() throws Exception { MockEndpoint mock = getMockEndpoint("mock:result"); diff --git a/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json b/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json index 905518eb3d89..e1c237003a65 100644 --- a/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json +++ b/components/camel-spring-parent/camel-spring-jdbc/src/generated/resources/META-INF/org/apache/camel/component/spring/jdbc/spring-jdbc.json @@ -33,14 +33,14 @@ "CamelJdbcUpdateCount": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is an UPDATE, query the update count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_UPDATE_COUNT" }, "CamelJdbcRowCount": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "If the query is a SELECT and the outputType is SelectList, the row count is returned in this OUT header.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_ROW_COUNT" }, "CamelJdbcColumnNames": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Set<String>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The column names from the ResultSet as a java.util.Set type.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_COLUMN_NAMES" }, - "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "A java.util.Map which has the headers to be used if useHeadersAsParameters has been enabled.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_PARAMETERS" }, + "CamelJdbcParameters": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The values of the query parameters when useHeadersAsParameters has been enabled: a java.util.Map with the values of the named parameters, or a java.util.List with the values of the positional placeholders, in order.", "constantName": [...] "CamelRetrieveGeneratedKeys": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "Set its value to true to retrieve generated keys", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_RETRIEVE_GENERATED_KEYS" }, "CamelGeneratedColumns": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "String[] or int[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Set it to specify the expected generated columns", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_COLUMNS" }, "CamelGeneratedKeysRowCount": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of rows in the header that contains generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_ROW_COUNT" }, "CamelGeneratedKeysRows": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "producer", "required": false, "javaType": "List<Map<String, Object>>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Rows that contains the generated keys.", "constantName": "org.apache.camel.component.jdbc.JdbcConstants#JDBC_GENERATED_KEYS_DATA" } }, "bodyTypes": { - "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its placeholders come from the CamelJdbcParameters header, or else from the headers" }, + "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ], "description": "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List is not a statement: set the SQL as the body. With useHeadersAsParameters, the values of its named parameters (:?name) come from the CamelJdbcParameters header, or else from the headers, and the values of plain ? placeholders, in order, only from the CamelJdbcParameters header" }, "producerOut": { "carriers": [ "list", "unchanged" ], "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [ "map", "text", "value", "bytes", "pojo", "null", "unchanged" ], "javaTypeOption": "outputClass" }, { "option": "outputType", "value": "StreamList", "carriers": [ "iterator", "unchanged" ] } ], "description": "The rows of a select: a List of Maps of the column names and their values (beans of outputClass when set). SelectOne gives the Map of the one row, th [...] }, "properties": { @@ -53,7 +53,7 @@ "resetAutoCommit": { "index": 6, "kind": "parameter", "displayName": "Reset Auto Commit", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Camel will set the autoCommit on the JDBC connection to be false, commit the change after executed the statement and reset the autoCommit flag of the connection at the end, if the resetAutoCommit is true. If [...] "transacted": { "index": 7, "kind": "parameter", "displayName": "Transacted", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": true, "deprecationNote": "Not used: the producer does not read this option", "autowired": false, "secret": false, "defaultValue": false, "description": "Deprecation note: Not used: the producer does not read this option" }, "useGetBytesForBlob": { "index": 8, "kind": "parameter", "displayName": "Use Get Bytes For Blob", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "To read BLOB columns as bytes (a byte) instead of a java.sql.Blob, which cannot be read after the producer has closed the connection. This may be needed for certain databases such as Oracle where yo [...] - "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] + "useHeadersAsParameters": { "index": 9, "kind": "parameter", "displayName": "Use Headers As Parameters", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Set this option to true to use the prepareStatementStrategy with named parameters. This allows to define queries with named placeholders, and use headers with the dynamic values for the query [...] "useJDBC4ColumnNameAndLabelSemantics": { "index": 10, "kind": "parameter", "displayName": "Use JDBC4 Column Name And Label Semantics", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Sets whether to use JDBC 4 or JDBC 3.0 or older semantic when retrieving column name. JDBC 4.0 uses columnLabel to get the column name where as JDBC 3.0 uses both [...] "lazyStartProducer": { "index": 11, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] "beanRowMapper": { "index": 12, "kind": "parameter", "displayName": "Bean Row Mapper", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.jdbc.BeanRowMapper", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom org.apache.camel.component.jdbc.BeanRowMapper when using outputClass. The default implementation will lower case the row names and skip underscores, and dashes. For exampl [...] diff --git a/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java b/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java index be34e4e8860c..14228b9ff76e 100644 --- a/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java +++ b/components/camel-spring-parent/camel-spring-jdbc/src/main/java/org/apache/camel/component/spring/jdbc/SpringJdbcEndpoint.java @@ -35,8 +35,9 @@ import org.apache.camel.spi.UriEndpoint; @BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE }, description = "The SQL statement as text, or bytes, a stream or a file of it; a Map or a List" + " is not a statement: set the SQL as the body. With useHeadersAsParameters," - + " the values of its placeholders come from the CamelJdbcParameters header, or" - + " else from the headers"), + + " the values of its named parameters (:?name) come from the" + + " CamelJdbcParameters header, or else from the headers, and the values of" + + " plain ? placeholders, in order, only from the CamelJdbcParameters header"), producerOut = @BodyType(value = { Carrier.LIST, Carrier.UNCHANGED }, byOption = { @BodyType.When(option = "outputType", value = "SelectOne", diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index c7fa7e136c87..ab5af7824254 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -1190,6 +1190,19 @@ CamelContext of that provider. It now closes each of them only when no other `JC it returned the closed cache, whose operations throw `IllegalStateException`. This matters for code that calls a `JCacheManager` (for example one created by a custom `JCacheManagerFactory`) after it was closed. +=== camel-jdbc, camel-spring-jdbc - plain ? placeholders take their values only from the CamelJdbcParameters header + +With `useHeadersAsParameters=true` and a query with only plain `?` placeholders (no named parameters `:?name`), the +`jdbc` and `spring-jdbc` producers bound the values of all the message headers by position, or of the +`CamelJdbcParameters` header when it was set. Which value went to which `?` then depended on the headers of the +message and their order (which changed in 4.21 from alphabetical to the order they were added). The values of plain +`?` placeholders now come only from the `CamelJdbcParameters` header: a `List` with the values in the order of the +placeholders (or a `Map`, whose values are taken in the order of its entries). When the header is not set, the +exchange fails with a `RuntimeExchangeException`. Set the values in that header, or use named parameters. + +Named parameters work as before: their values come from the `CamelJdbcParameters` header (a `Map`) when it is set, or +else from the message headers. + === camel-jgroups-raft - a body that does not convert to a byte array fails the exchange The `jgroups-raft` producer skipped a message body that has no conversion to `byte[]`, such as a `Map`, a POJO or a diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java index d70b960c88c7..d7513bf8576a 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/JdbcEndpointBuilderFactory.java @@ -310,7 +310,9 @@ public interface JdbcEndpointBuilderFactory { * Set this option to true to use the prepareStatementStrategy with * named parameters. This allows to define queries with named * placeholders, and use headers with the dynamic values for the query - * placeholders. + * placeholders. A query with only positional placeholders (no named + * parameters) takes their values, in order, only from the + * CamelJdbcParameters header (a List). * * The option is a: <code>boolean</code> type. * @@ -328,7 +330,9 @@ public interface JdbcEndpointBuilderFactory { * Set this option to true to use the prepareStatementStrategy with * named parameters. This allows to define queries with named * placeholders, and use headers with the dynamic values for the query - * placeholders. + * placeholders. A query with only positional placeholders (no named + * parameters) takes their values, in order, only from the + * CamelJdbcParameters header (a List). * * The option will be converted to a <code>boolean</code> type. * @@ -659,10 +663,12 @@ public interface JdbcEndpointBuilderFactory { return "CamelJdbcColumnNames"; } /** - * A java.util.Map which has the headers to be used if - * useHeadersAsParameters has been enabled. + * The values of the query parameters when useHeadersAsParameters has + * been enabled: a java.util.Map with the values of the named + * parameters, or a java.util.List with the values of the positional + * placeholders, in order. * - * The option is a: {@code Map} type. + * The option is a: {@code Object} type. * * Group: producer * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java index 8a05397e4f0c..0c49324312b0 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/SpringJdbcEndpointBuilderFactory.java @@ -304,7 +304,9 @@ public interface SpringJdbcEndpointBuilderFactory { * Set this option to true to use the prepareStatementStrategy with * named parameters. This allows to define queries with named * placeholders, and use headers with the dynamic values for the query - * placeholders. + * placeholders. A query with only positional placeholders (no named + * parameters) takes their values, in order, only from the + * CamelJdbcParameters header (a List). * * The option is a: <code>boolean</code> type. * @@ -322,7 +324,9 @@ public interface SpringJdbcEndpointBuilderFactory { * Set this option to true to use the prepareStatementStrategy with * named parameters. This allows to define queries with named * placeholders, and use headers with the dynamic values for the query - * placeholders. + * placeholders. A query with only positional placeholders (no named + * parameters) takes their values, in order, only from the + * CamelJdbcParameters header (a List). * * The option will be converted to a <code>boolean</code> type. * @@ -656,10 +660,12 @@ public interface SpringJdbcEndpointBuilderFactory { return "CamelJdbcColumnNames"; } /** - * A java.util.Map which has the headers to be used if - * useHeadersAsParameters has been enabled. + * The values of the query parameters when useHeadersAsParameters has + * been enabled: a java.util.Map with the values of the named + * parameters, or a java.util.List with the values of the positional + * placeholders, in order. * - * The option is a: {@code Map} type. + * The option is a: {@code Object} type. * * Group: producer *
