/*
* Hibernate OGM, Domain model persistence for NoSQL datastores
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.ogm.backendtck.compensation;
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.transaction.TransactionManager;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.HibernateEntityManagerFactory;
import org.hibernate.ogm.cfg.OgmProperties;
import org.hibernate.ogm.compensation.ErrorHandler.RollbackContext;
import org.hibernate.ogm.compensation.operation.CreateTupleWithKey;
import org.hibernate.ogm.compensation.operation.ExecuteBatch;
import org.hibernate.ogm.compensation.operation.GridDialectOperation;
import org.hibernate.ogm.compensation.operation.InsertOrUpdateTuple;
import org.hibernate.ogm.dialect.batch.spi.BatchableGridDialect;
import org.hibernate.ogm.dialect.batch.spi.GroupingByEntityDialect;
import org.hibernate.ogm.dialect.impl.GridDialects;
import org.hibernate.ogm.dialect.spi.DuplicateInsertPreventionStrategy;
import org.hibernate.ogm.dialect.spi.GridDialect;
import org.hibernate.ogm.model.impl.DefaultEntityKeyMetadata;
import org.hibernate.ogm.utils.GridDialectType;
import org.hibernate.ogm.utils.PackagingRule;
import org.hibernate.ogm.utils.SkipByGridDialect;
import org.hibernate.ogm.utils.TestHelper;
import org.hibernate.ogm.utils.jpa.GetterPersistenceUnitInfo;
import org.hibernate.ogm.utils.jpa.OgmJpaTestCase;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
/**
* Test for using the compensation SPI with JPA.
*
* @author Gunnar Morling
*
*/
public class CompensationSpiJpaTest extends OgmJpaTestCase {
@Rule
public PackagingRule packaging = new PackagingRule( "persistencexml/transaction-type-jta.xml", Shipment.class );
@Test
@SkipByGridDialect(value = { GridDialectType.MONGODB, GridDialectType.INFINISPAN_REMOTE },
comment = "MongoDB and Infinispan Remote tests runs w/o transaction manager")
public void onRollbackTriggeredThroughJtaPresentsAppliedInsertOperations() throws Exception {
Map<String, Object> settings = new HashMap<>();
settings.putAll( TestHelper.getDefaultTestSettings() );
settings.put( OgmProperties.ERROR_HANDLER, InvocationTrackingHandler.INSTANCE );
EntityManagerFactory emf = Persistence.createEntityManagerFactory( "transaction-type-jta", settings );
TransactionManager transactionManager = getTransactionManager( emf );
transactionManager.begin();
EntityManager em = emf.createEntityManager();
// given two inserted records
em.persist( new Shipment( "shipment-1", "INITIAL" ) );
em.persist( new Shipment( "shipment-2", "INITIAL" ) );
em.flush();
em.clear();
try {
// when provoking a duplicate-key exception
em.persist( new Shipment( "shipment-1", "INITIAL" ) );
transactionManager.commit();
fail( "Expected exception was not raised" );
}
catch (Exception e) {
// Nothing to do
}
// then expect the ops for inserting the two records
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( BatchableGridDialect.class ) || currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
}
// If LOOK_UP is used for duplicate prevention, the duplicated id will be detected prior to the actual insert
// itself; otherwise, the CreateTuple call will succeed, and only the insert call will fail
if ( currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( appliedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
}
transactionManager.begin();
em.joinTransaction();
Shipment shipment = em.find( Shipment.class, "shipment-1" );
if ( shipment != null ) {
em.remove( shipment );
}
shipment = em.find( Shipment.class, "shipment-2" );
if ( shipment != null ) {
em.remove( shipment );
}
transactionManager.commit();
em.close();
}
@Test
public void onRollbackTriggeredThroughManualRollbackPresentsAppliedInsertOperations() throws Exception {
EntityManager em = getFactory().createEntityManager();
em.getTransaction().begin();
// given two inserted records
em.persist( new Shipment( "shipment-1", "INITIAL" ) );
em.persist( new Shipment( "shipment-2", "INITIAL" ) );
em.flush();
em.clear();
try {
// when provoking a duplicate-key exception
em.persist( new Shipment( "shipment-1", "INITIAL" ) );
em.getTransaction().commit();
fail( "Expected exception was not raised" );
}
catch (Exception e) {
// Nothing to do
}
// then expect the ops for inserting the two records
Iterator<RollbackContext> onRollbackInvocations = InvocationTrackingHandler.INSTANCE.getOnRollbackInvocations().iterator();
Iterator<GridDialectOperation> appliedOperations = onRollbackInvocations.next().getAppliedGridDialectOperations().iterator();
assertThat( onRollbackInvocations.hasNext() ).isFalse();
if ( currentDialectHasFacet( BatchableGridDialect.class ) || currentDialectHasFacet( GroupingByEntityDialect.class ) ) {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
GridDialectOperation operation = appliedOperations.next();
assertThat( operation ).isInstanceOf( ExecuteBatch.class );
ExecuteBatch batch = operation.as( ExecuteBatch.class );
Iterator<GridDialectOperation> batchedOperations = batch.getOperations().iterator();
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( batchedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
assertThat( appliedOperations.next() ).isInstanceOf( InsertOrUpdateTuple.class );
}
// If LOOK_UP is used for duplicate prevention, the duplicated id will be detected prior to the actual insert
// itself; otherwise, the CreateTuple call will succeed, and only the insert call will fail
if ( currentDialectUsesLookupDuplicatePreventionStrategy() ) {
assertThat( appliedOperations.hasNext() ).isFalse();
}
else {
assertThat( appliedOperations.next() ).isInstanceOf( CreateTupleWithKey.class );
}
em.clear();
em.getTransaction().begin();
Shipment shipment = em.find( Shipment.class, "shipment-1" );
if ( shipment != null ) {
em.remove( shipment );
}
shipment = em.find( Shipment.class, "shipment-2" );
if ( shipment != null ) {
em.remove( shipment );
}
em.getTransaction().commit();
em.close();
}
@After
public void resetErrorHandler() throws Exception {
InvocationTrackingHandler.INSTANCE.clear();
}
@Override
public Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { Shipment.class };
}
@Override
protected void configure(GetterPersistenceUnitInfo info) {
info.getProperties().put( OgmProperties.ERROR_HANDLER, InvocationTrackingHandler.INSTANCE );
}
private boolean currentDialectHasFacet(Class<? extends GridDialect> facet) {
SessionFactoryImplementor sfi = (SessionFactoryImplementor) ( (HibernateEntityManagerFactory) getFactory() ).getSessionFactory();
GridDialect gridDialect = sfi.getServiceRegistry().getService( GridDialect.class );
return GridDialects.hasFacet( gridDialect, facet );
}
private boolean currentDialectUsesLookupDuplicatePreventionStrategy() {
SessionFactoryImplementor sfi = (SessionFactoryImplementor) ( (HibernateEntityManagerFactory) getFactory() ).getSessionFactory();
GridDialect gridDialect = sfi.getServiceRegistry().getService( GridDialect.class );
DefaultEntityKeyMetadata ekm = new DefaultEntityKeyMetadata( "Shipment", new String[]{"id"} );
return gridDialect.getDuplicateInsertPreventionStrategy( ekm ) == DuplicateInsertPreventionStrategy.LOOK_UP;
}
}