HHH-8395 - JPA StoredProcedureQuery#getUpdateCount should prefer return -1 rather than throw exceptions

This commit is contained in:
Steve Ebersole 2013-07-31 08:34:52 -05:00
parent 33b7e9f441
commit cd1c80b82d
11 changed files with 298 additions and 189 deletions

View File

@ -613,23 +613,7 @@ public class CustomLoader extends Loader {
rowProcessor.columnProcessors[i].performDiscovery( metadata, types, aliases ); rowProcessor.columnProcessors[i].performDiscovery( metadata, types, aliases );
} }
// lets make sure we did not end up with duplicate aliases. this can occur when the user supplied query validateAliases( aliases );
// did not rename same-named columns. e.g.:
// select u.username, u2.username from t_user u, t_user u2 ...
//
// the above will lead to an unworkable situation in most cases (the difference is how the driver/db
// handle this situation. But if the 'aliases' variable contains duplicate names, then we have that
// troublesome condition, so lets throw an error. See HHH-5992
final HashSet<String> aliasesSet = new HashSet<String>();
for ( String alias : aliases ) {
boolean alreadyExisted = !aliasesSet.add( alias );
if ( alreadyExisted ) {
throw new NonUniqueDiscoveredSqlAliasException(
"Encountered a duplicated sql alias [" + alias +
"] during auto-discovery of a native-sql query"
);
}
}
resultTypes = ArrayHelper.toTypeArray( types ); resultTypes = ArrayHelper.toTypeArray( types );
transformerAliases = ArrayHelper.toStringArray( aliases ); transformerAliases = ArrayHelper.toStringArray( aliases );
@ -639,6 +623,30 @@ public class CustomLoader extends Loader {
} }
} }
private void validateAliases(List<String> aliases) {
// lets make sure we did not end up with duplicate aliases. this can occur when the user supplied query
// did not rename same-named columns. e.g.:
// select u.username, u2.username from t_user u, t_user u2 ...
//
// the above will lead to an unworkable situation in most cases (the difference is how the driver/db
// handle this situation. But if the 'aliases' variable contains duplicate names, then we have that
// troublesome condition, so lets throw an error. See HHH-5992
final HashSet<String> aliasesSet = new HashSet<String>();
for ( String alias : aliases ) {
validateAlias( alias );
boolean alreadyExisted = !aliasesSet.add( alias );
if ( alreadyExisted ) {
throw new NonUniqueDiscoveredSqlAliasException(
"Encountered a duplicated sql alias [" + alias +
"] during auto-discovery of a native-sql query"
);
}
}
}
protected void validateAlias(String alias) {
}
private static class Metadata { private static class Metadata {
private final SessionFactoryImplementor factory; private final SessionFactoryImplementor factory;
private final ResultSet resultSet; private final ResultSet resultSet;

View File

@ -134,11 +134,12 @@ public interface ProcedureCall extends BasicQueryContract, SynchronizeableQuery
/** /**
* Retrieves access to outputs of this procedure call. Can be called multiple times, returning the same * Retrieves access to outputs of this procedure call. Can be called multiple times, returning the same
* Output instance each time. * ProcedureResult instance each time.
* <p/> * <p/>
* Note that the procedure will not actually be executed until the outputs are actually accessed. * If the procedure call has not actually be executed yet, it will be executed and then the ProcedureResult
* will be returned.
* *
* @return The outputs representation * @return The ProcedureResult representation
*/ */
public ProcedureResult getResult(); public ProcedureResult getResult();

View File

@ -29,7 +29,7 @@ import org.hibernate.Session;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
/** /**
* Represents a "memento" of a ProcedureCall * Represents a "memento" (disconnected, externalizable form) of a ProcedureCall
* *
* @author Steve Ebersole * @author Steve Ebersole
*/ */

View File

@ -385,6 +385,11 @@ public class ProcedureCallImpl extends AbstractBasicQueryContractImpl implements
// for now assume there are no resultClasses nor mappings defined.. // for now assume there are no resultClasses nor mappings defined..
// TOTAL PROOF-OF-CONCEPT!!!!!! // TOTAL PROOF-OF-CONCEPT!!!!!!
// todo : how to identify calls which should be in the form `{? = call procName...}` ??? (note leading param marker)
// more than likely this will need to be a method on the native API. I can see this as a trigger to
// both: (1) add the `? = ` part and also (2) register a REFCURSOR parameter for DBs (Oracle, PGSQL) that
// need it.
final StringBuilder buffer = new StringBuilder().append( "{call " ) final StringBuilder buffer = new StringBuilder().append( "{call " )
.append( procedureName ) .append( procedureName )
.append( "(" ); .append( "(" );

View File

@ -25,9 +25,7 @@ package org.hibernate.procedure.internal;
import java.sql.CallableStatement; import java.sql.CallableStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException;
import org.hibernate.JDBCException;
import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport; import org.hibernate.engine.jdbc.cursor.spi.RefCursorSupport;
import org.hibernate.procedure.ParameterRegistration; import org.hibernate.procedure.ParameterRegistration;
import org.hibernate.procedure.ProcedureResult; import org.hibernate.procedure.ProcedureResult;
@ -70,19 +68,25 @@ public class ProcedureResultImpl extends ResultImpl implements ProcedureResult {
} }
@Override @Override
protected CurrentReturnDescriptor buildCurrentReturnDescriptor(boolean isResultSet, int updateCount) { protected CurrentReturnState buildCurrentReturnDescriptor(boolean isResultSet, int updateCount) {
return new ProcedureCurrentReturnDescriptor( isResultSet, updateCount, refCursorParamIndex ); return new ProcedureCurrentReturnState( isResultSet, updateCount, refCursorParamIndex );
} }
protected boolean hasMoreReturns(CurrentReturnDescriptor descriptor) { protected boolean hasMoreReturns(CurrentReturnState descriptor) {
return super.hasMoreReturns( descriptor ) return super.hasMoreReturns( descriptor )
|| ( (ProcedureCurrentReturnDescriptor) descriptor ).refCursorParamIndex < refCursorParameters.length; || ( (ProcedureCurrentReturnState) descriptor ).refCursorParamIndex < refCursorParameters.length;
} }
@Override @Override
protected Return buildExtendedReturn(CurrentReturnDescriptor returnDescriptor) { protected boolean hasExtendedReturns(CurrentReturnState currentReturnState) {
return ProcedureCurrentReturnState.class.isInstance( currentReturnState )
&& ( (ProcedureCurrentReturnState) currentReturnState ).refCursorParamIndex < refCursorParameters.length;
}
@Override
protected Return buildExtendedReturn(CurrentReturnState returnDescriptor) {
this.refCursorParamIndex++; this.refCursorParamIndex++;
final int refCursorParamIndex = ( (ProcedureCurrentReturnDescriptor) returnDescriptor ).refCursorParamIndex; final int refCursorParamIndex = ( (ProcedureCurrentReturnState) returnDescriptor ).refCursorParamIndex;
final ParameterRegistrationImplementor refCursorParam = refCursorParameters[refCursorParamIndex]; final ParameterRegistrationImplementor refCursorParam = refCursorParameters[refCursorParamIndex];
ResultSet resultSet; ResultSet resultSet;
if ( refCursorParam.getName() != null ) { if ( refCursorParam.getName() != null ) {
@ -95,21 +99,13 @@ public class ProcedureResultImpl extends ResultImpl implements ProcedureResult {
.getService( RefCursorSupport.class ) .getService( RefCursorSupport.class )
.getResultSet( callableStatement, refCursorParam.getPosition() ); .getResultSet( callableStatement, refCursorParam.getPosition() );
} }
return new ResultSetReturn( this, resultSet ); return new ResultSetReturnImpl( extractResults( resultSet ) );
} }
protected JDBCException convert(SQLException e, String message) { protected class ProcedureCurrentReturnState extends CurrentReturnState {
return procedureCall.getSession().getFactory().getSQLExceptionHelper().convert(
e,
message,
procedureCall.getProcedureName()
);
}
protected static class ProcedureCurrentReturnDescriptor extends CurrentReturnDescriptor {
private final int refCursorParamIndex; private final int refCursorParamIndex;
private ProcedureCurrentReturnDescriptor(boolean isResultSet, int updateCount, int refCursorParamIndex) { private ProcedureCurrentReturnState(boolean isResultSet, int updateCount, int refCursorParamIndex) {
super( isResultSet, updateCount ); super( isResultSet, updateCount );
this.refCursorParamIndex = refCursorParamIndex; this.refCursorParamIndex = refCursorParamIndex;
} }

View File

@ -32,4 +32,8 @@ public class NoMoreReturnsException extends HibernateException {
public NoMoreReturnsException(String message) { public NoMoreReturnsException(String message) {
super( message ); super( message );
} }
public NoMoreReturnsException() {
super( "Results have been exhausted" );
}
} }

View File

@ -33,6 +33,13 @@ package org.hibernate.result;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public interface Result { public interface Result {
/**
* Retrieve the current return.
*
* @return The current return.
*/
public Return getCurrentReturn();
/** /**
* Are there any more returns associated with this result? * Are there any more returns associated with this result?
* *
@ -42,9 +49,12 @@ public interface Result {
public boolean hasMoreReturns(); public boolean hasMoreReturns();
/** /**
* Retrieve the next return. * Retrieve the next return
* *
* @return The next return. * @return The next return.
*
* @throws NoMoreReturnsException Thrown if there are no more returns associated with this Result, as would
* have been indicated by a {@code false} return from {@link #hasMoreReturns()}.
*/ */
public Return getNextReturn() throws NoMoreReturnsException; public Return getNextReturn() throws NoMoreReturnsException;
} }

View File

@ -40,7 +40,9 @@ import org.hibernate.loader.custom.sql.SQLQueryReturnProcessor;
import org.hibernate.loader.spi.AfterLoadAction; import org.hibernate.loader.spi.AfterLoadAction;
import org.hibernate.result.NoMoreReturnsException; import org.hibernate.result.NoMoreReturnsException;
import org.hibernate.result.Result; import org.hibernate.result.Result;
import org.hibernate.result.ResultSetReturn;
import org.hibernate.result.Return; import org.hibernate.result.Return;
import org.hibernate.result.UpdateCountReturn;
import org.hibernate.result.spi.ResultContext; import org.hibernate.result.spi.ResultContext;
/** /**
@ -51,102 +53,101 @@ public class ResultImpl implements Result {
private final PreparedStatement jdbcStatement; private final PreparedStatement jdbcStatement;
private final CustomLoaderExtension loader; private final CustomLoaderExtension loader;
private CurrentReturnDescriptor currentReturnDescriptor; private CurrentReturnState currentReturnState;
private boolean executed = false;
public ResultImpl(ResultContext context, PreparedStatement jdbcStatement) { public ResultImpl(ResultContext context, PreparedStatement jdbcStatement) {
this.context = context; this.context = context;
this.jdbcStatement = jdbcStatement; this.jdbcStatement = jdbcStatement;
// For now... // For now... but see the LoadPlan work; eventually this should just be a ResultSetProcessor.
this.loader = buildSpecializedCustomLoader( context ); this.loader = buildSpecializedCustomLoader( context );
try {
final boolean isResultSet = jdbcStatement.execute();
currentReturnState = buildCurrentReturnDescriptor( isResultSet );
}
catch (SQLException e) {
throw convert( e, "Error calling CallableStatement.getMoreResults" );
}
}
private CurrentReturnState buildCurrentReturnDescriptor(boolean isResultSet) {
int updateCount = -1;
if ( ! isResultSet ) {
try {
updateCount = jdbcStatement.getUpdateCount();
}
catch (SQLException e) {
throw convert( e, "Error calling CallableStatement.getUpdateCount" );
}
}
return buildCurrentReturnDescriptor( isResultSet, updateCount );
}
protected CurrentReturnState buildCurrentReturnDescriptor(boolean isResultSet, int updateCount) {
return new CurrentReturnState( isResultSet, updateCount );
}
@Override
public Return getCurrentReturn() {
if ( currentReturnState == null ) {
return null;
}
return currentReturnState.getReturn();
} }
@Override @Override
public boolean hasMoreReturns() { public boolean hasMoreReturns() {
if ( currentReturnDescriptor == null ) { // prepare the next return state
final boolean isResultSet; try {
final boolean isResultSet = jdbcStatement.getMoreResults();
if ( executed ) { currentReturnState = buildCurrentReturnDescriptor( isResultSet );
try { }
isResultSet = jdbcStatement.getMoreResults(); catch (SQLException e) {
} throw convert( e, "Error calling CallableStatement.getMoreResults" );
catch (SQLException e) {
throw convert( e, "Error calling CallableStatement.getMoreResults" );
}
}
else {
try {
isResultSet = jdbcStatement.execute();
}
catch (SQLException e) {
throw convert( e, "Error calling CallableStatement.execute" );
}
executed = true;
}
int updateCount = -1;
if ( ! isResultSet ) {
try {
updateCount = jdbcStatement.getUpdateCount();
}
catch (SQLException e) {
throw convert( e, "Error calling CallableStatement.getUpdateCount" );
}
}
currentReturnDescriptor = buildCurrentReturnDescriptor( isResultSet, updateCount );
} }
return hasMoreReturns( currentReturnDescriptor ); return hasMoreReturns( currentReturnState );
} }
protected CurrentReturnDescriptor buildCurrentReturnDescriptor(boolean isResultSet, int updateCount) { protected boolean hasMoreReturns(CurrentReturnState descriptor) {
return new CurrentReturnDescriptor( isResultSet, updateCount ); return descriptor != null && ( descriptor.isResultSet() || descriptor.getUpdateCount() >= 0 );
}
protected boolean hasMoreReturns(CurrentReturnDescriptor descriptor) {
return descriptor.isResultSet
|| descriptor.updateCount >= 0;
} }
@Override @Override
public Return getNextReturn() { public Return getNextReturn() {
if ( currentReturnDescriptor == null ) { if ( !hasMoreReturns() ) {
if ( executed ) {
throw new IllegalStateException( "Unexpected condition" );
}
else {
throw new IllegalStateException( "hasMoreReturns() not called before getNextReturn()" );
}
}
if ( ! hasMoreReturns( currentReturnDescriptor ) ) {
throw new NoMoreReturnsException( "Results have been exhausted" ); throw new NoMoreReturnsException( "Results have been exhausted" );
} }
CurrentReturnDescriptor copyReturnDescriptor = currentReturnDescriptor; return getCurrentReturn();
currentReturnDescriptor = null; }
if ( copyReturnDescriptor.isResultSet ) { protected boolean hasExtendedReturns(CurrentReturnState currentReturnState) {
try { return false;
return new ResultSetReturn( this, jdbcStatement.getResultSet() ); }
}
catch (SQLException e) { private List extractCurrentResults() {
throw convert( e, "Error calling CallableStatement.getResultSet" ); try {
} return extractResults( jdbcStatement.getResultSet() );
} }
else if ( copyReturnDescriptor.updateCount >= 0 ) { catch (SQLException e) {
return new UpdateCountReturn( this, copyReturnDescriptor.updateCount ); throw convert( e, "Error calling CallableStatement.getResultSet" );
}
else {
return buildExtendedReturn( copyReturnDescriptor );
} }
} }
protected Return buildExtendedReturn(CurrentReturnDescriptor copyReturnDescriptor) { protected List extractResults(ResultSet resultSet) {
throw new NoMoreReturnsException( "Results have been exhausted" ); try {
return loader.processResultSet( resultSet );
}
catch (SQLException e) {
throw convert( e, "Error extracting results from CallableStatement" );
}
}
protected Return buildExtendedReturn(CurrentReturnState copyReturnDescriptor) {
throw new NoMoreReturnsException();
} }
protected JDBCException convert(SQLException e, String message) { protected JDBCException convert(SQLException e, String message) {
@ -157,23 +158,55 @@ public class ResultImpl implements Result {
); );
} }
protected static class CurrentReturnDescriptor { /**
* Encapsulates the information needed to interpret the current return within a result
*/
protected class CurrentReturnState {
private final boolean isResultSet; private final boolean isResultSet;
private final int updateCount; private final int updateCount;
protected CurrentReturnDescriptor(boolean isResultSet, int updateCount) { private Return rtn;
protected CurrentReturnState(boolean isResultSet, int updateCount) {
this.isResultSet = isResultSet; this.isResultSet = isResultSet;
this.updateCount = updateCount; this.updateCount = updateCount;
} }
public boolean isResultSet() {
return isResultSet;
}
public int getUpdateCount() {
return updateCount;
}
public Return getReturn() {
if ( rtn == null ) {
rtn = buildReturn();
}
return rtn;
}
protected Return buildReturn() {
if ( isResultSet() ) {
return new ResultSetReturnImpl( extractCurrentResults() );
}
else if ( getUpdateCount() >= 0 ) {
return new UpdateCountReturnImpl( updateCount );
}
else if ( hasExtendedReturns( currentReturnState ) ) {
return buildExtendedReturn( currentReturnState );
}
throw new NoMoreReturnsException();
}
} }
protected static class ResultSetReturn implements org.hibernate.result.ResultSetReturn { protected static class ResultSetReturnImpl implements ResultSetReturn {
private final ResultImpl storedProcedureOutputs; private final List results;
private final ResultSet resultSet;
public ResultSetReturn(ResultImpl storedProcedureOutputs, ResultSet resultSet) { public ResultSetReturnImpl(List results) {
this.storedProcedureOutputs = storedProcedureOutputs; this.results = results;
this.resultSet = resultSet;
} }
@Override @Override
@ -184,17 +217,12 @@ public class ResultImpl implements Result {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List getResultList() { public List getResultList() {
try { return results;
return storedProcedureOutputs.loader.processResultSet( resultSet );
}
catch (SQLException e) {
throw storedProcedureOutputs.convert( e, "Error calling ResultSet.next" );
}
} }
@Override @Override
public Object getSingleResult() { public Object getSingleResult() {
List results = getResultList(); final List results = getResultList();
if ( results == null || results.isEmpty() ) { if ( results == null || results.isEmpty() ) {
return null; return null;
} }
@ -204,12 +232,10 @@ public class ResultImpl implements Result {
} }
} }
protected static class UpdateCountReturn implements org.hibernate.result.UpdateCountReturn { protected static class UpdateCountReturnImpl implements UpdateCountReturn {
private final ResultImpl result;
private final int updateCount; private final int updateCount;
public UpdateCountReturn(ResultImpl result, int updateCount) { public UpdateCountReturnImpl(int updateCount) {
this.result = result;
this.updateCount = updateCount; this.updateCount = updateCount;
} }
@ -266,6 +292,9 @@ public class ResultImpl implements Result {
private QueryParameters queryParameters; private QueryParameters queryParameters;
private SessionImplementor session; private SessionImplementor session;
// temp
private final CustomQuery customQuery;
public CustomLoaderExtension( public CustomLoaderExtension(
CustomQuery customQuery, CustomQuery customQuery,
QueryParameters queryParameters, QueryParameters queryParameters,
@ -273,6 +302,8 @@ public class ResultImpl implements Result {
super( customQuery, session.getFactory() ); super( customQuery, session.getFactory() );
this.queryParameters = queryParameters; this.queryParameters = queryParameters;
this.session = session; this.session = session;
this.customQuery = customQuery;
} }
// todo : this would be a great way to add locking to stored procedure support (at least where returning entities). // todo : this would be a great way to add locking to stored procedure support (at least where returning entities).
@ -289,5 +320,12 @@ public class ResultImpl implements Result {
Collections.<AfterLoadAction>emptyList() Collections.<AfterLoadAction>emptyList()
); );
} }
@Override
protected void validateAlias(String alias) {
System.out.println(
"TEMPORARY... discovered result set alias from stored procedure [" + alias + "] : " + customQuery.getSQL()
);
}
} }
} }

View File

@ -44,6 +44,7 @@ import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.junit4.ExtraAssertions; import org.hibernate.testing.junit4.ExtraAssertions;
import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -171,11 +172,9 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
ProcedureCall query = session.createStoredProcedureCall( "user"); ProcedureCall query = session.createStoredProcedureCall( "user");
ProcedureResult procedureResult = query.getResult(); ProcedureResult procedureResult = query.getResult();
assertTrue( "Checking ProcedureResult has more returns", procedureResult.hasMoreReturns() ); Return currentReturn = procedureResult.getCurrentReturn();
Return nextReturn = procedureResult.getNextReturn(); assertNotNull( currentReturn );
assertNotNull( nextReturn ); ResultSetReturn resultSetReturn = assertTyping( ResultSetReturn.class, currentReturn );
ExtraAssertions.assertClassAssignability( ResultSetReturn.class, nextReturn.getClass() );
ResultSetReturn resultSetReturn = (ResultSetReturn) nextReturn;
String name = (String) resultSetReturn.getSingleResult(); String name = (String) resultSetReturn.getSingleResult();
assertEquals( "SA", name ); assertEquals( "SA", name );
@ -190,13 +189,11 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
ProcedureCall query = session.createStoredProcedureCall( "findOneUser" ); ProcedureCall query = session.createStoredProcedureCall( "findOneUser" );
ProcedureResult procedureResult = query.getResult(); ProcedureResult procedureResult = query.getResult();
assertTrue( "Checking ProcedureResult has more returns", procedureResult.hasMoreReturns() ); Return currentReturn = procedureResult.getCurrentReturn();
Return nextReturn = procedureResult.getNextReturn(); assertNotNull( currentReturn );
assertNotNull( nextReturn ); ResultSetReturn resultSetReturn = assertTyping( ResultSetReturn.class, currentReturn );
ExtraAssertions.assertClassAssignability( ResultSetReturn.class, nextReturn.getClass() );
ResultSetReturn resultSetReturn = (ResultSetReturn) nextReturn;
Object result = resultSetReturn.getSingleResult(); Object result = resultSetReturn.getSingleResult();
ExtraAssertions.assertTyping( Object[].class, result ); assertTyping( Object[].class, result );
String name = (String) ( (Object[]) result )[1]; String name = (String) ( (Object[]) result )[1];
assertEquals( "Steve", name ); assertEquals( "Steve", name );
@ -211,16 +208,14 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
ProcedureCall query = session.createStoredProcedureCall( "findUsers" ); ProcedureCall query = session.createStoredProcedureCall( "findUsers" );
ProcedureResult procedureResult = query.getResult(); ProcedureResult procedureResult = query.getResult();
assertTrue( "Checking ProcedureResult has more returns", procedureResult.hasMoreReturns() ); Return currentReturn = procedureResult.getCurrentReturn();
Return nextReturn = procedureResult.getNextReturn(); assertNotNull( currentReturn );
assertNotNull( nextReturn ); ResultSetReturn resultSetReturn = assertTyping( ResultSetReturn.class, currentReturn );
ExtraAssertions.assertClassAssignability( ResultSetReturn.class, nextReturn.getClass() );
ResultSetReturn resultSetReturn = (ResultSetReturn) nextReturn;
List results = resultSetReturn.getResultList(); List results = resultSetReturn.getResultList();
assertEquals( 3, results.size() ); assertEquals( 3, results.size() );
for ( Object result : results ) { for ( Object result : results ) {
ExtraAssertions.assertTyping( Object[].class, result ); assertTyping( Object[].class, result );
Integer id = (Integer) ( (Object[]) result )[0]; Integer id = (Integer) ( (Object[]) result )[0];
String name = (String) ( (Object[]) result )[1]; String name = (String) ( (Object[]) result )[1];
if ( id.equals( 1 ) ) { if ( id.equals( 1 ) ) {
@ -250,15 +245,13 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
query.registerParameter( "start", Integer.class, ParameterMode.IN ).bindValue( 1 ); query.registerParameter( "start", Integer.class, ParameterMode.IN ).bindValue( 1 );
query.registerParameter( "end", Integer.class, ParameterMode.IN ).bindValue( 2 ); query.registerParameter( "end", Integer.class, ParameterMode.IN ).bindValue( 2 );
ProcedureResult procedureResult = query.getResult(); ProcedureResult procedureResult = query.getResult();
assertTrue( "Checking ProcedureResult has more returns", procedureResult.hasMoreReturns() ); Return currentReturn = procedureResult.getCurrentReturn();
Return nextReturn = procedureResult.getNextReturn(); assertNotNull( currentReturn );
assertNotNull( nextReturn ); ResultSetReturn resultSetReturn = assertTyping( ResultSetReturn.class, currentReturn );
ExtraAssertions.assertClassAssignability( ResultSetReturn.class, nextReturn.getClass() );
ResultSetReturn resultSetReturn = (ResultSetReturn) nextReturn;
List results = resultSetReturn.getResultList(); List results = resultSetReturn.getResultList();
assertEquals( 1, results.size() ); assertEquals( 1, results.size() );
Object result = results.get( 0 ); Object result = results.get( 0 );
ExtraAssertions.assertTyping( Object[].class, result ); assertTyping( Object[].class, result );
Integer id = (Integer) ( (Object[]) result )[0]; Integer id = (Integer) ( (Object[]) result )[0];
String name = (String) ( (Object[]) result )[1]; String name = (String) ( (Object[]) result )[1];
assertEquals( 1, (int) id ); assertEquals( 1, (int) id );
@ -277,15 +270,13 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
query.registerParameter( 1, Integer.class, ParameterMode.IN ).bindValue( 1 ); query.registerParameter( 1, Integer.class, ParameterMode.IN ).bindValue( 1 );
query.registerParameter( 2, Integer.class, ParameterMode.IN ).bindValue( 2 ); query.registerParameter( 2, Integer.class, ParameterMode.IN ).bindValue( 2 );
ProcedureResult procedureResult = query.getResult(); ProcedureResult procedureResult = query.getResult();
assertTrue( "Checking ProcedureResult has more returns", procedureResult.hasMoreReturns() ); Return currentReturn = procedureResult.getCurrentReturn();
Return nextReturn = procedureResult.getNextReturn(); assertNotNull( currentReturn );
assertNotNull( nextReturn ); ResultSetReturn resultSetReturn = assertTyping( ResultSetReturn.class, currentReturn );
ExtraAssertions.assertClassAssignability( ResultSetReturn.class, nextReturn.getClass() );
ResultSetReturn resultSetReturn = (ResultSetReturn) nextReturn;
List results = resultSetReturn.getResultList(); List results = resultSetReturn.getResultList();
assertEquals( 1, results.size() ); assertEquals( 1, results.size() );
Object result = results.get( 0 ); Object result = results.get( 0 );
ExtraAssertions.assertTyping( Object[].class, result ); assertTyping( Object[].class, result );
Integer id = (Integer) ( (Object[]) result )[0]; Integer id = (Integer) ( (Object[]) result )[0];
String name = (String) ( (Object[]) result )[1]; String name = (String) ( (Object[]) result )[1];
assertEquals( 1, (int) id ); assertEquals( 1, (int) id );
@ -307,9 +298,8 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
ProcedureCall query = session.createStoredProcedureCall( "findUserRange" ); ProcedureCall query = session.createStoredProcedureCall( "findUserRange" );
query.registerParameter( 1, Integer.class, ParameterMode.IN ); query.registerParameter( 1, Integer.class, ParameterMode.IN );
query.registerParameter( 2, Integer.class, ParameterMode.IN ).bindValue( 2 ); query.registerParameter( 2, Integer.class, ParameterMode.IN ).bindValue( 2 );
ProcedureResult procedureResult = query.getResult();
try { try {
procedureResult.hasMoreReturns(); query.getResult();
fail( "Expecting failure due to missing parameter bind" ); fail( "Expecting failure due to missing parameter bind" );
} }
catch (JDBCException expected) { catch (JDBCException expected) {
@ -320,9 +310,8 @@ public class StoredProcedureTest extends BaseCoreFunctionalTestCase {
ProcedureCall query = session.createStoredProcedureCall( "findUserRange" ); ProcedureCall query = session.createStoredProcedureCall( "findUserRange" );
query.registerParameter( "start", Integer.class, ParameterMode.IN ); query.registerParameter( "start", Integer.class, ParameterMode.IN );
query.registerParameter( "end", Integer.class, ParameterMode.IN ).bindValue( 2 ); query.registerParameter( "end", Integer.class, ParameterMode.IN ).bindValue( 2 );
ProcedureResult procedureResult = query.getResult();
try { try {
procedureResult.hasMoreReturns(); query.getResult();
fail( "Expecting failure due to missing parameter bind" ); fail( "Expecting failure due to missing parameter bind" );
} }
catch (JDBCException expected) { catch (JDBCException expected) {

View File

@ -25,8 +25,11 @@ package org.hibernate.jpa.internal;
import javax.persistence.FlushModeType; import javax.persistence.FlushModeType;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Parameter; import javax.persistence.Parameter;
import javax.persistence.ParameterMode; import javax.persistence.ParameterMode;
import javax.persistence.PersistenceException;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.StoredProcedureQuery; import javax.persistence.StoredProcedureQuery;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
@ -197,7 +200,8 @@ public class StoredProcedureQueryImpl extends BaseQueryImpl implements StoredPro
@Override @Override
public boolean execute() { public boolean execute() {
return outputs().hasMoreReturns(); final Return rtn = outputs().getCurrentReturn();
return rtn != null && ResultSetReturn.class.isInstance( rtn );
} }
@Override @Override
@ -212,34 +216,76 @@ public class StoredProcedureQueryImpl extends BaseQueryImpl implements StoredPro
@Override @Override
public int getUpdateCount() { public int getUpdateCount() {
final Return nextReturn = outputs().getNextReturn(); final Return rtn = outputs().getCurrentReturn();
if ( nextReturn.isResultSet() ) { if ( rtn == null ) {
return -1;
}
else if ( UpdateCountReturn.class.isInstance( rtn ) ) {
return ( (UpdateCountReturn) rtn ).getUpdateCount();
}
else {
return -1; return -1;
} }
return ( (UpdateCountReturn) nextReturn ).getUpdateCount();
} }
@Override @Override
public List getResultList() { public List getResultList() {
final Return nextReturn = outputs().getNextReturn(); final Return rtn = outputs().getCurrentReturn();
if ( ! nextReturn.isResultSet() ) { if ( ! ResultSetReturn.class.isInstance( rtn ) ) {
return null; // todo : what should be thrown/returned here? throw new IllegalStateException( "Current CallableStatement was not a ResultSet, but getResultList was called" );
} }
return ( (ResultSetReturn) nextReturn ).getResultList();
if ( outputs().hasMoreReturns() ) {
outputs().getNextReturn();
}
return ( (ResultSetReturn) rtn ).getResultList();
} }
@Override @Override
public Object getSingleResult() { public Object getSingleResult() {
final Return nextReturn = outputs().getNextReturn(); final List resultList = getResultList();
if ( ! nextReturn.isResultSet() ) { if ( resultList == null || resultList.isEmpty() ) {
return null; // todo : what should be thrown/returned here? throw new NoResultException(
String.format(
"Call to stored procedure [%s] returned no results",
procedureCall.getProcedureName()
)
);
} }
return ( (ResultSetReturn) nextReturn ).getSingleResult(); else if ( resultList.size() > 1 ) {
throw new NonUniqueResultException(
String.format(
"Call to stored procedure [%s] returned multiple results",
procedureCall.getProcedureName()
)
);
}
return resultList.get( 0 );
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> cls) { public <T> T unwrap(Class<T> cls) {
return null; if ( ProcedureCall.class.isAssignableFrom( cls ) ) {
return (T) procedureCall;
}
else if ( ProcedureResult.class.isAssignableFrom( cls ) ) {
return (T) outputs();
}
else if ( BaseQueryImpl.class.isAssignableFrom( cls ) ) {
return (T) this;
}
throw new PersistenceException(
String.format(
"Unsure how to unwrap %s impl [%s] as requested type [%s]",
StoredProcedureQuery.class.getSimpleName(),
this.getClass().getName(),
cls.getName()
)
);
} }

View File

@ -44,31 +44,43 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
* Tests various JPA usage scenarios for performing stored procedures. * Tests various JPA usage scenarios for performing stored procedures. Inspired by the awesomely well-done JPA TCK
* *
* @author Steve Ebersole * @author Steve Ebersole
*/ */
@RequiresDialect( H2Dialect.class ) @RequiresDialect( H2Dialect.class )
@FailureExpected( jiraKey = "HHH-8389", message = "Waiting clarification from EG" )
public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase { public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase {
/**
* Some tests inspired by the awesomely well-done JPA TCK
*/
@Test @Test
public void testJpaUsage1() { public void testMultipleGetUpdateCountCalls() {
EntityManager em = getOrCreateEntityManager(); EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin(); em.getTransaction().begin();
StoredProcedureQuery query = em.createStoredProcedureQuery( "findOneUser", User.class ); StoredProcedureQuery query = em.createStoredProcedureQuery( "findOneUser" );
// this is what the TCK attempts to do, don't shoot the messenger... // this is what the TCK attempts to do, don't shoot the messenger...
query.getUpdateCount(); query.getUpdateCount();
// yep, twice // yep, twice
int updateCount = query.getUpdateCount(); int updateCount = query.getUpdateCount();
em.getTransaction().commit();
em.close();
}
@Test
public void testBasicScalarResults() {
EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin();
StoredProcedureQuery query = em.createStoredProcedureQuery( "findOneUser" );
boolean isResult = query.execute();
assertTrue( isResult );
int updateCount = query.getUpdateCount();
boolean results = false; boolean results = false;
do { do {
List list = query.getResultList(); List list = query.getResultList();
assertEquals( 1, list.size() );
results = query.hasMoreResults(); results = query.hasMoreResults();
// and it only sets the updateCount once lol // and it only sets the updateCount once lol
} while ( results || updateCount != -1); } while ( results || updateCount != -1);
@ -78,15 +90,15 @@ public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase {
} }
@Test @Test
public void testJpaUsage2() { @FailureExpected( jiraKey = "HHH-8398" )
public void testResultClassHandling() {
EntityManager em = getOrCreateEntityManager(); EntityManager em = getOrCreateEntityManager();
em.getTransaction().begin(); em.getTransaction().begin();
StoredProcedureQuery query = em.createStoredProcedureQuery( "findOneUser", User.class ); StoredProcedureQuery query = em.createStoredProcedureQuery( "findOneUser", User.class );
boolean isResult = query.execute(); boolean isResult = query.execute();
assertTrue( isResult ); assertTrue( isResult );
// int updateCount = query.getUpdateCount(); int updateCount = query.getUpdateCount();
int updateCount = -1;
boolean results = false; boolean results = false;
do { do {
@ -122,7 +134,7 @@ public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase {
" return rs;\n" + " return rs;\n" +
"}\n" + "}\n" +
"$$"; "$$";
public static final String DROP_CMD = "DROP ALIAS findUser IF EXISTS"; public static final String DROP_CMD = "DROP ALIAS findOneUser IF EXISTS";
@Override @Override
protected void afterEntityManagerFactoryBuilt() { protected void afterEntityManagerFactoryBuilt() {
@ -130,6 +142,7 @@ public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase {
} }
private void execute(String sql) { private void execute(String sql) {
System.out.println( "Executing SQL : " + sql );
final SessionFactoryImplementor sf = entityManagerFactory().unwrap( SessionFactoryImplementor.class ); final SessionFactoryImplementor sf = entityManagerFactory().unwrap( SessionFactoryImplementor.class );
final Connection conn; final Connection conn;
try { try {
@ -153,14 +166,13 @@ public class JpaUsageTest extends BaseEntityManagerFunctionalTestCase {
} }
} }
catch (SQLException e) { catch (SQLException e) {
throw new RuntimeException( "Unable to execute SQL : " + sql ); throw new RuntimeException( "Unable to execute SQL : " + sql, e );
} }
} }
@Override @Override
public void releaseResources() { public void releaseResources() {
execute( DROP_CMD ); execute( DROP_CMD );
super.releaseResources(); super.releaseResources();
} }
} }