OPENJPA-1550:

Set failedObject on RollbackException.
Submitted By: Heath Thomann

git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@927267 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Michael Dick 2010-03-25 03:55:43 +00:00
parent 162b11b542
commit 666ec6c6de
9 changed files with 487 additions and 37 deletions

View File

@ -192,12 +192,11 @@ public class BatchingPreparedStatementManagerImpl extends
//similar to this path, or I should say, the path which is taken instead of this path when
//we aren't using batching), we see that the catch block doesn't do a 'se.getNextException'.
//When we do a 'getNextException', the 'next exception' doesn't contain the same message as se.
//That is, 'next exception' contains a subset msg which is contained in se. For legacy, should
//we continute to use 'sqex' in the 'old path' and use 'se' in the next path/code?????
//SQLException sqex = se.getNextException();
//if (sqex == null)
// sqex = se;
SQLException sqex = se;
//That is, 'next exception' contains a subset msg which is contained in se.
SQLException sqex = se.getNextException();
if (sqex == null){
sqex = se;
}
if (se instanceof ReportingSQLException){
int index = ((ReportingSQLException) se).getIndexOfFirstFailedObject();
@ -209,24 +208,23 @@ public class BatchingPreparedStatementManagerImpl extends
index = 0;
}
//index should not be less than 0 this path, but if for some reason it is, lets
//index should not be less than 0 in this path, but if for some reason it is, lets
//resort to the 'old way' and simply pass the 'ps' as the failed object.
if (index < 0){
throw SQLExceptions.getStore(sqex, ps, _dict);
throw SQLExceptions.getStore(se, ps, _dict);
}
else{
throw SQLExceptions.getStore(sqex, ((RowImpl)(_batchedRows.get(index))).getFailedObject(), _dict);
throw SQLExceptions.getStore(se, ((RowImpl)(_batchedRows.get(index))).getFailedObject(), _dict);
}
}
else{
//per comments above, use 'sqex' rather than 'se'.
throw SQLExceptions.getStore(sqex, ps, _dict);
}
} finally {
_batchedSql = null;
batchedRows.clear();
if (ps != null) {
//Clear the Params now....should this be done above? No.
//if JDBC provider using PureQuery, ps is null
ps.clearParameters();
try {
ps.close();

View File

@ -2276,8 +2276,14 @@ public class BrokerImpl
}
if (opt)
return new OptimisticException(t);
Object failedObject = null;
if (t[0] instanceof OpenJPAException){
failedObject = ((OpenJPAException)t[0]).getFailedObject();
}
return new StoreException(_loc.get("rolled-back")).
setNestedThrowables(t).setFatal(true);
setNestedThrowables(t).setFatal(true).setFailedObject(failedObject);
}
/**

View File

@ -242,7 +242,7 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
}
private SQLException wrap(SQLException sqle, Statement stmnt, int indexOfFailedBatchObject) {
return wrap(sqle, stmnt, null, -1);
return wrap(sqle, stmnt, null, indexOfFailedBatchObject);
}
/**
@ -1114,7 +1114,7 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
// we are tracking parameters, then set the current
// parameter set to be the index of the failed
// statement so that the ReportingSQLException will
// show the correct param
// show the correct param(s)
if (se instanceof BatchUpdateException
&& _paramBatch != null && shouldTrackParameters()) {
int[] count = ((BatchUpdateException) se).
@ -1161,7 +1161,6 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
throw err;
} finally {
logTime(start);
clearLogParameters(true);
handleSQLErrors(LoggingPreparedStatement.this, err);
}
}
@ -1409,16 +1408,12 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
}
private void clearLogParameters(boolean batch) {
//Made !batch...we only want to clear if
//we are NOT using batching. If we clear now,
//the _params will not be displayed in the resultant
//exception message. But when should we 'clear' them???
if (!batch){
if (_params != null)
_params.clear();
if (_paramBatch != null)
_paramBatch.clear();
if (_params != null) {
_params.clear();
}
if (batch && _paramBatch != null) {
_paramBatch.clear();
}
}
@ -1605,6 +1600,9 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
private final String _sql;
private List<String> _params = null;
private List<List<String>> _paramBatch = null;
//When batching is used, this variable contains the index into the last
//successfully executed batched statement.
int batchedRowsBaseIndex = 0;
public LoggingCallableStatement(CallableStatement stmt, String sql)
throws SQLException {
@ -1709,11 +1707,29 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
}
public int[] executeBatch() throws SQLException {
int indexOfFirstFailedObject = -1;
logBatchSQL(this);
long start = System.currentTimeMillis();
SQLException err = null;
try {
return super.executeBatch();
int[] toReturn = super.executeBatch();
//executeBatch is called any time the number of batched statements
//is equal to, or less than, batchLimit. In the 'catch' block below,
//the logic seeks to find an index based on the current executeBatch
//results. This is fine when executeBatch is only called once, but
//if executeBatch is called many times, the _paramsBatch will continue
//to grow, as such, to index into _paramsBatch, we need to take into
//account the number of times executeBatch is called in order to
//correctly index into _paramsBatch. To that end, each time executeBatch
//is called, lets get the size of _paramBatch. This will effectively
//tell us the index of the last successfully executed batch statement.
//If an exception is caused, then we know that _paramBatch.size was
//the index of the LAST row to successfully execute.
if (_paramBatch != null){
batchedRowsBaseIndex = _paramBatch.size();
}
return toReturn;
} catch (SQLException se) {
// if the exception is a BatchUpdateException, and
// we are tracking parameters, then set the current
@ -1726,12 +1742,11 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
getUpdateCounts();
if (count != null && count.length <= _paramBatch.size())
{
int index = -1;
for (int i = 0; i < count.length; i++) {
// -3 is Statement.STATEMENT_FAILED, but is
// only available in JDK 1.4+
if (count[i] == Statement.EXECUTE_FAILED) {
index = i;
indexOfFirstFailedObject = i;
break;
}
}
@ -1739,19 +1754,35 @@ public class LoggingConnectionDecorator implements ConnectionDecorator {
// no -3 element: it may be that the server stopped
// processing, so the size of the count will be
// the index
if (index == -1)
index = count.length + 1;
//See the Javadoc for 'getUpdateCounts'; a provider
//may stop processing when the first failure occurs,
//as such, it may only return 'UpdateCounts' for the
//first few which pass. As such, the failed
//index is 'count.length', NOT count.length+1. That
//is, if the provider ONLY returns the first few that
//passes (i.e. say an array of [1,1] is returned) then
//length is 2, and since _paramBatch starts at 0, we
//don't want to use length+1 as that will give us the
//wrong index.
if (indexOfFirstFailedObject == -1){
indexOfFirstFailedObject = count.length;
}
//Finally, whatever the index is at this point, add batchedRowsBaseIndex
//to it to get the final index. Recall, we need to start our index from the
//last batch which successfully executed.
indexOfFirstFailedObject += batchedRowsBaseIndex;
// set the current params to the saved values
if (index < _paramBatch.size())
_params = (List<String>) _paramBatch.get(index);
if (indexOfFirstFailedObject < _paramBatch.size()){
_params = (List<String>) _paramBatch.get(indexOfFirstFailedObject);
}
}
}
err = wrap(se, LoggingCallableStatement.this);
err = wrap(se, LoggingCallableStatement.this, indexOfFirstFailedObject);
throw err;
} finally {
logTime(start);
clearLogParameters(true);
handleSQLErrors(LoggingCallableStatement.this, err);
}
}

View File

@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.openjpa.persistence.batch.exception;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class Ent1 {
// primary key:
@Id
private int pk;
public int getPk() {return pk;}
public void setPk(int pk) {this.pk = pk;}
private String name;
public String getName(){return name;}
public void setName(String str){
name = str;
}
public Ent1() {}
public Ent1(int pk, String str) {this.pk = pk;name=str;}
public String toString(){
return "Ent1 [pk = " + pk + ", " + name +"]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + pk;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Ent1 other = (Ent1) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (pk != other.pk)
return false;
return true;
}
}

View File

@ -0,0 +1,324 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.openjpa.persistence.batch.exception;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import org.apache.openjpa.persistence.test.PersistenceTestCase;
import org.apache.openjpa.util.ExceptionInfo;
//This test was created for OPENJPA-1550. In this issue the user was
//not able to get the 'failed object' (the object causing the failure) when
//batch limit was -1 or a value greater than 1. Also, they found that the
//'params' listed in the prepared statement were missing. This test will set
//various batch limits and verify that with the fix to 1550, the correct
//'failed object' and prepared statement is returned.
public class TestBatchLimitException extends PersistenceTestCase {
static Ent1 expectedFailedObject;
final String expectedFailureMsg =
"INSERT INTO Ent1 (pk, name) VALUES (?, ?) [params=(int) 200, (String) twohundred]";
public EntityManagerFactory newEmf(String batchLimit) {
EntityManagerFactory emf =
createEMF(Ent1.class,
"openjpa.jdbc.SynchronizeMappings",
"buildSchema(ForeignKeys=true)",
"openjpa.jdbc.DBDictionary", batchLimit,
CLEAR_TABLES);
assertNotNull("Unable to create EntityManagerFactory", emf);
return emf;
}
public void setUp() {
expectedFailedObject = null;
}
// Test that we get the correct 'failed object' when we have a batchLimt
// of X and Y rows, where X>Y. A duplicate row will be inserted
// sometime within the Y rows. This will verify that we get the right
// 'failed object' and message.
public void testExceptionInFirstBatch() throws Throwable {
EntityManagerFactory emf = newEmf("batchLimit=-1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(new Ent1(1, "one"));
expectedFailedObject = new Ent1(200, "twohundred");
em.persist(expectedFailedObject);
em.persist(new Ent1(5, "five"));
em.getTransaction().commit();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
em2.persist(new Ent1(0, "zero"));
em2.persist(new Ent1(2, "two"));
em2.persist(new Ent1(200, "twohundred"));
em2.persist(new Ent1(3, "three"));
em2.persist(new Ent1(1, "one"));
em2.persist(new Ent1(5, "five"));
try {
em2.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
em2.close();
emf.close();
}
}
// Test that we get the correct 'failed object' when there is only one
// row in the batch. The 'batching' logic executes a different
// statement when only one row is to be updated/inserted.
public void testExceptionSingleBatchedRow() throws Throwable {
EntityManagerFactory emf = newEmf("batchLimit=-1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
expectedFailedObject = new Ent1(200, "twohundred");
em.persist(expectedFailedObject);
em.getTransaction().commit();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
em2.persist(new Ent1(200, "twohundred"));
try {
em2.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
em2.close();
emf.close();
}
}
// Test that we get the correct 'failed object' and message when we
// have a batchLimt of X and Y rows, where Y>X. In this case, the
// batch is executed every time the batchLimt is hit. A duplicate
// row will be inserted sometime after X (X+1, i.e right at the
// boundary of the batch) to verify that we get the right
// 'failed object' and msg no matter which batch a duplicate is
// contained in. This test is important because as part of the
// fix to OPENJPA-1510 we had to add extra logic to keep track
// of which batch the 'failed object' was in, along with the
// index into that batch.
public void testExceptionInSecondBatch() throws Throwable {
EntityManagerFactory emf = newEmf("batchLimit=9");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
expectedFailedObject = new Ent1(200, "twohundred");
em.persist(expectedFailedObject);
em.getTransaction().commit();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
// Put 9 objects/rows into the batch
for (int i = 0; i < 9; i++) {
em2.persist(new Ent1(i, "name" + i));
}
// Put the duplicate object/row as the first element in the second batch.
em2.persist(new Ent1(200, "twohundred"));
try {
em2.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
em2.close();
emf.close();
}
}
// Same as testRowsGreaterThanBatchLimit_boundaryCase, but the object to cause the failure
// is in the middle of the second batch. testExceptioninSecondBatch puts
// the failing object as the first element in the second batch, this test puts
// it somewhere in the middle of the third batch. Again, we want to make sure our
// indexing into the batch containing the 'failed object' is correct.
public void testExceptionInThirdBatch() throws Throwable {
EntityManagerFactory emf = newEmf("batchLimit=9");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
expectedFailedObject = new Ent1(200, "twohundred");
em.persist(expectedFailedObject);
em.getTransaction().commit();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
// Persist 21 objects/rows....as such we will have two 'full'
// batches (9*2=18) and 3 (21-18=3) objects/rows in the 3rd batch.
for (int i = 0; i < 22; i++) {
em2.persist(new Ent1(i, "name" + i));
}
// Put the duplicate row in the 3rd batch.
em2.persist(new Ent1(200, "twohundred"));
// Put a few more objects into the batch.
for (int i = 22; i < 40; i++) {
em2.persist(new Ent1(i, "name" + i));
}
try {
em2.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
em2.close();
emf.close();
}
}
// Similar to the previous two tests, but lets run the test with a large
// batch with a failure, and then commit, then run large batches
// again with failures again.....just want to make sure things are not in
// some way 're-used' between the two commits as far as the indexes go.
public void testSecondExceptionHasRightIndex() throws Throwable {
testExceptionInThirdBatch();
EntityManagerFactory emf = newEmf("batchLimit=9");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
for (int i = 40; i < 55; i++) {
em.persist(new Ent1(i, "name" + i));
}
em.persist(new Ent1(200, "twohundred"));
for (int i = 55; i < 65; i++) {
em.persist(new Ent1(i, "name" + i));
}
try {
em.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
em.close();
emf.close();
}
}
public void testExceptionWithMultipleCommits() throws Throwable {
EntityManagerFactory emf = newEmf("batchLimit=-1");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(new Ent1(1, "one"));
expectedFailedObject = new Ent1(200, "twohundred");
em.persist(expectedFailedObject);
em.persist(new Ent1(5, "five"));
em.getTransaction().commit();
em.close();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
em2.persist(new Ent1(0, "zero"));
em2.persist(new Ent1(2, "two"));
em2.persist(new Ent1(3, "three"));
em2.getTransaction().commit();
em2.getTransaction().begin();
em2.persist(new Ent1(6, "six"));
em2.persist(new Ent1(200, "twohundred"));
em2.persist(new Ent1(7, "seven"));
try {
em2.getTransaction().commit();
} catch (Throwable excp) {
verifyExDetails(excp);
}
finally {
if (em2.getTransaction().isActive()) {
em2.getTransaction().rollback();
}
em2.close();
emf.close();
}
}
// Verify that the resultant exception contains the correct 'failed object'
// and exception message.
public void verifyExDetails(Throwable excp) throws Throwable {
// The exception should contain the 'failed object'
verifyFailedObject(excp);
// The second cause should contain the message which shows the failing prepared statement.
Throwable cause = excp.getCause().getCause();
verifyExMsg(cause.getMessage());
}
public void verifyFailedObject(Throwable excp) throws Throwable {
if (excp instanceof ExceptionInfo) {
ExceptionInfo e = (ExceptionInfo) excp;
Ent1 failedObject = (Ent1) e.getFailedObject();
assertNotNull("Failed object was null.", failedObject);
assertEquals(expectedFailedObject, failedObject);
}
else {
throw excp;
}
}
public void verifyExMsg(String msg) {
assertNotNull("Exception message was null.", msg);
assertTrue("Did not see expected text in message. Expected <" + expectedFailureMsg + "> but was " + msg, msg
.contains(expectedFailureMsg));
}
}

View File

@ -81,6 +81,7 @@ import org.apache.openjpa.persistence.criteria.CriteriaBuilderImpl;
import org.apache.openjpa.persistence.criteria.OpenJPACriteriaBuilder;
import org.apache.openjpa.persistence.criteria.OpenJPACriteriaQuery;
import org.apache.openjpa.persistence.validation.ValidationUtils;
import org.apache.openjpa.util.ExceptionInfo;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.RuntimeExceptionTranslator;
@ -569,8 +570,15 @@ public class EntityManagerImpl
// normal exception translator, since the spec says they
// should be thrown whenever the commit fails for any reason at
// all, wheras the exception translator handles exceptions that
// are caused for specific reasons
throw new RollbackException(e);
// are caused for specific reasons
// pass along the failed object if one is available.
Object failedObject = null;
if (e instanceof ExceptionInfo){
failedObject = ((ExceptionInfo)e).getFailedObject();
}
throw new RollbackException(e).setFailedObject(failedObject);
}
}

View File

@ -38,6 +38,8 @@ import org.apache.openjpa.util.Exceptions;
public class RollbackException
extends javax.persistence.RollbackException
implements Serializable, ExceptionInfo {
private transient Object _failed = null;
private transient Throwable[] _nested;
@ -67,7 +69,12 @@ public class RollbackException
}
public Object getFailedObject() {
return null;
return _failed;
}
public RollbackException setFailedObject(Object failed) {
_failed = failed;
return this;
}
public String toString() {

View File

@ -206,6 +206,7 @@ Bug
* [OPENJPA-1544] - Remove WebSphere version number from org/apache/ee/localizer.properties
* [OPENJPA-1546] - OpenJPA doesn't work as internal JPA inside web applicaion in JBoss AS
* [OPENJPA-1547] - NOT IN with MEMBER OF returns syntax error
* [OPENJPA-1550] - When batchLimit=-1 or >1 and an exception is caused, the params and failedObject are missing from the resultant exception.
* [OPENJPA-1556] - Exception thrown on first use of @Strategy in @Embeddable classes
* [OPENJPA-1558] - Many side of a MxO relationship contains null reference if One side is loaded first.
* [OPENJPA-1562] - EntityManager:Refresh on Removed entity does not trigger IllegalArgumentException

View File

@ -301,6 +301,8 @@ in each release of OpenJPA.</P>
</li>
<li>[<a href='https://issues.apache.org/jira/browse/OPENJPA-1547'>OPENJPA-1547</a>] - NOT IN with MEMBER OF returns syntax error
</li>
<li>[<a href='https://issues.apache.org/jira/browse/OPENJPA-1550'>OPENJPA-1550</a>] - When batchLimit=-1 or >1 and an exception is caused, the params and failedObject are missing from the resultant exception.
</li>
<li>[<a href='https://issues.apache.org/jira/browse/OPENJPA-1556'>OPENJPA-1556</a>] - Exception thrown on first use of @Strategy in @Embeddable classes
</li>
<li>[<a href='https://issues.apache.org/jira/browse/OPENJPA-1558'>OPENJPA-1558</a>] - Many side of a MxO relationship contains null reference if One side is loaded first.