diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java index 4a125e50d..64ae8c81a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveCoordinatorFactory.java @@ -9,8 +9,11 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; import org.hibernate.metamodel.mapping.SingularAttributeMapping; +import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinatorSoft; import org.hibernate.reactive.persister.entity.mutation.ReactiveInsertCoordinatorStandard; import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinatorNoOp; @@ -39,10 +42,13 @@ public static ReactiveUpdateCoordinator buildUpdateCoordinator( return new ReactiveUpdateCoordinatorNoOp( entityPersister ); } - public static ReactiveDeleteCoordinator buildDeleteCoordinator( + public static DeleteCoordinator buildDeleteCoordinator( + SoftDeleteMapping softDeleteMapping, AbstractEntityPersister entityPersister, SessionFactoryImplementor factory) { - return new ReactiveDeleteCoordinator( entityPersister, factory ); + return softDeleteMapping != null + ? new ReactiveDeleteCoordinatorSoft( entityPersister, factory ) + : new ReactiveDeleteCoordinator( entityPersister, factory ); } public static ReactiveUpdateCoordinator buildMergeCoordinator( diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java index 8e4f24a1f..b9bfdfcb2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java @@ -152,7 +152,7 @@ protected UpdateCoordinator buildUpdateCoordinator() { @Override protected DeleteCoordinator buildDeleteCoordinator() { - return ReactiveCoordinatorFactory.buildDeleteCoordinator( this, getFactory() ); + return ReactiveCoordinatorFactory.buildDeleteCoordinator( super.getSoftDeleteMapping(), this, getFactory() ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java index e736d7319..22454b546 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java @@ -9,7 +9,6 @@ import java.util.List; import java.util.concurrent.CompletionStage; - import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; @@ -49,7 +48,7 @@ import org.hibernate.reactive.generator.values.ReactiveInsertGeneratedIdentifierDelegate; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; -import org.hibernate.reactive.persister.entity.mutation.ReactiveDeleteCoordinator; +import org.hibernate.reactive.persister.entity.mutation.ReactiveAbstractDeleteCoordinator; import org.hibernate.reactive.persister.entity.mutation.ReactiveInsertCoordinatorStandard; import org.hibernate.reactive.persister.entity.mutation.ReactiveUpdateCoordinator; import org.hibernate.reactive.util.impl.CompletionStages; @@ -115,7 +114,7 @@ protected InsertCoordinator buildInsertCoordinator() { @Override protected DeleteCoordinator buildDeleteCoordinator() { - return ReactiveCoordinatorFactory.buildDeleteCoordinator( this, getFactory() ); + return ReactiveCoordinatorFactory.buildDeleteCoordinator( super.getSoftDeleteMapping(), this, getFactory() ); } @Override @@ -357,7 +356,7 @@ public CompletionStage insertReactive(Object id, Object[] field @Override public CompletionStage deleteReactive(Object id, Object version, Object entity, SharedSessionContractImplementor session) { - return ( (ReactiveDeleteCoordinator) getDeleteCoordinator() ).reactiveDelete( entity, id, version, session ); + return ( (ReactiveAbstractDeleteCoordinator) getDeleteCoordinator() ).reactiveDelete( entity, id, version, session ); } /** diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java index 266aaa467..7792a27e9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java @@ -170,7 +170,7 @@ protected UpdateCoordinator buildUpdateCoordinator() { @Override protected DeleteCoordinator buildDeleteCoordinator() { - return ReactiveCoordinatorFactory.buildDeleteCoordinator( this, getFactory() ); + return ReactiveCoordinatorFactory.buildDeleteCoordinator( super.getSoftDeleteMapping(), this, getFactory() ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveAbstractDeleteCoordinator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveAbstractDeleteCoordinator.java new file mode 100644 index 000000000..03193d96f --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveAbstractDeleteCoordinator.java @@ -0,0 +1,21 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.persister.entity.mutation; + +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; + +/** + * With this interface we can have multiple delete coordinators that extend {@link org.hibernate.persister.entity.mutation.AbstractDeleteCoordinator}. + * + * @see ReactiveDeleteCoordinatorSoft + * @see ReactiveDeleteCoordinator + */ +public interface ReactiveAbstractDeleteCoordinator { + + CompletionStage reactiveDelete(Object entity, Object id, Object version, SharedSessionContractImplementor session); +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java index bcde8e153..fbb4d2751 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinator.java @@ -30,7 +30,7 @@ import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; -public class ReactiveDeleteCoordinator extends DeleteCoordinatorStandard { +public class ReactiveDeleteCoordinator extends DeleteCoordinatorStandard implements ReactiveAbstractDeleteCoordinator { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); @@ -45,6 +45,7 @@ public void delete(Object entity, Object id, Object version, SharedSessionContra throw LOG.nonReactiveMethodCall( "coordinateReactiveDelete" ); } + @Override public CompletionStage reactiveDelete(Object entity, Object id, Object version, SharedSessionContractImplementor session) { try { super.delete( entity, id, version, session ); @@ -142,7 +143,6 @@ protected void doStaticDelete(Object entity, Object id, Object rowId, Object[] l final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); applyId( id, rowId, mutationExecutor, getStaticMutationOperationGroup(), session ); - String[] identifierColumnNames = entityPersister().getIdentifierColumnNames(); mutationExecutor.executeReactive( entity, null, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinatorSoft.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinatorSoft.java new file mode 100644 index 000000000..5935ab052 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/mutation/ReactiveDeleteCoordinatorSoft.java @@ -0,0 +1,186 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.persister.entity.mutation; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.hibernate.engine.jdbc.mutation.JdbcValueBindings; +import org.hibernate.engine.jdbc.mutation.MutationExecutor; +import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; +import org.hibernate.engine.jdbc.mutation.spi.MutationExecutorService; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.persister.entity.AbstractEntityPersister; +import org.hibernate.persister.entity.mutation.DeleteCoordinatorSoft; +import org.hibernate.persister.entity.mutation.EntityTableMapping; +import org.hibernate.reactive.adaptor.impl.PrepareStatementDetailsAdaptor; +import org.hibernate.reactive.adaptor.impl.PreparedStatementAdaptor; +import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveMutationExecutor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.MutationOperationGroup; + +import static org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck; +import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +public class ReactiveDeleteCoordinatorSoft extends DeleteCoordinatorSoft implements ReactiveAbstractDeleteCoordinator { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + private CompletionStage stage; + + public ReactiveDeleteCoordinatorSoft( + AbstractEntityPersister entityPersister, + SessionFactoryImplementor factory) { + super( entityPersister, factory ); + } + + @Override + public void delete(Object entity, Object id, Object version, SharedSessionContractImplementor session) { + throw LOG.nonReactiveMethodCall( "coordinateReactiveDelete" ); + } + + @Override + public CompletionStage reactiveDelete(Object entity, Object id, Object version, SharedSessionContractImplementor session) { + try { + super.delete( entity, id, version, session ); + return stage != null ? stage : voidFuture(); + } + catch (Throwable t) { + if ( stage == null ) { + return failedFuture( t ); + } + stage.toCompletableFuture().completeExceptionally( t ); + return stage; + } + } + + @Override + protected void doDynamicDelete(Object entity, Object id, Object rowId, Object[] loadedState, SharedSessionContractImplementor session) { + stage = new CompletableFuture<>(); + final MutationOperationGroup operationGroup = generateOperationGroup( null, loadedState, true, session ); + final ReactiveMutationExecutor mutationExecutor = mutationExecutor( session, operationGroup ); + + for ( int i = 0; i < operationGroup.getNumberOfOperations(); i++ ) { + final MutationOperation mutation = operationGroup.getOperation( i ); + if ( mutation != null ) { + final String tableName = mutation.getTableDetails().getTableName(); + mutationExecutor.getPreparedStatementDetails( tableName ); + } + } + applyDynamicDeleteTableDetails( id, rowId, loadedState, mutationExecutor, operationGroup, session ); + mutationExecutor.executeReactive( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ) + .whenComplete( (o, t) -> mutationExecutor.release() ) + .whenComplete( this::complete ); + } + + @Override + protected void applyId( + Object id, + Object rowId, + MutationExecutor mutationExecutor, + MutationOperationGroup operationGroup, + SharedSessionContractImplementor session) { + final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + + for ( int position = 0; position < operationGroup.getNumberOfOperations(); position++ ) { + final MutationOperation jdbcMutation = operationGroup.getOperation( position ); + final EntityTableMapping tableDetails = (EntityTableMapping) jdbcMutation.getTableDetails(); + breakDownKeyJdbcValues( id, rowId, session, jdbcValueBindings, tableDetails ); + final PreparedStatementDetails statementDetails = mutationExecutor.getPreparedStatementDetails( tableDetails.getTableName() ); + if ( statementDetails != null ) { + PreparedStatementAdaptor.bind( statement -> { + PrepareStatementDetailsAdaptor detailsAdaptor = new PrepareStatementDetailsAdaptor( + statementDetails, + statement, + session.getJdbcServices() + ); + // force creation of the PreparedStatement + //noinspection resource + detailsAdaptor.resolveStatement(); + } ); + } + } + } + + @Override + protected void doStaticDelete(Object entity, Object id, Object rowId, Object[] loadedState, Object version, SharedSessionContractImplementor session) { + stage = new CompletableFuture<>(); + final boolean applyVersion = entity != null; + final MutationOperationGroup operationGroupToUse = entity == null + ? resolveNoVersionDeleteGroup( session ) + : getStaticMutationOperationGroup(); + + final ReactiveMutationExecutor mutationExecutor = mutationExecutor( session, operationGroupToUse ); + for ( int position = 0; position < getStaticMutationOperationGroup().getNumberOfOperations(); position++ ) { + final MutationOperation mutation = getStaticMutationOperationGroup().getOperation( position ); + if ( mutation != null ) { + mutationExecutor.getPreparedStatementDetails( mutation.getTableDetails().getTableName() ); + } + } + + if ( applyVersion ) { + applyLocking( version, null, mutationExecutor, session ); + } + final JdbcValueBindings jdbcValueBindings = mutationExecutor.getJdbcValueBindings(); + bindPartitionColumnValueBindings( loadedState, session, jdbcValueBindings ); + applyId( id, rowId, mutationExecutor, getStaticMutationOperationGroup(), session ); + mutationExecutor.executeReactive( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ) + .thenAccept( v -> mutationExecutor.release() ) + .whenComplete( this::complete ); + } + + private void complete(Object o, Throwable throwable) { + if ( throwable != null ) { + stage.toCompletableFuture().completeExceptionally( throwable ); + } + else { + stage.toCompletableFuture().complete( null ); + } + } + + private ReactiveMutationExecutor mutationExecutor( + SharedSessionContractImplementor session, + MutationOperationGroup operationGroup) { + final MutationExecutorService mutationExecutorService = session + .getFactory() + .getServiceRegistry() + .getService( MutationExecutorService.class ); + + return (ReactiveMutationExecutor) mutationExecutorService + .createExecutor( this::getBatchKey, operationGroup, session ); + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/SoftDeleteTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/SoftDeleteTest.java new file mode 100644 index 000000000..c25b25b1e --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/SoftDeleteTest.java @@ -0,0 +1,403 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.hibernate.annotations.SoftDelete; +import org.hibernate.annotations.SoftDeleteType; +import org.hibernate.reactive.testing.SqlStatementTracker; +import org.hibernate.type.YesNoConverter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Uni; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; + +/* + * Tests validity of @SoftDelete annotation value options + * as well as verifying logged 'create table' and 'update' queries for each database + * + * @see org.hibernate.orm.test.softdelete.SimpleSoftDeleteTests + */ +public class SoftDeleteTest extends BaseReactiveTest { + + private static SqlStatementTracker sqlTracker; + + static final Deletable[] activeEntities = { + new ActiveEntity( 1, "active first" ), + new ActiveEntity( 2, "active second" ), + new ActiveEntity( 3, "active third" ), + }; + static final Deletable[] deletedEntities = { + new DeletedEntity( 1, "deleted first" ), + new DeletedEntity( 2, "deleted second" ), + new DeletedEntity( 3, "deleted third" ), + }; + static final Deletable[] implicitEntities = { + new ImplicitEntity( 1, "implicit first" ), + new ImplicitEntity( 2, "implicit second" ), + new ImplicitEntity( 3, "implicit third" ) + }; + + @Override + protected Collection> annotatedEntities() { + return List.of( ActiveEntity.class, DeletedEntity.class, ImplicitEntity.class ); + } + + @BeforeEach + public void populateDB(VertxTestContext context) { + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( activeEntities ) ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session.persistAll( deletedEntities ) ) ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session.persistAll( implicitEntities ) ) ) + ); + } + + // We need to actually empty the tables, or the populate db will fail the second time + @Override + protected CompletionStage cleanDb() { + return loop( annotatedEntities(), aClass -> getSessionFactory() + .withTransaction( s -> s.createNativeQuery( "delete from " + aClass.getSimpleName() ).executeUpdate() ) + ); + } + + @Test + public void testActiveStrategyWithYesNoConverter(VertxTestContext context) { + testSoftDelete( context, s -> s.equals( "N" ), "active", ActiveEntity.class, activeEntities, + () -> getMutinySessionFactory().withTransaction( s -> s + .remove( s.getReference( ActiveEntity.class, activeEntities[0].getId() ) ) + ) + ); + } + + @Test + public void testDeletedStrategyWithYesNoConverter(VertxTestContext context) { + testSoftDelete( context, s -> s.equals( "Y" ), "deleted", DeletedEntity.class, deletedEntities, + () -> getMutinySessionFactory().withTransaction( s -> s + .remove( s.getReference( DeletedEntity.class, deletedEntities[0].getId() ) ) + ) + ); + } + + @Test + public void testDefaults(VertxTestContext context) { + Predicate deleted = obj -> { + switch ( dbType() ) { + case DB2: + return ( (short) obj ) == 1; + case ORACLE: + return ( (Number) obj ).intValue() == 1; + default: + return (boolean) obj; + } + }; + testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities, + () -> getMutinySessionFactory().withTransaction( s -> s + .remove( s.getReference( ImplicitEntity.class, implicitEntities[0].getId() ) ) + ) + ); + } + + @Test + public void testDeletionWithHQLQuery(VertxTestContext context) { + Predicate deleted = obj -> { + switch ( dbType() ) { + case DB2: + return ( (short) obj ) == 1; + case ORACLE: + return ( (Number) obj ).intValue() == 1; + default: + return (boolean) obj; + } + }; + testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities, + () -> getMutinySessionFactory().withTransaction( s -> s + .createMutationQuery( "delete from ImplicitEntity where name = :name" ) + .setParameter( "name", implicitEntities[0].getName() ) + .executeUpdate() + ) + ); + } + + @Test + public void testDeletionWithCriteria(VertxTestContext context) { + Predicate deleted = obj -> { + switch ( dbType() ) { + case DB2: + return ( (short) obj ) == 1; + case ORACLE: + return ( (Number) obj ).intValue() == 1; + default: + return (boolean) obj; + } + }; + testSoftDelete( context, deleted, "deleted", ImplicitEntity.class, implicitEntities, + () -> getMutinySessionFactory().withTransaction( s -> { + CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder(); + CriteriaDelete delete = cb.createCriteriaDelete( ImplicitEntity.class ); + Root root = delete.from( ImplicitEntity.class ); + delete.where( cb.equal( root.get( "name" ), implicitEntities[0].getName() ) ); + return s.createQuery( delete ).executeUpdate(); + } ) + ); + } + + private void testSoftDelete( + VertxTestContext context, + Predicate deleted, + String deletedColumn, + Class entityClass, + Deletable[] entities, Supplier> deleteEntity) { + test( context, getMutinySessionFactory() + // Check that the soft delete column exists and has the expected initial value + .withSession( s -> s + // This SQL query should be compatible with all databases + .createNativeQuery( "select id, name, " + deletedColumn + " from " + entityClass.getSimpleName() + " order by id" ) + .getResultList() + .invoke( rows -> { + assertThat( rows ).hasSize( entities.length ); + for ( int i = 0; i < entities.length; i++ ) { + Object[] row = (Object[]) rows.get( i ); + Integer actualId = ( (Number) row[0] ).intValue(); + assertThat( actualId ).isEqualTo( entities[i].getId() ); + assertThat( row[1] ).isEqualTo( entities[i].getName() ); + // Only the first element should be deleted + assertThat( deleted.test( row[2] ) ).isFalse(); + } + } ) + ) + // Delete an entity + .call( deleteEntity::get ) + // Test select all + .call( () -> getMutinySessionFactory().withTransaction( s -> s + .createSelectionQuery( "from " + entityClass.getSimpleName() + " order by id", Object.class ) + .getResultList() + .invoke( list -> assertThat( list ).containsExactly( entities[1], entities[2] ) ) + ) ) + // Test find + .call( () -> getMutinySessionFactory().withTransaction( s -> s + .find( entityClass, entities[0].getId() ) + .invoke( entity -> assertThat( entity ).isNull() ) + ) ) + // Test table content with a native query + .call( () -> getMutinySessionFactory().withSession( s -> s + // This SQL query should be compatible with all databases + .createNativeQuery( "select id, name, " + deletedColumn + " from " + entityClass.getSimpleName() + " order by id" ) + .getResultList() + .invoke( rows -> { + assertThat( rows ).hasSize( entities.length ); + for ( int i = 0; i < entities.length; i++ ) { + Object[] row = (Object[]) rows.get( i ); + Integer actualId = ( (Number) row[0] ).intValue(); + assertThat( actualId ).isEqualTo( entities[i].getId() ); + assertThat( row[1] ).isEqualTo( entities[i].getName() ); + // Only the first element should have been deleted + System.out.println( Arrays.toString( row ) ); + System.out.println( "Index: " + i + ", Actual: " + deleted.test( row[2] ) + " Expected: " + ( i == 0 ) ); + assertThat( deleted.test( row[2] ) ).isEqualTo( i == 0 ); + } + } ) + ) ) + ); + } + + // The interface helps with simplifying the code for the test + private interface Deletable { + Integer getId(); + + String getName(); + } + + @Entity(name = "ActiveEntity") + @Table(name = "ActiveEntity") + @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.ACTIVE) + public static class ActiveEntity implements Deletable { + @Id + private Integer id; + private String name; + + public ActiveEntity() { + } + + public ActiveEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + ActiveEntity that = (ActiveEntity) o; + return Objects.equals( name, that.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return this.getClass() + ":" + id + ":" + name; + } + } + + @Entity(name = "DeletedEntity") + @Table(name = "DeletedEntity") + @SoftDelete(converter = YesNoConverter.class, strategy = SoftDeleteType.DELETED) + public static class DeletedEntity implements Deletable { + @Id + private Integer id; + private String name; + + public DeletedEntity() { + } + + public DeletedEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + DeletedEntity that = (DeletedEntity) o; + return Objects.equals( name, that.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return this.getClass() + ":" + id + ":" + name; + } + } + + @Entity(name = "ImplicitEntity") + @Table(name = "ImplicitEntity") + @SoftDelete + public static class ImplicitEntity implements Deletable { + @Id + private Integer id; + private String name; + + public ImplicitEntity() { + } + + public ImplicitEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + ImplicitEntity that = (ImplicitEntity) o; + return Objects.equals( name, that.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return this.getClass() + ":" + id + ":" + name; + } + } +}