Added "createTemporaryLob" flag to DefaultLobHandler, using JDBC 4.0's createBlob/Clob mechanism

Issue: SPR-10339
This commit is contained in:
Juergen Hoeller
2013-03-01 21:49:11 +01:00
committed by unknown
parent 7ce0406fbc
commit 2bd584ca75
3 changed files with 237 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -32,18 +32,20 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
/** /**
* Default implementation of the {@link LobHandler} interface. Invokes * Default implementation of the {@link LobHandler} interface.
* the direct accessor methods that {@code java.sql.ResultSet} * Invokes the direct accessor methods that {@code java.sql.ResultSet}
* and {@code java.sql.PreparedStatement} offer. * and {@code java.sql.PreparedStatement} offer.
* *
* <p>This LobHandler should work for any JDBC driver that is JDBC compliant * <p>This LobHandler should work for any JDBC driver that is JDBC compliant
* in terms of the spec's suggestions regarding simple BLOB and CLOB handling. * in terms of the spec's suggestions regarding simple BLOB and CLOB handling.
* This does not apply to Oracle 9i, and only to a limited degree to Oracle 10g! * This does not apply to Oracle 9i's drivers at all; as of Oracle 10g,
* As a consequence, use {@link OracleLobHandler} for accessing Oracle BLOBs/CLOBs. * it does work but may still come with LOB size limitations. Consider using
* recent Oracle drivers even when working against an older database server.
* See the {@link LobHandler} javadoc for the full set of recommendations.
* *
* <p>Some JDBC drivers require values with a BLOB/CLOB target column to be * <p>Some JDBC drivers require values with a BLOB/CLOB target column to be
* explicitly set through the JDBC {@code setBlob} / {@code setClob} * explicitly set through the JDBC {@code setBlob} / {@code setClob} API:
* API: for example, PostgreSQL's driver. Switch the {@link #setWrapAsLob "wrapAsLob"} * for example, PostgreSQL's driver. Switch the {@link #setWrapAsLob "wrapAsLob"}
* property to "true" when operating against such a driver. * property to "true" when operating against such a driver.
* *
* <p>On JDBC 4.0, this LobHandler also supports streaming the BLOB/CLOB content * <p>On JDBC 4.0, this LobHandler also supports streaming the BLOB/CLOB content
@@ -51,11 +53,15 @@ import org.apache.commons.logging.LogFactory;
* argument directly. Consider switching the {@link #setStreamAsLob "streamAsLob"} * argument directly. Consider switching the {@link #setStreamAsLob "streamAsLob"}
* property to "true" when operating against a fully compliant JDBC 4.0 driver. * property to "true" when operating against a fully compliant JDBC 4.0 driver.
* *
* <p>See the {@link LobHandler} javadoc for a summary of recommendations. * <p>Finally, primarily as a direct equivalent to {@link OracleLobHandler},
* this LobHandler also supports the creation of temporary BLOB/CLOB objects.
* Consider switching the {@link #setCreateTemporaryLob "createTemporaryLob"}
* property to "true" when "streamAsLob" happens to run into LOB size limitations.
*
* <p>See the {@link LobHandler} interface javadoc for a summary of recommendations.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 04.12.2003 * @since 04.12.2003
* @see #setStreamAsLob
* @see java.sql.ResultSet#getBytes * @see java.sql.ResultSet#getBytes
* @see java.sql.ResultSet#getBinaryStream * @see java.sql.ResultSet#getBinaryStream
* @see java.sql.ResultSet#getString * @see java.sql.ResultSet#getString
@@ -75,15 +81,18 @@ public class DefaultLobHandler extends AbstractLobHandler {
private boolean streamAsLob = false; private boolean streamAsLob = false;
private boolean createTemporaryLob = false;
/** /**
* Specify whether to submit a byte array / String to the JDBC driver * Specify whether to submit a byte array / String to the JDBC driver
* wrapped in a JDBC Blob / Clob object, using the JDBC {@code setBlob} / * wrapped in a JDBC Blob / Clob object, using the JDBC {@code setBlob} /
* {@code setClob} method with a Blob / Clob argument. * {@code setClob} method with a Blob / Clob argument.
* <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream} * <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
* / {@code setCharacterStream} method for setting the content. * / {@code setCharacterStream} method for setting the content. Switch this
* Switch this to "true" for explicit Blob / Clob wrapping against * to "true" for explicit Blob / Clob wrapping against JDBC drivers that
* JDBC drivers that are known to require such wrapping (e.g. PostgreSQL's). * are known to require such wrapping (e.g. PostgreSQL's for access to OID
* columns, whereas BYTEA columns need to be accessed the standard way).
* <p>This setting affects byte array / String arguments as well as stream * <p>This setting affects byte array / String arguments as well as stream
* arguments, unless {@link #setStreamAsLob "streamAsLob"} overrides this * arguments, unless {@link #setStreamAsLob "streamAsLob"} overrides this
* handling to use JDBC 4.0's new explicit streaming support (if available). * handling to use JDBC 4.0's new explicit streaming support (if available).
@@ -100,7 +109,7 @@ public class DefaultLobHandler extends AbstractLobHandler {
* {@code setClob} method with a stream argument. * {@code setClob} method with a stream argument.
* <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream} * <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
* / {@code setCharacterStream} method for setting the content. * / {@code setCharacterStream} method for setting the content.
* Switch this to "true" for explicit JDBC 4.0 usage, provided that your * Switch this to "true" for explicit JDBC 4.0 streaming, provided that your
* JDBC driver actually supports those JDBC 4.0 operations (e.g. Derby's). * JDBC driver actually supports those JDBC 4.0 operations (e.g. Derby's).
* <p>This setting affects stream arguments as well as byte array / String * <p>This setting affects stream arguments as well as byte array / String
* arguments, requiring JDBC 4.0 support. For supporting LOB content against * arguments, requiring JDBC 4.0 support. For supporting LOB content against
@@ -112,6 +121,23 @@ public class DefaultLobHandler extends AbstractLobHandler {
this.streamAsLob = streamAsLob; this.streamAsLob = streamAsLob;
} }
/**
* Specify whether to copy a byte array / String into a temporary JDBC
* Blob / Clob object created through the JDBC 4.0 {@code createBlob} /
* {@code createClob} methods.
* <p>Default is "false", using the common JDBC 2.0 {@code setBinaryStream}
* / {@code setCharacterStream} method for setting the content. Switch this
* to "true" for explicit Blob / Clob creation using JDBC 4.0.
* <p>This setting affects stream arguments as well as byte array / String
* arguments, requiring JDBC 4.0 support. For supporting LOB content against
* JDBC 3.0, check out the {@link #setWrapAsLob "wrapAsLob"} setting.
* @see java.sql.Connection#createBlob()
* @see java.sql.Connection#createClob()
*/
public void setCreateTemporaryLob(boolean createTemporaryLob) {
this.createTemporaryLob = createTemporaryLob;
}
public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException {
logger.debug("Returning BLOB as bytes"); logger.debug("Returning BLOB as bytes");
@@ -169,12 +195,12 @@ public class DefaultLobHandler extends AbstractLobHandler {
} }
public LobCreator getLobCreator() { public LobCreator getLobCreator() {
return new DefaultLobCreator(); return (this.createTemporaryLob ? new TemporaryLobCreator() : new DefaultLobCreator());
} }
/** /**
* Default LobCreator implementation as inner class. * Default LobCreator implementation as an inner class.
* Can be subclassed in DefaultLobHandler extensions. * Can be subclassed in DefaultLobHandler extensions.
*/ */
protected class DefaultLobCreator implements LobCreator { protected class DefaultLobCreator implements LobCreator {
@@ -268,15 +294,10 @@ public class DefaultLobHandler extends AbstractLobHandler {
PreparedStatement ps, int paramIndex, InputStream asciiStream, int contentLength) PreparedStatement ps, int paramIndex, InputStream asciiStream, int contentLength)
throws SQLException { throws SQLException {
if (streamAsLob || wrapAsLob) { if (streamAsLob) {
if (asciiStream != null) { if (asciiStream != null) {
try { try {
if (streamAsLob) { ps.setClob(paramIndex, new InputStreamReader(asciiStream, "US-ASCII"), contentLength);
ps.setClob(paramIndex, new InputStreamReader(asciiStream, "US-ASCII"), contentLength);
}
else {
ps.setClob(paramIndex, new PassThroughClob(asciiStream, contentLength));
}
} }
catch (UnsupportedEncodingException ex) { catch (UnsupportedEncodingException ex) {
throw new SQLException("US-ASCII encoding not supported: " + ex); throw new SQLException("US-ASCII encoding not supported: " + ex);
@@ -286,6 +307,14 @@ public class DefaultLobHandler extends AbstractLobHandler {
ps.setClob(paramIndex, (Clob) null); ps.setClob(paramIndex, (Clob) null);
} }
} }
else if (wrapAsLob) {
if (asciiStream != null) {
ps.setClob(paramIndex, new PassThroughClob(asciiStream, contentLength));
}
else {
ps.setClob(paramIndex, (Clob) null);
}
}
else { else {
ps.setAsciiStream(paramIndex, asciiStream, contentLength); ps.setAsciiStream(paramIndex, asciiStream, contentLength);
} }
@@ -325,7 +354,7 @@ public class DefaultLobHandler extends AbstractLobHandler {
} }
public void close() { public void close() {
// nothing to do here // nothing to do when not creating temporary LOBs
} }
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2013 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ import java.sql.SQLException;
/** /**
* Abstraction for handling large binary fields and large text fields in * Abstraction for handling large binary fields and large text fields in
* specific databases, no matter if represented as simple types or Large OBjects. * specific databases, no matter if represented as simple types or Large OBjects.
* Its main purpose is to isolate Oracle's peculiar handling of LOBs in * Its main purpose is to isolate Oracle 9i's peculiar handling of LOBs in
* {@link OracleLobHandler}; most other databases should be able to work * {@link OracleLobHandler}; most other databases should be able to work
* with the provided {@link DefaultLobHandler}. * with the provided {@link DefaultLobHandler}.
* *
@@ -45,8 +45,10 @@ import java.sql.SQLException;
* proprietary BLOB/CLOB API, and additionally doesn't accept large streams for * proprietary BLOB/CLOB API, and additionally doesn't accept large streams for
* PreparedStatement's corresponding setter methods. Therefore, you need to use * PreparedStatement's corresponding setter methods. Therefore, you need to use
* {@link OracleLobHandler} there, which uses Oracle's BLOB/CLOB API for both types * {@link OracleLobHandler} there, which uses Oracle's BLOB/CLOB API for both types
* of access. The Oracle 10g JDBC driver should basically work with * of access. The Oracle 10g+ JDBC driver will work with {@link DefaultLobHandler}
* {@link DefaultLobHandler} as well, with some limitations in terms of LOB sizes. * as well, with some limitations in terms of LOB sizes depending on DBMS setup;
* as of Oracle 11g (or actually, using the 11g driver even against older databases),
* there should be no need to use {@link OracleLobHandler} at all anymore.
* *
* <p>Of course, you need to declare different field types for each database. * <p>Of course, you need to declare different field types for each database.
* In Oracle, any binary content needs to go into a BLOB, and all character content * In Oracle, any binary content needs to go into a BLOB, and all character content
@@ -57,12 +59,20 @@ import java.sql.SQLException;
* *
* <p><b>Summarizing the recommended options (for actual LOB fields):</b> * <p><b>Summarizing the recommended options (for actual LOB fields):</b>
* <ul> * <ul>
* <li><b>JDBC 4.0 driver:</b> {@link DefaultLobHandler} with {@code streamAsLob=true}. * <li><b>JDBC 4.0 driver (including Oracle 11g driver):</b> Use {@link DefaultLobHandler},
* <li><b>PostgreSQL:</b> {@link DefaultLobHandler} with {@code wrapAsLob=true}. * potentially with {@code streamAsLob=true} if your database driver requires that
* <li><b>Oracle 9i/10g:</b> {@link OracleLobHandler} with a connection-pool-specific * hint when populating a LOB field. Fall back to {@code createTemporaryLob=true}
* if you happen to run into LOB size limitations with your (Oracle) database setup.
* <li><b>Oracle 10g driver:</b> Use {@link DefaultLobHandler} with standard setup.
* On Oracle 10.1, set the "SetBigStringTryClob" connection property; as of Oracle 10.2,
* DefaultLobHandler should work with standard setup out of the box. Alternatively,
* consider using the proprietary {@link OracleLobHandler} (see below).
* <li><b>Oracle 9i driver:</b> Use {@link OracleLobHandler} with a connection-pool-specific
* {@link OracleLobHandler#setNativeJdbcExtractor NativeJdbcExtractor}. * {@link OracleLobHandler#setNativeJdbcExtractor NativeJdbcExtractor}.
* <li><b>PostgreSQL:</b> Configure {@link DefaultLobHandler} with {@code wrapAsLob=true},
* and use that LobHandler to access OID columns (but not BYTEA) in your database tables.
* <li>For all other database drivers (and for non-LOB fields that might potentially * <li>For all other database drivers (and for non-LOB fields that might potentially
* turn into LOBs on some databases): a plain {@link DefaultLobHandler}. * turn into LOBs on some databases): Simply use a plain {@link DefaultLobHandler}.
* </ul> * </ul>
* *
* @author Juergen Hoeller * @author Juergen Hoeller

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jdbc.support.lob;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.util.FileCopyUtils;
/**
* {@link LobCreator} implementation based on temporary LOBs,
* using JDBC 4.0's {@link java.sql.Connection#createBlob()} /
* {@link java.sql.Connection#createClob()} mechanism.
*
* <p>Used by DefaultLobHandler's {@link DefaultLobHandler#setCreateTemporaryLob} mode.
* Can also be used directly to reuse the tracking and freeing of temporary LOBs.
*
* @author Juergen Hoeller
* @since 3.2.2
* @see DefaultLobHandler#setCreateTemporaryLob
* @see java.sql.Connection#createBlob()
* @see java.sql.Connection#createClob()
*/
public class TemporaryLobCreator implements LobCreator {
protected static final Log logger = LogFactory.getLog(TemporaryLobCreator.class);
private final Set<Blob> temporaryBlobs = new LinkedHashSet<Blob>(1);
private final Set<Clob> temporaryClobs = new LinkedHashSet<Clob>(1);
public void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte[] content)
throws SQLException {
Blob blob = ps.getConnection().createBlob();
blob.setBytes(1, content);
this.temporaryBlobs.add(blob);
ps.setBlob(paramIndex, blob);
if (logger.isDebugEnabled()) {
logger.debug(content != null ? "Copied bytes into temporary BLOB with length " + content.length :
"Set BLOB to null");
}
}
public void setBlobAsBinaryStream(
PreparedStatement ps, int paramIndex, InputStream binaryStream, int contentLength)
throws SQLException {
Blob blob = ps.getConnection().createBlob();
try {
FileCopyUtils.copy(binaryStream, blob.setBinaryStream(1));
}
catch (IOException ex) {
throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex);
}
this.temporaryBlobs.add(blob);
ps.setBlob(paramIndex, blob);
if (logger.isDebugEnabled()) {
logger.debug(binaryStream != null ?
"Copied binary stream into temporary BLOB with length " + contentLength :
"Set BLOB to null");
}
}
public void setClobAsString(PreparedStatement ps, int paramIndex, String content)
throws SQLException {
Clob clob = ps.getConnection().createClob();
clob.setString(1, content);
this.temporaryClobs.add(clob);
ps.setClob(paramIndex, clob);
if (logger.isDebugEnabled()) {
logger.debug(content != null ? "Copied string into temporary CLOB with length " + content.length() :
"Set CLOB to null");
}
}
public void setClobAsAsciiStream(
PreparedStatement ps, int paramIndex, InputStream asciiStream, int contentLength)
throws SQLException {
Clob clob = ps.getConnection().createClob();
try {
FileCopyUtils.copy(asciiStream, clob.setAsciiStream(1));
}
catch (IOException ex) {
throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex);
}
this.temporaryClobs.add(clob);
ps.setClob(paramIndex, clob);
if (logger.isDebugEnabled()) {
logger.debug(asciiStream != null ?
"Copied ASCII stream into temporary CLOB with length " + contentLength :
"Set CLOB to null");
}
}
public void setClobAsCharacterStream(
PreparedStatement ps, int paramIndex, Reader characterStream, int contentLength)
throws SQLException {
Clob clob = ps.getConnection().createClob();
try {
FileCopyUtils.copy(characterStream, clob.setCharacterStream(1));
}
catch (IOException ex) {
throw new DataAccessResourceFailureException("Could not copy into LOB stream", ex);
}
this.temporaryClobs.add(clob);
ps.setClob(paramIndex, clob);
if (logger.isDebugEnabled()) {
logger.debug(characterStream != null ?
"Copied character stream into temporary CLOB with length " + contentLength :
"Set CLOB to null");
}
}
public void close() {
try {
for (Blob blob : this.temporaryBlobs) {
blob.free();
}
for (Clob clob : this.temporaryClobs) {
clob.free();
}
}
catch (SQLException ex) {
logger.error("Could not free LOB", ex);
}
}
}