HHH-2680 - Blobs not updated on Session.merge() for detached instances

This commit is contained in:
Steve Ebersole 2011-03-23 17:30:13 -05:00
parent fd08540859
commit a491f64570
14 changed files with 438 additions and 41 deletions

View File

@ -23,7 +23,12 @@
*/
package org.hibernate.dialect;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.NClob;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
@ -50,6 +55,8 @@ import org.hibernate.dialect.lock.PessimisticForceIncrementLockingStrategy;
import org.hibernate.dialect.lock.PessimisticReadSelectLockingStrategy;
import org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy;
import org.hibernate.dialect.lock.SelectLockingStrategy;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.jdbc.LobCreator;
import org.hibernate.exception.SQLExceptionConverter;
import org.hibernate.exception.SQLStateConverter;
import org.hibernate.exception.ViolatedConstraintNameExtracter;
@ -58,6 +65,8 @@ import org.hibernate.id.SequenceGenerator;
import org.hibernate.id.TableHiLoGenerator;
import org.hibernate.internal.util.ReflectHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.io.StreamCopier;
import org.hibernate.mapping.Column;
import org.hibernate.persister.entity.Lockable;
import org.hibernate.sql.ANSICaseFragment;
@ -357,6 +366,123 @@ public abstract class Dialect {
return descriptor;
}
/**
* Merge strategy based on transferring contents based on streams.
*/
protected static final LobMergeStrategy STREAM_XFER_LOB_MERGE_STRATEGY = new LobMergeStrategy() {
@Override
public Blob mergeBlob(Blob original, Blob target, SessionImplementor session) {
if ( original != target ) {
try {
OutputStream connectedStream = target.setBinaryStream( 1L ); // the BLOB just read during the load phase of merge
InputStream detachedStream = original.getBinaryStream(); // the BLOB from the detached state
StreamCopier.copy( detachedStream, connectedStream );
return target;
}
catch (SQLException e ) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge BLOB data" );
}
}
else {
return NEW_LOCATOR_LOB_MERGE_STRATEGY.mergeBlob( original, target, session );
}
}
@Override
public Clob mergeClob(Clob original, Clob target, SessionImplementor session) {
if ( original != target ) {
try {
OutputStream connectedStream = target.setAsciiStream( 1L ); // the CLOB just read during the load phase of merge
InputStream detachedStream = original.getAsciiStream(); // the CLOB from the detached state
StreamCopier.copy( detachedStream, connectedStream );
return target;
}
catch (SQLException e ) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge CLOB data" );
}
}
else {
return NEW_LOCATOR_LOB_MERGE_STRATEGY.mergeClob( original, target, session );
}
}
@Override
public NClob mergeNClob(NClob original, NClob target, SessionImplementor session) {
if ( original != target ) {
try {
OutputStream connectedStream = target.setAsciiStream( 1L ); // the NCLOB just read during the load phase of merge
InputStream detachedStream = original.getAsciiStream(); // the NCLOB from the detached state
StreamCopier.copy( detachedStream, connectedStream );
return target;
}
catch (SQLException e ) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge NCLOB data" );
}
}
else {
return NEW_LOCATOR_LOB_MERGE_STRATEGY.mergeNClob( original, target, session );
}
}
};
/**
* Merge strategy based on creating a new LOB locator.
*/
protected static final LobMergeStrategy NEW_LOCATOR_LOB_MERGE_STRATEGY = new LobMergeStrategy() {
@Override
public Blob mergeBlob(Blob original, Blob target, SessionImplementor session) {
if ( original == null && target == null ) {
return null;
}
try {
LobCreator lobCreator = session.getFactory().getJdbcServices().getLobCreator( session );
return original == null
? lobCreator.createBlob( ArrayHelper.EMPTY_BYTE_ARRAY )
: lobCreator.createBlob( original.getBinaryStream(), original.length() );
}
catch (SQLException e) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge BLOB data" );
}
}
@Override
public Clob mergeClob(Clob original, Clob target, SessionImplementor session) {
if ( original == null && target == null ) {
return null;
}
try {
LobCreator lobCreator = session.getFactory().getJdbcServices().getLobCreator( session );
return original == null
? lobCreator.createClob( "" )
: lobCreator.createClob( original.getCharacterStream(), original.length() );
}
catch (SQLException e) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge CLOB data" );
}
}
@Override
public NClob mergeNClob(NClob original, NClob target, SessionImplementor session) {
if ( original == null && target == null ) {
return null;
}
try {
LobCreator lobCreator = session.getFactory().getJdbcServices().getLobCreator( session );
return original == null
? lobCreator.createNClob( "" )
: lobCreator.createNClob( original.getCharacterStream(), original.length() );
}
catch (SQLException e) {
throw session.getFactory().getSQLExceptionHelper().convert( e, "unable to merge NCLOB data" );
}
}
};
public LobMergeStrategy getLobMergeStrategy() {
return NEW_LOCATOR_LOB_MERGE_STRATEGY;
}
// hibernate type mapping support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/**
@ -1907,6 +2033,7 @@ public abstract class Dialect {
* @since 3.2
*/
public boolean supportsLobValueChangePropogation() {
// todo : pretty sure this is the same as the java.sql.DatabaseMetaData.locatorsUpdateCopy method added in JDBC 4, see HHH-6046
return true;
}

View File

@ -0,0 +1,70 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.dialect;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.NClob;
import org.hibernate.engine.SessionImplementor;
/**
* Strategy for how dialects need {@code LOB} values to be merged.
*
* @author Steve Ebersole
*/
public interface LobMergeStrategy {
/**
* Perform merge on {@link Blob} values.
*
* @param original The detached {@code BLOB} state
* @param target The managed {@code BLOB} state
* @param session The session
*
* @return The merged {@code BLOB} state
*/
public Blob mergeBlob(Blob original, Blob target, SessionImplementor session);
/**
* Perform merge on {@link Clob} values.
*
* @param original The detached {@code CLOB} state
* @param target The managed {@code CLOB} state
* @param session The session
*
* @return The merged {@code CLOB} state
*/
public Clob mergeClob(Clob original, Clob target, SessionImplementor session);
/**
* Perform merge on {@link NClob} values.
*
* @param original The detached {@code NCLOB} state
* @param target The managed {@code NCLOB} state
* @param session The session
*
* @return The merged {@code NCLOB} state
*/
public NClob mergeNClob(NClob original, NClob target, SessionImplementor session);
}

View File

@ -38,6 +38,7 @@ import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.collection.PersistentCollection;
import org.hibernate.engine.jdbc.LobCreationContext;
import org.hibernate.engine.query.sql.NativeSQLQuerySpecification;
import org.hibernate.engine.transaction.spi.TransactionCoordinator;
import org.hibernate.event.EventListeners;
@ -55,7 +56,7 @@ import org.hibernate.type.Type;
* @see org.hibernate.impl.SessionImpl the actual implementation
* @author Gavin King
*/
public interface SessionImplementor extends Serializable {
public interface SessionImplementor extends Serializable, LobCreationContext {
/**
* Retrieves the interceptor currently in use by this event source.

View File

@ -22,6 +22,7 @@
* Boston, MA 02110-1301 USA
*/
package org.hibernate.engine.jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
@ -31,6 +32,7 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Clob;
import java.sql.SQLException;
import org.hibernate.type.descriptor.java.DataHelper;
/**
@ -173,7 +175,7 @@ public class ClobProxy implements InvocationHandler {
}
}
catch ( IOException ioe ) {
throw new SQLException( "could not reset reader" );
throw new SQLException( "could not reset reader", ioe );
}
needsReset = true;
}

View File

@ -25,6 +25,8 @@
package org.hibernate.impl;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
@ -37,11 +39,14 @@ import org.hibernate.engine.NamedSQLQueryDefinition;
import org.hibernate.engine.QueryParameters;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.engine.jdbc.LobCreationContext;
import org.hibernate.engine.query.HQLQueryPlan;
import org.hibernate.engine.query.NativeSQLQueryPlan;
import org.hibernate.engine.query.sql.NativeSQLQuerySpecification;
import org.hibernate.engine.transaction.spi.TransactionContext;
import org.hibernate.engine.transaction.spi.TransactionEnvironment;
import org.hibernate.jdbc.WorkExecutor;
import org.hibernate.jdbc.WorkExecutorVisitable;
/**
* Functionality common to stateless and stateful sessions
@ -66,6 +71,26 @@ public abstract class AbstractSessionImpl implements Serializable, SessionImplem
return factory.getTransactionEnvironment();
}
@Override
public <T> T execute(final LobCreationContext.Callback<T> callback) {
return getTransactionCoordinator().getJdbcCoordinator().coordinateWork(
new WorkExecutorVisitable<T>() {
@Override
public T accept(WorkExecutor<T> workExecutor, Connection connection) throws SQLException {
try {
return callback.executeOnConnection( connection );
}
catch ( SQLException e ) {
throw getFactory().getSQLExceptionHelper().convert(
e,
"Error creating contextual LOB : " + e.getMessage()
);
}
}
}
);
}
public boolean isClosed() {
return closed;
}

View File

@ -2051,25 +2051,6 @@ public final class SessionImpl
oos.writeObject( childSessionsByEntityMode );
}
/**
* {@inheritDoc}
*/
public Object execute(LobCreationContext.Callback callback) {
Connection connection = transactionCoordinator.getJdbcCoordinator().getLogicalConnection().getConnection();
try {
return callback.executeOnConnection( connection );
}
catch ( SQLException e ) {
throw getFactory().getSQLExceptionHelper().convert(
e,
"Error creating contextual LOB : " + e.getMessage()
);
}
finally {
transactionCoordinator.getJdbcCoordinator().getLogicalConnection().afterStatementExecution();
}
}
/**
* {@inheritDoc}
*/

View File

@ -258,6 +258,7 @@ public final class ArrayHelper {
public static final Class[] EMPTY_CLASS_ARRAY = {};
public static final Object[] EMPTY_OBJECT_ARRAY = {};
public static final Type[] EMPTY_TYPE_ARRAY = {};
public static final byte[] EMPTY_BYTE_ARRAY = {};
public static int[] getBatchSizes(int maxBatchSize) {
int batchSize = maxBatchSize;

View File

@ -0,0 +1,65 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.internal.util.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.hibernate.HibernateException;
/**
* Utilities for copying I/O streams.
*
* @author Steve Ebersole
*/
public class StreamCopier {
public static final int BUFFER_SIZE = 1024 * 4;
public static final byte[] BUFFER = new byte[ BUFFER_SIZE ];
public static long copy(InputStream from, OutputStream into) {
try {
long totalRead = 0;
while ( true ) {
synchronized ( BUFFER ) {
int amountRead = from.read( BUFFER );
if ( amountRead == -1 ) {
break;
}
into.write( BUFFER, 0, amountRead );
totalRead += amountRead;
if ( amountRead < BUFFER_SIZE ) {
// should mean there is no more data in the stream, no need for next read
break;
}
}
}
return totalRead;
}
catch (IOException e ) {
throw new HibernateException( "Unable to copy stream content", e );
}
}
}

View File

@ -87,7 +87,7 @@ public abstract class AbstractStandardBasicType<T>
return javaTypeDescriptor.getMutabilityPlan();
}
protected T getReplacement(T original, T target) {
protected T getReplacement(T original, T target, SessionImplementor session) {
if ( !isMutable() ) {
return original;
}
@ -375,7 +375,7 @@ public abstract class AbstractStandardBasicType<T>
@SuppressWarnings({ "unchecked" })
public final Object replace(Object original, Object target, SessionImplementor session, Object owner, Map copyCache) {
return getReplacement( (T) original, (T) target );
return getReplacement( (T) original, (T) target, session );
}
@SuppressWarnings({ "unchecked" })
@ -387,7 +387,7 @@ public abstract class AbstractStandardBasicType<T>
Map copyCache,
ForeignKeyDirection foreignKeyDirection) {
return ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT == foreignKeyDirection
? getReplacement( (T) original, (T) target )
? getReplacement( (T) original, (T) target, session )
: target;
}
}

View File

@ -23,6 +23,8 @@
*/
package org.hibernate.type;
import java.sql.Blob;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.type.descriptor.java.BlobTypeDescriptor;
/**
@ -51,8 +53,8 @@ public class BlobType extends AbstractSingleColumnStandardBasicType<Blob> {
}
@Override
protected Blob getReplacement(Blob original, Blob target) {
return target;
protected Blob getReplacement(Blob original, Blob target, SessionImplementor session) {
return session.getFactory().getDialect().getLobMergeStrategy().mergeBlob( original, target, session );
}
}

View File

@ -23,6 +23,8 @@
*/
package org.hibernate.type;
import java.sql.Clob;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.type.descriptor.java.ClobTypeDescriptor;
/**
@ -48,8 +50,8 @@ public class ClobType extends AbstractSingleColumnStandardBasicType<Clob> {
}
@Override
protected Clob getReplacement(Clob original, Clob target) {
return target;
protected Clob getReplacement(Clob original, Clob target, SessionImplementor session) {
return session.getFactory().getDialect().getLobMergeStrategy().mergeClob( original, target, session );
}
}

View File

@ -56,8 +56,8 @@ public class BlobLocatorTest extends BaseCoreFunctionalTestCase {
@Test
public void testBoundedBlobLocatorAccess() throws Throwable {
byte[] original = buildRecursively( BLOB_SIZE, true );
byte[] changed = buildRecursively( BLOB_SIZE, false );
byte[] original = buildByteArray( BLOB_SIZE, true );
byte[] changed = buildByteArray( BLOB_SIZE, false );
byte[] empty = new byte[] {};
Session s = openSession();
@ -142,7 +142,7 @@ public class BlobLocatorTest extends BaseCoreFunctionalTestCase {
// unsupported; most databases would not allow such a construct anyway.
// Thus here we are only testing materialization...
byte[] original = buildRecursively( BLOB_SIZE, true );
byte[] original = buildByteArray( BLOB_SIZE, true );
Session s = openSession();
s.beginTransaction();
@ -170,12 +170,12 @@ public class BlobLocatorTest extends BaseCoreFunctionalTestCase {
s.close();
}
private byte[] extractData(Blob blob) throws Throwable {
public static byte[] extractData(Blob blob) throws Exception {
return blob.getBytes( 1, ( int ) blob.length() );
}
private byte[] buildRecursively(long size, boolean on) {
public static byte[] buildByteArray(long size, boolean on) {
byte[] data = new byte[(int)size];
data[0] = mask( on );
for ( int i = 0; i < size; i++ ) {
@ -185,11 +185,11 @@ public class BlobLocatorTest extends BaseCoreFunctionalTestCase {
return data;
}
private byte mask(boolean on) {
private static byte mask(boolean on) {
return on ? ( byte ) 1 : ( byte ) 0;
}
private static void assertEquals(byte[] val1, byte[] val2) {
public static void assertEquals(byte[] val1, byte[] val2) {
if ( !ArrayHelper.isEquals( val1, val2 ) ) {
throw new AssertionFailedError( "byte arrays did not match" );
}

View File

@ -57,8 +57,8 @@ public class ClobLocatorTest extends BaseCoreFunctionalTestCase {
@Test
public void testBoundedClobLocatorAccess() throws Throwable {
String original = buildRecursively( CLOB_SIZE, 'x' );
String changed = buildRecursively( CLOB_SIZE, 'y' );
String original = buildString( CLOB_SIZE, 'x' );
String changed = buildString( CLOB_SIZE, 'y' );
String empty = "";
Session s = openSession();
@ -143,7 +143,7 @@ public class ClobLocatorTest extends BaseCoreFunctionalTestCase {
// unsupported; most databases would not allow such a construct anyway.
// Thus here we are only testing materialization...
String original = buildRecursively( CLOB_SIZE, 'x' );
String original = buildString( CLOB_SIZE, 'x' );
Session s = openSession();
s.beginTransaction();
@ -171,12 +171,11 @@ public class ClobLocatorTest extends BaseCoreFunctionalTestCase {
s.close();
}
private String extractData(Clob clob) throws Throwable {
public static String extractData(Clob clob) throws Exception {
return DataHelper.extractString( clob.getCharacterStream() );
}
private String buildRecursively(int size, char baseChar) {
public static String buildString(int size, char baseChar) {
StringBuffer buff = new StringBuffer();
for( int i = 0; i < size; i++ ) {
buff.append( baseChar );

View File

@ -0,0 +1,122 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.lob;
import org.hibernate.Session;
import org.hibernate.internal.util.collections.ArrayHelper;
import org.junit.Test;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Steve Ebersole
*/
@TestForIssue( jiraKey = "HHH-2680" )
@RequiresDialectFeature( DialectChecks.SupportsExpectedLobUsagePattern.class )
public class LobMergeTest extends BaseCoreFunctionalTestCase {
private static final int LOB_SIZE = 10000;
public String[] getMappings() {
return new String[] { "lob/LobMappings.hbm.xml" };
}
@Test
public void testMergingBlobData() throws Exception {
final byte[] original = BlobLocatorTest.buildByteArray( LOB_SIZE, true );
final byte[] updated = BlobLocatorTest.buildByteArray( LOB_SIZE, false );
Session s = openSession();
s.beginTransaction();
LobHolder entity = new LobHolder();
entity.setBlobLocator( s.getLobHelper().createBlob( original ) );
s.save( entity );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
// entity still detached...
entity.setBlobLocator( s.getLobHelper().createBlob( updated ) );
entity = (LobHolder) s.merge( entity );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
entity = (LobHolder) s.get( LobHolder.class, entity.getId() );
assertEquals( "blob sizes did not match after merge", LOB_SIZE, entity.getBlobLocator().length() );
assertTrue(
"blob contents did not match after merge",
ArrayHelper.isEquals( updated, BlobLocatorTest.extractData( entity.getBlobLocator() ) )
);
s.delete( entity );
s.getTransaction().commit();
s.close();
}
@Test
public void testMergingClobData() throws Exception {
final String original = ClobLocatorTest.buildString( LOB_SIZE, 'a' );
final String updated = ClobLocatorTest.buildString( LOB_SIZE, 'z' );
Session s = openSession();
s.beginTransaction();
LobHolder entity = new LobHolder();
entity.setClobLocator( s.getLobHelper().createClob( original ) );
s.save( entity );
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
// entity still detached...
entity.setClobLocator( s.getLobHelper().createClob( updated ) );
entity = (LobHolder) s.merge( entity );
s.flush();
s.getTransaction().commit();
s.close();
s = openSession();
s.beginTransaction();
entity = (LobHolder) s.get( LobHolder.class, entity.getId() );
assertEquals( "clob sizes did not match after merge", LOB_SIZE, entity.getClobLocator().length() );
assertEquals(
"clob contents did not match after merge",
updated,
ClobLocatorTest.extractData( entity.getClobLocator() )
);
s.delete( entity );
s.getTransaction().commit();
s.close();
}
}