From 8b0c521a055b46033d34cad26fe43fe13d08f0ba Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Jan 2018 20:36:47 +0100 Subject: [PATCH] [#472, #496, #497, #499, #443, #503] Introduced entity collection management API and made use of it for cascading deletes in entity views. Ironed out many inverse mapping issues --- .../blaze-persistence/checkstyle-config.xml | 2 +- .../persistence/CriteriaBuilderFactory.java | 67 +++ ...ingModificationCriteriaBuilderFactory.java | 62 +++ .../persistence/spi/ExtendedAttribute.java | 147 ++++++ .../persistence/spi/ExtendedManagedType.java | 69 +++ .../persistence/spi/ExtendedQuerySupport.java | 21 - .../blazebit/persistence/spi/JoinTable.java | 89 ++++ .../blazebit/persistence/spi/JpaProvider.java | 63 ++- .../persistence/spi/JpaProviderFactory.java | 2 +- .../impl/AbstractCTECriteriaBuilder.java | 32 +- .../impl/AbstractCommonQueryBuilder.java | 3 +- ...stractDeleteCollectionCriteriaBuilder.java | 148 ++++++ ...stractInsertCollectionCriteriaBuilder.java | 217 +++++++++ .../AbstractModificationCriteriaBuilder.java | 63 +-- ...stractUpdateCollectionCriteriaBuilder.java | 241 ++++++++++ .../impl/BaseInsertCriteriaBuilderImpl.java | 94 +++- .../impl/BaseUpdateCriteriaBuilderImpl.java | 50 +- .../persistence/impl/CachingJpaProvider.java | 281 ++++++++++++ .../impl/CriteriaBuilderFactoryImpl.java | 82 +++- .../DeleteCollectionCriteriaBuilderImpl.java | 32 ++ .../persistence/impl/EntityMetamodelImpl.java | 432 +++++++++++++++--- .../InsertCollectionCriteriaBuilderImpl.java | 33 ++ .../persistence/impl/JoinManager.java | 16 +- .../blazebit/persistence/impl/JoinNode.java | 2 +- .../blazebit/persistence/impl/MainQuery.java | 2 +- ...ngDeleteCollectionCriteriaBuilderImpl.java | 39 ++ ...ngInsertCollectionCriteriaBuilderImpl.java | 39 ++ ...ModificationCriteraBuilderFactoryImpl.java | 30 ++ ...ngUpdateCollectionCriteriaBuilderImpl.java | 39 ++ .../UpdateCollectionCriteriaBuilderImpl.java | 32 ++ ...nDeleteModificationQuerySpecification.java | 85 ++++ ...nInsertModificationQuerySpecification.java | 73 +++ ...nUpdateModificationQuerySpecification.java | 99 ++++ .../impl/query/CustomQuerySpecification.java | 4 +- .../query/ModificationQuerySpecification.java | 41 +- .../persistence/impl/util/SqlUtils.java | 118 ++++- .../persistence/impl/EntityMetamodel.java | 5 +- .../impl/util/JpaMetamodelUtils.java | 77 +++- .../testsuite/entity/Document.java | 2 +- .../testsuite/entity/DocumentTupleEntity.java | 5 +- .../testsuite/entity/IndexedEmbeddable.java | 16 + .../testsuite/entity/IndexedNode.java | 34 +- .../testsuite/entity/IndexedNode2.java | 3 +- .../testsuite/entity/KeyedEmbeddable.java | 16 + .../testsuite/entity/KeyedNode.java | 34 +- .../testsuite/entity/KeyedNode2.java | 3 +- .../testsuite/entity/ProjectLeader.java | 3 +- .../persistence/testsuite/entity/Root.java | 110 ++++- .../persistence/testsuite/entity/Version.java | 3 +- .../testsuite/CollectionRoleDeleteTest.java | 330 +++++++++++++ .../testsuite/CollectionRoleInsertTest.java | 343 ++++++++++++++ .../testsuite/CollectionRoleUpdateTest.java | 351 ++++++++++++++ .../builder/AbstractTreatVariationsTest.java | 48 +- .../persistence/view/CascadeType.java | 13 +- .../persistence/view/EntityViewManager.java | 45 ++ .../persistence/view/MappingInverse.java | 2 + .../persistence/view/UpdatableMapping.java | 12 + .../view/metamodel/MethodAttribute.java | 20 +- .../spi/EntityViewMethodAttributeMapping.java | 12 +- .../view/impl/EntityViewManagerImpl.java | 78 +++- .../impl/collection/CollectionAction.java | 2 +- .../collection/CollectionAddAllAction.java | 2 +- .../collection/CollectionClearAction.java | 7 +- .../collection/CollectionRemoveAllAction.java | 7 +- .../collection/CollectionRemoveListener.java | 32 ++ .../collection/CollectionRetainAllAction.java | 16 +- .../view/impl/collection/ListAction.java | 2 +- .../view/impl/collection/ListAddAction.java | 2 +- .../impl/collection/ListAddAllAction.java | 2 +- .../impl/collection/ListRemoveAction.java | 7 +- .../view/impl/collection/ListSetAction.java | 11 +- .../view/impl/collection/MapAction.java | 2 +- .../view/impl/collection/MapClearAction.java | 12 +- .../view/impl/collection/MapPutAction.java | 11 +- .../view/impl/collection/MapPutAllAction.java | 18 +- .../view/impl/collection/MapRemoveAction.java | 16 +- .../collection/MapRemoveAllEntriesAction.java | 33 +- .../collection/MapRemoveAllKeysAction.java | 30 +- .../collection/MapRemoveAllValuesAction.java | 37 +- .../impl/collection/MapRemoveEntryAction.java | 132 ------ .../impl/collection/MapRemoveValueAction.java | 144 ------ .../collection/MapRetainAllEntriesAction.java | 19 +- .../collection/MapRetainAllKeysAction.java | 25 +- .../collection/MapRetainAllValuesAction.java | 25 +- .../impl/collection/RecordingCollection.java | 4 +- .../impl/collection/RecordingEntrySet.java | 2 +- .../collection/RecordingEntrySetIterator.java | 2 +- .../impl/collection/RecordingIterator.java | 2 +- .../collection/RecordingKeySetIterator.java | 2 +- .../collection/RecordingListIterator.java | 6 +- .../view/impl/collection/RecordingMap.java | 4 +- .../collection/RecordingValuesCollection.java | 2 +- .../collection/RecordingValuesIterator.java | 2 +- .../entity/AbstractEntityToEntityMapper.java | 10 +- .../entity/AbstractViewToEntityMapper.java | 10 + .../entity/DefaultEntityToEntityMapper.java | 6 +- .../impl/entity/ElementToEntityMapper.java | 3 +- .../entity/InverseEntityToEntityMapper.java | 2 +- .../entity/InverseViewToEntityMapper.java | 46 +- .../entity/LoadOnlyEntityToEntityMapper.java | 5 +- .../entity/LoadOnlyViewToEntityMapper.java | 5 + .../impl/entity/ReferenceEntityLoader.java | 5 + .../view/impl/entity/ViewToEntityMapper.java | 1 + .../impl/metamodel/AbstractAttribute.java | 27 +- .../metamodel/AbstractMethodAttribute.java | 32 +- .../AbstractMethodPluralAttribute.java | 134 ++++-- .../AbstractMethodSingularAttribute.java | 133 ++++-- ...nnotationMethodAttributeMappingReader.java | 2 +- .../metamodel/MethodAttributeMapping.java | 11 +- .../impl/metamodel/ViewMetamodelImpl.java | 5 +- .../view/impl/proxy/ProxyFactory.java | 5 + .../view/impl/update/EntityViewUpdater.java | 2 + .../impl/update/EntityViewUpdaterImpl.java | 230 +++++++--- .../impl/update/InitialStateResetter.java | 3 + .../ResetInitialStateSynchronization.java | 38 ++ .../flush/AbstractPluralAttributeFlusher.java | 39 +- ...stractUnmappedAttributeCascadeDeleter.java | 60 +++ .../update/flush/BasicAttributeFlusher.java | 134 +++++- .../flush/CollectionAttributeFlusher.java | 227 +++++++-- .../CollectionElementAttributeFlusher.java | 41 +- .../CollectionElementFetchGraphNode.java | 2 +- .../flush/CompositeAttributeFlusher.java | 412 ++++++++++++++--- .../update/flush/DirtyAttributeFlusher.java | 17 +- .../flush/EmbeddableAttributeFlusher.java | 38 +- .../flush/EntityCollectionRemoveListener.java | 44 ++ .../flush/IndexedListAttributeFlusher.java | 10 +- ...erseCollectionElementAttributeFlusher.java | 6 +- .../impl/update/flush/InverseFlusher.java | 79 +++- .../update/flush/MapAttributeFlusher.java | 187 +++++++- ...ergeCollectionElementAttributeFlusher.java | 7 +- .../ParentReferenceAttributeFlusher.java | 4 +- ...sistCollectionElementAttributeFlusher.java | 7 +- ...ostRemoveCollectionElementByIdDeleter.java | 45 ++ .../PostRemoveCollectionElementDeleter.java | 45 ++ .../impl/update/flush/PostRemoveDeleter.java | 30 ++ ...veInverseCollectionElementByIdDeleter.java | 44 ++ .../update/flush/SubviewAttributeFlusher.java | 155 ++++++- .../impl/update/flush/TypeDescriptor.java | 39 +- .../UnmappedAttributeCascadeDeleter.java | 36 ++ .../UnmappedAttributeCascadeDeleterUtil.java | 80 ++++ .../UnmappedBasicAttributeCascadeDeleter.java | 201 ++++++++ ...ppedCollectionAttributeCascadeDeleter.java | 118 +++++ .../UnmappedMapAttributeCascadeDeleter.java | 104 +++++ ...dateCollectionElementAttributeFlusher.java | 2 +- .../update/flush/VersionAttributeFlusher.java | 20 +- .../flush/ViewCollectionRemoveListener.java | 48 ++ .../update/flush/ViewTypeCascadeDeleter.java | 55 +++ ...ocumentExtensionForElementCollections.java | 3 +- .../ExtendedDocumentForCollections.java | 3 +- .../ExtendedPersonForCollections.java | 3 +- .../ExtendedPersonForElementCollections.java | 3 +- .../entity/simple/DocumentForCollections.java | 3 +- .../entity/simple/PersonForCollections.java | 14 +- .../simple/PersonForElementCollections.java | 3 +- ...ritanceMappingInAnotherEntityViewTest.java | 2 +- .../NestedInheritanceMappingTest.java | 2 +- .../AbstractEntityViewUpdateDocumentTest.java | 25 +- .../update/AbstractEntityViewUpdateTest.java | 24 +- .../model/UpdatableDocumentBasicView.java | 3 +- ...tableDocumentBasicWithCollectionsView.java | 3 +- .../UpdatableDocumentBasicWithMapsView.java | 3 +- .../UpdatableDocumentEmbeddableView.java | 3 +- ...DocumentEmbeddableWithCollectionsView.java | 3 +- ...datableDocumentEmbeddableWithMapsView.java | 2 +- .../mutable/model/UpdatableDocumentView.java | 3 +- .../UpdatableDocumentWithCollectionsView.java | 3 +- .../model/UpdatableDocumentWithMapsView.java | 3 +- .../UpdatableNameObjectContainerView.java | 3 +- .../mutable/model/UpdatableDocumentView.java | 2 +- .../UpdatableDocumentWithCollectionsView.java | 2 +- .../model/UpdatableDocumentWithMapsView.java | 2 +- .../metamodel/EntityViewMetamodelTest.java | 9 +- .../AbstractEntityViewRemoveDocumentTest.java | 242 ++++++++++ ...iewRemoveNestedSubviewCollectionsTest.java | 163 +++++++ ...EntityViewRemoveNestedSubviewMapsTest.java | 163 +++++++ .../EntityViewRemoveNestedSubviewTest.java | 148 ++++++ .../remove/nested/model/FriendPersonView.java | 26 ++ .../remove/nested/model/PersonView.java | 35 ++ .../nested/model/UpdatableDocumentView.java | 55 +++ .../UpdatableDocumentWithCollectionsView.java | 51 +++ .../model/UpdatableDocumentWithMapsView.java | 51 +++ .../nested/model/UpdatablePersonView.java | 29 ++ .../model/UpdatableResponsiblePersonView.java | 34 ++ .../EntityViewUpdateSubviewGraphTest.java | 4 +- ...ViewUpdateSubviewInverseEmbeddedTest.java} | 40 +- .../{ => embedded}/model/IdHolderView.java | 2 +- .../model/LegacyOrderIdView.java | 2 +- .../LegacyOrderPositionDefaultIdView.java | 2 +- .../model/LegacyOrderPositionIdView.java | 6 +- ...datableLegacyOrderPositionDefaultView.java | 2 +- .../UpdatableLegacyOrderPositionView.java | 10 +- .../model/UpdatableLegacyOrderView.java | 2 +- ...tyViewUpdateSubviewInverseUmappedTest.java | 179 ++++++++ .../unmapped/model/DocumentIdView.java | 30 ++ .../inverse/unmapped/model/IdHolderView.java | 34 ++ .../inverse/unmapped/model/PersonIdView.java | 31 ++ .../unmapped/model/UpdatableDocumentView.java | 50 ++ .../unmapped/model/UpdatablePersonView.java | 38 ++ .../unmapped/model/UpdatableVersionView.java | 34 ++ .../inverse/unmapped/model/VersionIdView.java | 33 ++ .../DataNucleusExtendedQuerySupport.java | 29 +- .../datanucleus/DataNucleusJpaProvider.java | 82 +++- ...NucleusEntityManagerFactoryIntegrator.java | 2 +- .../src/test/resources/logging.properties | 2 +- .../eclipselink/EclipseLinkJpaProvider.java | 77 +++- .../EclipseLinkEntityManagerIntegrator.java | 2 +- ...rnate42EntityManagerFactoryIntegrator.java | 4 +- ...rnate43EntityManagerFactoryIntegrator.java | 4 +- ...rnate52EntityManagerFactoryIntegrator.java | 4 +- ...ernate5EntityManagerFactoryIntegrator.java | 4 +- ...rnate60EntityManagerFactoryIntegrator.java | 4 +- .../HibernateExtendedQuerySupport.java | 94 ---- .../hibernate/HibernateJpa21Provider.java | 71 ++- .../impl/hibernate/HibernateJpaProvider.java | 285 ++++++++---- .../impl/openjpa/OpenJPAJpaProvider.java | 36 +- .../spring/data/impl/entity/Document.java | 3 +- .../base/AbstractPersistenceTest.java | 7 +- .../base/AbstractJpaPersistenceTest.java | 71 +-- .../AbstractAssertStatementBuilder.java | 9 + .../AssertDeleteStatementBuilder.java | 9 - .../AssertInsertStatementBuilder.java | 9 - .../base/assertion/AssertMultiStatement.java | 64 +++ .../AssertMultiStatementBuilder.java | 56 +++ .../assertion/AssertStatementBuilder.java | 24 +- .../AssertUpdateStatementBuilder.java | 9 - 225 files changed, 9290 insertions(+), 1381 deletions(-) create mode 100644 core/api/src/main/java/com/blazebit/persistence/spi/ExtendedAttribute.java create mode 100644 core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java create mode 100644 core/api/src/main/java/com/blazebit/persistence/spi/JoinTable.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/AbstractDeleteCollectionCriteriaBuilder.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/AbstractInsertCollectionCriteriaBuilder.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/AbstractUpdateCollectionCriteriaBuilder.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/CachingJpaProvider.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/DeleteCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/InsertCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/ReturningDeleteCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/ReturningInsertCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/ReturningUpdateCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/UpdateCollectionCriteriaBuilderImpl.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionDeleteModificationQuerySpecification.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionInsertModificationQuerySpecification.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionUpdateModificationQuerySpecification.java create mode 100644 core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleDeleteTest.java create mode 100644 core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleInsertTest.java create mode 100644 core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleUpdateTest.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveListener.java delete mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveEntryAction.java delete mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveValueAction.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractUnmappedAttributeCascadeDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EntityCollectionRemoveListener.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementByIdDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveInverseCollectionElementByIdDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleterUtil.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedBasicAttributeCascadeDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedCollectionAttributeCascadeDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedMapAttributeCascadeDeleter.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewCollectionRemoveListener.java create mode 100644 entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewTypeCascadeDeleter.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/AbstractEntityViewRemoveDocumentTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewCollectionsTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewMapsTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/FriendPersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/PersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithCollectionsView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithMapsView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatablePersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableResponsiblePersonView.java rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{EntityViewUpdateSubviewInverseTest.java => embedded/EntityViewUpdateSubviewInverseEmbeddedTest.java} (84%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/IdHolderView.java (97%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/LegacyOrderIdView.java (97%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/LegacyOrderPositionDefaultIdView.java (98%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/LegacyOrderPositionIdView.java (86%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/UpdatableLegacyOrderPositionDefaultView.java (98%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/UpdatableLegacyOrderPositionView.java (78%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/{ => embedded}/model/UpdatableLegacyOrderView.java (98%) create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/EntityViewUpdateSubviewInverseUmappedTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/DocumentIdView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/IdHolderView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/PersonIdView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableDocumentView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatablePersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableVersionView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/VersionIdView.java create mode 100644 testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatement.java create mode 100644 testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatementBuilder.java diff --git a/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-config.xml b/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-config.xml index bdcc52588c..a6f770bd3b 100644 --- a/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-config.xml +++ b/checkstyle-rules/src/main/resources/blaze-persistence/checkstyle-config.xml @@ -123,7 +123,7 @@ - + diff --git a/core/api/src/main/java/com/blazebit/persistence/CriteriaBuilderFactory.java b/core/api/src/main/java/com/blazebit/persistence/CriteriaBuilderFactory.java index ec89884b6d..b8e496c347 100644 --- a/core/api/src/main/java/com/blazebit/persistence/CriteriaBuilderFactory.java +++ b/core/api/src/main/java/com/blazebit/persistence/CriteriaBuilderFactory.java @@ -100,6 +100,33 @@ public interface CriteriaBuilderFactory extends ServiceProvider, ConfigurationSo */ public DeleteCriteriaBuilder delete(EntityManager entityManager, Class deleteClass, String alias); + /** + * Like {@link CriteriaBuilderFactory#deleteCollection(javax.persistence.EntityManager, java.lang.Class, java.lang.String, java.lang.String)} but with the alias + * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the delete owner class returns. + * + * @param entityManager The entity manager to use for the delete criteria builder + * @param deleteOwnerClass The entity class owning the collection for the delete criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the delete criteria + * @return A new delete criteria builder + * @since 1.2.0 + */ + public DeleteCriteriaBuilder deleteCollection(EntityManager entityManager, Class deleteOwnerClass, String collectionName); + + /** + * Creates a new delete criteria builder for the given entity class and collection name to delete elements of the + * entity class's collection. + * + * @param entityManager The entity manager to use for the delete criteria builder + * @param deleteOwnerClass The entity class owning the collection for the delete criteria + * @param alias The alias that should be used for the entity + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the delete criteria + * @return A new delete criteria builder + * @since 1.2.0 + */ + public DeleteCriteriaBuilder deleteCollection(EntityManager entityManager, Class deleteOwnerClass, String alias, String collectionName); + /** * Like {@link CriteriaBuilderFactory#update(javax.persistence.EntityManager, java.lang.Class, java.lang.String)} but with the alias * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the update class returns. @@ -124,6 +151,33 @@ public interface CriteriaBuilderFactory extends ServiceProvider, ConfigurationSo */ public UpdateCriteriaBuilder update(EntityManager entityManager, Class updateClass, String alias); + /** + * Like {@link CriteriaBuilderFactory#updateCollection(javax.persistence.EntityManager, java.lang.Class, java.lang.String, java.lang.String)} but with the alias + * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the delete owner class returns. + * + * @param entityManager The entity manager to use for the update criteria builder + * @param updateOwnerClass The entity class owning the collection for the update criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the update criteria + * @return A new update criteria builder + * @since 1.2.0 + */ + public UpdateCriteriaBuilder updateCollection(EntityManager entityManager, Class updateOwnerClass, String collectionName); + + /** + * Creates a new update criteria builder for the given entity class and collection name to update elements of the + * entity class's collection. + * + * @param entityManager The entity manager to use for the update criteria builder + * @param updateOwnerClass The entity class owning the collection for the update criteria + * @param alias The alias that should be used for the entity + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the update criteria + * @return A new update criteria builder + * @since 1.2.0 + */ + public UpdateCriteriaBuilder updateCollection(EntityManager entityManager, Class updateOwnerClass, String alias, String collectionName); + /** * Creates a new insert criteria builder for the given entity class. * @@ -134,4 +188,17 @@ public interface CriteriaBuilderFactory extends ServiceProvider, ConfigurationSo * @since 1.1.0 */ public InsertCriteriaBuilder insert(EntityManager entityManager, Class insertClass); + + /** + * Creates a new insert criteria builder for the given entity class and collection name to update elements of the + * entity class's collection. + * + * @param entityManager The entity manager to use for the insert criteria builder + * @param insertOwnerClass The entity class owning the collection for the insert criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the insert criteria + * @return A new insert criteria builder + * @since 1.2.0 + */ + public InsertCriteriaBuilder insertCollection(EntityManager entityManager, Class insertOwnerClass, String collectionName); } diff --git a/core/api/src/main/java/com/blazebit/persistence/ReturningModificationCriteriaBuilderFactory.java b/core/api/src/main/java/com/blazebit/persistence/ReturningModificationCriteriaBuilderFactory.java index 59f3dcfe98..c3c2c3a319 100644 --- a/core/api/src/main/java/com/blazebit/persistence/ReturningModificationCriteriaBuilderFactory.java +++ b/core/api/src/main/java/com/blazebit/persistence/ReturningModificationCriteriaBuilderFactory.java @@ -47,6 +47,31 @@ public interface ReturningModificationCriteriaBuilderFactory { */ public ReturningDeleteCriteriaBuilder delete(Class deleteClass, String alias); + /** + * Like {@link ReturningModificationCriteriaBuilderFactory#deleteCollection(java.lang.Class, java.lang.String, java.lang.String)} but with the alias + * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the delete owner class returns. + * + * @param deleteOwnerClass The entity class owning the collection for the delete criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the delete criteria + * @return A new delete criteria builder + * @since 1.2.0 + */ + public ReturningDeleteCriteriaBuilder deleteCollection(Class deleteOwnerClass, String collectionName); + + /** + * Creates a new delete criteria builder for the given entity class and collection name to delete elements of the + * entity class's collection. + * + * @param deleteOwnerClass The entity class owning the collection for the delete criteria + * @param alias The alias that should be used for the entity + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the delete criteria + * @return A new delete criteria builder + * @since 1.2.0 + */ + public ReturningDeleteCriteriaBuilder deleteCollection(Class deleteOwnerClass, String alias, String collectionName); + /** * Like {@link ReturningModificationCriteriaBuilderFactory#update(java.lang.Class, java.lang.String)} but with the alias * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the update class returns. @@ -69,6 +94,31 @@ public interface ReturningModificationCriteriaBuilderFactory { */ public ReturningUpdateCriteriaBuilder update(Class updateClass, String alias); + /** + * Like {@link CriteriaBuilderFactory#updateCollection(javax.persistence.EntityManager, java.lang.Class, java.lang.String, java.lang.String)} but with the alias + * equivalent to the camel cased result of what {@link Class#getSimpleName()} of the delete owner class returns. + * + * @param updateOwnerClass The entity class owning the collection for the update criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the update criteria + * @return A new update criteria builder + * @since 1.2.0 + */ + public ReturningUpdateCriteriaBuilder updateCollection(Class updateOwnerClass, String collectionName); + + /** + * Creates a new update criteria builder for the given entity class and collection name to update elements of the + * entity class's collection. + * + * @param updateOwnerClass The entity class owning the collection for the update criteria + * @param alias The alias that should be used for the entity + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the update criteria + * @return A new update criteria builder + * @since 1.2.0 + */ + public ReturningUpdateCriteriaBuilder updateCollection(Class updateOwnerClass, String alias, String collectionName); + /** * Creates a new insert criteria builder for the given entity class. * @@ -78,4 +128,16 @@ public interface ReturningModificationCriteriaBuilderFactory { * @since 1.1.0 */ public ReturningInsertCriteriaBuilder insert(Class insertClass); + + /** + * Creates a new insert criteria builder for the given entity class and collection name to update elements of the + * entity class's collection. + * + * @param insertOwnerClass The entity class owning the collection for the insert criteria + * @param collectionName The name of the collection contained in the owner entity class + * @param The type of the entity for the insert criteria + * @return A new insert criteria builder + * @since 1.2.0 + */ + public ReturningInsertCriteriaBuilder insertCollection(Class insertOwnerClass, String collectionName); } diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedAttribute.java b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedAttribute.java new file mode 100644 index 0000000000..6938a481af --- /dev/null +++ b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedAttribute.java @@ -0,0 +1,147 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.spi; + +import com.blazebit.persistence.JoinType; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import java.util.List; +import java.util.Map; + +/** + * This is a wrapper around the JPA {@link javax.persistence.metamodel.Attribute} that allows additionally efficient access to properties of the metamodel. + * + * @param The Java type represented by the managed type owning the attribute + * @param The Java element type of the attribute + * @author Christian Beikov + * @since 1.2.0 + */ +public interface ExtendedAttribute { + + /** + * Returns the underlying attribute. + * + * @return The attribute + */ + public Attribute getAttribute(); + + /** + * Returns the path from the owning entity type to this attribute. + * + * @return The path to the attribute + */ + public List> getAttributePath(); + + /** + * Returns the element type of the attribute. + * + * @return The element type + */ + public Class getElementClass(); + + /** + * Returns whether the type of the attribute causes a cascading delete cycle. + * + * @return True if it has a cascading delete cycle, false otherwise + */ + public boolean hasCascadingDeleteCycle(); + + /** + * Whether the join columns for the attribute are in a foreign table. + * + * @return True if join columns are in a foreign table, false otherwise + */ + public boolean isForeignJoinColumn(); + + /** + * Whether columns for the attribute are shared between multiple subtypes + * or shared by occupying the same slot in the resulting SQL. + * + * @return True if columns of the attribute are shared, false otherwise + */ + public boolean isColumnShared(); + + /** + * Whether the attribute is a non-indexed and non-ordered collection a.k.a. a bag. + * + * @return True if it is a bag, false otherwise + */ + public boolean isBag(); + + /** + * Whether orphan removal is activated for the attribute. + * + * @return True if orphan removal is activated, else false + */ + public boolean isOrphanRemoval(); + + /** + * Whether delete cascading is activated for the attribute. + * + * @return True if delete cascading is activated, else false + */ + public boolean isDeleteCascaded(); + + /** + * Returns where to put treat filters for a treat joined association of this attribute. + * + * @param joinType The join type used for the treat join + * @return The constraint type for the treat filter + */ + public JpaProvider.ConstraintType getJoinTypeIndexedRequiresTreatFilter(JoinType joinType); + + /** + * If the attribute is insertable = false and updatable = false it returns the writable mappings for the inverse type. + * Otherwise returns null. + * + * @param inverseType The type containing the inverse relation + * @return The writable mappings for the inverse type if the attribute is not insertable or updatable, null otherwise + */ + public Map getWritableMappedByMappings(EntityType inverseType); + + /** + * If the attribute is an inverse collection, the mapped by attribute name is returned. + * Otherwise returns null. + * + * @return The mapped by attribute name if the attribute is an inverse collection, null otherwise + */ + public String getMappedBy(); + + /** + * If the attribute is a collection that uses a join table, returns it's descriptor. + * Otherwise returns null. + * + * @return The join table information if the attribute has one, null otherwise + */ + public JoinTable getJoinTable(); + + /** + * Returns the column names of the attribute. + * + * @return The column names of the attribute + */ + public String[] getColumnNames(); + + /** + * Returns the SQL column type names of the attribute. + * + * @return The SQL column type names for the attribute + */ + public String[] getColumnTypes(); +} + diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java new file mode 100644 index 0000000000..ff43225cf5 --- /dev/null +++ b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.spi; + +import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.SingularAttribute; +import java.util.Map; + +/** + * This is a wrapper around the JPA {@link javax.persistence.metamodel.ManagedType} that allows additionally efficient access to properties of the metamodel. + * + * @param The Java type represented by this managed type + * @author Christian Beikov + * @since 1.2.0 + */ +public interface ExtendedManagedType { + + /** + * Returns the underlying managed type. + * + * @return The managed type + */ + public ManagedType getType(); + + /** + * Returns whether the type has a cascading delete cycle. + * + * @return True if it has a cascading delete cycle, false otherwise + */ + public boolean hasCascadingDeleteCycle(); + + /** + * Returns the id attribute if it has one, otherwise null. + * + * @return The id attribute or null + */ + public SingularAttribute getIdAttribute(); + + /** + * Returns the extended attributes of the managed type. + * + * @return The extended attributes + */ + public Map> getAttributes(); + + /** + * Returns the extended attribute of the managed type for the given attribute name. + * + * @param attributeName The attribute name + * @return The extended attributes + * @throws IllegalArgumentException Is thrown when the attribute doesn't exist + */ + public ExtendedAttribute getAttribute(String attributeName); +} + diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedQuerySupport.java b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedQuerySupport.java index a8a8e9acb6..cb1c61ef82 100644 --- a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedQuerySupport.java +++ b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedQuerySupport.java @@ -20,7 +20,6 @@ import javax.persistence.EntityManager; import javax.persistence.Query; -import javax.persistence.metamodel.EntityType; import java.util.List; /** @@ -58,26 +57,6 @@ public interface ExtendedQuerySupport { */ public List getCascadingDeleteSql(EntityManager em, Query query); - /** - * Returns the column names of the attribute of the given entity type. - * - * @param em The current entity manager - * @param entityType The entity type - * @param attributeName The attribute name - * @return The column names of the attribute - */ - public String[] getColumnNames(EntityManager em, EntityType entityType, String attributeName); - - /** - * Returns the SQL column type names of the given attribute of the given entity type. - * - * @param em The current entity manager - * @param entityType The entity type - * @param attributeName The attribute name - * @return The SQL column type names for the attribute - */ - public String[] getColumnTypes(EntityManager em, EntityType entityType, String attributeName); - /** * Returns the SQL table alias of the JPQL from node alias in the given query. * diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/JoinTable.java b/core/api/src/main/java/com/blazebit/persistence/spi/JoinTable.java new file mode 100644 index 0000000000..3a3a0280cc --- /dev/null +++ b/core/api/src/main/java/com/blazebit/persistence/spi/JoinTable.java @@ -0,0 +1,89 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.spi; + +import java.util.Map; + +/** + * A structure for accessing join table information. + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class JoinTable { + + private final String tableName; + private final Map idColumnMappings; + private final Map keyColumnMappings; + private final Map targetIdColumnMappings; + + /** + * Constructs a JoinTable. + * + * @param tableName The join table name + * @param idColumnMappings The id column mappings + * @param keyColumnMappings The key column mappings + * @param targetIdColumnMappings The target id column mappings + */ + public JoinTable(String tableName, Map idColumnMappings, Map keyColumnMappings, Map targetIdColumnMappings) { + this.tableName = tableName; + this.idColumnMappings = idColumnMappings; + this.keyColumnMappings = keyColumnMappings; + this.targetIdColumnMappings = targetIdColumnMappings; + } + + /** + * The name of the join table. + * + * @return The join table name + */ + public String getTableName() { + return tableName; + } + + /** + * Returns the foreign key mappings of the join tables column names to the owner table column names. + * The keys are column names of the join table. Values are the column names of the owner table. + * + * @return The id column mappings + */ + public Map getIdColumnMappings() { + return idColumnMappings; + } + + /** + * Returns the column mappings of the key/index column if there are any, otherwise null. + * In case of a simple key/index type, this returns the column name mapping to itself. + * Otherwise returns foreign key mappings of the join tables key column names to the key target table column names. + * The keys are column names of the join table. Values are the column names of the key target table. + * + * @return The key column mappings + */ + public Map getKeyColumnMappings() { + return keyColumnMappings; + } + + /** + * Returns the foreign key mappings of the join tables column names to the target table column names. + * The keys are column names of the join table. Values are the column names of the target table. + * + * @return The target id column mappings + */ + public Map getTargetColumnMappings() { + return targetIdColumnMappings; + } +} diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/JpaProvider.java b/core/api/src/main/java/com/blazebit/persistence/spi/JpaProvider.java index 832c75ec3d..671d894a0b 100644 --- a/core/api/src/main/java/com/blazebit/persistence/spi/JpaProvider.java +++ b/core/api/src/main/java/com/blazebit/persistence/spi/JpaProvider.java @@ -20,6 +20,7 @@ import javax.persistence.EntityManager; import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.ManagedType; import java.util.Map; /** @@ -237,8 +238,26 @@ public interface JpaProvider { public boolean isColumnShared(EntityType ownerType, String attributeName); /** - * Whether the association defined by owner type and attribute name, when treat joined requires a type filter to be applied via an ON - * clause to correctly filter subtypes. This is for JPA providers that don't correctly filter the types. + * Returns the column names of the attribute of the given entity type. + * + * @param ownerType The owner of the attribute + * @param attributeName The attribute name + * @return The column names of the attribute + */ + public String[] getColumnNames(EntityType ownerType, String attributeName); + + /** + * Returns the SQL column type names of the given attribute of the given entity type. + * + * @param ownerType The owner of the attribute + * @param attributeName The attribute name + * @return The SQL column type names for the attribute + */ + public String[] getColumnTypes(EntityType ownerType, String attributeName); + + /** + * Returns where to put treat filters for a treat joined association of this attribute. + * This is for JPA providers that don't correctly filter the types. * * Hibernate for example does not automatically add the type constraint to treat joins of a type that is uses * the table per class inheritance strategy. @@ -246,7 +265,7 @@ public interface JpaProvider { * @param ownerType The declaring type of the attribute to check * @param attributeName The attribute name for which to check the treat filter requirement or null * @param joinType The join type used for the treat join - * @return True if a treat filter is required, false otherwise + * @return The constraint type for the treat filter */ public ConstraintType requiresTreatFilter(EntityType ownerType, String attributeName, JoinType joinType); @@ -272,14 +291,14 @@ public interface JpaProvider { public Map getWritableMappedByMappings(EntityType inverseType, EntityType ownerType, String attributeName); /** - * If the given attribute is a collection that uses a join table, returns it's name. + * If the given attribute is a collection that uses a join table, returns it's descriptor. * Otherwise returns null. * * @param ownerType The declaring type of the attribute to check * @param attributeName The name of the attribute for which to retrieve the join table name - * @return The join table name if the attribute uses one, null otherwise + * @return The join table information if the attribute has one, null otherwise */ - public String getJoinTable(EntityType ownerType, String attributeName); + public JoinTable getJoinTable(EntityType ownerType, String attributeName); /** * Whether the given attribute is a non-indexed and non-ordered collection a.k.a. a bag. @@ -290,6 +309,24 @@ public interface JpaProvider { */ public boolean isBag(EntityType ownerType, String attributeName); + /** + * Whether orphan removal is activated for the given attribute. + * + * @param ownerType The declaring type of the attribute to check + * @param attributeName The name of the attribute to check + * @return True if orphan removal is activated, else false + */ + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName); + + /** + * Whether delete cascading is activated for the given attribute. + * + * @param ownerType The declaring type of the attribute to check + * @param attributeName The name of the attribute to check + * @return True if delete cascading is activated, else false + */ + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName); + /** * Returns whether the entity with the id is contained in the entity managers persistence context. * @@ -367,6 +404,20 @@ public interface JpaProvider { */ public boolean needsTypeConstraintForColumnSharing(); + /** + * Indicates whether the provider clears collection table entries on bulk delete operations. + * + * @return true if supported, else false + */ + public boolean supportsCollectionTableCleanupOnDelete(); + + /** + * Indicates whether the provider clears join table entries on bulk delete operations. + * + * @return true if supported, else false + */ + public boolean supportsJoinTableCleanupOnDelete(); + /** * The possible locations of a constraint. * diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/JpaProviderFactory.java b/core/api/src/main/java/com/blazebit/persistence/spi/JpaProviderFactory.java index 3188a05ae3..a662371c80 100644 --- a/core/api/src/main/java/com/blazebit/persistence/spi/JpaProviderFactory.java +++ b/core/api/src/main/java/com/blazebit/persistence/spi/JpaProviderFactory.java @@ -30,7 +30,7 @@ public interface JpaProviderFactory { /** * Create a {@link JpaProvider} for the given entity manager. * - * @param em The entity manager to use for creating the provider + * @param em The optional entity manager to use for creating the provider * @return The jpa provider instance */ public JpaProvider createJpaProvider(EntityManager em); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java index abeac76795..d990e0c322 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java @@ -28,6 +28,8 @@ import com.blazebit.persistence.impl.query.QuerySpecification; import com.blazebit.persistence.impl.util.JpaMetamodelUtils; import com.blazebit.persistence.spi.DbmsStatementType; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; import com.blazebit.persistence.spi.SetOperationType; import javax.persistence.Query; @@ -55,7 +57,7 @@ public abstract class AbstractCTECriteriaBuilder cteType; - protected final Map> attributeColumnMappings; + protected final Map attributeEntries; protected final Map bindingMap; protected final Map columnBindingMap; protected final CTEBuilderListenerImpl subListener; @@ -67,10 +69,10 @@ public AbstractCTECriteriaBuilder(MainQuery mainQuery, String cteName, Class(attributeColumnMappings.size()); - this.columnBindingMap = new LinkedHashMap(attributeColumnMappings.size()); + this.bindingMap = new LinkedHashMap(attributeEntries.size()); + this.columnBindingMap = new LinkedHashMap(attributeEntries.size()); this.subListener = new CTEBuilderListenerImpl(); } @@ -136,18 +138,19 @@ protected Query getQuery() { } public SelectBuilder bind(String cteAttribute) { - Map.Entry attributeEntry = attributeColumnMappings.get(cteAttribute); + ExtendedAttribute attributeEntry = attributeEntries.get(cteAttribute); if (attributeEntry == null) { - if (cteType.getAttribute(cteAttribute) != null) { - throw new IllegalArgumentException("Can't bind the embeddable cte attribute [" + cteAttribute + "] directly! Please bind the respective sub attributes."); - } throw new IllegalArgumentException("The cte attribute [" + cteAttribute + "] does not exist!"); } + // TODO: move this check and a type check to end() +// if (attributeEntry.getAttribute().getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { +// throw new IllegalArgumentException("Can't bind the embeddable cte attribute [" + cteAttribute + "] directly! Please bind the respective sub attributes."); +// } if (bindingMap.put(cteAttribute, selectManager.getSelectInfos().size()) != null) { throw new IllegalArgumentException("The cte attribute [" + cteAttribute + "] has already been bound!"); } - for (String column : attributeEntry.getValue()) { + for (String column : attributeEntry.getColumnNames()) { if (columnBindingMap.put(column, cteAttribute) != null) { throw new IllegalArgumentException("The cte column [" + column + "] has already been bound!"); } @@ -187,12 +190,13 @@ protected List prepareAndGetAttributes() { for (Map.Entry bindingEntry : bindingMap.entrySet()) { final String attributeName = bindingEntry.getKey(); - AttributePath attributePath = attributeColumnMappings.get(attributeName).getKey(); + ExtendedAttribute attributeEntry = attributeEntries.get(attributeName); + List> attributePath = attributeEntry.getAttributePath(); attributes.add(attributeName); - if (JpaMetamodelUtils.isJoinable(attributePath.getAttributes().get(attributePath.getAttributes().size() - 1))) { + if (JpaMetamodelUtils.isJoinable(attributePath.get(attributePath.size() - 1))) { // We have to map *-to-one relationships to their ids - EntityType type = mainQuery.metamodel.entity(attributePath.getAttributeClass()); + EntityType type = mainQuery.metamodel.entity(attributeEntry.getElementClass()); Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(type); // NOTE: Since we are talking about *-to-ones, the expression can only be a path to an object // so it is safe to just append the id to the path @@ -218,8 +222,8 @@ protected List prepareAndGetAttributes() { protected List prepareAndGetColumnNames() { StringBuilder sb = null; - for (Map.Entry entry : attributeColumnMappings.values()) { - for (String column : entry.getValue()) { + for (ExtendedAttribute entry : attributeEntries.values()) { + for (String column : entry.getColumnNames()) { if (!columnBindingMap.containsKey(column)) { if (sb == null) { sb = new StringBuilder(); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java index b3762d5e19..abe2573523 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java @@ -68,6 +68,7 @@ import com.blazebit.persistence.spi.DbmsDialect; import com.blazebit.persistence.spi.DbmsModificationState; import com.blazebit.persistence.spi.DbmsStatementType; +import com.blazebit.persistence.spi.ExtendedManagedType; import com.blazebit.persistence.spi.JpaProvider; import com.blazebit.persistence.spi.JpqlFunction; import com.blazebit.persistence.spi.JpqlMacro; @@ -1651,7 +1652,7 @@ protected List getCteNodes(Query baseQuery, boolean isSubquery) { List list = new ArrayList<>(columnNames.size()); for (int i = 0; i < columnNames.size(); i++) { - String[] columns = cbf.getExtendedQuerySupport().getColumnNames(em, cteInfo.cteType, columnNames.get(i)); + String[] columns = mainQuery.metamodel.getManagedType(ExtendedManagedType.class, cteInfo.cteType.getJavaType()).getAttribute(columnNames.get(i)).getColumnNames(); for (String column : columns) { list.add(column); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractDeleteCollectionCriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractDeleteCollectionCriteriaBuilder.java new file mode 100644 index 0000000000..37850720d4 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractDeleteCollectionCriteriaBuilder.java @@ -0,0 +1,148 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.BaseDeleteCriteriaBuilder; +import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.ReturningBuilder; +import com.blazebit.persistence.impl.function.entity.ValuesEntity; +import com.blazebit.persistence.impl.query.CTENode; +import com.blazebit.persistence.impl.query.CollectionDeleteModificationQuerySpecification; +import com.blazebit.persistence.impl.query.CustomSQLQuery; +import com.blazebit.persistence.impl.query.QuerySpecification; +import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.spi.DbmsModificationState; +import com.blazebit.persistence.spi.ExtendedQuerySupport; +import com.blazebit.persistence.spi.JoinTable; + +import javax.persistence.Query; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class AbstractDeleteCollectionCriteriaBuilder, Y> extends BaseDeleteCriteriaBuilderImpl { + + private final String collectionName; + + public AbstractDeleteCollectionCriteriaBuilder(MainQuery mainQuery, boolean isMainQuery, Class clazz, String alias, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, isMainQuery, clazz, alias, cteName, cteClass, result, listener); + this.collectionName = collectionName; + // Add the join here so that references in the where clause go the the expected join node + // Also, this validates the collection actually exists + joinManager.join(entityAlias + "." + collectionName, CollectionDeleteModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS, JoinType.LEFT, false, true); + } + + @Override + protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { + if (externalRepresentation) { + sbSelectFrom.append("DELETE FROM "); + sbSelectFrom.append(entityType.getName()); + sbSelectFrom.append('(').append(collectionName).append(") "); + sbSelectFrom.append(entityAlias); + appendWhereClause(sbSelectFrom); + } else { + // The internal representation is just a "hull" to hold the parameters at the appropriate positions + sbSelectFrom.append("SELECT 1 FROM "); + sbSelectFrom.append(entityType.getName()); + sbSelectFrom.append(' '); + sbSelectFrom.append(entityAlias); + sbSelectFrom.append(" LEFT JOIN "); + sbSelectFrom.append(entityAlias).append('.').append(collectionName) + .append(' ').append(CollectionDeleteModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + appendWhereClause(sbSelectFrom); + } + } + + @Override + protected Query getQuery(Map includedModificationStates) { + Query query = em.createQuery(getBaseQueryStringWithCheck()); + Set parameterListNames = parameterManager.getParameterListNames(query); + + boolean isEmbedded = this instanceof ReturningBuilder; + String[] returningColumns = getReturningColumns(); + boolean shouldRenderCteNodes = renderCteNodes(isEmbedded); + List ctes = shouldRenderCteNodes ? getCteNodes(query, isEmbedded) : Collections.EMPTY_LIST; + + // Prepare a Map + // This is used to replace references to id columns properly in the final sql query + ExtendedQuerySupport extendedQuerySupport = getService(ExtendedQuerySupport.class); + String sql = extendedQuerySupport.getSql(em, query); + String ownerAlias = extendedQuerySupport.getSqlAlias(em, query, entityAlias); + String targetAlias = extendedQuerySupport.getSqlAlias(em, query, CollectionDeleteModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + JoinTable joinTable = jpaProvider.getJoinTable(entityType, collectionName); + if (joinTable == null) { + throw new IllegalStateException("Deleting inverse collections is not supported!"); + } + int joinTableIndex = SqlUtils.indexOfTableName(sql, joinTable.getTableName()); + String collectionAlias = SqlUtils.extractAlias(sql, joinTableIndex + joinTable.getTableName().length()); + + String deleteSql = "delete from " + joinTable.getTableName() + " " + collectionAlias; + Map columnExpressionRemappings = new HashMap<>(joinTable.getIdColumnMappings().size()); + for (Map.Entry entry : joinTable.getIdColumnMappings().entrySet()) { + columnExpressionRemappings.put(ownerAlias + "." + entry.getValue(), collectionAlias + "." + entry.getKey()); + } + for (Map.Entry entry : joinTable.getTargetColumnMappings().entrySet()) { + columnExpressionRemappings.put(targetAlias + "." + entry.getValue(), collectionAlias + "." + entry.getKey()); + } + + QuerySpecification querySpecification = new CollectionDeleteModificationQuerySpecification( + this, + query, + getCountExampleQuery(), + parameterListNames, + mainQuery.cteManager.isRecursive(), + ctes, + shouldRenderCteNodes, + isEmbedded, + returningColumns, + includedModificationStates, + returningAttributeBindingMap, + getDeleteExampleQuery(), + deleteSql, + columnExpressionRemappings + ); + + query = new CustomSQLQuery( + querySpecification, + query, + parameterManager.getValuesParameters(), + parameterManager.getValuesBinders() + ); + + parameterManager.parameterizeQuery(query); + + return query; + } + + protected Query getDeleteExampleQuery() { + // This is the query we use as "hull" to put other sqls into + // We chose ValuesEntity as deletion base because it is known to be non-polymorphic + // We could have used the owner entity type as well, but at the time of writing, + // it wasn't clear if problems might arise when the entity type were polymorphic + String exampleQueryString = "DELETE FROM " + ValuesEntity.class.getSimpleName(); + return em.createQuery(exampleQueryString); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractInsertCollectionCriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractInsertCollectionCriteriaBuilder.java new file mode 100644 index 0000000000..ad5d34e617 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractInsertCollectionCriteriaBuilder.java @@ -0,0 +1,217 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.BaseInsertCriteriaBuilder; +import com.blazebit.persistence.ReturningBuilder; +import com.blazebit.persistence.impl.function.entity.ValuesEntity; +import com.blazebit.persistence.impl.query.CTENode; +import com.blazebit.persistence.impl.query.CollectionInsertModificationQuerySpecification; +import com.blazebit.persistence.impl.query.CustomSQLQuery; +import com.blazebit.persistence.impl.query.EntityFunctionNode; +import com.blazebit.persistence.impl.query.QuerySpecification; +import com.blazebit.persistence.impl.util.JpaMetamodelUtils; +import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.spi.DbmsModificationState; +import com.blazebit.persistence.spi.ExtendedQuerySupport; +import com.blazebit.persistence.spi.JoinTable; + +import javax.persistence.Query; +import javax.persistence.metamodel.Attribute; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class AbstractInsertCollectionCriteriaBuilder, Y> extends BaseInsertCriteriaBuilderImpl { + + private static final String COLLECTION_BASE_QUERY_ALIAS = "_collection"; + private final String collectionName; + + public AbstractInsertCollectionCriteriaBuilder(MainQuery mainQuery, boolean isMainQuery, Class clazz, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, isMainQuery, clazz, cteName, cteClass, result, listener); + this.collectionName = collectionName; + // TODO: validate the collection name exists + } + + @Override + protected void appendInsertIntoFragment(StringBuilder sbSelectFrom, boolean externalRepresentation) { + super.appendInsertIntoFragment(sbSelectFrom, externalRepresentation); + if (externalRepresentation) { + sbSelectFrom.append('.').append(collectionName); + } + } + + @Override + protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { + if (externalRepresentation) { + super.buildBaseQueryString(sbSelectFrom, externalRepresentation); + } else { + buildSelectBaseQueryString(sbSelectFrom, externalRepresentation); + } + } + + @Override + protected void addBind(String attributeName) { + AttributePath attributePath = JpaMetamodelUtils.getJoinTableCollectionAttributePath(getMetamodel(), entityType, attributeName, collectionName); + StringBuilder sb = new StringBuilder(); + List> attributes = attributePath.getAttributes(); + Attribute attribute = attributes.get(0); + // Replace the collection name with the alias for easier processing + if (attribute instanceof QualifiedAttribute) { + sb.append(((QualifiedAttribute) attribute).getQualificationExpression()); + sb.append('('); + sb.append(COLLECTION_BASE_QUERY_ALIAS); + sb.append(')'); + } else if (collectionName.equals(attribute.getName())) { + sb.append(COLLECTION_BASE_QUERY_ALIAS); + } else { + sb.append(entityAlias).append('.'); + sb.append(attribute.getName()); + } + for (int i = 1; i < attributes.size(); i++) { + attribute = attributes.get(i); + sb.append('.'); + sb.append(attribute.getName()); + } + attributeName = sb.toString(); + Integer attributeBindIndex = bindingMap.get(attributeName); + + if (attributeBindIndex != null) { + throw new IllegalArgumentException("The attribute [" + attributeName + "] has already been bound!"); + } + + bindingMap.put(attributeName, selectManager.getSelectInfos().size()); + } + + @Override + protected Query getQuery(Map includedModificationStates) { + Query query = em.createQuery(getBaseQueryStringWithCheck()); + Set parameterListNames = parameterManager.getParameterListNames(query); + Set keyRestrictedLeftJoins = joinManager.getKeyRestrictedLeftJoins(); + + List keyRestrictedLeftJoinAliases = getKeyRestrictedLeftJoinAliases(query, keyRestrictedLeftJoins, Collections.EMPTY_SET); + List entityFunctionNodes = getEntityFunctionNodes(query); + + boolean isEmbedded = this instanceof ReturningBuilder; + String[] returningColumns = getReturningColumns(); + boolean shouldRenderCteNodes = renderCteNodes(isEmbedded); + List ctes = shouldRenderCteNodes ? getCteNodes(query, isEmbedded) : Collections.EMPTY_LIST; + + ExtendedQuerySupport extendedQuerySupport = getService(ExtendedQuerySupport.class); + + Query insertExampleQuery = getInsertExampleQuery(); + String insertExampleSql = extendedQuerySupport.getSql(em, insertExampleQuery); + String ownerAlias = extendedQuerySupport.getSqlAlias(em, insertExampleQuery, entityAlias); + String targetAlias = extendedQuerySupport.getSqlAlias(em, insertExampleQuery, COLLECTION_BASE_QUERY_ALIAS); + JoinTable joinTable = jpaProvider.getJoinTable(entityType, collectionName); + int joinTableIndex = SqlUtils.indexOfTableName(insertExampleSql, joinTable.getTableName()); + String collectionAlias = SqlUtils.extractAlias(insertExampleSql, joinTableIndex + joinTable.getTableName().length()); + String[] selectItemExpressions = SqlUtils.getSelectItemExpressions(insertExampleSql, SqlUtils.indexOfSelect(insertExampleSql)); + + // Prepare a Map + // This is used to replace references to id columns properly in the final sql query + Map columnExpressionRemappings = new HashMap<>(selectItemExpressions.length); + + for (Map.Entry entry : joinTable.getKeyColumnMappings().entrySet()) { + columnExpressionRemappings.put(collectionAlias + "." + entry.getValue(), entry.getKey()); + } + for (Map.Entry entry : joinTable.getIdColumnMappings().entrySet()) { + columnExpressionRemappings.put(ownerAlias + "." + entry.getValue(), entry.getKey()); + } + for (Map.Entry entry : joinTable.getTargetColumnMappings().entrySet()) { + columnExpressionRemappings.put(targetAlias + "." + entry.getValue(), entry.getKey()); + } + + StringBuilder insertSqlSb = new StringBuilder(); + insertSqlSb.append("insert into ").append(joinTable.getTableName()).append("("); + for (String selectItemExpression : selectItemExpressions) { + insertSqlSb.append(columnExpressionRemappings.get(selectItemExpression.trim())).append(','); + } + insertSqlSb.setCharAt(insertSqlSb.length() - 1, ')'); + + QuerySpecification querySpecification = new CollectionInsertModificationQuerySpecification( + this, + query, + getCountExampleQuery(), + parameterListNames, + keyRestrictedLeftJoinAliases, + entityFunctionNodes, + mainQuery.cteManager.isRecursive(), + ctes, + shouldRenderCteNodes, + isEmbedded, + returningColumns, + includedModificationStates, + returningAttributeBindingMap, + getInsertExecutorQuery(), + insertSqlSb.toString() + ); + + query = new CustomSQLQuery( + querySpecification, + query, + parameterManager.getValuesParameters(), + parameterManager.getValuesBinders() + ); + + parameterManager.parameterizeQuery(query); + + query.setFirstResult(firstResult); + query.setMaxResults(maxResults); + + return query; + } + + protected Query getInsertExampleQuery() { + // We produce this query just to extract the select expressions for the bindings + StringBuilder sb = new StringBuilder(); + + sb.append("SELECT "); + for (Map.Entry entry : bindingMap.entrySet()) { + sb.append(entry.getKey()); + sb.append(','); + } + + sb.setCharAt(sb.length() - 1, ' '); + sb.append("FROM "); + sb.append(entityType.getName()); + sb.append(' '); + sb.append(entityAlias); + sb.append(" LEFT JOIN "); + sb.append(entityAlias).append('.').append(collectionName) + .append(' ').append(COLLECTION_BASE_QUERY_ALIAS); + return em.createQuery(sb.toString()); + } + + protected Query getInsertExecutorQuery() { + // This is the query we use as "hull" to put other sqls into + // We chose ValuesEntity as insert base because it is known to be non-polymorphic + // We could have used the owner entity type as well, but at the time of writing, + // it wasn't clear if problems might arise when the entity type were polymorphic + String exampleQueryString = "UPDATE " + ValuesEntity.class.getSimpleName() + " SET value = NULL"; + return em.createQuery(exampleQueryString); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java index 5791ed96f2..06ea543c62 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java @@ -16,14 +16,6 @@ package com.blazebit.persistence.impl; -import java.util.*; - -import javax.persistence.Query; -import javax.persistence.Tuple; -import javax.persistence.TypedQuery; -import javax.persistence.metamodel.Attribute; -import javax.persistence.metamodel.EntityType; - import com.blazebit.persistence.BaseModificationCriteriaBuilder; import com.blazebit.persistence.FullSelectCTECriteriaBuilder; import com.blazebit.persistence.ReturningBuilder; @@ -43,6 +35,23 @@ import com.blazebit.persistence.impl.util.JpaMetamodelUtils; import com.blazebit.persistence.spi.DbmsModificationState; import com.blazebit.persistence.spi.DbmsStatementType; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; + +import javax.persistence.Query; +import javax.persistence.Tuple; +import javax.persistence.TypedQuery; +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.EntityType; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; /** * @@ -61,7 +70,7 @@ public abstract class AbstractModificationCriteriaBuilder>> returningAttributes; protected final Map returningAttributeBindingMap; - protected final Map> attributeColumnMappings; + protected final Map attributeEntries; protected final Map columnBindingMap; @SuppressWarnings("unchecked") @@ -87,7 +96,7 @@ public AbstractModificationCriteriaBuilder(MainQuery mainQuery, boolean isMainQu this.isReturningEntityAliasAllowed = false; this.returningAttributes = new LinkedHashMap<>(0); this.returningAttributeBindingMap = new LinkedHashMap(0); - this.attributeColumnMappings = null; + this.attributeEntries = null; this.columnBindingMap = null; } else { this.cteType = mainQuery.metamodel.entity(cteClass); @@ -95,9 +104,9 @@ public AbstractModificationCriteriaBuilder(MainQuery mainQuery, boolean isMainQu // Returning the "entity" is only allowed in CTEs this.isReturningEntityAliasAllowed = true; this.returningAttributes = null; - this.attributeColumnMappings = mainQuery.metamodel.getAttributeColumnNameMapping(cteClass); - this.returningAttributeBindingMap = new LinkedHashMap(attributeColumnMappings.size()); - this.columnBindingMap = new LinkedHashMap(attributeColumnMappings.size()); + this.attributeEntries = mainQuery.metamodel.getManagedType(ExtendedManagedType.class, cteClass).getAttributes(); + this.returningAttributeBindingMap = new LinkedHashMap(attributeEntries.size()); + this.columnBindingMap = new LinkedHashMap(attributeEntries.size()); } } @@ -156,8 +165,7 @@ protected Query getQuery(Map includedModification // see https://github.com/Blazebit/blaze-persistence/issues/306 // We use this to make these features only available to Hibernate as it is the only provider that supports sql replace yet - if (statementType == DbmsStatementType.INSERT - || (hasLimit() || mainQuery.cteManager.hasCtes() || returningAttributeBindingMap.size() > 0)) { + if (hasLimit() || mainQuery.cteManager.hasCtes() || returningAttributeBindingMap.size() > 0) { // We need to change the underlying sql when doing a limit with hibernate since it does not support limiting insert ... select statements // For CTEs we will also need to change the underlying sql @@ -195,12 +203,6 @@ protected Query getQuery(Map includedModification parameterManager.parameterizeQuery(query); - // Don't set the values for UPDATE or DELETE statements, otherwise Datanucleus will pass through the values to the JDBC statement - if (statementType == DbmsStatementType.INSERT) { - query.setFirstResult(firstResult); - query.setMaxResults(maxResults); - } - return query; } @@ -426,7 +428,7 @@ public X returning(String cteAttribute, String modificationQueryAttribute) { throw new IllegalArgumentException("Invalid empty modificationQueryAttribute"); } - Map.Entry attributeEntry = attributeColumnMappings.get(cteAttribute); + ExtendedAttribute attributeEntry = attributeEntries.get(cteAttribute); if (attributeEntry == null) { if (cteType.getAttribute(cteAttribute) != null) { @@ -446,8 +448,7 @@ public X returning(String cteAttribute, String modificationQueryAttribute) { queryAttrType = queryAttributePath.getAttributeClass(); } - AttributePath cteAttr = attributeEntry.getKey(); - Class cteAttrType = cteAttr.getAttributeClass(); + Class cteAttrType = attributeEntry.getElementClass(); // NOTE: Actually we would check if the dbms supports returning this kind of attribute, // but if it already supports the returning clause, it can only also support returning all columns if (!cteAttrType.isAssignableFrom(queryAttrType)) { @@ -458,7 +459,7 @@ public X returning(String cteAttribute, String modificationQueryAttribute) { if (returningAttributeBindingMap.put(cteAttribute, modificationQueryAttribute) != null) { throw new IllegalArgumentException("The cte attribute [" + cteAttribute + "] has already been bound!"); } - for (String column : attributeEntry.getValue()) { + for (String column : attributeEntry.getColumnNames()) { if (columnBindingMap.put(column, cteAttribute) != null) { throw new IllegalArgumentException("The cte column [" + column + "] has already been bound!"); } @@ -506,7 +507,7 @@ private String[] getReturningColumns(List>> attributes) { for (int i = 1; i < returningAttribute.size(); i++) { sb.append('.').append(returningAttribute.get(i).getName()); } - for (String column : cbf.getExtendedQuerySupport().getColumnNames(em, entityType, sb.toString())) { + for (String column : mainQuery.metamodel.getManagedType(ExtendedManagedType.class, entityType.getJavaType()).getAttribute(sb.toString()).getColumnNames()) { columns.add(column); } sb.setLength(0); @@ -515,7 +516,7 @@ private String[] getReturningColumns(List>> attributes) { return columns.toArray(new String[columns.size()]); } - private String[] getReturningColumns() { + protected String[] getReturningColumns() { if (returningAttributeBindingMap.isEmpty()) { return null; } @@ -524,7 +525,7 @@ private String[] getReturningColumns() { List columns = new ArrayList(returningAttributeNames.size()); for (String returningAttributeName : returningAttributeBindingMap.values()) { - for (String column : cbf.getExtendedQuerySupport().getColumnNames(em, entityType, returningAttributeName)) { + for (String column : mainQuery.metamodel.getManagedType(ExtendedManagedType.class, entityType.getJavaType()).getAttribute(returningAttributeName).getColumnNames()) { columns.add(column); } } @@ -532,7 +533,7 @@ private String[] getReturningColumns() { return columns.toArray(new String[columns.size()]); } - private Query getCountExampleQuery() { + protected Query getCountExampleQuery() { StringBuilder sb = new StringBuilder(); sb.append("SELECT COUNT(e) FROM "); sb.append(entityType.getName()); @@ -589,8 +590,8 @@ protected List prepareAndGetAttributes() { protected List prepareAndGetColumnNames() { StringBuilder sb = null; - for (Map.Entry entry : attributeColumnMappings.values()) { - for (String column : entry.getValue()) { + for (ExtendedAttribute entry : attributeEntries.values()) { + for (String column : entry.getColumnNames()) { if (!columnBindingMap.containsKey(column)) { if (sb == null) { sb = new StringBuilder(); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractUpdateCollectionCriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractUpdateCollectionCriteriaBuilder.java new file mode 100644 index 0000000000..bc829e5d55 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractUpdateCollectionCriteriaBuilder.java @@ -0,0 +1,241 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.BaseUpdateCriteriaBuilder; +import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.ReturningBuilder; +import com.blazebit.persistence.impl.expression.Expression; +import com.blazebit.persistence.impl.function.entity.ValuesEntity; +import com.blazebit.persistence.impl.query.CTENode; +import com.blazebit.persistence.impl.query.CollectionUpdateModificationQuerySpecification; +import com.blazebit.persistence.impl.query.CustomSQLQuery; +import com.blazebit.persistence.impl.query.QuerySpecification; +import com.blazebit.persistence.impl.util.JpaMetamodelUtils; +import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.spi.DbmsModificationState; +import com.blazebit.persistence.spi.ExtendedQuerySupport; +import com.blazebit.persistence.spi.JoinTable; + +import javax.persistence.Query; +import javax.persistence.metamodel.Attribute; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class AbstractUpdateCollectionCriteriaBuilder, Y> extends BaseUpdateCriteriaBuilderImpl { + + private final String collectionName; + + private List cachedBaseQueryStrings; + + public AbstractUpdateCollectionCriteriaBuilder(MainQuery mainQuery, boolean isMainQuery, Class clazz, String alias, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, isMainQuery, clazz, alias, cteName, cteClass, result, listener); + this.collectionName = collectionName; + // Add the join here so that references in the where clause go the the expected join node + // Also, this validates the collection actually exists + joinManager.join(entityAlias + "." + collectionName, CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS, JoinType.LEFT, false, true); + } + + @Override + protected String checkAttribute(String attributeName) { + // Assert the attribute exists and "clean" the attribute path + AttributePath attributePath = JpaMetamodelUtils.getJoinTableCollectionAttributePath(getMetamodel(), entityType, attributeName, collectionName); + StringBuilder sb = new StringBuilder(); + for (Attribute attribute : attributePath.getAttributes()) { + // Replace the collection name with the alias for easier processing + if (attribute instanceof QualifiedAttribute) { + sb.append(((QualifiedAttribute) attribute).getQualificationExpression()); + sb.append('('); + sb.append(CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + sb.append(')'); + } else if (collectionName.equals(attribute.getName())) { + sb.append(CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + } else { + sb.append(attribute.getName()); + } + sb.append('.'); + } + attributeName = sb.substring(0, sb.length() - 1); + Expression attributeExpression = setAttributes.get(attributeName); + + if (attributeExpression != null) { + throw new IllegalArgumentException("The attribute [" + attributeName + "] has already been bound!"); + } + return attributeName; + } + + @Override + protected void prepareForModification() { + super.prepareForModification(); + cachedBaseQueryStrings = null; + } + + @Override + protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { + if (externalRepresentation) { + sbSelectFrom.append("UPDATE "); + sbSelectFrom.append(entityType.getName()); + sbSelectFrom.append('(').append(collectionName).append(") "); + sbSelectFrom.append(entityAlias); + appendSetClause(sbSelectFrom); + appendWhereClause(sbSelectFrom); + } else { + // The internal representation is just a "hull" to hold the parameters at the appropriate positions + sbSelectFrom.append("SELECT 1 FROM "); + sbSelectFrom.append(entityType.getName()); + sbSelectFrom.append(' '); + sbSelectFrom.append(entityAlias); + sbSelectFrom.append(" LEFT JOIN "); + sbSelectFrom.append(entityAlias).append('.').append(collectionName) + .append(' ').append(CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + appendWhereClause(sbSelectFrom); + + // Create the select query strings that are used for the set items + // The idea is to encode a set item as an equality predicate in a dedicated query + // Each query will then participate in the overall query as parameter container + // and the extracted equality predicates can be used as-is for the set clause + cachedBaseQueryStrings = new ArrayList<>(); + StringBuilder sbSetExpressionQuery = new StringBuilder(); + + for (Map.Entry attributeEntry : setAttributes.entrySet()) { + fillCachedBaseQueryStrings(sbSetExpressionQuery, attributeEntry.getKey(), attributeEntry.getValue()); + } + } + } + + private void fillCachedBaseQueryStrings(StringBuilder sbSetExpressionQuery, String attributePath, Expression value) { + sbSetExpressionQuery.setLength(0); + StringBuilder oldBuffer = queryGenerator.getQueryBuffer(); + queryGenerator.setClauseType(ClauseType.SET); + queryGenerator.setQueryBuffer(sbSetExpressionQuery); + SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.CASE_WHEN); + + sbSetExpressionQuery.append("SELECT 1 FROM "); + sbSetExpressionQuery.append(entityType.getName()); + sbSetExpressionQuery.append(' '); + sbSetExpressionQuery.append(entityAlias); + sbSetExpressionQuery.append(" LEFT JOIN "); + sbSetExpressionQuery.append(entityAlias).append('.').append(collectionName) + .append(' ').append(CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + sbSetExpressionQuery.append(" WHERE "); + sbSetExpressionQuery.append(attributePath).append('='); + value.accept(queryGenerator); + cachedBaseQueryStrings.add(sbSetExpressionQuery.toString()); + + queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); + queryGenerator.setClauseType(null); + queryGenerator.setQueryBuffer(oldBuffer); + } + + @Override + protected boolean appendSetElementEntityPrefix(String trimmedPath) { + // Prevent collection aliases to be prefixed + return !trimmedPath.startsWith(CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS) && super.appendSetElementEntityPrefix(trimmedPath); + } + + @Override + protected Query getQuery(Map includedModificationStates) { + Query query = em.createQuery(getBaseQueryStringWithCheck()); + Set parameterListNames = parameterManager.getParameterListNames(query); + + boolean isEmbedded = this instanceof ReturningBuilder; + String[] returningColumns = getReturningColumns(); + boolean shouldRenderCteNodes = renderCteNodes(isEmbedded); + List ctes = shouldRenderCteNodes ? getCteNodes(query, isEmbedded) : Collections.EMPTY_LIST; + + ExtendedQuerySupport extendedQuerySupport = getService(ExtendedQuerySupport.class); + String sql = extendedQuerySupport.getSql(em, query); + String ownerAlias = extendedQuerySupport.getSqlAlias(em, query, entityAlias); + String targetAlias = extendedQuerySupport.getSqlAlias(em, query, CollectionUpdateModificationQuerySpecification.COLLECTION_BASE_QUERY_ALIAS); + JoinTable joinTable = jpaProvider.getJoinTable(entityType, collectionName); + int joinTableIndex = SqlUtils.indexOfTableName(sql, joinTable.getTableName()); + String collectionAlias = SqlUtils.extractAlias(sql, joinTableIndex + joinTable.getTableName().length()); + + String updateSql = "update " + joinTable.getTableName() + " set "; + + // Prepare a Map + // This is used to replace references to id columns properly in the final sql query + Map columnExpressionRemappings = new HashMap<>(); + + for (Map.Entry entry : joinTable.getKeyColumnMappings().entrySet()) { + columnExpressionRemappings.put(collectionAlias + "." + entry.getValue(), joinTable.getTableName() + "." + entry.getKey()); + } + for (Map.Entry entry : joinTable.getIdColumnMappings().entrySet()) { + columnExpressionRemappings.put(ownerAlias + "." + entry.getValue(), joinTable.getTableName() + "." + entry.getKey()); + } + for (Map.Entry entry : joinTable.getTargetColumnMappings().entrySet()) { + columnExpressionRemappings.put(targetAlias + "." + entry.getValue(), joinTable.getTableName() + "." + entry.getKey()); + } + + List setExpressionContainingUpdateQueries = new ArrayList<>(); + + for (String cachedBaseQueryString : cachedBaseQueryStrings) { + Query setExpressionQuery = em.createQuery(cachedBaseQueryString); + parameterListNames.addAll(parameterManager.getParameterListNames(query)); + setExpressionContainingUpdateQueries.add(setExpressionQuery); + } + + QuerySpecification querySpecification = new CollectionUpdateModificationQuerySpecification( + this, + query, + getCountExampleQuery(), + parameterListNames, + mainQuery.cteManager.isRecursive(), + ctes, + shouldRenderCteNodes, + isEmbedded, + returningColumns, + includedModificationStates, + returningAttributeBindingMap, + getUpdateExampleQuery(), + updateSql, + setExpressionContainingUpdateQueries, + columnExpressionRemappings + ); + + query = new CustomSQLQuery( + querySpecification, + query, + parameterManager.getValuesParameters(), + parameterManager.getValuesBinders() + ); + + parameterManager.parameterizeQuery(query); + + return query; + } + + protected Query getUpdateExampleQuery() { + // This is the query we use as "hull" to put other sqls into + // We chose ValuesEntity as update base because it is known to be non-polymorphic + // We could have used the owner entity type as well, but at the time of writing, + // it wasn't clear if problems might arise when the entity type were polymorphic + String exampleQueryString = "UPDATE " + ValuesEntity.class.getSimpleName() + " SET value = NULL"; + return em.createQuery(exampleQueryString); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseInsertCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseInsertCriteriaBuilderImpl.java index d62a1b0cd7..ac0d012a44 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseInsertCriteriaBuilderImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseInsertCriteriaBuilderImpl.java @@ -17,14 +17,23 @@ package com.blazebit.persistence.impl; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import javax.persistence.Query; import com.blazebit.persistence.BaseInsertCriteriaBuilder; +import com.blazebit.persistence.ReturningBuilder; import com.blazebit.persistence.SelectBuilder; +import com.blazebit.persistence.impl.query.CTENode; +import com.blazebit.persistence.impl.query.CustomSQLQuery; +import com.blazebit.persistence.impl.query.EntityFunctionNode; +import com.blazebit.persistence.impl.query.ModificationQuerySpecification; +import com.blazebit.persistence.impl.query.QuerySpecification; +import com.blazebit.persistence.spi.DbmsModificationState; import com.blazebit.persistence.spi.DbmsStatementType; /** @@ -35,7 +44,7 @@ */ public class BaseInsertCriteriaBuilderImpl, Y> extends AbstractModificationCriteriaBuilder implements BaseInsertCriteriaBuilder, SelectBuilder { - private final Map bindingMap = new TreeMap(); + protected final Map bindingMap = new TreeMap(); public BaseInsertCriteriaBuilderImpl(MainQuery mainQuery, boolean isMainQuery, Class clazz, String cteName, Class cteClass, Y result, CTEBuilderListener listener) { super(mainQuery, isMainQuery, DbmsStatementType.INSERT, clazz, null, cteName, cteClass, result, listener); @@ -53,19 +62,7 @@ protected void appendSelectClause(StringBuilder sbSelectFrom) { @Override @SuppressWarnings("unchecked") public X bind(String attributeName, Object value) { - // NOTE: We are not resolving embedded properties, because hibernate does not support them - // Just do that to assert the attribute exists - if (entityType.getAttribute(attributeName) == null) { - // Well, some implementations might not be fully spec compliant.. - throw new IllegalArgumentException("Attribute '" + attributeName + "' does not exist on '" + entityType.getName() + "'!"); - } - Integer attributeBindIndex = bindingMap.get(attributeName); - - if (attributeBindIndex != null) { - throw new IllegalArgumentException("The attribute [" + attributeName + "] has already been bound!"); - } - - bindingMap.put(attributeName, selectManager.getSelectInfos().size()); + addBind(attributeName); selectManager.select(parameterManager.addParameterExpression(value, ClauseType.SELECT), null); return (X) this; @@ -73,6 +70,11 @@ public X bind(String attributeName, Object value) { @Override public SelectBuilder bind(String attributeName) { + addBind(attributeName); + return this; + } + + protected void addBind(String attributeName) { // NOTE: We are not resolving embedded properties, because hibernate does not support them // Just do that to assert the attribute exists if (entityType.getAttribute(attributeName) == null) { @@ -80,13 +82,12 @@ public SelectBuilder bind(String attributeName) { throw new IllegalArgumentException("Attribute '" + attributeName + "' does not exist on '" + entityType.getName() + "'!"); } Integer attributeBindIndex = bindingMap.get(attributeName); - + if (attributeBindIndex != null) { throw new IllegalArgumentException("The attribute [" + attributeName + "] has already been bound!"); } - + bindingMap.put(attributeName, selectManager.getSelectInfos().size()); - return this; } @Override @@ -116,8 +117,8 @@ protected boolean isJoinRequiredForSelect() { @Override protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { - sbSelectFrom.append("INSERT INTO "); - sbSelectFrom.append(entityType.getName()).append('('); + appendInsertIntoFragment(sbSelectFrom, externalRepresentation); + sbSelectFrom.append('('); boolean first = true; for (Map.Entry attributeEntry : bindingMap.entrySet()) { @@ -131,9 +132,18 @@ protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean external } sbSelectFrom.append(")\n"); + buildSelectBaseQueryString(sbSelectFrom, externalRepresentation); + } + + protected void buildSelectBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { super.buildBaseQueryString(sbSelectFrom, externalRepresentation); } + protected void appendInsertIntoFragment(StringBuilder sbSelectFrom, boolean externalRepresentation) { + sbSelectFrom.append("INSERT INTO "); + sbSelectFrom.append(entityType.getName()); + } + @Override public Query getQuery() { if (jpaProvider.supportsInsertStatement()) { @@ -144,4 +154,50 @@ public Query getQuery() { } } + @Override + protected Query getQuery(Map includedModificationStates) { + // We need to change the underlying sql when doing a limit with hibernate since it does not support limiting insert ... select statements + Query query = em.createQuery(getBaseQueryStringWithCheck()); + Set parameterListNames = parameterManager.getParameterListNames(query); + Set keyRestrictedLeftJoins = joinManager.getKeyRestrictedLeftJoins(); + + List keyRestrictedLeftJoinAliases = getKeyRestrictedLeftJoinAliases(query, keyRestrictedLeftJoins, Collections.EMPTY_SET); + List entityFunctionNodes = getEntityFunctionNodes(query); + + boolean isEmbedded = this instanceof ReturningBuilder; + String[] returningColumns = getReturningColumns(); + boolean shouldRenderCteNodes = renderCteNodes(isEmbedded); + List ctes = shouldRenderCteNodes ? getCteNodes(query, isEmbedded) : Collections.EMPTY_LIST; + + QuerySpecification querySpecification = new ModificationQuerySpecification( + this, + query, + getCountExampleQuery(), + parameterListNames, + keyRestrictedLeftJoinAliases, + entityFunctionNodes, + mainQuery.cteManager.isRecursive(), + ctes, + shouldRenderCteNodes, + isEmbedded, + returningColumns, + includedModificationStates, + returningAttributeBindingMap + ); + + query = new CustomSQLQuery( + querySpecification, + query, + parameterManager.getValuesParameters(), + parameterManager.getValuesBinders() + ); + + parameterManager.parameterizeQuery(query); + + query.setFirstResult(firstResult); + query.setMaxResults(maxResults); + + return query; + } + } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseUpdateCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseUpdateCriteriaBuilderImpl.java index a9e6a7071e..b842d1499a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseUpdateCriteriaBuilderImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseUpdateCriteriaBuilderImpl.java @@ -41,7 +41,7 @@ */ public class BaseUpdateCriteriaBuilderImpl, Y> extends AbstractModificationCriteriaBuilder implements BaseUpdateCriteriaBuilder, SubqueryBuilderListener, ExpressionBuilderEndedListener { - private final Map setAttributes = new LinkedHashMap(); + protected final Map setAttributes = new LinkedHashMap<>(); private SubqueryInternalBuilder currentSubqueryBuilder; private String currentAttribute; @@ -53,7 +53,7 @@ public BaseUpdateCriteriaBuilderImpl(MainQuery mainQuery, boolean isMainQuery, C @SuppressWarnings("unchecked") public X set(String attributeName, Object value) { verifyBuilderEnded(); - checkAttribute(attributeName); + attributeName = checkAttribute(attributeName); Expression attributeExpression = parameterManager.addParameterExpression(value, ClauseType.SET); setAttributes.put(attributeName, attributeExpression); return (X) this; @@ -63,7 +63,7 @@ public X set(String attributeName, Object value) { @SuppressWarnings("unchecked") public X setExpression(String attributeName, String expression) { verifyBuilderEnded(); - checkAttribute(attributeName); + attributeName = checkAttribute(attributeName); Expression attributeExpression = expressionFactory.createScalarExpression(expression); parameterManager.collectParameterRegistrations(attributeExpression, ClauseType.SET); setAttributes.put(attributeName, attributeExpression); @@ -74,7 +74,7 @@ public X setExpression(String attributeName, String expression) { @Override public SubqueryInitiator set(String attribute) { verifySubqueryBuilderEnded(); - checkAttribute(attribute); + attribute = checkAttribute(attribute); this.currentAttribute = attribute; return subqueryInitFactory.createSubqueryInitiator((X) this, this, false); } @@ -83,7 +83,7 @@ public SubqueryInitiator set(String attribute) { @Override public MultipleSubqueryInitiator setSubqueries(String attribute, String expression) { verifySubqueryBuilderEnded(); - checkAttribute(attribute); + attribute = checkAttribute(attribute); this.currentAttribute = attribute; Expression expr = expressionFactory.createSimpleExpression(expression, true); MultipleSubqueryInitiator initiator = new MultipleSubqueryInitiatorImpl((X) this, expr, this, subqueryInitFactory); @@ -93,7 +93,7 @@ public MultipleSubqueryInitiator setSubqueries(String attribute, String expre @Override public SubqueryBuilder set(String attribute, FullQueryBuilder criteriaBuilder) { verifySubqueryBuilderEnded(); - checkAttribute(attribute); + attribute = checkAttribute(attribute); this.currentAttribute = attribute; return subqueryInitFactory.createSubqueryBuilder((X) this, this, false, criteriaBuilder); } @@ -154,7 +154,7 @@ public void onInitiatorStarted(SubqueryInitiator initiator) { throw new IllegalArgumentException("Initiator started not valid!"); } - private void checkAttribute(String attributeName) { + protected String checkAttribute(String attributeName) { // Just do that to assert the attribute exists JpaMetamodelUtils.getBasicAttributePath(getMetamodel(), entityType, attributeName); Expression attributeExpression = setAttributes.get(attributeName); @@ -162,6 +162,7 @@ private void checkAttribute(String attributeName) { if (attributeExpression != null) { throw new IllegalArgumentException("The attribute [" + attributeName + "] has already been bound!"); } + return attributeName; } @Override @@ -169,32 +170,47 @@ protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean external sbSelectFrom.append("UPDATE "); sbSelectFrom.append(entityType.getName()).append(' '); sbSelectFrom.append(entityAlias); + appendSetClause(sbSelectFrom); + appendWhereClause(sbSelectFrom); + } + + protected void appendSetClause(StringBuilder sbSelectFrom) { sbSelectFrom.append(" SET "); queryGenerator.setClauseType(ClauseType.SET); queryGenerator.setQueryBuffer(sbSelectFrom); SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.CASE_WHEN); - + Iterator> setAttributeIter = setAttributes.entrySet().iterator(); if (setAttributeIter.hasNext()) { Map.Entry attributeEntry = setAttributeIter.next(); - sbSelectFrom.append(entityAlias).append('.'); - sbSelectFrom.append(attributeEntry.getKey()); - sbSelectFrom.append(" = "); - queryGenerator.generate(attributeEntry.getValue()); + appendSetElement(sbSelectFrom, attributeEntry.getKey(), attributeEntry.getValue()); while (setAttributeIter.hasNext()) { attributeEntry = setAttributeIter.next(); sbSelectFrom.append(','); - sbSelectFrom.append(entityAlias).append('.'); - sbSelectFrom.append(attributeEntry.getKey()); - sbSelectFrom.append(" = "); - queryGenerator.generate(attributeEntry.getValue()); + appendSetElement(sbSelectFrom, attributeEntry.getKey(), attributeEntry.getValue()); } } queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); queryGenerator.setClauseType(null); - appendWhereClause(sbSelectFrom); + } + + protected final void appendSetElement(StringBuilder sbSelectFrom, String attribute, Expression valueExpression) { + String trimmedPath = attribute.trim(); + if (appendSetElementEntityPrefix(trimmedPath)) { + sbSelectFrom.append(entityAlias).append('.'); + } + sbSelectFrom.append(attribute); + sbSelectFrom.append(" = "); + queryGenerator.generate(valueExpression); + } + + protected boolean appendSetElementEntityPrefix(String trimmedPath) { + String indexStart = "index("; + String keyStart = "key("; + return !trimmedPath.regionMatches(true, 0, indexStart, 0, indexStart.length()) + && !trimmedPath.regionMatches(true, 0, keyStart, 0, keyStart.length()); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CachingJpaProvider.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CachingJpaProvider.java new file mode 100644 index 0000000000..9b3d154e0b --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CachingJpaProvider.java @@ -0,0 +1,281 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.spi.JoinTable; +import com.blazebit.persistence.spi.JpaProvider; + +import javax.persistence.EntityManager; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.ManagedType; +import java.util.Map; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public final class CachingJpaProvider implements JpaProvider { + + private final JpaProvider jpaProvider; + private final EntityMetamodelImpl entityMetamodel; + + public CachingJpaProvider(EntityMetamodelImpl entityMetamodel) { + this.jpaProvider = entityMetamodel.getJpaProvider(); + this.entityMetamodel = entityMetamodel; + } + + public JpaProvider getJpaProvider() { + return jpaProvider; + } + + @Override + public boolean isForeignJoinColumn(EntityType ownerType, String attributeName) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute != null && attribute.isForeignJoinColumn(); + } + + @Override + public boolean isColumnShared(EntityType ownerType, String attributeName) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute != null && attribute.isColumnShared(); + } + + @Override + public ConstraintType requiresTreatFilter(EntityType ownerType, String attributeName, JoinType joinType) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute == null ? ConstraintType.NONE : attribute.getJoinTypeIndexedRequiresTreatFilter(joinType); + } + + @Override + public String getMappedBy(EntityType ownerType, String attributeName) { + return entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttribute(attributeName).getMappedBy(); + } + + @Override + public String[] getColumnNames(EntityType ownerType, String attributeName) { + return entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttribute(attributeName).getColumnNames(); + } + + @Override + public String[] getColumnTypes(EntityType ownerType, String attributeName) { + return entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttribute(attributeName).getColumnTypes(); + } + + @Override + public Map getWritableMappedByMappings(EntityType inverseType, EntityType ownerType, String attributeName) { + return entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttribute(attributeName).getWritableMappedByMappings(inverseType); + } + + @Override + public JoinTable getJoinTable(EntityType ownerType, String attributeName) { + return entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttribute(attributeName).getJoinTable(); + } + + @Override + public boolean isBag(EntityType ownerType, String attributeName) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute != null && attribute.isBag(); + } + + @Override + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute != null && attribute.isOrphanRemoval(); + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + ExtendedAttribute attribute = (ExtendedAttribute) entityMetamodel.getManagedType(ExtendedManagedType.class, ownerType.getJavaType()).getAttributes().get(attributeName); + return attribute != null && attribute.isDeleteCascaded(); + } + + // Simple delegates + + @Override + public boolean supportsJpa21() { + return jpaProvider.supportsJpa21(); + } + + @Override + public boolean supportsEntityJoin() { + return jpaProvider.supportsEntityJoin(); + } + + @Override + public boolean supportsInsertStatement() { + return jpaProvider.supportsInsertStatement(); + } + + @Override + public boolean needsBracketsForListParamter() { + return jpaProvider.needsBracketsForListParamter(); + } + + @Override + public boolean needsJoinSubqueryRewrite() { + return jpaProvider.needsJoinSubqueryRewrite(); + } + + @Override + public String getBooleanExpression(boolean value) { + return jpaProvider.getBooleanExpression(value); + } + + @Override + public String getBooleanConditionalExpression(boolean value) { + return jpaProvider.getBooleanConditionalExpression(value); + } + + @Override + public String getNullExpression() { + return jpaProvider.getNullExpression(); + } + + @Override + public String getOnClause() { + return jpaProvider.getOnClause(); + } + + @Override + public String getCollectionValueFunction() { + return jpaProvider.getCollectionValueFunction(); + } + + @Override + public boolean supportsCollectionValueDereference() { + return jpaProvider.supportsCollectionValueDereference(); + } + + @Override + public Class getDefaultQueryResultType() { + return jpaProvider.getDefaultQueryResultType(); + } + + @Override + public String getCustomFunctionInvocation(String functionName, int argumentCount) { + return jpaProvider.getCustomFunctionInvocation(functionName, argumentCount); + } + + @Override + public String escapeCharacter(char character) { + return jpaProvider.escapeCharacter(character); + } + + @Override + public boolean supportsNullPrecedenceExpression() { + return jpaProvider.supportsNullPrecedenceExpression(); + } + + @Override + public void renderNullPrecedence(StringBuilder sb, String expression, String resolvedExpression, String order, String nulls) { + jpaProvider.renderNullPrecedence(sb, expression, resolvedExpression, order, nulls); + } + + @Override + public boolean supportsRootTreat() { + return jpaProvider.supportsRootTreat(); + } + + @Override + public boolean supportsTreatJoin() { + return jpaProvider.supportsTreatJoin(); + } + + @Override + public boolean supportsTreatCorrelation() { + return jpaProvider.supportsTreatCorrelation(); + } + + @Override + public boolean supportsRootTreatJoin() { + return jpaProvider.supportsRootTreatJoin(); + } + + @Override + public boolean supportsRootTreatTreatJoin() { + return jpaProvider.supportsRootTreatTreatJoin(); + } + + @Override + public boolean supportsSubtypePropertyResolving() { + return jpaProvider.supportsSubtypePropertyResolving(); + } + + @Override + public boolean supportsSubtypeRelationResolving() { + return jpaProvider.supportsSubtypeRelationResolving(); + } + + @Override + public boolean supportsCountStar() { + return jpaProvider.supportsCountStar(); + } + + @Override + public boolean containsEntity(EntityManager em, Class entityClass, Object id) { + return jpaProvider.containsEntity(em, entityClass, id); + } + + @Override + public boolean supportsSingleValuedAssociationIdExpressions() { + return jpaProvider.supportsSingleValuedAssociationIdExpressions(); + } + + @Override + public boolean supportsForeignAssociationInOnClause() { + return jpaProvider.supportsForeignAssociationInOnClause(); + } + + @Override + public boolean supportsUpdateSetEmbeddable() { + return jpaProvider.supportsUpdateSetEmbeddable(); + } + + @Override + public boolean supportsTransientEntityAsParameter() { + return jpaProvider.supportsTransientEntityAsParameter(); + } + + @Override + public boolean needsAssociationToIdRewriteInOnClause() { + return jpaProvider.needsAssociationToIdRewriteInOnClause(); + } + + @Override + public boolean needsBrokenAssociationToIdRewriteInOnClause() { + return jpaProvider.needsBrokenAssociationToIdRewriteInOnClause(); + } + + @Override + public boolean needsTypeConstraintForColumnSharing() { + return jpaProvider.needsTypeConstraintForColumnSharing(); + } + + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return jpaProvider.supportsCollectionTableCleanupOnDelete(); + } + + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return jpaProvider.supportsJoinTableCleanupOnDelete(); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java index 4f131f2643..33d4793aa1 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java @@ -71,28 +71,9 @@ public class CriteriaBuilderFactoryImpl implements CriteriaBuilderFactory { private final DbmsDialect configuredDbmsDialect; private final Map configuredRegisteredFunctions; private final JpaProviderFactory configuredJpaProviderFactory; + private final JpaProvider jpaProvider; public CriteriaBuilderFactoryImpl(CriteriaBuilderConfigurationImpl config, EntityManagerFactory entityManagerFactory) { - this.queryConfiguration = new ImmutableQueryConfiguration((Map) (Map) config.getProperties()); - final boolean compatibleMode = queryConfiguration.isCompatibleModeEnabled(); - final boolean optimize = queryConfiguration.isExpressionOptimizationEnabled(); - - this.entityManagerFactory = entityManagerFactory; - this.metamodel = new EntityMetamodelImpl(entityManagerFactory, config.getExtendedQuerySupport()); - this.transientEntityParameterTransformerFactory = new TransientEntityAssociationParameterTransformerFactory(metamodel, new AssociationToIdParameterTransformer(entityManagerFactory.getPersistenceUnitUtil())); - this.extendedQuerySupport = config.getExtendedQuerySupport(); - this.aggregateFunctions = resolveAggregateFunctions(config.getFunctions()); - this.treatFunctions = resolveTreatTypes(config.getTreatTypes()); - - ExpressionFactory originalExpressionFactory = new ExpressionFactoryImpl(aggregateFunctions, metamodel.getEntityTypes(), metamodel.getEnumTypes(), !compatibleMode, optimize); - this.expressionCache = createCache(queryConfiguration.getExpressionCacheClass()); - ExpressionFactory cachingExpressionFactory = new SimpleCachingExpressionFactory(originalExpressionFactory, expressionCache); - ExpressionFactory cachingSubqueryExpressionFactory = new SimpleCachingExpressionFactory(new SubqueryExpressionFactory(aggregateFunctions, metamodel.getEntityTypes(), metamodel.getEnumTypes(), !compatibleMode, optimize, originalExpressionFactory)); - this.macroConfiguration = MacroConfiguration.of(JpqlMacroAdapter.createMacros(config.getMacros(), cachingExpressionFactory)); - JpqlMacroStorage macroStorage = new JpqlMacroStorage(null, macroConfiguration); - this.expressionFactory = new JpqlMacroAwareExpressionFactory(cachingExpressionFactory, macroStorage); - this.subqueryExpressionFactory = new JpqlMacroAwareExpressionFactory(cachingSubqueryExpressionFactory, macroStorage); - List integrators = config.getEntityManagerIntegrators(); if (integrators.size() < 1) { throw new IllegalArgumentException("No EntityManagerFactoryIntegrator was found on the classpath! Please check if an integration for your JPA provider is visible on the classpath!"); @@ -106,7 +87,7 @@ public CriteriaBuilderFactoryImpl(CriteriaBuilderConfigurationImpl config, Entit String dbms = integrator.getDbms(emf); Map dbmsDialects = config.getDbmsDialects(); DbmsDialect dialect = dbmsDialects.get(dbms); - + // Use the default dialect if (dialect == null) { dialect = dbmsDialects.get(null); @@ -116,6 +97,28 @@ public CriteriaBuilderFactoryImpl(CriteriaBuilderConfigurationImpl config, Entit this.configuredDbmsDialect = dialect; this.configuredRegisteredFunctions = registeredFunctions; this.configuredJpaProviderFactory = integrator.getJpaProviderFactory(emf); + + this.queryConfiguration = new ImmutableQueryConfiguration((Map) (Map) config.getProperties()); + final boolean compatibleMode = queryConfiguration.isCompatibleModeEnabled(); + final boolean optimize = queryConfiguration.isExpressionOptimizationEnabled(); + + this.entityManagerFactory = entityManagerFactory; + this.metamodel = new EntityMetamodelImpl(entityManagerFactory, configuredJpaProviderFactory); + this.jpaProvider = new CachingJpaProvider(metamodel); + + this.transientEntityParameterTransformerFactory = new TransientEntityAssociationParameterTransformerFactory(metamodel, new AssociationToIdParameterTransformer(entityManagerFactory.getPersistenceUnitUtil())); + this.extendedQuerySupport = config.getExtendedQuerySupport(); + this.aggregateFunctions = resolveAggregateFunctions(config.getFunctions()); + this.treatFunctions = resolveTreatTypes(config.getTreatTypes()); + + ExpressionFactory originalExpressionFactory = new ExpressionFactoryImpl(aggregateFunctions, metamodel.getEntityTypes(), metamodel.getEnumTypes(), !compatibleMode, optimize); + this.expressionCache = createCache(queryConfiguration.getExpressionCacheClass()); + ExpressionFactory cachingExpressionFactory = new SimpleCachingExpressionFactory(originalExpressionFactory, expressionCache); + ExpressionFactory cachingSubqueryExpressionFactory = new SimpleCachingExpressionFactory(new SubqueryExpressionFactory(aggregateFunctions, metamodel.getEntityTypes(), metamodel.getEnumTypes(), !compatibleMode, optimize, originalExpressionFactory)); + this.macroConfiguration = MacroConfiguration.of(JpqlMacroAdapter.createMacros(config.getMacros(), cachingExpressionFactory)); + JpqlMacroStorage macroStorage = new JpqlMacroStorage(null, macroConfiguration); + this.expressionFactory = new JpqlMacroAwareExpressionFactory(cachingExpressionFactory, macroStorage); + this.subqueryExpressionFactory = new JpqlMacroAwareExpressionFactory(cachingSubqueryExpressionFactory, macroStorage); } private ExpressionCache createCache(String className) { @@ -144,8 +147,8 @@ private static Map, String> resolveTreatTypes(Map> tre return Collections.unmodifiableMap(types); } - public JpaProvider createJpaProvider(EntityManager em) { - return configuredJpaProviderFactory.createJpaProvider(em); + public JpaProvider getJpaProvider() { + return jpaProvider; } public QueryConfiguration getQueryConfiguration() { @@ -253,6 +256,18 @@ public DeleteCriteriaBuilder delete(EntityManager entityManager, Class return cb; } + @Override + public DeleteCriteriaBuilder deleteCollection(EntityManager entityManager, Class deleteOwnerClass, String collectionName) { + return deleteCollection(entityManager, deleteOwnerClass, null, collectionName); + } + + @Override + public DeleteCriteriaBuilder deleteCollection(EntityManager entityManager, Class deleteOwnerClass, String alias, String collectionName) { + MainQuery mainQuery = createMainQuery(entityManager); + DeleteCollectionCriteriaBuilderImpl cb = new DeleteCollectionCriteriaBuilderImpl(mainQuery, deleteOwnerClass, alias, collectionName); + return cb; + } + @Override public UpdateCriteriaBuilder update(EntityManager entityManager, Class updateClass) { return update(entityManager, updateClass, null); @@ -265,6 +280,18 @@ public UpdateCriteriaBuilder update(EntityManager entityManager, Class return cb; } + @Override + public UpdateCriteriaBuilder updateCollection(EntityManager entityManager, Class updateOwnerClass, String collectionName) { + return updateCollection(entityManager, updateOwnerClass, null, collectionName); + } + + @Override + public UpdateCriteriaBuilder updateCollection(EntityManager entityManager, Class updateOwnerClass, String alias, String collectionName) { + MainQuery mainQuery = createMainQuery(entityManager); + UpdateCollectionCriteriaBuilderImpl cb = new UpdateCollectionCriteriaBuilderImpl(mainQuery, updateOwnerClass, alias, collectionName); + return cb; + } + @Override public InsertCriteriaBuilder insert(EntityManager entityManager, Class insertClass) { MainQuery mainQuery = createMainQuery(entityManager); @@ -272,6 +299,13 @@ public InsertCriteriaBuilder insert(EntityManager entityManager, Class return cb; } + @Override + public InsertCriteriaBuilder insertCollection(EntityManager entityManager, Class insertOwnerClass, String collectionName) { + MainQuery mainQuery = createMainQuery(entityManager); + InsertCollectionCriteriaBuilderImpl cb = new InsertCollectionCriteriaBuilderImpl(mainQuery, insertOwnerClass, collectionName); + return cb; + } + @Override @SuppressWarnings("unchecked") public T getService(Class serviceClass) { @@ -287,6 +321,8 @@ public T getService(Class serviceClass) { return (T) extendedQuerySupport; } else if (JpaProviderFactory.class.equals(serviceClass)) { return (T) configuredJpaProviderFactory; + } else if (JpaProvider.class.equals(serviceClass)) { + return (T) jpaProvider; } else if (ExpressionCache.class.equals(serviceClass)) { return (T) expressionCache; } else if (Metamodel.class.isAssignableFrom(serviceClass)) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/DeleteCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/DeleteCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..1f21a7aef7 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/DeleteCollectionCriteriaBuilderImpl.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.DeleteCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class DeleteCollectionCriteriaBuilderImpl extends AbstractDeleteCollectionCriteriaBuilder, Void> implements DeleteCriteriaBuilder { + + public DeleteCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class deleteOwnerClass, String alias, String collectionName) { + super(mainQuery, true, deleteOwnerClass, alias, null, null, null, null, collectionName); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java index f3a10ccd3f..4722f35df0 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java @@ -18,8 +18,13 @@ import com.blazebit.annotation.AnnotationUtils; import com.blazebit.persistence.CTE; +import com.blazebit.persistence.JoinType; import com.blazebit.persistence.impl.util.JpaMetamodelUtils; -import com.blazebit.persistence.spi.ExtendedQuerySupport; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.spi.JoinTable; +import com.blazebit.persistence.spi.JpaProvider; +import com.blazebit.persistence.spi.JpaProviderFactory; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; @@ -27,16 +32,19 @@ import javax.persistence.metamodel.BasicType; import javax.persistence.metamodel.EmbeddableType; import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.MapAttribute; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; -import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -46,21 +54,21 @@ * This is a wrapper around the JPA {@link Metamodel} allows additionally efficient access by other attributes than a Class. * * @author Christian Beikov - * @since 1.2 + * @since 1.2.0 */ public class EntityMetamodelImpl implements EntityMetamodel { private final Metamodel delegate; + private final JpaProvider jpaProvider; private final Map> entityNameMap; private final Map> entityTypes; private final Map>> enumTypes; private final Map, Type> classMap; private final ConcurrentMap, Type> basicTypeMap = new ConcurrentHashMap<>(); private final Map, ManagedType> cteMap; - private final Map, Map>> typeAttributeColumnNameMap; - private final Map, Map>> typeAttributeColumnTypeMap; + private final Map, ExtendedManagedTypeImpl> extendedManagedTypes; - public EntityMetamodelImpl(EntityManagerFactory emf, ExtendedQuerySupport extendedQuerySupport) { + public EntityMetamodelImpl(EntityManagerFactory emf, JpaProviderFactory jpaProviderFactory) { this.delegate = emf.getMetamodel(); Set> managedTypes = delegate.getManagedTypes(); Map> nameToType = new HashMap<>(managedTypes.size()); @@ -68,11 +76,11 @@ public EntityMetamodelImpl(EntityManagerFactory emf, ExtendedQuerySupport extend Map>> enumTypes = new HashMap<>(managedTypes.size()); Map, Type> classToType = new HashMap<>(managedTypes.size()); Map, ManagedType> cteToType = new HashMap<>(managedTypes.size()); - Map, Map>> typeAttributeColumnNames = new HashMap<>(managedTypes.size()); - Map, Map>> typeAttributeColumnTypeNames = new HashMap<>(managedTypes.size()); EntityManager em = emf.createEntityManager(); + this.jpaProvider = jpaProviderFactory.createJpaProvider(em); Set> seenTypesForEnumResolving = new HashSet<>(); + Map, TemporaryExtendedManagedType> temporaryExtendedManagedTypes = new HashMap<>(); try { for (ManagedType t : managedTypes) { @@ -83,38 +91,33 @@ public EntityMetamodelImpl(EntityManagerFactory emf, ExtendedQuerySupport extend entityTypes.put(e.getName(), e.getJavaType()); entityTypes.put(e.getJavaType().getName(), e.getJavaType()); - if (extendedQuerySupport != null && extendedQuerySupport.supportsAdvancedSql()) { - Set> attributes = (Set>) t.getAttributes(); - - Map> attributeMap = new HashMap<>(attributes.size()); - typeAttributeColumnNames.put(t.getJavaType(), Collections.unmodifiableMap(attributeMap)); - - Map> attributeTypeMap = new HashMap<>(attributes.size()); - typeAttributeColumnTypeNames.put(t.getJavaType(), Collections.unmodifiableMap(attributeTypeMap)); - - seenTypesForEnumResolving.add(t.getJavaType()); - - for (Attribute attribute : attributes) { - Class fieldType = JpaMetamodelUtils.resolveFieldClass(t.getJavaType(), attribute); - if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { - collectColumnNames(extendedQuerySupport, em, e, attributeMap, attribute.getName(), delegate.embeddable(fieldType)); - } - - AttributePath path = new AttributePath(Arrays.>asList(attribute), fieldType); - - // Collect column names - String[] columnNames = extendedQuerySupport.getColumnNames(em, e, attribute.getName()); - attributeMap.put(attribute.getName(), new AbstractMap.SimpleEntry<>(path, columnNames)); - - // Collect column types - String[] columnTypes = extendedQuerySupport.getColumnTypes(em, e, attribute.getName()); - attributeTypeMap.put(attribute.getName(), new AbstractMap.SimpleEntry<>(path, columnTypes)); - - discoverEnumTypes(seenTypesForEnumResolving, enumTypes, e.getJavaType(), attribute); + Set> attributes = (Set>) t.getAttributes(); + + Map attributeMap = new HashMap<>(attributes.size()); + temporaryExtendedManagedTypes.put(t.getJavaType(), new TemporaryExtendedManagedType(t, attributeMap)); + + seenTypesForEnumResolving.add(t.getJavaType()); + + for (Attribute attribute : attributes) { + List> parents = Collections.>singletonList(attribute); + Class fieldType = JpaMetamodelUtils.resolveFieldClass(t.getJavaType(), attribute); + if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { + collectColumnNames(e, attributeMap, attribute.getName(), parents, delegate.embeddable(fieldType), temporaryExtendedManagedTypes); + } else if (attribute.isAssociation() && !attribute.isCollection() && !jpaProvider.isForeignJoinColumn(e, attribute.getName())) { + // We create an attribute entry for the id attribute of *ToOne relations if the columns reside on the Many side + EntityType fieldEntityType = delegate.entity(fieldType); + SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute(fieldEntityType); + Class idType = JpaMetamodelUtils.resolveFieldClass(fieldType, idAttribute); + String idPath = attribute.getName() + "." + idAttribute.getName(); + attributeMap.put(idPath, new AttributeEntry(jpaProvider, t, idAttribute, idPath, idType, Arrays.asList(attribute, idAttribute))); } - } else { - discoverEnumTypes(seenTypesForEnumResolving, enumTypes, t); + + attributeMap.put(attribute.getName(), new AttributeEntry(jpaProvider, t, attribute, attribute.getName(), fieldType, parents)); + discoverEnumTypes(seenTypesForEnumResolving, enumTypes, e.getJavaType(), attribute); } + } else if (!temporaryExtendedManagedTypes.containsKey(t.getJavaType())) { + // We are going to discover embeddables through their owning entity types + temporaryExtendedManagedTypes.put(t.getJavaType(), new TemporaryExtendedManagedType(t, new HashMap())); } classToType.put(t.getJavaType(), t); @@ -127,13 +130,73 @@ public EntityMetamodelImpl(EntityManagerFactory emf, ExtendedQuerySupport extend em.close(); } + Set> cascadingDeleteCycleSet = new HashSet<>(); + for (ManagedType t : managedTypes) { + if (t instanceof EntityType) { + Class targetClass = t.getJavaType(); + TemporaryExtendedManagedType targetManagedType = temporaryExtendedManagedTypes.get(targetClass); + cascadingDeleteCycleSet.add(targetClass); + for (Map.Entry entry : targetManagedType.attributes.entrySet()) { + AttributeEntry attribute = entry.getValue(); + detectCascadingDeleteCycles(targetManagedType, temporaryExtendedManagedTypes, cascadingDeleteCycleSet, targetClass, attribute); + if (targetManagedType.done) { + if (targetManagedType.cascadingDeleteCycle) { + entry.setValue(attribute.withCascadingDeleteCycle()); + } + } + } + cascadingDeleteCycleSet.remove(targetClass); + targetManagedType.done = true; + } + } + + Map, ExtendedManagedTypeImpl> extendedManagedTypes = new HashMap<>(temporaryExtendedManagedTypes.size()); + for (Map.Entry, TemporaryExtendedManagedType> entry : temporaryExtendedManagedTypes.entrySet()) { + TemporaryExtendedManagedType value = entry.getValue(); + extendedManagedTypes.put(entry.getKey(), new ExtendedManagedTypeImpl(value.managedType, value.cascadingDeleteCycle, Collections.unmodifiableMap(value.attributes))); + } + this.entityNameMap = Collections.unmodifiableMap(nameToType); this.entityTypes = Collections.unmodifiableMap(entityTypes); this.enumTypes = Collections.unmodifiableMap(enumTypes); this.classMap = Collections.unmodifiableMap(classToType); this.cteMap = Collections.unmodifiableMap(cteToType); - this.typeAttributeColumnNameMap = Collections.unmodifiableMap(typeAttributeColumnNames); - this.typeAttributeColumnTypeMap = Collections.unmodifiableMap(typeAttributeColumnTypeNames); + this.extendedManagedTypes = Collections.unmodifiableMap(extendedManagedTypes); + } + + private void detectCascadingDeleteCycles(TemporaryExtendedManagedType ownerManagedType, Map, TemporaryExtendedManagedType> extendedManagedTypes, Set> cascadingDeleteCycleSet, Class ownerClass, AttributeEntry attributeEntry) { + Class targetClass = attributeEntry.getElementClass(); + TemporaryExtendedManagedType targetManagedType = extendedManagedTypes.get(targetClass); + if (targetManagedType != null) { + if (!cascadingDeleteCycleSet.add(targetClass)) { + // Found a cascading delete cycle + ownerManagedType.done = true; + ownerManagedType.cascadingDeleteCycle = true; + targetManagedType.done = true; + targetManagedType.cascadingDeleteCycle = true; + } else { + if (targetManagedType.done) { + // Mark owner as done if target already found a cascading delete cycle + if (targetManagedType.cascadingDeleteCycle) { + ownerManagedType.done = true; + ownerManagedType.cascadingDeleteCycle = true; + } + } else { + for (Map.Entry entry : targetManagedType.attributes.entrySet()) { + AttributeEntry attribute = entry.getValue(); + detectCascadingDeleteCycles(targetManagedType, extendedManagedTypes, cascadingDeleteCycleSet, targetClass, attribute); + if (targetManagedType.done) { + if (targetManagedType.cascadingDeleteCycle) { + entry.setValue(attribute.withCascadingDeleteCycle()); + } + } + } + targetManagedType.done = true; + } + + cascadingDeleteCycleSet.remove(targetClass); + } + } } private void discoverEnumTypes(Set> seenTypesForEnumResolving, Map>> enumTypes, ManagedType t) { @@ -179,28 +242,42 @@ private void discoverEnumTypes(Set> seenTypesForEnumResolving, Map e, Map> attributeMap, String parent, EmbeddableType type) { + private void collectColumnNames(EntityType e, Map attributeMap, String parent, List> parents, EmbeddableType type, Map, TemporaryExtendedManagedType> temporaryExtendedManagedTypes) { Set> attributes = (Set>) type.getAttributes(); + TemporaryExtendedManagedType extendedManagedType = temporaryExtendedManagedTypes.get(type.getJavaType()); + if (extendedManagedType == null) { + extendedManagedType = new TemporaryExtendedManagedType(type, new HashMap()); + temporaryExtendedManagedTypes.put(type.getJavaType(), extendedManagedType); + } for (Attribute attribute : attributes) { + ArrayList> newParents = new ArrayList<>(parents.size() + 1); + newParents.addAll(parents); + newParents.add(attribute); + String attributeName = parent + "." + attribute.getName(); Class fieldType = JpaMetamodelUtils.resolveFieldClass(type.getJavaType(), attribute); if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED) { - collectColumnNames(extendedQuerySupport, em, e, attributeMap, parent + "." + attribute.getName(), delegate.embeddable(fieldType)); + collectColumnNames(e, attributeMap, attributeName, newParents, delegate.embeddable(fieldType), temporaryExtendedManagedTypes); + } else if (attribute.isAssociation() && !attribute.isCollection() && !jpaProvider.isForeignJoinColumn(e, attributeName)) { + // We create an attribute entry for the id attribute of *ToOne relations if the columns reside on the Many side + EntityType fieldEntityType = delegate.entity(fieldType); + SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute(fieldEntityType); + Class idType = JpaMetamodelUtils.resolveFieldClass(fieldType, idAttribute); + String idPath = attributeName + "." + idAttribute.getName(); + ArrayList> idParents = new ArrayList<>(newParents.size() + 1); + idParents.addAll(newParents); + idParents.add(idAttribute); + attributeMap.put(idPath, new AttributeEntry(jpaProvider, e, idAttribute, idPath, idType, idParents)); } - String attributeName = parent + "." + attribute.getName(); - String[] columnNames = extendedQuerySupport.getColumnNames(em, e, attributeName); - AttributePath path = new AttributePath(Arrays.>asList(attribute), fieldType); - attributeMap.put(attributeName, new AbstractMap.SimpleEntry(path, columnNames)); + AttributeEntry attributeEntry = new AttributeEntry(jpaProvider, e, attribute, attributeName, fieldType, newParents); + attributeMap.put(attributeName, attributeEntry); + extendedManagedType.attributes.put(attribute.getName(), attributeEntry); } } - public Map> getAttributeColumnNameMapping(Class cls) { - return typeAttributeColumnNameMap.get(cls); - } - - public Map> getAttributeColumnTypeMapping(Class cls) { - return typeAttributeColumnTypeMap.get(cls); + public JpaProvider getJpaProvider() { + return jpaProvider; } @Override @@ -307,6 +384,257 @@ public Set> getEmbeddables() { return delegate.getEmbeddables(); } + @Override + public T getManagedType(Class cls, Class managedType) { + ExtendedManagedType extendedManagedType = getEntry(managedType); + if (cls == ExtendedManagedType.class) { + return (T) extendedManagedType; + } + return null; + } + + private ExtendedManagedType getEntry(Class ownerType) { + ExtendedManagedType extendedManagedType = extendedManagedTypes.get(ownerType); + if (extendedManagedType == null) { + throw new IllegalArgumentException("Unknown managed type '" + ownerType.getName() + "'"); + } + return extendedManagedType; + } + + private static final class TemporaryExtendedManagedType { + private final ManagedType managedType; + private final Map attributes; + private boolean done; + private boolean cascadingDeleteCycle; + + private TemporaryExtendedManagedType(ManagedType managedType, Map attributes) { + this.managedType = managedType; + this.attributes = attributes; + } + } + + private static final class ExtendedManagedTypeImpl implements ExtendedManagedType { + private final ManagedType managedType; + private final boolean hasCascadingDeleteCycle; + private final SingularAttribute idAttribute; + private final Map attributes; + + private ExtendedManagedTypeImpl(ManagedType managedType, boolean hasCascadingDeleteCycle, Map attributes) { + this.managedType = managedType; + if (managedType instanceof EntityType) { + this.idAttribute = (SingularAttribute) JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedType); + } else { + this.idAttribute = null; + } + this.hasCascadingDeleteCycle = hasCascadingDeleteCycle; + this.attributes = attributes; + } + + @Override + public ManagedType getType() { + return managedType; + } + + @Override + public boolean hasCascadingDeleteCycle() { + return hasCascadingDeleteCycle; + } + + @Override + public SingularAttribute getIdAttribute() { + return idAttribute; + } + + @Override + @SuppressWarnings("unchecked") + public Map> getAttributes() { + return (Map>) (Map) attributes; + } + + @Override + @SuppressWarnings("unchecked") + public ExtendedAttribute getAttribute(String attributeName) { + AttributeEntry entry = attributes.get(attributeName); + if (entry == null) { + throw new IllegalArgumentException("Could not find attribute '" + attributeName + "' on managed type '" + managedType.getJavaType().getName() + "'"); + } + return entry; + } + } + + private static final class AttributeEntry implements ExtendedAttribute { + + private static final Map NO_MAPPINGS = new HashMap<>(); + + private final JpaProvider jpaProvider; + private final ManagedType ownerType; + private final Attribute attribute; + private final List> attributePath; + private final Class elementClass; + private final boolean hasCascadeDeleteCycle; + private final boolean isForeignJoinColumn; + private final boolean isColumnShared; + private final boolean isBag; + private final boolean isOrphanRemoval; + private final boolean isDeleteCascaded; + private final JpaProvider.ConstraintType[] joinTypeIndexedRequiresTreatFilter; + private final String mappedBy; + private final JoinTable joinTable; + private final String[] columnNames; + private final String[] columnTypes; + private final ConcurrentMap, Map> inverseAttributeCache = new ConcurrentHashMap<>(); + + public AttributeEntry(JpaProvider jpaProvider, ManagedType ownerType, Attribute attribute, String attributeName, Class fieldType, List> parents) { + this.jpaProvider = jpaProvider; + this.ownerType = ownerType; + this.attribute = attribute; + + this.attributePath = Collections.unmodifiableList(parents); + this.elementClass = fieldType; + this.isOrphanRemoval = jpaProvider.isOrphanRemoval(ownerType, attributeName); + this.isDeleteCascaded = jpaProvider.isDeleteCascaded(ownerType, attributeName); + this.hasCascadeDeleteCycle = false; + JoinType[] joinTypes = JoinType.values(); + JpaProvider.ConstraintType[] requiresTreatFilter = new JpaProvider.ConstraintType[joinTypes.length]; + if (ownerType instanceof EntityType) { + EntityType entityType = (EntityType) ownerType; + this.isForeignJoinColumn = jpaProvider.isForeignJoinColumn(entityType, attributeName); + this.isColumnShared = jpaProvider.isColumnShared(entityType, attributeName); + this.isBag = jpaProvider.isBag(entityType, attributeName); + this.mappedBy = jpaProvider.getMappedBy(entityType, attributeName); + this.joinTable = jpaProvider.getJoinTable(entityType, attributeName); + this.columnNames = jpaProvider.getColumnNames(entityType, attributeName); + this.columnTypes = jpaProvider.getColumnTypes(entityType, attributeName); + for (JoinType joinType : joinTypes) { + requiresTreatFilter[joinType.ordinal()] = jpaProvider.requiresTreatFilter(entityType, attributeName, joinType); + } + } else { + this.isForeignJoinColumn = false; + this.isColumnShared = false; + this.isBag = false; + this.mappedBy = null; + this.joinTable = null; + this.columnNames = null; + this.columnTypes = null; + } + this.joinTypeIndexedRequiresTreatFilter = requiresTreatFilter; + } + + private AttributeEntry(AttributeEntry original, boolean hasCascadeDeleteCycle) { + this.jpaProvider = original.jpaProvider; + this.ownerType = original.ownerType; + this.attribute = original.attribute; + this.attributePath = original.attributePath; + this.elementClass = original.elementClass; + this.hasCascadeDeleteCycle = hasCascadeDeleteCycle; + this.isForeignJoinColumn = original.isForeignJoinColumn; + this.isColumnShared = original.isColumnShared; + this.isBag = original.isBag; + this.isOrphanRemoval = original.isOrphanRemoval; + this.isDeleteCascaded = original.isDeleteCascaded; + this.joinTypeIndexedRequiresTreatFilter = original.joinTypeIndexedRequiresTreatFilter; + this.mappedBy = original.mappedBy; + this.joinTable = original.joinTable; + this.columnNames = original.columnNames; + this.columnTypes = original.columnTypes; + } + + @Override + public Map getWritableMappedByMappings(EntityType inverseType) { + Map mappings = inverseAttributeCache.get(inverseType); + if (mappings == null) { + mappings = jpaProvider.getWritableMappedByMappings(inverseType, (EntityType) ownerType, attribute.getName()); + if (mappings == null) { + inverseAttributeCache.putIfAbsent(inverseType, NO_MAPPINGS); + } else { + mappings = Collections.unmodifiableMap(mappings); + inverseAttributeCache.putIfAbsent(inverseType, mappings); + } + } else if (mappings == NO_MAPPINGS) { + // Special value to indicate "no mappings" + return null; + } + return mappings; + } + + @Override + public Attribute getAttribute() { + return attribute; + } + + @Override + public List> getAttributePath() { + return attributePath; + } + + @Override + public Class getElementClass() { + return elementClass; + } + + @Override + public boolean hasCascadingDeleteCycle() { + return hasCascadeDeleteCycle; + } + + @Override + public boolean isForeignJoinColumn() { + return isForeignJoinColumn; + } + + @Override + public boolean isColumnShared() { + return isColumnShared; + } + + @Override + public boolean isBag() { + return isBag; + } + + @Override + public boolean isOrphanRemoval() { + return isOrphanRemoval; + } + + @Override + public boolean isDeleteCascaded() { + return isDeleteCascaded; + } + + @Override + public JpaProvider.ConstraintType getJoinTypeIndexedRequiresTreatFilter(JoinType joinType) { + return joinTypeIndexedRequiresTreatFilter[joinType.ordinal()]; + } + + @Override + public String getMappedBy() { + return mappedBy; + } + + @Override + public JoinTable getJoinTable() { + return joinTable; + } + + @Override + public String[] getColumnNames() { + return columnNames; + } + + @Override + public String[] getColumnTypes() { + return columnTypes; + } + + public AttributeEntry withCascadingDeleteCycle() { + if (hasCascadeDeleteCycle) { + return this; + } + return new AttributeEntry<>(this, true); + } + } + private static class BasicTypeImpl implements BasicType { private final Class cls; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/InsertCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/InsertCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..4bf3bb40dc --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/InsertCollectionCriteriaBuilderImpl.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.InsertCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class InsertCollectionCriteriaBuilderImpl extends AbstractInsertCollectionCriteriaBuilder, Void> implements InsertCriteriaBuilder { + + public InsertCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class clazz, String collectionName) { + super(mainQuery, true, clazz, null, null, null, null, collectionName); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java index 9803bdb406..685b97ad0a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java @@ -54,6 +54,8 @@ import com.blazebit.persistence.spi.DbmsDialect; import com.blazebit.persistence.spi.DbmsModificationState; import com.blazebit.persistence.spi.DbmsStatementType; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; import com.blazebit.persistence.spi.JpaProvider; import com.blazebit.persistence.spi.ValuesStrategy; @@ -431,21 +433,21 @@ private Query getValuesExampleQuery(Class clazz, boolean identifiableReferenc sb.append("e."); Attribute attribute = attributeSet.iterator().next(); attributes[0] = attribute.getName(); - String[] columnTypes = metamodel.getAttributeColumnTypeMapping(clazz).get(attribute.getName()).getValue(); + String[] columnTypes = metamodel.getManagedType(ExtendedManagedType.class, clazz).getAttribute(attribute.getName()).getColumnTypes(); attributeParameter[0] = getCastedParameters(new StringBuilder(), mainQuery.dbmsDialect, columnTypes); pathExpressions[0] = com.blazebit.reflection.ExpressionUtils.getExpression(clazz, attributes[0]); sb.append(attributes[0]); sb.append(','); } else { Iterator> iter = attributeSet.iterator(); - Map> mapping = metamodel.getAttributeColumnTypeMapping(clazz); + Map mapping = metamodel.getManagedType(ExtendedManagedType.class, clazz).getAttributes(); StringBuilder paramBuilder = new StringBuilder(); for (int i = 0; i < attributes.length; i++) { sb.append("e."); Attribute attribute = iter.next(); attributes[i] = attribute.getName(); - Map.Entry entry = mapping.get(attribute.getName()); - String[] columnTypes = entry.getValue(); + ExtendedAttribute entry = mapping.get(attribute.getName()); + String[] columnTypes = entry.getColumnTypes(); attributeParameter[i] = getCastedParameters(paramBuilder, mainQuery.dbmsDialect, columnTypes); pathExpressions[i] = com.blazebit.reflection.ExpressionUtils.getExpression(clazz, attributes[i]); sb.append(attributes[i]); @@ -454,7 +456,7 @@ private Query getValuesExampleQuery(Class clazz, boolean identifiableReferenc // otherwise we would fetch all of the types attributes, but the VALUES clause can only ever contain the id if (attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.EMBEDDED) { - ManagedType managedAttributeType = metamodel.managedType(entry.getKey().getAttributeClass()); + ManagedType managedAttributeType = metamodel.managedType(entry.getElementClass()); Attribute attributeTypeIdAttribute = JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedAttributeType); sb.append('.'); sb.append(attributeTypeIdAttribute.getName()); @@ -639,9 +641,9 @@ String addRoot(String correlationPath, String rootAlias) { } } - ManagedType managedType = metamodel.managedType(attributeType); + Type type = metamodel.type(attributeType); JoinAliasInfo rootAliasInfo = new JoinAliasInfo(rootAlias, rootAlias, true, true, aliasManager); - JoinNode rootNode = JoinNode.createCorrelationRootNode(correlationParent, correlatedAttribute, managedType, treatEntityType, rootAliasInfo); + JoinNode rootNode = JoinNode.createCorrelationRootNode(correlationParent, correlatedAttribute, type, treatEntityType, rootAliasInfo); rootAliasInfo.setJoinNode(rootNode); rootNodes.add(rootNode); // register root alias in aliasManager diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinNode.java b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinNode.java index 80c07c37f1..8d97c94e48 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinNode.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinNode.java @@ -181,7 +181,7 @@ public static JoinNode createValuesRootNode(ManagedType nodeType, String valu return new JoinNode(nodeType, valuesFunction, valueCount, attributeCount, valueQuery, valuesClause, valuesAliases, aliasInfo); } - public static JoinNode createCorrelationRootNode(JoinNode correlationParent, String correlationPath, ManagedType nodeType, EntityType treatType, JoinAliasInfo aliasInfo) { + public static JoinNode createCorrelationRootNode(JoinNode correlationParent, String correlationPath, Type nodeType, EntityType treatType, JoinAliasInfo aliasInfo) { return new JoinNode(null, null, null, correlationParent, correlationPath, nodeType, treatType, null, aliasInfo); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java b/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java index 03a050ebe4..0b77e5c864 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java @@ -76,7 +76,7 @@ public static MainQuery create(CriteriaBuilderFactoryImpl cbf, EntityManager em, } ParameterManager parameterManager = new ParameterManager(); - return new MainQuery(cbf, em, cbf.createJpaProvider(em), dbmsDialect, registeredFunctions, parameterManager); + return new MainQuery(cbf, em, cbf.getJpaProvider(), dbmsDialect, registeredFunctions, parameterManager); } public final void registerMacro(String macroName, JpqlMacro jpqlMacro) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningDeleteCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningDeleteCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..68e8ba487a --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningDeleteCollectionCriteriaBuilderImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.ReturningDeleteCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class ReturningDeleteCollectionCriteriaBuilderImpl extends AbstractDeleteCollectionCriteriaBuilder, Y> implements ReturningDeleteCriteriaBuilder { + + public ReturningDeleteCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class clazz, String alias, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, false, clazz, alias, cteName, cteClass, result, listener, collectionName); + } + + @Override + protected void buildExternalQueryString(StringBuilder sbSelectFrom) { + super.buildExternalQueryString(sbSelectFrom); + applyJpaReturning(sbSelectFrom); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningInsertCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningInsertCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..1a4cf3d2be --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningInsertCollectionCriteriaBuilderImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.ReturningInsertCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class ReturningInsertCollectionCriteriaBuilderImpl extends AbstractInsertCollectionCriteriaBuilder, Y> implements ReturningInsertCriteriaBuilder { + + public ReturningInsertCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class clazz, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, false, clazz, cteName, cteClass, result, listener, collectionName); + } + + @Override + protected void buildExternalQueryString(StringBuilder sbSelectFrom) { + super.buildExternalQueryString(sbSelectFrom); + applyJpaReturning(sbSelectFrom); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningModificationCriteraBuilderFactoryImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningModificationCriteraBuilderFactoryImpl.java index 2729f2c29e..b288056f77 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningModificationCriteraBuilderFactoryImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningModificationCriteraBuilderFactoryImpl.java @@ -55,6 +55,18 @@ public ReturningDeleteCriteriaBuilder delete(Class deleteClass, Str return cb; } + @Override + public ReturningDeleteCriteriaBuilder deleteCollection(Class deleteOwnerClass, String collectionName) { + return deleteCollection(deleteOwnerClass, null, collectionName); + } + + @Override + public ReturningDeleteCriteriaBuilder deleteCollection(Class deleteOwnerClass, String alias, String collectionName) { + ReturningDeleteCollectionCriteriaBuilderImpl cb = new ReturningDeleteCollectionCriteriaBuilderImpl(mainQuery, deleteOwnerClass, alias, cteName, cteClass, result, listener, collectionName); + listener.onBuilderStarted(cb); + return cb; + } + @Override public ReturningUpdateCriteriaBuilder update(Class updateClass) { return update(updateClass, null); @@ -67,6 +79,18 @@ public ReturningUpdateCriteriaBuilder update(Class updateClass, Str return cb; } + @Override + public ReturningUpdateCriteriaBuilder updateCollection(Class updateOwnerClass, String collectionName) { + return updateCollection(updateOwnerClass, null, collectionName); + } + + @Override + public ReturningUpdateCriteriaBuilder updateCollection(Class updateOwnerClass, String alias, String collectionName) { + ReturningUpdateCollectionCriteriaBuilderImpl cb = new ReturningUpdateCollectionCriteriaBuilderImpl(mainQuery, updateOwnerClass, alias, cteName, cteClass, result, listener, collectionName); + listener.onBuilderStarted(cb); + return cb; + } + @Override public ReturningInsertCriteriaBuilder insert(Class insertClass) { ReturningInsertCriteriaBuilderImpl cb = new ReturningInsertCriteriaBuilderImpl(mainQuery, insertClass, cteName, cteClass, result, listener); @@ -74,4 +98,10 @@ public ReturningInsertCriteriaBuilder insert(Class insertClass) { return cb; } + @Override + public ReturningInsertCriteriaBuilder insertCollection(Class insertOwnerClass, String collectionName) { + ReturningInsertCollectionCriteriaBuilderImpl cb = new ReturningInsertCollectionCriteriaBuilderImpl(mainQuery, insertOwnerClass, cteName, cteClass, result, listener, collectionName); + listener.onBuilderStarted(cb); + return cb; + } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningUpdateCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningUpdateCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..25fb0927af --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ReturningUpdateCollectionCriteriaBuilderImpl.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.ReturningUpdateCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class ReturningUpdateCollectionCriteriaBuilderImpl extends AbstractUpdateCollectionCriteriaBuilder, Y> implements ReturningUpdateCriteriaBuilder { + + public ReturningUpdateCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class clazz, String alias, String cteName, Class cteClass, Y result, CTEBuilderListener listener, String collectionName) { + super(mainQuery, false, clazz, alias, cteName, cteClass, result, listener, collectionName); + } + + @Override + protected void buildExternalQueryString(StringBuilder sbSelectFrom) { + super.buildExternalQueryString(sbSelectFrom); + applyJpaReturning(sbSelectFrom); + } + +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/UpdateCollectionCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/UpdateCollectionCriteriaBuilderImpl.java new file mode 100644 index 0000000000..90799397d0 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/UpdateCollectionCriteriaBuilderImpl.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl; + +import com.blazebit.persistence.UpdateCriteriaBuilder; + +/** + * + * @param The query result type + * @author Christian Beikov + * @since 1.2.0 + */ +public class UpdateCollectionCriteriaBuilderImpl extends AbstractUpdateCollectionCriteriaBuilder, Void> implements UpdateCriteriaBuilder { + + public UpdateCollectionCriteriaBuilderImpl(MainQuery mainQuery, Class updateOwnerClass, String alias, String collectionName) { + super(mainQuery, true, updateOwnerClass, alias, null, null, null, null, collectionName); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionDeleteModificationQuerySpecification.java b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionDeleteModificationQuerySpecification.java new file mode 100644 index 0000000000..5c6cfa92bd --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionDeleteModificationQuerySpecification.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl.query; + +import com.blazebit.persistence.impl.AbstractCommonQueryBuilder; +import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.spi.DbmsModificationState; + +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class CollectionDeleteModificationQuerySpecification extends ModificationQuerySpecification { + + public static final String COLLECTION_BASE_QUERY_ALIAS = "_collection"; + + private final Query deleteExampleQuery; + private final String deleteSql; + private final Map columnExpressionRemappings; + + public CollectionDeleteModificationQuerySpecification(AbstractCommonQueryBuilder commonQueryBuilder, Query baseQuery, Query exampleQuery, Set parameterListNames, boolean recursive, List ctes, boolean shouldRenderCteNodes, + boolean isEmbedded, String[] returningColumns, Map includedModificationStates, Map returningAttributeBindingMap, Query deleteExampleQuery, String deleteSql, Map columnExpressionRemappings) { + super(commonQueryBuilder, baseQuery, exampleQuery, parameterListNames, recursive, ctes, shouldRenderCteNodes, isEmbedded, returningColumns, includedModificationStates, returningAttributeBindingMap); + this.deleteExampleQuery = deleteExampleQuery; + this.deleteSql = deleteSql; + this.columnExpressionRemappings = columnExpressionRemappings; + } + + @Override + protected void initialize() { + List participatingQueries = new ArrayList(); + + StringBuilder sqlSb = new StringBuilder(extendedQuerySupport.getSql(em, baseQuery)); + + // Replace the "select ... from ..." part of the base query by the "delete from collectionTable" part + int whereIndex = SqlUtils.indexOfWhere(sqlSb); + if (whereIndex == -1) { + sqlSb.setLength(0); + sqlSb.append(deleteSql); + } else { + sqlSb.replace(0, whereIndex, deleteSql); + } + + remapColumnExpressions(sqlSb, columnExpressionRemappings); + + StringBuilder withClause = applyCtes(sqlSb, baseQuery, participatingQueries); + // NOTE: CTEs will only be added, if this is a subquery + Map addedCtes = applyExtendedSql(sqlSb, false, isEmbedded, withClause, returningColumns, includedModificationStates); + participatingQueries.add(baseQuery); + + // Some dbms like DB2 will need to wrap modification queries in select queries when using CTEs + boolean hasCtes = withClause != null && withClause.length() != 0 || addedCtes != null && !addedCtes.isEmpty(); + if (hasCtes && returningAttributeBindingMap.isEmpty() && !dbmsDialect.usesExecuteUpdateWhenWithClauseInModificationQuery()) { + query = exampleQuery; + } else { + query = deleteExampleQuery; + } + + this.sql = sqlSb.toString(); + this.participatingQueries = participatingQueries; + this.addedCtes = addedCtes; + this.dirty = false; + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionInsertModificationQuerySpecification.java b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionInsertModificationQuerySpecification.java new file mode 100644 index 0000000000..9452cd30d4 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionInsertModificationQuerySpecification.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl.query; + +import com.blazebit.persistence.impl.AbstractCommonQueryBuilder; +import com.blazebit.persistence.spi.DbmsModificationState; + +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class CollectionInsertModificationQuerySpecification extends ModificationQuerySpecification { + + private final Query insertExampleQuery; + private final String insertSql; + + public CollectionInsertModificationQuerySpecification(AbstractCommonQueryBuilder commonQueryBuilder, Query baseQuery, Query exampleQuery, Set parameterListNames, + List keyRestrictedLeftJoinAliases, List entityFunctionNodes, boolean recursive, List ctes, boolean shouldRenderCteNodes, + boolean isEmbedded, String[] returningColumns, Map includedModificationStates, Map returningAttributeBindingMap, Query insertExampleQuery, String insertSql) { + super(commonQueryBuilder, baseQuery, exampleQuery, parameterListNames, keyRestrictedLeftJoinAliases, entityFunctionNodes, recursive, ctes, shouldRenderCteNodes, isEmbedded, returningColumns, includedModificationStates, returningAttributeBindingMap); + this.insertExampleQuery = insertExampleQuery; + this.insertSql = insertSql; + } + + @Override + protected void initialize() { + List participatingQueries = new ArrayList(); + + String sql = extendedQuerySupport.getSql(em, baseQuery); + StringBuilder sqlSb = applySqlTransformations(baseQuery, sql, participatingQueries); + sqlSb.insert(0, insertSql); + sqlSb.insert(insertSql.length(), ' '); + + StringBuilder withClause = applyCtes(sqlSb, baseQuery, participatingQueries); + // NOTE: CTEs will only be added, if this is a subquery + Map addedCtes = applyExtendedSql(sqlSb, false, isEmbedded, withClause, returningColumns, includedModificationStates); + participatingQueries.add(baseQuery); + + // Some dbms like DB2 will need to wrap modification queries in select queries when using CTEs + boolean hasCtes = withClause != null && withClause.length() != 0 || addedCtes != null && !addedCtes.isEmpty(); + if (hasCtes && returningAttributeBindingMap.isEmpty() && !dbmsDialect.usesExecuteUpdateWhenWithClauseInModificationQuery()) { + query = exampleQuery; + } else { + query = insertExampleQuery; + } + + this.sql = sqlSb.toString(); + this.participatingQueries = participatingQueries; + this.addedCtes = addedCtes; + this.dirty = false; + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionUpdateModificationQuerySpecification.java b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionUpdateModificationQuerySpecification.java new file mode 100644 index 0000000000..298b87a1a9 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CollectionUpdateModificationQuerySpecification.java @@ -0,0 +1,99 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.impl.query; + +import com.blazebit.persistence.impl.AbstractCommonQueryBuilder; +import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.spi.DbmsModificationState; + +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class CollectionUpdateModificationQuerySpecification extends ModificationQuerySpecification { + + public static final String COLLECTION_BASE_QUERY_ALIAS = "_collection"; + + private final Query updateExampleQuery; + private final String updateSql; + private final List setExpressionContainingUpdateQueries; + private final Map columnExpressionRemappings; + + public CollectionUpdateModificationQuerySpecification(AbstractCommonQueryBuilder commonQueryBuilder, Query baseQuery, Query exampleQuery, Set parameterListNames, boolean recursive, List ctes, boolean shouldRenderCteNodes, + boolean isEmbedded, String[] returningColumns, Map includedModificationStates, Map returningAttributeBindingMap, Query updateExampleQuery, String updateSql, List setExpressionContainingUpdateQueries, Map columnExpressionRemappings) { + super(commonQueryBuilder, baseQuery, exampleQuery, parameterListNames, recursive, ctes, shouldRenderCteNodes, isEmbedded, returningColumns, includedModificationStates, returningAttributeBindingMap); + this.updateExampleQuery = updateExampleQuery; + this.updateSql = updateSql; + this.setExpressionContainingUpdateQueries = setExpressionContainingUpdateQueries; + this.columnExpressionRemappings = columnExpressionRemappings; + } + + @Override + protected void initialize() { + List participatingQueries = new ArrayList(); + + StringBuilder sqlSb = new StringBuilder(extendedQuerySupport.getSql(em, baseQuery)); + StringBuilder setClauseSqlSb = new StringBuilder(updateSql); + + // The queries are in sequence, each containing set clause entries for the source or target table + for (Query updateQuery : setExpressionContainingUpdateQueries) { + participatingQueries.add(updateQuery); + String setExpressionSql = extendedQuerySupport.getSql(em, updateQuery); + int assignIndex = SqlUtils.indexOfWhere(setExpressionSql) + " where ".length(); + // TODO: fix this for row values/embeddables which might have parenthesis around or use OR + setClauseSqlSb.append(setExpressionSql, assignIndex, setExpressionSql.length()); + setClauseSqlSb.append(','); + } + setClauseSqlSb.setLength(setClauseSqlSb.length() - 1); + + // Replace the "select ... from ..." part of the base query by the "update collectionTable" part + int whereIndex = SqlUtils.indexOfWhere(sqlSb); + if (whereIndex == -1) { + sqlSb.setLength(0); + sqlSb.append(setClauseSqlSb); + } else { + sqlSb.replace(0, whereIndex, setClauseSqlSb.toString()); + } + + remapColumnExpressions(sqlSb, columnExpressionRemappings); + + StringBuilder withClause = applyCtes(sqlSb, baseQuery, participatingQueries); + // NOTE: CTEs will only be added, if this is a subquery + Map addedCtes = applyExtendedSql(sqlSb, false, isEmbedded, withClause, returningColumns, includedModificationStates); + participatingQueries.add(baseQuery); + + // Some dbms like DB2 will need to wrap modification queries in select queries when using CTEs + boolean hasCtes = withClause != null && withClause.length() != 0 || addedCtes != null && !addedCtes.isEmpty(); + if (hasCtes && returningAttributeBindingMap.isEmpty() && !dbmsDialect.usesExecuteUpdateWhenWithClauseInModificationQuery()) { + query = exampleQuery; + } else { + query = updateExampleQuery; + } + + this.sql = sqlSb.toString(); + this.participatingQueries = participatingQueries; + this.addedCtes = addedCtes; + this.dirty = false; + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/query/CustomQuerySpecification.java b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CustomQuerySpecification.java index 592b2b3fb4..5ee70a72a6 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/query/CustomQuerySpecification.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/query/CustomQuerySpecification.java @@ -147,7 +147,7 @@ protected Map applyExtendedSql(StringBuilder sqlSb, boolean isSu } protected StringBuilder applyCtes(StringBuilder sqlSb, Query baseQuery, List participatingQueries) { - if (!shouldRenderCtes || (ctes.isEmpty() && statementType != DbmsStatementType.DELETE)) { + if (!shouldRenderCtes || ctes.isEmpty()) { return null; } // EntityAlias -> CteName @@ -202,7 +202,7 @@ public String extract(StringBuilder sb, int index, int currentPosition) { newSqlSb.append(','); } - String originalAlias = SqlUtils.extractAlias(sb, index); + String originalAlias = SqlUtils.extractAlias(sb); int aliasPosition = sb.length() - originalAlias.length() - 1; // Replace the original alias with the new one if (aliasPosition != -1 && sb.charAt(aliasPosition) == ' ') { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/query/ModificationQuerySpecification.java b/core/impl/src/main/java/com/blazebit/persistence/impl/query/ModificationQuerySpecification.java index 3561b68fb2..77f5590976 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/query/ModificationQuerySpecification.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/query/ModificationQuerySpecification.java @@ -33,22 +33,28 @@ */ public class ModificationQuerySpecification extends CustomQuerySpecification { - private final Query exampleQuery; - private final boolean isEmbedded; - private final String[] returningColumns; - private final Map includedModificationStates; - private final Map returningAttributeBindingMap; + protected final Query exampleQuery; + protected final boolean isEmbedded; + protected final String[] returningColumns; + protected final Map includedModificationStates; + protected final Map returningAttributeBindingMap; - private Query query; + protected Query query; public ModificationQuerySpecification(AbstractCommonQueryBuilder commonQueryBuilder, Query baseQuery, Query exampleQuery, Set parameterListNames, boolean recursive, List ctes, boolean shouldRenderCteNodes, + boolean isEmbedded, String[] returningColumns, Map includedModificationStates, Map returningAttributeBindingMap) { + this(commonQueryBuilder, baseQuery, exampleQuery, parameterListNames, Collections.emptyList(), Collections.emptyList(), recursive, ctes, shouldRenderCteNodes, isEmbedded, returningColumns, includedModificationStates, returningAttributeBindingMap); + } + + public ModificationQuerySpecification(AbstractCommonQueryBuilder commonQueryBuilder, Query baseQuery, Query exampleQuery, Set parameterListNames, + List keyRestrictedLeftJoinAliases, List entityFunctionNodes, boolean recursive, List ctes, boolean shouldRenderCteNodes, boolean isEmbedded, String[] returningColumns, Map includedModificationStates, Map returningAttributeBindingMap) { - super(commonQueryBuilder, baseQuery, parameterListNames, null, null, Collections.EMPTY_LIST, Collections.EMPTY_LIST, recursive, ctes, shouldRenderCteNodes); + super(commonQueryBuilder, baseQuery, parameterListNames, null, null, keyRestrictedLeftJoinAliases, entityFunctionNodes, recursive, ctes, shouldRenderCteNodes); this.exampleQuery = exampleQuery; this.isEmbedded = isEmbedded; this.returningColumns = returningColumns; this.includedModificationStates = includedModificationStates; - this.returningAttributeBindingMap = new HashMap(returningAttributeBindingMap); + this.returningAttributeBindingMap = new HashMap<>(returningAttributeBindingMap); } @Override @@ -78,7 +84,8 @@ public Query getBaseQuery() { protected void initialize() { List participatingQueries = new ArrayList(); - StringBuilder sqlSb = new StringBuilder(extendedQuerySupport.getSql(em, baseQuery)); + String sqlQuery = extendedQuerySupport.getSql(em, baseQuery); + StringBuilder sqlSb = applySqlTransformations(baseQuery, sqlQuery, participatingQueries); StringBuilder withClause = applyCtes(sqlSb, baseQuery, participatingQueries); // NOTE: CTEs will only be added, if this is a subquery Map addedCtes = applyExtendedSql(sqlSb, false, isEmbedded, withClause, returningColumns, includedModificationStates); @@ -97,4 +104,20 @@ protected void initialize() { this.addedCtes = addedCtes; this.dirty = false; } + + protected final void remapColumnExpressions(StringBuilder sqlSb, Map columnExpressionRemappings) { + // Replace usages of the owner entities id columns by the corresponding join table id columns + for (Map.Entry entry : columnExpressionRemappings.entrySet()) { + String sourceExpression = entry.getKey(); + String targetExpression = entry.getValue(); + if (sourceExpression.equals(targetExpression)) { + continue; + } + int index = 0; + while ((index = sqlSb.indexOf(sourceExpression, index)) != -1) { + sqlSb.replace(index, index + sourceExpression.length(), targetExpression); + index += targetExpression.length() - sourceExpression.length(); + } + } + } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java b/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java index 08f1438ff5..e6bc4c5059 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java @@ -28,6 +28,7 @@ public class SqlUtils { private static final String SELECT = "select "; + private static final String SET = " set "; private static final String FROM = " from "; private static final String WITH = "with "; private static final String WHERE = " where "; @@ -36,6 +37,7 @@ public class SqlUtils { private static final String FROM_FINAL_TABLE = " from final table ("; private static final String NEXT_VALUE_FOR = "next value for "; private static final PatternFinder SELECT_FINDER = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(SELECT)); + private static final PatternFinder SET_FINDER = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(SET)); private static final PatternFinder FROM_FINDER = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(FROM)); private static final PatternFinder WITH_FINDER = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(WITH)); private static final PatternFinder WHERE_FINDER = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(WHERE)); @@ -51,21 +53,21 @@ public static interface SelectItemExtractor { private static final SelectItemExtractor ALIAS_EXTRACTOR = new SelectItemExtractor() { @Override public String extract(StringBuilder sb, int index, int currentPosition) { - return extractAlias(sb, index); + return extractAlias(sb); } }; private static final SelectItemExtractor EXPRESSION_EXTRACTOR = new SelectItemExtractor() { @Override public String extract(StringBuilder sb, int index, int currentPosition) { - return extractExpression(sb, index); + return extractExpression(sb); } }; private static final SelectItemExtractor COLUMN_EXTRACTOR = new SelectItemExtractor() { @Override public String extract(StringBuilder sb, int index, int currentPosition) { - return extractColumn(sb, index); + return extractColumn(sb); } }; @@ -158,6 +160,21 @@ public static List getExpressionItems(CharSequence sql, int i, int end, return selectItems; } + public static CharSequence getSetElementSequence(CharSequence sql) { + int setIndex = SET_FINDER.indexIn(sql); + if (setIndex == -1) { + return null; + } + + setIndex += SET.length(); + int whereIndex = indexOfWhere(sql); + if (whereIndex == -1) { + whereIndex = sql.length(); + } + + return sql.subSequence(setIndex, whereIndex); + } + /** * Finds the toplevel SELECT keyword in an arbitrary SELECT query. * @@ -330,7 +347,40 @@ public static int[] indexOfFinalTableSubquery(CharSequence sql, int selectIndex) return new int[] { 0, sql.length() }; } - public static String extractAlias(StringBuilder sb, int index) { + /** + * Finds the table name within a FROM clause of an arbitrary SELECT query. + * + * @param sql The SQL query + * @param tableName The table name to look for + * @return The index of the table name or -1 if it couldn't be found + */ + public static int indexOfTableName(CharSequence sql, String tableName) { + int startIndex = FROM_FINDER.indexIn(sql, 0); + if (startIndex == -1) { + return -1; + } + startIndex += FROM.length(); + int whereIndex = indexOfWhere(sql); + if (whereIndex == -1) { + whereIndex = sql.length(); + } + + PatternFinder finder = new QuotedIdentifierAwarePatternFinder(new BoyerMooreCaseInsensitiveAsciiFirstPatternFinder(" " + tableName + " ")); + int index = finder.indexIn(sql, startIndex, whereIndex); + if (index == -1) { + return -1; + } + + return index + 1; + } + + /** + * Extracts the alias part of a select item expression. + * + * @param sb The string builder containing the select item expression + * @return The alias of the select item expression + */ + public static String extractAlias(StringBuilder sb) { int aliasEndCharIndex = findLastNonWhitespace(sb); QuoteMode mode = QuoteMode.NONE.onCharBackwards(sb.charAt(aliasEndCharIndex)); int endIndex = aliasEndCharIndex; @@ -353,7 +403,57 @@ public static String extractAlias(StringBuilder sb, int index) { return sb.substring(aliasBeforeIndex + 1, aliasEndCharIndex + 1); } - private static String extractExpression(StringBuilder sb, int index) { + /** + * Extracts the next alias from the given expression starting the given index. + * + * @param sb The char sequence containing the alias + * @param index The start index + * @return The next alias of the char sequence + */ + public static String extractAlias(CharSequence sb, int index) { + int aliasBeginCharIndex = skipWhitespaces(sb, index); + + int asIndex = AS_FINDER.indexIn(sb, aliasBeginCharIndex - 1); + if (asIndex != -1) { + aliasBeginCharIndex = skipWhitespaces(sb, asIndex + AS.length()); + } + + QuoteMode mode = QuoteMode.NONE; + int i = aliasBeginCharIndex; + int end = sb.length(); + while (i < end) { + final char c = sb.charAt(i); + mode = mode.onChar(c); + + // When we encounter the next whitespace, the alias ended + if (mode == QuoteMode.NONE && Character.isWhitespace(c)) { + break; + } + + i++; + } + return sb.subSequence(aliasBeginCharIndex, i).toString(); + } + + private static int skipWhitespaces(CharSequence charSequence, int index) { + while (Character.isWhitespace(charSequence.charAt(index))) { + int nextIndex = index + 1; + if (nextIndex == charSequence.length()) { + return nextIndex; + } else { + index = nextIndex; + } + } + return index; + } + + /** + * Extracts the expression part of a select item expression. + * + * @param sb The string builder containing the select item expression + * @return The expression part of the select item expression + */ + private static String extractExpression(StringBuilder sb) { int asIndex = AS_FINDER.indexIn(sb); if (asIndex == -1) { return sb.toString(); @@ -362,7 +462,13 @@ private static String extractExpression(StringBuilder sb, int index) { return sb.substring(0, asIndex); } - private static String extractColumn(StringBuilder sb, int index) { + /** + * Extracts the column name part of a select item expression. + * + * @param sb The string builder containing the select item expression + * @return The column name part of the select item expression + */ + private static String extractColumn(StringBuilder sb) { int asIndex = AS_FINDER.indexIn(sb); if (asIndex == -1) { return sb.substring(findLastDot(sb, sb.length()) + 1); diff --git a/core/parser/src/main/java/com/blazebit/persistence/impl/EntityMetamodel.java b/core/parser/src/main/java/com/blazebit/persistence/impl/EntityMetamodel.java index 25a6c21215..6427c6761c 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/impl/EntityMetamodel.java +++ b/core/parser/src/main/java/com/blazebit/persistence/impl/EntityMetamodel.java @@ -25,7 +25,7 @@ * This is a wrapper around the JPA {@link javax.persistence.metamodel.Metamodel} that allows additionally efficient access by other attributes than a Class. * * @author Christian Beikov - * @since 1.2 + * @since 1.2.0 */ public interface EntityMetamodel extends Metamodel { @@ -40,4 +40,7 @@ public interface EntityMetamodel extends Metamodel { public ManagedType getManagedType(Class cls); public Type type(Class cls); + + public T getManagedType(Class cls, Class managedType); + } diff --git a/core/parser/src/main/java/com/blazebit/persistence/impl/util/JpaMetamodelUtils.java b/core/parser/src/main/java/com/blazebit/persistence/impl/util/JpaMetamodelUtils.java index ef3d17ef04..6658bff3b2 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/impl/util/JpaMetamodelUtils.java +++ b/core/parser/src/main/java/com/blazebit/persistence/impl/util/JpaMetamodelUtils.java @@ -18,6 +18,8 @@ import com.blazebit.persistence.impl.AttributePath; import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.impl.ListIndexAttribute; +import com.blazebit.persistence.impl.MapKeyAttribute; import com.blazebit.reflection.ReflectionUtils; import javax.persistence.metamodel.Attribute; @@ -34,6 +36,8 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.TypeVariable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; /** @@ -195,7 +199,16 @@ public static Class resolveFieldClass(Class baseClass, Attribute att public static SingularAttribute getIdAttribute(IdentifiableType entityType) { Class idClass = null; try { - idClass = entityType.getIdType().getJavaType(); + Type idType = entityType.getIdType(); + if (idType == null) { + // Hibernate treats ManyToOne's mapped as @Id differently, we need to scan the type and look for the id.. + for (SingularAttribute attribute : entityType.getSingularAttributes()) { + if (attribute.isId()) { + return attribute; + } + } + } + idClass = idType.getJavaType(); return entityType.getId(idClass); } catch (IllegalArgumentException e) { /** @@ -377,6 +390,68 @@ public static AttributePath getBasicAttributePath(Metamodel metamodel, ManagedTy return new AttributePath(attrPath, currentClass); } + public static AttributePath getJoinTableCollectionAttributePath(Metamodel metamodel, EntityType type, String attributePath, String collectionName) { + String trimmedPath = attributePath.trim(); + String indexStart = "index("; + String keyStart = "key("; + int collectionArgumentStart; + Attribute collectionFunction; + if (trimmedPath.regionMatches(true, 0, indexStart, 0, indexStart.length())) { + collectionArgumentStart = indexStart.length(); + collectionFunction = new ListIndexAttribute<>(type.getList(collectionName)); + } else if (trimmedPath.regionMatches(true, 0, keyStart, 0, keyStart.length())) { + collectionArgumentStart = keyStart.length(); + collectionFunction = new MapKeyAttribute<>(type.getMap(collectionName)); + } else { + int dotIndex = trimmedPath.indexOf('.'); + if (!trimmedPath.equals(collectionName) && (dotIndex == -1 || !trimmedPath.substring(0, dotIndex).equals(collectionName))) { + SingularAttribute idAttribute = getIdAttribute(type); + if (!idAttribute.getName().equals(attributePath)) { + throw new IllegalArgumentException("Only access to the owner type's id attribute '" + idAttribute.getName() + "' is allowed. Invalid access to different attribute through the expression: " + attributePath); + } + return new AttributePath(new ArrayList>(Collections.singletonList(idAttribute)), resolveFieldClass(type.getJavaType(), idAttribute)); + } + + Attribute collectionAttribute = getAttribute(type, collectionName); + Class targetClass = resolveFieldClass(type.getJavaType(), collectionAttribute); + if (dotIndex == -1) { + return new AttributePath(new ArrayList>(Collections.singletonList(collectionAttribute)), resolveFieldClass(targetClass, collectionAttribute)); + } + + String collectionElementAttributeName = trimmedPath.substring(dotIndex + 1); + ManagedType targetManagedType = metamodel.managedType(targetClass); + if (targetManagedType instanceof EntityType) { + EntityType targetEntityType = (EntityType) targetManagedType; + SingularAttribute idAttribute = getIdAttribute(targetEntityType); + String actualIdAttributeName = idAttribute.getName(); + if (!actualIdAttributeName.equals(collectionElementAttributeName)) { + throw new IllegalArgumentException("Only access to the target element type's id attribute '" + actualIdAttributeName + "' is allowed. Invalid access to different attribute through the expression: " + attributePath); + } + return new AttributePath(new ArrayList<>(Arrays.asList(collectionAttribute, idAttribute)), resolveFieldClass(targetClass, idAttribute)); + } else { + Attribute attribute = null; + Throwable cause = null; + try { + attribute = targetManagedType.getAttribute(collectionElementAttributeName); + } catch (IllegalArgumentException ex) { + cause = ex; + } + if (attribute == null) { + throw new IllegalArgumentException("Couldn't find attribute '" + collectionElementAttributeName + "' on managed type '" + targetClass.getName() + "'. Invalid access through the expression: " + attributePath, cause); + } + return new AttributePath(new ArrayList<>(Arrays.asList(collectionAttribute, attribute)), resolveFieldClass(targetClass, attribute)); + } + } + + // assume the last character is the closing parenthesis + String collectionAttributeName = trimmedPath.substring(collectionArgumentStart, trimmedPath.length() - 1); + if (!collectionAttributeName.equals(collectionName)) { + throw new IllegalArgumentException("Collection functions are only allowed to be used with the collection '" + collectionName + "'!. Invalid use in the expression: " + attributePath); + } + + return new AttributePath(new ArrayList>(Collections.singletonList(collectionFunction)), collectionFunction.getJavaType()); + } + public static boolean isJoinable(Attribute attr) { if (attr.isCollection()) { return true; diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java index 447b12f577..150d1b992d 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java @@ -164,7 +164,7 @@ public void setNameContainer(NameObjectContainer nameContainer) { this.nameContainer = nameContainer; } - @OneToMany(mappedBy = "document", cascade = { CascadeType.PERSIST }) + @OneToMany(mappedBy = "document", cascade = { CascadeType.PERSIST, CascadeType.REMOVE }) public Set getVersions() { return versions; } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/DocumentTupleEntity.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/DocumentTupleEntity.java index 5491a944f5..e0924a7703 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/DocumentTupleEntity.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/DocumentTupleEntity.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.testsuite.entity; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; import java.io.Serializable; @@ -38,7 +39,7 @@ public DocumentTupleEntity(Document element1, Document element2) { } @Id - @ManyToOne(optional = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) public Document getElement1() { return element1; } @@ -47,7 +48,7 @@ public void setElement1(Document element1) { this.element1 = element1; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) public Document getElement2() { return element2; } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedEmbeddable.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedEmbeddable.java index 4e30255a7c..f1ae491eab 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedEmbeddable.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedEmbeddable.java @@ -39,4 +39,20 @@ public IndexedEmbeddable(String value, String value2) { this.value = value; this.value2 = value2; } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getValue2() { + return value2; + } + + public void setValue2(String value2) { + this.value2 = value2; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode.java index eee0311968..9b7b025f24 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode.java @@ -18,6 +18,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -31,8 +32,39 @@ public class IndexedNode { @Id private Integer id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private Root parent; @Column(name = "list_index") private Integer index; + + public IndexedNode() { + } + + public IndexedNode(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Root getParent() { + return parent; + } + + public void setParent(Root parent) { + this.parent = parent; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode2.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode2.java index 6e7402a7fa..090d50a630 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode2.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/IndexedNode2.java @@ -18,6 +18,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -31,7 +32,7 @@ public class IndexedNode2 { @Id private Integer id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private Root2 parent; @Column(name = "list_index") private Integer index; diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedEmbeddable.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedEmbeddable.java index 4d9ad62f73..8194161b38 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedEmbeddable.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedEmbeddable.java @@ -39,4 +39,20 @@ public KeyedEmbeddable(String value, String value2) { this.value = value; this.value2 = value2; } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getValue2() { + return value2; + } + + public void setValue2(String value2) { + this.value2 = value2; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode.java index 8b7df2b29a..75b20dd613 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode.java @@ -18,6 +18,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -31,8 +32,39 @@ public class KeyedNode { @Id private Integer id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private Root parent; @Column(name = "map_key", length = 10) private String key; + + public KeyedNode() { + } + + public KeyedNode(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Root getParent() { + return parent; + } + + public void setParent(Root parent) { + this.parent = parent; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode2.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode2.java index 1ff70bc04b..77208bd36d 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode2.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/KeyedNode2.java @@ -18,6 +18,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -31,7 +32,7 @@ public class KeyedNode2 { @Id private Integer id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) private Root2 parent; @Column(name = "map_key", length = 10) private String key; diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/ProjectLeader.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/ProjectLeader.java index b9c070efe6..31dcf4d734 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/ProjectLeader.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/ProjectLeader.java @@ -23,6 +23,7 @@ import javax.persistence.Basic; import javax.persistence.DiscriminatorColumn; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Inheritance; @@ -73,7 +74,7 @@ public void setName(String name) { this.name = name; } - @ManyToOne(optional = true, targetEntity = Project.class) + @ManyToOne(fetch = FetchType.LAZY, optional = true, targetEntity = Project.class) public P getCurrentProject() { return currentProject; } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Root.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Root.java index 98f40128a8..94292bd2ce 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Root.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Root.java @@ -25,6 +25,8 @@ import javax.persistence.MapKeyColumn; import javax.persistence.OneToMany; import javax.persistence.OrderColumn; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,41 +40,133 @@ public class Root { @Id private Integer id; + private String name; @OneToMany @JoinTable(name = "list_one_to_many") @OrderColumn(name = "join_table_list_index") - private List indexedNodes; + private List indexedNodes = new ArrayList<>(); @OneToMany @JoinTable(name = "map_one_to_many") @MapKeyColumn(name = "join_table_map_key1", length = 10) - private Map keyedNodes; + private Map keyedNodes = new HashMap<>(); @ManyToMany @JoinTable(name = "list_many_to_many") @OrderColumn(name = "join_table_list_index") - private List indexedNodesMany; + private List indexedNodesMany = new ArrayList<>(); @ManyToMany @JoinTable(name = "map_many_to_many") @MapKeyColumn(name = "join_table_map_key2", length = 10) - private Map keyedNodesMany; + private Map keyedNodesMany = new HashMap<>(); @ManyToMany @JoinTable(name = "list_many_to_many_duplicate") @OrderColumn(name = "list_index") - private List indexedNodesManyDuplicate; + private List indexedNodesManyDuplicate = new ArrayList<>(); @ManyToMany @JoinTable(name = "map_many_to_many_duplicate") @MapKeyColumn(name = "map_key", length = 10) - private Map keyedNodesManyDuplicate; + private Map keyedNodesManyDuplicate = new HashMap<>(); @ElementCollection @CollectionTable(name = "list_collection_table") @OrderColumn(name = "list_index") - private List indexedNodesElementCollection; + private List indexedNodesElementCollection = new ArrayList<>(); @ElementCollection @CollectionTable(name = "map_collection_table") @MapKeyColumn(name = "map_key", length = 10) - private Map keyedNodesElementCollection; + private Map keyedNodesElementCollection = new HashMap<>(); + public Root() { + } + + public Root(Integer id) { + this.id = id; + } + + public Root(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getIndexedNodes() { + return indexedNodes; + } + + public void setIndexedNodes(List indexedNodes) { + this.indexedNodes = indexedNodes; + } + + public Map getKeyedNodes() { + return keyedNodes; + } + + public void setKeyedNodes(Map keyedNodes) { + this.keyedNodes = keyedNodes; + } + + public List getIndexedNodesMany() { + return indexedNodesMany; + } + + public void setIndexedNodesMany(List indexedNodesMany) { + this.indexedNodesMany = indexedNodesMany; + } + + public Map getKeyedNodesMany() { + return keyedNodesMany; + } + + public void setKeyedNodesMany(Map keyedNodesMany) { + this.keyedNodesMany = keyedNodesMany; + } + + public List getIndexedNodesManyDuplicate() { + return indexedNodesManyDuplicate; + } + + public void setIndexedNodesManyDuplicate(List indexedNodesManyDuplicate) { + this.indexedNodesManyDuplicate = indexedNodesManyDuplicate; + } + + public Map getKeyedNodesManyDuplicate() { + return keyedNodesManyDuplicate; + } + + public void setKeyedNodesManyDuplicate(Map keyedNodesManyDuplicate) { + this.keyedNodesManyDuplicate = keyedNodesManyDuplicate; + } + + public List getIndexedNodesElementCollection() { + return indexedNodesElementCollection; + } + + public void setIndexedNodesElementCollection(List indexedNodesElementCollection) { + this.indexedNodesElementCollection = indexedNodesElementCollection; + } + + public Map getKeyedNodesElementCollection() { + return keyedNodesElementCollection; + } + + public void setKeyedNodesElementCollection(Map keyedNodesElementCollection) { + this.keyedNodesElementCollection = keyedNodesElementCollection; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Version.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Version.java index e4debe2dd5..4ee2388c76 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Version.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Version.java @@ -21,6 +21,7 @@ import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -56,7 +57,7 @@ public void setId(Long id) { this.id = id; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) public Document getDocument() { return document; } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleDeleteTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleDeleteTest.java new file mode 100644 index 0000000000..99e6b4971a --- /dev/null +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleDeleteTest.java @@ -0,0 +1,330 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.testsuite; + +import com.blazebit.persistence.DeleteCriteriaBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.base.category.NoOpenJPA; +import com.blazebit.persistence.testsuite.entity.IndexedEmbeddable; +import com.blazebit.persistence.testsuite.entity.IndexedNode; +import com.blazebit.persistence.testsuite.entity.KeyedEmbeddable; +import com.blazebit.persistence.testsuite.entity.KeyedNode; +import com.blazebit.persistence.testsuite.entity.Root; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import javax.persistence.EntityManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +// NOTE: No advanced sql support for datanucleus, eclipselink and openjpa yet +@Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) +public class CollectionRoleDeleteTest extends AbstractCoreTest { + + @Before + public void setUp() { + cleanDatabase(); + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + Root r = new Root(1, "r"); + IndexedNode i1 = new IndexedNode(2); + KeyedNode k1 = new KeyedNode(3); + + r.getIndexedNodes().add(i1); + r.getIndexedNodesMany().add(i1); + r.getIndexedNodesManyDuplicate().add(i1); + + r.getIndexedNodesElementCollection().add(new IndexedEmbeddable("a", "b")); + + r.getKeyedNodes().put("a", k1); + r.getKeyedNodesMany().put("a", k1); + r.getKeyedNodesManyDuplicate().put("a", k1); + + r.getKeyedNodesElementCollection().put("a", new KeyedEmbeddable("a", "b")); + + em.persist(i1); + em.persist(k1); + em.persist(r); + } + }); + } + + @Override + protected Class[] getEntityClasses() { + return new Class[]{ + Root.class, + IndexedNode.class, + KeyedNode.class, + KeyedEmbeddable.class, + IndexedEmbeddable.class + }; + } + + private Root getRoot(EntityManager em) { + return cbf.create(em, Root.class) + .fetch("indexedNodes", "indexedNodesMany", "indexedNodesManyDuplicate", "indexedNodesElementCollection") + .fetch("keyedNodes", "keyedNodesMany", "keyedNodesManyDuplicate", "keyedNodesElementCollection") + .where("id").eq(1) + .getResultList() + .get(0); + } + + @Test + @Ignore("#501") + public void deleteIndexedAccessOtherAttributes() { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodes"); + try { + criteria.where("r.name"); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Only access to the owner type's id attribute")); + } + } + + @Test + public void deleteIndexed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodes"); + criteria.where("INDEX(indexedNodes)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodes.id").eq(2); + + assertEquals("DELETE FROM Root(indexedNodes) r" + + " WHERE INDEX(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(0, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteIndexedSubquery() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodes"); + criteria.whereExists() + .from(Root.class, "subRoot") + .where("subRoot.id").eqExpression("r.id") + .where("subRoot.name").eq("r") + .where("INDEX(r.indexedNodes)").eq(0) + .where("r.id").eq(1) + .where("r.indexedNodes.id").eq(2) + .end(); + + assertEquals("DELETE FROM Root(indexedNodes) r" + + " WHERE EXISTS (SELECT 1 FROM Root subRoot WHERE subRoot.id = r.id AND subRoot.name = :param_0 AND INDEX(_collection) = :param_1 AND r.id = :param_2 AND _collection.id = :param_3)", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(0, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteIndexedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodesMany"); + criteria.where("INDEX(indexedNodesMany)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesMany.id").eq(2); + + assertEquals("DELETE FROM Root(indexedNodesMany) r" + + " WHERE INDEX(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(0, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteIndexedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodesManyDuplicate"); + criteria.where("INDEX(indexedNodesManyDuplicate)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesManyDuplicate.id").eq(2); + + assertEquals("DELETE FROM Root(indexedNodesManyDuplicate) r" + + " WHERE INDEX(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(0, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteIndexedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "indexedNodesElementCollection"); + criteria.where("INDEX(indexedNodesElementCollection)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesElementCollection.value").eq("a"); + criteria.where("r.indexedNodesElementCollection.value2").eq("b"); + + assertEquals("DELETE FROM Root(indexedNodesElementCollection) r" + + " WHERE INDEX(_collection) = :param_0 AND r.id = :param_1 AND _collection.value = :param_2 AND _collection.value2 = :param_3", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(0, r.getIndexedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteKeyed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "keyedNodes"); + criteria.where("KEY(keyedNodes)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodes.id").eq(3); + + assertEquals("DELETE FROM Root(keyedNodes) r" + + " WHERE KEY(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(0, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteKeyedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "keyedNodesMany"); + criteria.where("KEY(keyedNodesMany)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesMany.id").eq(3); + + assertEquals("DELETE FROM Root(keyedNodesMany) r" + + " WHERE KEY(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(0, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteKeyedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "keyedNodesManyDuplicate"); + criteria.where("KEY(keyedNodesManyDuplicate)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesManyDuplicate.id").eq(3); + + assertEquals("DELETE FROM Root(keyedNodesManyDuplicate) r" + + " WHERE KEY(_collection) = :param_0 AND r.id = :param_1 AND _collection.id = :param_2", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(0, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + } + }); + } + + @Test + public void deleteKeyedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + DeleteCriteriaBuilder criteria = cbf.deleteCollection(em, Root.class, "r", "keyedNodesElementCollection"); + criteria.where("KEY(keyedNodesElementCollection)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesElementCollection.value").eq("a"); + criteria.where("r.keyedNodesElementCollection.value2").eq("b"); + + assertEquals("DELETE FROM Root(keyedNodesElementCollection) r" + + " WHERE KEY(_collection) = :param_0 AND r.id = :param_1 AND _collection.value = :param_2 AND _collection.value2 = :param_3", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(0, r.getKeyedNodesElementCollection().size()); + } + }); + } +} diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleInsertTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleInsertTest.java new file mode 100644 index 0000000000..ce43b6c32f --- /dev/null +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleInsertTest.java @@ -0,0 +1,343 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.testsuite; + +import com.blazebit.persistence.InsertCriteriaBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.base.category.NoOpenJPA; +import com.blazebit.persistence.testsuite.entity.IndexedEmbeddable; +import com.blazebit.persistence.testsuite.entity.IndexedNode; +import com.blazebit.persistence.testsuite.entity.KeyedEmbeddable; +import com.blazebit.persistence.testsuite.entity.KeyedNode; +import com.blazebit.persistence.testsuite.entity.Root; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import javax.persistence.EntityManager; + +import java.util.Collections; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +// NOTE: No advanced sql support for datanucleus, eclipselink and openjpa yet +@Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) +public class CollectionRoleInsertTest extends AbstractCoreTest { + + private static final Integer I2_ID = 4; + private static final Integer K2_ID = 5; + + @Before + public void setUp() { + cleanDatabase(); + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + Root r = new Root(1); + IndexedNode i1 = new IndexedNode(2); + KeyedNode k1 = new KeyedNode(3); + + r.getIndexedNodes().add(i1); + r.getIndexedNodesMany().add(i1); + r.getIndexedNodesManyDuplicate().add(i1); + + r.getIndexedNodesElementCollection().add(new IndexedEmbeddable("a", "b")); + + r.getKeyedNodes().put("a", k1); + r.getKeyedNodesMany().put("a", k1); + r.getKeyedNodesManyDuplicate().put("a", k1); + + r.getKeyedNodesElementCollection().put("a", new KeyedEmbeddable("a", "b")); + + em.persist(i1); + em.persist(k1); + em.persist(r); + + IndexedNode i2 = new IndexedNode(I2_ID); + em.persist(i2); + KeyedNode k2 = new KeyedNode(K2_ID); + em.persist(k2); + } + }); + } + + @Override + protected Class[] getEntityClasses() { + return new Class[]{ + Root.class, + IndexedNode.class, + KeyedNode.class, + KeyedEmbeddable.class, + IndexedEmbeddable.class + }; + } + + private Root getRoot(EntityManager em) { + return cbf.create(em, Root.class) + .fetch("indexedNodes", "indexedNodesMany", "indexedNodesManyDuplicate", "indexedNodesElementCollection") + .fetch("keyedNodes", "keyedNodesMany", "keyedNodesManyDuplicate", "keyedNodesElementCollection") + .where("id").eq(1) + .getResultList() + .get(0); + } + + @Test + public void insertIndexedAccessOtherAttributes() { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "indexedNodes"); + try { + criteria.bind("name"); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Only access to the owner type's id attribute")); + } + } + + @Test + public void insertIndexed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "indexedNodes"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("INDEX(indexedNodes)").select("1"); + criteria.bind("indexedNodes.id").select("4"); + + assertEquals("INSERT INTO Root.indexedNodes(INDEX(_collection), _collection.id, root.id)\n" + + "SELECT 1, 4, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(2, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodes().get(1).getId()); + } + }); + } + + @Test + public void insertIndexedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "indexedNodesMany"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("INDEX(indexedNodesMany)").select("1"); + criteria.bind("indexedNodesMany.id").select("4"); + + assertEquals("INSERT INTO Root.indexedNodesMany(INDEX(_collection), _collection.id, root.id)\n" + + "SELECT 1, 4, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(2, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodesMany().get(1).getId()); + } + }); + } + + @Test + public void insertIndexedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "indexedNodesManyDuplicate"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("INDEX(indexedNodesManyDuplicate)").select("1"); + criteria.bind("indexedNodesManyDuplicate.id").select("4"); + + assertEquals("INSERT INTO Root.indexedNodesManyDuplicate(INDEX(_collection), _collection.id, root.id)\n" + + "SELECT 1, 4, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(2, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodesManyDuplicate().get(1).getId()); + } + }); + } + + @Test + public void insertIndexedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "indexedNodesElementCollection"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("INDEX(indexedNodesElementCollection)").select("1"); + criteria.bind("indexedNodesElementCollection.value").select("'B'"); + criteria.bind("indexedNodesElementCollection.value2").select("'P'"); + + assertEquals("INSERT INTO Root.indexedNodesElementCollection(INDEX(_collection), _collection.value, _collection.value2, root.id)\n" + + "SELECT 1, 'B', 'P', 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(2, r.getIndexedNodesElementCollection().size()); + + assertEquals("B", r.getIndexedNodesElementCollection().get(1).getValue()); + assertEquals("P", r.getIndexedNodesElementCollection().get(1).getValue2()); + } + }); + } + + @Test + public void insertKeyed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "keyedNodes"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("KEY(keyedNodes)").select("'b'"); + criteria.bind("keyedNodes.id").select("5"); + + assertEquals("INSERT INTO Root.keyedNodes(KEY(_collection), _collection.id, root.id)\n" + + "SELECT 'b', 5, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(2, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodes().get("b").getId()); + } + }); + } + + @Test + public void insertKeyedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "keyedNodesMany"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("KEY(keyedNodesMany)").select("'b'"); + criteria.bind("keyedNodesMany.id").select("5"); + + assertEquals("INSERT INTO Root.keyedNodesMany(KEY(_collection), _collection.id, root.id)\n" + + "SELECT 'b', 5, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(2, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodesMany().get("b").getId()); + } + }); + } + + @Test + public void insertKeyedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "keyedNodesManyDuplicate"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("KEY(keyedNodesManyDuplicate)").select("'b'"); + criteria.bind("keyedNodesManyDuplicate.id").select("5"); + + assertEquals("INSERT INTO Root.keyedNodesManyDuplicate(KEY(_collection), _collection.id, root.id)\n" + + "SELECT 'b', 5, 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(2, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodesManyDuplicate().get("b").getId()); + } + }); + } + + @Test + public void insertKeyedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + InsertCriteriaBuilder criteria = cbf.insertCollection(em, Root.class, "keyedNodesElementCollection"); + criteria.fromValues(Integer.class, "valuesAlias", Collections.singletonList(0)); + criteria.bind("id").select("1"); + criteria.bind("KEY(keyedNodesElementCollection)").select("'b'"); + criteria.bind("keyedNodesElementCollection.value").select("'B'"); + criteria.bind("keyedNodesElementCollection.value2").select("'P'"); + + assertEquals("INSERT INTO Root.keyedNodesElementCollection(KEY(_collection), _collection.value, _collection.value2, root.id)\n" + + "SELECT 'b', 'B', 'P', 1" + + " FROM (VALUES (?)) valuesAlias", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(2, r.getKeyedNodesElementCollection().size()); + + assertEquals("B", r.getKeyedNodesElementCollection().get("b").getValue()); + assertEquals("P", r.getKeyedNodesElementCollection().get("b").getValue2()); + } + }); + } +} diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleUpdateTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleUpdateTest.java new file mode 100644 index 0000000000..2d2220a54d --- /dev/null +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CollectionRoleUpdateTest.java @@ -0,0 +1,351 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.testsuite; + +import com.blazebit.persistence.UpdateCriteriaBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.base.category.NoOpenJPA; +import com.blazebit.persistence.testsuite.entity.IndexedEmbeddable; +import com.blazebit.persistence.testsuite.entity.IndexedNode; +import com.blazebit.persistence.testsuite.entity.KeyedEmbeddable; +import com.blazebit.persistence.testsuite.entity.KeyedNode; +import com.blazebit.persistence.testsuite.entity.Root; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import javax.persistence.EntityManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +// NOTE: No advanced sql support for datanucleus, eclipselink and openjpa yet +@Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) +public class CollectionRoleUpdateTest extends AbstractCoreTest { + + private static final Integer I2_ID = 4; + private static final Integer K2_ID = 5; + + @Before + public void setUp() { + cleanDatabase(); + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + Root r = new Root(1); + IndexedNode i1 = new IndexedNode(2); + KeyedNode k1 = new KeyedNode(3); + + r.getIndexedNodes().add(i1); + r.getIndexedNodesMany().add(i1); + r.getIndexedNodesManyDuplicate().add(i1); + + r.getIndexedNodesElementCollection().add(new IndexedEmbeddable("a", "b")); + + r.getKeyedNodes().put("a", k1); + r.getKeyedNodesMany().put("a", k1); + r.getKeyedNodesManyDuplicate().put("a", k1); + + r.getKeyedNodesElementCollection().put("a", new KeyedEmbeddable("a", "b")); + + em.persist(i1); + em.persist(k1); + em.persist(r); + + IndexedNode i2 = new IndexedNode(I2_ID); + em.persist(i2); + KeyedNode k2 = new KeyedNode(K2_ID); + em.persist(k2); + } + }); + } + + @Override + protected Class[] getEntityClasses() { + return new Class[]{ + Root.class, + IndexedNode.class, + KeyedNode.class, + KeyedEmbeddable.class, + IndexedEmbeddable.class + }; + } + + private Root getRoot(EntityManager em) { + return cbf.create(em, Root.class) + .fetch("indexedNodes", "indexedNodesMany", "indexedNodesManyDuplicate", "indexedNodesElementCollection") + .fetch("keyedNodes", "keyedNodesMany", "keyedNodesManyDuplicate", "keyedNodesElementCollection") + .where("id").eq(1) + .getResultList() + .get(0); + } + + @Test + public void updateIndexedAccessOtherAttributes() { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "indexedNodes"); + try { + criteria.set("name", "BLA"); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Only access to the owner type's id attribute")); + } + } + + @Test + public void updateIndexed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "indexedNodes"); + criteria.set("INDEX(indexedNodes)", 0); + criteria.set("indexedNodes.id", I2_ID); + criteria.where("INDEX(indexedNodes)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodes.id").eq(2); + + assertEquals("UPDATE Root(indexedNodes) r" + + " SET INDEX(_collection) = :param_0,_collection.id = :param_1" + + " WHERE INDEX(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodes().get(0).getId()); + } + }); + } + + @Test + public void updateIndexedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "indexedNodesMany"); + criteria.set("INDEX(indexedNodesMany)", 0); + criteria.set("indexedNodesMany.id", I2_ID); + criteria.where("INDEX(indexedNodesMany)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesMany.id").eq(2); + + assertEquals("UPDATE Root(indexedNodesMany) r" + + " SET INDEX(_collection) = :param_0,_collection.id = :param_1" + + " WHERE INDEX(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodesMany().get(0).getId()); + } + }); + } + + @Test + public void updateIndexedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "indexedNodesManyDuplicate"); + criteria.set("INDEX(indexedNodesManyDuplicate)", 0); + criteria.set("indexedNodesManyDuplicate.id", I2_ID); + criteria.where("INDEX(indexedNodesManyDuplicate)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesManyDuplicate.id").eq(2); + + assertEquals("UPDATE Root(indexedNodesManyDuplicate) r" + + " SET INDEX(_collection) = :param_0,_collection.id = :param_1" + + " WHERE INDEX(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals(I2_ID, r.getIndexedNodesManyDuplicate().get(0).getId()); + } + }); + } + + @Test + public void updateIndexedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "indexedNodesElementCollection"); + criteria.set("INDEX(indexedNodesElementCollection)", 0); + criteria.set("indexedNodesElementCollection.value", "B"); + criteria.set("indexedNodesElementCollection.value2", "P"); + criteria.where("INDEX(indexedNodesElementCollection)").eq(0); + criteria.where("r.id").eq(1); + criteria.where("r.indexedNodesElementCollection.value").eq("a"); + criteria.where("r.indexedNodesElementCollection.value2").eq("b"); + + assertEquals("UPDATE Root(indexedNodesElementCollection) r" + + " SET INDEX(_collection) = :param_0,_collection.value = :param_1,_collection.value2 = :param_2" + + " WHERE INDEX(_collection) = :param_3 AND r.id = :param_4 AND _collection.value = :param_5 AND _collection.value2 = :param_6", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getIndexedNodes().size()); + assertEquals(1, r.getIndexedNodesMany().size()); + assertEquals(1, r.getIndexedNodesManyDuplicate().size()); + assertEquals(1, r.getIndexedNodesElementCollection().size()); + + assertEquals("B", r.getIndexedNodesElementCollection().get(0).getValue()); + assertEquals("P", r.getIndexedNodesElementCollection().get(0).getValue2()); + } + }); + } + + @Test + public void updateKeyed() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "keyedNodes"); + criteria.set("KEY(keyedNodes)", "b"); + criteria.set("keyedNodes.id", K2_ID); + criteria.where("KEY(keyedNodes)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodes.id").eq(3); + + assertEquals("UPDATE Root(keyedNodes) r" + + " SET KEY(_collection) = :param_0,_collection.id = :param_1" + + " WHERE KEY(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodes().get("b").getId()); + } + }); + } + + @Test + public void updateKeyedMany() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "keyedNodesMany"); + criteria.set("KEY(keyedNodesMany)", "b"); + criteria.set("keyedNodesMany.id", K2_ID); + criteria.where("KEY(keyedNodesMany)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesMany.id").eq(3); + + assertEquals("UPDATE Root(keyedNodesMany) r" + + " SET KEY(_collection) = :param_0,_collection.id = :param_1" + + " WHERE KEY(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodesMany().get("b").getId()); + } + }); + } + + @Test + public void updateKeyedManyDuplicate() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "keyedNodesManyDuplicate"); + criteria.set("KEY(keyedNodesManyDuplicate)", "b"); + criteria.set("keyedNodesManyDuplicate.id", K2_ID); + criteria.where("KEY(keyedNodesManyDuplicate)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesManyDuplicate.id").eq(3); + + assertEquals("UPDATE Root(keyedNodesManyDuplicate) r" + + " SET KEY(_collection) = :param_0,_collection.id = :param_1" + + " WHERE KEY(_collection) = :param_2 AND r.id = :param_3 AND _collection.id = :param_4", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals(K2_ID, r.getKeyedNodesManyDuplicate().get("b").getId()); + } + }); + } + + @Test + public void updateKeyedElementCollection() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + UpdateCriteriaBuilder criteria = cbf.updateCollection(em, Root.class, "r", "keyedNodesElementCollection"); + criteria.set("KEY(keyedNodesElementCollection)", "b"); + criteria.set("keyedNodesElementCollection.value", "B"); + criteria.set("keyedNodesElementCollection.value2", "P"); + criteria.where("KEY(keyedNodesElementCollection)").eq("a"); + criteria.where("r.id").eq(1); + criteria.where("r.keyedNodesElementCollection.value").eq("a"); + criteria.where("r.keyedNodesElementCollection.value2").eq("b"); + + assertEquals("UPDATE Root(keyedNodesElementCollection) r" + + " SET KEY(_collection) = :param_0,_collection.value = :param_1,_collection.value2 = :param_2" + + " WHERE KEY(_collection) = :param_3 AND r.id = :param_4 AND _collection.value = :param_5 AND _collection.value2 = :param_6", criteria.getQueryString()); + int updated = criteria.executeUpdate(); + Root r = getRoot(em); + + assertEquals(1, updated); + assertEquals(1, r.getKeyedNodes().size()); + assertEquals(1, r.getKeyedNodesMany().size()); + assertEquals(1, r.getKeyedNodesManyDuplicate().size()); + assertEquals(1, r.getKeyedNodesElementCollection().size()); + + assertEquals("B", r.getKeyedNodesElementCollection().get("b").getValue()); + assertEquals("P", r.getKeyedNodesElementCollection().get("b").getValue2()); + } + }); + } +} diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/treat/builder/AbstractTreatVariationsTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/treat/builder/AbstractTreatVariationsTest.java index 991d7ac778..651f6274cc 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/treat/builder/AbstractTreatVariationsTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/treat/builder/AbstractTreatVariationsTest.java @@ -18,6 +18,8 @@ package com.blazebit.persistence.testsuite.treat.builder; import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.impl.CachingJpaProvider; +import com.blazebit.persistence.spi.JpaProvider; import com.blazebit.persistence.testsuite.AbstractCoreTest; import com.blazebit.persistence.testsuite.entity.IntIdEntity; import com.blazebit.persistence.testsuite.treat.entity.Base; @@ -287,18 +289,38 @@ protected void assertRemoved(List list, Object expected) { Assert.fail(list + " does not contain expected entry: " + expected); } + private boolean isHibernate() { + return getJpaProvider().getClass().getName().contains("Hibernate"); + } + + private boolean isEclipseLink() { + return getJpaProvider().getClass().getName().contains("Eclipse"); + } + + private boolean isDataNucleus() { + return getJpaProvider().getClass().getName().contains("DataNucleus"); + } + + private JpaProvider getJpaProvider() { + if (jpaProvider instanceof CachingJpaProvider) { + return ((CachingJpaProvider) jpaProvider).getJpaProvider(); + } + + return jpaProvider; + } + protected void assumeQueryLanguageSupportsKeyDeReference() { Assume.assumeTrue("The JPA provider does not support de-referencing map keys", supportsMapKeyDeReference()); } protected void assumeHibernateSupportsMultiTpcWithTypeExpression() { // TODO: create issue for this - Assume.assumeTrue("Hibernate does not prefix the table per class discriminator column properly when using a type expression!", !strategy.equals("TablePerClass") || !jpaProvider.getClass().getName().contains("Hibernate")); + Assume.assumeTrue("Hibernate does not prefix the table per class discriminator column properly when using a type expression!", !strategy.equals("TablePerClass") || !isHibernate()); } protected void assumeHibernateSupportsMapKeyTypeExpressionInSubquery() { // TODO: create issue for this - Assume.assumeTrue("Hibernate currently does not support a type expression with a key in a subquery!", !jpaProvider.getClass().getName().contains("Hibernate")); + Assume.assumeTrue("Hibernate currently does not support a type expression with a key in a subquery!", !isHibernate()); } protected void assumeInverseSetCorrelationJoinsSubtypesWhenJoined() { @@ -314,11 +336,11 @@ protected void assumeTreatJoinWithRootTreatSupportedOrEmulated() { } protected void assumeCollectionTreatJoinWithRootTreatWorks() { - Assume.assumeTrue("Eclipselink does not support a treat join of a collection when containing a root treat!", !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support a treat join of a collection when containing a root treat!", !isEclipseLink()); } protected void assumeAccessTreatedOuterQueryVariableWorks() { - Assume.assumeTrue("Eclipselink does not support using a treat in a subquery referring to the outer query!", !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support using a treat in a subquery referring to the outer query!", !isEclipseLink()); } protected void assumeTreatInSubqueryCorrelationWorks() { @@ -326,41 +348,41 @@ protected void assumeTreatInSubqueryCorrelationWorks() { } protected void assumeMapInEmbeddableIsSupported() { - Assume.assumeTrue("Only Hibernate supports mapping a java.util.Map in an embeddable!", jpaProvider.getClass().getName().contains("Hibernate")); + Assume.assumeTrue("Only Hibernate supports mapping a java.util.Map in an embeddable!", isHibernate()); } protected void assumeTreatMapAssociationIsSupported() { // Seems the code assumes it's the "key" of the map when it's actually a "treat" - Assume.assumeTrue("Eclipselink does not support treating an association of type java.util.Map!", !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support treating an association of type java.util.Map!", !isEclipseLink()); } protected void assumeMultipleTreatJoinWithSingleTableIsNotBroken() { // So with Eclipselink having two different treat joins that use the same join path will result in a single sql join. Type restrictions are in the WHERE clause - Assume.assumeTrue("Eclipselink does not support multiple treat joins on the same relation with single table inheritance!", !strategy.equals("SingleTable") || !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support multiple treat joins on the same relation with single table inheritance!", !strategy.equals("SingleTable") || !isEclipseLink()); } protected void assumeMultipleInnerTreatJoinWithSingleTableIsNotBroken() { // So with Hibernate having two different inner treat joins that use the same join path will share the type restrictions when the association type uses single table inheritance - Assume.assumeTrue("Hibernate does not support multiple inner treat joins on the same relation with single table inheritance!", !strategy.equals("SingleTable") || !jpaProvider.getClass().getName().contains("Hibernate")); + Assume.assumeTrue("Hibernate does not support multiple inner treat joins on the same relation with single table inheritance!", !strategy.equals("SingleTable") || !isHibernate()); } protected void assumeLeftTreatJoinWithSingleTableIsNotBroken() { // Eclipselink puts the type restriction of a left treat join in the WHERE clause which is wrong. The type restriction should be part of the ON clause - Assume.assumeTrue("Eclipselink does not support left treat joins with single table inheritance properly as the type filter is not part of the join condition!", !strategy.equals("SingleTable") || !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support left treat joins with single table inheritance properly as the type filter is not part of the join condition!", !strategy.equals("SingleTable") || !isEclipseLink()); } protected void assumeLeftTreatJoinWithRootTreatIsNotBroken() { // Eclipselink puts the type restriction of a left treat join in the WHERE clause which is wrong. The type restriction should be part of the ON clause - Assume.assumeTrue("Eclipselink does not support left treat joins with root treats properly as the type filter is not part of the join condition!", !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipselink does not support left treat joins with root treats properly as the type filter is not part of the join condition!", !isEclipseLink()); } protected void assumeTreatInNonPredicateDoesNotFilter() { // Eclipselink creates type restrictions for every treat expression it encounters, regardless of the location - Assume.assumeTrue("Eclipelink does not support treat in non-predicates without filtering the result!", !jpaProvider.getClass().getName().contains("Eclipse")); + Assume.assumeTrue("Eclipelink does not support treat in non-predicates without filtering the result!", !isEclipseLink()); } private boolean supportsTablePerClassInheritance() { - return !jpaProvider.getClass().getName().contains("Eclipse"); + return !isEclipseLink(); } private void assumeTablePerClassSupportedWithTreat() { @@ -368,7 +390,7 @@ private void assumeTablePerClassSupportedWithTreat() { } private boolean supportsJoinedInheritance() { - return !jpaProvider.getClass().getName().contains("DataNucleus"); + return !isDataNucleus(); } private void assumeJoinedSupportedWithTreat() { diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/CascadeType.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/CascadeType.java index 249dd3d353..50b7d30ef8 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/CascadeType.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/CascadeType.java @@ -25,8 +25,9 @@ public enum CascadeType { /** - * Defines that whether {@link #PERSIST} and/or {@link #UPDATE} cascading should be applied - * is determined based on the element type and the availability of a setter. + * Defines that {@link #PERSIST} and/or {@link #UPDATE} cascading should be applied + * based on the element type and the availability of a setter. + * Whether {@link #DELETE} cascading is activated is determined based on the entity mapping. */ AUTO, /** @@ -36,5 +37,11 @@ public enum CascadeType { /** * Defines that existing elements should be updated. */ - UPDATE; + UPDATE, + /** + * Defines that when the declaring type of an attribute is deleted, elements of the attribute are deleted as well. + * Note that this cascading type is redundant if {@link UpdatableMapping#orphanRemoval()} is active for the attribute + * or when the attribute is an inverse attribute. The {@link MappingInverse#removeStrategy()} defines how the deletion is done. + */ + DELETE; } diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java index 0d1204849c..309312b61a 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java @@ -39,6 +39,30 @@ public interface EntityViewManager { */ public ViewMetamodel getMetamodel(); + /** + * Loads and returns an entity view of the given type having the given entity id. + * + * @param entityManager The entity manager to use for querying + * @param entityViewClass The entity view class to use + * @param id The id of the entity + * @param The type of the entity view class + * @return The loaded instance of the given entity view type with the id + * @since 1.2.0 + */ + public T find(EntityManager entityManager, Class entityViewClass, Object id); + + /** + * Loads and returns an entity view as determined by the given type {@link EntityViewSetting} having the given entity id. + * + * @param entityManager The entity manager to use for querying + * @param entityViewSetting The entity view setting to use + * @param id The id of the entity + * @param The type of the entity view class + * @return The loaded instance of the given entity view type with the id + * @since 1.2.0 + */ + public T find(EntityManager entityManager, EntityViewSetting> entityViewSetting, Object id); + /** * Creates a reference instance of the entity view class for the given id and returns it. * @@ -118,6 +142,27 @@ public interface EntityViewManager { */ public void updateFull(EntityManager entityManager, Object view); + /** + * Removes the entity represented by the given view. + * Also cascades deletes to attributes that have {@link CascadeType#DELETE} enabled. + * + * @param entityManager The entity manager to use for the removing + * @param view The view for which the entity should be removed + * @since 1.2.0 + */ + public void remove(EntityManager entityManager, Object view); + + /** + * Removes the entity represented by the entity type defiend for the given view and the given entity id. + * Also cascades deletes to attributes that have {@link CascadeType#DELETE} enabled. + * + * @param entityManager The entity manager to use for the removing + * @param entityViewClass The entity view class to use + * @param id The id of the entity + * @since 1.2.0 + */ + public void remove(EntityManager entityManager, Class entityViewClass, Object id); + /** * Applies the entity view setting to the given criteria builder. * diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/MappingInverse.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/MappingInverse.java index 5f340a1d36..925f983a25 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/MappingInverse.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/MappingInverse.java @@ -43,6 +43,8 @@ /** * The strategy to use for elements that were removed from this relation. + * Note that inverse mappings automatically have {@link CascadeType#DELETE} activated. + * When {@link UpdatableMapping#orphanRemoval()} is activated, only the {@link InverseRemoveStrategy#REMOVE} strategy is a valid configuration. * * @return The remove strategy */ diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/UpdatableMapping.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/UpdatableMapping.java index 263fbf265b..860fe444ae 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/UpdatableMapping.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/UpdatableMapping.java @@ -45,6 +45,18 @@ */ public boolean updatable() default true; + /** + * Specifies whether an element should be deleted when it is removed/replaced in the attribute value. + * + * If the backing entity of the updatable entity view defines orphan removal, this setting must be set to true, otherwise it is a configuration error. + * The rationale behind this, is that it should be apparent by looking at the attribute definition that orphan removal happens. + * If this weren't a configuration error, the #{@link FlushStrategy#ENTITY} flush strategy would behave differently than other strategies, + * as the JPA provider would do the orphan removal in case of an update whereas other strategies would adhere to the orphan removal defined on this attribute. + * + * @return Whether removed/replaced elements on the attribute should be deleted + */ + public boolean orphanRemoval() default false; + /** * The actions that should cascade for the runtime type of objects assigned to the annotated attribute. * Allows to override the default cascading strategy for a method attribute. diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/metamodel/MethodAttribute.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/metamodel/MethodAttribute.java index f3f8b5dc72..7ec60a8701 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/metamodel/MethodAttribute.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/metamodel/MethodAttribute.java @@ -80,7 +80,7 @@ public interface MethodAttribute extends Attribute { public boolean isOptimisticLockProtected(); /** - * Returns whether the persisting of a referenced object is allowed. + * Returns whether the persisting of referenced objects is allowed. * * @return Whether persisting should be done * @since 1.2.0 @@ -88,13 +88,29 @@ public interface MethodAttribute extends Attribute { public boolean isPersistCascaded(); /** - * Returns whether the updating of a referenced object is allowed. + * Returns whether the updating of referenced objects is allowed. * * @return Whether updating should be done * @since 1.2.0 */ public boolean isUpdateCascaded(); + /** + * Returns whether delete cascading for referenced objects should be done. + * + * @return Whether delete cascading should be done + * @since 1.2.0 + */ + public boolean isDeleteCascaded(); + + /** + * Returns whether orphaned objects should be deleted during an update. + * + * @return Whether orphaned objects are deleted + * @since 1.2.0 + */ + public boolean isOrphanRemoval(); + /** * Returns the subtypes that are allowed to be used when cascading {@link com.blazebit.persistence.view.CascadeType#PERSIST} events. * diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/spi/EntityViewMethodAttributeMapping.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/spi/EntityViewMethodAttributeMapping.java index 11c3995c99..89845de378 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/spi/EntityViewMethodAttributeMapping.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/spi/EntityViewMethodAttributeMapping.java @@ -54,6 +54,15 @@ public interface EntityViewMethodAttributeMapping extends EntityViewAttributeMap */ public Boolean getUpdatable(); + /** + * Returns whether the elements that are removed from the attribute should be deleted. + * If null(the default), whether the attribute is updatable is determined + * during the building phase({@link EntityViewConfiguration#createEntityViewManager(CriteriaBuilderFactory)}). + * + * @return Whether the attribute should do orphan removal or null if that should be determined during building phase + */ + public Boolean getOrphanRemoval(); + /** * Returns the cascade types that are configured for this attribute. * @@ -65,12 +74,13 @@ public interface EntityViewMethodAttributeMapping extends EntityViewAttributeMap * Set whether the attribute is updatable along with cascading configuration and the allowed subtypes. * * @param updatable Whether the attribute should be updatable + * @param orphanRemoval Whether orphaned objects should be deleted * @param cascadeTypes The enabled cascade types * @param subtypes The allowed subtypes for both, persist and update cascades * @param persistSubtypes The allowed subtypes for persist cascades * @param updateSubtypes The allowed subtypes for update cascades */ - public void setUpdatable(boolean updatable, CascadeType[] cascadeTypes, Class[] subtypes, Class[] persistSubtypes, Class[] updateSubtypes); + public void setUpdatable(boolean updatable, boolean orphanRemoval, CascadeType[] cascadeTypes, Class[] subtypes, Class[] persistSubtypes, Class[] updateSubtypes); /** * Returns the mapping to the inverse attribute relative to the element type or null if there is none. diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java index 274faa9b6a..183bdbc1c5 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java @@ -25,8 +25,9 @@ import com.blazebit.persistence.ObjectBuilder; import com.blazebit.persistence.impl.EntityMetamodel; import com.blazebit.persistence.impl.expression.ExpressionFactory; +import com.blazebit.persistence.impl.util.JpaMetamodelUtils; +import com.blazebit.persistence.spi.DbmsDialect; import com.blazebit.persistence.spi.JpaProvider; -import com.blazebit.persistence.spi.JpaProviderFactory; import com.blazebit.persistence.view.AttributeFilterProvider; import com.blazebit.persistence.view.EntityViewManager; import com.blazebit.persistence.view.EntityViewSetting; @@ -83,9 +84,12 @@ import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.SingularAttribute; import java.lang.reflect.Constructor; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -100,6 +104,7 @@ public class EntityViewManagerImpl implements EntityViewManager { private final CriteriaBuilderFactory cbf; private final JpaProvider jpaProvider; + private final DbmsDialect dbmsDialect; private final ExpressionFactory expressionFactory; private final AttributeAccessor entityIdAccessor; private final ViewMetamodelImpl metamodel; @@ -115,8 +120,9 @@ public class EntityViewManagerImpl implements EntityViewManager { public EntityViewManagerImpl(EntityViewConfigurationImpl config, CriteriaBuilderFactory cbf) { this.cbf = cbf; - this.jpaProvider = cbf.getService(JpaProviderFactory.class) - .createJpaProvider(null); + this.jpaProvider = cbf.getService(JpaProvider.class); + this.dbmsDialect = cbf.getService(DbmsDialect.class); + EntityMetamodel entityMetamodel = cbf.getService(EntityMetamodel.class); this.expressionFactory = cbf.getService(ExpressionFactory.class); this.entityIdAccessor = new EntityIdAttributeAccessor(cbf.getService(EntityManagerFactory.class).getPersistenceUnitUtil()); this.unsafeDisabled = !Boolean.valueOf(String.valueOf(config.getProperty(ConfigurationProperties.PROXY_UNSAFE_ALLOWED))); @@ -129,7 +135,7 @@ public EntityViewManagerImpl(EntityViewConfigurationImpl config, CriteriaBuilder MetamodelBuildingContext context = new MetamodelBuildingContextImpl( config.getProperties(), new DefaultBasicUserTypeRegistry(config.getUserTypeRegistry(), cbf), - cbf.getService(EntityMetamodel.class), + entityMetamodel, jpaProvider, cbf.getRegisteredFunctions(), expressionFactory, @@ -137,7 +143,7 @@ public EntityViewManagerImpl(EntityViewConfigurationImpl config, CriteriaBuilder config.getBootContext().getViewMappingMap(), errors ); - this.metamodel = new ViewMetamodelImpl(cbf, context, validateExpressions); + this.metamodel = new ViewMetamodelImpl(entityMetamodel, context, validateExpressions); if (!errors.isEmpty()) { StringBuilder sb = new StringBuilder(); @@ -197,6 +203,10 @@ public JpaProvider getJpaProvider() { return jpaProvider; } + public DbmsDialect getDbmsDialect() { + return dbmsDialect; + } + public AttributeAccessor getEntityIdAccessor() { return entityIdAccessor; } @@ -205,6 +215,22 @@ public ProxyFactory getProxyFactory() { return proxyFactory; } + @Override + public T find(EntityManager entityManager, Class entityViewClass, Object id) { + return find(entityManager, EntityViewSetting.create(entityViewClass), id); + } + + @Override + public T find(EntityManager entityManager, EntityViewSetting> entityViewSetting, Object id) { + ViewTypeImpl managedViewType = metamodel.view(entityViewSetting.getEntityViewClass()); + EntityType entityType = (EntityType) managedViewType.getJpaManagedType(); + SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute(entityType); + CriteriaBuilder cb = cbf.create(entityManager, managedViewType.getEntityClass()) + .where(idAttribute.getName()).eq(id); + List resultList = applySetting(entityViewSetting, cb).getResultList(); + return resultList.isEmpty() ? null : resultList.get(0); + } + @Override public T getReference(Class entityViewClass, Object id) { // TODO: cache constructor @@ -290,6 +316,48 @@ public void updateFull(EntityManager em, Object view) { update(em, view, true); } + @Override + public void remove(EntityManager entityManager, Object view) { + if (!(view instanceof EntityViewProxy)) { + throw new IllegalArgumentException("Can't remove non entity view object: " + view); + } + DefaultUpdateContext context = new DefaultUpdateContext(this, entityManager, false); + EntityViewProxy proxy = (EntityViewProxy) view; + Class entityViewClass = proxy.$$_getEntityViewClass(); + ManagedViewTypeImplementor viewType = metamodel.managedView(entityViewClass); + EntityViewUpdater updater = getUpdater(viewType, null); + try { + if (proxy.$$_isNew()) { + MutableStateTrackable updatableProxy = (MutableStateTrackable) proxy; + // If it has a parent, we can't just ignore this call + if (updatableProxy.$$_hasParent()) { + throw new IllegalStateException("Can't remove not-yet-persisted object [" + view + "] that is referenced by: " + updatableProxy.$$_getParent()); + } + } else { + updater.remove(context, proxy); + } + } catch (Throwable t) { + context.getSynchronizationStrategy().markRollbackOnly(); + ExceptionUtils.doThrow(t); + } + } + + @Override + public void remove(EntityManager entityManager, Class entityViewClass, Object id) { + DefaultUpdateContext context = new DefaultUpdateContext(this, entityManager, false); + ManagedViewTypeImplementor viewType = metamodel.managedView(entityViewClass); + if (viewType == null) { + throw new IllegalArgumentException("Can't remove non entity view object: " + entityViewClass.getName()); + } + EntityViewUpdater updater = getUpdater(viewType, null); + try { + updater.remove(context, id); + } catch (Throwable t) { + context.getSynchronizationStrategy().markRollbackOnly(); + ExceptionUtils.doThrow(t); + } + } + public void update(EntityManager em, Object view, boolean forceFull) { update(new DefaultUpdateContext(this, em, forceFull), view); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAction.java index eba5a78273..a4688a55ce 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAction.java @@ -29,7 +29,7 @@ */ public interface CollectionAction> { - public void doAction(T collection, UpdateContext context, ViewToEntityMapper mapper); + public void doAction(T collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener); public boolean containsObject(T collection, Object o); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAddAllAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAddAllAction.java index 57e71f1e10..c081d4bc65 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAddAllAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionAddAllAction.java @@ -62,7 +62,7 @@ public CollectionAddAllAction(Collection collection, boolean allowD @Override @SuppressWarnings("unchecked") - public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { if (mapper != null) { if (collection instanceof ArrayList) { ((ArrayList) collection).ensureCapacity(collection.size() + elements.size()); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionClearAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionClearAction.java index 1edf80e4ca..670e6dfecc 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionClearAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionClearAction.java @@ -31,7 +31,12 @@ public class CollectionClearAction, E> implements CollectionAction { @Override - public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { + if (removeListener != null) { + for (E e : collection) { + removeListener.onCollectionRemove(context, e); + } + } collection.clear(); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveAllAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveAllAction.java index d5788da67e..4561dfb6ce 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveAllAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveAllAction.java @@ -62,7 +62,12 @@ public CollectionRemoveAllAction(Collection collection, boolean allowDuplicat @Override @SuppressWarnings("unchecked") - public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { + if (removeListener != null) { + for (E e : collection) { + removeListener.onCollectionRemove(context, e); + } + } if (mapper != null) { for (Object e : elements) { collection.remove(mapper.applyToEntity(context, null, e)); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveListener.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveListener.java new file mode 100644 index 0000000000..94be537db1 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRemoveListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.collection; + +import com.blazebit.persistence.view.impl.update.UpdateContext; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public interface CollectionRemoveListener { + + public void onEntityCollectionRemove(UpdateContext context, Object element); + + public void onCollectionRemove(UpdateContext context, Object element); + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRetainAllAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRetainAllAction.java index ccc3a4e265..7545d15f49 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRetainAllAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/CollectionRetainAllAction.java @@ -38,14 +38,28 @@ public CollectionRetainAllAction(Collection collection) { } @Override - public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { if (mapper != null) { List mappedElements = new ArrayList<>(elements.size()); for (Object e : elements) { mappedElements.add(mapper.applyToEntity(context, null, e)); } + if (removeListener != null) { + for (E e : collection) { + if (!mappedElements.contains(e)) { + removeListener.onCollectionRemove(context, e); + } + } + } collection.retainAll(mappedElements); } else { + if (removeListener != null) { + for (E e : collection) { + if (!elements.contains(e)) { + removeListener.onCollectionRemove(context, e); + } + } + } collection.retainAll(elements); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAction.java index e7bc2082dc..a01e618762 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAction.java @@ -29,6 +29,6 @@ public interface ListAction> extends CollectionAction { @Override - public void doAction(T list, UpdateContext context, ViewToEntityMapper mapper); + public void doAction(T list, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAction.java index 544d2f4bd7..80f1fe5b60 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAction.java @@ -40,7 +40,7 @@ public ListAddAction(int index, E element) { @Override @SuppressWarnings("unchecked") - public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { if (mapper != null) { list.add(index, (E) mapper.applyToEntity(context, null, element)); } else { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAllAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAllAction.java index 51a71ba3c7..3b76c1ba49 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAllAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListAddAllAction.java @@ -41,7 +41,7 @@ public ListAddAllAction(int index, Collection collection) { @Override @SuppressWarnings("unchecked") - public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { if (mapper != null) { if (list instanceof ArrayList) { ((ArrayList) list).ensureCapacity(list.size() + elements.size()); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListRemoveAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListRemoveAction.java index c5a2f2765c..796abccacb 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListRemoveAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListRemoveAction.java @@ -37,8 +37,11 @@ public ListRemoveAction(int index) { } @Override - public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper) { - list.remove(index); + public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { + E removeElement = list.remove(index); + if (removeListener != null && removeElement != null) { + removeListener.onCollectionRemove(context, removeElement); + } } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListSetAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListSetAction.java index c8e2f7f3fc..47ddb3d3da 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListSetAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListSetAction.java @@ -40,11 +40,16 @@ public ListSetAction(int index, E element) { @Override @SuppressWarnings("unchecked") - public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper) { + public void doAction(C list, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { + E removedElement; if (mapper != null) { - list.set(index, (E) mapper.applyToEntity(context, null, element)); + removedElement = list.set(index, (E) mapper.applyToEntity(context, null, element)); } else { - list.set(index, element); + removedElement = list.set(index, element); + } + + if (removeListener != null && removedElement != null) { + removeListener.onCollectionRemove(context, removedElement); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapAction.java index 321016ca68..66483aa6a9 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapAction.java @@ -30,7 +30,7 @@ */ public interface MapAction> { - public void doAction(T map, UpdateContext context, MapViewToEntityMapper mapper); + public void doAction(T map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener); public Collection getAddedObjects(T collection); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapClearAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapClearAction.java index 76cac0b71c..5a377b28de 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapClearAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapClearAction.java @@ -32,7 +32,17 @@ public class MapClearAction, K, V> implements MapAction { @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + if (keyRemoveListener != null || valueRemoveListener != null) { + for (Map.Entry entry : map.entrySet()) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, entry.getKey()); + } + if (valueRemoveListener != null && entry.getValue() != null) { + valueRemoveListener.onCollectionRemove(context, entry.getValue()); + } + } + } map.clear(); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAction.java index 30c1f36f9f..f507219391 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAction.java @@ -42,7 +42,8 @@ public MapPutAction(K key, V value) { @Override @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + V oldValue; if (mapper != null) { K k = key; V v = value; @@ -54,9 +55,13 @@ public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) v = (V) mapper.getValueMapper().applyToEntity(context, null, v); } - map.put(k, v); + oldValue = map.put(k, v); } else { - map.put(key, value); + oldValue = map.put(key, value); + } + + if (valueRemoveListener != null && oldValue != null) { + valueRemoveListener.onCollectionRemove(context, oldValue); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAllAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAllAction.java index 3d1c89a652..11ccd9a6cd 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAllAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapPutAllAction.java @@ -42,7 +42,7 @@ public MapPutAllAction(Map map) { @Override @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null) { ViewToEntityMapper keyMapper = mapper.getKeyMapper(); ViewToEntityMapper valueMapper = mapper.getValueMapper(); @@ -58,10 +58,22 @@ public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) v = (V) valueMapper.applyToEntity(context, null, v); } - map.put(k, v); + V oldValue = map.put(k, v); + if (valueRemoveListener != null && oldValue != null) { + valueRemoveListener.onCollectionRemove(context, oldValue); + } } } else { - map.putAll(elements); + if (map.size() > 0 && valueRemoveListener != null) { + for (Map.Entry e : elements.entrySet()) { + V oldValue = map.put(e.getKey(), e.getValue()); + if (oldValue != null) { + valueRemoveListener.onCollectionRemove(context, oldValue); + } + } + } else { + map.putAll(elements); + } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAction.java index e060b2ce78..1e6ea2aea0 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAction.java @@ -40,11 +40,21 @@ public MapRemoveAction(Object key) { @Override @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + V value; if (mapper != null && mapper.getKeyMapper() != null) { - map.remove(mapper.getKeyMapper().applyToEntity(context, null, key)); + value = map.remove(mapper.getKeyMapper().applyToEntity(context, null, key)); } else { - map.remove(key); + value = map.remove(key); + } + + if (value != null) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, key); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, value); + } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllEntriesAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllEntriesAction.java index e82bcf572d..6b3d7292fb 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllEntriesAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllEntriesAction.java @@ -36,13 +36,17 @@ public class MapRemoveAllEntriesAction, K, V> implements Map private final Collection> elements; + public MapRemoveAllEntriesAction(Map.Entry entry) { + this(new ArrayList<>(Collections.singleton(entry))); + } + public MapRemoveAllEntriesAction(Collection> elements) { this.elements = elements; } @Override @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null) { Collection> entrySet = map.entrySet(); ViewToEntityMapper keyMapper = mapper.getKeyMapper(); @@ -60,10 +64,33 @@ public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) } Map.Entry e = new AbstractMap.SimpleEntry(k, v); - entrySet.remove(e); + if (entrySet.remove(e)) { + if (keyRemoveListener != null && k != null) { + keyRemoveListener.onCollectionRemove(context, k); + } + if (valueRemoveListener != null && v != null) { + valueRemoveListener.onCollectionRemove(context, v); + } + } } } else { - map.entrySet().removeAll(elements); + if (map.size() > 0 && (keyRemoveListener != null || valueRemoveListener != null)) { + Collection> entrySet = map.entrySet(); + for (Map.Entry e : elements) { + if (entrySet.remove(e)) { + K k = e.getKey(); + V v = e.getValue(); + if (keyRemoveListener != null && k != null) { + keyRemoveListener.onCollectionRemove(context, k); + } + if (valueRemoveListener != null && v != null) { + valueRemoveListener.onCollectionRemove(context, v); + } + } + } + } else { + map.entrySet().removeAll(elements); + } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllKeysAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllKeysAction.java index f8e45b4f6a..35558ecd34 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllKeysAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllKeysAction.java @@ -39,14 +39,36 @@ public MapRemoveAllKeysAction(Collection elements) { } @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null && mapper.getKeyMapper() != null) { - Collection collection = map.keySet(); for (Object e : elements) { - collection.remove(mapper.getKeyMapper().applyToEntity(context, null, e)); + K key = (K) mapper.getKeyMapper().applyToEntity(context, null, e); + V value = map.remove(key); + if (value != null) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, key); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, value); + } + } } } else { - map.keySet().removeAll(elements); + if (map.size() > 0 && (keyRemoveListener != null || valueRemoveListener != null)) { + for (Object k : elements) { + V v = map.remove(k); + if (v != null) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, k); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, v); + } + } + } + } else { + map.keySet().removeAll(elements); + } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllValuesAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllValuesAction.java index 1dc571b498..5ea5397361 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllValuesAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveAllValuesAction.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -34,19 +35,47 @@ public class MapRemoveAllValuesAction, K, V> implements MapA private final Collection elements; + public MapRemoveAllValuesAction(Object o) { + this(new ArrayList<>(Collections.singletonList(o))); + } + public MapRemoveAllValuesAction(Collection elements) { this.elements = elements; } @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null && mapper.getValueMapper() != null) { - Collection collection = map.values(); for (Object e : elements) { - collection.remove(mapper.getValueMapper().applyToEntity(context, null, e)); + V value = (V) mapper.getValueMapper().applyToEntity(context, null, e); + removeByValue(context, map, value, keyRemoveListener, valueRemoveListener); } } else { - map.values().removeAll(elements); + if (map.size() > 0 && (keyRemoveListener != null || valueRemoveListener != null)) { + for (Object e : elements) { + removeByValue(context, map, (V) e, keyRemoveListener, valueRemoveListener); + } + } else { + map.values().removeAll(elements); + } + } + } + + private void removeByValue(UpdateContext context, C map, V value, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + K k = entry.getKey(); + V v = entry.getValue(); + + if (value.equals(v)) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, k); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, v); + } + } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveEntryAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveEntryAction.java deleted file mode 100644 index 2503c96d99..0000000000 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveEntryAction.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2014 - 2018 Blazebit. - * - * Licensed 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 com.blazebit.persistence.view.impl.collection; - -import com.blazebit.persistence.view.impl.entity.MapViewToEntityMapper; -import com.blazebit.persistence.view.impl.update.UpdateContext; - -import java.util.AbstractMap; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * - * @author Christian Beikov - * @since 1.2.0 - */ -public class MapRemoveEntryAction, K, V> implements MapAction { - - private final Map.Entry entry; - - public MapRemoveEntryAction(Map.Entry entry) { - this.entry = entry; - } - - @Override - @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { - if (mapper != null) { - K k = entry.getKey(); - V v = entry.getValue(); - - if (mapper.getKeyMapper() != null) { - k = (K) mapper.getKeyMapper().applyToEntity(context, null, k); - } - if (mapper.getValueMapper() != null) { - v = (V) mapper.getValueMapper().applyToEntity(context, null, v); - } - - Map.Entry e = new AbstractMap.SimpleEntry<>(k, v); - map.entrySet().remove(e); - } else { - map.entrySet().remove(entry); - } - } - - @Override - public Collection getAddedObjects(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedObjects(C collection) { - V value = collection.get(entry.getKey()); - if (value == null) { - return Collections.emptyList(); - } else if (value.equals(entry.getValue())) { - if (entry.getKey() == null) { - return (Collection) Collections.singleton(entry.getValue()); - } else { - return Arrays.asList(entry.getKey(), entry.getValue()); - } - } else { - return Collections.emptyList(); - } - } - - @Override - public Collection getAddedKeys(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedKeys(C collection) { - if (entry.getKey() == null) { - return Collections.emptyList(); - } - V value = collection.get(entry.getKey()); - if (value == null || !value.equals(entry.getValue())) { - return Collections.emptyList(); - } - return (Collection) Collections.singletonList(entry.getKey()); - } - - @Override - public Collection getAddedElements(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedElements(C collection) { - V value = collection.get(entry.getKey()); - if (value == null) { - return Collections.emptyList(); - } else if (value.equals(entry.getValue())) { - return (Collection) Collections.singleton(value); - } else { - return Collections.emptyList(); - } - } - - @Override - @SuppressWarnings("unchecked") - public MapAction replaceObject(Object oldKey, Object oldValue, Object newKey, Object newValue) { - if (oldKey == entry.getKey() || oldValue == entry.getValue()) { - return new MapRemoveEntryAction(new AbstractMap.SimpleEntry<>(newKey, newValue)); - } - return null; - } - - @Override - public void addAction(List> actions, Collection addedKeys, Collection removedKeys, Collection addedElements, Collection removedElements) { - actions.add(this); - } - -} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveValueAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveValueAction.java deleted file mode 100644 index 9dbf9b2be5..0000000000 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRemoveValueAction.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2014 - 2018 Blazebit. - * - * Licensed 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 com.blazebit.persistence.view.impl.collection; - -import com.blazebit.persistence.view.impl.entity.MapViewToEntityMapper; -import com.blazebit.persistence.view.impl.update.UpdateContext; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * - * @author Christian Beikov - * @since 1.2.0 - */ -public class MapRemoveValueAction, K, V> implements MapAction { - - private final Object value; - - public MapRemoveValueAction(Object value) { - this.value = value; - } - - @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { - if (mapper != null && mapper.getValueMapper() != null) { - map.values().remove(mapper.getValueMapper().applyToEntity(context, null, value)); - } else { - map.values().remove(value); - } - } - - @Override - public Collection getAddedObjects(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedObjects(C collection) { - if (value == null) { - for (Map.Entry entry : collection.entrySet()) { - if (entry.getValue() == null) { - if (entry.getKey() == null) { - return Collections.emptyList(); - } - return (Collection) Collections.singletonList(entry.getKey()); - } - } - } else { - for (Map.Entry entry : collection.entrySet()) { - if (value.equals(entry.getValue())) { - if (entry.getKey() == null) { - return Collections.singletonList(value); - } - return Arrays.asList(entry.getKey(), value); - } - } - } - - return Collections.emptyList(); - } - - @Override - public Collection getAddedKeys(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedKeys(C collection) { - if (value == null) { - for (Map.Entry entry : collection.entrySet()) { - if (entry.getValue() == null) { - if (entry.getKey() == null) { - return Collections.emptyList(); - } - return (Collection) Collections.singletonList(entry.getKey()); - } - } - } else { - for (Map.Entry entry : collection.entrySet()) { - if (value.equals(entry.getValue())) { - if (entry.getKey() == null) { - return Collections.emptyList(); - } - return (Collection) Collections.singleton(entry.getKey()); - } - } - } - - return Collections.emptyList(); - } - - @Override - public Collection getAddedElements(C collection) { - return Collections.emptyList(); - } - - @Override - public Collection getRemovedElements(C collection) { - if (value == null) { - return Collections.emptyList(); - } else { - for (Map.Entry entry : collection.entrySet()) { - if (value.equals(entry.getValue())) { - return Collections.singletonList(value); - } - } - } - - return Collections.emptyList(); - } - - @Override - @SuppressWarnings("unchecked") - public MapAction replaceObject(Object oldKey, Object oldValue, Object newKey, Object newValue) { - if (oldValue == value) { - return new MapRemoveValueAction(newValue); - } - return null; - } - - @Override - public void addAction(List> actions, Collection addedKeys, Collection removedKeys, Collection addedElements, Collection removedElements) { - actions.add(this); - } - -} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllEntriesAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllEntriesAction.java index 02b279f58b..c1330e9ddc 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllEntriesAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllEntriesAction.java @@ -42,7 +42,7 @@ public MapRetainAllEntriesAction(Collection> elements) { @Override @SuppressWarnings("unchecked") - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null) { List> mappedElements = new ArrayList<>(elements.size()); ViewToEntityMapper keyMapper = mapper.getKeyMapper(); @@ -62,12 +62,29 @@ public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) mappedElements.add(new AbstractMap.SimpleEntry(k, v)); } + invokeRemoveListeners(context, map, mappedElements, keyRemoveListener, valueRemoveListener); map.entrySet().retainAll(mappedElements); } else { + invokeRemoveListeners(context, map, elements, keyRemoveListener, valueRemoveListener); map.entrySet().retainAll(elements); } } + private void invokeRemoveListeners(UpdateContext context, C map, Collection> elements, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + if (keyRemoveListener != null || valueRemoveListener != null) { + for (Map.Entry entry : map.entrySet()) { + if (!elements.contains(entry)) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, entry.getKey()); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, entry.getValue()); + } + } + } + } + } + @Override public Collection getAddedObjects(C collection) { return Collections.emptyList(); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllKeysAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllKeysAction.java index 0be453a457..ddbb4f53da 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllKeysAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllKeysAction.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -39,13 +40,33 @@ public MapRetainAllKeysAction(Collection elements) { } @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null && mapper.getKeyMapper() != null) { List mappedElements = new ArrayList<>(elements.size()); for (Object e : elements) { mappedElements.add(mapper.getKeyMapper().applyToEntity(context, null, e)); } - map.keySet().retainAll(mappedElements); + retainKeys(context, map, mappedElements, keyRemoveListener, valueRemoveListener); + } else { + retainKeys(context, map, elements, keyRemoveListener, valueRemoveListener); + } + } + + private void retainKeys(UpdateContext context, C map, Collection elements, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + if (keyRemoveListener != null || valueRemoveListener != null) { + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + if (!elements.contains(entry.getKey())) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, entry.getKey()); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, entry.getValue()); + } + iter.remove(); + } + } } else { map.keySet().retainAll(elements); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllValuesAction.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllValuesAction.java index d3e138834e..aa46d32d35 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllValuesAction.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/MapRetainAllValuesAction.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -39,13 +40,33 @@ public MapRetainAllValuesAction(Collection elements) { } @Override - public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void doAction(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (mapper != null && mapper.getValueMapper() != null) { List mappedElements = new ArrayList<>(elements.size()); for (Object e : elements) { mappedElements.add(mapper.getValueMapper().applyToEntity(context, null, e)); } - map.values().retainAll(mappedElements); + retainValues(context, map, mappedElements, keyRemoveListener, valueRemoveListener); + } else { + retainValues(context, map, elements, keyRemoveListener, valueRemoveListener); + } + } + + private void retainValues(UpdateContext context, C map, Collection elements, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { + if (keyRemoveListener != null || valueRemoveListener != null) { + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + if (!elements.contains(entry.getValue())) { + if (keyRemoveListener != null) { + keyRemoveListener.onCollectionRemove(context, entry.getKey()); + } + if (valueRemoveListener != null) { + valueRemoveListener.onCollectionRemove(context, entry.getValue()); + } + iter.remove(); + } + } } else { map.values().retainAll(elements); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingCollection.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingCollection.java index 0b5fdf249b..f38ae39aba 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingCollection.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingCollection.java @@ -306,10 +306,10 @@ protected final void addAction(CollectionAction action) { $$_markDirty(-1); } - public void replay(C collection, UpdateContext context, ViewToEntityMapper mapper) { + public void replay(C collection, UpdateContext context, ViewToEntityMapper mapper, CollectionRemoveListener removeListener) { if (actions != null) { for (CollectionAction action : resetActions(context)) { - action.doAction(collection, context, mapper); + action.doAction(collection, context, mapper, removeListener); } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySet.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySet.java index d48185229e..7870b6990d 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySet.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySet.java @@ -49,7 +49,7 @@ public boolean addAll(Collection> c) { @Override @SuppressWarnings("unchecked") public boolean remove(Object o) { - recordingMap.addAction(new MapRemoveEntryAction((Map.Entry) o)); + recordingMap.addAction(new MapRemoveAllEntriesAction((Map.Entry) o)); return delegate.remove(o); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySetIterator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySetIterator.java index 4297dc8888..9f51a9e496 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySetIterator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingEntrySetIterator.java @@ -51,8 +51,8 @@ public void remove() { throw new IllegalStateException(); } - iterator.remove(); recordingMap.addRemoveAction(current); + iterator.remove(); current = null; } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingIterator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingIterator.java index c04337e10c..2112d3c367 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingIterator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingIterator.java @@ -47,8 +47,8 @@ public void remove() { throw new IllegalStateException(); } - iterator.remove(); recordingCollection.addRemoveAction(current); + iterator.remove(); current = null; } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingKeySetIterator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingKeySetIterator.java index ee9871aa7f..4dfecfc495 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingKeySetIterator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingKeySetIterator.java @@ -49,8 +49,8 @@ public void remove() { throw new IllegalStateException(); } - iterator.remove(); recordingMap.addRemoveAction(current); + iterator.remove(); current = null; } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingListIterator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingListIterator.java index a6c9847a1c..066c6c6541 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingListIterator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingListIterator.java @@ -43,22 +43,22 @@ public E next() { public void remove() { int idx = iterator.previousIndex(); - iterator.remove(); recordingList.addRemoveAction(idx); + iterator.remove(); } @Override public void set(E e) { int idx = iterator.previousIndex(); - iterator.set(e); recordingList.addSetAction(idx, e); + iterator.set(e); } @Override public void add(E e) { int idx = iterator.nextIndex(); - iterator.add(e); recordingList.addAddAction(idx, e); + iterator.add(e); } /************** diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingMap.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingMap.java index 0cf8bcfc5e..37c094eec8 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingMap.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingMap.java @@ -313,10 +313,10 @@ public Set getRemovedElements() { return removedElements.keySet(); } - public void replay(C map, UpdateContext context, MapViewToEntityMapper mapper) { + public void replay(C map, UpdateContext context, MapViewToEntityMapper mapper, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener valueRemoveListener) { if (actions != null) { for (MapAction action : resetActions(context)) { - action.doAction(map, context, mapper); + action.doAction(map, context, mapper, keyRemoveListener, valueRemoveListener); } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesCollection.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesCollection.java index 96ecc219b2..9cdd988124 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesCollection.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesCollection.java @@ -47,7 +47,7 @@ public boolean addAll(Collection c) { @Override public boolean remove(Object o) { - recordingMap.addAction(new MapRemoveValueAction(o)); + recordingMap.addAction(new MapRemoveAllValuesAction(o)); return delegate.remove(o); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesIterator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesIterator.java index e718ed8d97..72adbcb43a 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesIterator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingValuesIterator.java @@ -51,8 +51,8 @@ public void remove() { throw new IllegalStateException(); } - iterator.remove(); recordingMap.addRemoveAction(current); + iterator.remove(); current = null; } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityToEntityMapper.java index 5c5c3914cf..d69308a51c 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityToEntityMapper.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.view.impl.entity; import com.blazebit.persistence.view.impl.update.UpdateContext; +import com.blazebit.persistence.view.impl.update.flush.UnmappedAttributeCascadeDeleter; /** * @@ -26,9 +27,11 @@ public abstract class AbstractEntityToEntityMapper implements EntityToEntityMapper { protected final EntityLoaderFetchGraphNode entityLoaderFetchGraphNode; + private final UnmappedAttributeCascadeDeleter deleter; - public AbstractEntityToEntityMapper(EntityLoaderFetchGraphNode entityLoaderFetchGraphNode) { + public AbstractEntityToEntityMapper(EntityLoaderFetchGraphNode entityLoaderFetchGraphNode, UnmappedAttributeCascadeDeleter deleter) { this.entityLoaderFetchGraphNode = entityLoaderFetchGraphNode; + this.deleter = deleter; } @Override @@ -36,6 +39,11 @@ public void remove(UpdateContext context, Object element) { context.getEntityManager().remove(element); } + @Override + public void removeById(UpdateContext context, Object elementId) { + deleter.removeById(context, elementId); + } + @Override public EntityLoaderFetchGraphNode getFullGraphNode() { return entityLoaderFetchGraphNode; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractViewToEntityMapper.java index 06d4d4f900..2e98d654f5 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractViewToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractViewToEntityMapper.java @@ -149,6 +149,16 @@ public void remove(UpdateContext context, Object element) { updater.remove(context, (EntityViewProxy) element); } + @Override + public void removeById(UpdateContext context, Object id) { + defaultUpdater.remove(context, id); + } + + @Override + public Object applyToEntity(UpdateContext context, Object entity, Object element) { + return null; + } + @Override public , E, V> DirtyAttributeFlusher getNestedDirtyFlusher(UpdateContext context, MutableStateTrackable current, DirtyAttributeFlusher fullFlusher) { if (current == null) { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityToEntityMapper.java index 25c88da08f..95e099a528 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityToEntityMapper.java @@ -20,6 +20,7 @@ import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.impl.update.flush.BasicDirtyChecker; import com.blazebit.persistence.view.impl.update.flush.TypeDescriptor; +import com.blazebit.persistence.view.impl.update.flush.UnmappedAttributeCascadeDeleter; import com.blazebit.persistence.view.spi.type.BasicUserType; /** @@ -34,8 +35,8 @@ public class DefaultEntityToEntityMapper extends AbstractEntityToEntityMapper { private final BasicUserType basicUserType; private final BasicDirtyChecker dirtyChecker; - public DefaultEntityToEntityMapper(boolean shouldPersist, boolean shouldMerge, BasicUserType basicUserType, EntityLoaderFetchGraphNode entityLoaderFetchGraphNode) { - super(entityLoaderFetchGraphNode); + public DefaultEntityToEntityMapper(boolean shouldPersist, boolean shouldMerge, BasicUserType basicUserType, EntityLoaderFetchGraphNode entityLoaderFetchGraphNode, UnmappedAttributeCascadeDeleter deleter) { + super(entityLoaderFetchGraphNode, deleter); this.shouldPersist = shouldPersist; this.shouldMerge = shouldMerge; this.basicUserType = (BasicUserType) basicUserType; @@ -49,6 +50,7 @@ public DefaultEntityToEntityMapper(boolean shouldPersist, boolean shouldMerge, B shouldPersist, shouldMerge, null, + null, (BasicUserType) basicUserType, null, null, diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ElementToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ElementToEntityMapper.java index 2834213b00..41efe02c0f 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ElementToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ElementToEntityMapper.java @@ -27,6 +27,7 @@ public interface ElementToEntityMapper { public void remove(UpdateContext context, Object element); - public Object applyToEntity(UpdateContext context, Object entity, Object element); + public void removeById(UpdateContext context, Object elementId); + public Object applyToEntity(UpdateContext context, Object entity, Object element); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java index 41dab60d4c..dc6a30e0c6 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java @@ -80,7 +80,7 @@ public void flushEntity(UpdateContext context, Object newParent, Object child, D parentEntityOnChildEntityMapper.map(newParent, child); if (nestedGraphNode != null) { - nestedGraphNode.flushEntity(context, null, null, child); + nestedGraphNode.flushEntity(context, null, null, child, null); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java index 47dfc0f6b8..d4218f0eed 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java @@ -69,42 +69,40 @@ public InverseViewToEntityMapper(EntityViewManagerImpl evm, ViewType childVie } @Override - public void flushEntity(UpdateContext context, Object newParent, Object child, DirtyAttributeFlusher nestedGraphNode) { + public void flushEntity(UpdateContext context, final Object newParent, final Object child, DirtyAttributeFlusher nestedGraphNode) { if (child == null) { return; } Object elementEntity = null; - // Set the "newParent" on the view object "child" + Object id = viewIdAccessor.getValue(child); + Runnable parentEntityOnChildViewMapperListener = null; + // Afterwards, set the "newParent" on the view object "child" if (parentEntityOnChildViewMapper != null) { - parentEntityOnChildViewMapper.map(newParent, child); - if (shouldPersist(child)) { - elementEntity = entityLoader.toEntity(context, null); - } + parentEntityOnChildViewMapperListener = new Runnable() { + @Override + public void run() { + parentEntityOnChildViewMapper.map(newParent, child); + } + }; + } + // If the view doesn't map the parent, we need to set it on the entity + if (shouldPersist(child)) { + elementEntity = entityLoader.toEntity(context, null); + + parentEntityOnChildEntityMapper.map(newParent, elementEntity); if (nestedGraphNode != null) { - nestedGraphNode.flushEntity(context, (E) elementEntity, null, child); + nestedGraphNode.flushEntity(context, (E) elementEntity, null, child, parentEntityOnChildViewMapperListener); } elementViewToEntityMapper.applyToEntity(context, elementEntity, child); } else { - Object id = viewIdAccessor.getValue(child); - // If the view doesn't map the parent, we need to set it on the entity - if (shouldPersist(child)) { - elementEntity = entityLoader.toEntity(context, null); - - parentEntityOnChildEntityMapper.map(newParent, elementEntity); - if (nestedGraphNode != null) { - nestedGraphNode.flushEntity(context, (E) elementEntity, null, child); - } - elementViewToEntityMapper.applyToEntity(context, elementEntity, child); - } else { - elementEntity = entityLoader.toEntity(context, id); + elementEntity = entityLoader.toEntity(context, id); - parentEntityOnChildEntityMapper.map(newParent, elementEntity); - if (nestedGraphNode != null) { - nestedGraphNode.flushEntity(context, (E) elementEntity, null, child); - } - elementViewToEntityMapper.applyToEntity(context, elementEntity, child); + parentEntityOnChildEntityMapper.map(newParent, elementEntity); + if (nestedGraphNode != null) { + nestedGraphNode.flushEntity(context, (E) elementEntity, null, child, parentEntityOnChildViewMapperListener); } + elementViewToEntityMapper.applyToEntity(context, elementEntity, child); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyEntityToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyEntityToEntityMapper.java index 038c013280..ebd1692c5e 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyEntityToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyEntityToEntityMapper.java @@ -18,6 +18,7 @@ import com.blazebit.persistence.view.impl.change.DirtyChecker; import com.blazebit.persistence.view.impl.update.UpdateContext; +import com.blazebit.persistence.view.impl.update.flush.UnmappedAttributeCascadeDeleter; /** * @@ -26,8 +27,8 @@ */ public class LoadOnlyEntityToEntityMapper extends AbstractEntityToEntityMapper { - public LoadOnlyEntityToEntityMapper(EntityLoaderFetchGraphNode entityLoaderFetchGraphNode) { - super(entityLoaderFetchGraphNode); + public LoadOnlyEntityToEntityMapper(EntityLoaderFetchGraphNode entityLoaderFetchGraphNode, UnmappedAttributeCascadeDeleter deleter) { + super(entityLoaderFetchGraphNode, deleter); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyViewToEntityMapper.java index 4adc616e61..b164dc913c 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyViewToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/LoadOnlyViewToEntityMapper.java @@ -56,6 +56,11 @@ public void remove(UpdateContext context, Object element) { } + @Override + public void removeById(UpdateContext context, Object id) { + + } + @Override public , E, V> DirtyAttributeFlusher getNestedDirtyFlusher(UpdateContext context, MutableStateTrackable current, DirtyAttributeFlusher fullFlusher) { return fullFlusher; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ReferenceEntityLoader.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ReferenceEntityLoader.java index 7195317095..2116199281 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ReferenceEntityLoader.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ReferenceEntityLoader.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.view.impl.entity; import com.blazebit.persistence.view.impl.EntityViewManagerImpl; +import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.metamodel.ManagedViewType; @@ -33,6 +34,10 @@ public ReferenceEntityLoader(EntityViewManagerImpl evm, ManagedViewType subvi super(subviewType.getEntityClass(), jpaIdOf(evm, subviewType), viewIdMapper, evm.getEntityIdAccessor()); } + public ReferenceEntityLoader(Class entityClass, javax.persistence.metamodel.SingularAttribute idAttribute, ViewToEntityMapper viewIdMapper, AttributeAccessor entityIdAccessor) { + super(entityClass, idAttribute, viewIdMapper, entityIdAccessor); + } + @Override public Object toEntity(UpdateContext context, Object id) { if (id == null) { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ViewToEntityMapper.java index 3265c2a874..dd2b901a99 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ViewToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ViewToEntityMapper.java @@ -43,4 +43,5 @@ public interface ViewToEntityMapper extends ElementToEntityMapper { public Query createUpdateQuery(UpdateContext context, Object view, DirtyAttributeFlusher nestedGraphNode); public EntityViewUpdater getUpdater(Object view); + } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractAttribute.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractAttribute.java index 8d7614fb81..1e7382025e 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractAttribute.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractAttribute.java @@ -579,7 +579,14 @@ public void checkAttribute(ManagedType managedType, MetamodelBuildingContext // Validate the fetch expression parses context.getExpressionFactory().createPathExpression(fetch).accept(visitor); } catch (SyntaxErrorException ex) { - context.addError("Syntax error in " + errorLocation + " '" + fetch + "' of the " + getLocation() + ": " + ex.getMessage()); + try { + context.getExpressionFactory().createSimpleExpression(fetch, false); + // The used expression is not usable for fetches + context.addError("Invalid fetch expression '" + fetch + "' of the " + getLocation() + ". Simplify the fetch expression to a simple path expression. Encountered error: " + ex.getMessage()); + } catch (SyntaxErrorException ex2) { + // This is a real syntax error + context.addError("Syntax error in " + errorLocation + " '" + fetch + "' of the " + getLocation() + ": " + ex.getMessage()); + } } catch (IllegalArgumentException ex) { context.addError("An error occurred while trying to resolve the " + errorLocation + " '" + fetch + "' of the " + getLocation() + ": " + ex.getMessage()); } @@ -678,7 +685,14 @@ public void checkAttribute(ManagedType managedType, MetamodelBuildingContext context.addError("Multiple possible target type for the mapping in the " + getLocation() + ": " + possibleTargets); } } catch (SyntaxErrorException ex) { - context.addError("Syntax error in mapping expression '" + mapping + "' of the " + getLocation() + ": " + ex.getMessage()); + try { + context.getExpressionFactory().createSimpleExpression(mapping, false); + // The used expression is not usable for updatable mappings + context.addError("Invalid mapping expression '" + mapping + "' of the " + getLocation() + " for an updatable attribute. Consider annotating the attribute with @UpdatableMapping(updatable = false) or simplify the mapping expression to a simple path expression. Encountered error: " + ex.getMessage()); + } catch (SyntaxErrorException ex2) { + // This is a real syntax error + context.addError("Syntax error in mapping expression '" + mapping + "' of the " + getLocation() + ": " + ex.getMessage()); + } } catch (IllegalArgumentException ex) { context.addError("There is an error for the " + getLocation() + ": " + ex.getMessage()); } @@ -784,7 +798,14 @@ private Class getPluralContainerType(MetamodelBuildingContext context) { } return possibleTargets.values().iterator().next()[0]; } catch (SyntaxErrorException ex) { - context.addError("Syntax error in mapping expression '" + mapping + "' of the " + getLocation() + ": " + ex.getMessage()); + try { + context.getExpressionFactory().createSimpleExpression(mapping, false); + // The used expression is not usable for updatable mappings + context.addError("Invalid mapping expression '" + mapping + "' of the " + getLocation() + " for an updatable attribute. Consider annotating the attribute with @UpdatableMapping(updatable = false) or simplify the mapping expression to a simple path expression. Encountered error: " + ex.getMessage()); + } catch (SyntaxErrorException ex2) { + // This is a real syntax error + context.addError("Syntax error in mapping expression '" + mapping + "' of the " + getLocation() + ": " + ex.getMessage()); + } } catch (IllegalArgumentException ex) { context.addError("There is an error for the " + getLocation() + ": " + ex.getMessage()); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodAttribute.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodAttribute.java index 8e1a1a794d..3e302403b5 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodAttribute.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodAttribute.java @@ -28,16 +28,13 @@ import com.blazebit.persistence.view.MappingSubquery; import com.blazebit.persistence.view.metamodel.AttributeFilterMapping; import com.blazebit.persistence.view.metamodel.BasicType; -import com.blazebit.persistence.view.metamodel.FlatViewType; import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.MethodAttribute; import com.blazebit.persistence.view.metamodel.Type; -import com.blazebit.persistence.view.metamodel.ViewType; import com.blazebit.reflection.ReflectionUtils; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -158,37 +155,16 @@ private void addToUpdateSubtypeSet(Set> set, Class superType, Set elementType, MetamodelBuildingContext context, boolean requiresSetter) { - // Non-basic mappings are never considered updatable - if (getMappingType() != MappingType.BASIC) { - return false; - } - Method setter = ReflectionUtils.getSetter(getDeclaringType().getJavaType(), getName()); - boolean hasSetter = setter != null && (setter.getModifiers() & Modifier.ABSTRACT) != 0; - if (!requiresSetter) { - if (elementType instanceof ViewType) { - ViewType t = (ViewType) elementType; - return hasSetter || t.isUpdatable() || t.isCreatable(); - } else if (elementType instanceof FlatViewType) { - FlatViewType t = (FlatViewType) elementType; - return t.isUpdatable() || t.isCreatable(); - } - } - return hasSetter; - } - - protected boolean determineMutable(Type elementType, MetamodelBuildingContext context) { + protected boolean determineMutable(Type elementType) { if (isUpdatable()) { return true; - } else if (elementType instanceof ManagedViewType) { - ManagedViewType viewType = (ManagedViewType) elementType; - return viewType.isUpdatable() || viewType.isCreatable() || !getPersistCascadeAllowedSubtypes().isEmpty() || !getUpdateCascadeAllowedSubtypes().isEmpty(); } - if (elementType == null) { return false; } - return ((BasicType) elementType).getUserType().isMutable() && (isPersistCascaded() || isUpdateCascaded()); + + // Essentially, the checks for whether the type is updatable etc. have been done during update cascade determination already + return isUpdateCascaded(); } protected boolean determineOptimisticLockProtected(MethodAttributeMapping mapping, MetamodelBuildingContext context, boolean mutable) { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodPluralAttribute.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodPluralAttribute.java index efcd3fc301..80e4f35a13 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodPluralAttribute.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodPluralAttribute.java @@ -23,8 +23,11 @@ import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.PluralAttribute; import com.blazebit.persistence.view.metamodel.Type; +import com.blazebit.reflection.ReflectionUtils; import javax.persistence.metamodel.ManagedType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.Comparator; import java.util.Map; @@ -47,6 +50,8 @@ public abstract class AbstractMethodPluralAttribute extends AbstractMet private final boolean optimisticLockProtected; private final boolean persistCascaded; private final boolean updateCascaded; + private final boolean deleteCascaded; + private final boolean orphanRemoval; private final Set> persistSubtypes; private final Set> updateSubtypes; private final Set> allowedSubtypes; @@ -68,49 +73,63 @@ public AbstractMethodPluralAttribute(ManagedViewTypeImplementor viewType, Met } this.elementType = (Type) mapping.getElementType(context); + // The declaring type must be mutable, otherwise attributes can't be considered updatable if (mapping.getUpdatable() == null) { - // Plural attributes are only updatable if we have a setter or they are explicitly configured to be so - this.updatable = determineUpdatable(elementType, context, true); - } else { - this.updatable = mapping.getUpdatable(); - } - if (mapping.getUpdatable() == null || mapping.getCascadeTypes().contains(CascadeType.AUTO)) { - if (!declaringType.isUpdatable() && !declaringType.isCreatable()) { - this.persistSubtypes = Collections.emptySet(); - this.updateSubtypes = Collections.emptySet(); + if (declaringType.isUpdatable() || declaringType.isCreatable()) { + this.updatable = determineUpdatable(elementType); } else { - this.persistSubtypes = determinePersistSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); - this.updateSubtypes = determineUpdateSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); + this.updatable = false; } - this.persistCascaded = !persistSubtypes.isEmpty(); - this.updateCascaded = !updateSubtypes.isEmpty(); } else { - if (!declaringType.isUpdatable() && !declaringType.isCreatable()) { + this.updatable = mapping.getUpdatable(); + if (updatable && !declaringType.isUpdatable() && !declaringType.isCreatable()) { + // Note that although orphanRemoval and delete cascading makes sense for read only views, we don't want to mix up concepts for now.. context.addError("Illegal occurrences of @UpdatableMapping for non-updatable and non-creatable view type '" + declaringType.getJavaType().getName() + "' on the " + mapping.getErrorLocation() + "!"); - this.persistCascaded = false; + } + } + boolean definesDeleteCascading = mapping.getCascadeTypes().contains(CascadeType.DELETE); + boolean allowsDeleteCascading = updatable || mapping.getCascadeTypes().contains(CascadeType.AUTO); + + if (updatable) { + this.persistSubtypes = determinePersistSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); + this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST) + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !persistSubtypes.isEmpty(); + } else { + this.persistCascaded = false; + this.persistSubtypes = Collections.emptySet(); + } + + // The declaring type must be mutable, otherwise attributes can't have cascading + if (mapping.isId() || mapping.isVersion() || !declaringType.isUpdatable() && !declaringType.isCreatable()) { + this.updateCascaded = false; + this.updateSubtypes = Collections.emptySet(); + } else { + // TODO: maybe allow to override mutability? + Set> updateCascadeAllowedSubtypes = determineUpdateSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); + boolean updateCascaded = mapping.getCascadeTypes().contains(CascadeType.UPDATE) + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !updateCascadeAllowedSubtypes.isEmpty(); + if (updateCascaded) { + this.updateCascaded = true; + this.updateSubtypes = updateCascadeAllowedSubtypes; + } else { this.updateCascaded = false; - this.persistSubtypes = Collections.emptySet(); this.updateSubtypes = Collections.emptySet(); - } else { - this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST); - this.updateCascaded = mapping.getCascadeTypes().contains(CascadeType.UPDATE); - this.persistSubtypes = determinePersistSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); - this.updateSubtypes = determineUpdateSubtypeSet(elementType, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); - - if ((isUpdatable() != persistCascaded || isUpdatable() != updateCascaded)) { - if (elementType instanceof BasicType && context.getEntityMetamodel().getEntity(elementType.getJavaType()) == null - || elementType instanceof FlatViewType) { - context.addError("Cascading configuration for basic, embeddable or flat view type attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); - } - } - if (!isUpdatable() && persistCascaded) { - context.addError("Persist cascading for non-updatable attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); - } } } + + this.mutable = determineMutable(elementType); + + if (!mapping.getCascadeTypes().contains(CascadeType.AUTO)) { + if (elementType instanceof BasicType && context.getEntityMetamodel().getEntity(elementType.getJavaType()) == null + || elementType instanceof FlatViewType) { + context.addError("Cascading configuration for basic, embeddable or flat view type attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + } + if (!updatable && mapping.getCascadeTypes().contains(CascadeType.PERSIST)) { + context.addError("Persist cascading for non-updatable attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + this.allowedSubtypes = createAllowedSubtypesSet(); - // TODO: maybe allow to override mutability? - this.mutable = determineMutable(elementType, context); this.optimisticLockProtected = determineOptimisticLockProtected(mapping, context, mutable); this.elementInheritanceSubtypes = (Map, String>) (Map) mapping.getElementInheritanceSubtypes(context); this.dirtyStateIndex = determineDirtyStateIndex(dirtyStateIndex); @@ -130,6 +149,27 @@ public AbstractMethodPluralAttribute(ManagedViewTypeImplementor viewType, Met } } + if (Boolean.FALSE.equals(mapping.getOrphanRemoval())) { + this.orphanRemoval = false; + } else { + // Determine orphan removal based on remove strategy + this.orphanRemoval = inverseRemoveStrategy == InverseRemoveStrategy.REMOVE || Boolean.TRUE.equals(mapping.getOrphanRemoval()); + } + + // Orphan removal implies delete cascading, inverse attributes also always do delete cascading + this.deleteCascaded = orphanRemoval || definesDeleteCascading || allowsDeleteCascading && inverseRemoveStrategy != null; + + if (updatable) { + boolean jpaOrphanRemoval = context.getJpaProvider().isOrphanRemoval(declaringType.getJpaManagedType(), getMapping()); + if (jpaOrphanRemoval && !orphanRemoval) { + context.addError("Orphan removal configuration via @UpdatableMapping must be defined if entity attribute defines orphan removal. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + boolean jpaDeleteCascaded = context.getJpaProvider().isDeleteCascaded(declaringType.getJpaManagedType(), getMapping()); + if (jpaDeleteCascaded && !deleteCascaded) { + context.addError("Delete cascading configuration via @UpdatableMapping must be defined if entity attribute defines delete cascading. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + } + this.sorted = mapping.isSorted(); this.ordered = mapping.getContainerBehavior() == AttributeMapping.ContainerBehavior.ORDERED; @@ -137,6 +177,24 @@ public AbstractMethodPluralAttribute(ManagedViewTypeImplementor viewType, Met this.comparator = MetamodelUtils.getComparator(comparatorClass); } + private boolean determineUpdatable(Type elementType) { + // Non-basic mappings(Subquery, Correlation, etc.) are never considered updatable + if (getMappingType() != MappingType.BASIC) { + return false; + } + Method setter = ReflectionUtils.getSetter(getDeclaringType().getJavaType(), getName()); + boolean hasSetter = setter != null && (setter.getModifiers() & Modifier.ABSTRACT) != 0; + + // For a plural attribute being considered updatable, there must be a setter or the view type must be creatable + if (elementType instanceof ManagedViewType) { + ManagedViewType t = (ManagedViewType) elementType; + return hasSetter || t.isCreatable(); + } + + // We exclude entity types from this since there is no clear intent + return hasSetter; + } + @Override public int getDirtyStateIndex() { return dirtyStateIndex; @@ -182,6 +240,16 @@ public boolean isUpdateCascaded() { return updateCascaded; } + @Override + public boolean isDeleteCascaded() { + return deleteCascaded; + } + + @Override + public boolean isOrphanRemoval() { + return orphanRemoval; + } + @Override public Set> getPersistCascadeAllowedSubtypes() { return persistSubtypes; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodSingularAttribute.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodSingularAttribute.java index b09e3248da..b83ad24311 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodSingularAttribute.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractMethodSingularAttribute.java @@ -27,8 +27,11 @@ import com.blazebit.persistence.view.metamodel.SingularAttribute; import com.blazebit.persistence.view.metamodel.Type; import com.blazebit.persistence.view.spi.type.VersionBasicUserType; +import com.blazebit.reflection.ReflectionUtils; import javax.persistence.metamodel.ManagedType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -50,6 +53,8 @@ public abstract class AbstractMethodSingularAttribute extends AbstractMeth private final boolean optimisticLockProtected; private final boolean persistCascaded; private final boolean updateCascaded; + private final boolean deleteCascaded; + private final boolean orphanRemoval; private final Set> persistSubtypes; private final Set> updateSubtypes; private final Set> allowedSubtypes; @@ -65,12 +70,13 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M } } + // The declaring type must be mutable, otherwise attributes can't be considered updatable if (mapping.getUpdatable() == null) { // Id and version are never updatable - if (mapping.isId() || mapping.isVersion()) { + if (mapping.isId() || mapping.isVersion() || !declaringType.isUpdatable() && !declaringType.isCreatable()) { this.updatable = false; } else { - this.updatable = determineUpdatable(type, context, false); + this.updatable = determineUpdatable(type); } } else { this.updatable = mapping.getUpdatable(); @@ -80,45 +86,55 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M } else if (mapping.isVersion()) { context.addError("Illegal @UpdatableMapping along with @Version on the " + mapping.getErrorLocation() + "!"); } + if (!declaringType.isUpdatable() && !declaringType.isCreatable()) { + // Note that although orphanRemoval and delete cascading makes sense for read only views, we don't want to mix up concepts for now.. + context.addError("Illegal occurrences of @UpdatableMapping for non-updatable and non-creatable view type '" + declaringType.getJavaType().getName() + "' on the " + mapping.getErrorLocation() + "!"); + } } } - if (mapping.getUpdatable() == null || mapping.getCascadeTypes().contains(CascadeType.AUTO)) { - if (!declaringType.isUpdatable() && !declaringType.isCreatable()) { - this.persistSubtypes = Collections.emptySet(); - this.updateSubtypes = Collections.emptySet(); - } else { - this.persistSubtypes = determinePersistSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); - this.updateSubtypes = determineUpdateSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); - } - this.persistCascaded = !persistSubtypes.isEmpty(); - this.updateCascaded = !updateSubtypes.isEmpty(); + boolean definesDeleteCascading = mapping.getCascadeTypes().contains(CascadeType.DELETE); + boolean allowsDeleteCascading = updatable || mapping.getCascadeTypes().contains(CascadeType.AUTO); + + if (updatable) { + this.persistSubtypes = determinePersistSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); + this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST) + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !persistSubtypes.isEmpty(); } else { - if (!declaringType.isUpdatable() && !declaringType.isCreatable()) { - context.addError("Illegal occurrences of @UpdatableMapping for non-updatable and non-creatable view type '" + declaringType.getJavaType().getName() + "' on the " + mapping.getErrorLocation() + "!"); - this.persistCascaded = false; + this.persistCascaded = false; + this.persistSubtypes = Collections.emptySet(); + } + + // The declaring type must be mutable, otherwise attributes can't have cascading + if (mapping.isId() || mapping.isVersion() || !declaringType.isUpdatable() && !declaringType.isCreatable()) { + this.updateCascaded = false; + this.updateSubtypes = Collections.emptySet(); + } else { + // TODO: maybe allow to override mutability? + Set> updateCascadeAllowedSubtypes = determineUpdateSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); + boolean updateCascaded = mapping.getCascadeTypes().contains(CascadeType.UPDATE) + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !updateCascadeAllowedSubtypes.isEmpty(); + if (updateCascaded) { + this.updateCascaded = true; + this.updateSubtypes = updateCascadeAllowedSubtypes; + } else { this.updateCascaded = false; - this.persistSubtypes = Collections.emptySet(); this.updateSubtypes = Collections.emptySet(); - } else { - this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST); - this.updateCascaded = mapping.getCascadeTypes().contains(CascadeType.UPDATE); - this.persistSubtypes = determinePersistSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadePersistSubtypes(context), context); - this.updateSubtypes = determineUpdateSubtypeSet(type, mapping.getCascadeSubtypes(context), mapping.getCascadeUpdateSubtypes(context), context); - - if ((isUpdatable() != persistCascaded || isUpdatable() != updateCascaded)) { - if (type instanceof BasicType && context.getEntityMetamodel().getEntity(type.getJavaType()) == null - || type instanceof FlatViewType) { - context.addError("Cascading configuration for basic, embeddable or flat view type attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); - } - } - if (!isUpdatable() && persistCascaded) { - context.addError("Persist cascading for non-updatable attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); - } } } + + this.mutable = determineMutable(type); + + if (!mapping.getCascadeTypes().contains(CascadeType.AUTO)) { + if (type instanceof BasicType && context.getEntityMetamodel().getEntity(type.getJavaType()) == null + || type instanceof FlatViewType) { + context.addError("Cascading configuration for basic, embeddable or flat view type attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + } + if (!updatable && mapping.getCascadeTypes().contains(CascadeType.PERSIST)) { + context.addError("Persist cascading for non-updatable attributes is not allowed. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + this.allowedSubtypes = createAllowedSubtypesSet(); - // TODO: maybe allow to override mutability? - this.mutable = determineMutable(type, context); this.optimisticLockProtected = determineOptimisticLockProtected(mapping, context, mutable); this.inheritanceSubtypes = (Map, String>) (Map) mapping.getInheritanceSubtypes(context); this.dirtyStateIndex = determineDirtyStateIndex(dirtyStateIndex); @@ -137,6 +153,47 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M this.writableMappedByMapping = mapping.determineWritableMappedByMappings(managedType, mappedBy, context); } } + + if (Boolean.FALSE.equals(mapping.getOrphanRemoval())) { + this.orphanRemoval = false; + } else { + // Determine orphan removal based on remove strategy + this.orphanRemoval = inverseRemoveStrategy == InverseRemoveStrategy.REMOVE || Boolean.TRUE.equals(mapping.getOrphanRemoval()); + } + + // Orphan removal implies delete cascading, inverse attributes also always do delete cascading + this.deleteCascaded = orphanRemoval || definesDeleteCascading || allowsDeleteCascading && inverseRemoveStrategy != null; + + if (updatable) { + boolean jpaOrphanRemoval = context.getJpaProvider().isOrphanRemoval(declaringType.getJpaManagedType(), getMapping()); + if (jpaOrphanRemoval && !orphanRemoval) { + context.addError("Orphan removal configuration via @UpdatableMapping must be defined if entity attribute defines orphan removal. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + boolean jpaDeleteCascaded = context.getJpaProvider().isDeleteCascaded(declaringType.getJpaManagedType(), getMapping()); + if (jpaDeleteCascaded && !deleteCascaded) { + context.addError("Delete cascading configuration via @UpdatableMapping must be defined if entity attribute defines delete cascading. Invalid definition found on the " + mapping.getErrorLocation() + "!"); + } + } + } + + private boolean determineUpdatable(Type elementType) { + // Non-basic mappings(Subquery, Correlation, etc.) are never considered updatable + if (getMappingType() != MappingType.BASIC) { + return false; + } + + Method setter = ReflectionUtils.getSetter(getDeclaringType().getJavaType(), getName()); + boolean hasSetter = setter != null && (setter.getModifiers() & Modifier.ABSTRACT) != 0; + + // For a singular attribute being considered updatable, there must be a setter + // If the type is a flat view, it must be updatable or creatable and have a setter + if (elementType instanceof FlatViewType) { + FlatViewType t = (FlatViewType) elementType; + return t.isUpdatable() || hasSetter && t.isCreatable(); + } + + // We exclude entity types from this since there is no clear intent + return hasSetter; } @Override @@ -214,6 +271,16 @@ public boolean isUpdateCascaded() { return updateCascaded; } + @Override + public boolean isDeleteCascaded() { + return deleteCascaded; + } + + @Override + public boolean isOrphanRemoval() { + return orphanRemoval; + } + @Override public Set> getPersistCascadeAllowedSubtypes() { return persistSubtypes; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AnnotationMethodAttributeMappingReader.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AnnotationMethodAttributeMappingReader.java index ab753c977f..b959991804 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AnnotationMethodAttributeMappingReader.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AnnotationMethodAttributeMappingReader.java @@ -95,7 +95,7 @@ public MethodAttributeMapping readMethodAttributeMapping(ViewMapping viewMapping UpdatableMapping updatableMapping = AnnotationUtils.findAnnotation(method, UpdatableMapping.class); if (updatableMapping != null) { - attributeMapping.setUpdatable(updatableMapping.updatable(), updatableMapping.cascade(), updatableMapping.subtypes(), updatableMapping.persistSubtypes(), updatableMapping.updateSubtypes()); + attributeMapping.setUpdatable(updatableMapping.updatable(), updatableMapping.orphanRemoval(), updatableMapping.cascade(), updatableMapping.subtypes(), updatableMapping.persistSubtypes(), updatableMapping.updateSubtypes()); } MappingInverse inverseMapping = AnnotationUtils.findAnnotation(method, MappingInverse.class); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/MethodAttributeMapping.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/MethodAttributeMapping.java index 464bdc82ae..88eccdf1b9 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/MethodAttributeMapping.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/MethodAttributeMapping.java @@ -68,11 +68,12 @@ public class MethodAttributeMapping extends AttributeMapping implements EntityVi // Updatable configs private Boolean isUpdatable; + private Boolean isOrphanRemoval; private Boolean isOptimisticLockProtected; private String mappedBy; private boolean mappedByResolved; private InverseRemoveStrategy inverseRemoveStrategy = InverseRemoveStrategy.SET_NULL; - private Set cascadeTypes = Collections.emptySet(); + private Set cascadeTypes = Collections.singleton(CascadeType.AUTO); private Set> cascadeSubtypeClasses; private Set> cascadePersistSubtypeClasses; private Set> cascadeUpdateSubtypeClasses; @@ -113,6 +114,11 @@ public Boolean getUpdatable() { return isUpdatable; } + @Override + public Boolean getOrphanRemoval() { + return isOrphanRemoval; + } + public Boolean getOptimisticLockProtected() { return isOptimisticLockProtected; } @@ -133,8 +139,9 @@ public Set getCascadeTypes() { } @Override - public void setUpdatable(boolean updatable, CascadeType[] cascadeTypes, Class[] subtypes, Class[] persistSubtypes, Class[] updateSubtypes) { + public void setUpdatable(boolean updatable, boolean orphanRemoval, CascadeType[] cascadeTypes, Class[] subtypes, Class[] persistSubtypes, Class[] updateSubtypes) { this.isUpdatable = updatable; + this.isOrphanRemoval = orphanRemoval; this.cascadeTypes = new HashSet<>(Arrays.asList(cascadeTypes)); this.cascadeSubtypeClasses = new HashSet<>(Arrays.asList(subtypes)); this.cascadePersistSubtypeClasses = new HashSet<>(Arrays.asList(persistSubtypes)); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ViewMetamodelImpl.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ViewMetamodelImpl.java index 4491f16c30..2bca2819ad 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ViewMetamodelImpl.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ViewMetamodelImpl.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.impl.metamodel; import com.blazebit.persistence.impl.EntityMetamodel; -import com.blazebit.persistence.spi.ServiceProvider; import com.blazebit.persistence.view.metamodel.FlatViewType; import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.ViewMetamodel; @@ -44,8 +43,8 @@ public class ViewMetamodelImpl implements ViewMetamodel { private final Map, FlatViewTypeImpl> flatViews; private final Map, ManagedViewTypeImplementor> managedViews; - public ViewMetamodelImpl(ServiceProvider serviceProvider, MetamodelBuildingContext context, boolean validateExpressions) { - this.metamodel = serviceProvider.getService(EntityMetamodel.class); + public ViewMetamodelImpl(EntityMetamodel entityMetamodel, MetamodelBuildingContext context, boolean validateExpressions) { + this.metamodel = entityMetamodel; Collection viewMappings = context.getViewMappings(); Map, ViewTypeImpl> views = new HashMap<>(viewMappings.size()); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java index b91f68f9cb..9cb6f62911 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java @@ -69,6 +69,8 @@ import javassist.bytecode.MethodInfo; import javassist.bytecode.Opcode; import javassist.bytecode.SignatureAttribute; +import javassist.bytecode.StackMap; +import javassist.bytecode.StackMapTable; import javassist.compiler.CompileError; import javassist.compiler.JvstCodeGen; import javassist.compiler.Lex; @@ -1793,6 +1795,9 @@ private CtConstructor createConstructor(ManagedViewType managedViewType, CtCl bc.addInvokespecial(managedViewType.getJavaType().getName(), postCreateMethod.getName(), postCreateMethodDescriptor); bc.addReturn(null); + CodeAttribute newCodeAttribute = bc.toCodeAttribute(); + newCodeAttribute.setAttribute((StackMap) codeAttribute.getAttribute(StackMap.tag)); + newCodeAttribute.setAttribute((StackMapTable) codeAttribute.getAttribute(StackMapTable.tag)); ctConstructor.getMethodInfo().setCodeAttribute(bc.toCodeAttribute()); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdater.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdater.java index 47ee841086..0adccf4234 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdater.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdater.java @@ -46,6 +46,8 @@ public interface EntityViewUpdater { public void remove(UpdateContext context, EntityViewProxy entityView); + public void remove(UpdateContext context, Object id); + public Query createUpdateQuery(UpdateContext context, MutableStateTrackable view, DirtyAttributeFlusher nestedGraphNode); public DirtyChecker getDirtyChecker(); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java index 4f1b93abbc..1490e1c388 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java @@ -20,6 +20,9 @@ import com.blazebit.persistence.impl.EntityMetamodel; import com.blazebit.persistence.impl.expression.ExpressionFactory; import com.blazebit.persistence.impl.util.JpaMetamodelUtils; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.view.FlushMode; import com.blazebit.persistence.view.FlushStrategy; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -30,6 +33,7 @@ import com.blazebit.persistence.view.impl.accessor.InitialValueAttributeAccessor; import com.blazebit.persistence.view.impl.change.DirtyChecker; import com.blazebit.persistence.view.impl.collection.CollectionInstantiator; +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; import com.blazebit.persistence.view.impl.collection.MapInstantiator; import com.blazebit.persistence.view.impl.collection.RecordingList; import com.blazebit.persistence.view.impl.collection.RecordingMap; @@ -46,7 +50,7 @@ import com.blazebit.persistence.view.impl.mapper.ViewMapper; import com.blazebit.persistence.view.impl.metamodel.AbstractMethodAttribute; import com.blazebit.persistence.view.impl.metamodel.ManagedViewTypeImplementor; -import com.blazebit.persistence.view.impl.metamodel.ViewTypeImpl; +import com.blazebit.persistence.view.impl.metamodel.ViewTypeImplementor; import com.blazebit.persistence.view.impl.proxy.DirtyStateTrackable; import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.update.flush.BasicAttributeFlusher; @@ -54,6 +58,7 @@ import com.blazebit.persistence.view.impl.update.flush.CompositeAttributeFlusher; import com.blazebit.persistence.view.impl.update.flush.DirtyAttributeFlusher; import com.blazebit.persistence.view.impl.update.flush.EmbeddableAttributeFlusher; +import com.blazebit.persistence.view.impl.update.flush.EntityCollectionRemoveListener; import com.blazebit.persistence.view.impl.update.flush.FetchGraphNode; import com.blazebit.persistence.view.impl.update.flush.IndexedListAttributeFlusher; import com.blazebit.persistence.view.impl.update.flush.InverseFlusher; @@ -61,8 +66,12 @@ import com.blazebit.persistence.view.impl.update.flush.SimpleMapViewToEntityMapper; import com.blazebit.persistence.view.impl.update.flush.SubviewAttributeFlusher; import com.blazebit.persistence.view.impl.update.flush.TypeDescriptor; +import com.blazebit.persistence.view.impl.update.flush.UnmappedAttributeCascadeDeleter; +import com.blazebit.persistence.view.impl.update.flush.UnmappedBasicAttributeCascadeDeleter; +import com.blazebit.persistence.view.impl.update.flush.UnmappedCollectionAttributeCascadeDeleter; +import com.blazebit.persistence.view.impl.update.flush.UnmappedMapAttributeCascadeDeleter; import com.blazebit.persistence.view.impl.update.flush.VersionAttributeFlusher; -import com.blazebit.persistence.view.metamodel.Attribute; +import com.blazebit.persistence.view.impl.update.flush.ViewCollectionRemoveListener; import com.blazebit.persistence.view.metamodel.BasicType; import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.MapAttribute; @@ -75,14 +84,14 @@ import javax.persistence.Query; import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.SingularAttribute; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; /** * @@ -109,7 +118,8 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement Class entityClass = viewType.getEntityClass(); this.fullFlush = viewType.getFlushMode() == FlushMode.FULL; this.flushStrategy = viewType.getFlushStrategy(); - EntityType entityType = evm.getMetamodel().getEntityMetamodel().getEntity(entityClass); + EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); + EntityType entityType = entityMetamodel.getEntity(entityClass); ViewToEntityMapper viewIdMapper = null; final AttributeAccessor viewIdAccessor; @@ -165,6 +175,7 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement this.fullEntityLoader = null; } Set> attributes = (Set>) (Set) viewType.getAttributes(); + String idAttributeName = null; AbstractMethodAttribute idAttribute; AbstractMethodAttribute versionAttribute; DirtyAttributeFlusher versionFlusher; @@ -186,7 +197,8 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement javax.persistence.metamodel.SingularAttribute jpaIdAttribute = null; if (idAttribute != null) { - jpaIdAttribute = JpaMetamodelUtils.getIdAttribute(evm.getMetamodel().getEntityMetamodel().entity(entityClass)); + jpaIdAttribute = JpaMetamodelUtils.getIdAttribute(entityMetamodel.entity(entityClass)); + idAttributeName = jpaIdAttribute.getName(); String mapping = idAttribute.getMapping(); // Read only entity views don't have this restriction if ((viewType.isCreatable() || viewType.isUpdatable()) && !mapping.equals(jpaIdAttribute.getName())) { @@ -227,8 +239,19 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement } } + UnmappedAttributeCascadeDeleter[] cascadeDeleteUnmappedFlushers = null; + // Exclude it and version attributes from unmapped attributes as they can't have join tables + Map joinTableUnmappedEntityAttributes = new TreeMap<>(entityMetamodel.getManagedType(ExtendedManagedType.class, entityClass).getAttributes()); + if (jpaIdAttribute != null) { + joinTableUnmappedEntityAttributes.remove(jpaIdAttribute.getName()); + } + if (versionAttribute != null) { + joinTableUnmappedEntityAttributes.remove(versionAttribute.getMapping()); + } + // Only construct attribute flushers for creatable or updatable entity views if (mutable) { + // Create flushers for mapped attributes for (MethodAttribute attribute : attributes) { if (attribute == idAttribute || attribute == versionAttribute) { continue; @@ -238,7 +261,10 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement if (!methodAttribute.isUpdateMappable()) { continue; } - DirtyAttributeFlusher flusher = createAttributeFlusher(evm, viewType, flushStrategy, methodAttribute); + + // Exclude mapped attributes + joinTableUnmappedEntityAttributes.remove(methodAttribute.getMapping()); + DirtyAttributeFlusher flusher = createAttributeFlusher(evm, viewType, idAttributeName, flushStrategy, methodAttribute); if (flusher != null) { if (sb != null) { int endIndex = sb.length(); @@ -266,9 +292,39 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement } } + // Remove attributes that don't have a join table + Iterator> iterator = joinTableUnmappedEntityAttributes.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + ExtendedAttribute attributeEntry = entry.getValue(); + JoinTable joinTable = attributeEntry.getJoinTable(); + if (joinTable == null && !attributeEntry.isDeleteCascaded()) { + iterator.remove(); + } + } + + // Create cascade delete flushers for unmapped attributes + if (!joinTableUnmappedEntityAttributes.isEmpty()) { + cascadeDeleteUnmappedFlushers = new UnmappedAttributeCascadeDeleter[joinTableUnmappedEntityAttributes.size()]; + int i = 0; + for (Map.Entry entry : joinTableUnmappedEntityAttributes.entrySet()) { + ExtendedAttribute extendedAttribute = entry.getValue(); + if (extendedAttribute.getAttribute().isCollection()) { + if (((javax.persistence.metamodel.PluralAttribute) extendedAttribute.getAttribute()).getCollectionType() == javax.persistence.metamodel.PluralAttribute.CollectionType.MAP) { + cascadeDeleteUnmappedFlushers[i++] = new UnmappedMapAttributeCascadeDeleter(evm, entry.getKey(), extendedAttribute, entityClass, idAttributeName, false); + } else { + cascadeDeleteUnmappedFlushers[i++] = new UnmappedCollectionAttributeCascadeDeleter(evm, entry.getKey(), extendedAttribute, entityClass, idAttributeName, false); + } + } else { + cascadeDeleteUnmappedFlushers[i++] = new UnmappedBasicAttributeCascadeDeleter(evm, entry.getKey(), extendedAttribute, idAttributeName, false); + } + } + } + this.fullFlusher = new CompositeAttributeFlusher( viewType.getJavaType(), - entityClass, + viewType.getEntityClass(), + viewType.getJpaManagedType(), persistable, persistViewMapper, jpaIdAttribute, @@ -279,6 +335,7 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement idViewBuilder, idFlusher, versionFlusher, + cascadeDeleteUnmappedFlushers, flushers.toArray(new DirtyAttributeFlusher[flushers.size()]), viewType.getFlushMode(), flushStrategy @@ -337,7 +394,7 @@ public static DirtyAttributeFlusher createIdFlusher(EntityVie ); } else { TypeDescriptor typeDescriptor = TypeDescriptor.forType(evm, idAttribute, type); - return new BasicAttributeFlusher<>(attributeName, attributeMapping, true, false, true, typeDescriptor, null, ID_PARAM_NAME, entityAttributeAccessor, viewAttributeAccessor); + return new BasicAttributeFlusher<>(attributeName, attributeMapping, true, false, true, false, false, false, typeDescriptor, null, ID_PARAM_NAME, entityAttributeAccessor, viewAttributeAccessor, null); } } @@ -448,7 +505,7 @@ private void update(UpdateContext context, Object entity, MutableStateTrackable try { // TODO: Flush strategy AUTO should do flushEntity when entity is known to be fetched already if (flushStrategy == FlushStrategy.ENTITY || !flusher.supportsQueryFlush()) { - flusher.flushEntity(context, entity, updatableProxy, updatableProxy); + flusher.flushEntity(context, entity, updatableProxy, updatableProxy, null); } else { Query query = createUpdateQuery(context, updatableProxy, flusher); flusher.flushQuery(context, null, query, updatableProxy, updatableProxy); @@ -473,17 +530,25 @@ public Object executePersist(UpdateContext context, MutableStateTrackable updata @Override public Object executePersist(UpdateContext context, Object entity, MutableStateTrackable updatableProxy) { - fullFlusher.flushEntity(context, entity, updatableProxy, updatableProxy); + fullFlusher.flushEntity(context, entity, updatableProxy, updatableProxy, null); return entity; } @Override public void remove(UpdateContext context, EntityViewProxy entityView) { + if (flushStrategy == FlushStrategy.ENTITY) { + // TODO: pre-load cascade deleted entity graph + } fullFlusher.remove(context, null, entityView, entityView); } + @Override + public void remove(UpdateContext context, Object id) { + fullFlusher.remove(context, id); + } + @SuppressWarnings({"unchecked", "checkstyle:methodlength"}) - private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImpl evm, ManagedViewType viewType, FlushStrategy flushStrategy, AbstractMethodAttribute attribute) { + private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImpl evm, ManagedViewType viewType, String idAttributeName, FlushStrategy flushStrategy, AbstractMethodAttribute attribute) { EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); Class entityClass = viewType.getEntityClass(); String attributeName = attribute.getName(); @@ -492,6 +557,8 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp String attributeLocation = attribute.getLocation(); boolean cascadePersist = attribute.isPersistCascaded(); boolean cascadeUpdate = attribute.isUpdateCascaded(); + boolean cascadeDelete = attribute.isDeleteCascaded(); + boolean viewOnlyDeleteCascaded = cascadeDelete && !entityMetamodel.getManagedType(ExtendedManagedType.class, entityClass).getAttribute(attributeMapping).isDeleteCascaded(); boolean optimisticLockProtected = attribute.isOptimisticLockProtected(); Set> persistAllowedSubtypes = attribute.getPersistCascadeAllowedSubtypes(); Set> updateAllowedSubtypes = attribute.getUpdateCascadeAllowedSubtypes(); @@ -501,10 +568,22 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp InitialValueAttributeAccessor viewAttributeAccessor = Accessors.forMutableViewAttribute(evm, attribute); TypeDescriptor elementDescriptor = TypeDescriptor.forType(evm, attribute, pluralAttribute.getElementType()); boolean collectionUpdatable = attribute.isUpdatable(); + CollectionRemoveListener elementRemoveListener = createOrphanRemoveListener(attribute, elementDescriptor); + CollectionRemoveListener elementCascadeDeleteListener = createCascadeDeleteListener(attribute, elementDescriptor); + boolean jpaProviderDeletesCollection; + + if (elementDescriptor.getEntityIdAttributeName() != null) { + jpaProviderDeletesCollection = evm.getJpaProvider().supportsJoinTableCleanupOnDelete(); + } else { + jpaProviderDeletesCollection = evm.getJpaProvider().supportsCollectionTableCleanupOnDelete(); + } if (attribute instanceof MapAttribute) { MapAttribute mapAttribute = (MapAttribute) attribute; TypeDescriptor keyDescriptor = TypeDescriptor.forType(evm, attribute, mapAttribute.getKeyType()); + // TODO: currently there is no possibility to set this + CollectionRemoveListener keyRemoveListener = null; + CollectionRemoveListener keyCascadeDeleteListener = null; if (collectionUpdatable || keyDescriptor.shouldFlushMutations() || elementDescriptor.shouldFlushMutations() || shouldPassThrough(evm, viewType, attribute)) { MapViewToEntityMapper mapper = new SimpleMapViewToEntityMapper(keyDescriptor.getViewToEntityMapper(), elementDescriptor.getViewToEntityMapper()); @@ -514,11 +593,19 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp return new MapAttributeFlusher, ?, ?>>( attributeName, attributeMapping, + entityClass, + idAttributeName, flushStrategy, entityAttributeAccessor, viewAttributeAccessor, - collectionUpdatable, optimisticLockProtected, + collectionUpdatable, + keyCascadeDeleteListener, + elementCascadeDeleteListener, + keyRemoveListener, + elementRemoveListener, + viewOnlyDeleteCascaded, + jpaProviderDeletesCollection, keyDescriptor, elementDescriptor, mapper, @@ -538,11 +625,17 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp return new IndexedListAttributeFlusher>>( attributeName, attributeMapping, + entityClass, + idAttributeName, flushStrategy, entityAttributeAccessor, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, + viewOnlyDeleteCascaded, + jpaProviderDeletesCollection, + elementCascadeDeleteListener, + elementRemoveListener, collectionInstantiator, elementDescriptor, inverseFlusher, @@ -552,11 +645,17 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp return new CollectionAttributeFlusher( attributeName, attributeMapping, + entityClass, + idAttributeName, flushStrategy, entityAttributeAccessor, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, + viewOnlyDeleteCascaded, + jpaProviderDeletesCollection, + elementCascadeDeleteListener, + elementRemoveListener, collectionInstantiator, elementDescriptor, inverseFlusher, @@ -571,7 +670,7 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp boolean shouldFlushUpdates = cascadeUpdate && !updateAllowedSubtypes.isEmpty(); boolean shouldFlushPersists = cascadePersist && !persistAllowedSubtypes.isEmpty(); - ManagedViewTypeImplementor subviewType = getManagedViewType(attribute); + ManagedViewTypeImplementor subviewType = (ManagedViewTypeImplementor) ((com.blazebit.persistence.view.metamodel.SingularAttribute) attribute).getType(); boolean passThrough = false; if (attribute.isUpdatable() || shouldFlushUpdates || (passThrough = shouldPassThrough(evm, viewType, attribute))) { @@ -613,20 +712,18 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp ); } else { // Subview refers to entity type - ViewTypeImpl attributeViewType = (ViewTypeImpl) subviewType; + ViewTypeImplementor attributeViewType = (ViewTypeImplementor) subviewType; InitialValueAttributeAccessor viewAttributeAccessor = Accessors.forMutableViewAttribute(evm, attribute); AttributeAccessor subviewIdAccessor = Accessors.forViewId(evm, attributeViewType, true); ViewToEntityMapper viewToEntityMapper; boolean fetch = shouldFlushUpdates; String parameterName = null; - String updateFragment = null; + SingularAttribute entityIdAttribute = JpaMetamodelUtils.getIdAttribute(evm.getMetamodel().getEntityMetamodel().entity(entityClass)); + String idAttributeMapping = attributeMapping + "." + entityIdAttribute.getName(); if (attribute.isUpdatable()) { - SingularAttribute entityIdAttribute = JpaMetamodelUtils.getIdAttribute(evm.getMetamodel().getEntityMetamodel().entity(entityClass)); - String idAttributeMapping = attributeMapping + "." + entityIdAttribute.getName(); viewToEntityMapper = createViewToEntityMapper(attributeLocation, evm, attributeViewType, cascadePersist, cascadeUpdate, persistAllowedSubtypes, updateAllowedSubtypes); parameterName = attributeName; - updateFragment = idAttributeMapping; } else { if (shouldFlushUpdates) { viewToEntityMapper = new UpdaterBasedViewToEntityMapper( @@ -669,8 +766,11 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp attributeMapping, optimisticLockProtected, attribute.isUpdatable(), + cascadeDelete, + attribute.isOrphanRemoval(), + viewOnlyDeleteCascaded, subviewType.getConverter(), fetch, - updateFragment, + idAttributeMapping, parameterName, passThrough, entityAttributeAccessor, @@ -692,6 +792,20 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp // Basic attributes can normally be updated by queries String parameterName = attributeName; String updateFragment = attributeMapping; + UnmappedBasicAttributeCascadeDeleter deleter; + + if (elementDescriptor.isJpaEntity() && cascadeDelete) { + String elementIdAttributeName = entityMetamodel.getManagedType(ExtendedManagedType.class, attributeType.getJavaType()).getIdAttribute().getName(); + deleter = new UnmappedBasicAttributeCascadeDeleter( + evm, + attributeName, + entityMetamodel.getManagedType(ExtendedManagedType.class, entityClass).getAttribute(attributeMapping), + attributeMapping + "." + elementIdAttributeName, + false + ); + } else { + deleter = null; + } // When wanting to read the actual value of non-updatable attributes or writing values to attributes we need the view attribute accessor // Whenever we merge or persist, we are going to need that @@ -703,13 +817,37 @@ private static DirtyAttributeFlusher createAttributeFlusher(EntityViewManagerImp } boolean supportsQueryFlush = !elementDescriptor.isJpaEmbeddable() || evm.getJpaProvider().supportsUpdateSetEmbeddable(); - return new BasicAttributeFlusher<>(attributeName, attributeMapping, supportsQueryFlush, optimisticLockProtected, updatable, elementDescriptor, updateFragment, parameterName, entityAttributeAccessor, viewAttributeAccessor); + return new BasicAttributeFlusher<>(attributeName, attributeMapping, supportsQueryFlush, optimisticLockProtected, updatable, cascadeDelete, attribute.isOrphanRemoval(), viewOnlyDeleteCascaded, elementDescriptor, updateFragment, parameterName, entityAttributeAccessor, viewAttributeAccessor, deleter); } else { return null; } } } + private static CollectionRemoveListener createOrphanRemoveListener(AbstractMethodAttribute attribute, TypeDescriptor elementDescriptor) { + if (!attribute.isOrphanRemoval()) { + return null; + } + + if (elementDescriptor.isSubview()) { + return new ViewCollectionRemoveListener(elementDescriptor.getLoadOnlyViewToEntityMapper()); + } else { + return EntityCollectionRemoveListener.INSTANCE; + } + } + + private static CollectionRemoveListener createCascadeDeleteListener(AbstractMethodAttribute attribute, TypeDescriptor elementDescriptor) { + if (!attribute.isDeleteCascaded()) { + return null; + } + + if (elementDescriptor.isSubview()) { + return new ViewCollectionRemoveListener(elementDescriptor.getLoadOnlyViewToEntityMapper()); + } else { + return EntityCollectionRemoveListener.INSTANCE; + } + } + private static boolean shouldPassThrough(EntityViewManagerImpl evm, ManagedViewType viewType, AbstractMethodAttribute attribute) { // For an attribute being eligible for pass through, the declaring view must be for an embeddable type // and the attribute must be update mappable @@ -717,50 +855,10 @@ private static boolean shouldPassThrough(EntityViewManagerImpl evm, ManagedViewT && attribute.isUpdateMappable(); } - private static Map> getFetchGraph(String[] fetches, String attributeMapping, ManagedType managedType) { - Map> fetchGraph = null; - boolean isEntity = managedType instanceof EntityType; - if (managedType != null && (isEntity || fetches.length > 0)) { - fetchGraph = new HashMap<>(); - Map> subGraph = new HashMap<>(); - String partPrefix; - - // Entity types, contrary to embeddable types, can be join fetched - if (isEntity) { - fetchGraph.put(attributeMapping, subGraph); - partPrefix = ""; - } else { - // If this is an embeddable type, the subgraph relations get prefixed with the embeddable attribute name - fetchGraph = subGraph; - partPrefix = attributeMapping + "."; - } - - for (String subFetch : fetches) { - String[] parts = subFetch.split("\\."); - Map> currentSubGraph = subGraph; - - for (String part : parts) { - String subPath = partPrefix + part; - Map map = currentSubGraph.get(subPath); - if (map == null) { - map = new HashMap<>(); - currentSubGraph.put(subPath, map); - } - - currentSubGraph = (Map>) map; - } - } - } - return fetchGraph; - } - private static ViewToEntityMapper createViewToEntityMapper(String attributeLocation, EntityViewManagerImpl evm, ViewType viewType, boolean cascadePersist, boolean cascadeUpdate, Set> persistAllowedSubtypes, Set> updateAllowedSubtypes) { EntityLoader entityLoader = new ReferenceEntityLoader(evm, viewType, createViewIdMapper(evm, viewType)); + AttributeAccessor viewIdAccessor = viewIdAccessor = Accessors.forViewId(evm, viewType, true); - AttributeAccessor viewIdAccessor = null; - if (viewType instanceof ViewType) { - viewIdAccessor = Accessors.forViewId(evm, viewType, true); - } Class viewTypeClass = viewType.getJavaType(); boolean mutable = viewType.isUpdatable() || viewType.isCreatable() || !persistAllowedSubtypes.isEmpty() || !updateAllowedSubtypes.isEmpty(); if (!mutable || !cascadeUpdate) { @@ -787,12 +885,4 @@ private static ViewToEntityMapper createViewToEntityMapper(String attributeLocat ); } - private static ManagedViewTypeImplementor getManagedViewType(MethodAttribute methodAttribute) { - if (methodAttribute.getAttributeType() == Attribute.AttributeType.SINGULAR) { - return (ManagedViewTypeImplementor) ((com.blazebit.persistence.view.metamodel.SingularAttribute) methodAttribute).getType(); - } else { - return (ManagedViewTypeImplementor) ((com.blazebit.persistence.view.metamodel.PluralAttribute) methodAttribute).getElementType(); - } - } - } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/InitialStateResetter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/InitialStateResetter.java index 153d175d4c..e00f210842 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/InitialStateResetter.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/InitialStateResetter.java @@ -21,6 +21,7 @@ import com.blazebit.persistence.view.impl.collection.RecordingCollection; import com.blazebit.persistence.view.impl.collection.RecordingMap; import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; import java.util.List; import java.util.Map; @@ -40,6 +41,8 @@ public interface InitialStateResetter { public void addUpdatedView(MutableStateTrackable updatedView); + public void addRemovedView(EntityViewProxy view); + public void addVersionedView(MutableStateTrackable updatedView, Object oldVersion); public void addState(Object[] reference, Object[] copy); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/ResetInitialStateSynchronization.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/ResetInitialStateSynchronization.java index eff1b87f8c..0c4140118f 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/ResetInitialStateSynchronization.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/ResetInitialStateSynchronization.java @@ -20,7 +20,9 @@ import com.blazebit.persistence.view.impl.collection.MapAction; import com.blazebit.persistence.view.impl.collection.RecordingCollection; import com.blazebit.persistence.view.impl.collection.RecordingMap; +import com.blazebit.persistence.view.impl.proxy.DirtyTracker; import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; import javax.transaction.Status; import javax.transaction.Synchronization; @@ -40,6 +42,7 @@ public class ResetInitialStateSynchronization implements Synchronization, Initia private List coalescedRecordingActions; private List persistedViews; private List updatedViews; + private List removedViews; private List versionedViews; @Override @@ -85,6 +88,25 @@ public void addUpdatedView(MutableStateTrackable updatedView) { updatedViews.add(updatedView.$$_resetDirty()); } + @Override + public void addRemovedView(EntityViewProxy view) { + if (removedViews == null) { + removedViews = new ArrayList<>(); + } + removedViews.add(view); + if (view instanceof MutableStateTrackable) { + MutableStateTrackable removedView = (MutableStateTrackable) view; + removedViews.add(removedView.$$_getParent()); + removedViews.add(removedView.$$_getParentIndex()); + removedViews.add(removedView.$$_resetDirty()); + removedView.$$_unsetParent(); + } else { + removedViews.add(null); + removedViews.add(null); + removedViews.add(null); + } + } + @Override public void addVersionedView(MutableStateTrackable updatedView, Object oldVersion) { if (versionedViews == null) { @@ -154,6 +176,22 @@ public void afterCompletion(int status) { view.$$_setDirty((long[]) updatedViews.get(i + 1)); } } + if (removedViews != null) { + for (int i = 0; i < removedViews.size(); i += 4) { + EntityViewProxy view = (EntityViewProxy) removedViews.get(i); + if (view instanceof MutableStateTrackable) { + MutableStateTrackable removedView = (MutableStateTrackable) view; + DirtyTracker parent = (DirtyTracker) removedViews.get(i + 1); + if (parent != null) { + removedView.$$_setParent(parent, (Integer) removedViews.get(i + 2)); + } + long[] dirtyArray = (long[]) removedViews.get(i + 3); + if (dirtyArray != null) { + removedView.$$_setDirty(dirtyArray); + } + } + } + } if (versionedViews != null) { for (int i = 0; i < versionedViews.size(); i += 2) { MutableStateTrackable view = (MutableStateTrackable) versionedViews.get(i); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractPluralAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractPluralAttributeFlusher.java index 16d7b1fa7e..65aac0cf66 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractPluralAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractPluralAttributeFlusher.java @@ -20,6 +20,7 @@ import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.accessor.InitialValueAttributeAccessor; import com.blazebit.persistence.view.impl.change.PluralDirtyChecker; +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; @@ -37,11 +38,17 @@ */ public abstract class AbstractPluralAttributeFlusher, A, R, E, V> extends AttributeFetchGraphNode> implements DirtyAttributeFlusher, PluralDirtyChecker { + protected final Class ownerEntityClass; + protected final String ownerIdAttributeName; protected final FlushStrategy flushStrategy; protected final AttributeAccessor entityAttributeMapper; protected final InitialValueAttributeAccessor viewAttributeAccessor; protected final boolean optimisticLockProtected; protected final boolean collectionUpdatable; + protected final boolean viewOnlyDeleteCascaded; + protected final boolean jpaProviderDeletesCollection; + protected final CollectionRemoveListener cascadeDeleteListener; + protected final CollectionRemoveListener removeListener; protected final TypeDescriptor elementDescriptor; protected final BasicDirtyChecker elementDirtyChecker; protected final PluralFlushOperation flushOperation; @@ -49,13 +56,20 @@ public abstract class AbstractPluralAttributeFlusher> elementFlushers; @SuppressWarnings("unchecked") - public AbstractPluralAttributeFlusher(String attributeName, String mapping, boolean fetch, FlushStrategy flushStrategy, AttributeAccessor entityAttributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, TypeDescriptor elementDescriptor) { + public AbstractPluralAttributeFlusher(String attributeName, String mapping, boolean fetch, Class ownerEntityClass, String ownerIdAttributeName, FlushStrategy flushStrategy, AttributeAccessor entityAttributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, + boolean viewOnlyDeleteCascaded, boolean jpaProviderDeletesCollection, CollectionRemoveListener cascadeDeleteListener, CollectionRemoveListener removeListener, TypeDescriptor elementDescriptor) { super(attributeName, mapping, fetch, elementDescriptor.getViewToEntityMapper() == null ? null : elementDescriptor.getViewToEntityMapper().getFullGraphNode()); + this.ownerEntityClass = ownerEntityClass; + this.ownerIdAttributeName = ownerIdAttributeName; this.flushStrategy = flushStrategy; this.entityAttributeMapper = entityAttributeMapper; this.viewAttributeAccessor = viewAttributeAccessor; this.optimisticLockProtected = optimisticLockProtected; this.collectionUpdatable = collectionUpdatable; + this.viewOnlyDeleteCascaded = viewOnlyDeleteCascaded; + this.jpaProviderDeletesCollection = jpaProviderDeletesCollection; + this.cascadeDeleteListener = cascadeDeleteListener; + this.removeListener = removeListener; this.elementDescriptor = elementDescriptor; if (elementDescriptor.isSubview() || elementDescriptor.isJpaEntity()) { this.elementDirtyChecker = null; @@ -73,11 +87,17 @@ protected AbstractPluralAttributeFlusher(AbstractPluralAttributeFlusher original protected AbstractPluralAttributeFlusher(AbstractPluralAttributeFlusher original, boolean fetch, PluralFlushOperation flushOperation, List collectionActions, List> elementFlushers) { super(original.attributeName, original.mapping, fetch, elementFlushers == null ? original.nestedGraphNode : computeElementFetchGraphNode(elementFlushers)); + this.ownerEntityClass = original.ownerEntityClass; + this.ownerIdAttributeName = original.ownerIdAttributeName; this.flushStrategy = original.flushStrategy; this.entityAttributeMapper = original.entityAttributeMapper; this.viewAttributeAccessor = original.viewAttributeAccessor; this.optimisticLockProtected = original.optimisticLockProtected; this.collectionUpdatable = original.collectionUpdatable; + this.viewOnlyDeleteCascaded = original.viewOnlyDeleteCascaded; + this.jpaProviderDeletesCollection = original.jpaProviderDeletesCollection; + this.cascadeDeleteListener = original.cascadeDeleteListener; + this.removeListener = original.removeListener; this.elementDescriptor = original.elementDescriptor; this.elementDirtyChecker = original.elementDirtyChecker; this.flushOperation = flushOperation; @@ -123,7 +143,7 @@ protected void invokeFlushOperation(UpdateContext context, Object view, E entity case COLLECTION_REPLAY_AND_ELEMENT: if (flushStrategy == FlushStrategy.ENTITY) { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { - elementFlusher.flushEntity(context, entity, view, value); + elementFlusher.flushEntity(context, entity, view, value, null); } } else { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { @@ -138,7 +158,7 @@ protected void invokeFlushOperation(UpdateContext context, Object view, E entity case COLLECTION_REPLACE_AND_ELEMENT: if (flushStrategy == FlushStrategy.ENTITY) { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { - elementFlusher.flushEntity(context, entity, view, value); + elementFlusher.flushEntity(context, entity, view, value, null); } } else { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { @@ -167,6 +187,11 @@ public boolean isPassThrough() { return !collectionUpdatable && !elementDescriptor.shouldFlushMutations(); } + @Override + public String getElementIdAttributeName() { + return null; + } + @Override public AttributeAccessor getViewAttributeAccessor() { return viewAttributeAccessor; @@ -182,11 +207,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } - protected final X persistOrMerge(UpdateContext context, EntityManager em, X object) { - return persistOrMerge(context, em, object, elementDescriptor); + protected final X persistOrMerge(EntityManager em, X object) { + return persistOrMerge(em, object, elementDescriptor); } - protected final X persistOrMerge(UpdateContext context, EntityManager em, X object, TypeDescriptor typeDescriptor) { + protected final X persistOrMerge(EntityManager em, X object, TypeDescriptor typeDescriptor) { if (object != null) { if (typeDescriptor.getBasicUserType().shouldPersist(object)) { if (typeDescriptor.shouldJpaPersist()) { @@ -200,7 +225,7 @@ protected final X persistOrMerge(UpdateContext context, EntityManager em, X return object; } - protected final void persistIfNeeded(UpdateContext context, EntityManager em, Object object, BasicUserType basicUserType) { + protected final void persistIfNeeded(EntityManager em, Object object, BasicUserType basicUserType) { if (object != null) { if (basicUserType.shouldPersist(object)) { em.persist(object); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractUnmappedAttributeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractUnmappedAttributeCascadeDeleter.java new file mode 100644 index 0000000000..5653c70ad3 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/AbstractUnmappedAttributeCascadeDeleter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; + + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public abstract class AbstractUnmappedAttributeCascadeDeleter implements UnmappedAttributeCascadeDeleter { + + protected static final UnmappedAttributeCascadeDeleter[] EMPTY = new UnmappedAttributeCascadeDeleter[0]; + protected final Class elementEntityClass; + protected final String elementIdAttributeName; + protected final String attributeName; + protected final String attributeValuePath; + protected final boolean cascadeDeleteElement; + + public AbstractUnmappedAttributeCascadeDeleter(EntityViewManagerImpl evm, String attributeName, ExtendedAttribute attribute) { + EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); + this.elementEntityClass = attribute.getElementClass(); + this.attributeName = attributeName; + if (entityMetamodel.getEntity(elementEntityClass) == null) { + this.elementIdAttributeName = null; + this.attributeValuePath = attributeName; + this.cascadeDeleteElement = false; + } else { + ExtendedManagedType extendedManagedType = entityMetamodel.getManagedType(ExtendedManagedType.class, elementEntityClass); + this.elementIdAttributeName = extendedManagedType.getIdAttribute().getName(); + this.attributeValuePath = attributeName + "." + elementIdAttributeName; + this.cascadeDeleteElement = attribute.isDeleteCascaded(); + } + } + + @Override + public String getAttributeValuePath() { + return attributeValuePath; + + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/BasicAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/BasicAttributeFlusher.java index 2bdc10affc..9d1b9537f2 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/BasicAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/BasicAttributeFlusher.java @@ -17,12 +17,16 @@ package com.blazebit.persistence.view.impl.update.flush; import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; +import com.blazebit.persistence.view.impl.accessor.InitialValueAttributeAccessor; import com.blazebit.persistence.view.impl.entity.EntityLoaderFetchGraphNode; +import com.blazebit.persistence.view.impl.proxy.DirtyStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.spi.type.TypeConverter; import javax.persistence.Query; +import java.util.Collections; import java.util.List; +import java.util.Objects; /** * @@ -41,17 +45,25 @@ public class BasicAttributeFlusher extends BasicDirtyChecker implements private final boolean optimisticLockProtected; private final EntityLoaderFetchGraphNode fetchGraphNode; private final boolean updatable; + private final boolean cascadeDelete; + private final boolean orphanRemoval; + private final boolean viewOnlyDeleteCascaded; private final String updateFragment; private final AttributeAccessor viewAttributeAccessor; + private final UnmappedBasicAttributeCascadeDeleter deleter; private final V value; private final boolean update; private final BasicFlushOperation flushOperation; - public BasicAttributeFlusher(String attributeName, String mapping, boolean supportsQueryFlush, boolean optimisticLockProtected, boolean updatable, TypeDescriptor elementDescriptor, String updateFragment, String parameterName, AttributeAccessor entityAttributeAccessor, AttributeAccessor viewAttributeAccessor) { + public BasicAttributeFlusher(String attributeName, String mapping, boolean supportsQueryFlush, boolean optimisticLockProtected, boolean updatable, boolean cascadeDelete, boolean orphanRemoval, boolean viewOnlyDeleteCascaded, + TypeDescriptor elementDescriptor, String updateFragment, String parameterName, AttributeAccessor entityAttributeAccessor, AttributeAccessor viewAttributeAccessor, UnmappedBasicAttributeCascadeDeleter deleter) { super(elementDescriptor); this.attributeName = attributeName; this.mapping = mapping; this.optimisticLockProtected = optimisticLockProtected; + this.cascadeDelete = cascadeDelete; + this.orphanRemoval = orphanRemoval; + this.viewOnlyDeleteCascaded = viewOnlyDeleteCascaded; this.fetch = elementDescriptor.shouldJpaMerge(); this.supportsQueryFlush = supportsQueryFlush; this.fetchGraphNode = elementDescriptor.getEntityToEntityMapper() == null ? null : elementDescriptor.getEntityToEntityMapper().getFullGraphNode(); @@ -60,6 +72,7 @@ public BasicAttributeFlusher(String attributeName, String mapping, boolean suppo this.parameterName = parameterName; this.entityAttributeAccessor = entityAttributeAccessor; this.viewAttributeAccessor = viewAttributeAccessor; + this.deleter = deleter; this.value = null; this.update = updatable; this.flushOperation = null; @@ -74,10 +87,14 @@ private BasicAttributeFlusher(BasicAttributeFlusher original, EntityLoader this.optimisticLockProtected = original.optimisticLockProtected; this.fetchGraphNode = fetchGraphNode; this.updatable = original.updatable; + this.cascadeDelete = original.cascadeDelete; + this.orphanRemoval = original.orphanRemoval; + this.viewOnlyDeleteCascaded = original.viewOnlyDeleteCascaded; this.updateFragment = original.updateFragment; this.parameterName = original.parameterName; this.entityAttributeAccessor = original.entityAttributeAccessor; this.viewAttributeAccessor = original.viewAttributeAccessor; + this.deleter = original.deleter; this.value = value; this.update = update; this.flushOperation = flushOperation; @@ -161,35 +178,48 @@ public boolean supportsQueryFlush() { @Override public void flushQuery(UpdateContext context, String parameterPrefix, Query query, Object view, V value) { - value = getConvertedValue(value); - value = persistOrMerge(context, null, view, value); - if (query != null && (updatable || isPassThrough()) && (flushOperation == null || update)) { + V finalValue; + if (flushOperation != null) { + finalValue = this.value; + } else { + finalValue = value; + } + finalValue = getConvertedValue(finalValue); + boolean doUpdate = query != null && (updatable || isPassThrough()) && (flushOperation == null || update); + // Orphan removal is only valid for entity types + if (doUpdate && orphanRemoval) { + Object oldValue = viewAttributeAccessor.getValue(view); + if (!Objects.equals(oldValue, finalValue)) { + context.getEntityManager().remove(oldValue); + } + } + finalValue = persistOrMerge(context, null, view, finalValue); + if (doUpdate) { String parameter; if (parameterPrefix == null) { parameter = parameterName; } else { parameter = parameterPrefix + parameterName; } - query.setParameter(parameter, value); + query.setParameter(parameter, finalValue); } } private V persistOrMerge(UpdateContext context, E entity, Object view, V value) { if (flushOperation != null) { - V finalValue = this.value; if (flushOperation == BasicFlushOperation.PERSIST) { - context.getEntityManager().persist(finalValue); + context.getEntityManager().persist(value); } else if (flushOperation == BasicFlushOperation.MERGE) { if (fetchGraphNode != null) { - Object id = fetchGraphNode.getEntityId(context, finalValue); + Object id = fetchGraphNode.getEntityId(context, value); Object loadedEntity = fetchGraphNode.toEntity(context, id); } - finalValue = context.getEntityManager().merge(finalValue); - if (updatable && finalValue != this.value) { - viewAttributeAccessor.setValue(view, finalValue); + value = context.getEntityManager().merge(value); + if (updatable && value != this.value) { + viewAttributeAccessor.setValue(view, value); } } - return finalValue; + return value; } if (elementDescriptor.shouldJpaPersistOrMerge()) { boolean shouldJpaPersistOrMerge; @@ -240,11 +270,25 @@ protected final V getConvertedValue(V value) { } @Override - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { - value = getConvertedValue(value); - value = persistOrMerge(context, entity, view, value); - if (updatable || isPassThrough()) { - entityAttributeAccessor.setValue(entity, value); + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { + V finalValue; + if (flushOperation != null) { + finalValue = this.value; + } else { + finalValue = value; + } + finalValue = getConvertedValue(finalValue); + boolean doUpdate = updatable || isPassThrough(); + // Orphan removal is only valid for entity types + if (doUpdate && orphanRemoval) { + Object oldValue = viewAttributeAccessor.getValue(view); + if (!Objects.equals(oldValue, finalValue)) { + context.getEntityManager().remove(oldValue); + } + } + finalValue = persistOrMerge(context, entity, view, finalValue); + if (doUpdate) { + entityAttributeAccessor.setValue(entity, finalValue); return true; } @@ -252,8 +296,55 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value } @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // No-op + public List remove(UpdateContext context, E entity, Object view, V value) { + if (cascadeDelete) { + V valueToDelete; + if (view instanceof DirtyStateTrackable && viewAttributeAccessor instanceof InitialValueAttributeAccessor) { + valueToDelete = (V) ((InitialValueAttributeAccessor) viewAttributeAccessor).getInitialValue(view); + } else { + valueToDelete = value; + } + if (valueToDelete != null) { + context.getEntityManager().remove(getConvertedValue(valueToDelete)); + } + } + return Collections.emptyList(); + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + if (cascadeDelete) { + V valueToDelete = (V) entityAttributeAccessor.getValue(entity); + if (valueToDelete != null) { + context.getEntityManager().remove(valueToDelete); + } + } + } + + @Override + public List removeByOwnerId(UpdateContext context, Object ownerId) { + if (deleter != null) { + deleter.removeByOwnerId(context, ownerId); + } + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + if (deleter != null) { + deleter.removeById(context, id); + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + // First the owner of the attribute must be deleted, otherwise we might get an FK violation + return cascadeDelete; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return viewOnlyDeleteCascaded; } private boolean idEqual(Object entityValue, Object newValue) { @@ -269,6 +360,11 @@ public boolean isPassThrough() { return !updatable && !elementDescriptor.shouldFlushMutations(); } + @Override + public String getElementIdAttributeName() { + return attributeName; + } + @Override public AttributeAccessor getViewAttributeAccessor() { return viewAttributeAccessor; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionAttributeFlusher.java index 019a49f814..85879dfccd 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionAttributeFlusher.java @@ -16,8 +16,10 @@ package com.blazebit.persistence.view.impl.update.flush; +import com.blazebit.persistence.DeleteCriteriaBuilder; import com.blazebit.persistence.view.FlushStrategy; import com.blazebit.persistence.view.InverseRemoveStrategy; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.accessor.InitialValueAttributeAccessor; import com.blazebit.persistence.view.impl.change.DirtyChecker; @@ -27,15 +29,18 @@ import com.blazebit.persistence.view.impl.collection.CollectionInstantiator; import com.blazebit.persistence.view.impl.collection.CollectionRemoveAllAction; import com.blazebit.persistence.view.impl.collection.RecordingCollection; +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; import com.blazebit.persistence.view.impl.proxy.DirtyStateTrackable; import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.spi.type.BasicUserType; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; import javax.persistence.EntityManager; import javax.persistence.Query; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -43,6 +48,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; /** * @@ -52,15 +58,14 @@ public class CollectionAttributeFlusher> extends AbstractPluralAttributeFlusher, CollectionAction, RecordingCollection, E, V> implements DirtyAttributeFlusher, E, V> { private static final Object REMOVED_MARKER = new Object(); - private final CollectionInstantiator collectionInstantiator; private final InverseFlusher inverseFlusher; private final InverseCollectionElementAttributeFlusher.Strategy inverseRemoveStrategy; @SuppressWarnings("unchecked") - public CollectionAttributeFlusher(String attributeName, String mapping, FlushStrategy flushStrategy, AttributeAccessor entityAttributeAccessor, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, CollectionInstantiator collectionInstantiator, TypeDescriptor elementDescriptor, - InverseFlusher inverseFlusher, InverseRemoveStrategy inverseRemoveStrategy) { - super(attributeName, mapping, collectionUpdatable || elementDescriptor.shouldFlushMutations(), flushStrategy, entityAttributeAccessor, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, elementDescriptor); + public CollectionAttributeFlusher(String attributeName, String mapping, Class ownerEntityClass, String ownerIdAttributeName, FlushStrategy flushStrategy, AttributeAccessor entityAttributeAccessor, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, + boolean viewOnlyDeleteCascaded, boolean jpaProviderDeletesCollection, CollectionRemoveListener cascadeDeleteListener, CollectionRemoveListener removeListener, CollectionInstantiator collectionInstantiator, TypeDescriptor elementDescriptor, InverseFlusher inverseFlusher, InverseRemoveStrategy inverseRemoveStrategy) { + super(attributeName, mapping, collectionUpdatable || elementDescriptor.shouldFlushMutations(), ownerEntityClass, ownerIdAttributeName, flushStrategy, entityAttributeAccessor, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, viewOnlyDeleteCascaded, jpaProviderDeletesCollection, cascadeDeleteListener, removeListener, elementDescriptor); this.collectionInstantiator = collectionInstantiator; this.inverseFlusher = inverseFlusher; this.inverseRemoveStrategy = InverseCollectionElementAttributeFlusher.Strategy.of(inverseRemoveStrategy); @@ -118,7 +123,7 @@ protected void invokeCollectionAction(UpdateContext context, V targetCollection, // NOTE: We don't care if the actual collection and the initial collection differ // If an error is desired, a user should configure optimistic locking for (CollectionAction action : (List>) (List) collectionActions) { - action.doAction(targetCollection, context, viewToEntityMapper); + action.doAction(targetCollection, context, viewToEntityMapper, removeListener); } } @@ -164,9 +169,9 @@ public boolean requiresFlushAfterPersist(V value) { return false; } - protected boolean executeActions(UpdateContext context, List jpaCollection, List>> actions, ViewToEntityMapper mapper) { + protected boolean executeActions(UpdateContext context, Collection jpaCollection, List>> actions, ViewToEntityMapper mapper) { for (CollectionAction> action : actions) { - action.doAction(jpaCollection, context, mapper); + action.doAction(jpaCollection, context, mapper, removeListener); } return !actions.isEmpty(); } @@ -200,7 +205,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { if (flushOperation != null) { replaceWithRecordingCollection(context, view, value, collectionActions); invokeFlushOperation(context, view, entity, value); @@ -251,7 +256,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value wasDirty = true; } else { if (fetch && elementDescriptor.supportsDeepEqualityCheck()) { - List jpaCollection = (List) entityAttributeMapper.getValue(entity); + Collection jpaCollection = (Collection) entityAttributeMapper.getValue(entity); EqualityChecker equalityChecker; if (elementDescriptor.getBasicUserType() != null) { equalityChecker = new DeepEqualityChecker(elementDescriptor.getBasicUserType()); @@ -276,7 +281,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value } if (!replace) { - recordingCollection.replay((Collection) entityAttributeMapper.getValue(entity), context, elementDescriptor.getLoadOnlyViewToEntityMapper()); + recordingCollection.replay((Collection) entityAttributeMapper.getValue(entity), context, elementDescriptor.getLoadOnlyViewToEntityMapper(), removeListener); } } else { actions = new ArrayList<>(); @@ -323,7 +328,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value if (!replace) { // When we know the collection was fetched, we can try to "merge" the changes into the JPA collection // If either of the collections is empty, we simply do the replace logic - List jpaCollection = (List) entityAttributeMapper.getValue(entity); + Collection jpaCollection = (Collection) entityAttributeMapper.getValue(entity); if (jpaCollection == null || jpaCollection.isEmpty()) { replace = true; } else if (equalityChecker != null) { @@ -364,8 +369,131 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value } @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // TODO: implement proper deletion when delete cascading is activated and collection role management is implemented via #443 + public List remove(UpdateContext context, E entity, Object view, V value) { + V collection; + if (view instanceof DirtyStateTrackable) { + collection = (V) viewAttributeAccessor.getInitialValue(view); + } else { + collection = value; + } + + if (collection != null && !collection.isEmpty()) { + // Entity flushing will do the delete anyway, so we can skip this + if (flushStrategy == FlushStrategy.QUERY && !jpaProviderDeletesCollection) { + removeByOwnerId(context, ((EntityViewProxy) view).$$_getId(), false); + } + if (cascadeDeleteListener != null) { + List elements; + if (collection instanceof RecordingCollection) { + RecordingCollection recordingCollection = (RecordingCollection) collection; + Set removedElements = recordingCollection.getRemovedElements(); + Set addedElements = recordingCollection.getAddedElements(); + elements = new ArrayList<>(collection.size() + removedElements.size()); + + for (Object element : collection) { + // Only report removes for objects that previously existed + if (!addedElements.contains(element)) { + elements.add(element); + } + } + + // Report removed object that would have previously existed as removed + elements.addAll(removedElements); + } else { + elements = new ArrayList<>(collection); + } + if (elements.size() > 0) { + if (inverseFlusher == null) { + return Collections.singletonList(new PostRemoveCollectionElementDeleter(cascadeDeleteListener, elements)); + } else { + // Invoke deletes immediately for inverse relations + for (Object element : elements) { + cascadeDeleteListener.onCollectionRemove(context, element); + } + } + } + } + } + + return Collections.emptyList(); + } + + @Override + public List removeByOwnerId(UpdateContext context, Object id) { + return removeByOwnerId(context, id, true); + } + + private List removeByOwnerId(UpdateContext context, Object ownerId, boolean cascade) { + EntityViewManagerImpl evm = context.getEntityViewManager(); + if (cascade) { + List elementIds; + if (inverseFlusher == null) { + // If there is no inverseFlusher/mapped by attribute, the collection has a join table + if (evm.getDbmsDialect().supportsReturningColumns()) { + elementIds = Arrays.asList(evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName) + .where(ownerIdAttributeName).eq(ownerId) + .executeWithReturning("e." + attributeName + "." + elementDescriptor.getEntityIdAttributeName()) + .getResultList() + .toArray()); + } else { + elementIds = (List) evm.getCriteriaBuilderFactory().create(context.getEntityManager(), ownerEntityClass, "e") + .where(ownerIdAttributeName).eq(ownerId) + .select("e." + attributeName + "." + elementDescriptor.getEntityIdAttributeName()) + .getResultList(); + if (!elementIds.isEmpty() && !jpaProviderDeletesCollection) { + // We must always delete this, otherwise we might get a constraint violation because of the cascading delete + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } + + return Collections.singletonList(new PostRemoveCollectionElementByIdDeleter(elementDescriptor.getElementToEntityMapper(), elementIds)); + } else { + return inverseFlusher.removeByOwnerId(context, ownerId); + } + } else if (!jpaProviderDeletesCollection) { + // delete from Entity(collectionRole) e where e.id = :id + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where("e." + ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + throw new UnsupportedOperationException("Unsupported!"); + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + V value = (V) entityAttributeMapper.getValue(entity); + + if (value != null) { + // In any case we clear the collection + if (cascadeDeleteListener != null) { + if (!value.isEmpty()) { + for (Object element : value) { + cascadeDeleteListener.onEntityCollectionRemove(context, element); + } + entityAttributeMapper.setValue(entity, null); + } + } else { + value.clear(); + } + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return viewOnlyDeleteCascaded; } @Override @@ -373,7 +501,7 @@ protected boolean mergeCollectionElements(UpdateContext context, Object view, E if (elementFlushers != null) { if (flushStrategy == FlushStrategy.ENTITY) { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { - elementFlusher.flushEntity(context, entity, view, value); + elementFlusher.flushEntity(context, entity, view, value, null); } } else { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { @@ -407,7 +535,7 @@ protected boolean mergeCollectionElements(UpdateContext context, Object view, E EntityManager em = context.getEntityManager(); BasicUserType basicUserType = elementDescriptor.getBasicUserType(); for (Object o : value) { - persistIfNeeded(context, em, o, basicUserType); + persistIfNeeded(em, o, basicUserType); } return true; } @@ -422,7 +550,7 @@ protected boolean mergeAndRequeue(UpdateContext context, RecordingCollection rec Iterator iter = newCollection.iterator(); while (iter.hasNext()) { Object o = iter.next(); - Object merged = persistOrMerge(context, em, o); + Object merged = persistOrMerge(em, o); if (o != merged) { if (queuedElements == null) { @@ -1038,7 +1166,7 @@ public void onAddedAndUpdatedInverseElement(DirtyAttributeFlusher flush @Override public void onUpdatedInverseElement(DirtyAttributeFlusher flusher, Object element) { new UpdateCollectionElementAttributeFlusher<>(flusher, element, optimisticLockProtected, elementDescriptor.getViewToEntityMapper()) - .flushEntity(context, null, null, null); + .flushEntity(context, null, null, null, null); } @Override @@ -1046,7 +1174,27 @@ public void onRemovedInverseElement(Object element) { if (inverseRemoveStrategy == InverseCollectionElementAttributeFlusher.Strategy.SET_NULL) { inverseFlusher.flushEntitySetElement(context, element, null, null); } else { - inverseFlusher.removeElement(context, element); + // We need to remove the element from the entity backing collection as well, otherwise it might not be removed properly when using cascading + // Note that this is only necessary for entity flushing which is handled by this code. JPA DML statements like use for query flushing don't respect cascading configurations + Collection entityCollection = (Collection) entityAttributeMapper.getValue(entity); + if (elementDescriptor.getViewToEntityMapper() == null) { + // Element is an entity object so just remove + entityCollection.remove(element); + } else { + final AttributeAccessor entityIdAccessor = elementDescriptor.getViewToEntityMapper().getEntityIdAccessor(); + final AttributeAccessor subviewIdAccessor = elementDescriptor.getViewToEntityMapper().getViewIdAccessor(); + final Object subviewId = subviewIdAccessor.getValue(element); + final Iterator iterator = entityCollection.iterator(); + while (iterator.hasNext()) { + Object collectionElement = iterator.next(); + Object elementId = entityIdAccessor.getValue(collectionElement); + if (elementId.equals(subviewId)) { + iterator.remove(); + break; + } + } + } + inverseFlusher.removeElement(context, entity, element); } } } @@ -1088,7 +1236,7 @@ public void onRemovedInverseElement(Object element) { if (inverseRemoveStrategy == InverseCollectionElementAttributeFlusher.Strategy.SET_NULL) { inverseFlusher.flushQuerySetElement(context, element, null, null, null); } else { - inverseFlusher.removeElement(context, element); + inverseFlusher.removeElement(context, entity, element); } } } @@ -1102,29 +1250,39 @@ private List> getInverseElementFlushersF private void visitInverseElementFlushersForActions(UpdateContext context, Iterable current, Map added, Map removed, ElementChangeListener listener) { if (elementDescriptor.isSubview()) { final ViewToEntityMapper mapper = elementDescriptor.getViewToEntityMapper(); - for (Object o : current) { - if (o instanceof MutableStateTrackable) { - MutableStateTrackable element = (MutableStateTrackable) o; - @SuppressWarnings("unchecked") - DirtyAttributeFlusher flusher = (DirtyAttributeFlusher) (DirtyAttributeFlusher) mapper.getNestedDirtyFlusher(context, element, (DirtyAttributeFlusher) null); - if (flusher != null) { - Object addedElement = added.remove(element); - if (addedElement != null) { - listener.onAddedAndUpdatedInverseElement(flusher, element); - } else { - listener.onUpdatedInverseElement(flusher, element); + // First remove elements, then persist, otherwise we might get a constrain violation + for (Object element : removed.values()) { + listener.onRemovedInverseElement(element); + } + final Iterator iter = getRecordingIterator((V) current); + try { + while (iter.hasNext()) { + Object elem = iter.next(); + if (elem instanceof MutableStateTrackable) { + MutableStateTrackable element = (MutableStateTrackable) elem; + @SuppressWarnings("unchecked") + DirtyAttributeFlusher flusher = (DirtyAttributeFlusher) (DirtyAttributeFlusher) mapper.getNestedDirtyFlusher(context, element, (DirtyAttributeFlusher) null); + if (flusher != null) { + Object addedElement = added.remove(element); + if (addedElement != null) { + listener.onAddedAndUpdatedInverseElement(flusher, element); + } else { + listener.onUpdatedInverseElement(flusher, element); + } } } } - } - for (Object element : removed.values()) { - listener.onRemovedInverseElement(element); + } finally { + resetRecordingIterator((V) current); } // Non-dirty added values for (Object element : added.values()) { listener.onAddedInverseElement(element); } } else if (elementDescriptor.isJpaEntity()) { + for (Object element : removed.values()) { + listener.onRemovedInverseElement(element); + } for (Object element : current) { if (elementDescriptor.getBasicUserType().shouldPersist(element) && elementDescriptor.shouldJpaPersist()) { CollectionElementAttributeFlusher flusher = new PersistCollectionElementAttributeFlusher<>(element, optimisticLockProtected); @@ -1150,9 +1308,6 @@ private void visitInverseElementFlushersForActions(UpdateContext context, Iterab } } } - for (Object element : removed.values()) { - listener.onRemovedInverseElement(element); - } } else { throw new UnsupportedOperationException("Not yet implemented!"); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementAttributeFlusher.java index 5f605594c3..06ebcd4ed3 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementAttributeFlusher.java @@ -21,6 +21,8 @@ import com.blazebit.persistence.view.impl.update.UpdateContext; import javax.persistence.Query; +import java.util.Collections; +import java.util.List; /** * @@ -65,14 +67,45 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { - return nestedGraphNode.flushEntity(context, null, null, (V) element); + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { + return nestedGraphNode.flushEntity(context, null, null, (V) element, null); } @Override @SuppressWarnings("unchecked") - public void remove(UpdateContext context, E entity, Object view, V value) { - nestedGraphNode.remove(context, null, null, (V) element); + public List remove(UpdateContext context, E entity, Object view, V value) { + // No-op + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + // No-op + } + + @Override + public List removeByOwnerId(UpdateContext context, Object id) { + // No-op + return Collections.emptyList(); + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + } + + @Override + public String getElementIdAttributeName() { + return null; + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return false; } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementFetchGraphNode.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementFetchGraphNode.java index 57a67aa9f0..e29d32430b 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementFetchGraphNode.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CollectionElementFetchGraphNode.java @@ -46,7 +46,6 @@ public void appendFetchJoinQueryFragment(String base, StringBuilder sb) { @SuppressWarnings("unchecked") public FetchGraphNode mergeWith(List fetchGraphNodes) { List nestedNodes = new ArrayList<>(fetchGraphNodes.size()); - T firstNode = fetchGraphNodes.get(0).nestedGraphNode; for (int i = 0; i < fetchGraphNodes.size(); i++) { X node = fetchGraphNodes.get(i); if (node.nestedGraphNode != null) { @@ -57,6 +56,7 @@ public FetchGraphNode mergeWith(List fetchGraphNodes) { if (nestedNodes.isEmpty()) { return this; } + T firstNode = nestedNodes.get(0); FetchGraphNode fetchGraphNode = firstNode.mergeWith((List) nestedNodes); if (fetchGraphNode == firstNode) { return this; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CompositeAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CompositeAttributeFlusher.java index 6b68dcd6bd..2b2dfe330d 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CompositeAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/CompositeAttributeFlusher.java @@ -16,17 +16,23 @@ package com.blazebit.persistence.view.impl.update.flush; +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.DeleteCriteriaBuilder; import com.blazebit.persistence.ObjectBuilder; +import com.blazebit.persistence.ReturningResult; import com.blazebit.persistence.view.FlushMode; import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.OptimisticLockException; import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.change.DirtyChecker; import com.blazebit.persistence.view.impl.collection.RecordingCollection; import com.blazebit.persistence.view.impl.collection.RecordingMap; import com.blazebit.persistence.view.impl.entity.EntityLoader; import com.blazebit.persistence.view.impl.entity.EntityTupleizer; +import com.blazebit.persistence.view.impl.entity.ReferenceEntityLoader; import com.blazebit.persistence.view.impl.mapper.ViewMapper; import com.blazebit.persistence.view.impl.proxy.DirtyTracker; +import com.blazebit.persistence.view.impl.update.EntityViewUpdaterImpl; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.impl.entity.FlusherBasedEntityLoader; import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; @@ -35,8 +41,12 @@ import com.blazebit.persistence.view.spi.type.EntityViewProxy; import javax.persistence.Query; +import javax.persistence.Tuple; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.SingularAttribute; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -52,6 +62,7 @@ public class CompositeAttributeFlusher extends CompositeAttributeFetchGraphNode< private static final int FEATURE_SUPPORTS_QUERY_FLUSH = 0; private static final int FEATURE_HAS_PASS_THROUGH_FLUSHER = 1; private static final int FEATURE_IS_ANY_OPTIMISTIC_LOCK_PROTECTED = 2; + private static final UnmappedAttributeCascadeDeleter[] EMPTY = new UnmappedAttributeCascadeDeleter[0]; private final Class entityClass; private final boolean persistable; @@ -64,9 +75,15 @@ public class CompositeAttributeFlusher extends CompositeAttributeFetchGraphNode< private final ObjectBuilder idViewBuilder; private final DirtyAttributeFlusher idFlusher; private final DirtyAttributeFlusher versionFlusher; + // split in pre- and post-object remove based on requiresDeleteCascadeAfterRemove() + private final UnmappedAttributeCascadeDeleter[] unmappedPreRemoveCascadeDeleters; + private final UnmappedAttributeCascadeDeleter[] unmappedPostRemoveCascadeDeleters; private final FlushMode flushMode; private final FlushStrategy flushStrategy; private final EntityLoader entityLoader; + private final EntityLoader referenceEntityLoader; + private final String deleteQuery; + private final String versionedDeleteQuery; private final boolean supportsQueryFlush; private final boolean hasPassThroughFlushers; private final boolean optimisticLockProtected; @@ -74,9 +91,9 @@ public class CompositeAttributeFlusher extends CompositeAttributeFetchGraphNode< private final Object element; @SuppressWarnings("unchecked") - public CompositeAttributeFlusher(Class viewType, Class entityClass, boolean persistable, ViewMapper persistViewMapper, SingularAttribute jpaIdAttribute, AttributeAccessor entityIdAccessor, + public CompositeAttributeFlusher(Class viewType, Class entityClass, ManagedType managedType, boolean persistable, ViewMapper persistViewMapper, SingularAttribute jpaIdAttribute, AttributeAccessor entityIdAccessor, ViewToEntityMapper viewIdMapper, AttributeAccessor viewIdAccessor, EntityTupleizer tupleizer, ObjectBuilder idViewBuilder, DirtyAttributeFlusher idFlusher, - DirtyAttributeFlusher versionFlusher, DirtyAttributeFlusher[] flushers, FlushMode flushMode, FlushStrategy flushStrategy) { + DirtyAttributeFlusher versionFlusher, UnmappedAttributeCascadeDeleter[] cascadeDeleteUnmappedFlushers, DirtyAttributeFlusher[] flushers, FlushMode flushMode, FlushStrategy flushStrategy) { super(viewType, flushers, null); this.entityClass = entityClass; this.persistable = persistable; @@ -89,9 +106,14 @@ public CompositeAttributeFlusher(Class viewType, Class entityClass, boolea this.idViewBuilder = idViewBuilder; this.idFlusher = idFlusher; this.versionFlusher = versionFlusher; + this.unmappedPreRemoveCascadeDeleters = getPreRemoveFlushers(cascadeDeleteUnmappedFlushers); + this.unmappedPostRemoveCascadeDeleters = getPostRemoveFlushers(cascadeDeleteUnmappedFlushers); this.flushMode = flushMode; this.flushStrategy = flushStrategy; this.entityLoader = new FlusherBasedEntityLoader(entityClass, jpaIdAttribute, viewIdMapper, entityIdAccessor, flushers); + this.referenceEntityLoader = new ReferenceEntityLoader(entityClass, jpaIdAttribute, viewIdMapper, entityIdAccessor); + this.deleteQuery = createDeleteQuery(managedType, jpaIdAttribute); + this.versionedDeleteQuery = createVersionedDeleteQuery(deleteQuery, versionFlusher); boolean[] features = determineFeatures(flushers); this.supportsQueryFlush = flushStrategy != FlushStrategy.ENTITY && features[FEATURE_SUPPORTS_QUERY_FLUSH]; this.hasPassThroughFlushers = features[FEATURE_HAS_PASS_THROUGH_FLUSHER]; @@ -112,15 +134,64 @@ private CompositeAttributeFlusher(CompositeAttributeFlusher original, DirtyAttri this.idViewBuilder = original.idViewBuilder; this.idFlusher = original.idFlusher; this.versionFlusher = original.versionFlusher; + this.unmappedPreRemoveCascadeDeleters = original.unmappedPreRemoveCascadeDeleters; + this.unmappedPostRemoveCascadeDeleters = original.unmappedPostRemoveCascadeDeleters; this.flushMode = original.flushMode; this.flushStrategy = original.flushStrategy; this.entityLoader = new FlusherBasedEntityLoader(entityClass, jpaIdAttribute, viewIdMapper, entityIdAccessor, flushers); + this.referenceEntityLoader = original.referenceEntityLoader; + this.deleteQuery = original.deleteQuery; + this.versionedDeleteQuery = original.versionedDeleteQuery; this.supportsQueryFlush = supportsQueryFlush(flushers); this.hasPassThroughFlushers = original.hasPassThroughFlushers; this.optimisticLockProtected = original.optimisticLockProtected; this.element = element; } + private UnmappedAttributeCascadeDeleter[] getPreRemoveFlushers(UnmappedAttributeCascadeDeleter[] cascadeDeleteUnmappedFlushers) { + if (cascadeDeleteUnmappedFlushers == null) { + return EMPTY; + } + List flusherList = new ArrayList<>(cascadeDeleteUnmappedFlushers.length); + for (UnmappedAttributeCascadeDeleter flusher : cascadeDeleteUnmappedFlushers) { + if (!flusher.requiresDeleteCascadeAfterRemove()) { + flusherList.add(flusher); + } + } + + return flusherList.toArray(new UnmappedAttributeCascadeDeleter[flusherList.size()]); + } + + private UnmappedAttributeCascadeDeleter[] getPostRemoveFlushers(UnmappedAttributeCascadeDeleter[] cascadeDeleteUnmappedFlushers) { + if (cascadeDeleteUnmappedFlushers == null) { + return EMPTY; + } + List flusherList = new ArrayList<>(cascadeDeleteUnmappedFlushers.length); + for (UnmappedAttributeCascadeDeleter flusher : cascadeDeleteUnmappedFlushers) { + if (flusher.requiresDeleteCascadeAfterRemove()) { + flusherList.add(flusher); + } + } + + return flusherList.toArray(new UnmappedAttributeCascadeDeleter[flusherList.size()]); + } + + private static String createDeleteQuery(ManagedType managedType, SingularAttribute jpaIdAttribute) { + if (managedType instanceof EntityType && jpaIdAttribute != null) { + return "DELETE FROM " + ((EntityType) managedType).getName() + " e WHERE e." + jpaIdAttribute.getName() + " = :" + EntityViewUpdaterImpl.ID_PARAM_NAME; + } + + return null; + } + + private static String createVersionedDeleteQuery(String deleteQuery, DirtyAttributeFlusher versionFlusher) { + if (deleteQuery != null && versionFlusher != null) { + return deleteQuery + " AND e." + versionFlusher.getAttributeName() + " = :" + EntityViewUpdaterImpl.VERSION_PARAM_NAME; + } + + return null; + } + private static boolean[] determineFeatures(DirtyAttributeFlusher[] flushers) { boolean hasPassThroughFlusher = false; boolean supportsQueryFlush = true; @@ -213,7 +284,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer // Object persisting only works via entity flushing boolean shouldPersist = persist == Boolean.TRUE || persist == null && element.$$_isNew(); if (shouldPersist) { - flushEntity(context, null, value, value); + flushEntity(context, null, value, value, null); return; } @@ -251,7 +322,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, Object entity, Object view, Object value) { + public boolean flushEntity(UpdateContext context, Object entity, Object view, Object value, Runnable postReplaceListener) { if (element != null) { value = element; } @@ -267,15 +338,63 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob List deferredFlushers = null; try { Object id = updatableProxy.$$_getId(); + DirtyTracker parent = updatableProxy.$$_getParent(); + RecordingCollection recordingCollection = null; + RecordingMap recordingMap = null; + Object removedValue = null; + Set removedKeys = null; if (doPersist) { // In case of nested attributes, the entity instance we get is the container of the attribute if (entity == null || !entityClass.isInstance(entity)) { entity = entityLoader.toEntity(context, null); } + // If the parent is a hash based collection, or the view is re-mapped to a different type, remove before setting the id/re-mapping + // There are two cases here, either we are in full flushing and we can get a RecordingIterator via getCurrentIterator + // Or we are in the elementFlusher case where we don't iterate through the backing collection and thus can operate on the backing collection directly + if (parent != null) { + if (parent instanceof RecordingCollection && ((recordingCollection = (RecordingCollection) parent).isHashBased() || persistViewMapper != null)) { + if (recordingCollection.getCurrentIterator() != null) { + recordingCollection.getCurrentIterator().replace(); + } else { + recordingCollection.getDelegate().remove(updatableProxy); + } + } else if (parent instanceof RecordingMap && (persistViewMapper != null || updatableProxy.$$_getParentIndex() == 1 && (recordingMap = (RecordingMap) parent).isHashBased())) { + recordingMap = (RecordingMap) parent; + // Parent index 1 in a recording map means it is part of the key + if (updatableProxy.$$_getParentIndex() == 1) { + if (recordingMap.getCurrentIterator() != null) { + removedValue = recordingMap.getCurrentIterator().replace(); + } else { + removedValue = recordingMap.getDelegate().remove(updatableProxy); + } + } else { + if (removedKeys == null) { + removedKeys = new HashSet<>(); + } + // TODO: replaceValue currently only handles the current value, which is inconsistent regarding what we do in the elementFlusher case + // Not sure if a creatable view should be allowed to occur multiple times in the map as value.. + if (recordingMap.getCurrentIterator() == null) { + for (Map.Entry entry : recordingMap.getDelegate().entrySet()) { + if (entry.getValue().equals(updatableProxy)) { + removedKeys.add(entry.getKey()); + } + } + } else { + recordingMap.getCurrentIterator().replaceValue(removedKeys); + } + } + } + } + + // I know, that this is likely the ugliest hack ever, but to fix this properly would require a major redesign of the flusher handling which is too much work for this version + // A version 2.0 or 3.0 might improve on this when redesigning for operation queueing + if (postReplaceListener != null) { + postReplaceListener.run(); + } if (id != null) { - idFlusher.flushEntity(context, entity, updatableProxy, id); + idFlusher.flushEntity(context, entity, updatableProxy, id, null); } } else { // In case of nested attributes, the entity instance we get is the container of the attribute @@ -302,7 +421,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob deferredFlushers.add(i); wasDirty = true; } else { - wasDirty |= flusher.flushEntity(context, entity, value, state[i]); + wasDirty |= flusher.flushEntity(context, entity, value, state[i], null); } } } @@ -311,7 +430,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob final DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { initialState[i] = flusher.cloneDeep(value, initialState[i], state[i]); - wasDirty |= flusher.flushEntity(context, entity, value, state[i]); + wasDirty |= flusher.flushEntity(context, entity, value, state[i], null); } } } @@ -320,7 +439,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob for (int i = 0; i < state.length; i++) { final DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { - wasDirty |= flusher.flushEntity(context, entity, value, state[i]); + wasDirty |= flusher.flushEntity(context, entity, value, state[i], null); } } } else { @@ -334,7 +453,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob deferredFlushers.add(i); wasDirty = true; } else { - wasDirty |= flusher.flushEntity(context, entity, value, state[i]); + wasDirty |= flusher.flushEntity(context, entity, value, state[i], null); } } } @@ -345,12 +464,12 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob for (int i = state.length; i < flushers.length; i++) { final DirtyAttributeFlusher flusher = flushers[i]; if (flushers[i] != null) { - wasDirty |= flusher.flushEntity(context, entity, value, flusher.getViewAttributeAccessor().getValue(value)); + wasDirty |= flusher.flushEntity(context, entity, value, flusher.getViewAttributeAccessor().getValue(value), null); } } if (versionFlusher != null && wasDirty) { context.getInitialStateResetter().addVersionedView(updatableProxy, updatableProxy.$$_getVersion()); - versionFlusher.flushEntity(context, entity, value, updatableProxy.$$_getVersion()); + versionFlusher.flushEntity(context, entity, value, updatableProxy.$$_getVersion(), null); } if (doPersist) { // If the class of the object is an entity, we persist the object @@ -360,48 +479,6 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob Object[] tuple = tupleizer.tupleize(id); id = idViewBuilder.build(tuple); } - // If the parent is a hash based collection, or the view is re-mapped to a different type, remove before setting the id/re-mapping - DirtyTracker parent = updatableProxy.$$_getParent(); - RecordingCollection recordingCollection = null; - RecordingMap recordingMap = null; - Object removedValue = null; - Set removedKeys = null; - // There are two cases here, either we are in full flushing and we can get a RecordingIterator via getCurrentIterator - // Or we are in the elementFlusher case where we don't iterate through the backing collection and thus can operate on the backing collection directly - if (parent != null) { - if (parent instanceof RecordingCollection && ((recordingCollection = (RecordingCollection) parent).isHashBased() || persistViewMapper != null)) { - if (recordingCollection.getCurrentIterator() != null) { - recordingCollection.getCurrentIterator().replace(); - } else { - recordingCollection.getDelegate().remove(updatableProxy); - } - } else if (parent instanceof RecordingMap && (persistViewMapper != null || updatableProxy.$$_getParentIndex() == 1 && (recordingMap = (RecordingMap) parent).isHashBased())) { - recordingMap = (RecordingMap) parent; - // Parent index 1 in a recording map means it is part of the key - if (updatableProxy.$$_getParentIndex() == 1) { - if (recordingMap.getCurrentIterator() != null) { - removedValue = recordingMap.getCurrentIterator().replace(); - } else { - removedValue = recordingMap.getDelegate().remove(updatableProxy); - } - } else { - if (removedKeys == null) { - removedKeys = new HashSet<>(); - } - // TODO: replaceValue currently only handles the current value, which is inconsistent regarding what we do in the elementFlusher case - // Not sure if a creatable view should be allowed to occur multiple times in the map as value.. - if (recordingMap.getCurrentIterator() == null) { - for (Map.Entry entry : recordingMap.getDelegate().entrySet()) { - if (entry.getValue().equals(updatableProxy)) { - removedKeys.add(entry.getKey()); - } - } - } else { - recordingMap.getCurrentIterator().replaceValue(removedKeys); - } - } - } - } viewIdAccessor.setValue(updatableProxy, id); Object newObject = updatableProxy; if (persistViewMapper != null) { @@ -409,7 +486,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob } if (recordingCollection != null && (recordingCollection.isHashBased() || persistViewMapper != null)) { if (recordingCollection.getCurrentIterator() == null) { - recordingCollection.getDelegate().add(updatableProxy); + recordingCollection.getDelegate().add(newObject); } else { recordingCollection.getCurrentIterator().add(newObject); } @@ -446,7 +523,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob for (int i = 0; i < deferredFlushers.size(); i++) { final int index = deferredFlushers.get(i); final DirtyAttributeFlusher flusher = flushers[index]; - flusher.flushEntity(context, entity, value, state[index]); + flusher.flushEntity(context, entity, value, state[index], null); } } } @@ -454,42 +531,230 @@ public boolean flushEntity(UpdateContext context, Object entity, Object view, Ob } @Override - public void remove(UpdateContext context, Object entity, Object view, Object value) { + public List remove(UpdateContext context, Object entity, Object view, Object value) { if (value instanceof MutableStateTrackable) { MutableStateTrackable updatableProxy = (MutableStateTrackable) value; - // Only remove object that are + // Only remove objects that are // 1. Persistable i.e. of an entity type that can be removed // 2. Have no parent // 3. Haven't been removed yet - // 4. Aren't new i.e. only existing objects, no need to delete object that haven't been persisted yet + // 4. Aren't new i.e. only existing objects, no need to delete object that hasn't been persisted yet if (persistable && !updatableProxy.$$_hasParent() && context.addRemovedObject(value) && !updatableProxy.$$_isNew()) { Object[] state = updatableProxy.$$_getMutableState(); + List postRemoveDeleters = new ArrayList<>(); + for (int i = 0; i < state.length; i++) { final DirtyAttributeFlusher flusher = flushers[i]; - if (flusher != null) { - flusher.remove(context, entity, value, state[i]); + if (flusher != null && !flusher.requiresDeleteCascadeAfterRemove()) { + postRemoveDeleters.addAll(flusher.remove(context, entity, value, state[i])); } } - Object id = updatableProxy.$$_getId(); - entity = entityLoader.toEntity(context, id); - context.getEntityManager().remove(entity); + remove(context, entity, updatableProxy, updatableProxy.$$_getId(), updatableProxy.$$_getVersion(), false); + + for (PostRemoveDeleter postRemoveDeleter : postRemoveDeleters) { + postRemoveDeleter.execute(context); + } + + for (int i = 0; i < state.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && flusher.requiresDeleteCascadeAfterRemove()) { + flusher.remove(context, entity, value, state[i]); + } + } } } else { EntityViewProxy entityView = (EntityViewProxy) value; if (context.addRemovedObject(value)) { - Object id = entityView.$$_getId(); - entity = entityLoader.toEntity(context, id); - context.getEntityManager().remove(entity); + remove(context, entity, entityView, entityView.$$_getId(), entityView.$$_getVersion(), false); + } + } + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + remove(context, null, null, id, null, true); + } + + @Override + public void removeFromEntity(UpdateContext context, Object entity) { + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); + } + + private void remove(UpdateContext context, Object entity, Object view, Object id, Object version, boolean cascadeMappedDeletes) { + if (flushStrategy == FlushStrategy.ENTITY) { + if (entity == null) { + entity = referenceEntityLoader.toEntity(context, id); + } + + // Ensure the entity version is the expected one + if (version != null && versionFlusher != null) { + versionFlusher.remove(context, entity, null, version); + } + + if (cascadeMappedDeletes) { + for (int i = 0; i < flushers.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && !flusher.requiresDeleteCascadeAfterRemove()) { + flusher.removeFromEntity(context, entity); + } + } + } + + context.getEntityManager().remove(entity); + + if (cascadeMappedDeletes) { + // nested cascades + for (int i = 0; i < flushers.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && flusher.requiresDeleteCascadeAfterRemove()) { + flusher.removeFromEntity(context, entity); + } + } + } + } else { + // Query flush strategy + + // TODO: in the future, we could try to aggregate deletes into modification CTEs if we know there are no cycles + + // We only need to cascade delete unmapped attributes for query flushing since entity flushing takes care of that for us + for (int i = 0; i < unmappedPreRemoveCascadeDeleters.length; i++) { + unmappedPreRemoveCascadeDeleters[i].removeByOwnerId(context, id); + } + + Object[] returnedValues = null; + List postRemoveDeleters = new ArrayList<>(); + if (cascadeMappedDeletes) { + for (int i = 0; i < flushers.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && !flusher.requiresDeleteCascadeAfterRemove()) { + postRemoveDeleters.addAll(flusher.removeByOwnerId(context, id)); + } + } + } + + boolean doDelete = true; + // need to "return" the values from the delete query for the post deleters since the values aren't available after executing the delete query + if (cascadeMappedDeletes || unmappedPostRemoveCascadeDeleters.length != 0) { + List returningAttributes = new ArrayList<>(); + for (int i = 0; i < unmappedPostRemoveCascadeDeleters.length; i++) { + returningAttributes.add(unmappedPostRemoveCascadeDeleters[i].getAttributeValuePath()); + } + if (cascadeMappedDeletes) { + for (int i = 0; i < flushers.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && flusher.requiresDeleteCascadeAfterRemove()) { + String elementIdAttributeName = flushers[i].getElementIdAttributeName(); + if (elementIdAttributeName != null) { + returningAttributes.add(elementIdAttributeName); + } + } + } + } + + if (!returningAttributes.isEmpty()) { + // If the dbms supports it, we use the returning feature to do this + if (context.getEntityViewManager().getDbmsDialect().supportsReturningColumns()) { + DeleteCriteriaBuilder cb = context.getEntityViewManager().getCriteriaBuilderFactory().delete(context.getEntityManager(), entityClass); + cb.where(idFlusher.getAttributeName()).eq(id); + if (version != null && versionFlusher != null) { + cb.where(versionFlusher.getAttributeName()).eq(version); + } + + ReturningResult result = cb.executeWithReturning(returningAttributes.toArray(new String[returningAttributes.size()])); + if (version != null && versionFlusher != null) { + if (result.getUpdateCount() != 1) { + throw new OptimisticLockException(entity, view); + } + } + returnedValues = result.getLastResult().toArray(); + doDelete = false; + } else { + // Otherwise we query the attributes + CriteriaBuilder cb = context.getEntityViewManager().getCriteriaBuilderFactory().create(context.getEntityManager(), Object[].class); + cb.from(entityClass); + cb.where(idFlusher.getAttributeName()).eq(id); + for (String attribute : returningAttributes) { + cb.select(attribute); + } + Object result = cb.getSingleResult(); + // Hibernate might return the object itself although we specified that we want an Object[] return... + if (result instanceof Object[]) { + returnedValues = (Object[]) result; + } else { + returnedValues = new Object[]{result}; + } + } + } + } + + if (doDelete) { + if (version != null && versionFlusher != null) { + Query query = context.getEntityManager().createQuery(versionedDeleteQuery); + idFlusher.flushQuery(context, null, query, view, id); + query.setParameter(EntityViewUpdaterImpl.VERSION_PARAM_NAME, version); + int updated = query.executeUpdate(); + if (updated != 1) { + throw new OptimisticLockException(entity, view); + } + } else { + Query query = context.getEntityManager().createQuery(deleteQuery); + idFlusher.flushQuery(context, null, query, view, id); + query.executeUpdate(); + } + } + + for (PostRemoveDeleter postRemoveDeleter : postRemoveDeleters) { + postRemoveDeleter.execute(context); + } + + for (int i = 0; i < unmappedPostRemoveCascadeDeleters.length; i++) { + unmappedPostRemoveCascadeDeleters[i].removeById(context, returnedValues[i]); + } + + if (cascadeMappedDeletes) { + int valueIndex = unmappedPostRemoveCascadeDeleters.length; + for (int i = 0; i < flushers.length; i++) { + final DirtyAttributeFlusher flusher = flushers[i]; + if (flusher != null && flusher.requiresDeleteCascadeAfterRemove() && flusher.getElementIdAttributeName() != null) { + flusher.remove(context, returnedValues[valueIndex++]); + } + } } } } + @Override + public List removeByOwnerId(UpdateContext context, Object id) { + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); + } + @Override public boolean isPassThrough() { - // TODO: Not sure if a composite can ever be a pass through flusher - return false; + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); + } + + @Override + public String getElementIdAttributeName() { + // A composite flusher needs to be wrapped in a subview or collection flusher + throw new UnsupportedOperationException(); } @Override @@ -555,17 +820,18 @@ public DirtyKind getDirtyKind(Object initial, Object current) { @SuppressWarnings("unchecked") public , E, V> DirtyAttributeFlusher getNestedDirtyFlusher(UpdateContext context, MutableStateTrackable updatableProxy) { - if (context.isForceFull() || flushMode == FlushMode.FULL || !(updatableProxy instanceof DirtyStateTrackable)) { + // When we persist, always flush all attributes + boolean shouldPersist = updatableProxy.$$_isNew(); + if (context.isForceFull() || flushMode == FlushMode.FULL || shouldPersist || !(updatableProxy instanceof DirtyStateTrackable)) { return (DirtyAttributeFlusher) this; } - boolean shouldPersist = updatableProxy.$$_isNew(); Object[] initialState = ((DirtyStateTrackable) updatableProxy).$$_getInitialState(); Object[] originalDirtyState = updatableProxy.$$_getMutableState(); @SuppressWarnings("unchecked") DirtyAttributeFlusher[] flushers = new DirtyAttributeFlusher[originalDirtyState.length]; // Copy flushers to the target candidate flushers - if (!updatableProxy.$$_copyDirty(this.flushers, flushers) && !shouldPersist) { + if (!updatableProxy.$$_copyDirty(this.flushers, flushers)) { // If the dirty detection says nothing is dirty, we don't need to do anything return null; } @@ -589,7 +855,7 @@ public , E, V> DirtyAttributeFlusher, E, V> DirtyAttributeFlusher) new CompositeAttributeFlusher(this, flushers, updatableProxy, shouldPersist); + return (DirtyAttributeFlusher) new CompositeAttributeFlusher(this, flushers, updatableProxy, false); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/DirtyAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/DirtyAttributeFlusher.java index c32a208579..b088b28c99 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/DirtyAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/DirtyAttributeFlusher.java @@ -21,6 +21,7 @@ import com.blazebit.persistence.view.impl.update.UpdateContext; import javax.persistence.Query; +import java.util.List; /** * @@ -39,17 +40,29 @@ public interface DirtyAttributeFlusher, public void flushQuery(UpdateContext context, String parameterPrefix, Query query, Object view, V value); - public boolean flushEntity(UpdateContext context, E entity, Object view, V value); + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener); - public void remove(UpdateContext context, E entity, Object view, V value); + public void removeFromEntity(UpdateContext context, E entity); + + public List remove(UpdateContext context, E entity, Object view, V value); + + public void remove(UpdateContext context, Object id); + + public List removeByOwnerId(UpdateContext context, Object id); public V cloneDeep(Object view, V oldValue, V newValue); public boolean isPassThrough(); + public String getElementIdAttributeName(); + public AttributeAccessor getViewAttributeAccessor(); public boolean isOptimisticLockProtected(); public boolean requiresFlushAfterPersist(V value); + + public boolean requiresDeleteCascadeAfterRemove(); + + public boolean isViewOnlyDeleteCascaded(); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EmbeddableAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EmbeddableAttributeFlusher.java index 672f849313..7f57e3b46b 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EmbeddableAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EmbeddableAttributeFlusher.java @@ -24,6 +24,8 @@ import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import javax.persistence.Query; +import java.util.Collections; +import java.util.List; /** * @@ -119,14 +121,41 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { entityAttributeAccessor.setValue(entity, viewToEntityMapper.applyToEntity(context, entityAttributeAccessor.getValue(entity), value)); return true; } @Override - public void remove(UpdateContext context, E entity, Object view, V value) { + public List remove(UpdateContext context, E entity, Object view, V value) { // No-op + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + // No-op + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + // No-op + } + + @Override + public List removeByOwnerId(UpdateContext context, Object id) { + // No-op + return Collections.emptyList(); + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return false; } @Override @@ -134,6 +163,11 @@ public boolean isPassThrough() { return passThrough; } + @Override + public String getElementIdAttributeName() { + return null; + } + @Override public AttributeAccessor getViewAttributeAccessor() { return viewAttributeAccessor; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EntityCollectionRemoveListener.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EntityCollectionRemoveListener.java new file mode 100644 index 0000000000..cbebd869eb --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/EntityCollectionRemoveListener.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class EntityCollectionRemoveListener implements CollectionRemoveListener { + + public static final CollectionRemoveListener INSTANCE = new EntityCollectionRemoveListener(); + + private EntityCollectionRemoveListener() { + } + + @Override + public void onEntityCollectionRemove(UpdateContext context, Object element) { + context.getEntityManager().remove(element); + } + + @Override + public void onCollectionRemove(UpdateContext context, Object element) { + context.getEntityManager().remove(element); + } + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/IndexedListAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/IndexedListAttributeFlusher.java index d0a4f99971..3be7168519 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/IndexedListAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/IndexedListAttributeFlusher.java @@ -26,6 +26,7 @@ import com.blazebit.persistence.view.impl.collection.ListRemoveAction; import com.blazebit.persistence.view.impl.collection.RecordingCollection; import com.blazebit.persistence.view.impl.collection.RecordingList; +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; import com.blazebit.persistence.view.impl.proxy.DirtyStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; @@ -43,11 +44,10 @@ * @since 1.2.0 */ public class IndexedListAttributeFlusher> extends CollectionAttributeFlusher { - @SuppressWarnings("unchecked") - public IndexedListAttributeFlusher(String attributeName, String mapping, FlushStrategy flushStrategy, AttributeAccessor attributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, CollectionInstantiator collectionInstantiator, TypeDescriptor elementDescriptor, - InverseFlusher inverseFlusher, InverseRemoveStrategy inverseRemoveStrategy) { - super(attributeName, mapping, flushStrategy, attributeMapper, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, collectionInstantiator, elementDescriptor, inverseFlusher, inverseRemoveStrategy); + public IndexedListAttributeFlusher(String attributeName, String mapping, Class ownerEntityClass, String ownerIdAttributeName, FlushStrategy flushStrategy, AttributeAccessor attributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, + boolean viewOnlyDeleteCascaded, boolean jpaProviderDeletesCollection, CollectionRemoveListener cascadeDeleteListener, CollectionRemoveListener removeListener, CollectionInstantiator collectionInstantiator, TypeDescriptor elementDescriptor, InverseFlusher inverseFlusher, InverseRemoveStrategy inverseRemoveStrategy) { + super(attributeName, mapping, ownerEntityClass, ownerIdAttributeName, flushStrategy, attributeMapper, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, viewOnlyDeleteCascaded, jpaProviderDeletesCollection, cascadeDeleteListener, removeListener, collectionInstantiator, elementDescriptor, inverseFlusher, inverseRemoveStrategy); } public IndexedListAttributeFlusher(IndexedListAttributeFlusher original, boolean fetch) { @@ -73,7 +73,7 @@ protected boolean mergeAndRequeue(UpdateContext context, RecordingCollection rec List realCollection = (List) newCollection; for (int i = 0; i < realCollection.size(); i++) { Object elem = realCollection.get(i); - Object merged = persistOrMerge(context, em, elem); + Object merged = persistOrMerge(em, elem); if (elem != merged) { if (recordingCollection != null) { recordingCollection.replaceActionElement(elem, merged); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseCollectionElementAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseCollectionElementAttributeFlusher.java index 66f6d979fd..f4eaf7d7ec 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseCollectionElementAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseCollectionElementAttributeFlusher.java @@ -40,7 +40,7 @@ public InverseCollectionElementAttributeFlusher(DirtyAttributeFlusher n @Override public void flushQuery(UpdateContext context, String parameterPrefix, Query query, Object view, V value) { if (strategy == Strategy.REMOVE) { - inverseFlusher.removeElement(context, element); + inverseFlusher.removeElement(context, null, element); } else if (strategy != Strategy.IGNORE) { inverseFlusher.flushQuerySetElement(context, (V) element, strategy == Strategy.SET_NULL ? null : view, parameterPrefix, (DirtyAttributeFlusher) nestedGraphNode); } @@ -48,9 +48,9 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { if (strategy == Strategy.REMOVE) { - inverseFlusher.removeElement(context, element); + inverseFlusher.removeElement(context, entity, element); } else if (strategy != Strategy.IGNORE) { inverseFlusher.flushEntitySetElement(context, (V) element, strategy == Strategy.SET_NULL ? null : entity, (DirtyAttributeFlusher) nestedGraphNode); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseFlusher.java index c6479bef36..98b45d5e5b 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/InverseFlusher.java @@ -16,6 +16,10 @@ package com.blazebit.persistence.view.impl.update.flush; +import com.blazebit.persistence.DeleteCriteriaBuilder; +import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; import com.blazebit.persistence.view.OptimisticLockException; import com.blazebit.persistence.view.impl.EntityViewManagerImpl; import com.blazebit.persistence.view.impl.accessor.Accessors; @@ -40,6 +44,7 @@ import javax.persistence.Query; import java.util.Collections; +import java.util.List; /** * @@ -48,6 +53,11 @@ */ public final class InverseFlusher { + private final Class parentEntityClass; + private final String attributeName; + private final String parentIdAttributeName; + private final String childIdAttributeName; + private final UnmappedAttributeCascadeDeleter deleter; // Maps the parent view object to an entity via means of em.getReference private final ViewToEntityMapper parentReferenceViewToEntityMapper; // Allows to flush a parent reference value for a child element @@ -67,9 +77,15 @@ public final class InverseFlusher { private final Mapper parentEntityOnChildEntityMapper; private final InverseEntityToEntityMapper childEntityToEntityMapper; - public InverseFlusher(ViewToEntityMapper parentReferenceViewToEntityMapper, DirtyAttributeFlusher parentReferenceAttributeFlusher, + public InverseFlusher(Class parentEntityClass, String attributeName, String parentIdAttributeName, String childIdAttributeName, UnmappedAttributeCascadeDeleter deleter, + ViewToEntityMapper parentReferenceViewToEntityMapper, DirtyAttributeFlusher parentReferenceAttributeFlusher, Mapper parentEntityOnChildViewMapper, InverseViewToEntityMapper childViewToEntityMapper, ViewToEntityMapper childReferenceViewToEntityMapper, Mapper parentEntityOnChildEntityMapper, InverseEntityToEntityMapper childEntityToEntityMapper) { + this.parentEntityClass = parentEntityClass; + this.attributeName = attributeName; + this.parentIdAttributeName = parentIdAttributeName; + this.childIdAttributeName = childIdAttributeName; + this.deleter = deleter; this.parentReferenceViewToEntityMapper = parentReferenceViewToEntityMapper; this.parentReferenceAttributeFlusher = parentReferenceAttributeFlusher; this.parentEntityOnChildViewMapper = parentEntityOnChildViewMapper; @@ -83,6 +99,7 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana if (attribute.getMappedBy() != null) { String attributeLocation = attribute.getLocation(); PluralAttribute pluralAttribute = (PluralAttribute) attribute; + Class elementEntityClass = null; AttributeAccessor parentReferenceAttributeAccessor = null; Mapper parentEntityOnChildViewMapper = null; @@ -100,14 +117,19 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana // This happens when the mapped by attribute is insertable=false and updatable=false if (childTypeDescriptor.isSubview()) { ViewType childViewType = (ViewType) pluralAttribute.getElementType(); + elementEntityClass = childViewType.getEntityClass(); parentEntityOnChildViewMapper = (Mapper) Mappers.forEntityAttributeMappingConvertToViewAttributeMapping( evm, viewType.getEntityClass(), childViewType, attribute.getWritableMappedByMappings() ); - //TODO: determine the view accessor to set the inverse id on the view object - parentEntityOnChildEntityMapper = null; + parentEntityOnChildEntityMapper = (Mapper) Mappers.forEntityAttributeMapping( + evm.getMetamodel().getEntityMetamodel(), + viewType.getEntityClass(), + childViewType.getEntityClass(), + attribute.getWritableMappedByMappings() + ); childReferenceViewToEntityMapper = new LoadOrPersistViewToEntityMapper( attributeLocation, evm, @@ -120,6 +142,7 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana ); } else if (childTypeDescriptor.isJpaEntity()) { Class childType = pluralAttribute.getElementType().getJavaType(); + elementEntityClass = childType; parentEntityOnChildViewMapper = (Mapper) Mappers.forEntityAttributeMapping( evm.getMetamodel().getEntityMetamodel(), viewType.getEntityClass(), @@ -130,6 +153,7 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana } else { if (childTypeDescriptor.isSubview()) { ViewType childViewType = (ViewType) pluralAttribute.getElementType(); + elementEntityClass = childViewType.getEntityClass(); parentReferenceAttributeAccessor = Accessors.forEntityMapping( evm.getMetamodel().getEntityMetamodel(), childViewType.getEntityClass(), @@ -148,6 +172,7 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana parentEntityOnChildEntityMapper = Mappers.forAccessor(parentReferenceAttributeAccessor); } else if (childTypeDescriptor.isJpaEntity()) { Class childType = pluralAttribute.getElementType().getJavaType(); + elementEntityClass = childType; parentReferenceAttributeAccessor = Accessors.forEntityMapping( evm.getMetamodel().getEntityMetamodel(), childType, @@ -155,7 +180,6 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana ); parentEntityOnChildViewMapper = Mappers.forAccessor(parentReferenceAttributeAccessor); } - } DirtyAttributeFlusher parentReferenceAttributeFlusher = new ParentReferenceAttributeFlusher<>( @@ -167,6 +191,25 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana parentEntityOnChildViewMapper ); + UnmappedAttributeCascadeDeleter deleter = null; + String parentIdAttributeName = null; + String childIdAttributeName = null; + // Only construct when orphanRemoval or delete cascading is enabled, orphanRemoval implies delete cascading + if (attribute.isDeleteCascaded()) { + EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); + ExtendedManagedType elementManagedType = entityMetamodel.getManagedType(ExtendedManagedType.class, elementEntityClass); + parentIdAttributeName = entityMetamodel.getManagedType(ExtendedManagedType.class, viewType.getEntityClass()).getIdAttribute().getName(); + childIdAttributeName = elementManagedType.getIdAttribute().getName(); + + String mapping = attribute.getMappedBy(); + ExtendedAttribute extendedAttribute = elementManagedType.getAttribute(mapping); + if (childTypeDescriptor.isSubview()) { + deleter = new ViewTypeCascadeDeleter(childTypeDescriptor.getViewToEntityMapper()); + } else if (childTypeDescriptor.isJpaEntity()) { + deleter = new UnmappedBasicAttributeCascadeDeleter(evm, mapping, extendedAttribute, mapping + "." + parentIdAttributeName, false); + } + } + if (childTypeDescriptor.isSubview()) { ViewType childViewType = (ViewType) pluralAttribute.getElementType(); childViewToEntityMapper = new InverseViewToEntityMapper( @@ -189,7 +232,11 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana } return new InverseFlusher( - parentReferenceViewToEntityMapper, + viewType.getEntityClass(), + attribute.getMapping(), + parentIdAttributeName, + childIdAttributeName, + deleter, parentReferenceViewToEntityMapper, parentReferenceAttributeFlusher, parentEntityOnChildViewMapper, childViewToEntityMapper, @@ -202,7 +249,23 @@ public static InverseFlusher forAttribute(EntityViewManagerImpl evm, Mana return null; } - public void removeElement(UpdateContext context, Object element) { + public List removeByOwnerId(UpdateContext context, Object ownerId) { + EntityViewManagerImpl evm = context.getEntityViewManager(); + List elementIds = (List) evm.getCriteriaBuilderFactory().create(context.getEntityManager(), parentEntityClass, "e") + .where(parentIdAttributeName).eq(ownerId) + .select("e." + attributeName + "." + childIdAttributeName) + .getResultList(); + if (!elementIds.isEmpty()) { + // We must always delete this, otherwise we might get a constraint violation because of the cascading delete + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), parentEntityClass, "e", attributeName); + cb.where(parentIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + + return Collections.singletonList(new PostRemoveInverseCollectionElementByIdDeleter(deleter, elementIds)); + } + + public void removeElement(UpdateContext context, Object ownerEntity, Object element) { if (childViewToEntityMapper != null) { removeViewElement(context, element); } else { @@ -264,10 +327,10 @@ public void flushQuerySetEntityOnElement(UpdateContext context, Object element, } private void flushQuerySetEntityOnViewElement(UpdateContext context, Object element, E newValue, String parameterPrefix, DirtyAttributeFlusher nestedGraphNode) { + flushQuerySetEntityOnElement(context, element, newValue, parameterPrefix, nestedGraphNode, childViewToEntityMapper); if (parentEntityOnChildViewMapper != null) { parentEntityOnChildViewMapper.map(newValue, element); } - flushQuerySetEntityOnElement(context, element, newValue, parameterPrefix, nestedGraphNode, childViewToEntityMapper); } private void flushQuerySetEntityOnEntityElement(UpdateContext context, Object element, E newValue, String parameterPrefix, DirtyAttributeFlusher nestedGraphNode) { @@ -277,7 +340,7 @@ private void flushQuerySetEntityOnEntityElement(UpdateContext context, Object el private void flushQuerySetEntityOnElement(UpdateContext context, Object element, E newValue, String parameterPrefix, DirtyAttributeFlusher nestedGraphNode, InverseElementToEntityMapper elementToEntityMapper) { if (shouldPersist(element)) { - nestedGraphNode.flushQuery(context, parameterPrefix, null, null, element); + elementToEntityMapper.flushEntity(context, newValue, element, nestedGraphNode); } else { Query q = elementToEntityMapper.createInverseUpdateQuery(context, element, nestedGraphNode, parentReferenceAttributeFlusher); if (nestedGraphNode != null) { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MapAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MapAttributeFlusher.java index d326a6be44..3ff7975174 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MapAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MapAttributeFlusher.java @@ -16,7 +16,9 @@ package com.blazebit.persistence.view.impl.update.flush; +import com.blazebit.persistence.DeleteCriteriaBuilder; import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.accessor.InitialValueAttributeAccessor; import com.blazebit.persistence.view.impl.change.DirtyChecker; @@ -28,21 +30,25 @@ import com.blazebit.persistence.view.impl.collection.MapPutAllAction; import com.blazebit.persistence.view.impl.collection.MapRemoveAction; import com.blazebit.persistence.view.impl.collection.RecordingMap; +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; import com.blazebit.persistence.view.impl.entity.MapViewToEntityMapper; import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; import com.blazebit.persistence.view.impl.proxy.DirtyStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.spi.type.BasicUserType; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; import javax.persistence.EntityManager; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * @@ -56,13 +62,16 @@ public class MapAttributeFlusher> extends AbstractPluralA private final MapInstantiator mapInstantiator; private final MapViewToEntityMapper mapper; private final MapViewToEntityMapper loadOnlyMapper; + private final CollectionRemoveListener keyCascadeDeleteListener; + private final CollectionRemoveListener keyRemoveListener; private final TypeDescriptor keyDescriptor; private final BasicDirtyChecker keyDirtyChecker; @SuppressWarnings("unchecked") - public MapAttributeFlusher(String attributeName, String mapping, FlushStrategy flushStrategy, AttributeAccessor attributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean collectionUpdatable, boolean optimisticLockProtected, TypeDescriptor keyDescriptor, TypeDescriptor elementDescriptor, - MapViewToEntityMapper mapper, MapViewToEntityMapper loadOnlyMapper, MapInstantiator mapInstantiator) { - super(attributeName, mapping, collectionUpdatable || elementDescriptor.isMutable(), flushStrategy, attributeMapper, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, elementDescriptor); + public MapAttributeFlusher(String attributeName, String mapping, Class ownerEntityClass, String ownerIdAttributeName, FlushStrategy flushStrategy, AttributeAccessor attributeMapper, InitialValueAttributeAccessor viewAttributeAccessor, boolean optimisticLockProtected, boolean collectionUpdatable, + CollectionRemoveListener keyCascadeDeleteListener, CollectionRemoveListener elementCascadeDeleteListener, CollectionRemoveListener keyRemoveListener, CollectionRemoveListener elementRemoveListener, + boolean viewOnlyDeleteCascaded, boolean jpaProviderDeletesCollection, TypeDescriptor keyDescriptor, TypeDescriptor elementDescriptor, MapViewToEntityMapper mapper, MapViewToEntityMapper loadOnlyMapper, MapInstantiator mapInstantiator) { + super(attributeName, mapping, collectionUpdatable || elementDescriptor.isMutable(), ownerEntityClass, ownerIdAttributeName, flushStrategy, attributeMapper, viewAttributeAccessor, optimisticLockProtected, collectionUpdatable, viewOnlyDeleteCascaded, jpaProviderDeletesCollection, elementCascadeDeleteListener, elementRemoveListener, elementDescriptor); this.mapInstantiator = mapInstantiator; this.keyDescriptor = keyDescriptor; if (keyDescriptor.isSubview() || keyDescriptor.isJpaEntity()) { @@ -70,6 +79,8 @@ public MapAttributeFlusher(String attributeName, String mapping, FlushStrategy f } else { this.keyDirtyChecker = new BasicDirtyChecker<>(keyDescriptor); } + this.keyRemoveListener = keyRemoveListener; + this.keyCascadeDeleteListener = keyCascadeDeleteListener; this.mapper = mapper; this.loadOnlyMapper = loadOnlyMapper; } @@ -83,6 +94,8 @@ protected MapAttributeFlusher(MapAttributeFlusher original, boolean fetch, Plura this.mapInstantiator = original.mapInstantiator; this.keyDescriptor = original.keyDescriptor; this.keyDirtyChecker = original.keyDirtyChecker; + this.keyRemoveListener = original.keyRemoveListener; + this.keyCascadeDeleteListener = original.keyCascadeDeleteListener; this.mapper = original.mapper; this.loadOnlyMapper = original.loadOnlyMapper; } @@ -149,7 +162,7 @@ public boolean isPassThrough() { @Override protected void invokeCollectionAction(UpdateContext context, V targetCollection, List> collectionActions) { for (MapAction action : (List>) (List) collectionActions) { - action.doAction(targetCollection, context, loadOnlyMapper); + action.doAction(targetCollection, context, loadOnlyMapper, keyRemoveListener, removeListener); } } @@ -183,7 +196,7 @@ protected V replaceWithRecordingCollection(UpdateContext context, Object view, V @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { if (flushOperation != null) { replaceWithRecordingCollection(context, view, value, collectionActions); invokeFlushOperation(context, view, entity, value); @@ -246,7 +259,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value replace = true; } else { for (MapAction> action : actions) { - action.doAction(jpaCollection, context, loadOnlyMapper); + action.doAction(jpaCollection, context, loadOnlyMapper, keyRemoveListener, removeListener); } return !actions.isEmpty(); } @@ -257,7 +270,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value } } if (!replace) { - recordingMap.replay((Map) entityAttributeMapper.getValue(entity), context, loadOnlyMapper); + recordingMap.replay((Map) entityAttributeMapper.getValue(entity), context, loadOnlyMapper, keyRemoveListener, removeListener); } } else { actions = new ArrayList<>(); @@ -375,7 +388,7 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value replace = true; } else { for (MapAction> action : actions) { - action.doAction(jpaCollection, context, loadOnlyMapper); + action.doAction(jpaCollection, context, loadOnlyMapper, keyRemoveListener, removeListener); } wasDirty |= !actions.isEmpty(); } @@ -421,8 +434,154 @@ private void resetRecordingIterator(Map value) { } @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // TODO: implement proper deletion when delete cascading is activated and collection role management is implemented via #443 + public List remove(UpdateContext context, E entity, Object view, V value) { + V map; + if (view instanceof DirtyStateTrackable) { + map = (V) viewAttributeAccessor.getInitialValue(view); + } else { + map = value; + } + + if (map != null && !map.isEmpty()) { + // Entity flushing will do the delete anyway, so we can skip this + if (flushStrategy == FlushStrategy.QUERY && !jpaProviderDeletesCollection) { + removeByOwnerId(context, ((EntityViewProxy) view).$$_getId(), false); + } + + if (cascadeDeleteListener != null || keyCascadeDeleteListener != null) { + List keys; + List values; + if (map instanceof RecordingMap) { + RecordingMap recordingMap = (RecordingMap) map; + Set removedKeys = recordingMap.getRemovedKeys(); + Set removedElements = recordingMap.getRemovedElements(); + Set addedKeys = recordingMap.getAddedKeys(); + Set addedElements = recordingMap.getAddedElements(); + keys = new ArrayList<>(recordingMap.size() + removedKeys.size()); + values = new ArrayList<>(recordingMap.size() + removedElements.size()); + + for (Map.Entry entry : map.entrySet()) { + // Only report removes for objects that previously existed + if (keyCascadeDeleteListener != null && !addedKeys.contains(entry.getKey())) { + keys.add(entry.getKey()); + } + if (cascadeDeleteListener != null && !addedElements.contains(entry.getValue())) { + values.add(entry.getValue()); + } + } + + // Report removed object that would have previously existed as removed + if (keyCascadeDeleteListener != null) { + keys.addAll(removedKeys); + } + if (cascadeDeleteListener != null) { + values.addAll(removedElements); + } + } else { + keys = new ArrayList<>(map.size()); + values = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + if (keyCascadeDeleteListener != null) { + keys.add(entry.getKey()); + } + if (cascadeDeleteListener != null) { + values.add(entry.getValue()); + } + } + } + if (keys.size() > 0 || values.size() > 0) { + List list = new ArrayList<>(2); + if (keys.size() > 0) { + list.add(new PostRemoveCollectionElementDeleter(keyCascadeDeleteListener, keys)); + } + if (values.size() > 0) { + list.add(new PostRemoveCollectionElementDeleter(cascadeDeleteListener, values)); + } + return list; + } + } + } + + return Collections.emptyList(); + } + + @Override + public List removeByOwnerId(UpdateContext context, Object id) { + return removeByOwnerId(context, id, true); + } + + private List removeByOwnerId(UpdateContext context, Object ownerId, boolean cascade) { + EntityViewManagerImpl evm = context.getEntityViewManager(); + if (cascade) { + List elementIds; + // If there is no inverseFlusher/mapped by attribute, the collection has a join table + if (evm.getDbmsDialect().supportsReturningColumns()) { + elementIds = Arrays.asList(evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName) + .where(ownerIdAttributeName).eq(ownerId) + .executeWithReturning("e." + attributeName + "." + elementDescriptor.getEntityIdAttributeName()) + .getResultList() + .toArray()); + } else { + elementIds = (List) evm.getCriteriaBuilderFactory().create(context.getEntityManager(), ownerEntityClass, "e") + .where(ownerIdAttributeName).eq(ownerId) + .select("e." + attributeName + "." + elementDescriptor.getEntityIdAttributeName()) + .getResultList(); + if (!elementIds.isEmpty() && !jpaProviderDeletesCollection) { + // We must always delete this, otherwise we might get a constraint violation because of the cascading delete + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } + + return Collections.singletonList(new PostRemoveCollectionElementByIdDeleter(elementDescriptor.getElementToEntityMapper(), elementIds)); + } else if (!jpaProviderDeletesCollection) { + // delete from Entity(collectionRole) e where e.id = :id + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where("e." + ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + throw new UnsupportedOperationException("Unsupported!"); + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + V value = (V) entityAttributeMapper.getValue(entity); + + if (value != null) { + // In any case we clear the collection + if (cascadeDeleteListener != null || keyCascadeDeleteListener != null) { + if (!value.isEmpty()) { + for (Map.Entry entry : value.entrySet()) { + if (keyCascadeDeleteListener != null) { + keyCascadeDeleteListener.onEntityCollectionRemove(context, entry.getKey()); + } + if (cascadeDeleteListener != null) { + cascadeDeleteListener.onEntityCollectionRemove(context, entry.getValue()); + } + } + entityAttributeMapper.setValue(entity, null); + } + } else { + value.clear(); + } + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return viewOnlyDeleteCascaded; } protected EqualityChecker getElementEqualityChecker() { @@ -442,7 +601,7 @@ protected EqualityChecker getElementEqualityChecker() { } protected final X persistOrMergeKey(UpdateContext context, EntityManager em, X object) { - return persistOrMerge(context, em, object, keyDescriptor); + return persistOrMerge(em, object, keyDescriptor); } private boolean mergeAndRequeue(UpdateContext context, RecordingMap recordingCollection, Map newCollection) { @@ -466,7 +625,7 @@ private boolean mergeAndRequeue(UpdateContext context, RecordingMap recordingCol } if (flushValue) { - value = persistOrMerge(context, em, value); + value = persistOrMerge(em, value); } else if (valueMapper != null) { valueMapper.applyToEntity(context, null, value); } @@ -499,7 +658,7 @@ protected boolean mergeCollectionElements(UpdateContext context, Object view, E if (elementFlushers != null) { if (flushStrategy == FlushStrategy.ENTITY) { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { - elementFlusher.flushEntity(context, entity, view, value); + elementFlusher.flushEntity(context, entity, view, value, null); } } else { for (CollectionElementAttributeFlusher elementFlusher : elementFlushers) { @@ -541,7 +700,7 @@ protected boolean mergeCollectionElements(UpdateContext context, Object view, E if (v != null) { if (flushValue) { - v = persistOrMerge(context, em, v); + v = persistOrMerge(em, v); } else if (valueMapper != null) { valueMapper.applyToEntity(context, null, v); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MergeCollectionElementAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MergeCollectionElementAttributeFlusher.java index bb8b4ba7e4..4b594b2a9d 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MergeCollectionElementAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/MergeCollectionElementAttributeFlusher.java @@ -52,16 +52,11 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { context.getEntityManager().merge(element); return true; } - @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // No-op - } - @Override public DirtyAttributeFlusher, E, V> getDirtyFlusher(UpdateContext context, Object view, Object initial, Object current) { // Actually this should never be called, but let's return this to be safe diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ParentReferenceAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ParentReferenceAttributeFlusher.java index c2c6b137ed..0ef1f924ef 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ParentReferenceAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ParentReferenceAttributeFlusher.java @@ -37,7 +37,7 @@ public class ParentReferenceAttributeFlusher extends BasicAttributeFlusher private final String[] updateQueryFragments; public ParentReferenceAttributeFlusher(String attributeName, String mapping, Map writableMappings, TypeDescriptor typeDescriptor, AttributeAccessor attributeAccessor, Mapper mapper) { - super(attributeName, mapping, true, false, true, typeDescriptor, mapping, mapping, attributeAccessor, null); + super(attributeName, mapping, true, false, true, false, false, false, typeDescriptor, mapping, mapping, attributeAccessor, null, null); this.writableMappings = writableMappings; this.mapper = mapper; if (writableMappings != null) { @@ -89,7 +89,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer } @Override - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { mapper.map(value, entity); return true; } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PersistCollectionElementAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PersistCollectionElementAttributeFlusher.java index c03df6b17c..0e79a487de 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PersistCollectionElementAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PersistCollectionElementAttributeFlusher.java @@ -52,16 +52,11 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { context.getEntityManager().persist(element); return true; } - @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // No-op - } - @Override public DirtyAttributeFlusher, E, V> getDirtyFlusher(UpdateContext context, Object view, Object initial, Object current) { // Actually this should never be called, but let's return this to be safe diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementByIdDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementByIdDeleter.java new file mode 100644 index 0000000000..c733ca97e7 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementByIdDeleter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.entity.ElementToEntityMapper; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class PostRemoveCollectionElementByIdDeleter implements PostRemoveDeleter { + + private final ElementToEntityMapper elementToEntityMapper; + private final List elementIds; + + public PostRemoveCollectionElementByIdDeleter(ElementToEntityMapper elementToEntityMapper, List elementIds) { + this.elementToEntityMapper = elementToEntityMapper; + this.elementIds = elementIds; + } + + @Override + public void execute(UpdateContext context) { + for (Object elementId : elementIds) { + elementToEntityMapper.removeById(context, elementId); + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementDeleter.java new file mode 100644 index 0000000000..f1d04a313c --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveCollectionElementDeleter.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class PostRemoveCollectionElementDeleter implements PostRemoveDeleter { + + private final CollectionRemoveListener collectionRemoveListener; + private final List elements; + + public PostRemoveCollectionElementDeleter(CollectionRemoveListener collectionRemoveListener, List elements) { + this.collectionRemoveListener = collectionRemoveListener; + this.elements = elements; + } + + @Override + public void execute(UpdateContext context) { + for (Object element : elements) { + collectionRemoveListener.onCollectionRemove(context, element); + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveDeleter.java new file mode 100644 index 0000000000..984b47c9ec --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveDeleter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.update.UpdateContext; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public interface PostRemoveDeleter { + + public void execute(UpdateContext context); + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveInverseCollectionElementByIdDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveInverseCollectionElementByIdDeleter.java new file mode 100644 index 0000000000..f6055ddb69 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/PostRemoveInverseCollectionElementByIdDeleter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class PostRemoveInverseCollectionElementByIdDeleter implements PostRemoveDeleter { + + private final UnmappedAttributeCascadeDeleter deleter; + private final List elementIds; + + public PostRemoveInverseCollectionElementByIdDeleter(UnmappedAttributeCascadeDeleter deleter, List elementIds) { + this.deleter = deleter; + this.elementIds = elementIds; + } + + @Override + public void execute(UpdateContext context) { + for (Object elementId : elementIds) { + deleter.removeById(context, elementId); + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/SubviewAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/SubviewAttributeFlusher.java index 82716182cb..22e0d0cd32 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/SubviewAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/SubviewAttributeFlusher.java @@ -25,8 +25,12 @@ import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.update.EntityViewUpdater; import com.blazebit.persistence.view.impl.update.UpdateContext; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; +import com.blazebit.persistence.view.spi.type.TypeConverter; import javax.persistence.Query; +import java.util.Collections; +import java.util.List; import java.util.Objects; /** @@ -38,9 +42,13 @@ public class SubviewAttributeFlusher extends AttributeFetchGraphNode converter; private final AttributeAccessor entityAttributeAccessor; private final InitialValueAttributeAccessor viewAttributeAccessor; private final AttributeAccessor subviewIdAccessor; @@ -50,11 +58,16 @@ public class SubviewAttributeFlusher extends AttributeFetchGraphNode converter, boolean fetch, String elementIdAttributePath, String parameterName, boolean passThrough, + AttributeAccessor entityAttributeAccessor, InitialValueAttributeAccessor viewAttributeAccessor, AttributeAccessor subviewIdAccessor, ViewToEntityMapper viewToEntityMapper) { super(attributeName, mapping, fetch, (DirtyAttributeFlusher) viewToEntityMapper.getFullGraphNode()); this.optimisticLockProtected = optimisticLockProtected; this.updatable = updatable; - this.updateFragment = updateFragment; + this.cascadeDelete = cascadeDelete; + this.orphanRemoval = orphanRemoval; + this.viewOnlyDeleteCascaded = viewOnlyDeleteCascaded; + this.converter = (TypeConverter) converter; + this.elementIdAttributePath = elementIdAttributePath; this.parameterName = parameterName; this.passThrough = passThrough; this.entityAttributeAccessor = entityAttributeAccessor; @@ -70,7 +83,11 @@ private SubviewAttributeFlusher(SubviewAttributeFlusher original, boolean fetch, super(original.attributeName, original.mapping, fetch, nestedFlusher); this.optimisticLockProtected = original.optimisticLockProtected; this.updatable = original.updatable; - this.updateFragment = original.updateFragment; + this.cascadeDelete = original.cascadeDelete; + this.orphanRemoval = original.orphanRemoval; + this.viewOnlyDeleteCascaded = original.viewOnlyDeleteCascaded; + this.converter = original.converter; + this.elementIdAttributePath = original.elementIdAttributePath; this.parameterName = original.parameterName; this.passThrough = original.passThrough; this.entityAttributeAccessor = original.entityAttributeAccessor; @@ -102,10 +119,10 @@ public void appendUpdateQueryFragment(UpdateContext context, StringBuilder sb, S String mapping; String parameter; if (mappingPrefix == null) { - mapping = updateFragment; + mapping = elementIdAttributePath; parameter = parameterName; } else { - mapping = mappingPrefix + updateFragment; + mapping = mappingPrefix + elementIdAttributePath; parameter = parameterPrefix + parameterName; } sb.append(mapping); @@ -121,22 +138,37 @@ public boolean supportsQueryFlush() { @Override public void flushQuery(UpdateContext context, String parameterPrefix, Query query, Object view, V value) { + V finalValue; + if (flushOperation != null) { + finalValue = this.value; + } else { + finalValue = value; + } + finalValue = getConvertedValue(finalValue); + boolean doUpdate = updatable || isPassThrough(); + // Orphan removal is only valid for entity types + if (doUpdate && orphanRemoval) { + Object oldValue = viewAttributeAccessor.getValue(view); + if (!Objects.equals(oldValue, finalValue)) { + viewToEntityMapper.remove(context, oldValue); + } + } if (flushOperation != null) { if (flushOperation == ViewFlushOperation.CASCADE) { - Query q = viewToEntityMapper.createUpdateQuery(context, this.value, nestedGraphNode); - nestedGraphNode.flushQuery(context, parameterPrefix, q, null, this.value); + Query q = viewToEntityMapper.createUpdateQuery(context, finalValue, nestedGraphNode); + nestedGraphNode.flushQuery(context, parameterPrefix, q, null, finalValue); if (q != null) { int updated = q.executeUpdate(); if (updated != 1) { - throw new OptimisticLockException(null, this.value); + throw new OptimisticLockException(null, finalValue); } } } - Object v = viewToEntityMapper.applyToEntity(context, null, this.value); + Object v = viewToEntityMapper.applyToEntity(context, null, finalValue); if (query != null && update) { - Object realValue = v == null ? null : subviewIdAccessor.getValue(this.value); + Object realValue = v == null ? null : subviewIdAccessor.getValue(finalValue); String parameter; if (parameterPrefix == null) { parameter = parameterName; @@ -148,7 +180,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer // If the view is creatable, the CompositeAttributeFlusher re-maps the view object and puts the new object to the mutable state array Object newValue = viewAttributeAccessor.getMutableStateValue(view); - if (this.value != newValue) { + if (finalValue != newValue) { viewAttributeAccessor.setValue(view, newValue); } return; @@ -211,55 +243,123 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer @Override @SuppressWarnings("unchecked") - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { + V finalValue; + if (flushOperation != null) { + finalValue = this.value; + } else { + finalValue = value; + } + finalValue = getConvertedValue(finalValue); + boolean doUpdate = updatable || isPassThrough(); + // Orphan removal is only valid for entity types + if (doUpdate && orphanRemoval) { + Object oldValue = viewAttributeAccessor.getValue(view); + if (!Objects.equals(oldValue, finalValue)) { + viewToEntityMapper.remove(context, oldValue); + } + } if (flushOperation != null) { if (flushOperation == ViewFlushOperation.CASCADE) { - nestedGraphNode.flushEntity(context, null, null, this.value); + nestedGraphNode.flushEntity(context, null, null, finalValue, null); } - Object v = viewToEntityMapper.applyToEntity(context, null, value); + Object v = viewToEntityMapper.applyToEntity(context, null, finalValue); if (update) { entityAttributeAccessor.setValue(entity, v); } // If the view is creatable, the CompositeAttributeFlusher re-maps the view object and puts the new object to the mutable state array Object newValue = viewAttributeAccessor.getMutableStateValue(view); - if (this.value != newValue) { + if (finalValue != newValue) { viewAttributeAccessor.setValue(view, newValue); } return true; } if (updatable || isPassThrough()) { if (nestedGraphNode != null && nestedGraphNode != viewToEntityMapper.getFullGraphNode()) { - nestedGraphNode.flushEntity(context, null, null, value); + nestedGraphNode.flushEntity(context, null, null, finalValue, null); } - Object v = viewToEntityMapper.applyToEntity(context, null, value); + Object v = viewToEntityMapper.applyToEntity(context, null, finalValue); if (update) { entityAttributeAccessor.setValue(entity, v); } // If the view is creatable, the CompositeAttributeFlusher re-maps the view object and puts the new object to the mutable state array Object newValue = viewAttributeAccessor.getMutableStateValue(view); - if (value != newValue) { + if (finalValue != newValue) { viewAttributeAccessor.setValue(view, newValue); } } else { V realValue = (V) viewAttributeAccessor.getValue(view); if (nestedGraphNode != null && nestedGraphNode != viewToEntityMapper.getFullGraphNode()) { - nestedGraphNode.flushEntity(context, null, null, realValue); + nestedGraphNode.flushEntity(context, null, null, realValue, null); } else { - if (realValue != null && (value == realValue || viewIdEqual(value, realValue)) && jpaAndViewIdEqual(context, entityAttributeAccessor.getValue(entity), realValue)) { + if (realValue != null && (finalValue == realValue || viewIdEqual(finalValue, realValue)) && jpaAndViewIdEqual(entityAttributeAccessor.getValue(entity), realValue)) { viewToEntityMapper.applyToEntity(context, null, realValue); } } - if (view != null && value != realValue && viewIdEqual(value, realValue)) { + if (view != null && finalValue != realValue && viewIdEqual(finalValue, realValue)) { viewAttributeAccessor.setValue(view, realValue); } } return true; } + @SuppressWarnings("unchecked") + protected final V getConvertedValue(V value) { + if (converter != null) { + return (V) converter.convertToEntityType(value); + } + return value; + } + @Override - public void remove(UpdateContext context, E entity, Object view, V value) { - // TODO: implement proper deletion when delete cascading is activated + public List remove(UpdateContext context, E entity, Object view, V value) { + if (cascadeDelete) { + V valueToDelete; + if (view instanceof DirtyStateTrackable) { + valueToDelete = (V) viewAttributeAccessor.getInitialValue(view); + } else { + valueToDelete = value; + } + if (valueToDelete != null) { + V convertedValue = getConvertedValue(valueToDelete); + context.getInitialStateResetter().addRemovedView((EntityViewProxy) convertedValue); + viewToEntityMapper.remove(context, convertedValue); + } + } + return Collections.emptyList(); + } + + @Override + public void remove(UpdateContext context, Object id) { + viewToEntityMapper.removeById(context, id); + } + + @Override + public List removeByOwnerId(UpdateContext context, Object id) { +// inverseFlusher.removeByOwnerId(context, id); + throw new UnsupportedOperationException("Not yet implemented"); + } + + @Override + public void removeFromEntity(UpdateContext context, E entity) { + if (cascadeDelete) { + V valueToDelete = (V) entityAttributeAccessor.getValue(entity); + if (valueToDelete != null) { + viewToEntityMapper.removeById(context, viewToEntityMapper.getEntityIdAccessor().getValue(valueToDelete)); + } + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + // First the owner of the attribute must be deleted, otherwise we might get an FK violation + return true; + } + + @Override + public boolean isViewOnlyDeleteCascaded() { + return viewOnlyDeleteCascaded; } @Override @@ -267,6 +367,11 @@ public boolean isPassThrough() { return passThrough; } + @Override + public String getElementIdAttributeName() { + return elementIdAttributePath; + } + @Override public AttributeAccessor getViewAttributeAccessor() { return viewAttributeAccessor; @@ -363,7 +468,7 @@ public DirtyAttributeFlusher, E, V> getDirtyFlushe } } - private boolean jpaAndViewIdEqual(UpdateContext context, Object entity, V view) { + private boolean jpaAndViewIdEqual(Object entity, V view) { if (entity == null || view == null) { return false; } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/TypeDescriptor.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/TypeDescriptor.java index 82cad1ecdd..e4708a8b21 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/TypeDescriptor.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/TypeDescriptor.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.view.impl.update.flush; import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.spi.ExtendedManagedType; import com.blazebit.persistence.view.impl.EntityViewManagerImpl; import com.blazebit.persistence.view.impl.accessor.Accessors; import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; @@ -32,16 +33,17 @@ import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; import com.blazebit.persistence.view.impl.metamodel.AbstractMethodAttribute; import com.blazebit.persistence.view.impl.update.EntityViewUpdaterImpl; -import com.blazebit.persistence.view.spi.type.MutableBasicUserType; import com.blazebit.persistence.view.metamodel.BasicType; import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.Type; import com.blazebit.persistence.view.metamodel.ViewType; import com.blazebit.persistence.view.spi.type.BasicUserType; +import com.blazebit.persistence.view.spi.type.MutableBasicUserType; import com.blazebit.persistence.view.spi.type.TypeConverter; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.ManagedType; +import javax.persistence.metamodel.SingularAttribute; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -61,13 +63,14 @@ public class TypeDescriptor { private final boolean shouldJpaPersist; private final boolean cascadePersist; private final boolean cascadeUpdate; + private final String entityIdAttributeName; private final TypeConverter converter; private final BasicUserType basicUserType; private final EntityToEntityMapper entityToEntityMapper; private final ViewToEntityMapper viewToEntityMapper; private final ViewToEntityMapper loadOnlyViewToEntityMapper; - public TypeDescriptor(boolean mutable, boolean identifiable, boolean jpaManaged, boolean jpaEntity, boolean shouldJpaMerge, boolean shouldJpaPersist, boolean cascadePersist, boolean cascadeUpdate, + public TypeDescriptor(boolean mutable, boolean identifiable, boolean jpaManaged, boolean jpaEntity, boolean shouldJpaMerge, boolean shouldJpaPersist, boolean cascadePersist, boolean cascadeUpdate, String entityIdAttributeName, TypeConverter converter, BasicUserType basicUserType, EntityToEntityMapper entityToEntityMapper, ViewToEntityMapper viewToEntityMapper, ViewToEntityMapper loadOnlyViewToEntityMapper) { this.mutable = mutable; this.identifiable = identifiable; @@ -77,6 +80,7 @@ public TypeDescriptor(boolean mutable, boolean identifiable, boolean jpaManaged, this.shouldJpaPersist = shouldJpaPersist; this.cascadePersist = cascadePersist; this.cascadeUpdate = cascadeUpdate; + this.entityIdAttributeName = entityIdAttributeName; this.converter = converter; this.basicUserType = basicUserType; this.entityToEntityMapper = entityToEntityMapper; @@ -102,6 +106,7 @@ public static TypeDescriptor forType(EntityViewManagerImpl evm, AbstractMethodAt final boolean identifiable; TypeConverter converter = (TypeConverter) type.getConverter(); BasicUserType basicUserType; + String entityIdAttributeName = null; // TODO: currently we only check if the declared type is mutable, but we have to let the collection flusher which types are considered updatable/creatable if (type instanceof BasicType) { basicUserType = (BasicUserType) ((BasicType) type).getUserType(); @@ -109,20 +114,36 @@ public static TypeDescriptor forType(EntityViewManagerImpl evm, AbstractMethodAt identifiable = jpaEntity || !jpaManaged; if (jpaEntity) { Map> fetchGraph = null; + UnmappedAttributeCascadeDeleter deleter = null; // We only need to know the fetch graph when we actually do updates if (cascadeUpdate) { fetchGraph = getFetchGraph(attribute.getFetches(), attribute.getMapping(), managedType); } + entityIdAttributeName = evm.getMetamodel().getEntityMetamodel().getManagedType(ExtendedManagedType.class, type.getJavaType()).getIdAttribute().getName(); + + // Only construct when orphanRemoval or delete cascading is enabled, orphanRemoval implies delete cascading + if (attribute.isDeleteCascaded()) { + String mapping = attribute.getMapping(); + ExtendedManagedType elementManagedType = entityMetamodel.getManagedType(ExtendedManagedType.class, attribute.getDeclaringType().getEntityClass()); + deleter = new UnmappedBasicAttributeCascadeDeleter( + evm, + mapping, + elementManagedType.getAttribute(mapping), + mapping + "." + entityIdAttributeName, + false + ); + } + entityToEntityMapper = new DefaultEntityToEntityMapper( cascadePersist, cascadeUpdate, basicUserType, new DefaultEntityLoaderFetchGraphNode( evm, attribute.getName(), (EntityType) managedType, fetchGraph - ) - ); + ), + deleter); } } else { ManagedViewType elementType = (ManagedViewType) type; @@ -131,6 +152,10 @@ public static TypeDescriptor forType(EntityViewManagerImpl evm, AbstractMethodAt viewToEntityMapper = createViewToEntityMapper(attributeLocation, evm, elementType, cascadePersist, cascadeUpdate, persistAllowedSubtypes, updateAllowedSubtypes); loadOnlyViewToEntityMapper = createLoadOnlyViewToEntityMapper(attributeLocation, evm, elementType, cascadePersist, cascadeUpdate, persistAllowedSubtypes, updateAllowedSubtypes); identifiable = viewToEntityMapper.getViewIdAccessor() != null; + SingularAttribute idAttribute = evm.getMetamodel().getEntityMetamodel().getManagedType(ExtendedManagedType.class, elementType.getEntityClass()).getIdAttribute(); + if (idAttribute != null) { + entityIdAttributeName = idAttribute.getName(); + } } final boolean shouldJpaMerge = jpaEntity && mutable && cascadeUpdate; @@ -146,6 +171,7 @@ public static TypeDescriptor forType(EntityViewManagerImpl evm, AbstractMethodAt shouldJpaPersist, shouldFlushPersists, shouldFlushUpdates, + entityIdAttributeName, converter, basicUserType, entityToEntityMapper, @@ -167,6 +193,7 @@ public static TypeDescriptor forInverseAttribute(ViewToEntityMapper viewToEntity null, null, null, + null, viewToEntityMapper, viewToEntityMapper ); @@ -349,6 +376,10 @@ public boolean isCascadeUpdate() { return cascadeUpdate; } + public String getEntityIdAttributeName() { + return entityIdAttributeName; + } + public TypeConverter getConverter() { return converter; } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleter.java new file mode 100644 index 0000000000..bb43c49525 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleter.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.update.UpdateContext; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public interface UnmappedAttributeCascadeDeleter { + + public void removeById(UpdateContext context, Object id); + + public void removeByOwnerId(UpdateContext context, Object ownerId); + + public String getAttributeValuePath(); + + public boolean requiresDeleteCascadeAfterRemove(); + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleterUtil.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleterUtil.java new file mode 100644 index 0000000000..35282cf8ab --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedAttributeCascadeDeleterUtil.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class UnmappedAttributeCascadeDeleterUtil { + + private UnmappedAttributeCascadeDeleterUtil() { + } + + public static List createUnmappedCascadeDeleters(EntityViewManagerImpl evm, Class entityClass, String ownerIdAttributeName) { + EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); + ExtendedManagedType extendedManagedType = entityMetamodel.getManagedType(ExtendedManagedType.class, entityClass); + Map> attributes = extendedManagedType.getAttributes(); + List deleters = new ArrayList<>(attributes.size()); + + for (Map.Entry> entry : attributes.entrySet()) { + ExtendedAttribute extendedAttribute = entry.getValue(); + if (extendedAttribute.getAttribute().isCollection()) { + if (((javax.persistence.metamodel.PluralAttribute) extendedAttribute.getAttribute()).getCollectionType() == javax.persistence.metamodel.PluralAttribute.CollectionType.MAP) { + deleters.add(new UnmappedMapAttributeCascadeDeleter( + evm, + entry.getKey(), + extendedAttribute, + entityClass, + ownerIdAttributeName, + true + )); + } else { + deleters.add(new UnmappedCollectionAttributeCascadeDeleter( + evm, + entry.getKey(), + extendedAttribute, + entityClass, + ownerIdAttributeName, + true + )); + } + } else if (extendedAttribute.getAttribute().isAssociation() && extendedAttribute.isDeleteCascaded()) { + deleters.add(new UnmappedBasicAttributeCascadeDeleter( + evm, + entry.getKey(), + extendedAttribute, + ownerIdAttributeName, + true + )); + } + } + + return deleters; + } + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedBasicAttributeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedBasicAttributeCascadeDeleter.java new file mode 100644 index 0000000000..511b5133d0 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedBasicAttributeCascadeDeleter.java @@ -0,0 +1,201 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.DeleteCriteriaBuilder; +import com.blazebit.persistence.ReturningResult; +import com.blazebit.persistence.impl.EntityMetamodel; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.ExtendedManagedType; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import javax.persistence.Query; +import javax.persistence.Tuple; +import javax.persistence.metamodel.EntityType; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class UnmappedBasicAttributeCascadeDeleter extends AbstractUnmappedAttributeCascadeDeleter { + + private final String ownerIdAttributeName; + private final String deleteQuery; + private final String deleteByOwnerIdQuery; + private final boolean requiresDeleteCascadeAfterRemove; + private final boolean requiresDeleteAsEntity; + private final UnmappedAttributeCascadeDeleter[] unmappedPreRemoveCascadeDeleters; + private final UnmappedAttributeCascadeDeleter[] unmappedPostRemoveCascadeDeleters; + + public UnmappedBasicAttributeCascadeDeleter(EntityViewManagerImpl evm, String attributeName, ExtendedAttribute attribute, String ownerIdAttributeName, boolean disallowCycle) { + super(evm, attributeName, attribute); + EntityMetamodel entityMetamodel = evm.getMetamodel().getEntityMetamodel(); + ExtendedManagedType extendedManagedType = entityMetamodel.getManagedType(ExtendedManagedType.class, elementEntityClass); + EntityType entityType = (EntityType) extendedManagedType.getType(); + this.requiresDeleteCascadeAfterRemove = !attribute.isForeignJoinColumn(); + this.ownerIdAttributeName = ownerIdAttributeName; + this.deleteQuery = "DELETE FROM " + entityType.getName() + " e WHERE e." + elementIdAttributeName + " = :id"; + this.deleteByOwnerIdQuery = "DELETE FROM " + entityType.getName() + " e WHERE e." + ownerIdAttributeName + " = :ownerId"; + + if (elementIdAttributeName == null) { + this.requiresDeleteAsEntity = false; + this.unmappedPreRemoveCascadeDeleters = this.unmappedPostRemoveCascadeDeleters = EMPTY; + } else { + // If the attribute introduces a cycle, we can't construct pre- and post-deleters. We must do entity deletion, otherwise we'd get a stack overflow + if (disallowCycle && attribute.hasCascadingDeleteCycle()) { + this.requiresDeleteAsEntity = true; + this.unmappedPreRemoveCascadeDeleters = this.unmappedPostRemoveCascadeDeleters = EMPTY; + } else { + List unmappedCascadeDeleters = UnmappedAttributeCascadeDeleterUtil.createUnmappedCascadeDeleters(evm, elementEntityClass, elementIdAttributeName); + List unmappedPreRemoveCascadeDeleters = new ArrayList<>(unmappedCascadeDeleters.size()); + List unmappedPostRemoveCascadeDeleters = new ArrayList<>(unmappedCascadeDeleters.size()); + for (UnmappedAttributeCascadeDeleter deleter : unmappedCascadeDeleters) { + if (deleter.requiresDeleteCascadeAfterRemove()) { + unmappedPostRemoveCascadeDeleters.add(deleter); + } else { + unmappedPreRemoveCascadeDeleters.add(deleter); + } + } + + this.requiresDeleteAsEntity = false; + this.unmappedPreRemoveCascadeDeleters = unmappedPreRemoveCascadeDeleters.toArray(new UnmappedAttributeCascadeDeleter[unmappedPreRemoveCascadeDeleters.size()]); + this.unmappedPostRemoveCascadeDeleters = unmappedPostRemoveCascadeDeleters.toArray(new UnmappedAttributeCascadeDeleter[unmappedPostRemoveCascadeDeleters.size()]); + } + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return requiresDeleteCascadeAfterRemove; + } + + @Override + public void removeById(UpdateContext context, Object id) { + for (int i = 0; i < unmappedPreRemoveCascadeDeleters.length; i++) { + unmappedPreRemoveCascadeDeleters[i].removeByOwnerId(context, id); + } + removeWithoutPreCascadeDelete(context, null, null, id); + } + + @Override + public void removeByOwnerId(UpdateContext context, Object ownerId) { + Object[] returnedValues = null; + Object id = null; + if (requiresDeleteAsEntity) { + CriteriaBuilder cb = context.getEntityViewManager().getCriteriaBuilderFactory().create(context.getEntityManager(), elementEntityClass); + cb.where(ownerIdAttributeName).eq(ownerId); + context.getEntityManager().remove(cb.getSingleResult()); + // We need to flush here, otherwise the deletion will be deferred and might cause a constraint violation + context.getEntityManager().flush(); + } else { + if (unmappedPreRemoveCascadeDeleters.length != 0) { + // If we have pre remove cascade deleters, we need to query the id first so we can remove these elements + List returningAttributes = new ArrayList<>(); + for (int i = 0; i < unmappedPostRemoveCascadeDeleters.length; i++) { + returningAttributes.add(unmappedPostRemoveCascadeDeleters[i].getAttributeValuePath()); + } + + CriteriaBuilder cb = context.getEntityViewManager().getCriteriaBuilderFactory().create(context.getEntityManager(), Object[].class); + cb.from(elementEntityClass); + cb.where(ownerIdAttributeName).eq(ownerId); + for (String attribute : returningAttributes) { + cb.select(attribute); + } + cb.select(elementIdAttributeName); + returnedValues = cb.getSingleResult(); + id = returnedValues[returnedValues.length - 1]; + + for (int i = 0; i < unmappedPreRemoveCascadeDeleters.length; i++) { + unmappedPreRemoveCascadeDeleters[i].removeByOwnerId(context, id); + } + } + removeWithoutPreCascadeDelete(context, ownerId, returnedValues, id); + } + } + + private void removeWithoutPreCascadeDelete(UpdateContext context, Object ownerId, Object[] returnedValues, Object id) { + boolean doDelete = true; + // need to "return" the values from the delete query for the post deleters since the values aren't available after executing the delete query + if (unmappedPostRemoveCascadeDeleters.length != 0 && returnedValues == null) { + List returningAttributes = new ArrayList<>(); + for (int i = 0; i < unmappedPostRemoveCascadeDeleters.length; i++) { + returningAttributes.add(unmappedPostRemoveCascadeDeleters[i].getAttributeValuePath()); + } + + EntityViewManagerImpl evm = context.getEntityViewManager(); + // If the dbms supports it, we use the returning feature to do this + if (evm.getDbmsDialect().supportsReturningColumns()) { + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().delete(context.getEntityManager(), elementEntityClass); + if (id == null) { + cb.where(ownerIdAttributeName).eq(ownerId); + } else { + cb.where(elementIdAttributeName).eq(id); + } + + ReturningResult result = cb.executeWithReturning(returningAttributes.toArray(new String[returningAttributes.size()])); + returnedValues = result.getLastResult().toArray(); + doDelete = false; + } else { + // Otherwise we query the attributes + CriteriaBuilder cb = evm.getCriteriaBuilderFactory().create(context.getEntityManager(), Object[].class); + cb.from(elementEntityClass); + if (id == null) { + cb.where(ownerIdAttributeName).eq(ownerId); + } else { + cb.where(elementIdAttributeName).eq(id); + } + for (String attribute : returningAttributes) { + cb.select(attribute); + } + cb.select(elementIdAttributeName); + returnedValues = cb.getSingleResult(); + id = returnedValues[returnedValues.length - 1]; + } + } + + if (doDelete) { + if (requiresDeleteAsEntity) { + if (id == null) { + throw new UnsupportedOperationException("Delete by owner id should not be invoked!"); + } + context.getEntityManager().remove(context.getEntityManager().getReference(elementEntityClass, id)); + } else { + if (id == null) { + Query query = context.getEntityManager().createQuery(deleteByOwnerIdQuery); + query.setParameter("ownerId", ownerId); + query.executeUpdate(); + } else { + Query query = context.getEntityManager().createQuery(deleteQuery); + query.setParameter("id", id); + query.executeUpdate(); + } + } + } + + for (int i = 0; i < unmappedPostRemoveCascadeDeleters.length; i++) { + if (returnedValues[i] != null) { + unmappedPostRemoveCascadeDeleters[i].removeById(context, returnedValues[i]); + } + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedCollectionAttributeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedCollectionAttributeCascadeDeleter.java new file mode 100644 index 0000000000..f6c38bfdcb --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedCollectionAttributeCascadeDeleter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.DeleteCriteriaBuilder; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.spi.JpaProvider; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import java.util.Arrays; +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class UnmappedCollectionAttributeCascadeDeleter extends AbstractUnmappedAttributeCascadeDeleter { + + private final Class ownerEntityClass; + private final String ownerIdAttributeName; + private final String mappedByAttributeName; + private final boolean jpaProviderDeletesCollection; + private final UnmappedBasicAttributeCascadeDeleter elementDeleter; + + public UnmappedCollectionAttributeCascadeDeleter(EntityViewManagerImpl evm, String attributeName, ExtendedAttribute attribute, Class ownerEntityClass, String ownerIdAttributeName, boolean disallowCycle) { + super(evm, attributeName, attribute); + this.ownerEntityClass = ownerEntityClass; + this.ownerIdAttributeName = ownerIdAttributeName; + this.mappedByAttributeName = attribute.getMappedBy(); + JpaProvider jpaProvider = evm.getJpaProvider(); + if (elementIdAttributeName != null) { + this.jpaProviderDeletesCollection = jpaProvider.supportsJoinTableCleanupOnDelete(); + if (cascadeDeleteElement) { + String elementOwnerIdAttributeName = null; + if (mappedByAttributeName != null) { + elementOwnerIdAttributeName = mappedByAttributeName + "." + ownerIdAttributeName; + } + this.elementDeleter = new UnmappedBasicAttributeCascadeDeleter( + evm, + "", + attribute, + elementOwnerIdAttributeName, + disallowCycle + ); + } else { + this.elementDeleter = null; + } + } else { + this.jpaProviderDeletesCollection = jpaProvider.supportsCollectionTableCleanupOnDelete(); + this.elementDeleter = null; + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public void removeById(UpdateContext context, Object id) { + throw new UnsupportedOperationException("Can't delete collection attribute by id!"); + } + + @Override + public void removeByOwnerId(UpdateContext context, Object ownerId) { + EntityViewManagerImpl evm = context.getEntityViewManager(); + if (cascadeDeleteElement) { + List elementIds; + if (mappedByAttributeName == null) { + // If there is no mapped by attribute, the collection has a join table + if (evm.getDbmsDialect().supportsReturningColumns()) { + elementIds = Arrays.asList(evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName) + .where(ownerIdAttributeName).eq(ownerId) + .executeWithReturning("e." + attributeName + "." + elementIdAttributeName) + .getResultList() + .toArray()); + } else { + elementIds = (List) evm.getCriteriaBuilderFactory().create(context.getEntityManager(), ownerEntityClass, "e") + .where(ownerIdAttributeName).eq(ownerId) + .select("e." + attributeName + "." + elementIdAttributeName) + .getResultList(); + if (!elementIds.isEmpty()) { + // We must always delete this, otherwise we might get a constraint violation because of the cascading delete + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } + for (Object elementId : elementIds) { + elementDeleter.removeById(context, elementId); + } + } else { + // Since there is a mapped by attribute, there is no join table to clear. Just delete the element by the owner id + elementDeleter.removeByOwnerId(context, ownerId); + } + } else if (!jpaProviderDeletesCollection) { + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedMapAttributeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedMapAttributeCascadeDeleter.java new file mode 100644 index 0000000000..5200a7f334 --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UnmappedMapAttributeCascadeDeleter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.DeleteCriteriaBuilder; +import com.blazebit.persistence.spi.ExtendedAttribute; +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; +import com.blazebit.persistence.view.impl.update.UpdateContext; + +import java.util.Arrays; +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class UnmappedMapAttributeCascadeDeleter extends AbstractUnmappedAttributeCascadeDeleter { + + private final Class ownerEntityClass; + private final String ownerIdAttributeName; + private final boolean jpaProviderDeletesCollection; + private final UnmappedBasicAttributeCascadeDeleter elementDeleter; + + public UnmappedMapAttributeCascadeDeleter(EntityViewManagerImpl evm, String attributeName, ExtendedAttribute attribute, Class ownerEntityClass, String ownerIdAttributeName, boolean disallowCycle) { + super(evm, attributeName, attribute); + this.ownerEntityClass = ownerEntityClass; + this.ownerIdAttributeName = ownerIdAttributeName; + if (elementIdAttributeName != null) { + this.jpaProviderDeletesCollection = evm.getJpaProvider().supportsJoinTableCleanupOnDelete(); + if (cascadeDeleteElement) { + this.elementDeleter = new UnmappedBasicAttributeCascadeDeleter( + evm, + "", + attribute, + null, + disallowCycle + ); + } else { + this.elementDeleter = null; + } + } else { + this.jpaProviderDeletesCollection = evm.getJpaProvider().supportsCollectionTableCleanupOnDelete(); + this.elementDeleter = null; + } + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + return false; + } + + @Override + public void removeById(UpdateContext context, Object id) { + throw new UnsupportedOperationException("Can't delete collection attribute by id!"); + } + + @Override + public void removeByOwnerId(UpdateContext context, Object ownerId) { + EntityViewManagerImpl evm = context.getEntityViewManager(); + if (cascadeDeleteElement) { + List elementIds; + if (evm.getDbmsDialect().supportsReturningColumns()) { + elementIds = Arrays.asList(evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName) + .where(ownerIdAttributeName).eq(ownerId) + .executeWithReturning("e." + attributeName + "." + elementIdAttributeName) + .getResultList() + .toArray()); + } else { + elementIds = (List) evm.getCriteriaBuilderFactory().create(context.getEntityManager(), ownerEntityClass, "e") + .where(ownerIdAttributeName).eq(ownerId) + .select("e." + attributeName + "." + elementIdAttributeName) + .getResultList(); + if (!elementIds.isEmpty()) { + // We must always delete this, otherwise we might get a constraint violation because of the cascading delete + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } + for (Object elementId : elementIds) { + elementDeleter.removeById(context, elementId); + } + } else if (!jpaProviderDeletesCollection) { + DeleteCriteriaBuilder cb = evm.getCriteriaBuilderFactory().deleteCollection(context.getEntityManager(), ownerEntityClass, "e", attributeName); + cb.where(ownerIdAttributeName).eq(ownerId); + cb.executeUpdate(); + } + } +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UpdateCollectionElementAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UpdateCollectionElementAttributeFlusher.java index a820221740..42ff56727b 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UpdateCollectionElementAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/UpdateCollectionElementAttributeFlusher.java @@ -43,7 +43,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer q = viewToEntityMapper.createUpdateQuery(context, element, nestedGraphNode); if (!nestedGraphNode.supportsQueryFlush()) { - nestedGraphNode.flushEntity(context, null, (V) element, (V) element); + nestedGraphNode.flushEntity(context, null, (V) element, (V) element, null); } else { nestedGraphNode.flushQuery(context, parameterPrefix, q, null, (V) element); } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/VersionAttributeFlusher.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/VersionAttributeFlusher.java index 326444ae23..54460e28c6 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/VersionAttributeFlusher.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/VersionAttributeFlusher.java @@ -23,6 +23,8 @@ import com.blazebit.persistence.view.spi.type.VersionBasicUserType; import javax.persistence.Query; +import java.util.Collections; +import java.util.List; /** * @@ -34,7 +36,7 @@ public class VersionAttributeFlusher extends BasicAttributeFlusher { private final boolean jpaVersion; public VersionAttributeFlusher(String attributeName, String mapping, VersionBasicUserType userType, String updateFragment, String parameterName, AttributeAccessor entityAttributeAccessor, AttributeAccessor viewAttributeAccessor, boolean jpaVersion) { - super(attributeName, mapping, true, false, true, new TypeDescriptor( + super(attributeName, mapping, true, false, true, false, false, false, new TypeDescriptor( false, false, false, @@ -44,11 +46,12 @@ public VersionAttributeFlusher(String attributeName, String mapping, VersionBasi false, false, null, + null, userType, null, null, null - ), updateFragment, parameterName, entityAttributeAccessor, viewAttributeAccessor); + ), updateFragment, parameterName, entityAttributeAccessor, viewAttributeAccessor, null); this.jpaVersion = jpaVersion; } @@ -72,7 +75,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer } @Override - public boolean flushEntity(UpdateContext context, E entity, Object view, V value) { + public boolean flushEntity(UpdateContext context, E entity, Object view, V value, Runnable postReplaceListener) { Object entityValue = entityAttributeAccessor.getValue(entity); if (value != entityValue && !elementDescriptor.getBasicUserType().isDeepEqual(value, entityValue)) { throw new OptimisticLockException(entity, view); @@ -87,4 +90,15 @@ public boolean flushEntity(UpdateContext context, E entity, Object view, V value ((MutableStateTrackable) view).$$_setVersion(nextValue); return true; } + + @Override + public List remove(UpdateContext context, E entity, Object view, V value) { + if (entity != null) { + Object entityValue = entityAttributeAccessor.getValue(entity); + if (value != entityValue && !elementDescriptor.getBasicUserType().isDeepEqual(value, entityValue)) { + throw new OptimisticLockException(entity, view); + } + } + return Collections.emptyList(); + } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewCollectionRemoveListener.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewCollectionRemoveListener.java new file mode 100644 index 0000000000..6be000fc2a --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewCollectionRemoveListener.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.collection.CollectionRemoveListener; +import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; +import com.blazebit.persistence.view.impl.update.UpdateContext; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class ViewCollectionRemoveListener implements CollectionRemoveListener { + + private final ViewToEntityMapper viewToEntityMapper; + + public ViewCollectionRemoveListener(ViewToEntityMapper viewToEntityMapper) { + this.viewToEntityMapper = viewToEntityMapper; + } + + @Override + public void onEntityCollectionRemove(UpdateContext context, Object element) { + viewToEntityMapper.removeById(context, viewToEntityMapper.getEntityIdAccessor().getValue(element)); + } + + @Override + public void onCollectionRemove(UpdateContext context, Object element) { + context.getInitialStateResetter().addRemovedView((EntityViewProxy) element); + viewToEntityMapper.remove(context, element); + } + +} diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewTypeCascadeDeleter.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewTypeCascadeDeleter.java new file mode 100644 index 0000000000..fce62bad6d --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/flush/ViewTypeCascadeDeleter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.impl.update.flush; + +import com.blazebit.persistence.view.impl.entity.ViewToEntityMapper; +import com.blazebit.persistence.view.impl.update.UpdateContext; + + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class ViewTypeCascadeDeleter implements UnmappedAttributeCascadeDeleter { + + private final ViewToEntityMapper viewToEntityMapper; + + public ViewTypeCascadeDeleter(ViewToEntityMapper viewToEntityMapper) { + this.viewToEntityMapper = viewToEntityMapper; + } + + @Override + public void removeById(UpdateContext context, Object id) { + viewToEntityMapper.removeById(context, id); + } + + @Override + public void removeByOwnerId(UpdateContext context, Object ownerId) { + throw new UnsupportedOperationException(); + } + + @Override + public String getAttributeValuePath() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean requiresDeleteCascadeAfterRemove() { + throw new UnsupportedOperationException(); + } +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/DocumentExtensionForElementCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/DocumentExtensionForElementCollections.java index b2fabd6947..e65d302005 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/DocumentExtensionForElementCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/DocumentExtensionForElementCollections.java @@ -21,6 +21,7 @@ import java.util.Set; import javax.persistence.Embeddable; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; @@ -38,7 +39,7 @@ public class DocumentExtensionForElementCollections implements Serializable { private ExtendedDocumentForElementCollections parent; private Set childDocuments = new HashSet(); - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ext_doc_elem_coll_parent") public ExtendedDocumentForElementCollections getParent() { return parent; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedDocumentForCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedDocumentForCollections.java index 946a96af97..45d2df79eb 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedDocumentForCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedDocumentForCollections.java @@ -25,6 +25,7 @@ import java.util.Set; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinTable; @@ -75,7 +76,7 @@ public void setName(String name) { this.name = name; } - @ManyToOne(optional = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) public ExtendedPersonForCollections getOwner() { return owner; } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForCollections.java index 87e762f7ca..d07c5420b9 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForCollections.java @@ -21,6 +21,7 @@ import java.util.Set; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -58,7 +59,7 @@ public void setId(Long id) { this.id = id; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) public ExtendedDocumentForCollections getPartnerDocument() { return partnerDocument; } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForElementCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForElementCollections.java index 21d7cea931..a5251caba9 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForElementCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/extended/ExtendedPersonForElementCollections.java @@ -20,6 +20,7 @@ import javax.persistence.Column; import javax.persistence.Embeddable; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @@ -52,7 +53,7 @@ public void setFullname(String fullname) { this.fullname = fullname; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ext_pers_elem_coll_partner_doc") public ExtendedDocumentForElementCollections getPartnerDocument() { return partnerDocument; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/DocumentForCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/DocumentForCollections.java index 0b311f1a79..fb1584d691 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/DocumentForCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/DocumentForCollections.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.view.testsuite.collections.entity.simple; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; @@ -77,7 +78,7 @@ public void setName(String name) { this.name = name; } - @ManyToOne(optional = false) + @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "owner_id") public PersonForCollections getOwner() { return owner; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForCollections.java index 4d56cbc9e1..c02fa0cc7c 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForCollections.java @@ -21,6 +21,7 @@ import java.util.Set; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; @@ -61,7 +62,7 @@ public void setId(Long id) { this.id = id; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "partner_doc_id") public DocumentForCollections getPartnerDocument() { return partnerDocument; @@ -92,7 +93,7 @@ public void setOwnedDocuments(Set ownedDocuments) { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((getId() == null) ? 0 : getId().hashCode()); return result; } @@ -102,13 +103,14 @@ public boolean equals(Object obj) { return true; if (obj == null) return false; - if (getClass() != obj.getClass()) + if (!(obj instanceof PersonForCollections)) { return false; + } PersonForCollections other = (PersonForCollections) obj; - if (id == null) { - if (other.id != null) + if (getId() == null) { + if (other.getId() != null) return false; - } else if (!id.equals(other.id)) + } else if (!getId().equals(other.getId())) return false; return true; } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForElementCollections.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForElementCollections.java index c3ca2071c2..e8fae6fc2c 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForElementCollections.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/collections/entity/simple/PersonForElementCollections.java @@ -20,6 +20,7 @@ import javax.persistence.Column; import javax.persistence.Embeddable; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; @@ -52,7 +53,7 @@ public void setFullname(String fullname) { this.fullname = fullname; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "pers_elem_coll_partner_doc") public DocumentForElementCollections getPartnerDocument() { return partnerDocument; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/InheritanceMappingInAnotherEntityViewTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/InheritanceMappingInAnotherEntityViewTest.java index 6a43455cc2..e89cda5ac4 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/InheritanceMappingInAnotherEntityViewTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/InheritanceMappingInAnotherEntityViewTest.java @@ -97,7 +97,7 @@ public static class C { @Column private String name; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private A a; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/NestedInheritanceMappingTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/NestedInheritanceMappingTest.java index aa70d09a29..bbe6a82c8e 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/NestedInheritanceMappingTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/inheritance/polymorphic/NestedInheritanceMappingTest.java @@ -122,7 +122,7 @@ public static class D extends C { @Column private Integer someOtherValue; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn private A a; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateDocumentTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateDocumentTest.java index 2bf14c53a7..d852cd6c82 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateDocumentTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateDocumentTest.java @@ -145,40 +145,27 @@ protected void restartTransactionAndReload() { } protected T getDoc1View() { - return getDocumentView(doc1.getId()); + return evm.find(em, viewType, doc1.getId()); } protected T getDoc2View() { - return getDocumentView(doc2.getId()); + return evm.find(em, viewType, doc2.getId()); } protected

P getP1View(Class

personView) { - return getPersonView(p1.getId(), personView); + return evm.find(em, personView, p1.getId()); } protected

P getP2View(Class

personView) { - return getPersonView(p2.getId(), personView); - } - - protected T getDocumentView(Long id) { - CriteriaBuilder criteria = cbf.create(em, Document.class, "d").where("id").eq(id).orderByAsc("id"); - CriteriaBuilder cb = evm.applySetting(EntityViewSetting.create(viewType), criteria); - List results = cb.getResultList(); - return results.get(0); + return evm.find(em, personView, p2.getId()); } protected D getDocumentView(Long id, Class documentView) { - CriteriaBuilder criteria = cbf.create(em, Document.class, "d").where("id").eq(id).orderByAsc("id"); - CriteriaBuilder cb = evm.applySetting(EntityViewSetting.create(documentView), criteria); - List results = cb.getResultList(); - return results.get(0); + return evm.find(em, documentView, id); } protected

P getPersonView(Long id, Class

personView) { - CriteriaBuilder criteria = cbf.create(em, Person.class, "p").where("id").eq(id).orderByAsc("id"); - CriteriaBuilder

cb = evm.applySetting(EntityViewSetting.create(personView), criteria); - List

results = cb.getResultList(); - return results.get(0); + return evm.find(em, personView, id); } } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateTest.java index 560a9c64db..addec5f23f 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/AbstractEntityViewUpdateTest.java @@ -192,7 +192,7 @@ protected void assertNoUpdateAndReload(T docView) { restartTransactionAndReload(); clearQueries(); update(docView); - AssertStatementBuilder afterBuilder = assertQuerySequence(); + AssertStatementBuilder afterBuilder = assertUnorderedQuerySequence(); if (isFullMode()) { if (isQueryStrategy()) { fullUpdate(afterBuilder); @@ -287,6 +287,28 @@ public void work(EntityManager em) { }); } + protected void remove(final Class docView, final Object id) { + transactional(new TxVoidWork() { + + @Override + public void work(EntityManager em) { + evm.remove(em, docView, id); + em.flush(); + } + }); + } + + protected void remove(final T docView) { + transactional(new TxVoidWork() { + + @Override + public void work(EntityManager em) { + evm.remove(em, docView); + em.flush(); + } + }); + } + protected void updateWithRollback(final T docView) { transactional(new TxVoidWork() { diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicView.java index 68a8bcefd3..620c6fa724 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicView.java @@ -16,7 +16,6 @@ package com.blazebit.persistence.view.testsuite.update.basic.mutable.model; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.update.basic.model.UpdatableDocumentBasicViewBase; @@ -29,7 +28,7 @@ */ public interface UpdatableDocumentBasicView extends UpdatableDocumentBasicViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public Date getLastModified(); public void setLastModified(Date date); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithCollectionsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithCollectionsView.java index 15b3fa0ca9..91ab74eda6 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithCollectionsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithCollectionsView.java @@ -16,7 +16,6 @@ package com.blazebit.persistence.view.testsuite.update.basic.mutable.model; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.update.basic.model.UpdatableDocumentBasicWithCollectionsViewBase; @@ -29,7 +28,7 @@ */ public interface UpdatableDocumentBasicWithCollectionsView extends UpdatableDocumentBasicWithCollectionsViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public List getStrings(); public void setStrings(List strings); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithMapsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithMapsView.java index eb18570864..8ec14e333b 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithMapsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/basic/mutable/model/UpdatableDocumentBasicWithMapsView.java @@ -16,7 +16,6 @@ package com.blazebit.persistence.view.testsuite.update.basic.mutable.model; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.update.basic.model.UpdatableDocumentBasicWithMapsViewBase; @@ -29,7 +28,7 @@ */ public interface UpdatableDocumentBasicWithMapsView extends UpdatableDocumentBasicWithMapsViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public Map getStringMap(); public void setStringMap(Map stringMap); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableView.java index 366ac44b0c..5caf59d869 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.embeddable.mutable.model; import com.blazebit.persistence.testsuite.entity.NameObject; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.update.embeddable.model.UpdatableDocumentEmbeddableViewBase; @@ -28,7 +27,7 @@ */ public interface UpdatableDocumentEmbeddableView extends UpdatableDocumentEmbeddableViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public NameObject getNameObject(); public void setNameObject(NameObject nameObject); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithCollectionsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithCollectionsView.java index a3882b11cd..9603c4b779 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithCollectionsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithCollectionsView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.embeddable.mutable.model; import com.blazebit.persistence.testsuite.entity.NameObject; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.update.embeddable.model.UpdatableDocumentEmbeddableWithCollectionsViewBase; @@ -30,7 +29,7 @@ */ public interface UpdatableDocumentEmbeddableWithCollectionsView extends UpdatableDocumentEmbeddableWithCollectionsViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public List getNames(); public void setNames(List names); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithMapsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithMapsView.java index 1d779a596d..dc4969cbd5 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithMapsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/embeddable/mutable/model/UpdatableDocumentEmbeddableWithMapsView.java @@ -30,7 +30,7 @@ */ public interface UpdatableDocumentEmbeddableWithMapsView extends UpdatableDocumentEmbeddableWithMapsViewBase { - @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) + @UpdatableMapping public Map getNameMap(); public void setNameMap(Map nameMap); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentView.java index bde044882f..9be64fc2f9 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.flatview.nested.mutable.model; import com.blazebit.persistence.testsuite.entity.Document; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.IdMapping; import com.blazebit.persistence.view.UpdatableEntityView; @@ -37,7 +36,7 @@ public interface UpdatableDocumentView { public Long getVersion(); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public UpdatableNameObjectContainerView getNameContainer(); public void setNameContainer(UpdatableNameObjectContainerView nameContainer); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithCollectionsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithCollectionsView.java index 323ce4ee80..5d0b50d889 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithCollectionsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithCollectionsView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.flatview.nested.mutable.model; import com.blazebit.persistence.testsuite.entity.Document; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.IdMapping; import com.blazebit.persistence.view.UpdatableEntityView; @@ -43,7 +42,7 @@ public interface UpdatableDocumentWithCollectionsView { public void setName(String name); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public List getNameContainers(); public void setNameContainers(List nameContainers); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithMapsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithMapsView.java index 9652eb3ea0..8cd6874f9f 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithMapsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableDocumentWithMapsView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.flatview.nested.mutable.model; import com.blazebit.persistence.testsuite.entity.Document; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.IdMapping; import com.blazebit.persistence.view.UpdatableEntityView; @@ -43,7 +42,7 @@ public interface UpdatableDocumentWithMapsView { public void setName(String name); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public Map getNameContainerMap(); public void setNameContainerMap(Map nameContainerMap); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableNameObjectContainerView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableNameObjectContainerView.java index 952fb1b350..6a7438647b 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableNameObjectContainerView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/nested/mutable/model/UpdatableNameObjectContainerView.java @@ -17,7 +17,6 @@ package com.blazebit.persistence.view.testsuite.update.flatview.nested.mutable.model; import com.blazebit.persistence.testsuite.entity.NameObjectContainer; -import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.UpdatableEntityView; import com.blazebit.persistence.view.UpdatableMapping; @@ -33,7 +32,7 @@ public interface UpdatableNameObjectContainerView { public String getName(); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public UpdatableNameObjectView getNameObject(); public void setNameObject(UpdatableNameObjectView nameObject); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentView.java index 83e1198bcc..312a10e96e 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentView.java @@ -37,7 +37,7 @@ public interface UpdatableDocumentView { public Long getVersion(); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public UpdatableNameObjectView getNameObject(); public void setNameObject(UpdatableNameObjectView nameObject); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithCollectionsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithCollectionsView.java index cf39413804..485c1f25e0 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithCollectionsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithCollectionsView.java @@ -43,7 +43,7 @@ public interface UpdatableDocumentWithCollectionsView { public void setName(String name); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public List getNames(); public void setNames(List names); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithMapsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithMapsView.java index d78d1eeba5..df366d7105 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithMapsView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/flatview/simple/mutable/model/UpdatableDocumentWithMapsView.java @@ -43,7 +43,7 @@ public interface UpdatableDocumentWithMapsView { public void setName(String name); - @UpdatableMapping(updatable = true, cascade = { CascadeType.PERSIST, CascadeType.UPDATE }) + @UpdatableMapping public Map getNameMap(); public void setNameMap(Map nameMap); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/metamodel/EntityViewMetamodelTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/metamodel/EntityViewMetamodelTest.java index 75c3c14129..b1d846e613 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/metamodel/EntityViewMetamodelTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/metamodel/EntityViewMetamodelTest.java @@ -165,8 +165,9 @@ public void nonUpdatableEntityAttributeDefaults() { assertFalse(docViewType.getAttribute("partners").isUpdatable()); assertFalse(docViewType.getAttribute("owner").isUpdatable()); - assertTrue(docViewType.getAttribute("partners").isPersistCascaded()); - assertTrue(docViewType.getAttribute("owner").isPersistCascaded()); + // Cascades require the update to be updatable, so they are all disabled + assertFalse(docViewType.getAttribute("partners").isPersistCascaded()); + assertFalse(docViewType.getAttribute("owner").isPersistCascaded()); assertTrue(docViewType.getAttribute("partners").isUpdateCascaded()); assertTrue(docViewType.getAttribute("owner").isUpdateCascaded()); @@ -243,9 +244,9 @@ public static interface DocumentViewWithMutableViewTypes extends DocumentBaseVie public void nonUpdatableMutableViewAttributeDefaults() { ViewMetamodel metamodel = build(DocumentViewWithMutableViewTypes.class, PersonUpdateView.class); ManagedViewType docViewType = metamodel.managedView(DocumentViewWithMutableViewTypes.class); - // By default, the collection relations are not updatable + // Collections are only considered being updatable if the element type is "persistable" assertFalse(docViewType.getAttribute("partners").isUpdatable()); - assertTrue(docViewType.getAttribute("owner").isUpdatable()); + assertFalse(docViewType.getAttribute("owner").isUpdatable()); assertFalse(docViewType.getAttribute("people").isUpdatable()); // Non-updatable attributes with mutable types cascade only UPDATE by default diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/AbstractEntityViewRemoveDocumentTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/AbstractEntityViewRemoveDocumentTest.java new file mode 100644 index 0000000000..85906e8047 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/AbstractEntityViewRemoveDocumentTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove; + +import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.LongSequenceEntity; +import com.blazebit.persistence.testsuite.entity.NameObject; +import com.blazebit.persistence.testsuite.entity.NameObjectContainer; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateTest; + +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.1.0 + */ +public abstract class AbstractEntityViewRemoveDocumentTest extends AbstractEntityViewUpdateTest { + + protected Document doc1; + protected Document doc2; + protected Person p1; + protected Person p2; + protected Person p3; + protected Person p4; + protected Person p5; + protected Person p6; + + public AbstractEntityViewRemoveDocumentTest(FlushMode mode, FlushStrategy strategy, boolean version, Class viewType) { + super(mode, strategy, version, viewType); + } + + public AbstractEntityViewRemoveDocumentTest(FlushMode mode, FlushStrategy strategy, boolean version, Class viewType, Class... views) { + super(mode, strategy, version, viewType, views); + } + + @Override + protected void prepareData(EntityManager em) { + doc1 = new Document("doc1", null, new Version()); + doc1.setVersion(1L); + doc1.setLastModified(new Date(EPOCH_2K)); + doc1.getNameObject().setPrimaryName("doc1"); + doc1.getNames().add(new NameObject("doc1", "doc1")); + doc1.getNameMap().put("doc1", new NameObject("doc1", "doc1")); + doc1.getNameContainer().setName("doc1"); + doc1.getNameContainer().getNameObject().setPrimaryName("doc1"); + doc1.getNameContainers().add(new NameObjectContainer("doc1", new NameObject("doc1", "doc1"))); + doc1.getNameContainerMap().put("doc1", new NameObjectContainer("doc1", new NameObject("doc1", "doc1"))); + doc2 = new Document("doc2"); + doc2.setVersion(1L); + doc2.setLastModified(new Date(EPOCH_2K)); + doc2.setNameObject(new NameObject("doc2", "doc2")); + doc2.getNames().add(new NameObject("doc2", "doc2")); + doc2.getNameMap().put("doc1", new NameObject("doc2", "doc2")); + doc2.getNameContainer().setName("doc2"); + doc2.getNameContainer().getNameObject().setPrimaryName("doc2"); + doc2.getNameContainers().add(new NameObjectContainer("doc2", new NameObject("doc2", "doc2"))); + doc2.getNameContainerMap().put("doc2", new NameObjectContainer("doc2", new NameObject("doc2", "doc2"))); + + p1 = new Person("pers1"); + p1.getNameObject().setPrimaryName("pers1"); + p1.getLocalized().put(1, "localized1"); + p2 = new Person("pers2"); + p2.getNameObject().setPrimaryName("pers2"); + p2.getLocalized().put(1, "localized2"); + + p3 = new Person("pers3"); + p3.getNameObject().setPrimaryName("pers3"); + p3.getLocalized().put(1, "localized3"); + p4 = new Person("pers4"); + p4.getNameObject().setPrimaryName("pers4"); + p4.getLocalized().put(1, "localized4"); + + p5 = new Person("pers3"); + p5.getNameObject().setPrimaryName("pers3"); + p5.getLocalized().put(1, "localized3"); + p6 = new Person("pers4"); + p6.getNameObject().setPrimaryName("pers4"); + p6.getLocalized().put(1, "localized4"); + + doc1.setOwner(p5); + doc1.setResponsiblePerson(p1); + doc1.getPeople().add(p1); + doc1.getContacts().put(1, p1); + doc1.getContacts2().put(2, p1); + doc1.getStrings().add("asd"); + doc1.getStringMap().put("doc1", "doc1"); + + doc2.setOwner(p6); + doc2.setResponsiblePerson(p2); + + em.persist(p1); + em.persist(p2); + em.persist(p3); + em.persist(p4); + em.persist(p5); + em.persist(p6); + em.persist(doc1); + em.persist(doc2); + + p1.setFriend(p3); + p2.setFriend(p4); + } + + @Override + protected void cleanDatabase() { + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + clearCollections(em, Person.class, Document.class); + em.createQuery("DELETE FROM Version").executeUpdate(); + em.createQuery("UPDATE Person SET partnerDocument = NULL, friend = NULL").executeUpdate(); + em.createQuery("DELETE FROM Document").executeUpdate(); + em.createQuery("DELETE FROM Person").executeUpdate(); + } + }); + } + + @Override + protected void restartTransactionAndReload() { + restartTransaction(); + // Load into PC, then access via find + cbf.create(em, Person.class) + .where("id").in(ids(p1, p2, p3, p4, p5, p6)) + .getResultList(); + cbf.create(em, Document.class) + .where("id").in(ids(doc1, doc2)) + .getResultList(); + doc1 = load(doc1); + doc2 = load(doc2); + p1 = load(p1); + p2 = load(p2); + p3 = load(p3); + p4 = load(p4); + p5 = load(p5); + p6 = load(p6); + } + + private List ids(LongSequenceEntity... entities) { + List ids = new ArrayList<>(entities.length); + for (LongSequenceEntity entity : entities) { + if (entity != null) { + ids.add(entity.getId()); + } + } + return ids; + } + + private Document load(Document d) { + if (d == null) { + return null; + } + return em.find(Document.class, d.getId()); + } + + private Person load(Person p) { + if (p == null) { + return null; + } + return em.find(Person.class, p.getId()); + } + + protected T getDoc1View() { + return evm.find(em, viewType, doc1.getId()); + } + + protected T getDoc2View() { + return evm.find(em, viewType, doc2.getId()); + } + + protected

P getP1View(Class

personView) { + return evm.find(em, personView, p1.getId()); + } + + protected

P getP2View(Class

personView) { + return evm.find(em, personView, p2.getId()); + } + + protected D getDocumentView(Long id, Class documentView) { + return evm.find(em, documentView, id); + } + + protected

P getPersonView(Long id, Class

personView) { + return evm.find(em, personView, id); + } + + protected AssertStatementBuilder deleteDocumentOwned(AssertStatementBuilder builder) { + return builder + .assertDelete().forRelation(Document.class, "contacts").and() + .assertDelete().forRelation(Document.class, "contacts2").and() + .assertDelete().forRelation(Document.class, "nameContainerMap").and() + .assertDelete().forRelation(Document.class, "nameContainers").and() + .assertDelete().forRelation(Document.class, "nameMap").and() + .assertDelete().forRelation(Document.class, "names").and() + .assertDelete().forRelation(Document.class, "people").and() + .assertDelete().forRelation(Document.class, "peopleListBag").and() + .assertDelete().forRelation(Document.class, "peopleCollectionBag").and() + .assertDelete().forRelation(Document.class, "stringMap").and() + .assertDelete().forRelation(Document.class, "strings").and() + ; + } + + protected AssertStatementBuilder deletePersonOwned(AssertStatementBuilder builder) { + return builder + .assertDelete().forRelation(Person.class, "favoriteDocuments").and() + .assertDelete().forRelation(Person.class, "localized").and() + ; + } + + @Override + protected AssertStatementBuilder fullFetch(AssertStatementBuilder builder) { + return null; + } + + @Override + protected AssertStatementBuilder versionUpdate(AssertStatementBuilder builder) { + return null; + } +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewCollectionsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewCollectionsTest.java new file mode 100644 index 0000000000..66bd3a0c2a --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewCollectionsTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested; + +import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.remove.AbstractEntityViewRemoveDocumentTest; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.FriendPersonView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableDocumentWithCollectionsView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableResponsiblePersonView; +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.Assert.*; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewRemoveNestedSubviewCollectionsTest extends AbstractEntityViewRemoveDocumentTest { + + public EntityViewRemoveNestedSubviewCollectionsTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentWithCollectionsView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.addEntityView(UpdatableResponsiblePersonView.class); + cfg.addEntityView(FriendPersonView.class); + } + + @Test + public void testSimpleRemove() { + // Given + final UpdatableDocumentWithCollectionsView docView = getDoc1View(); + clearQueries(); + + // When + remove(docView); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Person.class) + .select(Person.class) + .select(Document.class) + .select(Version.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // document.people.friend + builder.delete(Person.class) + // document.people + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } + + @Test + public void testRemoveById() { + // Given + clearQueries(); + + // When + remove(UpdatableDocumentWithCollectionsView.class, doc1.getId()); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Version.class); + // Hibernate flushes the changes done to the document because Person#1 was deleted => responsiblePerson set to NULL + builder.update(Document.class); + // Select for deletion because of possible cycle + // document.people.id + builder.select(Document.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // document.people + if (isQueryStrategy()) { + // The JPQL that joins people unfortunately joins all tables, though the collection table alone would suffice + builder.assertSelect() + .forEntity(Document.class) + .forRelation(Document.class, "people") + .fetching(Person.class) + .and(); + } else { + builder.assertSelect() + .fetching(Document.class, "people") + .fetching(Person.class) + .and(); + } + builder.delete(Person.class) + // document.people.friend + .select(Person.class) + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewMapsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewMapsTest.java new file mode 100644 index 0000000000..908779a813 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewMapsTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested; + +import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.remove.AbstractEntityViewRemoveDocumentTest; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.FriendPersonView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableDocumentWithMapsView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableResponsiblePersonView; +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.persistence.EntityManager; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewRemoveNestedSubviewMapsTest extends AbstractEntityViewRemoveDocumentTest { + + public EntityViewRemoveNestedSubviewMapsTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentWithMapsView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.addEntityView(UpdatableResponsiblePersonView.class); + cfg.addEntityView(FriendPersonView.class); + } + + @Test + public void testSimpleRemove() { + // Given + final UpdatableDocumentWithMapsView docView = getDoc1View(); + clearQueries(); + + // When + remove(docView); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Person.class) + .select(Person.class) + .select(Document.class) + .select(Version.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // document.contacts.friend + builder.delete(Person.class) + // document.contacts + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } + + @Test + public void testRemoveById() { + // Given + clearQueries(); + + // When + remove(UpdatableDocumentWithMapsView.class, doc1.getId()); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Version.class); + // Hibernate flushes the changes done to the document because Person#1 was deleted => responsiblePerson set to NULL + builder.update(Document.class); + + // Select for deletion because of possible cycle + // document.contacts.id + builder.select(Document.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // document.contacts + if (isQueryStrategy()) { + // The JPQL that joins people unfortunately joins all tables, though the collection table alone would suffice + builder.assertSelect() + .forEntity(Document.class) + .forRelation(Document.class, "contacts") + .fetching(Person.class) + .and(); + } else { + builder.assertSelect() + .fetching(Document.class, "contacts") + .fetching(Person.class) + .and(); + } + builder.delete(Person.class) + // document.contacts.friend + .select(Person.class) + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewTest.java new file mode 100644 index 0000000000..c31f0f86bc --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/EntityViewRemoveNestedSubviewTest.java @@ -0,0 +1,148 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested; + +import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateTest; +import com.blazebit.persistence.view.testsuite.update.remove.AbstractEntityViewRemoveDocumentTest; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.FriendPersonView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableDocumentView; +import com.blazebit.persistence.view.testsuite.update.remove.nested.model.UpdatableResponsiblePersonView; +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewRemoveNestedSubviewTest extends AbstractEntityViewRemoveDocumentTest { + + public EntityViewRemoveNestedSubviewTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.addEntityView(UpdatableResponsiblePersonView.class); + cfg.addEntityView(FriendPersonView.class); + } + + @Test + public void testSimpleRemove() { + // Given + final UpdatableDocumentView docView = getDoc1View(); + clearQueries(); + + // When + remove(docView); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Person.class) + .select(Person.class) + .select(Document.class) + .select(Version.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // document.responsiblePerson.friend + builder.delete(Person.class) + // document.responsiblePerson + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } + + @Test + public void testRemoveById() { + // Given + clearQueries(); + + // When + remove(UpdatableDocumentView.class, doc1.getId()); + + // Then + AssertStatementBuilder builder = assertQuerySequence().unordered(); + + if (!isQueryStrategy()) { + // Hibernate loads the entities before deleting? + builder.select(Version.class) + // document.responsiblePerson.friend + .select(Person.class); + } + + deleteDocumentOwned(builder); + deletePersonOwned(builder); + deletePersonOwned(builder); + + // Select for deletion because of possible cycle + // document.responsiblePerson.id + builder.select(Document.class); + + // document.responsiblePerson + builder.select(Person.class) + .delete(Person.class) + // document.responsiblePerson.friend + .delete(Person.class) + // document.versions + .delete(Version.class) + .delete(Document.class) + .validate(); + + restartTransactionAndReload(); + assertNull(doc1); + assertNull(p1); + assertNull(p3); + } +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/FriendPersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/FriendPersonView.java new file mode 100644 index 0000000000..3b998051c7 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/FriendPersonView.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public interface FriendPersonView extends PersonView { + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/PersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/PersonView.java new file mode 100644 index 0000000000..140ba25960 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/PersonView.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.testsuite.basic.model.IdHolderView; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@EntityView(Person.class) +public interface PersonView extends IdHolderView { + + public String getName(); + + public void setName(String name); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentView.java new file mode 100644 index 0000000000..fe04f27436 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentView.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.CascadeType; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.UpdatableMapping; + +import java.util.Date; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentView { + + @IdMapping + public Long getId(); + + public Long getVersion(); + + public String getName(); + + public void setName(String name); + + public Date getLastModified(); + + public void setLastModified(Date date); + + @UpdatableMapping(cascade = { CascadeType.AUTO, CascadeType.DELETE }) + public UpdatableResponsiblePersonView getResponsiblePerson(); + + public void setResponsiblePerson(UpdatableResponsiblePersonView responsiblePerson); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithCollectionsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithCollectionsView.java new file mode 100644 index 0000000000..0f8ac2869e --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithCollectionsView.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.CascadeType; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.UpdatableMapping; + +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentWithCollectionsView { + + @IdMapping + public Long getId(); + + public Long getVersion(); + + public String getName(); + + public void setName(String name); + + @UpdatableMapping(cascade = { CascadeType.AUTO, CascadeType.DELETE }) + public List getPeople(); + + public void setPeople(List people); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithMapsView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithMapsView.java new file mode 100644 index 0000000000..44af0a7ed2 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableDocumentWithMapsView.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.CascadeType; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.UpdatableMapping; + +import java.util.Map; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentWithMapsView { + + @IdMapping + public Long getId(); + + public Long getVersion(); + + public String getName(); + + public void setName(String name); + + @UpdatableMapping(cascade = { CascadeType.AUTO, CascadeType.DELETE }) + public Map getContacts(); + + public void setContacts(Map contacts); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatablePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatablePersonView.java new file mode 100644 index 0000000000..88726bba8e --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatablePersonView.java @@ -0,0 +1,29 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@UpdatableEntityView +public interface UpdatablePersonView extends PersonView { + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableResponsiblePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableResponsiblePersonView.java new file mode 100644 index 0000000000..4ae5451e6e --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/remove/nested/model/UpdatableResponsiblePersonView.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.remove.nested.model; + +import com.blazebit.persistence.view.CascadeType; +import com.blazebit.persistence.view.UpdatableMapping; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public interface UpdatableResponsiblePersonView extends UpdatablePersonView { + + @UpdatableMapping(cascade = { CascadeType.AUTO, CascadeType.DELETE }) + public FriendPersonView getFriend(); + + public void setFriend(FriendPersonView friend); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/graph/EntityViewUpdateSubviewGraphTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/graph/EntityViewUpdateSubviewGraphTest.java index 24ceb53099..ad9166b912 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/graph/EntityViewUpdateSubviewGraphTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/graph/EntityViewUpdateSubviewGraphTest.java @@ -99,7 +99,7 @@ public void testUpdateAddToCollectionAndSet() { update(docView); // Then - AssertStatementBuilder builder = assertQuerySequence(); + AssertStatementBuilder builder = assertUnorderedQuerySequence(); if (isFullMode()) { fullFetch(builder); @@ -182,7 +182,7 @@ public void testUpdateDifferentNestedGraphs() { update(docView); // Then - AssertStatementBuilder builder = assertQuerySequence(); + AssertStatementBuilder builder = assertUnorderedQuerySequence(); if (isQueryStrategy()) { if (isFullMode()) { diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/EntityViewUpdateSubviewInverseTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/EntityViewUpdateSubviewInverseEmbeddedTest.java similarity index 84% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/EntityViewUpdateSubviewInverseTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/EntityViewUpdateSubviewInverseEmbeddedTest.java index 0b83259aff..cd91f76985 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/EntityViewUpdateSubviewInverseTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/EntityViewUpdateSubviewInverseEmbeddedTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded; import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; @@ -24,7 +24,6 @@ import com.blazebit.persistence.view.EntityViewSetting; import com.blazebit.persistence.view.FlushMode; import com.blazebit.persistence.view.FlushStrategy; -import com.blazebit.persistence.view.change.ChangeModel; import com.blazebit.persistence.view.change.PluralChangeModel; import com.blazebit.persistence.view.spi.EntityViewConfiguration; import com.blazebit.persistence.view.testsuite.entity.LegacyOrder; @@ -33,18 +32,20 @@ import com.blazebit.persistence.view.testsuite.entity.LegacyOrderPositionDefaultId; import com.blazebit.persistence.view.testsuite.entity.LegacyOrderPositionId; import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateTest; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.LegacyOrderIdView; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.LegacyOrderPositionDefaultIdView; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.LegacyOrderPositionIdView; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.UpdatableLegacyOrderPositionDefaultView; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.UpdatableLegacyOrderPositionView; -import com.blazebit.persistence.view.testsuite.update.subview.inverse.model.UpdatableLegacyOrderView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.LegacyOrderIdView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.LegacyOrderPositionDefaultIdView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.LegacyOrderPositionIdView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.UpdatableLegacyOrderPositionDefaultView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.UpdatableLegacyOrderPositionView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model.UpdatableLegacyOrderView; import org.junit.Assert; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import static org.junit.Assert.assertFalse; + /** * * @author Christian Beikov @@ -53,7 +54,7 @@ @RunWith(Parameterized.class) // NOTE: No Datanucleus support yet @Category({ NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseTest extends AbstractEntityViewUpdateTest { +public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateTest { @Override protected Class[] getEntityClasses() { @@ -66,7 +67,7 @@ protected Class[] getEntityClasses() { }; } - public EntityViewUpdateSubviewInverseTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatableLegacyOrderView.class); } @@ -89,12 +90,18 @@ protected void registerViewTypes(EntityViewConfiguration cfg) { @Test public void testAddNewElementToCollection() { + // Given UpdatableLegacyOrderView newOrder = evm.create(UpdatableLegacyOrderView.class); update(newOrder); + + // When UpdatableLegacyOrderPositionView position = evm.create(UpdatableLegacyOrderPositionView.class); position.getId().setPositionId(0); newOrder.getPositions().add(position); update(newOrder); + + // Then + restartTransaction(); LegacyOrder legacyOrder = em.find(LegacyOrder.class, newOrder.getId()); Assert.assertEquals(1, legacyOrder.getPositions().size()); Assert.assertEquals(new LegacyOrderPositionId(newOrder.getId(), 0), legacyOrder.getPositions().iterator().next().getId()); @@ -102,18 +109,22 @@ public void testAddNewElementToCollection() { @Test public void testRemoveReadOnlyElementFromCollection() { + // Given UpdatableLegacyOrderView newOrder = evm.create(UpdatableLegacyOrderView.class); UpdatableLegacyOrderPositionView position = evm.create(UpdatableLegacyOrderPositionView.class); position.getId().setPositionId(0); newOrder.getPositions().add(position); update(newOrder); + // When + restartTransaction(); newOrder = evm.applySetting(EntityViewSetting.create(UpdatableLegacyOrderView.class), cbf.create(em, LegacyOrder.class)).getSingleResult(); newOrder.getPositions().remove(newOrder.getPositions().iterator().next()); PluralChangeModel positionsChangeModel = (PluralChangeModel) evm.getChangeModel(newOrder).get("positions"); Assert.assertEquals(1, positionsChangeModel.getRemovedElements().size()); update(newOrder); + // Then restartTransaction(); LegacyOrder legacyOrder = em.find(LegacyOrder.class, newOrder.getId()); Assert.assertEquals(0, legacyOrder.getPositions().size()); @@ -121,11 +132,20 @@ public void testRemoveReadOnlyElementFromCollection() { @Test public void testPersistAndAddNewElementToCollection() { + // When UpdatableLegacyOrderView newOrder = evm.create(UpdatableLegacyOrderView.class); UpdatableLegacyOrderPositionView position = evm.create(UpdatableLegacyOrderPositionView.class); position.getId().setPositionId(0); newOrder.getPositions().add(position); update(newOrder); + + // Then + // After update, the position is replaced with the declaration type + assertFalse(newOrder.getPositions().iterator().next() instanceof UpdatableLegacyOrderPositionView); + restartTransaction(); + LegacyOrder legacyOrder = em.find(LegacyOrder.class, newOrder.getId()); + Assert.assertEquals(1, legacyOrder.getPositions().size()); + Assert.assertEquals(new LegacyOrderPositionId(newOrder.getId(), 0), legacyOrder.getPositions().iterator().next().getId()); } @Override diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/IdHolderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/IdHolderView.java similarity index 97% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/IdHolderView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/IdHolderView.java index bd79070473..46d9f55431 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/IdHolderView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/IdHolderView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.IdMapping; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderIdView.java similarity index 97% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderIdView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderIdView.java index 82e9aad185..83004eaf9f 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderIdView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderIdView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.testsuite.entity.LegacyOrder; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionDefaultIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionDefaultIdView.java similarity index 98% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionDefaultIdView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionDefaultIdView.java index 796402493e..a04ba93120 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionDefaultIdView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionDefaultIdView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.UpdatableEntityView; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionIdView.java similarity index 86% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionIdView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionIdView.java index d8e2e076c8..f0a64fd732 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/LegacyOrderPositionIdView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/LegacyOrderPositionIdView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.UpdatableEntityView; @@ -42,8 +42,4 @@ interface Id { Integer getPositionId(); void setPositionId(Integer positionId); } - - @UpdatableMapping(subtypes = UpdatableLegacyOrderPositionDefaultView.class) - Set getDefaults(); - void setDefaults(Set defaults); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionDefaultView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionDefaultView.java similarity index 98% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionDefaultView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionDefaultView.java index b0655bc92e..3494c468d6 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionDefaultView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionDefaultView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionView.java similarity index 78% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionView.java index be6bca4782..a6e5be55c9 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderPositionView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderPositionView.java @@ -14,13 +14,16 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.UpdatableMapping; import com.blazebit.persistence.view.testsuite.entity.LegacyOrderPosition; +import java.util.Set; + /** * * @author Christian Beikov @@ -30,4 +33,9 @@ @UpdatableEntityView @EntityView(LegacyOrderPosition.class) public interface UpdatableLegacyOrderPositionView extends LegacyOrderPositionIdView { + + + @UpdatableMapping(subtypes = UpdatableLegacyOrderPositionDefaultView.class) + Set getDefaults(); + void setDefaults(Set defaults); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderView.java similarity index 98% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderView.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderView.java index b7e6748cc4..f8643ffeae 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/model/UpdatableLegacyOrderView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/model/UpdatableLegacyOrderView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.blazebit.persistence.view.testsuite.update.subview.inverse.model; +package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.model; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/EntityViewUpdateSubviewInverseUmappedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/EntityViewUpdateSubviewInverseUmappedTest.java new file mode 100644 index 0000000000..2e09d41a32 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/EntityViewUpdateSubviewInverseUmappedTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped; + +import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.EntityViewSetting; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.change.PluralChangeModel; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateTest; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.DocumentIdView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.PersonIdView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.UpdatableDocumentView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.UpdatablePersonView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.UpdatableVersionView; +import com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model.VersionIdView; +import org.junit.Assert; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewUpdateSubviewInverseUmappedTest extends AbstractEntityViewUpdateTest { + + public EntityViewUpdateSubviewInverseUmappedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.addEntityView(DocumentIdView.class); + cfg.addEntityView(UpdatableDocumentView.class); + cfg.addEntityView(PersonIdView.class); + cfg.addEntityView(UpdatablePersonView.class); + cfg.addEntityView(VersionIdView.class); + cfg.addEntityView(UpdatableVersionView.class); + } + + @Test + public void testAddNewElementToCollection() { + // Given + UpdatableDocumentView newDocument = evm.create(UpdatableDocumentView.class); + UpdatablePersonView owner = evm.create(UpdatablePersonView.class); + owner.setName("test"); + newDocument.setName("doc"); + newDocument.setOwner(owner); + update(newDocument); + + // When + UpdatableVersionView version = evm.create(UpdatableVersionView.class); + newDocument.getVersions().add(version); + update(newDocument); + + // Then + restartTransaction(); + Document doc = em.find(Document.class, newDocument.getId()); + Assert.assertEquals(1, doc.getVersions().size()); + Assert.assertEquals(version.getId(), doc.getVersions().iterator().next().getId()); + } + + @Test + public void testRemoveReadOnlyElementFromCollection() { + // Given + UpdatableDocumentView newDocument = evm.create(UpdatableDocumentView.class); + UpdatablePersonView owner = evm.create(UpdatablePersonView.class); + owner.setName("test"); + newDocument.setName("doc"); + newDocument.setOwner(owner); + UpdatableVersionView version = evm.create(UpdatableVersionView.class); + newDocument.getVersions().add(version); + update(newDocument); + + // When + restartTransaction(); + newDocument = evm.applySetting(EntityViewSetting.create(UpdatableDocumentView.class), cbf.create(em, Document.class)).getSingleResult(); + newDocument.getVersions().remove(newDocument.getVersions().iterator().next()); + PluralChangeModel positionsChangeModel = (PluralChangeModel) evm.getChangeModel(newDocument).get("versions"); + Assert.assertEquals(1, positionsChangeModel.getRemovedElements().size()); + update(newDocument); + + // Then + restartTransaction(); + Document doc = em.find(Document.class, newDocument.getId()); + Assert.assertEquals(0, doc.getVersions().size()); + } + + @Test + public void testPersistAndAddNewElementToCollection() { + // When + UpdatableDocumentView newDocument = evm.create(UpdatableDocumentView.class); + UpdatablePersonView owner = evm.create(UpdatablePersonView.class); + owner.setName("test"); + newDocument.setName("doc"); + newDocument.setOwner(owner); + UpdatableVersionView version = evm.create(UpdatableVersionView.class); + newDocument.getVersions().add(version); + update(newDocument); + + // Then + restartTransaction(); + Document doc = em.find(Document.class, newDocument.getId()); + Assert.assertEquals(1, doc.getVersions().size()); + Assert.assertEquals(version.getId(), doc.getVersions().iterator().next().getId()); + } + + @Override + protected void restartTransactionAndReload() { + restartTransaction(); + } + + @Override + protected AssertStatementBuilder fullFetch(AssertStatementBuilder builder) { + return builder.assertSelect() + .fetching(Document.class) + .fetching(Person.class) + .fetching(Person.class) + .fetching(Document.class) + .fetching(Document.class, "people") + .fetching(Person.class) + .fetching(Person.class) + .fetching(Person.class) + .and(); + } + + @Override + protected AssertStatementBuilder fullUpdate(AssertStatementBuilder builder) { + fullFetch(builder) + .update(Person.class) + .update(Person.class) + .update(Person.class) + .update(Person.class) + .update(Document.class) + .update(Person.class) + .update(Person.class) + .update(Person.class); + if (version) { + builder.update(Document.class); + } + + return builder; + } + + @Override + protected AssertStatementBuilder versionUpdate(AssertStatementBuilder builder) { + return builder.update(Document.class); + } +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/DocumentIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/DocumentIdView.java new file mode 100644 index 0000000000..a08c05d196 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/DocumentIdView.java @@ -0,0 +1,30 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.testsuite.entity.LegacyOrder; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@EntityView(Document.class) +public interface DocumentIdView extends IdHolderView { +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/IdHolderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/IdHolderView.java new file mode 100644 index 0000000000..e548f8e524 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/IdHolderView.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.view.IdMapping; + +import java.io.Serializable; + +/** + * + * @author Christian Beikov + * @since 1.0 + */ +public interface IdHolderView extends Serializable { + + @IdMapping + public T getId(); + + public void setId(T id); +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/PersonIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/PersonIdView.java new file mode 100644 index 0000000000..a6aed83c47 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/PersonIdView.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.EntityView; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@EntityView(Person.class) +public interface PersonIdView extends IdHolderView { + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableDocumentView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableDocumentView.java new file mode 100644 index 0000000000..301a9d83fb --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableDocumentView.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.InverseRemoveStrategy; +import com.blazebit.persistence.view.MappingInverse; +import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.UpdatableMapping; + +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@CreatableEntityView +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentView extends DocumentIdView { + + public String getName(); + public void setName(String name); + + @UpdatableMapping(subtypes = UpdatablePersonView.class) + PersonIdView getOwner(); + void setOwner(PersonIdView owner); + + @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) + @UpdatableMapping(subtypes = UpdatableVersionView.class) + Set getVersions(); + void setVersions(Set versions); +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatablePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatablePersonView.java new file mode 100644 index 0000000000..a282218c56 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatablePersonView.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@CreatableEntityView +@UpdatableEntityView +@EntityView(Person.class) +public interface UpdatablePersonView extends PersonIdView { + + public String getName(); + + public void setName(String name); +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableVersionView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableVersionView.java new file mode 100644 index 0000000000..1a8e888d35 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/UpdatableVersionView.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@CreatableEntityView +@UpdatableEntityView +@EntityView(Version.class) +public interface UpdatableVersionView extends VersionIdView { + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/VersionIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/VersionIdView.java new file mode 100644 index 0000000000..0937ff6893 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/unmapped/model/VersionIdView.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.view.testsuite.update.subview.inverse.unmapped.model; + +import com.blazebit.persistence.testsuite.entity.Version; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; +import com.blazebit.persistence.view.testsuite.entity.LegacyOrderPositionDefault; +import com.blazebit.persistence.view.testsuite.entity.LegacyOrderPositionDefaultId; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +@EntityView(Version.class) +public interface VersionIdView extends IdHolderView { + +} diff --git a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusExtendedQuerySupport.java b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusExtendedQuerySupport.java index 2a4652acce..f78e701c02 100644 --- a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusExtendedQuerySupport.java +++ b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusExtendedQuerySupport.java @@ -16,19 +16,16 @@ package com.blazebit.persistence.impl.datanucleus; -import java.lang.reflect.Field; -import java.util.List; - -import javax.persistence.EntityManager; -import javax.persistence.Query; -import javax.persistence.metamodel.EntityType; - -import org.datanucleus.store.rdbms.query.JPQLQuery; -import org.datanucleus.store.rdbms.query.RDBMSQueryCompilation; - import com.blazebit.apt.service.ServiceProvider; import com.blazebit.persistence.ReturningResult; import com.blazebit.persistence.spi.ExtendedQuerySupport; +import org.datanucleus.store.rdbms.query.JPQLQuery; +import org.datanucleus.store.rdbms.query.RDBMSQueryCompilation; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.lang.reflect.Field; +import java.util.List; @ServiceProvider(ExtendedQuerySupport.class) public class DataNucleusExtendedQuerySupport implements ExtendedQuerySupport { @@ -61,18 +58,6 @@ public List getCascadingDeleteSql(EntityManager em, Query query) { throw new UnsupportedOperationException("Not yet implemeneted!"); } - @Override - public String[] getColumnNames(EntityManager em, EntityType entityType, String attributeName) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemeneted!"); - } - - @Override - public String[] getColumnTypes(EntityManager em, EntityType entityType, String attributeName) { - // TODO: implement - throw new UnsupportedOperationException("Not yet implemeneted!"); - } - @Override public int getSqlSelectAliasPosition(EntityManager em, Query query, String alias) { // TODO: implement diff --git a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusJpaProvider.java b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusJpaProvider.java index 6719bbfc09..fbe3521591 100644 --- a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusJpaProvider.java +++ b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/DataNucleusJpaProvider.java @@ -17,12 +17,15 @@ package com.blazebit.persistence.impl.datanucleus; import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.spi.JpaProvider; import org.datanucleus.ExecutionContext; import org.datanucleus.api.jpa.metamodel.AttributeImpl; import org.datanucleus.api.jpa.metamodel.ManagedTypeImpl; import org.datanucleus.metadata.AbstractMemberMetaData; +import org.datanucleus.metadata.ColumnMetaData; import org.datanucleus.metadata.EmbeddedMetaData; +import org.datanucleus.metadata.KeyMetaData; import javax.persistence.EntityManager; import javax.persistence.metamodel.EntityType; @@ -30,6 +33,7 @@ import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; +import java.util.HashMap; import java.util.Map; /** @@ -39,11 +43,12 @@ */ public class DataNucleusJpaProvider implements JpaProvider { + private static final String[] EMPTY = {}; private final int major; private final int minor; private final int fix; - public DataNucleusJpaProvider(EntityManager em, int major, int minor, int fix) { + public DataNucleusJpaProvider(int major, int minor, int fix) { this.major = major; this.minor = minor; this.fix = fix; @@ -225,7 +230,17 @@ public Map getWritableMappedByMappings(EntityType inverseType return null; } - private AttributeImpl getAttribute(EntityType ownerType, String attributeName) { + @Override + public String[] getColumnNames(EntityType ownerType, String attributeName) { + return EMPTY; + } + + @Override + public String[] getColumnTypes(EntityType ownerType, String attributeName) { + return EMPTY; + } + + private AttributeImpl getAttribute(ManagedType ownerType, String attributeName) { if (attributeName.indexOf('.') == -1) { return (AttributeImpl) ownerType.getAttribute(attributeName); } @@ -245,10 +260,45 @@ public Map getWritableMappedByMappings(EntityType inverseType } @Override - public String getJoinTable(EntityType ownerType, String attributeName) { - AbstractMemberMetaData metaData = getAttribute(ownerType, attributeName).getMetadata(); + public JoinTable getJoinTable(EntityType ownerType, String attributeName) { + AttributeImpl attribute = getAttribute(ownerType, attributeName); + AbstractMemberMetaData metaData = attribute.getMetadata(); if (metaData.getJoinMetaData() != null) { - return metaData.getJoinMetaData().getTable(); + ColumnMetaData[] primaryKeyColumnMetaData = metaData.getJoinMetaData().getPrimaryKeyMetaData().getColumnMetaData(); + ColumnMetaData[] foreignKeyColumnMetaData = metaData.getJoinMetaData().getForeignKeyMetaData().getColumnMetaData(); + Map idColumnMapping = new HashMap<>(primaryKeyColumnMetaData.length); + for (int i = 0; i < foreignKeyColumnMetaData.length; i++) { + idColumnMapping.put(foreignKeyColumnMetaData[i].getName(), primaryKeyColumnMetaData[i].getName()); + } + Map keyMapping = null; + KeyMetaData keyMetaData = metaData.getKeyMetaData(); + if (keyMetaData != null) { + keyMapping = new HashMap<>(); + ColumnMetaData[] keyColumnMetaData = keyMetaData.getColumnMetaData(); + ColumnMetaData[] keyTargetPrimaryKeyColumnMetaData = keyMetaData.getForeignKeyMetaData().getColumnMetaData(); + if (keyTargetPrimaryKeyColumnMetaData == null) { + keyMapping.put(keyMetaData.getColumnName(), keyMetaData.getColumnName()); + } else { + for (int i = 0; i < keyTargetPrimaryKeyColumnMetaData.length; i++) { + keyMapping.put(keyColumnMetaData[i].getName(), keyTargetPrimaryKeyColumnMetaData[i].getName()); + } + } + } + + ColumnMetaData[] targetColumnMetaData = metaData.getJoinMetaData().getColumnMetaData(); + ColumnMetaData[] targetPrimaryKeyColumnMetaData = metaData.getElementMetaData().getForeignKeyMetaData().getColumnMetaData(); + Map targetIdColumnMapping = new HashMap<>(targetPrimaryKeyColumnMetaData.length); + + for (int i = 0; i < targetColumnMetaData.length; i++) { + targetIdColumnMapping.put(targetColumnMetaData[i].getName(), targetPrimaryKeyColumnMetaData[i].getName()); + } + + return new JoinTable( + metaData.getJoinMetaData().getTable(), + idColumnMapping, + keyMapping, + targetIdColumnMapping + ); } return null; @@ -269,6 +319,18 @@ public boolean isBag(EntityType ownerType, String attributeName) { return false; } + @Override + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { + AttributeImpl attribute = getAttribute(ownerType, attributeName); + return attribute != null && attribute.getMetadata().isCascadeRemoveOrphans(); + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + AttributeImpl attribute = getAttribute(ownerType, attributeName); + return attribute != null && attribute.getMetadata().isCascadeDelete(); + } + @Override public boolean containsEntity(EntityManager em, Class entityClass, Object id) { ExecutionContext ec = em.unwrap(ExecutionContext.class); @@ -309,4 +371,14 @@ public boolean needsBrokenAssociationToIdRewriteInOnClause() { public boolean needsTypeConstraintForColumnSharing() { return false; } + + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return false; + } + + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return false; + } } diff --git a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/function/DataNucleusEntityManagerFactoryIntegrator.java b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/function/DataNucleusEntityManagerFactoryIntegrator.java index 9d26e4949c..b8a5d46008 100644 --- a/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/function/DataNucleusEntityManagerFactoryIntegrator.java +++ b/integration/datanucleus/src/main/java/com/blazebit/persistence/impl/datanucleus/function/DataNucleusEntityManagerFactoryIntegrator.java @@ -100,7 +100,7 @@ public JpaProviderFactory getJpaProviderFactory(EntityManagerFactory entityManag return new JpaProviderFactory() { @Override public JpaProvider createJpaProvider(EntityManager em) { - return new DataNucleusJpaProvider(em, MAJOR, MINOR, FIX); + return new DataNucleusJpaProvider(MAJOR, MINOR, FIX); } }; } diff --git a/integration/deltaspike-data/testsuite/src/test/resources/logging.properties b/integration/deltaspike-data/testsuite/src/test/resources/logging.properties index 970654e209..32f9913f4a 100644 --- a/integration/deltaspike-data/testsuite/src/test/resources/logging.properties +++ b/integration/deltaspike-data/testsuite/src/test/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright 2014 - 2017 Blazebit. +# Copyright 2014 - 2018 Blazebit. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/EclipseLinkJpaProvider.java b/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/EclipseLinkJpaProvider.java index 0c6c9d6b23..da6bbac922 100644 --- a/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/EclipseLinkJpaProvider.java +++ b/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/EclipseLinkJpaProvider.java @@ -17,12 +17,14 @@ package com.blazebit.persistence.impl.eclipselink; import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.spi.JpaProvider; import org.eclipse.persistence.internal.jpa.metamodel.AttributeImpl; import org.eclipse.persistence.internal.jpa.metamodel.ManagedTypeImpl; import org.eclipse.persistence.jpa.JpaEntityManager; import org.eclipse.persistence.mappings.CollectionMapping; import org.eclipse.persistence.mappings.DatabaseMapping; +import org.eclipse.persistence.mappings.ForeignReferenceMapping; import org.eclipse.persistence.mappings.OneToOneMapping; import javax.persistence.EntityManager; @@ -31,6 +33,7 @@ import javax.persistence.metamodel.PluralAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; +import java.util.HashMap; import java.util.Map; /** @@ -40,8 +43,9 @@ */ public class EclipseLinkJpaProvider implements JpaProvider { - public EclipseLinkJpaProvider(EntityManager em) { + private static final String[] EMPTY = {}; + public EclipseLinkJpaProvider() { } @Override @@ -217,12 +221,47 @@ public Map getWritableMappedByMappings(EntityType inverseType } @Override - public String getJoinTable(EntityType ownerType, String attributeName) { + public String[] getColumnNames(EntityType ownerType, String attributeName) { + return EMPTY; + } + + @Override + public String[] getColumnTypes(EntityType ownerType, String attributeName) { + return EMPTY; + } + + @Override + public JoinTable getJoinTable(EntityType ownerType, String attributeName) { DatabaseMapping mapping = getAttribute(ownerType, attributeName).getMapping(); if (mapping instanceof OneToOneMapping) { OneToOneMapping oneToOneMapping = (OneToOneMapping) mapping; if (oneToOneMapping.hasRelationTable()) { - oneToOneMapping.getRelationTable().getName(); +// ColumnMetaData[] primaryKeyColumnMetaData = metaData.getJoinMetaData().getPrimaryKeyMetaData().getColumnMetaData(); +// ColumnMetaData[] foreignKeyColumnMetaData = metaData.getJoinMetaData().getForeignKeyMetaData().getColumnMetaData(); + Map idColumnMapping = new HashMap<>(); +// for (int i = 0; i < foreignKeyColumnMetaData.length; i++) { +// idColumnMapping.put(foreignKeyColumnMetaData[i].getName(), primaryKeyColumnMetaData[i].getName()); +// } + Map keyMapping = null; +// KeyMetaData keyMetaData = metaData.getKeyMetaData(); +// if (keyMetaData != null) { +// keyColumn = keyMetaData.getColumnName(); +// } + +// ColumnMetaData[] targetColumnMetaData = metaData.getJoinMetaData().getColumnMetaData(); +// ColumnMetaData[] targetPrimaryKeyColumnMetaData = metaData.getElementMetaData().getForeignKeyMetaData().getColumnMetaData(); + Map targetIdColumnMapping = new HashMap<>(); + +// for (int i = 0; i < targetColumnMetaData.length; i++) { +// targetIdColumnMapping.put(targetColumnMetaData[i].getName(), targetPrimaryKeyColumnMetaData[i].getName()); +// } + return new JoinTable( + oneToOneMapping.getRelationTable().getName(), + idColumnMapping, + keyMapping, + targetIdColumnMapping + + ); } } else if (mapping instanceof CollectionMapping) { CollectionMapping collectionMapping = (CollectionMapping) mapping; @@ -251,12 +290,32 @@ public boolean isBag(EntityType ownerType, String attributeName) { return false; } + @Override + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { + AttributeImpl attribute = getAttribute(ownerType, attributeName); + if (attribute != null && attribute.getMapping() instanceof ForeignReferenceMapping) { + ForeignReferenceMapping mapping = (ForeignReferenceMapping) attribute.getMapping(); + return mapping.isPrivateOwned() && mapping.isCascadeRemove(); + } + return false; + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + AttributeImpl attribute = getAttribute(ownerType, attributeName); + if (attribute != null && attribute.getMapping() instanceof ForeignReferenceMapping) { + ForeignReferenceMapping mapping = (ForeignReferenceMapping) attribute.getMapping(); + return mapping.isCascadeRemove(); + } + return false; + } + @Override public boolean containsEntity(EntityManager em, Class entityClass, Object id) { return em.unwrap(JpaEntityManager.class).getActiveSession().getIdentityMapAccessor().getFromIdentityMap(id, entityClass) != null; } - private AttributeImpl getAttribute(EntityType ownerType, String attributeName) { + private AttributeImpl getAttribute(ManagedType ownerType, String attributeName) { if (attributeName.indexOf('.') == -1) { return (AttributeImpl) ownerType.getAttribute(attributeName); } @@ -310,4 +369,14 @@ public boolean needsTypeConstraintForColumnSharing() { return false; } + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return true; + } + + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return true; + } + } diff --git a/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/function/EclipseLinkEntityManagerIntegrator.java b/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/function/EclipseLinkEntityManagerIntegrator.java index 9e89f009dc..a3ede8ee98 100644 --- a/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/function/EclipseLinkEntityManagerIntegrator.java +++ b/integration/eclipselink/src/main/java/com/blazebit/persistence/impl/eclipselink/function/EclipseLinkEntityManagerIntegrator.java @@ -80,7 +80,7 @@ public JpaProviderFactory getJpaProviderFactory(EntityManagerFactory entityManag return new JpaProviderFactory() { @Override public JpaProvider createJpaProvider(EntityManager em) { - return new EclipseLinkJpaProvider(em); + return new EclipseLinkJpaProvider(); } }; } diff --git a/integration/hibernate-4.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate42EntityManagerFactoryIntegrator.java b/integration/hibernate-4.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate42EntityManagerFactoryIntegrator.java index c03e51cce3..4edf4d1c59 100644 --- a/integration/hibernate-4.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate42EntityManagerFactoryIntegrator.java +++ b/integration/hibernate-4.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate42EntityManagerFactoryIntegrator.java @@ -97,10 +97,10 @@ public JpaProvider createJpaProvider(EntityManager em) { factory = (SessionFactoryImplementor) ((HibernateEntityManagerFactory) entityManagerFactory).getSessionFactory(); } if (entityManagerFactory instanceof HibernateEntityManagerFactory) { - return new HibernateJpaProvider(em, getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters()); + return new HibernateJpaProvider(getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters()); } } - return new HibernateJpaProvider(em, getDbms(em), getEntityPersisters(em), getCollectionPersisters(em)); + return new HibernateJpaProvider(getDbms(em), getEntityPersisters(em), getCollectionPersisters(em)); } }; } diff --git a/integration/hibernate-4.3/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate43EntityManagerFactoryIntegrator.java b/integration/hibernate-4.3/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate43EntityManagerFactoryIntegrator.java index e2fb7b58c9..f88f1e0205 100644 --- a/integration/hibernate-4.3/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate43EntityManagerFactoryIntegrator.java +++ b/integration/hibernate-4.3/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate43EntityManagerFactoryIntegrator.java @@ -90,10 +90,10 @@ public JpaProvider createJpaProvider(EntityManager em) { factory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); } if (factory != null) { - return new HibernateJpa21Provider(em, getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); } } - return new HibernateJpa21Provider(em, getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); } }; } diff --git a/integration/hibernate-5.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate52EntityManagerFactoryIntegrator.java b/integration/hibernate-5.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate52EntityManagerFactoryIntegrator.java index b48119dee1..e195f4a014 100644 --- a/integration/hibernate-5.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate52EntityManagerFactoryIntegrator.java +++ b/integration/hibernate-5.2/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate52EntityManagerFactoryIntegrator.java @@ -91,10 +91,10 @@ public JpaProvider createJpaProvider(EntityManager em) { factory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); } if (factory != null) { - return new HibernateJpa21Provider(em, getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); } } - return new HibernateJpa21Provider(em, getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); } }; } diff --git a/integration/hibernate-5/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate5EntityManagerFactoryIntegrator.java b/integration/hibernate-5/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate5EntityManagerFactoryIntegrator.java index 0772d8f105..290e64ef3d 100644 --- a/integration/hibernate-5/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate5EntityManagerFactoryIntegrator.java +++ b/integration/hibernate-5/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate5EntityManagerFactoryIntegrator.java @@ -91,10 +91,10 @@ public JpaProvider createJpaProvider(EntityManager em) { factory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); } if (factory != null) { - return new HibernateJpa21Provider(em, getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); } } - return new HibernateJpa21Provider(em, getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); } }; } diff --git a/integration/hibernate-6.0/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate60EntityManagerFactoryIntegrator.java b/integration/hibernate-6.0/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate60EntityManagerFactoryIntegrator.java index 3486b12ad1..049b807b74 100644 --- a/integration/hibernate-6.0/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate60EntityManagerFactoryIntegrator.java +++ b/integration/hibernate-6.0/src/main/java/com/blazebit/persistence/impl/hibernate/Hibernate60EntityManagerFactoryIntegrator.java @@ -91,10 +91,10 @@ public JpaProvider createJpaProvider(EntityManager em) { factory = entityManagerFactory.unwrap(SessionFactoryImplementor.class); } if (factory != null) { - return new HibernateJpa21Provider(em, getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbmsName(factory.getDialect()), factory.getEntityPersisters(), factory.getCollectionPersisters(), MAJOR, MINOR, FIX); } } - return new HibernateJpa21Provider(em, getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); + return new HibernateJpa21Provider(getDbms(em), getEntityPersisters(em), getCollectionPersisters(em), MAJOR, MINOR, FIX); } }; } diff --git a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateExtendedQuerySupport.java b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateExtendedQuerySupport.java index ec9249b6f7..6b82e73217 100644 --- a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateExtendedQuerySupport.java +++ b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateExtendedQuerySupport.java @@ -28,11 +28,9 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.MappingException; import org.hibernate.TypeMismatchException; import org.hibernate.engine.query.spi.HQLQueryPlan; import org.hibernate.engine.query.spi.QueryPlanCache; -import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.RowSelection; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -57,13 +55,8 @@ import org.hibernate.hql.spi.QueryTranslator; import org.hibernate.internal.util.collections.BoundedConcurrentHashMap; import org.hibernate.loader.hql.QueryLoader; -import org.hibernate.mapping.Column; -import org.hibernate.mapping.Table; import org.hibernate.param.ParameterSpecification; import org.hibernate.persister.entity.AbstractEntityPersister; -import org.hibernate.persister.entity.JoinedSubclassEntityPersister; -import org.hibernate.persister.entity.SingleTableEntityPersister; -import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; @@ -72,10 +65,8 @@ import javax.persistence.NonUniqueResultException; import javax.persistence.PersistenceException; import javax.persistence.Query; -import javax.persistence.metamodel.EntityType; import java.io.Serializable; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Deque; @@ -174,91 +165,6 @@ private HQLQueryPlan getOriginalQueryPlan(SessionImplementor session, Query quer return sfi.getQueryPlanCache().getHQLQueryPlan(queryString, false, Collections.EMPTY_MAP); } - @Override - public String[] getColumnNames(EntityManager em, EntityType entityType, String attributeName) { - SessionImplementor session = em.unwrap(SessionImplementor.class); - SessionFactoryImplementor sfi = session.getFactory(); - try { - return ((AbstractEntityPersister) sfi.getClassMetadata(entityType.getJavaType())).getPropertyColumnNames(attributeName); - } catch (MappingException e) { - throw new RuntimeException("Unknown property [" + attributeName + "] of entity [" + entityType.getJavaType() + "]", e); - } - } - - @Override - public String[] getColumnTypes(EntityManager em, EntityType entityType, String attributeName) { - SessionImplementor session = em.unwrap(SessionImplementor.class); - SessionFactoryImplementor sfi = session.getFactory(); - AbstractEntityPersister entityPersister = (AbstractEntityPersister) sfi.getClassMetadata(entityType.getJavaType()); - String[] columnNames = entityPersister.getPropertyColumnNames(attributeName); - Database database = sfi.getServiceRegistry().locateServiceBinding(Database.class).getService(); - Table[] tables; - - if (entityPersister instanceof JoinedSubclassEntityPersister) { - tables = new Table[((JoinedSubclassEntityPersister) entityPersister).getTableSpan()]; - for (int i = 0; i < tables.length; i++) { - tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); - } - } else if (entityPersister instanceof UnionSubclassEntityPersister) { - tables = new Table[((UnionSubclassEntityPersister) entityPersister).getTableSpan()]; - for (int i = 0; i < tables.length; i++) { - tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); - } - } else if (entityPersister instanceof SingleTableEntityPersister) { - tables = new Table[((SingleTableEntityPersister) entityPersister).getTableSpan()]; - for (int i = 0; i < tables.length; i++) { - tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); - } - } else { - tables = new Table[] { database.getTable(entityPersister.getTableName()) }; - } - - // In this case, the property might represent a formula - if (columnNames.length == 1 && columnNames[0] == null) { - Type propertyType = entityPersister.getPropertyType(attributeName); - long length; - int precision; - int scale; - try { - Method m = Type.class.getMethod("dictatedSizes", Mapping.class); - Object size = ((Object[]) m.invoke(propertyType, sfi))[0]; - length = (long) size.getClass().getMethod("getLength").invoke(size); - precision = (int) size.getClass().getMethod("getPrecision").invoke(size); - scale = (int) size.getClass().getMethod("getScale").invoke(size); - } catch (Exception ex) { - throw new RuntimeException("Could not determine the column type of the attribute: " + attributeName + " of the entity: " + entityType.getName()); - } - - return new String[] { - sfi.getDialect().getTypeName( - propertyType.sqlTypes(sfi)[0], - length, - precision, - scale - ) - }; - } - - String[] columnTypes = new String[columnNames.length]; - for (int i = 0; i < columnNames.length; i++) { - Column column = null; - for (int j = 0; j < tables.length; j++) { - column = tables[j].getColumn(new Column(columnNames[i])); - if (column != null) { - break; - } - } - - if (column == null) { - throw new IllegalArgumentException("Could not find column '" + columnNames[i] + "' in for entity: " + entityType.getName()); - } - - columnTypes[i] = column.getSqlType(); - } - - return columnTypes; - } - @Override public String getSqlAlias(EntityManager em, Query query, String alias) { SessionImplementor session = em.unwrap(SessionImplementor.class); diff --git a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpa21Provider.java b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpa21Provider.java index 27548871f2..2e749be126 100644 --- a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpa21Provider.java +++ b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpa21Provider.java @@ -16,10 +16,14 @@ package com.blazebit.persistence.impl.hibernate; +import org.hibernate.engine.spi.CascadingAction; import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.tuple.entity.EntityMetamodel; -import javax.persistence.EntityManager; +import javax.persistence.metamodel.ManagedType; +import java.lang.reflect.Method; import java.util.Map; /** @@ -29,14 +33,30 @@ */ public class HibernateJpa21Provider extends HibernateJpaProvider { + private static final Method HAS_ORPHAN_DELETE_METHOD; + private static final Method DO_CASCADE_METHOD; + private static final CascadingAction DELETE_CASCADE; + static { + try { + Class cascadeStyleClass = Class.forName("org.hibernate.engine.spi.CascadeStyle"); + HAS_ORPHAN_DELETE_METHOD = cascadeStyleClass.getMethod("hasOrphanDelete"); + DO_CASCADE_METHOD = cascadeStyleClass.getMethod("doCascade", CascadingAction.class); + DELETE_CASCADE = (CascadingAction) Class.forName("org.hibernate.engine.spi.CascadingActions").getField("DELETE").get(null); + } catch (Exception ex) { + throw new RuntimeException("Could not access cascading information. Please report your version of hibernate so we can provide support for it!", ex); + } + } + private final boolean supportsEntityJoin; private final boolean needsJoinSubqueryRewrite; private final boolean supportsForeignAssociationInOnClause; private final boolean needsAssociationToIdRewriteInOnClause; private final boolean needsBrokenAssociationToIdRewriteInOnClause; + private final boolean supportsCollectionTableCleanupOnDelete; + private final boolean supportsJoinTableCleanupOnDelete; - public HibernateJpa21Provider(EntityManager em, String dbms, Map entityPersisters, Map collectionPersisters, int major, int minor, int fix) { - super(em, dbms, entityPersisters, collectionPersisters); + public HibernateJpa21Provider(String dbms, Map entityPersisters, Map collectionPersisters, int major, int minor, int fix) { + super(dbms, entityPersisters, collectionPersisters); this.supportsEntityJoin = major > 5 || major == 5 && minor >= 1; // Got fixed in 5.2.3: https://hibernate.atlassian.net/browse/HHH-9329 but is still buggy: https://hibernate.atlassian.net/browse/HHH-11401 this.needsJoinSubqueryRewrite = major < 5 || major == 5 && minor < 2 || major == 5 && minor == 2 && fix < 7; @@ -46,6 +66,41 @@ public HibernateJpa21Provider(EntityManager em, String dbms, Map ownerType, String attributeName) { + AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + try { + return (boolean) HAS_ORPHAN_DELETE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index]); + } catch (Exception ex) { + throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + } + } + + return false; + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + try { + return (boolean) DO_CASCADE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index], DELETE_CASCADE); + } catch (Exception ex) { + throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + } + } + + return false; } @Override @@ -88,4 +143,14 @@ public boolean needsBrokenAssociationToIdRewriteInOnClause() { return needsBrokenAssociationToIdRewriteInOnClause; } + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return supportsCollectionTableCleanupOnDelete; + } + + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return supportsJoinTableCleanupOnDelete; + } + } diff --git a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpaProvider.java b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpaProvider.java index 1ef8c0c9cb..f2f1f30eb1 100644 --- a/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpaProvider.java +++ b/integration/hibernate-base/src/main/java/com/blazebit/persistence/impl/hibernate/HibernateJpaProvider.java @@ -17,17 +17,26 @@ package com.blazebit.persistence.impl.hibernate; import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.spi.JpaProvider; +import org.hibernate.MappingException; +import org.hibernate.engine.spi.CascadingAction; import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.Mapping; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.Table; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.AbstractEntityPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Joinable; +import org.hibernate.persister.entity.JoinedSubclassEntityPersister; import org.hibernate.persister.entity.SingleTableEntityPersister; import org.hibernate.persister.entity.UnionSubclassEntityPersister; +import org.hibernate.tuple.entity.EntityMetamodel; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; @@ -38,17 +47,17 @@ import javax.persistence.EntityManager; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; -import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SetAttribute; import javax.persistence.metamodel.SingularAttribute; import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; /** * @@ -57,10 +66,9 @@ */ public class HibernateJpaProvider implements JpaProvider { - private final DB db; - private final Map entityPersisters; - private final Map collectionPersisters; - private final ConcurrentMap columnSharingCache = new ConcurrentHashMap<>(); + protected final DB db; + protected final Map entityPersisters; + protected final Map collectionPersisters; private static enum DB { OTHER, @@ -69,11 +77,9 @@ private static enum DB { MSSQL; } - public HibernateJpaProvider(EntityManager em, String dbms, Map entityPersisters, Map collectionPersisters) { + public HibernateJpaProvider(String dbms, Map entityPersisters, Map collectionPersisters) { try { - if (em == null) { - db = DB.OTHER; - } else if ("mysql".equals(dbms)) { + if ("mysql".equals(dbms)) { db = DB.MY_SQL; } else if ("db2".equals(dbms)) { db = DB.DB2; @@ -290,24 +296,17 @@ public boolean isColumnShared(EntityType ownerType, String attributeName) { return false; } - ColumnSharingCacheKey cacheKey = new ColumnSharingCacheKey(ownerType, attributeName); - Boolean result = columnSharingCache.get(cacheKey); - if (result != null) { - return result; - } - if (persister instanceof SingleTableEntityPersister) { SingleTableEntityPersister singleTableEntityPersister = (SingleTableEntityPersister) persister; SingleTableEntityPersister rootPersister = (SingleTableEntityPersister) entityPersisters.get(singleTableEntityPersister.getRootEntityName()); - result = isColumnShared(singleTableEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); + return isColumnShared(singleTableEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); } else if (persister instanceof UnionSubclassEntityPersister) { UnionSubclassEntityPersister unionSubclassEntityPersister = (UnionSubclassEntityPersister) persister; UnionSubclassEntityPersister rootPersister = (UnionSubclassEntityPersister) entityPersisters.get(unionSubclassEntityPersister.getRootEntityName()); - result = isColumnShared(unionSubclassEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); + return isColumnShared(unionSubclassEntityPersister, rootPersister.getName(), rootPersister.getSubclassClosure(), attributeName); } - columnSharingCache.put(cacheKey, result); - return result; + return false; } @Override @@ -324,12 +323,16 @@ public ConstraintType requiresTreatFilter(EntityType ownerType, String attrib } // When the inner treat joined element is collection, we always need the constraint, see HHH-??? TODO: report issue - if (joinType == JoinType.INNER && propertyType instanceof CollectionType) { - if (isForeignKeyDirectionToParent((CollectionType) propertyType)) { - return ConstraintType.WHERE; - } + if (propertyType instanceof CollectionType) { + if (joinType == JoinType.INNER) { + if (isForeignKeyDirectionToParent((CollectionType) propertyType)) { + return ConstraintType.WHERE; + } - return ConstraintType.ON; + return ConstraintType.ON; + } else if (!((CollectionType) propertyType).getElementType(persister.getFactory()).isEntityType()) { + return ConstraintType.NONE; + } } String propertyEntityName = ((AssociationType) propertyType).getAssociatedEntityName(persister.getFactory()); @@ -468,7 +471,89 @@ protected String getMappedBy(CollectionPersister persister) { } @Override - public String getJoinTable(EntityType ownerType, String attributeName) { + public String[] getColumnNames(EntityType entityType, String attributeName) { + try { + return ((AbstractEntityPersister) entityPersisters.get(entityType.getJavaType().getName())).getPropertyColumnNames(attributeName); + } catch (MappingException e) { + throw new RuntimeException("Unknown property [" + attributeName + "] of entity [" + entityType.getJavaType() + "]", e); + } + } + + @Override + public String[] getColumnTypes(EntityType entityType, String attributeName) { + AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityPersisters.get(entityType.getJavaType().getName()); + SessionFactoryImplementor sfi = entityPersister.getFactory(); + String[] columnNames = entityPersister.getPropertyColumnNames(attributeName); + Database database = sfi.getServiceRegistry().locateServiceBinding(Database.class).getService(); + Table[] tables; + + if (entityPersister instanceof JoinedSubclassEntityPersister) { + tables = new Table[((JoinedSubclassEntityPersister) entityPersister).getTableSpan()]; + for (int i = 0; i < tables.length; i++) { + tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); + } + } else if (entityPersister instanceof UnionSubclassEntityPersister) { + tables = new Table[((UnionSubclassEntityPersister) entityPersister).getTableSpan()]; + for (int i = 0; i < tables.length; i++) { + tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); + } + } else if (entityPersister instanceof SingleTableEntityPersister) { + tables = new Table[((SingleTableEntityPersister) entityPersister).getTableSpan()]; + for (int i = 0; i < tables.length; i++) { + tables[i] = database.getTable(entityPersister.getSubclassTableName(i)); + } + } else { + tables = new Table[] { database.getTable(entityPersister.getTableName()) }; + } + + // In this case, the property might represent a formula + if (columnNames.length == 1 && columnNames[0] == null) { + Type propertyType = entityPersister.getPropertyType(attributeName); + long length; + int precision; + int scale; + try { + Method m = Type.class.getMethod("dictatedSizes", Mapping.class); + Object size = ((Object[]) m.invoke(propertyType, sfi))[0]; + length = (long) size.getClass().getMethod("getLength").invoke(size); + precision = (int) size.getClass().getMethod("getPrecision").invoke(size); + scale = (int) size.getClass().getMethod("getScale").invoke(size); + } catch (Exception ex) { + throw new RuntimeException("Could not determine the column type of the attribute: " + attributeName + " of the entity: " + entityType.getName()); + } + + return new String[] { + sfi.getDialect().getTypeName( + propertyType.sqlTypes(sfi)[0], + length, + precision, + scale + ) + }; + } + + String[] columnTypes = new String[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + Column column = null; + for (int j = 0; j < tables.length; j++) { + column = tables[j].getColumn(new Column(columnNames[i])); + if (column != null) { + break; + } + } + + if (column == null) { + throw new IllegalArgumentException("Could not find column '" + columnNames[i] + "' in for entity: " + entityType.getName()); + } + + columnTypes[i] = column.getSqlType(); + } + + return columnTypes; + } + + @Override + public JoinTable getJoinTable(EntityType ownerType, String attributeName) { String ownerTypeName = ownerType.getJavaType().getName(); StringBuilder sb = new StringBuilder(ownerTypeName.length() + attributeName.length() + 1); sb.append(ownerType.getJavaType().getName()); @@ -480,34 +565,92 @@ public String getJoinTable(EntityType ownerType, String attributeName) { QueryableCollection queryableCollection = (QueryableCollection) persister; if (!queryableCollection.getElementType().isEntityType()) { - return queryableCollection.getTableName(); + String[] targetColumnMetaData = queryableCollection.getElementColumnNames(); + Map targetColumnMapping = new HashMap<>(); + + for (int i = 0; i < targetColumnMetaData.length; i++) { + targetColumnMapping.put(targetColumnMetaData[i], targetColumnMetaData[i]); + } + return createJoinTable(queryableCollection, targetColumnMapping); } else if (queryableCollection.getElementPersister() instanceof Joinable) { String elementTableName = ((Joinable) queryableCollection.getElementPersister()).getTableName(); if (!queryableCollection.getTableName().equals(elementTableName)) { - return queryableCollection.getTableName(); + String[] targetColumnMetaData = queryableCollection.getElementColumnNames(); + String[] targetPrimaryKeyColumnMetaData = ((AbstractEntityPersister) queryableCollection.getElementPersister()).getKeyColumnNames(); + Map targetIdColumnMapping = new HashMap<>(); + + for (int i = 0; i < targetColumnMetaData.length; i++) { + targetIdColumnMapping.put(targetColumnMetaData[i], targetPrimaryKeyColumnMetaData[i]); + } + return createJoinTable(queryableCollection, targetIdColumnMapping); } } } return null; } + private JoinTable createJoinTable(QueryableCollection queryableCollection, Map targetColumnMapping) { + String[] indexColumnNames = queryableCollection.getIndexColumnNames(); + Map keyColumnMapping = null; + if (indexColumnNames != null) { + keyColumnMapping = new HashMap<>(indexColumnNames.length); + if (queryableCollection.getKeyType().isEntityType()) { + throw new IllegalArgumentException("Determining the join table key foreign key mappings is not yet supported!"); + } else { + keyColumnMapping.put(indexColumnNames[0], indexColumnNames[0]); + } + } + String[] primaryKeyColumnMetaData = ((AbstractEntityPersister) queryableCollection.getOwnerEntityPersister()).getKeyColumnNames(); + String[] foreignKeyColumnMetaData = queryableCollection.getKeyColumnNames(); + Map idColumnMapping = new HashMap<>(primaryKeyColumnMetaData.length); + for (int i = 0; i < foreignKeyColumnMetaData.length; i++) { + idColumnMapping.put(foreignKeyColumnMetaData[i], primaryKeyColumnMetaData[i]); + } + + return new JoinTable( + queryableCollection.getTableName(), + idColumnMapping, + keyColumnMapping, + targetColumnMapping + ); + } + @Override public boolean isBag(EntityType ownerType, String attributeName) { - Attribute attribute = getAttribute(ownerType, attributeName); - if (attribute instanceof PluralAttribute) { - PluralAttribute.CollectionType collectionType = ((PluralAttribute) attribute).getCollectionType(); - if (collectionType == PluralAttribute.CollectionType.COLLECTION) { - return true; - } else if (collectionType == PluralAttribute.CollectionType.LIST) { - String ownerTypeName = ownerType.getJavaType().getName(); - StringBuilder sb = new StringBuilder(ownerTypeName.length() + attributeName.length() + 1); - sb.append(ownerType.getJavaType().getName()); - sb.append('.'); - sb.append(attributeName); - - CollectionPersister persister = collectionPersisters.get(sb.toString()); - return !persister.hasIndex(); - } + CollectionPersister persister = null; + IdentifiableType type = ownerType; + StringBuilder sb = new StringBuilder(ownerType.getJavaType().getName().length() + attributeName.length() + 1); + while (persister == null && type != null) { + sb.setLength(0); + sb.append(type.getJavaType().getName()); + sb.append('.'); + sb.append(attributeName); + persister = collectionPersisters.get(sb.toString()); + type = type.getSupertype(); + } + + return persister != null && !persister.hasIndex() && !persister.isInverse() && !(getAttribute(ownerType, attributeName) instanceof SetAttribute); + } + + @Override + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { + AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + return entityMetamodel.getCascadeStyles()[index].hasOrphanDelete(); + } + + return false; + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityPersisters.get(ownerType.getJavaType().getName()); + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + return entityMetamodel.getCascadeStyles()[index].doCascade(CascadingAction.DELETE); } return false; @@ -526,13 +669,19 @@ public boolean containsEntity(EntityManager em, Class entityClass, Object id) return ownerType.getAttribute(attributeName); } ManagedType t = ownerType; - SingularAttribute attr = null; + Attribute attr = null; String[] parts = attributeName.split("\\."); for (int i = 0; i < parts.length; i++) { - attr = t.getSingularAttribute(parts[i]); - if (attr.getType().getPersistenceType() != javax.persistence.metamodel.Type.PersistenceType.BASIC) { - t = (ManagedType) attr.getType(); - } else if (i + 1 != parts.length) { + attr = t.getAttribute(parts[i]); + if (i + 1 != parts.length) { + if (attr instanceof SingularAttribute) { + SingularAttribute singularAttribute = (SingularAttribute) attr; + if (singularAttribute.getType().getPersistenceType() != javax.persistence.metamodel.Type.PersistenceType.BASIC) { + t = (ManagedType) singularAttribute.getType(); + continue; + } + } + throw new IllegalArgumentException("Illegal attribute name for type [" + ownerType.getJavaType().getName() + "]: " + attributeName); } } @@ -576,38 +725,14 @@ public boolean needsTypeConstraintForColumnSharing() { return true; } - private static class ColumnSharingCacheKey { - private final ManagedType managedType; - private final String attributeName; - - public ColumnSharingCacheKey(ManagedType managedType, String attributeName) { - this.managedType = managedType; - this.attributeName = attributeName; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - ColumnSharingCacheKey that = (ColumnSharingCacheKey) o; - - if (!managedType.equals(that.managedType)) { - return false; - } - return attributeName.equals(that.attributeName); - } + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return false; + } - @Override - public int hashCode() { - int result = managedType.hashCode(); - result = 31 * result + attributeName.hashCode(); - return result; - } + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return true; } } diff --git a/integration/openjpa/src/main/java/com/blazebit/persistence/impl/openjpa/OpenJPAJpaProvider.java b/integration/openjpa/src/main/java/com/blazebit/persistence/impl/openjpa/OpenJPAJpaProvider.java index cf6f4b0183..a5d72c4ccc 100644 --- a/integration/openjpa/src/main/java/com/blazebit/persistence/impl/openjpa/OpenJPAJpaProvider.java +++ b/integration/openjpa/src/main/java/com/blazebit/persistence/impl/openjpa/OpenJPAJpaProvider.java @@ -17,10 +17,12 @@ package com.blazebit.persistence.impl.openjpa; import com.blazebit.persistence.JoinType; +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.spi.JpaProvider; import javax.persistence.EntityManager; import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.ManagedType; import java.util.Map; /** @@ -31,6 +33,8 @@ */ public class OpenJPAJpaProvider implements JpaProvider { + private static final String[] EMPTY = {}; + @Override public boolean supportsJpa21() { return false; @@ -179,7 +183,17 @@ public Map getWritableMappedByMappings(EntityType inverseType } @Override - public String getJoinTable(EntityType ownerType, String attributeName) { + public String[] getColumnNames(EntityType ownerType, String attributeName) { + return EMPTY; + } + + @Override + public String[] getColumnTypes(EntityType ownerType, String attributeName) { + return EMPTY; + } + + @Override + public JoinTable getJoinTable(EntityType ownerType, String attributeName) { // just return null since we don't need that for openjpa anyway return null; } @@ -189,6 +203,16 @@ public boolean isBag(EntityType ownerType, String attributeName) { return false; } + @Override + public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { + return false; + } + + @Override + public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { + return false; + } + @Override public boolean containsEntity(EntityManager em, Class entityClass, Object id) { throw new UnsupportedOperationException("Not yet implemented!"); @@ -228,4 +252,14 @@ public boolean needsBrokenAssociationToIdRewriteInOnClause() { public boolean needsTypeConstraintForColumnSharing() { return false; } + + @Override + public boolean supportsCollectionTableCleanupOnDelete() { + return false; + } + + @Override + public boolean supportsJoinTableCleanupOnDelete() { + return false; + } } diff --git a/integration/spring-data/src/test/java/com/blazebit/persistence/spring/data/impl/entity/Document.java b/integration/spring-data/src/test/java/com/blazebit/persistence/spring/data/impl/entity/Document.java index 54c733ac68..3f79214bdc 100644 --- a/integration/spring-data/src/test/java/com/blazebit/persistence/spring/data/impl/entity/Document.java +++ b/integration/spring-data/src/test/java/com/blazebit/persistence/spring/data/impl/entity/Document.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.spring.data.impl.entity; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -75,7 +76,7 @@ public void setAge(long age) { this.age = age; } - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) public Person getOwner() { return owner; } diff --git a/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java b/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java index 00814eed81..8dbd87b10d 100644 --- a/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java +++ b/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java @@ -16,6 +16,7 @@ package com.blazebit.persistence.testsuite.base; +import com.blazebit.persistence.spi.JoinTable; import org.hibernate.dialect.SQLServer2012Dialect; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.persister.entity.AbstractEntityPersister; @@ -168,7 +169,11 @@ public String tableFromEntity(Class entityClass) { @Override public String tableFromEntityRelation(Class entityClass, String relationName) { - return jpaProvider.getJoinTable(em.getMetamodel().entity(entityClass), relationName); + JoinTable joinTable = jpaProvider.getJoinTable(em.getMetamodel().entity(entityClass), relationName); + if (joinTable != null) { + return joinTable.getTableName(); + } + return null; } }; } diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/AbstractJpaPersistenceTest.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/AbstractJpaPersistenceTest.java index 4afaf4a78a..9a2d937e95 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/AbstractJpaPersistenceTest.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/AbstractJpaPersistenceTest.java @@ -19,37 +19,8 @@ import com.blazebit.persistence.Criteria; import com.blazebit.persistence.CriteriaBuilderFactory; import com.blazebit.persistence.spi.CriteriaBuilderConfiguration; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.URL; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import javax.persistence.EntityManager; -import javax.persistence.EntityManagerFactory; -import javax.persistence.PersistenceException; -import javax.persistence.metamodel.EntityType; -import javax.persistence.metamodel.Metamodel; -import javax.persistence.metamodel.PluralAttribute; -import javax.persistence.spi.PersistenceProvider; -import javax.persistence.spi.PersistenceProviderResolver; -import javax.persistence.spi.PersistenceProviderResolverHolder; -import javax.persistence.spi.PersistenceUnitInfo; -import javax.persistence.spi.PersistenceUnitTransactionType; -import javax.sql.DataSource; - +import com.blazebit.persistence.spi.JoinTable; import com.blazebit.persistence.spi.JpaProvider; -import com.blazebit.persistence.spi.JpaProviderFactory; import com.blazebit.persistence.testsuite.base.assertion.AssertStatementBuilder; import com.blazebit.persistence.testsuite.base.cleaner.DB2DatabaseCleaner; import com.blazebit.persistence.testsuite.base.cleaner.DatabaseCleaner; @@ -69,6 +40,35 @@ import org.junit.Before; import org.junit.BeforeClass; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceException; +import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.persistence.spi.PersistenceUnitTransactionType; +import javax.sql.DataSource; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.logging.LogManager; +import java.util.logging.Logger; + /** * * @author Christian Beikov @@ -165,9 +165,10 @@ protected void clearCollections(EntityManager em, Class... entityClasses) { EntityType t = entityMetamodel.entity(c); deletes = new ArrayList<>(); for (PluralAttribute pluralAttribute : t.getPluralAttributes()) { - String joinTable = jpaProvider.getJoinTable(t, pluralAttribute.getName()); + JoinTable joinTable = jpaProvider.getJoinTable(t, pluralAttribute.getName()); + if (joinTable != null) { - deletes.add("delete from " + joinTable); + deletes.add("delete from " + joinTable.getTableName()); } } PLURAL_DELETES.put(c, deletes); @@ -289,7 +290,7 @@ public void clearData(Connection connection) { CriteriaBuilderConfiguration config = Criteria.getDefault(); config = configure(config); cbf = config.createCriteriaBuilderFactory(emf); - jpaProvider = cbf.getService(JpaProviderFactory.class).createJpaProvider(em); + jpaProvider = cbf.getService(JpaProvider.class); if (schemaChanged || !databaseCleaner.supportsClearSchema()) { recreateOrClearSchema(); @@ -542,6 +543,10 @@ public AssertStatementBuilder assertQuerySequence() { return new AssertStatementBuilder(getRelationalModelAccessor(), QueryInspectorListener.EXECUTED_QUERIES); } + public AssertStatementBuilder assertUnorderedQuerySequence() { + return assertQuerySequence().unordered(); + } + protected RelationalModelAccessor getRelationalModelAccessor() { return null; } diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AbstractAssertStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AbstractAssertStatementBuilder.java index c69a38f0e1..a23d740c06 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AbstractAssertStatementBuilder.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AbstractAssertStatementBuilder.java @@ -49,6 +49,15 @@ public T forEntity(Class entityClass) { return (T) this; } + public T forRelation(Class entityClass, String relationName) { + String table = tableFromEntityRelation(entityClass, relationName); + if (table != null) { + tables.add(table); + } + + return (T) this; + } + public AssertStatementBuilder and() { parentBuilder.unsetCurrentBuilder(this); parentBuilder.addStatement(build()); diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertDeleteStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertDeleteStatementBuilder.java index 16be916a46..f293ecbc45 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertDeleteStatementBuilder.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertDeleteStatementBuilder.java @@ -29,15 +29,6 @@ public AssertDeleteStatementBuilder(AssertStatementBuilder parentBuilder, Relati super(parentBuilder, relationalModelAccessor); } - public AssertDeleteStatementBuilder forRelation(Class entityClass, String relationName) { - String table = tableFromEntityRelation(entityClass, relationName); - if (table != null) { - tables.add(table); - } - - return this; - } - @Override protected AssertStatement build() { return new AssertDeleteStatement(tables); diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertInsertStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertInsertStatementBuilder.java index c854a2a74c..fd36adb228 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertInsertStatementBuilder.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertInsertStatementBuilder.java @@ -29,15 +29,6 @@ public AssertInsertStatementBuilder(AssertStatementBuilder parentBuilder, Relati super(parentBuilder, relationalModelAccessor); } - public AssertInsertStatementBuilder forRelation(Class entityClass, String relationName) { - String table = tableFromEntityRelation(entityClass, relationName); - if (table != null) { - tables.add(table); - } - - return this; - } - @Override protected AssertStatement build() { return new AssertInsertStatement(tables); diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatement.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatement.java new file mode 100644 index 0000000000..97a5fe6689 --- /dev/null +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatement.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.testsuite.base.assertion; + +import org.opentest4j.MultipleFailuresError; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class AssertMultiStatement implements AssertStatement { + + private final List statements; + private final List failures = new ArrayList<>(); + private int validations; + + public AssertMultiStatement(List statements) { + this.statements = statements; + this.validations = statements.size(); + } + + @Override + public void validate(String query) { + validations--; + List tryFailures = new ArrayList<>(); + boolean failed = true; + for (int i = 0; i < statements.size(); i++) { + try { + statements.get(i).validate(query); + statements.remove(i); + failed = false; + break; + } catch (Throwable t) { + tryFailures.add(t); + } + } + if (failed) { + failures.addAll(tryFailures); + } + + if (validations == 0 && !failures.isEmpty()) { + throw new MultipleFailuresError("Multiple query validations failed", failures); + } + } + +} diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatementBuilder.java new file mode 100644 index 0000000000..a91bb6c988 --- /dev/null +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertMultiStatementBuilder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed 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 com.blazebit.persistence.testsuite.base.assertion; + +import com.blazebit.persistence.testsuite.base.RelationalModelAccessor; + +/** + * + * @author Christian Beikov + * @since 1.2.0 + */ +public class AssertMultiStatementBuilder extends AssertStatementBuilder { + + private final AssertStatementBuilder parentBuilder; + + public AssertMultiStatementBuilder(AssertStatementBuilder parentBuilder, RelationalModelAccessor relationalModelAccessor) { + super(relationalModelAccessor, null); + this.parentBuilder = parentBuilder; + parentBuilder.setCurrentBuilder(this); + } + + @Override + public AssertMultiStatementBuilder unordered() { + throw new IllegalStateException("Multiple nested unordered statement asserters aren't supported!"); + } + + public AssertStatementBuilder and() { + validateBuilderEnded(); + parentBuilder.unsetCurrentBuilder(this); + // Add the same multi-statement instead of the real statements + // The multi-statement will "consume" the given statements unordered + AssertMultiStatement assertMultiStatement = new AssertMultiStatement(statements); + for (int i = 0; i < statements.size(); i++) { + parentBuilder.addStatement(assertMultiStatement); + } + return parentBuilder; + } + + public void validate() { + and().validate(); + } +} diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertStatementBuilder.java index cd299897ce..843b72772f 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertStatementBuilder.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertStatementBuilder.java @@ -30,10 +30,10 @@ */ public class AssertStatementBuilder { - private final RelationalModelAccessor relationalModelAccessor; - private final List queries; - private final List statements = new ArrayList<>(); - private Object currentBuilder; + protected final RelationalModelAccessor relationalModelAccessor; + protected final List queries; + protected final List statements = new ArrayList<>(); + protected Object currentBuilder; public AssertStatementBuilder(RelationalModelAccessor relationalModelAccessor, List queries) { this.relationalModelAccessor = relationalModelAccessor; @@ -57,6 +57,16 @@ void unsetCurrentBuilder(Object currentBuilder) { this.currentBuilder = null; } + void validateBuilderEnded() { + if (this.currentBuilder != null) { + throw new IllegalStateException("Unfinished builder: " + currentBuilder); + } + } + + public AssertMultiStatementBuilder unordered() { + return new AssertMultiStatementBuilder(this, relationalModelAccessor); + } + public AssertStatementBuilder select() { return assertSelect().and(); } @@ -134,7 +144,11 @@ public void validate() { try { statements.get(i).validate(queries.get(i)); } catch (Throwable t) { - failures.add(t); + if (t instanceof MultipleFailuresError) { + failures.addAll(((MultipleFailuresError) t).getFailures()); + } else { + failures.add(t); + } } } diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertUpdateStatementBuilder.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertUpdateStatementBuilder.java index 0224d4a652..12ef5ca4bc 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertUpdateStatementBuilder.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/assertion/AssertUpdateStatementBuilder.java @@ -29,15 +29,6 @@ public AssertUpdateStatementBuilder(AssertStatementBuilder parentBuilder, Relati super(parentBuilder, relationalModelAccessor); } - public AssertUpdateStatementBuilder forRelation(Class entityClass, String relationName) { - String table = tableFromEntityRelation(entityClass, relationName); - if (table != null) { - tables.add(table); - } - - return this; - } - @Override protected AssertStatement build() { return new AssertUpdateStatement(tables);