| /* |
| * 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.jdbc.conf.JDBCConfiguration; |
| import org.apache.openjpa.jdbc.sql.DBDictionary; |
| import org.apache.openjpa.jdbc.sql.OracleDictionary; |
| import org.apache.openjpa.jdbc.sql.PostgresDictionary; |
| import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; |
| import org.apache.openjpa.persistence.test.AbstractPersistenceTestCase; |
| 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 AbstractPersistenceTestCase { |
| |
| static Ent1 expectedFailedObject; |
| static Ent1 expectedFailedObjectOracle; |
| |
| static boolean isOracle = false; |
| static boolean isPostgres = false; |
| |
| final String expectedFailureMsg = |
| "INSERT INTO Ent1 (pk, name) VALUES (?, ?) [params=(int) 200, (String) twohundred]"; |
| final String expectedFailureMsg18 = |
| "INSERT INTO Ent1 (pk, name) VALUES (?, ?) [params=(int) 18, (String) name18]"; |
| String expectedFailureMsgOracle = expectedFailureMsg18; |
| |
| public EntityManagerFactory newEmf(String batchLimit) { |
| OpenJPAEntityManagerFactorySPI emf = |
| createEMF(Ent1.class, |
| "openjpa.jdbc.SynchronizeMappings", |
| "buildSchema(ForeignKeys=true)", |
| "openjpa.jdbc.DBDictionary", batchLimit, |
| "openjpa.ConnectionFactoryProperties", "PrintParameters=true", |
| CLEAR_TABLES); |
| |
| assertNotNull("Unable to create EntityManagerFactory", emf); |
| JDBCConfiguration conf = (JDBCConfiguration) emf.getConfiguration(); |
| DBDictionary dict = conf.getDBDictionaryInstance(); |
| isOracle = dict instanceof OracleDictionary; |
| isPostgres = dict instanceof PostgresDictionary; |
| return emf; |
| } |
| |
| @Override |
| public void setUp() { |
| expectedFailedObject = null; |
| expectedFailedObjectOracle = 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(); |
| // special case, due to how Oracle returns all statements in the batch |
| expectedFailedObjectOracle = new Ent1(18, "name18"); |
| expectedFailureMsgOracle = expectedFailureMsg18; |
| em2.persist(expectedFailedObjectOracle); |
| 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| // 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"); |
| expectedFailedObjectOracle = expectedFailedObject; |
| expectedFailureMsgOracle = expectedFailureMsg; |
| 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| // 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"); |
| expectedFailedObjectOracle = expectedFailedObject; |
| expectedFailureMsgOracle = expectedFailureMsg; |
| 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| // 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 { |
| final int batchLimit=9; |
| EntityManagerFactory emf = newEmf("batchLimit="+batchLimit); |
| 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. |
| int i=0; |
| for (; i < 2*batchLimit; i++) { |
| em2.persist(new Ent1(i, "name" + i)); |
| } |
| |
| // manually create third batch, due to how Oracle returns all statements in the batch |
| expectedFailedObjectOracle = new Ent1(i, "name" + i++); |
| expectedFailureMsgOracle = expectedFailureMsg18; |
| em2.persist(expectedFailedObjectOracle); // 18 |
| em2.persist(new Ent1(i, "name" + i++)); // 19 |
| em2.persist(new Ent1(i, "name" + i++)); // 20 |
| em2.persist(new Ent1(i, "name" + i++)); // 21 |
| // Put the duplicate row in the 3rd batch. |
| em2.persist(new Ent1(200, "twohundred")); |
| // Put a few more objects into the batch. |
| for (i = 22; i < 4*batchLimit; 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| // 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 { |
| final int batchLimit=9; |
| |
| testExceptionInThirdBatch(); |
| |
| EntityManagerFactory emf = newEmf("batchLimit=9"); |
| EntityManager em = emf.createEntityManager(); |
| |
| em.getTransaction().begin(); |
| |
| for (int i = 4*batchLimit; i < 5*batchLimit; i++) { |
| em.persist(new Ent1(i, "name" + i)); |
| } |
| |
| // manually capture start of batch, due to how Oracle returns all statements in the batch |
| expectedFailedObjectOracle = new Ent1(200, "twohundred"); |
| expectedFailureMsgOracle = expectedFailureMsg; |
| em.persist(expectedFailedObjectOracle); |
| |
| for (int i = 5*batchLimit; i < 7*batchLimit; 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| 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(4, "four")); |
| em2.persist(new Ent1(0, "zero")); |
| em2.persist(new Ent1(2, "two")); |
| em2.persist(new Ent1(3, "three")); |
| em2.getTransaction().commit(); |
| |
| em2.getTransaction().begin(); |
| // special case, due to how Oracle returns all statements in the batch |
| expectedFailedObjectOracle = new Ent1(18, "name18"); |
| expectedFailureMsgOracle = expectedFailureMsg18; |
| em2.persist(expectedFailedObjectOracle); |
| 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(); |
| closeEMF(emf); |
| } |
| } |
| |
| // 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); |
| if (!isOracle && !isPostgres) { |
| assertEquals(expectedFailedObject, failedObject); |
| } else if (isOracle) { |
| // since Oracle 18 we see a different behaviour, so test both ways |
| assertTrue(failedObject.equals(expectedFailedObject) || failedObject.equals(expectedFailedObjectOracle) ); |
| } else { |
| // special case, as Oracle returns all statements in the batch |
| assertEquals(expectedFailedObjectOracle, failedObject); |
| } |
| } |
| else { |
| throw excp; |
| } |
| } |
| |
| public void verifyExMsg(String msg) { |
| assertNotNull("Exception message was null.", msg); |
| if (!isOracle && !isPostgres) { |
| assertTrue("Did not see expected text in message. Expected <" + expectedFailureMsg + "> but was " + |
| msg, msg.contains(expectedFailureMsg)); |
| } else if (isOracle) { |
| // since Oracle 18 we see a different behaviour, so test both ways |
| assertTrue(msg.contains(expectedFailureMsg) || msg.contains(expectedFailureMsgOracle)); |
| } else { |
| // special case, as Oracle returns all statements in the batch |
| assertTrue("Did not see expected text in message. Expected <" + expectedFailureMsgOracle + "> but was " + |
| msg, msg.contains(expectedFailureMsgOracle)); |
| } |
| } |
| } |
| |