Spring Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

349 lines
14 KiB

[[jdbc-parameter-handling]]
= Common Problems with Parameter and Data Value Handling
Common problems with parameters and data values exist in the different approaches
provided by Spring Framework's JDBC support. This section covers how to address them.
[[jdbc-type-information]]
== Providing SQL Type Information for Parameters
Usually, Spring determines the SQL type of the parameters based on the type of parameter
passed in. It is possible to explicitly provide the SQL type to be used when setting
parameter values. This is sometimes necessary to correctly set `NULL` values.
You can provide SQL type information in several ways:
* Many update and query methods of the `JdbcTemplate` take an additional parameter in
the form of an `int` array. This array is used to indicate the SQL type of the
corresponding parameter by using constant values from the `java.sql.Types` class. Provide
one entry for each parameter.
* You can use the `SqlParameterValue` class to wrap the parameter value that needs this
additional information. To do so, create a new instance for each value and pass in the SQL type
and the parameter value in the constructor. You can also provide an optional scale
parameter for numeric values.
* For methods that work with named parameters, you can use the `SqlParameterSource` classes,
`BeanPropertySqlParameterSource` or `MapSqlParameterSource`. They both have methods
for registering the SQL type for any of the named parameter values.
[[jdbc-lob]]
== Handling BLOB and CLOB objects
You can store images, other binary data, and large chunks of text in the database. These
large objects are called BLOBs (Binary Large OBject) for binary data and CLOBs (Character
Large OBject) for character data. In Spring, you can handle these large objects by using
the `JdbcTemplate` directly and also when using the higher abstractions provided by RDBMS
Objects and the `SimpleJdbc` classes. All of these approaches use an implementation of
the `LobHandler` interface for the actual management of the LOB (Large OBject) data.
`LobHandler` provides access to a `LobCreator` class, through the `getLobCreator` method,
that is used for creating new LOB objects to be inserted.
`LobCreator` and `LobHandler` provide the following support for LOB input and output:
* BLOB
** `byte[]`: `getBlobAsBytes` and `setBlobAsBytes`
** `InputStream`: `getBlobAsBinaryStream` and `setBlobAsBinaryStream`
* CLOB
** `String`: `getClobAsString` and `setClobAsString`
** `InputStream`: `getClobAsAsciiStream` and `setClobAsAsciiStream`
** `Reader`: `getClobAsCharacterStream` and `setClobAsCharacterStream`
The next example shows how to create and insert a BLOB. Later we show how to read
it back from the database.
This example uses a `JdbcTemplate` and an implementation of the
`AbstractLobCreatingPreparedStatementCallback`. It implements one method,
`setValues`. This method provides a `LobCreator` that we use to set the values for the
LOB columns in your SQL insert statement.
For this example, we assume that there is a variable, `lobHandler`, that is already
set to an instance of a `DefaultLobHandler`. You typically set this value through
dependency injection.
The following example shows how to create and insert a BLOB:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
new AbstractLobCreatingPreparedStatementCallback(lobHandler) { // <1>
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); // <2>
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); // <3>
}
}
);
blobIs.close();
clobReader.close();
----
<1> Pass in the `lobHandler` that (in this example) is a plain `DefaultLobHandler`.
<2> Using the method `setClobAsCharacterStream` to pass in the contents of the CLOB.
<3> Using the method `setBlobAsBinaryStream` to pass in the contents of the BLOB.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val blobIn = File("spring2004.jpg")
val blobIs = FileInputStream(blobIn)
val clobIn = File("large.txt")
val clobIs = FileInputStream(clobIn)
val clobReader = InputStreamReader(clobIs)
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
object: AbstractLobCreatingPreparedStatementCallback(lobHandler) { // <1>
override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) {
ps.setLong(1, 1L)
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt()) // <2>
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt()) // <3>
}
}
)
blobIs.close()
clobReader.close()
----
<1> Pass in the `lobHandler` that (in this example) is a plain `DefaultLobHandler`.
<2> Using the method `setClobAsCharacterStream` to pass in the contents of the CLOB.
<3> Using the method `setBlobAsBinaryStream` to pass in the contents of the BLOB.
======
[NOTE]
====
If you invoke the `setBlobAsBinaryStream`, `setClobAsAsciiStream`, or
`setClobAsCharacterStream` method on the `LobCreator` returned from
`DefaultLobHandler.getLobCreator()`, you can optionally specify a negative value
for the `contentLength` argument. If the specified content length is negative, the
`DefaultLobHandler` uses the JDBC 4.0 variants of the set-stream methods without a
length parameter. Otherwise, it passes the specified length on to the driver.
See the documentation for the JDBC driver you use to verify that it supports streaming
a LOB without providing the content length.
====
Now it is time to read the LOB data from the database. Again, you use a `JdbcTemplate`
with the same instance variable `lobHandler` and a reference to a `DefaultLobHandler`.
The following example shows how to do so:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob"); // <1>
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); // <2>
results.put("BLOB", blobBytes);
return results;
}
});
----
<1> Using the method `getClobAsString` to retrieve the contents of the CLOB.
<2> Using the method `getBlobAsBytes` to retrieve the contents of the BLOB.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ ->
val clobText = lobHandler.getClobAsString(rs, "a_clob") // <1>
val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob") // <2>
mapOf("CLOB" to clobText, "BLOB" to blobBytes)
}
----
<1> Using the method `getClobAsString` to retrieve the contents of the CLOB.
<2> Using the method `getBlobAsBytes` to retrieve the contents of the BLOB.
======
[[jdbc-in-clause]]
== Passing in Lists of Values for IN Clause
The SQL standard allows for selecting rows based on an expression that includes a
variable list of values. A typical example would be `select * from T_ACTOR where id in
(1, 2, 3)`. This variable list is not directly supported for prepared statements by the
JDBC standard. You cannot declare a variable number of placeholders. You need a number
of variations with the desired number of placeholders prepared, or you need to generate
the SQL string dynamically once you know how many placeholders are required. The named
parameter support provided in the `NamedParameterJdbcTemplate` takes the latter approach.
You can pass in the values as a `java.util.List` (or any `Iterable`) of simple values.
This list is used to insert the required placeholders into the actual SQL statement
and pass in the values during statement execution.
NOTE: Be careful when passing in many values. The JDBC standard does not guarantee that
you can use more than 100 values for an `IN` expression list. Various databases exceed
this number, but they usually have a hard limit for how many values are allowed.
For example, Oracle's limit is 1000.
In addition to the primitive values in the value list, you can create a `java.util.List`
of object arrays. This list can support multiple expressions being defined for the `in`
clause, such as `+++select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2,
'Harrop'))+++`. This, of course, requires that your database supports this syntax.
[[jdbc-complex-types]]
== Handling Complex Types for Stored Procedure Calls
When you call stored procedures, you can sometimes use complex types specific to the
database. To accommodate these types, Spring provides a `SqlReturnType` for handling
them when they are returned from the stored procedure call and `SqlTypeValue` when they
are passed in as a parameter to the stored procedure.
The `SqlReturnType` interface has a single method (named `getTypeValue`) that must be
implemented. This interface is used as part of the declaration of an `SqlOutParameter`.
The following example shows returning the value of an Oracle `STRUCT` object of the user
declared type `ITEM_TYPE`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}));
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
// ...
declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName ->
val struct = cs.getObject(colIndx) as STRUCT
val attr = struct.getAttributes()
TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date)
})
// ...
}
}
----
======
You can use `SqlTypeValue` to pass the value of a Java object (such as `TestItem`) to a
stored procedure. The `SqlTypeValue` interface has a single method (named
`createTypeValue`) that you must implement. The active connection is passed in, and you
can use it to create database-specific objects, such as `StructDescriptor` instances
or `ArrayDescriptor` instances. The following example creates a `StructDescriptor` instance:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
val (id, description, expirationDate) = TestItem(123L, "A test item",
SimpleDateFormat("yyyy-M-d").parse("2010-12-31"))
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val itemDescriptor = StructDescriptor(typeName, conn)
return STRUCT(itemDescriptor, conn,
arrayOf(id, description, java.sql.Date(expirationDate.time)))
}
}
----
======
You can now add this `SqlTypeValue` to the `Map` that contains the input parameters for the
`execute` call of the stored procedure.
Another use for the `SqlTypeValue` is passing in an array of values to an Oracle stored
procedure. Oracle has its own internal `ARRAY` class that must be used in this case, and
you can use the `SqlTypeValue` to create an instance of the Oracle `ARRAY` and populate
it with values from the Java `ARRAY`, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() {
init {
val ids = arrayOf(1L, 2L)
val value = object : AbstractSqlTypeValue() {
override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any {
val arrayDescriptor = ArrayDescriptor(typeName, conn)
return ARRAY(arrayDescriptor, conn, ids)
}
}
}
}
----
======