From 4f1f2dafc69dbaee085fa4545bebd730e4884176 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Sun, 18 Nov 2018 18:30:57 +0100 Subject: [PATCH] [#681] Allow multiple non-cascading parents for updatable entity views --- CHANGELOG.md | 2 + .../persistence/impl/JoinManager.java | 45 ++- .../blazebit/persistence/impl/JoinNode.java | 2 - .../impl/query/CustomQuerySpecification.java | 2 +- .../testsuite/entity/Document.java | 10 +- .../en_US/08_updatable_entity_views.adoc | 94 ++++-- .../DirtyStateViewAttributeAccessor.java | 6 +- .../ListCollectionInstantiator.java | 6 +- .../OrderedCollectionInstantiator.java | 6 +- .../collection/OrderedMapInstantiator.java | 6 +- .../OrderedSetCollectionInstantiator.java | 6 +- .../impl/collection/RecordingCollection.java | 34 ++- .../view/impl/collection/RecordingList.java | 4 +- .../view/impl/collection/RecordingMap.java | 50 +++- .../collection/RecordingNavigableMap.java | 4 +- .../collection/RecordingNavigableSet.java | 4 +- .../view/impl/collection/RecordingSet.java | 8 +- .../impl/collection/RecordingSortedMap.java | 4 +- .../impl/collection/RecordingSortedSet.java | 4 +- .../collection/SortedMapInstantiator.java | 6 +- .../SortedSetCollectionInstantiator.java | 6 +- .../collection/UnorderedMapInstantiator.java | 6 +- .../UnorderedSetCollectionInstantiator.java | 6 +- .../entity/AbstractViewToEntityMapper.java | 6 + .../entity/LoadOnlyViewToEntityMapper.java | 5 + .../view/impl/entity/ViewToEntityMapper.java | 1 + .../impl/metamodel/AbstractAttribute.java | 20 +- .../metamodel/AbstractMethodAttribute.java | 20 ++ .../AbstractMethodPluralAttribute.java | 17 +- .../AbstractMethodSingularAttribute.java | 19 +- .../metamodel/AbstractParameterAttribute.java | 5 + .../view/impl/metamodel/AttributeMapping.java | 7 +- .../metamodel/MethodAttributeMapping.java | 40 ++- .../metamodel/ParameterAttributeMapping.java | 6 + .../view/impl/proxy/DirtyTracker.java | 2 + .../impl/proxy/MutableStateTrackable.java | 13 + .../view/impl/proxy/ProxyFactory.java | 190 +++++++++++- .../impl/update/InitialStateResetter.java | 6 +- .../ResetInitialStateSynchronization.java | 38 ++- .../flush/AbstractPluralAttributeFlusher.java | 5 + .../update/flush/BasicAttributeFlusher.java | 5 + .../flush/CollectionAttributeFlusher.java | 5 + .../CollectionElementAttributeFlusher.java | 5 + .../flush/CompositeAttributeFlusher.java | 129 +++++++-- .../update/flush/DirtyAttributeFlusher.java | 2 + .../flush/EmbeddableAttributeFlusher.java | 5 + .../update/flush/SubviewAttributeFlusher.java | 5 + ...ateCorrelatedUpdatableOnlySubviewTest.java | 49 +--- .../EntityViewUpdateRollbackTest.java | 39 ++- .../model/CreatePersonRollbackView.java | 34 +++ .../update/rollback/model/PersonNameView.java | 37 +++ .../model/UpdatableDocumentRollbackView.java | 7 + ...ateSubviewInverseEmbeddedComplexTest.java} | 4 +- .../model/UpdatableLegacyOrderView.java | 3 +- .../model/UpdatableLegacyOrderView.java | 3 +- ...dateSubviewInverseEmbeddedSimpleTest.java} | 4 +- .../model/UpdatableLegacyOrderView.java | 3 +- ...dateSubviewInverseOneToOneEntityTest.java} | 4 +- ...dateSubviewInverseOneToOneSimpleTest.java} | 4 +- ...tyViewUpdateSubviewInverseSimpleTest.java} | 4 +- .../simple/model/UpdatablePersonView.java | 3 +- .../unmapped/model/UpdatableDocumentView.java | 5 +- ...ewUpdateSubviewMultiParentInverseTest.java | 252 ++++++++++++++++ .../inverse/model/DocumentIdView.java | 34 +++ .../multiparent/inverse/model/PersonView.java | 32 ++ .../model/UpdatableDocumentWithGraphView.java | 57 ++++ .../inverse/model/UpdatablePersonView.java | 36 +++ ...UpdateSubviewMultiParentJoinTableTest.java | 274 ++++++++++++++++++ .../jointable/model/DocumentIdView.java | 34 +++ .../jointable/model/PersonView.java | 32 ++ .../model/UpdatableDocumentWithGraphView.java | 56 ++++ .../jointable/model/UpdatablePersonView.java | 36 +++ ...edUpdatableOnlySubviewCollectionsTest.java | 92 +----- ...ateNestedUpdatableOnlySubviewMapsTest.java | 96 +----- ...wUpdateNestedUpdatableOnlySubviewTest.java | 54 +--- ...leUpdatableOnlySubviewCollectionsTest.java | 64 +--- ...ateSimpleUpdatableOnlySubviewMapsTest.java | 63 +--- ...wUpdateSimpleUpdatableOnlySubviewTest.java | 42 +-- .../hibernate/base/HibernateJpaProvider.java | 2 +- 79 files changed, 1777 insertions(+), 559 deletions(-) create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/CreatePersonRollbackView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/PersonNameView.java rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/{EntityViewUpdateSubviewInverseEmbeddedTest.java => EntityViewUpdateSubviewInverseEmbeddedComplexTest.java} (97%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/{EntityViewUpdateSubviewInverseEmbeddedTest.java => EntityViewUpdateSubviewInverseEmbeddedSimpleTest.java} (97%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/{EntityViewUpdateSubviewInverseEmbeddedTest.java => EntityViewUpdateSubviewInverseOneToOneEntityTest.java} (96%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/{EntityViewUpdateSubviewInverseEmbeddedTest.java => EntityViewUpdateSubviewInverseOneToOneSimpleTest.java} (96%) rename entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/{EntityViewUpdateSubviewInverseEmbeddedTest.java => EntityViewUpdateSubviewInverseSimpleTest.java} (93%) create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/EntityViewUpdateSubviewMultiParentInverseTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/DocumentIdView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/PersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatableDocumentWithGraphView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatablePersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/EntityViewUpdateSubviewMultiParentJoinTableTest.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/DocumentIdView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/PersonView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatableDocumentWithGraphView.java create mode 100644 entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatablePersonView.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eb9e80c89..91dbff0e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ Not yet released * Implemented creatability validation for creatable entity views * Implemented `SET_NULL` inverse remove strategy validation for updatable entity views * Add updatable entity view support for inverse collections without a mapped by i.e. explicit join columns +* Rewrtite implicit joins in `ON` clause automatically to subqueries +* Add support for multiple non-cascading parent objects for updatable entity views ### Bug fixes 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 2b705396e4..f8739ea3b8 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 @@ -1111,7 +1111,8 @@ private JoinNode renderJoinNode(StringBuilder sb, JoinAliasInfo joinBase, JoinNo sb.append(node.getAliasInfo().getAlias()); renderedJoins.add(node); - boolean onClause = valuesNode != null || node.getOnPredicate() != null && !node.getOnPredicate().getChildren().isEmpty() || onCondition != null; + boolean realOnClause = node.getOnPredicate() != null && !node.getOnPredicate().getChildren().isEmpty() || onCondition != null; + boolean onClause = valuesNode != null || realOnClause; if (onClause) { sb.append(joinRestrictionKeyword); @@ -1125,7 +1126,9 @@ private JoinNode renderJoinNode(StringBuilder sb, JoinAliasInfo joinBase, JoinNo if (valuesNode != null) { if (!externalRepresentation) { renderValuesClausePredicate(sb, valuesNode, valuesNode.getAlias(), externalRepresentation); - sb.append(" AND "); + if (realOnClause) { + sb.append(" AND "); + } } valuesNode = null; } @@ -2185,7 +2188,9 @@ private boolean isSingleValuedAssociationId(JoinResult joinResult, List) { // Skip the foreign join column check for map keys // They aren't allowed as join sources in the JPA providers yet so we can only render them directly @@ -2193,18 +2198,24 @@ private boolean isSingleValuedAssociationId(JoinResult joinResult, List) { + if (baseType instanceof EmbeddableType) { if (node.getParentTreeNode() == null) { - attributePath = node.getValuesLikeAttribute() + "." + attributePath; + fullAttributePath = node.getValuesLikeAttribute() + "." + fullAttributePath; + elementCollectionPath = node.isValueClazzAttributeSingular() ? null : node.getValuesLikeAttribute(); baseType = node.getValueType(); - break; + } else { + if (node.getParentTreeNode().getAttribute().isCollection()) { + elementCollectionPath = node.getParentTreeNode().getRelationName(); + } else { + attributePath = node.getParentTreeNode().getRelationName() + "." + attributePath; + } + fullAttributePath = node.getParentTreeNode().getRelationName() + "." + fullAttributePath; + node = node.getParent(); + baseType = node.getNodeType(); } - attributePath = node.getParentTreeNode().getRelationName() + "." + attributePath; - node = node.getParent(); - baseType = node.getNodeType(); } - if (mainQuery.jpaProvider.isForeignJoinColumn((EntityType) baseType, attributePath)) { + if (mainQuery.jpaProvider.isForeignJoinColumn((EntityType) baseType, fullAttributePath)) { return false; } } else if (mainQuery.jpaProvider.isForeignJoinColumn((EntityType) baseType, maybeSingularAssociation.getName())) { @@ -2213,10 +2224,18 @@ private boolean isSingleValuedAssociationId(JoinResult joinResult, List managedType = metamodel.getManagedType(ExtendedManagedType.class, JpaMetamodelUtils.getTypeName(baseType)); - ExtendedAttribute associationAttribute = managedType.getOwnedSingularAttributes().get(attributePath); - return managedType.getOwnedSingularAttributes().containsKey(attributePath + "." + maybeSingularAssociationIdExpression) - && associationAttribute != null - && (contains(metamodel.getManagedType(ExtendedManagedType.class, associationAttribute.getElementClass()).getIdAttributes(), maybeSingularAssociationIdExpression) || mainQuery.jpaProvider.supportsSingleValuedAssociationNaturalIdExpressions()); + if (elementCollectionPath == null) { + ExtendedAttribute associationAttribute = managedType.getOwnedSingularAttributes().get(fullAttributePath); + return managedType.getOwnedSingularAttributes().containsKey(fullAttributePath + "." + maybeSingularAssociationIdExpression) + && associationAttribute != null + && (contains(metamodel.getManagedType(ExtendedManagedType.class, associationAttribute.getElementClass()).getIdAttributes(), maybeSingularAssociationIdExpression) || mainQuery.jpaProvider.supportsSingleValuedAssociationNaturalIdExpressions()); + } else { + // We assume that associations within element collections are always owned + ExtendedAttribute associationAttribute = managedType.getAttributes().get(fullAttributePath); + return !mainQuery.jpaProvider.needsElementCollectionIdCutoff() && managedType.getAttributes().containsKey(fullAttributePath + "." + maybeSingularAssociationIdExpression) + && associationAttribute != null + && (contains(metamodel.getManagedType(ExtendedManagedType.class, associationAttribute.getElementClass()).getIdAttributes(), maybeSingularAssociationIdExpression) || mainQuery.jpaProvider.supportsSingleValuedAssociationNaturalIdExpressions()); + } } private static boolean contains(Set> attributes, PathElementExpression expression) { 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 03c3754775..a06ce23acd 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 @@ -39,7 +39,6 @@ import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.ManagedType; -import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import java.util.ArrayList; import java.util.Arrays; @@ -841,7 +840,6 @@ public void appendDeReference(StringBuilder sb, String property, boolean renderT appendAlias(sb, renderTreat, externalRepresentation); // If we have a valuesTypeName, the property can only be "value" which is already handled in appendAlias if (property != null && valuesTypeName == null) { - Set> idAttributes; if (requiresElementCollectionIdCutoff && parentTreeNode != null && parentTreeNode.getAttribute().getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION && property.endsWith(".id")) { // See https://hibernate.atlassian.net/browse/HHH-13045 for details 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 fbe5ac8cf8..aa40725a92 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 @@ -397,7 +397,7 @@ protected StringBuilder applySqlTransformations(String sqlQuery) { int subselectIndex = sb.indexOf(subselectTableExpr, 0); if (subselectIndex == -1) { // this is probably a VALUES clause for an entity type - int syntheticPredicateStart = sb.indexOf(syntheticPredicate, SqlUtils.indexOfWhere(sb)); + int syntheticPredicateStart = sb.indexOf(syntheticPredicate, SqlUtils.indexOfFrom(sb)); int end = syntheticPredicateStart + syntheticPredicate.length(); if (sb.indexOf(andSeparator, end) == end) { sb.replace(syntheticPredicateStart, end + andSeparator.length(), ""); 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 675cb0d7a0..8faead0a87 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 @@ -210,7 +210,7 @@ public void setWrappedByteArray(Byte[] wrappedByteArray) { this.wrappedByteArray = wrappedByteArray; } - @OneToMany(mappedBy = "partnerDocument") + @OneToMany(mappedBy = "partnerDocument", cascade = CascadeType.PERSIST) public Set getPartners() { return partners; } @@ -260,8 +260,8 @@ public Map getContacts() { return contacts; } - public void setContacts(Map localized) { - this.contacts = localized; + public void setContacts(Map contacts) { + this.contacts = contacts; } @OneToMany @@ -271,8 +271,8 @@ public Map getContacts2() { return contacts2; } - public void setContacts2(Map localized) { - this.contacts2 = localized; + public void setContacts2(Map contacts2) { + this.contacts2 = contacts2; } @OneToMany diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/08_updatable_entity_views.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/08_updatable_entity_views.adoc index 45ecfdb652..bb05877fbf 100644 --- a/documentation/src/main/asciidoc/entity-view/manual/en_US/08_updatable_entity_views.adoc +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/08_updatable_entity_views.adoc @@ -5,8 +5,8 @@ except that changes to attributes are tracked and can be inspected through the < Updatable entity views are also a lot like normal entities and can be thought of being similar to what is sometimes referred to as _sub-entities_. The main idea is to model use-case specific representations with a limited scope of attributes that can change. -Usually, when using an entity type, many more attributes are exposed as being _changable_ to the consumer of the type, although they might not even need to be _updatable_. -Updatable entity views allows for perfect reuse of attribute declarations thanks to it's use of interfaces but also brings a lot more to the table than using plain entities. +Usually, when using an entity type, many more attributes are exposed as being _changable_ to the consumer of the type, although they might not even need to be always _changeable_. +Updatable entity views allow for perfect reuse of attribute declarations thanks to it's use of interfaces but also brings a lot more to the table than using plain entities. Apart from a concept for updating existing objects, {projectname} also has a notion for _creating_ new objects. With only JPA, a developer is often left with some open question like e.g. how to implement _equals-hashCode_ for entities. @@ -15,8 +15,8 @@ Thanks to the first class notion of creatable entity views, this question and ot === Update mapping To declare an entity view as being updatable, it is required to additionally annotate it with `@UpdatableEntityView`. -By default an updatable entity view will do full updates i.e. always update all (owned) updatable attributes if at least one (owned) attribute is dirty. -Owned attributes are ones that belong the the backing entity type like e.g. basic typed attributes. Inverse attributes aren't owned and are thus independent. +By default an updatable entity view will do full updates i.e. always update all (owned) updatable attributes *if* at least one (owned) attribute is dirty. +Owned attributes are the ones that belong to the backing entity type like e.g. basic typed attributes. Collections or inverse attributes aren't owned in this sense and are thus independent. This behavior can be configured by setting the `mode` attribute on the `@UpdatableEntityView`: * `PARTIAL` - The mode will only flush values of actually changed attributes @@ -32,11 +32,11 @@ The flushing, by default, is done by executing JPQL DML statements, but can be c To declare an entity view as being creatable, it is required to additionally annotate it with `@CreatableEntityView`. Note that updatable entity views for embeddable types are implicitly also creatable, yet the `@CreatableEntityView` annotation can still be applied for further configuration. -By default, a creatable entity view is valid@ated against the backing model regarding it's _persistability_ i.e. it is checked if an instance could be successfully persisted regarding the non-null constraints of the entity model. +By default, a creatable entity view is validated against the backing model regarding it's _persistability_ i.e. it is checked if an instance could be successfully persisted regarding the non-null constraints of the entity model. This allows to catch errors early that occur when adding new attributes to the entity model but forgetting to do so in the entity view. The validation can be disabled by setting the `validatePersistability` attribute on the `@CreatableEntityView` to `false` but can also be controlled in a fine grained manner by excluding specific entity attributes from the validation via the `excludedEntityAttributes` attribute. -The latter is useful for attributes that are known to be set on the entity model through entity listeners or entity view listeners. +The latter is useful for attributes that are known to be set on the entity model through inverse relationship, entity listeners or entity view listeners. Creatable views are converted to their context specific declaration type after persisting. This mean that if a creatable entity view is used as value for an attribute of an updatable entity view, the instance is replaced by an equivalent instance @@ -50,6 +50,7 @@ interface CatUpdateView { @IdMapping Long getId(); + @MappingUpdatable(cascade = CascadeType.PERSIST) OwnerView getOwner(); void setOwner(OwnerView owner); } @@ -69,7 +70,7 @@ interface OwnerCreateView extends OwnerView { When flushing an instance of the type `CatUpdateView` that contains an owner of the creatable entity view type `OwnerCreateView` the following happens -. A `Person` entity is created with the defined properties +. A `Person` entity is created with the defined properties of the `OwnerCreateView` . The `Person` entity is persisted via `EntityManager.persist()` . The generated identifier is set on the `OwnerCreateView` object . The `OwnerCreateView` object is converted to the context specific declared type `OwnerView` @@ -89,15 +90,19 @@ The query flush strategy requires support for collection DML queries which is cu If the provider doesn't support collection DML, or you choose to do entity flushing, the owning entity is loaded and changes are applied to that. For collections that are _not owned_ by the containing entity i.e. use a _mappedBy_, changes will be applied by creating/updating/deleting the target entities. +INFO: {projectname} will manage inverse relationships automatically and even update the parent object in the child object if mapped. + Creatable entity views are constructed via link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#create(java.lang.Class)[`EntityViewManager.create(Class type)`] and always result in a persist when being flushed directly or through an updatable attribute having the `CascadeType.PERSIST` enabled. Deletion of entities through view types works either by supplying an existing view object to link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#remove(javax.persistence.EntityManager,%20java.lang.Object)[`EntityViewManager.remove(EntityManager em, Object view)`] or by entity id via link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#remove(javax.persistence.EntityManager,%20java.lang.Class,%20java.lang.Object)[`EntityViewManager.remove(EntityManager em, Class viewType, Object id)`]. +The big advantage of using the remove APIs is that {projectname} will reduce the amount of queries significantly, especially if the a view object is passed that already provides information about the object graph. + === Lifecycle and listeners -An entity view similar to a JPA entity also has something like a lifecycle, though within entity views, the states correspond to different entity view java types, rather than a transaction state. +An entity view, similar to a JPA entity, also has something like a lifecycle, though within entity views, the states correspond to different entity view java types, rather than a transaction state. There are essentially 3 different kinds of entity views: *new*::: An instance of a creatable entity view type(`@CreatableEntityView`) that is created via `EntityViewManager.create(Class)`. @@ -488,15 +493,13 @@ WARNING: Not yet available. === Attribute mappings When an entity view has `@UpdatableEntityView` annotated, every attribute for which a setter method exists, is considered to be _updatable_. -For an attribute to be _updatable_ means that changes done to the attribute of an entity view, can be flushed to the attribute they map to of an entity. +For an attribute to be _updatable_ means that changes done to the attribute of an entity view, can be flushed to the entity attribute they map to. There is also a notion of _mutable_ attributes which means that an attribute is _updatable_ and/or the type of the attribute's value might be _mutable_. An unknown type is mutable by default and needs to be configured by registering a <>. Entity view types are only considered being mutable if they are updatable(`@UpdatableEntityView`) or creatable(`@CreatableEntityView`). Entity types are always considered to be mutable. -Singular attributes with an updatable flat view type are also considered updatable even without a setter method. - The mappings for updatable attributes must follow some rules * May not use complex expressions like arithmetic or functions @@ -513,7 +516,7 @@ The getters and setters of abstract entity view classes may use the protected or [[updatable-mappings-basic]] ==== Basic type mappings -Singular attributes with a basic type i.e. all types except entity view types, entity types or collection types, +Singular attributes with a basic type(all types except entity view types, entity types or collection types) do not have a nested domain structure since they are _basic_. Values of such types usually change by setting a different value, though there are some mutable types as well. Basic types in general are handled by registered <> and define the necessary means to safely handle values of such types. @@ -623,16 +626,53 @@ To disable or fine tune this behavior, it is possible to annotate the attribute Apart from defining which `CascadeType` is enabled, it is also possible to restrict the allowed subtypes via the attributes `subtypes`, `persistSubtypes` and `updateSubtypes`. By default, instances of the declared type i.e. the compile time attribute type, are allowed to be set as attribute values. Subtypes that are non-updatable and non-creatable are also allowed. -If the attribute defines `UPDATE` cascading or the declared type is updatable(`@UpdatableEntityView`), all updatable subtypes are also allowed. -If the attribute defines `PERSIST` cascading or the declared type is creatable(`@CreatableEntityView`), all creatable subtypes are also allowed. +If the attribute defines `UPDATE` cascading or the declared type is updatable(`@UpdatableEntityView`), all updatable subtypes that don't introduce a cycle are also allowed. +If the attribute defines `PERSIST` cascading or the declared type is creatable(`@CreatableEntityView`), all creatable subtypes that don't introduce a cycle are also allowed. -In case of immutable or non-updatable subview types the method link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#getReference(java.lang.Class,%20java.lang.Object)[`EntityViewManager.getReference(Class viewType, Object id)`] might come in handy. +When using immutable/non-updatable subview types the method link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#getReference(java.lang.Class,%20java.lang.Object)[`EntityViewManager.getReference(Class viewType, Object id)`] might come in handy. This method allows to retrieve an instance of the given view type having the defined identifier. This is very useful for cases when just a relationship role like e.g. _owner_ should be set without the need to query `PersonView` objects. A common use case might be to set the tenant which owns an object. There is no need to query the tenant as the information is unnecessary for simply setting the relationship role, but the tenant's identity is known. To be able to encapsulate the creation of subviews or the access to references for subviews it is recommended to make use of the <>. The idea is to define an abstract getter method with protected or default visibility returning an `EntityViewManager`. Methods that create subviews or want a reference to a subview by id can then invoke the getter to get access to the `EntityViewManager`. +The following encapsulated updatable entity views illustrate the usage: + +[source,java] +---- +@EntityView(Person.class) +interface PersonView { + @IdMapping + Long getId(); + + String getName(); +} + +@UpdatableEntityView +@EntityView(Cat.class) +abstract class CatUpdateView { + @IdMapping + public abstract Long getId(); + + public abstract String getName(); + + @Mapping("owner") + protected abstract PersonView getOwnerInternal(); + protected abstract void setOwnerInternal(PersonView owner); + protected abstract EntityViewManager evm(); + + public PersonView getOwner() { + return getOwnerInternal(); + } + public void setOwner(PersonView owner) { + setOwnerInternal(evm().convert(PersonView.class, owner)); + } + public void setOwnerId(Long id) { + setOwnerInternal(evm().getReference(PersonView.class, id)); + } +} +---- + [[updatable-mappings-flat-view]] ==== Flat view mappings @@ -727,9 +767,10 @@ Cat cat = entityManager.find(Cat.class, view.getId()); cat.getKittens().add(newKitten); ---- -Since the `kittens` collection is dirty i.e. a new kitten was added and the collection is _owned_ by the `Cat` entity, it will be loaded along with the `Cat`. +Since the `kittens` collection is dirty i.e. a new kitten was added and the collection is _owned_ by the `Cat` entity, +the collection will be loaded along with the `Cat` when using the entity flush strategy. -If you use query flushing, instead of loading and adding, the new kitten will be added via a collection DML statement +WIth query flushing, instead of loading and adding, the new kitten will be added via a collection DML statement [source,sql] ---- @@ -750,9 +791,9 @@ will cause the attribute being maintained by managing inverse relation objects. There are several strategies that can be configured to handle the removal of elements via the `removeStrategy` attribute of `@MappingInverse` -* `IGNORE` - The default. Ignores elements that have been removed i.e. does not maintain the relationship automatically. +* `IGNORE` - Ignores elements that have been removed i.e. does not maintain the relationship automatically. * `REMOVE` - Removes the inverse relation object when determined to be removed from the inverse relationship. -* `SET_NULL` - Sets the _mappedBy_ attribute to `NULL` on the inverse relation object when found to be removed from the inverse relationship. +* `SET_NULL` - The default. Sets the _mappedBy_ attribute to `NULL` on the inverse relation object when found to be removed from the inverse relationship. [source,java] ---- @@ -1684,7 +1725,7 @@ WARNING: This is still in development, so not all features might be available ye The cascade types defined in {projectname} entity views have different semantics than what JPA offers and should not be mixed up. JPA defines cascade types for _logical operations_ whereas {projectname} entity views defines cascade types for state changes. In a JPA entity, one can define for which operations the changes done to an attribute should be flushed. -For example the JPA `CascadeType.PERSIST` will cause a flush of an attributes affected values only if the owning entity is about to be persisted. +For example the JPA `CascadeType.PERSIST` will cause a flush of an attribute's affected values only if the owning entity is about to be persisted. {projectname} entity views cascade types define whether a value of an attribute may do a specific state transition. If an attribute defines `CascadeType.PERSIST`, it means that _new_ objects i.e. the ones created via `EntityViewManager.create()`, @@ -1705,7 +1746,7 @@ When removing a reference from entity A to entity B through an attribute that de Orphan removal also implies delete cascading, so entity B is also deleted when deleting entity A. Most JPA implementations only support cascading deletes and orphan removal for managed entities whereas DML statements for the entity types do not consider this configuration. -{projectname} respects the settings all the way, even for the removal by id action done via link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#remove(javax.persistence.EntityManager,%20java.lang.Class,%20java.lang.Object)[EntityViewManager.remove(EntityManager, Class, Object)]. +{projectname} respects the settings all the way and strives to avoid data loading even for the removal by id action done via link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#remove(javax.persistence.EntityManager,%20java.lang.Class,%20java.lang.Object)[EntityViewManager.remove(EntityManager, Class, Object)]. When an entity graph for an entity view type has an _arbitrary depth relationship_, {projectname} still has to do some entity data loading, but it tries to reduce the executed statements as much as possible. NOTE: At some point, DML statements might be grouped together via Updatable CTEs for DBMS that support that. For more information about that, see https://github.com/Blazebit/blaze-persistence/issues/500 @@ -1749,7 +1790,7 @@ Finally, the cascading deletes for the *ToOne relations are done e.g. the `Perso NOTE: A future strategy for deletion might facilitate temporary tables if the DBMS supports it rather than selecting. For more information see https://github.com/Blazebit/blaze-persistence/issues/220 -If the entity type for an updatable entity view uses delete cascading or orphan removal for an attribute, an updatable mapping for that attribute must use these configurations as well. +If the entity type for an updatable entity view uses delete cascading or orphan removal for an attribute, an updatable mapping for that attribute *must* use these configurations as well. So if the entity type uses delete cascading for the `owner` of `Cat`, it would be an error to omit the delete cascading configuration. [source,java] @@ -1775,7 +1816,7 @@ This where {projectname} entity views show their strength as they allow to contr === Conversion support As explained in the beginning, the vision for updatable entity views is to support the modelling of use case specific write models. -Although most of the data that is generally updatable is mostly loaded once when starting a _conversation_ it is rarely necessary to make it updatable right away. +Although most of the data that is generally updatable is mostly loaded already when starting a use case it is rarely necessary to make it updatable right away. Some use cases might require only a subset of the data to be updatable, while others require a different subset. To support modelling this appropriately it is possible to convert between entity views types. @@ -1812,7 +1853,7 @@ interface CatKittenUpdateView extends CatBaseView { } ---- -When navigating to the detail UI for a `Cat` the `CatBaseView` would be loaded. +When navigating to e.g. a detail UI for a `Cat` the `CatBaseView` would be loaded. If the UI had a special action to initiate a transfer to a different owner, doing that action would lead to the conversion of the `CatBaseView` to the `CatOwnerUpdateView`. [source,java] @@ -1835,9 +1876,8 @@ When initiating the kitten update action the conversion would be done to `CatKit Keep in mind that most UIs do not necessarily work this way and that the added complexity might not be beneficial in all cases. Although this mechanism enables a clear separation for use cases, it might just as well be the case, that use cases are so small that it is better to have just a single write model. In some special cases like e.g. when simply changing a status of an object, it might not even be necessary to have an explicit write model. -For such cases it is often more appropriate to have a specialized service method. +For such cases it is often more appropriate to have a specialized service method or event publishing of some sorts. Note that internally, the conversion feature is used for converting successfully persisted creatable entity views to their context specific declaration type. -There are of course other possible use cases for this feature like e.g. conversion from a _more detailed_ view to a view containing only a subset of the information, -though it is recommended to query the view with the subset of information rather than querying more if possible/practical to not do unnecessary data loading. \ No newline at end of file +There are of course other possible use cases for this feature like e.g. conversion from a _more detailed_ view to a view containing only a subset of the information. \ No newline at end of file diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/accessor/DirtyStateViewAttributeAccessor.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/accessor/DirtyStateViewAttributeAccessor.java index 2b596700e6..23d70fdbd0 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/accessor/DirtyStateViewAttributeAccessor.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/accessor/DirtyStateViewAttributeAccessor.java @@ -34,12 +34,14 @@ public final class DirtyStateViewAttributeAccessor extends ViewAttributeAccessor implements InitialValueAttributeAccessor { private final int dirtyStateIndex; + private final boolean updatableOnly; private final BasicUserType userType; @SuppressWarnings({ "rawtypes", "unchecked" }) public DirtyStateViewAttributeAccessor(EntityViewManagerImpl evm, MethodAttribute attribute) { super(evm, attribute, false); this.dirtyStateIndex = ((AbstractMethodAttribute) attribute).getDirtyStateIndex(); + this.updatableOnly = ((AbstractMethodAttribute) attribute).isUpdatableOnly() && !attribute.isCorrelated(); if (attribute instanceof SingularAttribute) { SingularAttribute singularAttribute = (SingularAttribute) attribute; if (singularAttribute.getType() instanceof BasicType) { @@ -82,7 +84,9 @@ public void setValue(Object view, Object value) { super.setValue(view, value); if (view instanceof MutableStateTrackable) { MutableStateTrackable mutableStateTrackable = (MutableStateTrackable) view; - if (value instanceof DirtyTracker) { + if (updatableOnly && value instanceof MutableStateTrackable) { + ((MutableStateTrackable) value).$$_addReadOnlyParent(mutableStateTrackable, dirtyStateIndex); + } else if (value instanceof DirtyTracker) { ((DirtyTracker) value).$$_setParent(mutableStateTrackable, dirtyStateIndex); } mutableStateTrackable.$$_getMutableState()[dirtyStateIndex] = value; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListCollectionInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListCollectionInstantiator.java index 6d7467705e..94cae3c072 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListCollectionInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/ListCollectionInstantiator.java @@ -33,15 +33,17 @@ public class ListCollectionInstantiator extends AbstractCollectionInstantiator { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean indexed; private final boolean optimize; private final boolean forceUnique; private final Comparator comparator; - public ListCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean indexed, boolean optimize, boolean forceUnique, Comparator comparator) { + public ListCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean indexed, boolean optimize, boolean forceUnique, Comparator comparator) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.indexed = indexed; this.optimize = optimize; @@ -90,6 +92,6 @@ public List createCollection(int size) { @Override public RecordingList createRecordingCollection(int size) { - return new RecordingList(createCollection(size), indexed, allowedSubtypes, updatable, optimize); + return new RecordingList(createCollection(size), indexed, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedCollectionInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedCollectionInstantiator.java index 05081e3da2..804b129ba2 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedCollectionInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedCollectionInstantiator.java @@ -32,14 +32,16 @@ public class OrderedCollectionInstantiator extends AbstractCollectionInstantiator { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; private final boolean forceUnique; private final Comparator comparator; - public OrderedCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize, boolean forceUnique, Comparator comparator) { + public OrderedCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, boolean forceUnique, Comparator comparator) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; this.forceUnique = forceUnique; @@ -87,6 +89,6 @@ public Collection createCollection(int size) { @Override public RecordingCollection, ?> createRecordingCollection(int size) { - return new RecordingCollection(createCollection(size), false, true, allowedSubtypes, updatable, optimize); + return new RecordingCollection(createCollection(size), false, true, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedMapInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedMapInstantiator.java index cfb5a2e53b..a88b894f45 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedMapInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedMapInstantiator.java @@ -28,12 +28,14 @@ public class OrderedMapInstantiator extends AbstractMapInstantiator, RecordingMap, ?, ?>> { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; - public OrderedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public OrderedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; } @@ -45,6 +47,6 @@ public OrderedMapInstantiator(PluralObjectFactory> collectionFactory, @Override public RecordingMap, ?, ?> createRecordingCollection(int size) { - return new RecordingMap(createCollection(size), true, allowedSubtypes, updatable, optimize); + return new RecordingMap(createCollection(size), true, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedSetCollectionInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedSetCollectionInstantiator.java index bdae44948f..be4c646c9a 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedSetCollectionInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/OrderedSetCollectionInstantiator.java @@ -28,12 +28,14 @@ public class OrderedSetCollectionInstantiator extends AbstractCollectionInstantiator { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; - public OrderedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public OrderedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; } @@ -50,6 +52,6 @@ public Set createCollection(int size) { @Override public RecordingSet, ?> createRecordingCollection(int size) { - return new RecordingSet(createCollection(size), true, allowedSubtypes, updatable, optimize); + return new RecordingSet(createCollection(size), true, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } 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 3b77005931..1b99e84326 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 @@ -46,6 +46,7 @@ public class RecordingCollection, E> implements Collecti protected final C delegate; protected final Set> allowedSubtypes; + protected final Set> parentRequiringSubtypes; protected final boolean updatable; protected final boolean indexed; private final boolean ordered; @@ -60,9 +61,10 @@ public class RecordingCollection, E> implements Collecti // We remember the iterator so we can do a proper hash based collection replacement private transient RecordingReplacingIterator currentIterator; - protected RecordingCollection(C delegate, boolean indexed, boolean ordered, Set> allowedSubtypes, boolean updatable, boolean optimize, boolean hashBased) { + protected RecordingCollection(C delegate, boolean indexed, boolean ordered, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, boolean hashBased) { this.delegate = delegate; this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.indexed = indexed; this.ordered = ordered; @@ -70,9 +72,10 @@ protected RecordingCollection(C delegate, boolean indexed, boolean ordered, Set< this.hashBased = hashBased; } - public RecordingCollection(C delegate, boolean indexed, boolean ordered, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public RecordingCollection(C delegate, boolean indexed, boolean ordered, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { this.delegate = delegate; this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.indexed = indexed; this.ordered = ordered; @@ -169,6 +172,25 @@ public RecordingCollection(C delegate, boolean indexed, boolean ordered, Set newCollection = new ArrayList<>(delegate.size()); + for (E e : delegate) { + if (e == oldObject) { + newCollection.add((E) newObject); + } else { + newCollection.add(e); + } + } + delegate.clear(); + delegate.addAll(newCollection); + } else { + delegate.remove(oldObject); + delegate.add((E) newObject); + } + } + public void $$_unsetParent() { this.parentIndex = 0; this.parent = null; @@ -490,11 +512,14 @@ protected void checkType(Object e, String action) { if (!allowedSubtypes.contains(c)) { throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed!"); } + if (parentRequiringSubtypes.contains(c) && !((DirtyTracker) e).$$_hasParent()) { + throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed until they are assigned to an attribute that cascades the type!"); + } } } protected void checkType(Collection collection, String action) { - if (collection != null && !allowedSubtypes.isEmpty()) { + if (collection != null && !collection.isEmpty() && !allowedSubtypes.isEmpty()) { for (Object e : collection) { Class c; if (e instanceof EntityViewProxy) { @@ -506,6 +531,9 @@ protected void checkType(Collection collection, String action) { if (!allowedSubtypes.contains(c)) { throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed!"); } + if (parentRequiringSubtypes.contains(c) && !((DirtyTracker) e).$$_hasParent()) { + throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed until they are assigned to an attribute that cascades the type!"); + } } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingList.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingList.java index e3f5954f41..6e625799ed 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingList.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingList.java @@ -30,8 +30,8 @@ */ public class RecordingList extends RecordingCollection, E> implements List { - public RecordingList(List delegate, boolean indexed, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, indexed, indexed, allowedSubtypes, updatable, optimize); + public RecordingList(List delegate, boolean indexed, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, indexed, indexed, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } @Override 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 c62ad1564f..b18930e787 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 @@ -18,6 +18,7 @@ import com.blazebit.persistence.view.impl.entity.MapViewToEntityMapper; import com.blazebit.persistence.view.impl.proxy.DirtyTracker; +import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.spi.type.BasicDirtyTracker; import com.blazebit.persistence.view.spi.type.EntityViewProxy; @@ -47,6 +48,7 @@ public class RecordingMap, K, V> implements Map, Dirty protected final C delegate; protected final Set> allowedSubtypes; + protected final Set> parentRequiringSubtypes; protected final boolean updatable; private final boolean optimize; private final boolean hashBased; @@ -62,18 +64,20 @@ public class RecordingMap, K, V> implements Map, Dirty // We remember the iterator so we can do a proper hash based collection replacement private transient RecordingEntrySetReplacingIterator currentIterator; - protected RecordingMap(C delegate, Set> allowedSubtypes, boolean updatable, boolean optimize, boolean hashBased, boolean ordered) { + protected RecordingMap(C delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, boolean hashBased, boolean ordered) { this.delegate = delegate; this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; this.hashBased = hashBased; this.ordered = ordered; } - public RecordingMap(C delegate, boolean ordered, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public RecordingMap(C delegate, boolean ordered, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { this.delegate = delegate; this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; this.ordered = ordered; @@ -176,6 +180,40 @@ public RecordingMap(C delegate, boolean ordered, Set> allowedSubtypes, return parent != null; } + @Override + public void $$_replaceAttribute(Object oldObject, int attributeIndex, Object newObject) { + if (oldObject instanceof MutableStateTrackable) { + ((MutableStateTrackable) oldObject).$$_removeReadOnlyParent(this, attributeIndex); + } + if (newObject instanceof MutableStateTrackable) { + ((MutableStateTrackable) newObject).$$_addReadOnlyParent(this, attributeIndex); + } + if (attributeIndex == 1) { + if (ordered) { + Map newMap = new LinkedHashMap<>(delegate.size()); + for (Entry entry : delegate.entrySet()) { + if (entry.getKey() == oldObject) { + newMap.put((K) newObject, entry.getValue()); + } else { + newMap.put(entry.getKey(), entry.getValue()); + } + } + delegate.clear(); + delegate.putAll(newMap); + } else { + V value = delegate.remove(oldObject); + delegate.put((K) newObject, value); + } + } else { + for (Entry entry : delegate.entrySet()) { + if (entry.getValue() == oldObject) { + entry.setValue((V) newObject); + break; + } + } + } + } + @Override public void $$_unsetParent() { this.parentIndex = 0; @@ -534,11 +572,14 @@ protected void checkType(Object e, String action) { if (!allowedSubtypes.contains(c)) { throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed!"); } + if (parentRequiringSubtypes.contains(c) && !((DirtyTracker) e).$$_hasParent()) { + throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed until they are assigned to an attribute that cascades the type!"); + } } } protected void checkType(Map collection, String action) { - if (collection != null && !allowedSubtypes.isEmpty()) { + if (collection != null && !collection.isEmpty() && !allowedSubtypes.isEmpty()) { for (Object e : collection.values()) { Class c; if (e instanceof EntityViewProxy) { @@ -550,6 +591,9 @@ protected void checkType(Map collection, String action) { if (!allowedSubtypes.contains(c)) { throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed!"); } + if (parentRequiringSubtypes.contains(c) && !((DirtyTracker) e).$$_hasParent()) { + throw new IllegalArgumentException(action + " instances of type [" + c.getName() + "] is not allowed until they are assigned to an attribute that cascades the type!"); + } } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableMap.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableMap.java index 03b47b5097..e4acf0cd31 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableMap.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableMap.java @@ -27,8 +27,8 @@ */ public class RecordingNavigableMap, K, V> extends RecordingSortedMap implements NavigableMap { - public RecordingNavigableMap(C delegate, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, allowedSubtypes, updatable, optimize); + public RecordingNavigableMap(C delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableSet.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableSet.java index d27f7a08fa..1905a48521 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableSet.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingNavigableSet.java @@ -27,8 +27,8 @@ */ public class RecordingNavigableSet extends RecordingSortedSet, E> implements NavigableSet { - public RecordingNavigableSet(NavigableSet delegate, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, allowedSubtypes, updatable, optimize); + public RecordingNavigableSet(NavigableSet delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSet.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSet.java index dc82d9f95e..d7f98af025 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSet.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSet.java @@ -25,12 +25,12 @@ */ public class RecordingSet, E> extends RecordingCollection implements Set { - protected RecordingSet(C delegate, Set> allowedSubtypes, boolean updatable, boolean optimize, boolean hashBased, boolean ordered) { - super(delegate, false, ordered, allowedSubtypes, updatable, optimize, hashBased); + protected RecordingSet(C delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, boolean hashBased, boolean ordered) { + super(delegate, false, ordered, allowedSubtypes, parentRequiringSubtypes, updatable, optimize, hashBased); } - public RecordingSet(C delegate, boolean ordered, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, false, ordered, allowedSubtypes, updatable, optimize, true); + public RecordingSet(C delegate, boolean ordered, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, false, ordered, allowedSubtypes, parentRequiringSubtypes, updatable, optimize, true); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedMap.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedMap.java index 82c408882e..2e2a66d934 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedMap.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedMap.java @@ -27,8 +27,8 @@ */ public class RecordingSortedMap, K, V> extends RecordingMap implements SortedMap { - protected RecordingSortedMap(C delegate, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, allowedSubtypes, updatable, optimize, false, false); + protected RecordingSortedMap(C delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, allowedSubtypes, parentRequiringSubtypes, updatable, optimize, false, false); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedSet.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedSet.java index 3d5f012a42..e3ffb83076 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedSet.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/RecordingSortedSet.java @@ -27,8 +27,8 @@ */ public class RecordingSortedSet, E> extends RecordingSet implements SortedSet { - protected RecordingSortedSet(C delegate, Set> allowedSubtypes, boolean updatable, boolean optimize) { - super(delegate, allowedSubtypes, updatable, optimize, false, false); + protected RecordingSortedSet(C delegate, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { + super(delegate, allowedSubtypes, parentRequiringSubtypes, updatable, optimize, false, false); } @Override diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedMapInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedMapInstantiator.java index 7176112d08..53f13c94f4 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedMapInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedMapInstantiator.java @@ -30,13 +30,15 @@ public class SortedMapInstantiator extends AbstractMapInstantiator, RecordingNavigableMap, ?, ?>> { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; private final Comparator comparator; - public SortedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize, Comparator comparator) { + public SortedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, Comparator comparator) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; this.comparator = comparator; @@ -49,6 +51,6 @@ public SortedMapInstantiator(PluralObjectFactory> collectionFactory, S @Override public RecordingNavigableMap, ?, ?> createRecordingCollection(int size) { - return new RecordingNavigableMap(createCollection(size), allowedSubtypes, updatable, optimize); + return new RecordingNavigableMap(createCollection(size), allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedSetCollectionInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedSetCollectionInstantiator.java index 8bd80d2836..276a80dcb2 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedSetCollectionInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/SortedSetCollectionInstantiator.java @@ -30,13 +30,15 @@ public class SortedSetCollectionInstantiator extends AbstractCollectionInstantiator { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; private final Comparator comparator; - public SortedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize, Comparator comparator) { + public SortedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize, Comparator comparator) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; this.comparator = comparator; @@ -54,6 +56,6 @@ public NavigableSet createCollection(int size) { @Override public RecordingSortedSet, ?> createRecordingCollection(int size) { - return new RecordingNavigableSet(createCollection(size), allowedSubtypes, updatable, optimize); + return new RecordingNavigableSet(createCollection(size), allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedMapInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedMapInstantiator.java index a921357941..59d56de2e9 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedMapInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedMapInstantiator.java @@ -28,12 +28,14 @@ public class UnorderedMapInstantiator extends AbstractMapInstantiator, RecordingMap, ?, ?>> { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; - public UnorderedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public UnorderedMapInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; } @@ -45,6 +47,6 @@ public UnorderedMapInstantiator(PluralObjectFactory> collectionFactory @Override public RecordingMap, ?, ?> createRecordingCollection(int size) { - return new RecordingMap(createCollection(size), false, allowedSubtypes, updatable, optimize); + return new RecordingMap(createCollection(size), false, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedSetCollectionInstantiator.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedSetCollectionInstantiator.java index 28233b0bfe..3569d8efb6 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedSetCollectionInstantiator.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/collection/UnorderedSetCollectionInstantiator.java @@ -28,12 +28,14 @@ public class UnorderedSetCollectionInstantiator extends AbstractCollectionInstantiator { private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final boolean updatable; private final boolean optimize; - public UnorderedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, boolean updatable, boolean optimize) { + public UnorderedSetCollectionInstantiator(PluralObjectFactory> collectionFactory, Set> allowedSubtypes, Set> parentRequiringSubtypes, boolean updatable, boolean optimize) { super(collectionFactory); this.allowedSubtypes = allowedSubtypes; + this.parentRequiringSubtypes = parentRequiringSubtypes; this.updatable = updatable; this.optimize = optimize; } @@ -50,6 +52,6 @@ public Set createCollection(int size) { @Override public RecordingSet, ?> createRecordingCollection(int size) { - return new RecordingSet(createCollection(size), false, allowedSubtypes, updatable, optimize); + return new RecordingSet(createCollection(size), false, allowedSubtypes, parentRequiringSubtypes, updatable, optimize); } } 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 c03cf81e6a..e386f59eb8 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 @@ -132,6 +132,12 @@ public void remove(UpdateContext context, Object element) { updater.remove(context, (EntityViewProxy) element); } + @Override + public boolean cascades(Object element) { + Class viewTypeClass = getViewTypeClass(element); + return persistUpdater.containsKey(viewTypeClass) || updateUpdater.containsKey(viewTypeClass); + } + @Override public void removeById(UpdateContext context, Object id) { defaultUpdater.remove(context, id); 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 a90a243933..8483138493 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 @@ -61,6 +61,11 @@ public void removeById(UpdateContext context, Object id) { } + @Override + public boolean cascades(Object value) { + return false; + } + @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/ViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/ViewToEntityMapper.java index 1a719f1243..a003fc8c08 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 @@ -48,4 +48,5 @@ public interface ViewToEntityMapper extends ElementToEntityMapper { public Object loadEntity(UpdateContext context, Object view); + public boolean cascades(Object value); } \ No newline at end of file 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 6b8cb7d7a7..e8db1603d8 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 @@ -922,6 +922,8 @@ protected boolean isEmbedded() { public abstract Set> getAllowedSubtypes(); + public abstract Set> getParentRequiringSubtypes(); + public abstract boolean isOptimizeCollectionActionsEnabled(); public abstract CollectionInstantiator getCollectionInstantiator(); @@ -937,24 +939,24 @@ protected final CollectionInstantiator createCollectionInstantiator(MetamodelBui if (comparator != null) { context.addError("Comparator can't be defined for indexed attribute at the " + getLocation()); } - return new ListCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), true, isOptimizeCollectionActionsEnabled(), false, null); + return new ListCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), true, isOptimizeCollectionActionsEnabled(), false, null); } else { if (sorted) { - return new SortedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), comparator); + return new SortedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), comparator); } else { if (getCollectionType() == PluralAttribute.CollectionType.SET) { if (comparator != null) { context.addError("Comparator can't be defined for non-sorted set attribute at the " + getLocation()); } if (ordered) { - return new OrderedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); + return new OrderedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); } else { - return new UnorderedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); + return new UnorderedSetCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); } } else if (getCollectionType() == PluralAttribute.CollectionType.LIST) { - return new ListCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), false, isOptimizeCollectionActionsEnabled(), isForcedUnique(), comparator); + return new ListCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), false, isOptimizeCollectionActionsEnabled(), isForcedUnique(), comparator); } else { - return new OrderedCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), isForcedUnique(), comparator); + return new OrderedCollectionInstantiator((PluralObjectFactory>) collectionFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), isForcedUnique(), comparator); } } } @@ -963,11 +965,11 @@ protected final CollectionInstantiator createCollectionInstantiator(MetamodelBui @SuppressWarnings({ "unchecked", "rawtypes" }) protected final MapInstantiator createMapInstantiator(PluralObjectFactory> mapFactory, boolean sorted, boolean ordered, Comparator comparator) { if (sorted) { - return new SortedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), comparator); + return new SortedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled(), comparator); } else if (ordered) { - return new OrderedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); + return new OrderedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); } else { - return new UnorderedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); + return new UnorderedMapInstantiator((PluralObjectFactory>) mapFactory, getAllowedSubtypes(), getParentRequiringSubtypes(), isUpdatable(), isOptimizeCollectionActionsEnabled()); } } 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 a1541664c1..8ec62a1e4a 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 @@ -399,6 +399,10 @@ public Y getValue(Object o) { public abstract int getDirtyStateIndex(); + public boolean isUpdatableOnly() { + return hasDirtyStateIndex() && !isPersistCascaded() && !isUpdateCascaded(); + } + public abstract Map getWritableMappedByMappings(); protected final Set> createAllowedSubtypesSet() { @@ -418,6 +422,22 @@ protected final Set> createAllowedSubtypesSet() { return Collections.unmodifiableSet(allowedSubtypes); } + protected final Set> createParentRequiringSubtypesSet() { + Set> readOnlyAllowedSubtypes = getReadOnlyAllowedSubtypes(); + Set> persistAllowedSubtypes = getPersistCascadeAllowedSubtypes(); + Set> updateAllowedSubtypes = getUpdateCascadeAllowedSubtypes(); + Set> allowedSubtypes = new HashSet<>(readOnlyAllowedSubtypes.size()); + for (Type t : readOnlyAllowedSubtypes) { + if (t instanceof ManagedViewTypeImplementor) { + ManagedViewTypeImplementor viewType = (ManagedViewTypeImplementor) t; + if (viewType.isUpdatable() && !updateAllowedSubtypes.contains(t) || viewType.isCreatable() && !persistAllowedSubtypes.contains(t)) { + allowedSubtypes.add(t.getJavaType()); + } + } + } + return Collections.unmodifiableSet(allowedSubtypes); + } + @Override public boolean isOptimizeCollectionActionsEnabled() { // For now, we optimize collection actions only when optimistic lock protected 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 8e66705291..32ddef1492 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 @@ -56,6 +56,7 @@ public abstract class AbstractMethodPluralAttribute extends AbstractMet private final Set> persistSubtypes; private final Set> updateSubtypes; private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final Map, String> elementInheritanceSubtypes; private final boolean sorted; private final boolean ordered; @@ -95,9 +96,14 @@ public AbstractMethodPluralAttribute(ManagedViewTypeImplementor viewType, Met this.readOnlySubtypes = (Set>) (Set) mapping.getReadOnlySubtypes(context, embeddableMapping); if (updatable) { - this.persistSubtypes = determinePersistSubtypeSet(elementType, mapping.getCascadeSubtypes(context, embeddableMapping), mapping.getCascadePersistSubtypes(context, embeddableMapping), context); + Set> types = determinePersistSubtypeSet(elementType, mapping.getCascadeSubtypes(context, embeddableMapping), mapping.getCascadePersistSubtypes(context, embeddableMapping), context); this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST) - || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !persistSubtypes.isEmpty(); + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !types.isEmpty(); + if (persistCascaded) { + this.persistSubtypes = types; + } else { + this.persistSubtypes = Collections.emptySet(); + } } else { this.persistCascaded = false; this.persistSubtypes = Collections.emptySet(); @@ -134,6 +140,8 @@ public AbstractMethodPluralAttribute(ManagedViewTypeImplementor viewType, Met } this.allowedSubtypes = createAllowedSubtypesSet(); + // We treat elements of correlated collections specially + this.parentRequiringSubtypes = isCorrelated() ? Collections.>emptySet() : createParentRequiringSubtypesSet(); this.optimisticLockProtected = determineOptimisticLockProtected(mapping, context, mutable); this.elementInheritanceSubtypes = (Map, String>) (Map) mapping.getElementInheritanceSubtypes(context, embeddableMapping); this.dirtyStateIndex = determineDirtyStateIndex(dirtyStateIndex); @@ -293,6 +301,11 @@ public Set> getAllowedSubtypes() { return allowedSubtypes; } + @Override + public Set> getParentRequiringSubtypes() { + return parentRequiringSubtypes; + } + @Override public AttributeType getAttributeType() { return AttributeType.PLURAL; 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 41c8a16dc9..876d51795a 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 @@ -61,6 +61,7 @@ public abstract class AbstractMethodSingularAttribute extends AbstractMeth private final Set> persistSubtypes; private final Set> updateSubtypes; private final Set> allowedSubtypes; + private final Set> parentRequiringSubtypes; private final Map, String> inheritanceSubtypes; @SuppressWarnings("unchecked") @@ -110,9 +111,14 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M this.readOnlySubtypes = (Set>) (Set) mapping.getReadOnlySubtypes(context, embeddableMapping); if (updatable) { - this.persistSubtypes = determinePersistSubtypeSet(type, mapping.getCascadeSubtypes(context, embeddableMapping), mapping.getCascadePersistSubtypes(context, embeddableMapping), context); + Set> types = determinePersistSubtypeSet(type, mapping.getCascadeSubtypes(context, embeddableMapping), mapping.getCascadePersistSubtypes(context, embeddableMapping), context); this.persistCascaded = mapping.getCascadeTypes().contains(CascadeType.PERSIST) - || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !persistSubtypes.isEmpty(); + || mapping.getCascadeTypes().contains(CascadeType.AUTO) && !types.isEmpty(); + if (persistCascaded) { + this.persistSubtypes = types; + } else { + this.persistSubtypes = Collections.emptySet(); + } } else { this.persistCascaded = false; this.persistSubtypes = Collections.emptySet(); @@ -120,7 +126,7 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M ManagedType managedType = context.getEntityMetamodel().getManagedType(declaringType.getEntityClass()); this.mappedBy = mapping.determineMappedBy(managedType, this.mapping, context, embeddableMapping); - this.disallowOwnedUpdatableSubview = context.isDisallowOwnedUpdatableSubview() && mapping.isDisallowOwnedUpdatableSubview() && type instanceof ManagedViewType && mappedBy == null + this.disallowOwnedUpdatableSubview = context.isDisallowOwnedUpdatableSubview() && mapping.determineDisallowOwnedUpdatableSubview(context, embeddableMapping, updateMappableAttribute) && type instanceof ManagedViewType && mappedBy == null && updateMappableAttribute != null && updateMappableAttribute.getPersistentAttributeType() != javax.persistence.metamodel.Attribute.PersistentAttributeType.EMBEDDED; // The declaring type must be mutable, otherwise attributes can't have cascading @@ -165,6 +171,8 @@ public AbstractMethodSingularAttribute(ManagedViewTypeImplementor viewType, M } this.allowedSubtypes = createAllowedSubtypesSet(); + // We treat correlated attributes specially + this.parentRequiringSubtypes = isCorrelated() ? Collections.>emptySet() : createParentRequiringSubtypesSet(); this.optimisticLockProtected = determineOptimisticLockProtected(mapping, context, mutable); this.inheritanceSubtypes = (Map, String>) (Map) mapping.getInheritanceSubtypes(context, embeddableMapping); this.dirtyStateIndex = determineDirtyStateIndex(dirtyStateIndex); @@ -342,6 +350,11 @@ public Set> getAllowedSubtypes() { return allowedSubtypes; } + @Override + public Set> getParentRequiringSubtypes() { + return parentRequiringSubtypes; + } + @Override public Type getType() { return type; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractParameterAttribute.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractParameterAttribute.java index fcbd0f273f..aef515b50d 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractParameterAttribute.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AbstractParameterAttribute.java @@ -137,6 +137,11 @@ public Set> getAllowedSubtypes() { return Collections.emptySet(); } + @Override + public Set> getParentRequiringSubtypes() { + return Collections.emptySet(); + } + @Override public boolean isOptimizeCollectionActionsEnabled() { return false; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AttributeMapping.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AttributeMapping.java index 24e9243c4e..5647672b2e 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AttributeMapping.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/AttributeMapping.java @@ -24,6 +24,7 @@ import com.blazebit.persistence.view.spi.EntityViewAttributeMapping; import com.blazebit.persistence.view.spi.type.TypeConverter; +import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.ManagedType; import java.lang.annotation.Annotation; import java.util.Arrays; @@ -67,7 +68,7 @@ public abstract class AttributeMapping implements EntityViewAttributeMapping { protected ContainerBehavior containerBehavior; protected Class> comparatorClass; protected boolean forceUniqueness; - protected boolean disallowOwnedUpdatableSubview = true; + protected Boolean disallowOwnedUpdatableSubview; // Other configs protected Integer defaultBatchSize; @@ -177,7 +178,7 @@ public void setForceUniqueness(boolean forceUniqueness) { @Override public boolean isDisallowOwnedUpdatableSubview() { - return disallowOwnedUpdatableSubview; + return !Boolean.FALSE.equals(disallowOwnedUpdatableSubview); } @Override @@ -210,6 +211,8 @@ public boolean isSorted() { return containerBehavior == ContainerBehavior.SORTED; } + public abstract boolean determineDisallowOwnedUpdatableSubview(MetamodelBuildingContext context, EmbeddableOwner embeddableMapping, Attribute updateMappableAttribute); + public abstract String determineMappedBy(ManagedType managedType, String mapping, MetamodelBuildingContext context, EmbeddableOwner embeddableMapping); public abstract Map determineWritableMappedByMappings(ManagedType managedType, String mappedBy, MetamodelBuildingContext context); 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 0921256bab..8c6b1be4c2 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 @@ -26,8 +26,8 @@ import com.blazebit.persistence.view.MappingSubquery; import com.blazebit.persistence.view.impl.metamodel.attribute.CorrelatedMethodCollectionAttribute; import com.blazebit.persistence.view.impl.metamodel.attribute.CorrelatedMethodListAttribute; -import com.blazebit.persistence.view.impl.metamodel.attribute.CorrelatedMethodSingularAttribute; import com.blazebit.persistence.view.impl.metamodel.attribute.CorrelatedMethodSetAttribute; +import com.blazebit.persistence.view.impl.metamodel.attribute.CorrelatedMethodSingularAttribute; import com.blazebit.persistence.view.impl.metamodel.attribute.MappingMethodCollectionAttribute; import com.blazebit.persistence.view.impl.metamodel.attribute.MappingMethodListAttribute; import com.blazebit.persistence.view.impl.metamodel.attribute.MappingMethodMapAttribute; @@ -321,25 +321,21 @@ public void initializeViewMappings(MetamodelBuildingContext context) { } // Also see AbstractMethodPluralAttribute#determineUpdatable() for the same logic if (attributeViewMapping != null) { + boolean allowCreatable = cascadeTypes.contains(CascadeType.PERSIST) || attributeViewMapping.isCreatable() && cascadeTypes.contains(CascadeType.AUTO); if (isUpdatable == Boolean.TRUE || getDeclaringView().isUpdatable()) { - if (hasSetter || isCollection && (cascadeTypes.contains(CascadeType.PERSIST) || attributeViewMapping.isCreatable())) { - boolean allowUpdatable; - if (isCollection) { - allowUpdatable = true; - } else { - allowUpdatable = !disallowOwnedUpdatableSubview || attributeViewMapping.isUpdatable(); - } + if (hasSetter || isCollection && allowCreatable) { + boolean allowUpdatable = cascadeTypes.contains(CascadeType.UPDATE) || attributeViewMapping.isUpdatable() && cascadeTypes.contains(CascadeType.AUTO); // But only if the attribute is explicitly or implicitly updatable - this.readOnlySubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), false, false); - this.cascadeSubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), allowUpdatable, true); + this.readOnlySubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), true, true); + this.cascadeSubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), allowUpdatable, allowCreatable); } else { this.cascadeSubtypeMappings = Collections.emptyMap(); } } else { // Allow all read-only subtypes and also creatable subtypes for creatable-only views - if (getDeclaringView().isCreatable() && (hasSetter || isCollection && (cascadeTypes.contains(CascadeType.PERSIST) || attributeViewMapping.isCreatable()))) { - this.readOnlySubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), false, false); - this.cascadePersistSubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), false, true); + if (getDeclaringView().isCreatable() && (hasSetter || isCollection && allowCreatable)) { + this.readOnlySubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), true, true); + this.cascadePersistSubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), false, allowCreatable); } this.cascadeSubtypeMappings = Collections.emptyMap(); } @@ -383,6 +379,24 @@ public boolean validateDependencies(MetamodelBuildingContext context, Set updateMappableAttribute) { + if (disallowOwnedUpdatableSubview != null) { + return disallowOwnedUpdatableSubview; + } + String mappedBy; + if (embeddableMapping == null) { + mappedBy = this.mappedBy; + } else { + if (embeddableMappedByMap == null) { + embeddableMappedByMap = new HashMap<>(1); + } + mappedBy = embeddableMappedByMap.get(embeddableMapping); + } + + return disallowOwnedUpdatableSubview = isCollection || mappedBy != null && !mappedBy.isEmpty() || updateMappableAttribute != null && updateMappableAttribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE; + } + public String determineMappedBy(ManagedType managedType, String mapping, MetamodelBuildingContext context, EmbeddableOwner embeddableMapping) { String mappedBy; if (embeddableMapping == null) { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ParameterAttributeMapping.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ParameterAttributeMapping.java index 7678bd79e9..9645b90d68 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ParameterAttributeMapping.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/metamodel/ParameterAttributeMapping.java @@ -35,6 +35,7 @@ import com.blazebit.persistence.view.spi.EntityViewMapping; import com.blazebit.persistence.view.spi.EntityViewParameterMapping; +import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.ManagedType; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; @@ -100,6 +101,11 @@ public String getMappedBy() { return null; } + @Override + public boolean determineDisallowOwnedUpdatableSubview(MetamodelBuildingContext context, EmbeddableOwner embeddableMapping, Attribute updateMappableAttribute) { + return false; + } + @Override public String determineMappedBy(ManagedType managedType, String mapping, MetamodelBuildingContext context, EmbeddableOwner embeddableMapping) { return null; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/DirtyTracker.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/DirtyTracker.java index ede50640bf..cc74fbc74e 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/DirtyTracker.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/DirtyTracker.java @@ -42,4 +42,6 @@ public interface DirtyTracker extends BasicDirtyTracker { public long $$_getSimpleDirty(); + public void $$_replaceAttribute(Object oldObject, int attributeIndex, Object newObject); + } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/MutableStateTrackable.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/MutableStateTrackable.java index ad88dbfe63..582dc87549 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/MutableStateTrackable.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/MutableStateTrackable.java @@ -18,6 +18,8 @@ import com.blazebit.persistence.view.spi.type.EntityViewProxy; +import java.util.List; + /** * @author Christian Beikov * @since 1.2.0 @@ -35,6 +37,17 @@ public interface MutableStateTrackable extends EntityViewProxy, DirtyTracker { public DirtyTracker $$_getParent(); + /** + * Returns an interleaved list of read only parent objects and parent indexes. + * + * @return An interleaved list of read only parent objects and parent indexes + */ + public List $$_getReadOnlyParents(); + + public void $$_addReadOnlyParent(DirtyTracker readOnlyParent, int parentIndex); + + public void $$_removeReadOnlyParent(DirtyTracker readOnlyParent, int parentIndex); + public int $$_getParentIndex(); public void $$_setIsNew(boolean isNew); 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 61b494487f..fa84ff2490 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 @@ -277,6 +277,7 @@ private Class createProxyClass(EntityViewManager entityViewMana boolean dirtyChecking = false; CtField dirtyField = null; + CtField readOnlyParentsField = null; CtField parentField = null; CtField parentIndexField = null; CtField initialStateField = null; @@ -307,6 +308,10 @@ private Class createProxyClass(EntityViewManager entityViewMana mutableStateField.setModifiers(getModifiers(false)); cc.addField(mutableStateField); + readOnlyParentsField = new CtField(pool.get(List.class.getName()), "$$_readOnlyParents", cc); + readOnlyParentsField.setModifiers(getModifiers(true)); + readOnlyParentsField.setGenericSignature(Descriptor.of(List.class.getName()) + "<" + Descriptor.of(Object.class.getName()) + ">;"); + cc.addField(readOnlyParentsField); parentField = new CtField(pool.get(DirtyTracker.class.getName()), "$$_parent", cc); parentField.setModifiers(getModifiers(true)); cc.addField(parentField); @@ -317,11 +322,14 @@ private Class createProxyClass(EntityViewManager entityViewMana dirtyChecking = true; addGetter(cc, mutableStateField, "$$_getMutableState"); + addGetter(cc, readOnlyParentsField, "$$_getReadOnlyParents"); addGetter(cc, parentField, "$$_getParent"); addGetter(cc, parentIndexField, "$$_getParentIndex"); + addAddReadOnlyParent(cc, readOnlyParentsField, parentField); + addRemoveReadOnlyParent(cc, readOnlyParentsField); addSetParent(cc, parentField, parentIndexField); addHasParent(cc, parentField); - addUnsetParent(cc, parentField, parentIndexField); + addUnsetParent(cc, parentField, parentIndexField, readOnlyParentsField); markDirtyStub = addMarkDirtyStub(cc); } @@ -387,6 +395,7 @@ private Class createProxyClass(EntityViewManager entityViewMana } if (dirtyChecking) { + addReplaceAttribute(cc, mutableAttributes); cc.removeMethod(markDirtyStub); if (mutableAttributeCount > 64) { throw new IllegalArgumentException("Support for more than 64 mutable attributes per view is not yet implemented! " + viewType.getJavaType().getName() + " has " + mutableAttributeCount); @@ -1247,6 +1256,100 @@ private boolean supportsDirtyTracking(AbstractMethodAttribute mutableAttri } } + private CtMethod addAddReadOnlyParent(CtClass cc, CtField readOnlyParentField, CtField parentField) throws CannotCompileException { + FieldInfo parentFieldInfo = readOnlyParentField.getFieldInfo2(); + String desc = "(" + Descriptor.of(DirtyTracker.class.getName()) + "I)V"; + ConstPool cp = parentFieldInfo.getConstPool(); + MethodInfo minfo = new MethodInfo(cp, "$$_addReadOnlyParent", desc); + minfo.setAccessFlags(AccessFlag.PUBLIC); + String readOnlyParentFieldName = parentFieldInfo.getName(); + String parentFieldName = parentField.getName(); + + StringBuilder sb = new StringBuilder(); + + sb.append("{\n"); + + // Strict check for write-parent + sb.append("\tif ($0.").append(parentFieldName).append(" == null) {\n"); + sb.append("\t\tthrow new IllegalStateException(\"Can't set read only parent for object \" + $0.toString() + \" util it doesn't have a writable parent! First add the object to an attribute with proper cascading. If you just want to reference it convert the object with EntityViewManager.getReference() or EntityViewManager.convert()!\");\n"); + sb.append("\t}\n"); + + sb.append("\tif ($0.").append(readOnlyParentFieldName).append(" == null) {\n"); + sb.append("\t\t$0.").append(readOnlyParentFieldName).append(" = new java.util.ArrayList();\n"); + sb.append("\t}\n"); + + sb.append("\t$0.").append(readOnlyParentFieldName).append(".add($1);\n"); + sb.append("\t$0.").append(readOnlyParentFieldName).append(".add(Integer#valueOf($2));\n"); + + sb.append('}'); + + CtMethod method = CtMethod.make(minfo, cc); + method.setBody(sb.toString()); + cc.addMethod(method); + return method; + } + + private CtMethod addRemoveReadOnlyParent(CtClass cc, CtField readOnlyParentField) throws CannotCompileException { + FieldInfo parentFieldInfo = readOnlyParentField.getFieldInfo2(); + String desc = "(" + Descriptor.of(DirtyTracker.class.getName()) + "I)V"; + ConstPool cp = parentFieldInfo.getConstPool(); + MethodInfo minfo = new MethodInfo(cp, "$$_removeReadOnlyParent", desc); + minfo.setAccessFlags(AccessFlag.PUBLIC); + String readOnlyParentFieldName = parentFieldInfo.getName(); + + StringBuilder sb = new StringBuilder(); + + sb.append("{\n"); + + sb.append("\tif ($0.").append(readOnlyParentFieldName).append(" != null) {\n"); + sb.append("\t\tint size = $0.").append(readOnlyParentFieldName).append(".size();\n"); + sb.append("\t\tfor (int i = 0; i < size; i += 2) {\n"); + sb.append("\t\t\tif ($0.").append(readOnlyParentFieldName).append(".get(i) == $1 && ((Integer) $0.").append(readOnlyParentFieldName).append(".get(i + 1)).intValue() == $2) {\n"); + sb.append("\t\t\t\t$0.").append(readOnlyParentFieldName).append(".remove(i + 1);\n"); + sb.append("\t\t\t\t$0.").append(readOnlyParentFieldName).append(".remove(i);\n"); + sb.append("\t\t\t\tbreak;\n"); + sb.append("\t\t\t}\n"); + sb.append("\t\t}\n"); + sb.append("\t}\n"); + + sb.append('}'); + + CtMethod method = CtMethod.make(minfo, cc); + method.setBody(sb.toString()); + cc.addMethod(method); + return method; + } + + private CtMethod addReplaceAttribute(CtClass cc, AbstractMethodAttribute[] attributes) throws CannotCompileException { + String desc = "(" + Descriptor.of(Object.class.getName()) + "I" + Descriptor.of(Object.class.getName()) + ")V"; + ConstPool cp = cc.getClassFile().getConstPool(); + MethodInfo minfo = new MethodInfo(cp, "$$_replaceAttribute", desc); + minfo.setAccessFlags(AccessFlag.PUBLIC); + + StringBuilder sb = new StringBuilder(); + + sb.append("{\n"); + + sb.append("\tswitch ($2) {"); + for (int i = 0; i < attributes.length; i++) { + AbstractMethodAttribute attribute = attributes[i]; + if (attribute != null && attribute.hasDirtyStateIndex() && ReflectionUtils.getSetter(attribute.getDeclaringType().getJavaType(), attribute.getName()) != null) { + sb.append("\t\tcase ").append(attribute.getDirtyStateIndex()).append(": $0.set").append(Character.toUpperCase(attribute.getName().charAt(0))).append(attribute.getName(), 1, attribute.getName().length()); + sb.append("((").append(attribute.getJavaType().getName()).append(") $3); break;\n"); + } + } + + sb.append("\t\tdefault: throw new IllegalArgumentException(\"Invalid non-mutable attribute index: \" + $2);\n"); + sb.append("\t}\n"); + + sb.append('}'); + + CtMethod method = CtMethod.make(minfo, cc); + method.setBody(sb.toString()); + cc.addMethod(method); + return method; + } + private CtMethod addSetParent(CtClass cc, CtField parentField, CtField parentIndexField) throws CannotCompileException { FieldInfo parentFieldInfo = parentField.getFieldInfo2(); FieldInfo parentIndexFieldInfo = parentIndexField.getFieldInfo2(); @@ -1294,7 +1397,7 @@ private CtMethod addHasParent(CtClass cc, CtField parentField) throws CannotComp return method; } - private CtMethod addUnsetParent(CtClass cc, CtField parentField, CtField parentIndexField) throws CannotCompileException { + private CtMethod addUnsetParent(CtClass cc, CtField parentField, CtField parentIndexField, CtField readOnlyParentsField) throws CannotCompileException { FieldInfo parentFieldInfo = parentField.getFieldInfo2(); FieldInfo parentIndexFieldInfo = parentIndexField.getFieldInfo2(); String desc = "()V"; @@ -1303,10 +1406,14 @@ private CtMethod addUnsetParent(CtClass cc, CtField parentField, CtField parentI minfo.setAccessFlags(AccessFlag.PUBLIC); String parentFieldName = parentFieldInfo.getName(); String parentIndexFieldName = parentIndexFieldInfo.getName(); + String readOnlyParentsFieldName = readOnlyParentsField.getName(); StringBuilder sb = new StringBuilder(); sb.append("{\n"); + sb.append("\tif ($0.").append(parentFieldName).append(" != null && $0.").append(readOnlyParentsFieldName).append(" != null && !$0.").append(readOnlyParentsFieldName).append(".isEmpty()) {\n"); + sb.append("\t\tthrow new IllegalStateException(\"Can't unset writable parent \" + $0.").append(parentFieldName).append(" + \" on object \" + $0.toString() + \" because it is still connected to read only parents: \" + $0.").append(readOnlyParentsFieldName).append(");\n"); + sb.append("\t}\n"); sb.append("\t$0.").append(parentFieldName).append(" = null;\n"); sb.append("\t$0.").append(parentIndexFieldName).append(" = 0;\n"); @@ -1472,6 +1579,7 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, // Only consider subviews here for now if (attribute.isSubview()) { String subtypeArray = addAllowedSubtypeField(cc, attribute); + addParentRequiringSubtypeField(cc, attribute); sb.append("\tif ($1 != null) {\n"); sb.append("\t\tClass c;\n"); sb.append("\t\tif ($1 instanceof ").append(EntityViewProxy.class.getName()).append(") {\n"); @@ -1486,12 +1594,19 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, sb.append(".concat(c.getName())"); sb.append(");\n"); sb.append("\t\t}\n"); + + sb.append("\t\tif (").append(attributeField.getDeclaringClass().getName()).append('#').append(attribute.getName()).append("_$$_parentRequiringSubtypes.contains(c) && !((").append(DirtyTracker.class.getName()).append(") $1).$$_hasParent()) {\n"); + sb.append("\t\t\tthrow new IllegalArgumentException("); + sb.append("\"Setting instances of type [\" + c.getName() + \"] on attribute '").append(attribute.getName()).append("' is not allowed until they are assigned to an attribute that cascades the type! If you want this attribute to cascade, annotate it with @UpdatableMapping(cascade = { UPDATE }) or @UpdatableMapping(cascade = { PERSIST })\""); + sb.append(");\n"); + sb.append("\t\t}\n"); sb.append("\t}\n"); } } } if (attribute != null && attribute.getDirtyStateIndex() != -1) { + int mutableStateIndex = attribute.getDirtyStateIndex(); // Unset previous object parent if (attribute.isCollection()) { sb.append("\tif ($0.").append(fieldName).append(" != null && $0.").append(fieldName).append(" != $1) {\n"); @@ -1506,12 +1621,18 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, } sb.append("\t}\n"); } else if (attribute.isSubview()) { - sb.append("\tif ($0.").append(fieldName).append(" != $1 && $0.").append(fieldName).append(" instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); - sb.append("\t\t((").append(BasicDirtyTracker.class.getName()).append(") $0.").append(fieldName).append(").$$_unsetParent();\n"); + if (attribute.isUpdatableOnly() && !attribute.isCorrelated()) { + sb.append("\tif ($0.").append(fieldName).append(" != $1 && $0.").append(fieldName).append(" instanceof ").append(MutableStateTrackable.class.getName()).append(") {\n"); + sb.append("\t\t\t((").append(MutableStateTrackable.class.getName()).append(") $0.").append(fieldName).append(").$$_removeReadOnlyParent($0, ").append(mutableStateIndex).append(");\n"); + sb.append("\t} else if ($0.").append(fieldName).append(" != $1 && $0.").append(fieldName).append(" instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); + sb.append("\t\t\t((").append(BasicDirtyTracker.class.getName()).append(") $0.").append(fieldName).append(").$$_unsetParent();\n"); + } else { + sb.append("\tif ($0.").append(fieldName).append(" != $1 && $0.").append(fieldName).append(" instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); + sb.append("\t\t((").append(BasicDirtyTracker.class.getName()).append(") $0.").append(fieldName).append(").$$_unsetParent();\n"); + } sb.append("\t}\n"); } - int mutableStateIndex = attribute.getDirtyStateIndex(); if (mutableStateField != null) { // this.mutableState[mutableStateIndex] = $1 sb.append("\t$0.").append(mutableStateField.getName()).append("[").append(mutableStateIndex).append("] = "); @@ -1535,8 +1656,16 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, sb.append("\t\t}\n"); } } else if (attribute.isSubview()) { - sb.append("\t\tif ($1 instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); - sb.append("\t\t\t((").append(BasicDirtyTracker.class.getName()).append(") $1).$$_setParent($0, ").append(mutableStateIndex).append(");\n"); + // Correlated attributes are treated special, we don't consider correlated attributes read-only parents + if (attribute.isUpdatableOnly() && !attribute.isCorrelated()) { + sb.append("\t\tif ($1 instanceof ").append(MutableStateTrackable.class.getName()).append(") {\n"); + sb.append("\t\t\t((").append(MutableStateTrackable.class.getName()).append(") $1).$$_addReadOnlyParent($0, ").append(mutableStateIndex).append(");\n"); + sb.append("\t\t} else if ($1 instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); + sb.append("\t\t\t((").append(BasicDirtyTracker.class.getName()).append(") $1).$$_setParent($0, ").append(mutableStateIndex).append(");\n"); + } else { + sb.append("\t\tif ($1 instanceof ").append(BasicDirtyTracker.class.getName()).append(") {\n"); + sb.append("\t\t\t((").append(BasicDirtyTracker.class.getName()).append(") $1).$$_setParent($0, ").append(mutableStateIndex).append(");\n"); + } sb.append("\t\t}\n"); } sb.append("\t}\n"); @@ -1969,6 +2098,7 @@ private void renderFieldInitialization(EntityViewManager entityViewManager, Mana if (methodAttribute instanceof PluralAttribute) { PluralAttribute pluralAttribute = (PluralAttribute) methodAttribute; addAllowedSubtypeField(attributeFields[i].getDeclaringClass(), methodAttribute); + addParentRequiringSubtypeField(attributeFields[i].getDeclaringClass(), methodAttribute); switch (pluralAttribute.getCollectionType()) { case MAP: @@ -2015,6 +2145,8 @@ private void renderFieldInitialization(EntityViewManager entityViewManager, Mana } sb.append(attributeFields[i].getDeclaringClass().getName()).append('#'); sb.append(methodAttribute.getName()).append("_$$_subtypes").append(','); + sb.append(attributeFields[i].getDeclaringClass().getName()).append('#'); + sb.append(methodAttribute.getName()).append("_$$_parentRequiringSubtypes").append(','); sb.append(methodAttribute.isUpdatable()).append(','); sb.append(methodAttribute.isOptimizeCollectionActionsEnabled()); sb.append(");\n"); @@ -2200,6 +2332,50 @@ private String addAllowedSubtypeField(CtClass declaringClass, AbstractMethodAttr return subtypeArray; } + private String addParentRequiringSubtypeField(CtClass declaringClass, AbstractMethodAttribute attribute) throws CannotCompileException { + Set> parentRequiringSubtypes = attribute.getParentRequiringSubtypes(); + String subtypeArray; + if (!parentRequiringSubtypes.isEmpty()) { + StringBuilder subtypeArrayBuilder = new StringBuilder(); + for (Class c : parentRequiringSubtypes) { + subtypeArrayBuilder.append(c.getName()); + subtypeArrayBuilder.append(", "); + } + + subtypeArrayBuilder.setLength(subtypeArrayBuilder.length() - 2); + subtypeArray = subtypeArrayBuilder.toString(); + } else { + subtypeArray = ""; + } + + try { + declaringClass.getDeclaredField(attribute.getName() + "_$$_parentRequiringSubtypes"); + return subtypeArray; + } catch (NotFoundException ex) { + } + + StringBuilder fieldSb = new StringBuilder(); + fieldSb.append("private static final java.util.Set "); + fieldSb.append(attribute.getName()); + fieldSb.append("_$$_parentRequiringSubtypes = "); + + if (!parentRequiringSubtypes.isEmpty()) { + fieldSb.append("new java.util.HashSet(java.util.Arrays.asList(new java.lang.Class[]{ "); + for (Class c : parentRequiringSubtypes) { + fieldSb.append(c.getName()); + fieldSb.append(".class, "); + } + + fieldSb.setLength(fieldSb.length() - 2); + fieldSb.append(" }));"); + } else { + fieldSb.append("java.util.Collections.emptySet();"); + } + CtField allowedSubtypesField = CtField.make(fieldSb.toString(), declaringClass); + declaringClass.addField(allowedSubtypesField); + return subtypeArray; + } + private String addEmptyObjectArray(CtClass declaringClass) throws NotFoundException, CannotCompileException { String name = "$$_empty_object_array"; CtField[] fields = declaringClass.getDeclaredFields(); 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 dda8c95468..a9f9907250 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 @@ -37,9 +37,11 @@ public interface InitialStateResetter { public void addRecordingMap(RecordingMap recordingMap, List> actions, Map addedKeys, Map removedKeys, Map addedElements, Map removedElements); - public void addPersistedView(MutableStateTrackable persistedView); + public int addPersistedView(MutableStateTrackable persistedView); - public void addPersistedView(MutableStateTrackable persistedView, Object oldId); + public int addPersistedView(MutableStateTrackable persistedView, Object oldId); + + public void addPersistedViewNewObject(int newObjectIndex, Object newObject); public void addUpdatedView(MutableStateTrackable updatedView); 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 388ea219b6..56a5abac23 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 @@ -28,6 +28,7 @@ import javax.transaction.Synchronization; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -71,25 +72,40 @@ public void addRecordingMap(RecordingMap recordingMap, List(); } persistedView.$$_setIsNew(false); persistedViews.add(persistedView); + persistedViews.add(null); persistedViews.add(NO_ID_MARKER); + persistedViews.add(persistedView.$$_getParent()); + persistedViews.add(persistedView.$$_getParentIndex()); + persistedViews.add(persistedView.$$_getReadOnlyParents() == null || persistedView.$$_getReadOnlyParents().isEmpty() ? Collections.emptyList() : new ArrayList<>(persistedView.$$_getReadOnlyParents())); persistedViews.add(persistedView.$$_resetDirty()); + return persistedViews.size() - 6; } @Override - public void addPersistedView(MutableStateTrackable persistedView, Object oldId) { + public int addPersistedView(MutableStateTrackable persistedView, Object oldId) { if (persistedViews == null) { persistedViews = new ArrayList<>(); } persistedView.$$_setIsNew(false); persistedViews.add(persistedView); + persistedViews.add(null); persistedViews.add(oldId); + persistedViews.add(persistedView.$$_getParent()); + persistedViews.add(persistedView.$$_getParentIndex()); + persistedViews.add(persistedView.$$_getReadOnlyParents() == null || persistedView.$$_getReadOnlyParents().isEmpty() ? Collections.emptyList() : new ArrayList<>(persistedView.$$_getReadOnlyParents())); persistedViews.add(persistedView.$$_resetDirty()); + return persistedViews.size() - 6; + } + + @Override + public void addPersistedViewNewObject(int newObjectIndex, Object newObject) { + persistedViews.set(newObjectIndex, newObject); } @Override @@ -177,14 +193,26 @@ public void afterCompletion(int status) { } } if (persistedViews != null) { - for (int i = 0; i < persistedViews.size(); i += 3) { + for (int i = 0; i < persistedViews.size(); i += 7) { MutableStateTrackable view = (MutableStateTrackable) persistedViews.get(i); + Object newObject = persistedViews.get(i + 1); view.$$_setIsNew(true); - Object id = persistedViews.get(i + 1); + Object id = persistedViews.get(i + 2); + DirtyTracker parent = (DirtyTracker) persistedViews.get(i + 3); + int parentIndex = (int) persistedViews.get(i + 4); + List readOnlyParents = (List) persistedViews.get(i + 5); if (id != NO_ID_MARKER) { view.$$_setId(id); } - view.$$_setDirty((long[]) persistedViews.get(i + 2)); + if (parent != null) { + parent.$$_replaceAttribute(newObject, parentIndex, view); + for (int j = 0; j < readOnlyParents.size(); j += 2) { + DirtyTracker readOnlyParent = (DirtyTracker) readOnlyParents.get(j); + int readOnlyParentIndex = (int) readOnlyParents.get(j + 1); + readOnlyParent.$$_replaceAttribute(newObject, readOnlyParentIndex, view); + } + } + view.$$_setDirty((long[]) persistedViews.get(i + 6)); } } if (updatedViews != null) { 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 699191771b..fe026637ef 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 @@ -320,6 +320,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } + @Override + public boolean requiresDeferredFlush(V value) { + return false; + } + protected final X persistOrMerge(EntityManager em, X object) { return persistOrMerge(em, object, elementDescriptor); } 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 bf7df09db3..80ffb449c5 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 @@ -496,6 +496,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } + @Override + public boolean requiresDeferredFlush(V value) { + return false; + } + @Override public DirtyAttributeFlusher, E, V> getDirtyFlusher(UpdateContext context, Object view, Object initial, Object current) { if (updatable) { 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 9fa6ea189d..13daf940f4 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 @@ -200,6 +200,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } + @Override + public boolean requiresDeferredFlush(V value) { + return false; + } + protected boolean executeActions(UpdateContext context, Collection jpaCollection, List>> actions, ViewToEntityMapper mapper) { for (CollectionAction> action : actions) { action.doAction(jpaCollection, context, mapper, removeListener); 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 082ef2db8b..e1c8ae4a53 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 @@ -138,6 +138,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } + @Override + public boolean requiresDeferredFlush(V value) { + return false; + } + @Override public DirtyChecker[] getNestedCheckers(V current) { throw new UnsupportedOperationException(); 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 9d4492acf6..73cde65240 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 @@ -386,6 +386,7 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer Object[] state = element.$$_getMutableState(); boolean optimisticLock = false; + List deferredFlushers = null; if (value instanceof DirtyStateTrackable) { Object[] initialState = ((DirtyStateTrackable) value).$$_getInitialState(); @@ -394,18 +395,34 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer for (int i = 0; i < state.length; i++) { DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { - optimisticLock |= flusher.isOptimisticLockProtected(); - Object newInitialValue = flusher.cloneDeep(value, initialState[i], state[i]); - flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[i], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[i]); - initialState[i] = flusher.getNewInitialValue(context, newInitialValue, state[i]); + if (flusher.requiresDeferredFlush(state[i])) { + if (deferredFlushers == null) { + deferredFlushers = new ArrayList<>(); + } + deferredFlushers.add(i); + optimisticLock |= flusher.isOptimisticLockProtected(); + } else { + optimisticLock |= flusher.isOptimisticLockProtected(); + Object newInitialValue = flusher.cloneDeep(value, initialState[i], state[i]); + flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[i], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[i]); + initialState[i] = flusher.getNewInitialValue(context, newInitialValue, state[i]); + } } } } else { for (int i = 0; i < state.length; i++) { DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { - optimisticLock |= flusher.isOptimisticLockProtected(); - flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[i], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[i]); + if (flusher.requiresDeferredFlush(state[i])) { + if (deferredFlushers == null) { + deferredFlushers = new ArrayList<>(); + } + deferredFlushers.add(i); + optimisticLock |= flusher.isOptimisticLockProtected(); + } else { + optimisticLock |= flusher.isOptimisticLockProtected(); + flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[i], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[i]); + } } } } @@ -423,6 +440,33 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer context.getInitialStateResetter().addVersionedView(element, element.$$_getVersion()); versionFlusher.flushQuery(context, parameterPrefix, query, ownerView, value, element.$$_getVersion(), null); } + + if (deferredFlushers != null) { + if (value instanceof DirtyStateTrackable) { + Object[] initialState = ((DirtyStateTrackable) value).$$_getInitialState(); + context.getInitialStateResetter().addState(initialState, initialState.clone()); + + for (int i = 0; i < deferredFlushers.size(); i++) { + final int index = deferredFlushers.get(i); + final DirtyAttributeFlusher flusher = flushers[index]; + if (flusher != null) { + optimisticLock |= flusher.isOptimisticLockProtected(); + Object newInitialValue = flusher.cloneDeep(value, initialState[index], state[index]); + flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[index], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[index]); + initialState[index] = flusher.getNewInitialValue(context, newInitialValue, state[index]); + } + } + } else { + for (int i = 0; i < deferredFlushers.size(); i++) { + final int index = deferredFlushers.get(i); + final DirtyAttributeFlusher flusher = flushers[index]; + if (flusher != null) { + optimisticLock |= flusher.isOptimisticLockProtected(); + flusher.flushQuery(context, parameterPrefix, query, ownerView, value, state[index], unmappedOwnerAwareCascadeDeleters == null ? null : unmappedOwnerAwareCascadeDeleters[index]); + } + } + } + } } public Object getEntityIdCopy(UpdateContext context, EntityViewProxy updatableProxy) { @@ -478,6 +522,7 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie RecordingMap recordingMap = null; Object removedValue = null; Set removedKeys = null; + List afterPersistFlushers = null; List deferredFlushers = null; boolean successful = false; try { @@ -553,6 +598,13 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie final DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { if (doPersist && flusher.requiresFlushAfterPersist(state[i])) { + if (afterPersistFlushers == null) { + afterPersistFlushers = new ArrayList<>(); + } + afterPersistFlushers.add(i); + wasDirty = true; + optimisticLock |= flusher.isOptimisticLockProtected(); + } else if (flusher.requiresDeferredFlush(state[i])) { if (deferredFlushers == null) { deferredFlushers = new ArrayList<>(); } @@ -574,6 +626,13 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie final DirtyAttributeFlusher flusher = flushers[i]; if (flusher != null) { if (doPersist && flusher.requiresFlushAfterPersist(state[i])) { + if (afterPersistFlushers == null) { + afterPersistFlushers = new ArrayList<>(); + } + afterPersistFlushers.add(i); + wasDirty = true; + optimisticLock |= flusher.isOptimisticLockProtected(); + } else if (flusher.requiresDeferredFlush(state[i])) { if (deferredFlushers == null) { deferredFlushers = new ArrayList<>(); } @@ -608,6 +667,9 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie } versionFlusher.flushEntity(context, entity, ownerView, value, updatableProxy.$$_getVersion(), null); } + if (deferredFlushers != null) { + deferredFlushEntity(context, entity, ownerView, updatableProxy, deferredFlushers); + } if (doPersist) { // If the class of the object is an entity, we persist the object context.getEntityManager().persist(entity); @@ -621,31 +683,45 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie successful = true; return wasDirty; } finally { + int newObjectIndex = -1; if (shouldPersist) { if (idFlusher == null) { // Embeddables don't have an id - context.getInitialStateResetter().addPersistedView(updatableProxy); + newObjectIndex = context.getInitialStateResetter().addPersistedView(updatableProxy); } else { - context.getInitialStateResetter().addPersistedView(updatableProxy, oldId); + newObjectIndex = context.getInitialStateResetter().addPersistedView(updatableProxy, oldId); } - - if (successful && deferredFlushers != null) { - deferredFlushEntity(context, entity, ownerView, updatableProxy, deferredFlushers); + if (successful && afterPersistFlushers != null) { + deferredFlushEntity(context, entity, ownerView, updatableProxy, afterPersistFlushers); } - } else if (successful && deferredFlushers != null) { - deferredFlushEntity(context, entity, ownerView, updatableProxy, deferredFlushers); + } else if (successful && afterPersistFlushers != null) { + deferredFlushEntity(context, entity, ownerView, updatableProxy, afterPersistFlushers); } + Object newObject = null; if (doPersist) { - Object newObject = updatableProxy; + newObject = updatableProxy; if (persistViewMapper != null) { newObject = persistViewMapper.map(newObject); + context.getInitialStateResetter().addPersistedViewNewObject(newObjectIndex, newObject); } + } + if (shouldPersist) { + } + if (doPersist) { if (recordingCollection != null && (recordingCollection.isHashBased() || persistViewMapper != null)) { // Reset the parent accordingly - updatableProxy.$$_unsetParent(); - if (newObject instanceof BasicDirtyTracker) { - ((BasicDirtyTracker) newObject).$$_setParent(parent, parentIndex); + List readOnlyParents = updatableProxy.$$_getReadOnlyParents(); + if (newObject != updatableProxy) { + if (newObject instanceof BasicDirtyTracker) { + ((BasicDirtyTracker) newObject).$$_setParent(parent, parentIndex); + if (newObject instanceof MutableStateTrackable && readOnlyParents != null) { + for (int i = 0; i < readOnlyParents.size(); i += 2) { + ((DirtyTracker) readOnlyParents.get(i)).$$_replaceAttribute(updatableProxy, (int) readOnlyParents.get(i + 1), newObject); + } + } + } + updatableProxy.$$_unsetParent(); } if (recordingCollection.getCurrentIterator() == null) { recordingCollection.getDelegate().add(newObject); @@ -654,9 +730,17 @@ public boolean flushEntity(UpdateContext context, Object entity, Object ownerVie } } else if (recordingMap != null && (persistViewMapper != null || updatableProxy.$$_getParentIndex() == 1 && recordingMap.isHashBased())) { // Reset the parent accordingly - updatableProxy.$$_unsetParent(); - if (newObject instanceof BasicDirtyTracker) { - ((BasicDirtyTracker) newObject).$$_setParent(parent, parentIndex); + List readOnlyParents = updatableProxy.$$_getReadOnlyParents(); + if (newObject != updatableProxy) { + if (newObject instanceof BasicDirtyTracker) { + ((BasicDirtyTracker) newObject).$$_setParent(parent, parentIndex); + if (newObject instanceof MutableStateTrackable) { + for (int i = 0; i < readOnlyParents.size(); i += 2) { + ((DirtyTracker) readOnlyParents.get(i)).$$_replaceAttribute(updatableProxy, (int) readOnlyParents.get(i + 1), newObject); + } + } + } + updatableProxy.$$_unsetParent(); } if (updatableProxy.$$_getParentIndex() == 1) { if (recordingMap.getCurrentIterator() == null) { @@ -955,6 +1039,11 @@ public boolean requiresFlushAfterPersist(Object value) { return false; } + @Override + public boolean requiresDeferredFlush(Object value) { + return false; + } + @Override public DirtyAttributeFlusher getDirtyFlusher(UpdateContext context, Object view, Object initial, Object current) { return this; 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 6001555ca7..11b71a1465 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 @@ -66,6 +66,8 @@ public interface DirtyAttributeFlusher, public boolean requiresFlushAfterPersist(V value); + public boolean requiresDeferredFlush(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 ca13af3264..90bbe4c71b 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 @@ -226,6 +226,11 @@ public boolean requiresFlushAfterPersist(V value) { return nestedGraphNode != null && nestedGraphNode.requiresFlushAfterPersist(value); } + @Override + public boolean requiresDeferredFlush(V value) { + return nestedGraphNode != null && nestedGraphNode.requiresDeferredFlush(value); + } + @Override public DirtyChecker[] getNestedCheckers(V current) { return viewToEntityMapper.getUpdater(current).getDirtyChecker().getNestedCheckers((DirtyStateTrackable) current); 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 5e1675faba..49ff70cfd5 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 @@ -507,6 +507,11 @@ public boolean requiresFlushAfterPersist(V value) { return false; } + @Override + public boolean requiresDeferredFlush(V value) { + return value instanceof EntityViewProxy && ((EntityViewProxy) value).$$_isNew() && !viewToEntityMapper.cascades(value); + } + @Override public DirtyChecker[] getNestedCheckers(V current) { return viewToEntityMapper.getUpdater(current).getDirtyChecker().getNestedCheckers((DirtyStateTrackable) current); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/correlated/updatableonly/EntityViewUpdateCorrelatedUpdatableOnlySubviewTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/correlated/updatableonly/EntityViewUpdateCorrelatedUpdatableOnlySubviewTest.java index 17053b1d4f..1a27455568 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/correlated/updatableonly/EntityViewUpdateCorrelatedUpdatableOnlySubviewTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/correlated/updatableonly/EntityViewUpdateCorrelatedUpdatableOnlySubviewTest.java @@ -33,6 +33,7 @@ import org.junit.runners.Parameterized; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @@ -91,27 +92,11 @@ public void testUpdateWithSubview() { clearQueries(); // When - docView.setResponsiblePerson(newPerson); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isFullMode()) { - if (isQueryStrategy()) { - builder.update(Document.class); - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - } + try { + docView.setResponsiblePerson(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView); - assertEquals(p1.getId(), doc1.getResponsiblePerson().getId()); } @Test @@ -123,27 +108,11 @@ public void testUpdateWithModifySubview() { // When newPerson.setName("newOwner"); - docView.setResponsiblePerson(newPerson); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isFullMode()) { - if (isQueryStrategy()) { - builder.update(Document.class); - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - } + try { + docView.setResponsiblePerson(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - builder.validate(); - - assertNoUpdateAndReload(docView); - Assert.assertEquals(p1.getId(), doc1.getResponsiblePerson().getId()); - Assert.assertEquals("pers2", p2.getName()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/EntityViewUpdateRollbackTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/EntityViewUpdateRollbackTest.java index 0803e47706..f02d165955 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/EntityViewUpdateRollbackTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/EntityViewUpdateRollbackTest.java @@ -23,8 +23,12 @@ import com.blazebit.persistence.testsuite.tx.TxVoidWork; import com.blazebit.persistence.view.FlushMode; import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.impl.proxy.DirtyTracker; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.rollback.model.PersonNameView; import com.blazebit.persistence.view.testsuite.update.rollback.model.UpdatableDocumentRollbackView; +import com.blazebit.persistence.view.testsuite.update.rollback.model.CreatePersonRollbackView; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @@ -34,7 +38,7 @@ import javax.persistence.EntityTransaction; import java.util.Date; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * @@ -55,6 +59,12 @@ public static Object[][] combinations() { return MODE_STRATEGY_VERSION_COMBINATIONS; } + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.addEntityView(PersonNameView.class); + cfg.addEntityView(CreatePersonRollbackView.class); + } + @Test public void testUpdateRollbacked() { // Given @@ -136,6 +146,33 @@ public void work(EntityManager em) { assertEquals("newNewDoc", docView.getName()); } + @Test + public void testRollbackPersisted() { + // Given + final UpdatableDocumentRollbackView docView = getDoc1View(); + CreatePersonRollbackView personView = evm.create(CreatePersonRollbackView.class); + personView.setName("test"); + + // When 1 + docView.setOwner(personView); + assertTrue(((DirtyTracker) personView).$$_hasParent()); + updateWithRollback(docView); + + // Then 1 + restartTransactionAndReload(); + assertTrue(docView.getOwner() == personView); + assertTrue(((DirtyTracker) personView).$$_hasParent()); + + // When 2 + update(docView); + + // Then 2 + assertNoUpdateAndReload(docView); + assertFalse(docView.getOwner() == personView); + assertFalse(((DirtyTracker) personView).$$_hasParent()); + assertEquals(doc1.getOwner().getId(), personView.getId()); + } + @Override protected AssertStatementBuilder fullFetch(AssertStatementBuilder builder) { return builder.assertSelect() diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/CreatePersonRollbackView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/CreatePersonRollbackView.java new file mode 100644 index 0000000000..de51707158 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/CreatePersonRollbackView.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.rollback.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@CreatableEntityView(excludedEntityAttributes = { "age" }) +@EntityView(Person.class) +public interface CreatePersonRollbackView extends PersonNameView { + + public void setName(String name); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/PersonNameView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/PersonNameView.java new file mode 100644 index 0000000000..d89f6e9320 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/PersonNameView.java @@ -0,0 +1,37 @@ +/* + * 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.rollback.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@EntityView(Person.class) +public interface PersonNameView { + + @IdMapping + public Long getId(); + + public String getName(); + + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/UpdatableDocumentRollbackView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/UpdatableDocumentRollbackView.java index fd67ad69f4..bd867be93d 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/UpdatableDocumentRollbackView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/rollback/model/UpdatableDocumentRollbackView.java @@ -17,9 +17,11 @@ package com.blazebit.persistence.view.testsuite.update.rollback.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; @@ -45,4 +47,9 @@ public interface UpdatableDocumentRollbackView { public void setLastModified(Date date); + @UpdatableMapping(cascade = { CascadeType.PERSIST }) + public PersonNameView getOwner(); + + public void setOwner(PersonNameView owner); + } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedComplexTest.java similarity index 97% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedComplexTest.java index 3887ba8215..270e1e8cbd 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/EntityViewUpdateSubviewInverseEmbeddedComplexTest.java @@ -56,7 +56,7 @@ @RunWith(Parameterized.class) // NOTE: No Datanucleus support yet @Category({ NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateTest { +public class EntityViewUpdateSubviewInverseEmbeddedComplexTest extends AbstractEntityViewUpdateTest { @Override protected Class[] getEntityClasses() { @@ -69,7 +69,7 @@ protected Class[] getEntityClasses() { }; } - public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseEmbeddedComplexTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatableLegacyOrderView.class); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/model/UpdatableLegacyOrderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/model/UpdatableLegacyOrderView.java index 1c7ea4f385..f69591eaa2 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/model/UpdatableLegacyOrderView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/complex/model/UpdatableLegacyOrderView.java @@ -16,6 +16,7 @@ package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.complex.model; +import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -37,7 +38,7 @@ public interface UpdatableLegacyOrderView extends LegacyOrderIdView { @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) Set getPositions(); void setPositions(Set positions); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/entityid/model/UpdatableLegacyOrderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/entityid/model/UpdatableLegacyOrderView.java index 00747cefba..e01fa59fb3 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/entityid/model/UpdatableLegacyOrderView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/entityid/model/UpdatableLegacyOrderView.java @@ -16,6 +16,7 @@ package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.entityid.model; +import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -37,7 +38,7 @@ public interface UpdatableLegacyOrderView extends LegacyOrderIdView { @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) Set getPositions(); void setPositions(Set positions); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedSimpleTest.java similarity index 97% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedSimpleTest.java index 6dcb1b40b6..3bfa7e4699 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/EntityViewUpdateSubviewInverseEmbeddedSimpleTest.java @@ -63,7 +63,7 @@ @RunWith(Parameterized.class) // NOTE: No Datanucleus support yet @Category({ NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateTest { +public class EntityViewUpdateSubviewInverseEmbeddedSimpleTest extends AbstractEntityViewUpdateTest { @Override protected Class[] getEntityClasses() { @@ -76,7 +76,7 @@ protected Class[] getEntityClasses() { }; } - public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseEmbeddedSimpleTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatableLegacyOrderView.class); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/model/UpdatableLegacyOrderView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/model/UpdatableLegacyOrderView.java index 689bff6abc..aa717ee54c 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/model/UpdatableLegacyOrderView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/embedded/simple/model/UpdatableLegacyOrderView.java @@ -16,6 +16,7 @@ package com.blazebit.persistence.view.testsuite.update.subview.inverse.embedded.simple.model; +import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -37,7 +38,7 @@ public interface UpdatableLegacyOrderView extends LegacyOrderIdView { @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) Set getPositions(); void setPositions(Set positions); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseEmbeddedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseOneToOneEntityTest.java similarity index 96% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseEmbeddedTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseOneToOneEntityTest.java index 67732f404d..4fabb7abad 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseEmbeddedTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/entity/EntityViewUpdateSubviewInverseOneToOneEntityTest.java @@ -48,7 +48,7 @@ //@Category({ NoDatanucleus.class, NoEclipselink.class}) // NOTE: Hibernate has a bug when selecting inverse one-to-ones: https://hibernate.atlassian.net/browse/HHH-12885 @Category({ NoHibernate.class, NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateTest { +public class EntityViewUpdateSubviewInverseOneToOneEntityTest extends AbstractEntityViewUpdateTest { private DocumentForSimpleOneToOne doc1; private DocumentForSimpleOneToOne doc2; @@ -61,7 +61,7 @@ protected Class[] getEntityClasses() { }; } - public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseOneToOneEntityTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatableDocumentForOneToOneView.class); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseOneToOneSimpleTest.java similarity index 96% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseOneToOneSimpleTest.java index 0375cf0568..198bb09b67 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/onetoone/simple/EntityViewUpdateSubviewInverseOneToOneSimpleTest.java @@ -47,7 +47,7 @@ @RunWith(Parameterized.class) // NOTE: No Datanucleus support yet @Category({ NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateTest { +public class EntityViewUpdateSubviewInverseOneToOneSimpleTest extends AbstractEntityViewUpdateTest { private DocumentForSimpleOneToOne doc1; private DocumentForSimpleOneToOne doc2; @@ -60,7 +60,7 @@ protected Class[] getEntityClasses() { }; } - public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseOneToOneSimpleTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatableDocumentForOneToOneView.class); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseSimpleTest.java similarity index 93% rename from entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java rename to entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseSimpleTest.java index d1063a69fe..03c88b045f 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseEmbeddedTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/EntityViewUpdateSubviewInverseSimpleTest.java @@ -42,9 +42,9 @@ @RunWith(Parameterized.class) // NOTE: No Datanucleus support yet @Category({ NoDatanucleus.class, NoEclipselink.class}) -public class EntityViewUpdateSubviewInverseEmbeddedTest extends AbstractEntityViewUpdateDocumentTest { +public class EntityViewUpdateSubviewInverseSimpleTest extends AbstractEntityViewUpdateDocumentTest { - public EntityViewUpdateSubviewInverseEmbeddedTest(FlushMode mode, FlushStrategy strategy, boolean version) { + public EntityViewUpdateSubviewInverseSimpleTest(FlushMode mode, FlushStrategy strategy, boolean version) { super(mode, strategy, version, UpdatablePersonView.class); } diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/model/UpdatablePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/model/UpdatablePersonView.java index ef8e43d2a5..8ce5462a14 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/model/UpdatablePersonView.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/inverse/simple/model/UpdatablePersonView.java @@ -17,6 +17,7 @@ package com.blazebit.persistence.view.testsuite.update.subview.inverse.simple.model; import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -40,7 +41,7 @@ public interface UpdatablePersonView extends PersonIdView { void setName(String name); @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) Set getOwnedDocuments2(); void setOwnedDocuments2(Set ownedDocuments2); } 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 index 97848b5331..ac216bbc2d 100644 --- 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 @@ -18,6 +18,7 @@ import com.blazebit.persistence.testsuite.entity.Document; import com.blazebit.persistence.view.AllowUpdatableEntityViews; +import com.blazebit.persistence.view.CascadeType; import com.blazebit.persistence.view.CreatableEntityView; import com.blazebit.persistence.view.EntityView; import com.blazebit.persistence.view.InverseRemoveStrategy; @@ -40,13 +41,13 @@ public interface UpdatableDocumentView extends DocumentIdView { public String getName(); public void setName(String name); - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) @AllowUpdatableEntityViews PersonIdView getOwner(); void setOwner(PersonIdView owner); @MappingInverse(removeStrategy = InverseRemoveStrategy.REMOVE) - @UpdatableMapping + @UpdatableMapping(cascade = { CascadeType.UPDATE, CascadeType.PERSIST }) Set getVersions(); void setVersions(Set versions); diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/EntityViewUpdateSubviewMultiParentInverseTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/EntityViewUpdateSubviewMultiParentInverseTest.java new file mode 100644 index 0000000000..5c08785773 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/EntityViewUpdateSubviewMultiParentInverseTest.java @@ -0,0 +1,252 @@ +/* + * 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.multiparent.inverse; + +import com.blazebit.persistence.testsuite.base.jpa.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.jpa.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.impl.ConfigurationProperties; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.inverse.model.DocumentIdView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.inverse.model.PersonView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.inverse.model.UpdatableDocumentWithGraphView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.inverse.model.UpdatablePersonView; +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 java.util.Collection; + +import static org.junit.Assert.*; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewUpdateSubviewMultiParentInverseTest extends AbstractEntityViewUpdateDocumentTest { + + public EntityViewUpdateSubviewMultiParentInverseTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentWithGraphView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.setProperty(ConfigurationProperties.UPDATER_DISALLOW_OWNED_UPDATABLE_SUBVIEW, "true"); + cfg.addEntityView(DocumentIdView.class); + cfg.addEntityView(PersonView.class); + cfg.addEntityView(UpdatablePersonView.class); + } + + @Override + protected String[] getFetchedCollections() { + return new String[] { "partners" }; + } + + @Test + public void testUpdateAddToCollectionAndSetNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + clearQueries(); + + // When + docView.getPartners().add(p2); + docView.setOwner(p2); + update(docView); + + // Then + verifyAdd(docView); + } + + @Test + public void testUpdateAddToCollectionAndSetMultipleNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + clearQueries(); + + // When + docView.getPartners().add(p2); + docView.setOwner(p2); + docView.setResponsiblePerson(p2); + update(docView); + + // Then + verifyAdd(docView); + } + + private void verifyAdd(UpdatableDocumentWithGraphView docView) { + // Assert that the document and the people are loaded, but only a relation insert is done + AssertStatementBuilder builder = assertUnorderedQuerySequence(); + + if (isQueryStrategy()) { + if (isFullMode()) { + builder.update(Person.class); + } + } else { + if (isFullMode()) { + fullFetch(builder); + } else { + builder.select(Document.class); + } + builder.select(Person.class); + } + + builder.update(Document.class); + + builder.update(Person.class) + .validate(); + + assertNoUpdateAndReload(docView, true); + assertSubviewEquals(doc1.getPartners(), docView.getPartners()); + } + + @Test + public void testUpdateAddNewToCollectionAndSetNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView pNew = evm.create(UpdatablePersonView.class); + pNew.setName("newPers"); + clearQueries(); + + // When + docView.getPartners().add(pNew); + docView.setOwner(pNew); + update(docView); + + // Then + verifyAddNew(docView); + } + + @Test + public void testUpdateAddNewToCollectionAndSetMultipleNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView pNew = evm.create(UpdatablePersonView.class); + pNew.setName("newPers"); + clearQueries(); + + // When + docView.getPartners().add(pNew); + docView.setOwner(pNew); + docView.setResponsiblePerson(pNew); + update(docView); + + // Then + verifyAddNew(docView); + } + + private void verifyAddNew(UpdatableDocumentWithGraphView docView) { + // Assert that the document and the people are loaded, but only a relation insert is done + AssertStatementBuilder builder = assertUnorderedQuerySequence(); + + if (isQueryStrategy()) { + if (isFullMode()) { + builder.update(Person.class); + builder.update(Person.class); + } + } else { + if (isFullMode()) { + fullFetch(builder); + } else { + builder.select(Document.class); + } + } + + builder.update(Document.class); + + builder.insert(Person.class) + .validate(); + + assertNoUpdateAndReload(docView, true); + assertSubviewEquals(doc1.getPartners(), docView.getPartners()); + } + + @Test + public void testUnsetParentWithReadOnlyParent() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + docView.getPartners().add(p2); + docView.setOwner(p2); + try { + docView.getPartners().remove(p2); + Assert.fail("Expected failure!"); + } catch (IllegalStateException ex) { + Assert.assertTrue(ex.getMessage(), ex.getMessage().contains("Can't unset writable parent")); + } + } + + public static void assertSubviewEquals(Collection persons, Collection personSubviews) { + if (persons == null) { + assertNull(personSubviews); + return; + } + + assertNotNull(personSubviews); + assertEquals(persons.size(), personSubviews.size()); + for (Person p : persons) { + boolean found = false; + for (PersonView pSub : personSubviews) { + if (p.getName().equals(pSub.getName())) { + found = true; + break; + } + } + + if (!found) { + Assert.fail("Could not find a person subview instance with the name: " + p.getName()); + } + } + } + + @Override + protected AssertStatementBuilder fullUpdate(AssertStatementBuilder builder) { + builder.update(Person.class); + builder.update(Person.class); + return versionUpdate(builder); + } + + @Override + protected AssertStatementBuilder fullFetch(AssertStatementBuilder builder) { + return builder.assertSelect() + .fetching(Document.class) + .fetching(Person.class) + .and(); + } + + @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/multiparent/inverse/model/DocumentIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/DocumentIdView.java new file mode 100644 index 0000000000..08af715fa6 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/DocumentIdView.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.multiparent.inverse.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@EntityView(Document.class) +public interface DocumentIdView { + + @IdMapping + public Long getId(); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/PersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/PersonView.java new file mode 100644 index 0000000000..3fd05e3f36 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/PersonView.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.testsuite.update.subview.multiparent.inverse.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.3.0 + */ +@EntityView(Person.class) +public interface PersonView extends IdHolderView { + + public String getName(); +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatableDocumentWithGraphView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatableDocumentWithGraphView.java new file mode 100644 index 0000000000..bec8b1466a --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatableDocumentWithGraphView.java @@ -0,0 +1,57 @@ +/* + * 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.multiparent.inverse.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.UpdatableEntityView; + +import java.util.List; +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentWithGraphView { + + @IdMapping + public Long getId(); + + public Long getVersion(); + + public String getName(); + + public void setName(String name); + + public PersonView getOwner(); + + public void setOwner(PersonView owner); + + public PersonView getResponsiblePerson(); + + public void setResponsiblePerson(PersonView responsiblePerson); + + public Set getPartners(); + + public void setPartners(Set partners); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatablePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatablePersonView.java new file mode 100644 index 0000000000..7e0bd92362 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/inverse/model/UpdatablePersonView.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.testsuite.update.subview.multiparent.inverse.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@CreatableEntityView(excludedEntityAttributes = "age") +@UpdatableEntityView +@EntityView(Person.class) +public interface UpdatablePersonView extends PersonView { + + public void setName(String name); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/EntityViewUpdateSubviewMultiParentJoinTableTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/EntityViewUpdateSubviewMultiParentJoinTableTest.java new file mode 100644 index 0000000000..d3afc4794b --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/EntityViewUpdateSubviewMultiParentJoinTableTest.java @@ -0,0 +1,274 @@ +/* + * 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.multiparent.jointable; + +import com.blazebit.persistence.testsuite.base.jpa.assertion.AssertStatementBuilder; +import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.jpa.category.NoEclipselink; +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.FlushMode; +import com.blazebit.persistence.view.FlushStrategy; +import com.blazebit.persistence.view.impl.ConfigurationProperties; +import com.blazebit.persistence.view.spi.EntityViewConfiguration; +import com.blazebit.persistence.view.testsuite.update.AbstractEntityViewUpdateDocumentTest; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.jointable.model.DocumentIdView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.jointable.model.PersonView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.jointable.model.UpdatableDocumentWithGraphView; +import com.blazebit.persistence.view.testsuite.update.subview.multiparent.jointable.model.UpdatablePersonView; +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 java.util.Collection; + +import static org.junit.Assert.*; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@RunWith(Parameterized.class) +// NOTE: No Datanucleus support yet +@Category({ NoDatanucleus.class, NoEclipselink.class}) +public class EntityViewUpdateSubviewMultiParentJoinTableTest extends AbstractEntityViewUpdateDocumentTest { + + public EntityViewUpdateSubviewMultiParentJoinTableTest(FlushMode mode, FlushStrategy strategy, boolean version) { + super(mode, strategy, version, UpdatableDocumentWithGraphView.class); + } + + @Parameterized.Parameters(name = "{0} - {1} - VERSIONED={2}") + public static Object[][] combinations() { + return MODE_STRATEGY_VERSION_COMBINATIONS; + } + + @Override + protected void registerViewTypes(EntityViewConfiguration cfg) { + cfg.setProperty(ConfigurationProperties.UPDATER_DISALLOW_OWNED_UPDATABLE_SUBVIEW, "true"); + cfg.addEntityView(DocumentIdView.class); + cfg.addEntityView(PersonView.class); + cfg.addEntityView(UpdatablePersonView.class); + } + + @Override + protected String[] getFetchedCollections() { + return new String[] { "people" }; + } + + @Test + public void testUpdateAddToCollectionAndSetNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + clearQueries(); + + // When + docView.getPeople().add(p2); + docView.setOwner(p2); + update(docView); + + // Then + verifyAdd(docView); + } + + @Test + public void testUpdateAddToCollectionAndSetMultipleNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + clearQueries(); + + // When + docView.getPeople().add(p2); + docView.setOwner(p2); + docView.setResponsiblePerson(p2); + update(docView); + + // Then + verifyAdd(docView); + } + + private void verifyAdd(UpdatableDocumentWithGraphView docView) { + // Assert that the document and the people are loaded, but only a relation insert is done + AssertStatementBuilder builder = assertUnorderedQuerySequence(); + + if (isQueryStrategy()) { + if (isFullMode()) { + assertReplaceAnd(builder); + builder.update(Person.class); + builder.update(Person.class); + } + } else { + fullFetch(builder); + if (isFullMode()) { + builder.select(Person.class); + } + } + + builder.update(Document.class); + + builder.assertInsert() + .forRelation(Document.class, "people") + .validate(); + + assertNoUpdateAndReload(docView, true); + assertSubviewEquals(doc1.getPeople(), docView.getPeople()); + } + + @Test + public void testUpdateAddNewToCollectionAndSetNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView pNew = evm.create(UpdatablePersonView.class); + pNew.setName("newPers"); + clearQueries(); + + // When + docView.getPeople().add(pNew); + docView.setOwner(pNew); + update(docView); + + // Then + verifyAddNew(docView); + } + + @Test + public void testUpdateAddNewToCollectionAndSetMultipleNonCascading() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView pNew = evm.create(UpdatablePersonView.class); + pNew.setName("newPers"); + clearQueries(); + + // When + docView.getPeople().add(pNew); + docView.setOwner(pNew); + docView.setResponsiblePerson(pNew); + update(docView); + + // Then + verifyAddNew(docView); + } + + private void verifyAddNew(UpdatableDocumentWithGraphView docView) { + // Assert that the document and the people are loaded, but only a relation insert is done + AssertStatementBuilder builder = assertUnorderedQuerySequence(); + + if (isQueryStrategy()) { + if (isFullMode()) { + assertReplaceAnd(builder); + builder.update(Person.class); + } + } else { + fullFetch(builder); + } + + builder.update(Document.class); + + builder.insert(Document.class, "people") + .insert(Person.class) + .validate(); + + assertNoUpdateAndReload(docView, true); + assertSubviewEquals(doc1.getPeople(), docView.getPeople()); + } + + @Test + public void testUpdateSetNonCascadingOnly() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + try { + docView.setOwner(p2); + Assert.fail("Expected failure!"); + } catch (IllegalArgumentException ex) { + Assert.assertTrue(ex.getMessage(), ex.getMessage().contains("Setting instances of type")); + } + } + + @Test + public void testUnsetParentWithReadOnlyParent() { + // Given + final UpdatableDocumentWithGraphView docView = getDoc1View(); + UpdatablePersonView p2 = getP2View(UpdatablePersonView.class); + docView.getPeople().add(p2); + docView.setOwner(p2); + try { + docView.getPeople().remove(docView.getPeople().size() - 1); + Assert.fail("Expected failure!"); + } catch (IllegalStateException ex) { + Assert.assertTrue(ex.getMessage(), ex.getMessage().contains("Can't unset writable parent")); + } + } + + public static void assertSubviewEquals(Collection persons, Collection personSubviews) { + if (persons == null) { + assertNull(personSubviews); + return; + } + + assertNotNull(personSubviews); + assertEquals(persons.size(), personSubviews.size()); + for (Person p : persons) { + boolean found = false; + for (PersonView pSub : personSubviews) { + if (p.getName().equals(pSub.getName())) { + found = true; + break; + } + } + + if (!found) { + Assert.fail("Could not find a person subview instance with the name: " + p.getName()); + } + } + } + + private AssertStatementBuilder assertReplaceAnd(AssertStatementBuilder builder) { + builder.delete(Document.class, "people") + .insert(Document.class, "people"); + if (doc1.getPeople().size() > 1) { + builder.insert(Document.class, "people"); + } + return builder; + } + + @Override + protected AssertStatementBuilder fullUpdate(AssertStatementBuilder builder) { + assertReplaceAnd(builder); + builder.update(Person.class); + builder.update(Person.class); + return versionUpdate(builder); + } + + @Override + protected AssertStatementBuilder fullFetch(AssertStatementBuilder builder) { + return builder.assertSelect() + .fetching(Document.class) + .fetching(Document.class, "people") + .fetching(Person.class) + .and(); + } + + @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/multiparent/jointable/model/DocumentIdView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/DocumentIdView.java new file mode 100644 index 0000000000..afda16553d --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/DocumentIdView.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.multiparent.jointable.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@EntityView(Document.class) +public interface DocumentIdView { + + @IdMapping + public Long getId(); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/PersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/PersonView.java new file mode 100644 index 0000000000..f2e0dae819 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/PersonView.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.testsuite.update.subview.multiparent.jointable.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.3.0 + */ +@EntityView(Person.class) +public interface PersonView extends IdHolderView { + + public String getName(); +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatableDocumentWithGraphView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatableDocumentWithGraphView.java new file mode 100644 index 0000000000..b8995117a5 --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatableDocumentWithGraphView.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.view.testsuite.update.subview.multiparent.jointable.model; + +import com.blazebit.persistence.testsuite.entity.Document; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; +import com.blazebit.persistence.view.UpdatableEntityView; + +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@UpdatableEntityView +@EntityView(Document.class) +public interface UpdatableDocumentWithGraphView { + + @IdMapping + public Long getId(); + + public Long getVersion(); + + public String getName(); + + public void setName(String name); + + public PersonView getOwner(); + + public void setOwner(PersonView owner); + + public PersonView getResponsiblePerson(); + + public void setResponsiblePerson(PersonView responsiblePerson); + + public List getPeople(); + + public void setPeople(List people); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatablePersonView.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatablePersonView.java new file mode 100644 index 0000000000..5cf2ccc17d --- /dev/null +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/multiparent/jointable/model/UpdatablePersonView.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.testsuite.update.subview.multiparent.jointable.model; + +import com.blazebit.persistence.testsuite.entity.Person; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +@CreatableEntityView(excludedEntityAttributes = "age") +@UpdatableEntityView +@EntityView(Person.class) +public interface UpdatablePersonView extends PersonView { + + public void setName(String name); + +} diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewCollectionsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewCollectionsTest.java index 6c56063875..eef159dc19 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewCollectionsTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewCollectionsTest.java @@ -242,33 +242,11 @@ public void testUpdateModifyCollectionElement() { clearQueries(); // When - docView.getPeople().get(0).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - builder.update(Person.class); - if (version || isFullMode()) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - builder.update(Person.class); + try { + docView.getPeople().get(0).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoCollectionUpdateAndReload(docView); - assertEquals(p4.getId(), p1.getFriend().getId()); - assertSubviewEquals(doc1.getPeople(), docView.getPeople()); } @Test @@ -280,33 +258,11 @@ public void testUpdateModifyCollectionElementCopy() { // When newFriend.setName("newFriend"); - docView.getPeople().get(0).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder).update(Person.class); - versionUpdate(builder); - } - } else { - if (isFullMode()) { - fullFetch(builder); - if (version) { - versionUpdate(builder); - } - } + try { + docView.getPeople().get(0).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoCollectionUpdateAndReload(docView); - assertEquals(p3.getId(), p1.getFriend().getId()); - assertEquals("pers3", p3.getName()); - docView.getPeople().get(0).getFriend().setName("pers3"); - assertSubviewEquals(doc1.getPeople(), docView.getPeople()); } @Test @@ -318,35 +274,11 @@ public void testUpdateModifyCollectionElementAndModify() { // When newFriend.setName("newFriend"); - docView.getPeople().get(0).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - builder.update(Person.class); - if (version ||isFullMode()) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - builder.update(Person.class); + try { + docView.getPeople().get(0).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoCollectionUpdateAndReload(docView); - assertEquals(p4.getId(), p1.getFriend().getId()); - assertEquals("pers4", p4.getName()); - docView.getPeople().get(0).getFriend().setName("pers4"); - assertSubviewEquals(doc1.getPeople(), docView.getPeople()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewMapsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewMapsTest.java index 3202d21feb..5eac93f73b 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewMapsTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewMapsTest.java @@ -238,34 +238,11 @@ public void testUpdateModifyCollectionElement() { clearQueries(); // When - docView.getContacts().get(1).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } else { - builder.update(Person.class); - } - if (version || isFullMode()) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - builder.update(Person.class); - if (version) { - builder.update(Document.class); - } + try { + docView.getContacts().get(1).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView, true); - assertEquals(p4.getId(), p1.getFriend().getId()); - assertSubviewEquals(doc1.getContacts(), docView.getContacts()); } @Test @@ -277,35 +254,11 @@ public void testUpdateModifyCollectionElementCopy() { // When newFriend.setName("newFriend"); - docView.getContacts().get(1).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - if (version || isFullMode()) { - versionUpdate(builder); - } - } - } else { - if (isFullMode()) { - fullFetch(builder); - if (version) { - versionUpdate(builder); - } - } + try { + docView.getContacts().get(1).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView, true); - assertEquals(p3.getId(), p1.getFriend().getId()); - assertEquals("pers3", p3.getName()); - docView.getContacts().get(1).getFriend().setName("pers3"); - assertSubviewEquals(doc1.getContacts(), docView.getContacts()); } @Test @@ -317,36 +270,11 @@ public void testUpdateModifyCollectionElementAndModify() { // When newFriend.setName("newFriend"); - docView.getContacts().get(1).setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } else { - builder.update(Person.class); - } - if (version || isFullMode()) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - builder.update(Person.class); - if (version) { - builder.update(Document.class); - } + try { + docView.getContacts().get(1).setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView, true); - assertEquals(p4.getId(), p1.getFriend().getId()); - assertEquals("pers4", p4.getName()); - docView.getContacts().get(1).getFriend().setName("pers4"); - assertSubviewEquals(doc1.getContacts(), docView.getContacts()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewTest.java index d8f86af4e2..3b3d133945 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/nested/updatableonly/EntityViewUpdateNestedUpdatableOnlySubviewTest.java @@ -35,6 +35,7 @@ import org.junit.runners.Parameterized; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @@ -102,29 +103,11 @@ public void testUpdateWithSubview() { clearQueries(); // When - docView.getResponsiblePerson().setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - builder.update(Person.class); - if (isFullMode() || version) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - builder.update(Person.class); + try { + docView.getResponsiblePerson().setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView); - assertEquals(p4.getId(), doc1.getResponsiblePerson().getFriend().getId()); } @Test @@ -136,30 +119,11 @@ public void testUpdateWithModifySubview() { // When newFriend.setName("newFriend"); - docView.getResponsiblePerson().setFriend(newFriend); - update(docView); - - // Then - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - builder.update(Person.class); - if (isFullMode() || version) { - builder.update(Document.class); - } - } else { - fullFetch(builder); - if (version) { - builder.update(Document.class); - } - builder.update(Person.class); + try { + docView.getResponsiblePerson().setFriend(newFriend); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.validate(); - - assertNoUpdateAndReload(docView); - assertEquals(p4.getId(), doc1.getResponsiblePerson().getFriend().getId()); - assertEquals("pers4", p4.getName()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewCollectionsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewCollectionsTest.java index 7f16c33291..2053ed939b 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewCollectionsTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewCollectionsTest.java @@ -109,31 +109,11 @@ public void testUpdateAddToCollection() { clearQueries(); // When - docView.getPeople().add(newPerson); - update(docView); - - // Then - // Assert that the document and the people are loaded, but only a relation insert is done - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - } else { - fullFetch(builder); + try { + docView.getPeople().add(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Adding instances of type")); } - - if (version || isFullMode() && isQueryStrategy()) { - builder.update(Document.class); - } - - builder.assertInsert() - .forRelation(Document.class, "people") - .validate(); - - assertNoUpdateAndReload(docView); - assertSubviewEquals(doc1.getPeople(), docView.getPeople()); } @Test @@ -189,39 +169,11 @@ public void testUpdateAddToCollectionAndModifySubview() { // When newPerson.setName("newPerson"); - docView.getPeople().add(newPerson); - update(docView); - - // Then - // In partial mode, only the document is loaded. In full mode, the people are also loaded - // Since we load the people in the full mode, we do a proper diff and can compute that only a single item was added - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - } else { - if (isFullMode()) { - fullFetch(builder); - } else { - if (preferLoadingAndDiffingOverRecreate()) { - fullFetch(builder); - } else { - assertReplaceAnd(builder); - } - } + try { + docView.getPeople().add(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Adding instances of type")); } - if (version || isFullMode() && isQueryStrategy()) { - builder.update(Document.class); - } - - builder.assertInsert() - .forRelation(Document.class, "people") - .validate(); - assertNoUpdateAndReload(docView); - assertEquals(doc1.getPeople().size(), docView.getPeople().size()); - assertEquals("pers2", p2.getName()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewMapsTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewMapsTest.java index 1282c17eec..e871476bf7 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewMapsTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewMapsTest.java @@ -106,30 +106,11 @@ public void testUpdateAddToCollection() { clearQueries(); // When - docView.getContacts().put(2, newPerson); - update(docView); - - // Then - // Assert that the document and the people are loaded, but only a relation insert is done - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - } else { - fullFetch(builder); + try { + docView.getContacts().put(2, newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Putting instances of type")); } - - if (version || isQueryStrategy() && isFullMode()) { - builder.update(Document.class); - } - - builder.insert(Document.class, "contacts") - .validate(); - - assertNoUpdateAndReload(docView); - assertSubviewEquals(doc1.getContacts(), docView.getContacts()); } @Test @@ -184,39 +165,11 @@ public void testUpdateAddToCollectionAndModifySubview() { // When newPerson.setName("newPerson"); - docView.getContacts().put(2, newPerson); - update(docView); - - // Then - // In partial mode, only the document is loaded. In full mode, the people are also loaded - // Since we load the people in the full mode, we do a proper diff and can compute that only a single item was added - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (isQueryStrategy()) { - if (isFullMode()) { - assertReplaceAnd(builder); - } - } else { - if (isFullMode()) { - fullFetch(builder); - } else { - if (preferLoadingAndDiffingOverRecreate()) { - fullFetch(builder); - } else { - assertReplaceAnd(builder); - } - } - } - - if (version || isQueryStrategy() && isFullMode()) { - builder.update(Document.class); + try { + docView.getContacts().put(2, newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Putting instances of type")); } - - builder.insert(Document.class, "contacts") - .validate(); - assertNoUpdateAndReload(docView); - assertEquals(doc1.getContacts().size(), docView.getContacts().size()); - assertEquals("pers2", p2.getName()); } @Test diff --git a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewTest.java b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewTest.java index 4c2abc8417..cd89a441d3 100644 --- a/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewTest.java +++ b/entity-view/testsuite/src/test/java/com/blazebit/persistence/view/testsuite/update/subview/simple/updatableonly/EntityViewUpdateSimpleUpdatableOnlySubviewTest.java @@ -33,6 +33,7 @@ import org.junit.runners.Parameterized; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** * @@ -92,23 +93,11 @@ public void testUpdateWithSubview() { clearQueries(); // When - docView.setResponsiblePerson(newPerson); - update(docView); - - // Then - // Assert that only the document is loaded and finally also updated - // There is no need to actually load the person - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (!isQueryStrategy()) { - fullFetch(builder); + try { + docView.setResponsiblePerson(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.update(Document.class) - .validate(); - - assertNoUpdateAndReload(docView); - assertEquals(p2.getId(), doc1.getResponsiblePerson().getId()); } @Test @@ -120,24 +109,11 @@ public void testUpdateWithModifySubview() { // When newPerson.setName("newOwner"); - docView.setResponsiblePerson(newPerson); - update(docView); - - // Then - // Assert that only the document is loaded and finally also updated - // But the person is not updated - AssertStatementBuilder builder = assertUnorderedQuerySequence(); - - if (!isQueryStrategy()) { - fullFetch(builder); + try { + docView.setResponsiblePerson(newPerson); + } catch (IllegalArgumentException ex) { + assertTrue(ex.getMessage().contains("Setting instances of type")); } - - builder.update(Document.class) - .validate(); - - assertNoUpdateAndReload(docView); - Assert.assertEquals(p2.getId(), doc1.getResponsiblePerson().getId()); - Assert.assertEquals("pers2", p2.getName()); } @Test diff --git a/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpaProvider.java b/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpaProvider.java index e3012b036a..4b5a916aa6 100644 --- a/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpaProvider.java +++ b/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpaProvider.java @@ -196,7 +196,7 @@ public HibernateJpaProvider(PersistenceUnitUtil persistenceUnitUtil, String dbms // See https://hibernate.atlassian.net/browse/HHH-12775 for details this.supportsSingleValuedAssociationNaturalIdExpressions = major > 5 || major == 5 && minor >= 4; // See https://hibernate.atlassian.net/browse/HHH-13045 for details - this.needsElementCollectionIdCutoffForCompositeIdOwner = major < 5 || major == 5 && minor < 4 || major == 5 && minor == 4 && "SNAPSHOT".equals(type); + this.needsElementCollectionIdCutoffForCompositeIdOwner = major < 5 || major == 5 && minor < 4; } catch (Exception e) { throw new RuntimeException(e); }