diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d750ab7d7..4c6dc8dd34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ Not yet released * Support for binding associations mapped by compound or foreign keys * Using a comparator with `List` and `Collection` types in entity views will sort the collection after load * Add option to force deduplication of elements in non-sets to `@CollectionMapping` -* Support `VALUES` clause with embeddable and most basic types +* Support plain `VALUES` clause with embeddable and most basic types * Support for binding embeddable parameters in CTEs, insert and update queries * Properly implement dirty state transfer when converting one entity view to another * Added validation for `equals`/`hashCode` implementations of JPA managed types that are used within entity views which can be disabled with the property `com.blazebit.persistence.view.managed_type_validation_disabled` @@ -21,6 +21,10 @@ Not yet released * Make use of Collection DML API when using the `QUERY` flush strategy in updatable entity views * Automatic embeddable splitting within `GROUP BY` clause to avoid Hibernate bugs * Support for entity view attribute filters and sorters on attributes of inheritance subtypes +* Introduced new method `EntityViewManager.getEntityReference()` to get an entity reference by an entity view object +* Allow to specify example attribute for `VALUES` clause for exact SQL types +* Implemented creatability validation for creatable entity views +* Implemented `SET_NULL` inverse remove strategy validation for updatable entity views ### Bug fixes @@ -39,7 +43,7 @@ Not yet released ### Backwards-incompatible changes -None yet +* Require `@AllowUpdatableEntityViews` to be able to use updatable entity view types by for *ToOne relationships in updatable entity views to avoid [possible problems](https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#updatable-mappings-subview) ## 1.3.0-Alpha3 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 7f981f254d..720b41fd5f 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 @@ -326,7 +326,7 @@ private void visitKeyOrIndexExpression(PathExpression pathExpression) { JoinNode node = (JoinNode) pathExpression.getBaseNode(); Attribute attribute = node.getParentTreeNode().getAttribute(); // Exclude element collections as they are not problematic - if (jpaProvider.getJpaMetamodelAccessor().isElementCollection(attribute)) { + if (!jpaProvider.getJpaMetamodelAccessor().isElementCollection(attribute)) { // There are weird mappings possible, we have to check if the attribute is a join table if (jpaProvider.getJoinTable(node.getParent().getEntityType(), attribute.getName()) != null) { keyRestrictedLeftJoins.add(node); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java b/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java index 39c9a87206..0a537b2096 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/util/SqlUtils.java @@ -511,7 +511,7 @@ public static int findJoinStartIndex(CharSequence sqlSb, int tokenEnd, Set previous() { Set previous() { return EnumSet.noneOf(JoinToken.class); } + + static JoinToken of(String text) { + switch (text) { + case ",": + return COMMA; + default: + return valueOf(text); + } + } } public static int findEndOfOnClause(CharSequence sqlSb, int predicateStartIndex, int whereIndex) { @@ -558,6 +568,7 @@ public static int findEndOfOnClause(CharSequence sqlSb, int predicateStartIndex, end = findJoinStartIndex(sqlSb, joinIndex, JoinToken.JOIN.previous()); } int potentialEndIndex = end; + int parenthesis = 0; QuoteMode mode = QuoteMode.NONE; for (int i = predicateStartIndex; i < end; i++) { char c = sqlSb.charAt(i); @@ -567,7 +578,9 @@ public static int findEndOfOnClause(CharSequence sqlSb, int predicateStartIndex, if (c == '(') { // While we are in a subcontext, consider the whole query end = whereIndex; + parenthesis++; } else if (c == ')') { + parenthesis--; // When we leave the context, reset the end to the potential end index if (i < potentialEndIndex) { end = potentialEndIndex; @@ -581,6 +594,9 @@ public static int findEndOfOnClause(CharSequence sqlSb, int predicateStartIndex, end = potentialEndIndex = findJoinStartIndex(sqlSb, joinIndex, JoinToken.JOIN.previous()); } } + } else if (c == ',' && parenthesis == 0) { + // Cross join via comma operator + return i; } } } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java index b8cbcbcde0..032ac63084 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java @@ -18,6 +18,7 @@ import com.blazebit.persistence.CriteriaBuilder; import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus; +import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus4; import com.blazebit.persistence.testsuite.base.jpa.category.NoFirebird; import com.blazebit.persistence.testsuite.base.jpa.category.NoH2; import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQL; @@ -138,6 +139,8 @@ public void testGroupByElementCollectionKey() { } @Test + // DataNucleus4 apparently can't handle join of associations within element collections + @Category({ NoDatanucleus4.class }) public void testGroupByElementCollectionValue() { CriteriaBuilder cb = cbf.create(em, Long.class) .from(Document.class, "d") diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java index dc7b2fe3d5..caa6b92db1 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java @@ -134,7 +134,8 @@ public void testValuesEntityFunctionWithEmbeddable() { } @Test - @Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) + // NOTE: Only the latest Hibernate 5.2 properly implements support for selecting element collections + @Category({ NoHibernate42.class, NoHibernate43.class, NoHibernate50.class, NoHibernate51.class, NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) public void testValuesEntityFunctionWithPluralOnlyEmbeddable() { CriteriaBuilder cb = cbf.create(em, Tuple.class); cb.fromValues(NameObjectContainer.class, "embeddable", Collections.singleton(new NameObjectContainer("test", new NameObject("abc", "123")))); @@ -253,7 +254,8 @@ public void testValuesEntityFunctionLikePluralBasic() { } @Test - @Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) + // NOTE: Only the latest Hibernate 5.2 properly implements support for selecting element collections + @Category({ NoHibernate42.class, NoHibernate43.class, NoHibernate50.class, NoHibernate51.class, NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class }) public void testValuesEntityFunctionLikePluralEmbeddable() { CriteriaBuilder cb = cbf.create(em, Tuple.class); cb.fromValues(Document.class, "names", "t", Collections.singleton(new NameObject("123", "abc"))); diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/07_querying_and_pagination_api.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/07_querying_and_pagination_api.adoc index d7d8adfaac..d5a14cb500 100644 --- a/documentation/src/main/asciidoc/entity-view/manual/en_US/07_querying_and_pagination_api.adoc +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/07_querying_and_pagination_api.adoc @@ -24,6 +24,28 @@ Allowing the actual data consumer i.e. the UI to specify these aspects is essent For a simple lookup by id there is also a convenience link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#find(javax.persistence.EntityManager,%20java.lang.Class,%20java.lang.Object)[`EntityViewManager.find()`] method available that allows you to skip some of the `CriteriaBuilder` ceremony and that works analogous to how `EntityManager.find()` works, but with entity views. +[source, java] +---- +CatView cat = entityViewManager.find(entityManager, CatView.class, catId); +---- + +To get just a _reference_ to an entity view similar to what an entity reference retrieved via `EntityManager.getReference()` represents, it is possible to use link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#getReference(%20java.lang.Class,%20java.lang.Object)[`EntityViewManager.getReference()`]. +Note that the returned object will only have the identifier set, all other attributes will have their default values. This is usually useful when wanting to compare a list of elements with some entity view type against an entity id +or also for setting *ToOne relationships. + +[source, java] +---- +CatView cat = entityViewManager.getReference(CatView.class, catId); +---- + +To get a _reference_ to an entity form an entity view one can use link:{entity_view_jdoc}/persistence/view/EntityViewManager.html#find(javax.persistence.EntityManager,%20java.lang.Object)[`EntityViewManager.getEntityReference()`] +which will return the entity reference object retrieved via `EntityManager.getReference()` for the given entity view object. + +[source, java] +---- +Cat cat = entityViewManager.getEntityReference(entityManager, catView); +---- + === Querying entity views Code in the presentation layer is intended to create an `EntityViewSetting` via the `create()` API and pass the entity view setting to a data access method. diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/16_faq.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/16_faq.adoc new file mode 100644 index 0000000000..09a4c81faa --- /dev/null +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/16_faq.adoc @@ -0,0 +1,39 @@ +== FAQ + +This section tries to cover some standard questions that often come up when introducing entity views or updatable entity views into a project +as well as some common problems with explanations and possible solutions. + +=== Why do I get an optimistic lock exception when updating an updatable entity view? + +The `com.blazebit.persistence.view.OptimisticLockException` is very similar to the `javax.persistence.OptimisticLockException` and when thrown, +it signals that an update isn't possible because of a change of an object that happened in the meantime. This can also happen when you do not use optimistic locking explicitly. + +==== If you try to update a non-existent entity + +When trying to update an entity that does not exist, the `EntityViewManager.update` operation will throw the `com.blazebit.persistence.view.OptimisticLockException`. + +* The entity could have been deleted in the meantime i.e. between loading the view and the update operation +* The entity view causing the exception is the result of a wrong usage of `EntityViewManager.convert` as it is missing the `ConvertOption.CREATE_NEW` + +==== If you try to update a concurrently updated entity + +Either the entity was updated within the current transaction or within another transaction through a different mechanism or a different entity view object. +If an update in a different transaction caused the exception, it is necessary to load the new version of the entity view and let the end-user enter the values to update again. +By inspecting the change model of the old instance one can assist the user by copying over non-conflicting value changes and just highlight conflicting changes. + +If a previous update in the same transaction causes the exception, the code should be adapted to prevent this from happening or updating the version on the entity view accordingly. + +=== Why do I get a "could not invoke proxy constructor" exception when fetching entity views? + +Entity views are type checked for most parts, but there are some dynamic non-declarative parts that can't be type checked that might cause this runtime exception when using a wrong result. +Usually, this happens when a `SubqueryProvider` or `CorrelationProvider` is in use. The implementations of these classes define the result type in a manner that is not type checkable. + +If a `SubqueryProvider` returns an integer via e.g. `select("1")` or `select("someIntAttribute")`, but the entity view attribute using the subquery provider uses a different type like e.g. `boolean`, +constructing an instance of that entity view might fail when trying to interpret the integer as boolean with an `IllegalArgumentException` saying that types are incompatible. +The obvious fix is to correct either the select item to return the correct type or the entity view attribute to declare the appropriate attribute type. +In case of a subquery provider it is also possible to wrap the subquery into a more complex expression by using e.g. `@MappingSubquery(value = MyProvider.class, subqueryAlias = "subquery", expression = "CASE WHEN EXISTS subquery THEN true ELSE false END")`. + +A `CorrelationProvider` can fail in a similar manner as it defines the entity type it correlates via `correlate(SomeEntity.class)`. +If the entity view attribute expects a different type that is not compatible, it will fail at runtime with an `IllegalArgumentException` saying that types are incompatible. +If a correlation result is defined via `@MappingCorrelated(correlationResult = "someAttributeOfCorrelatedEntity")` the type of that expression must be compatible which can be another cause for an error. +This problem can be fixed by adapting the correlation result expression, by changing the correlated entity in the correlation provider or by changing the declared attribute type. \ No newline at end of file diff --git a/documentation/src/main/asciidoc/entity-view/manual/en_US/index.adoc b/documentation/src/main/asciidoc/entity-view/manual/en_US/index.adoc index d08d7b8476..34a8d5e224 100644 --- a/documentation/src/main/asciidoc/entity-view/manual/en_US/index.adoc +++ b/documentation/src/main/asciidoc/entity-view/manual/en_US/index.adoc @@ -43,4 +43,6 @@ include::13_deltaspike_data.adoc[] include::14_metamodel.adoc[] -include::15_configuration.adoc[] \ No newline at end of file +include::15_configuration.adoc[] + +include::16_faq.adoc[] \ No newline at end of file diff --git a/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java b/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java index 29fe18f00b..2b0b8ed33a 100644 --- a/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java +++ b/entity-view/api/src/main/java/com/blazebit/persistence/view/EntityViewManager.java @@ -74,6 +74,17 @@ public interface EntityViewManager { */ public T getReference(Class entityViewClass, Object id); + /** + * Creates an entity reference for the given entity view and returns it. + * + * @param entityManager The entity manager to use for the entity reference + * @param entityView The entity view class for which to get the entity reference + * @param The type of the entity class + * @return An entity reference for given entity view object + * @since 1.3.0 + */ + public T getEntityReference(EntityManager entityManager, Object entityView); + /** * Gives access to the change model of the entity view instance. * diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java index 802c4299c8..e99b239d46 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/EntityViewManagerImpl.java @@ -54,10 +54,6 @@ import com.blazebit.persistence.view.impl.accessor.AttributeAccessor; import com.blazebit.persistence.view.impl.accessor.EntityIdAttributeAccessor; import com.blazebit.persistence.view.impl.change.ViewChangeModel; -import com.blazebit.persistence.view.impl.macro.EmbeddingViewJpqlMacro; -import com.blazebit.persistence.view.impl.mapper.ViewMapper; -import com.blazebit.persistence.view.impl.update.DefaultUpdateContext; -import com.blazebit.persistence.view.impl.update.UpdateContext; import com.blazebit.persistence.view.impl.filter.ContainsFilterImpl; import com.blazebit.persistence.view.impl.filter.ContainsIgnoreCaseFilterImpl; import com.blazebit.persistence.view.impl.filter.EndsWithFilterImpl; @@ -71,6 +67,8 @@ import com.blazebit.persistence.view.impl.filter.StartsWithFilterImpl; import com.blazebit.persistence.view.impl.filter.StartsWithIgnoreCaseFilterImpl; import com.blazebit.persistence.view.impl.macro.DefaultViewRootJpqlMacro; +import com.blazebit.persistence.view.impl.macro.EmbeddingViewJpqlMacro; +import com.blazebit.persistence.view.impl.mapper.ViewMapper; import com.blazebit.persistence.view.impl.metamodel.ManagedViewTypeImplementor; import com.blazebit.persistence.view.impl.metamodel.MappingConstructorImpl; import com.blazebit.persistence.view.impl.metamodel.MetamodelBuildingContext; @@ -82,8 +80,12 @@ import com.blazebit.persistence.view.impl.proxy.MutableStateTrackable; import com.blazebit.persistence.view.impl.proxy.ProxyFactory; import com.blazebit.persistence.view.impl.type.DefaultBasicUserTypeRegistry; +import com.blazebit.persistence.view.impl.update.DefaultUpdateContext; import com.blazebit.persistence.view.impl.update.EntityViewUpdater; import com.blazebit.persistence.view.impl.update.EntityViewUpdaterImpl; +import com.blazebit.persistence.view.impl.update.SimpleUpdateContext; +import com.blazebit.persistence.view.impl.update.UpdateContext; +import com.blazebit.persistence.view.impl.update.flush.CompositeAttributeFlusher; import com.blazebit.persistence.view.metamodel.ManagedViewType; import com.blazebit.persistence.view.metamodel.MappingConstructor; import com.blazebit.persistence.view.metamodel.ViewType; @@ -273,6 +275,20 @@ public T getReference(Class entityViewClass, Object id) { } } + @Override + public T getEntityReference(EntityManager entityManager, Object view) { + if (!(view instanceof EntityViewProxy)) { + throw new IllegalArgumentException("Can't remove non entity view object: " + view); + } + UpdateContext context = new SimpleUpdateContext(this, entityManager); + EntityViewProxy proxy = (EntityViewProxy) view; + Class entityViewClass = proxy.$$_getEntityViewClass(); + ManagedViewTypeImplementor viewType = metamodel.managedView(entityViewClass); + EntityViewUpdater updater = getUpdater(viewType, null, null, null); + Object entityId = ((CompositeAttributeFlusher) updater.getFullGraphNode()).getEntityIdCopy(context, proxy); + return (T) entityManager.getReference(proxy.$$_getJpaManagedClass(), entityId); + } + @Override public T create(Class entityViewClass) { Constructor constructor = (Constructor) createConstructorCache.get(entityViewClass); 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 a6f6741572..f45858fbbe 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 @@ -319,18 +319,27 @@ public void initializeViewMappings(MetamodelBuildingContext context) { isCollection = true; } // Also see AbstractMethodPluralAttribute#determineUpdatable() for the same logic - if (attributeViewMapping != null && (isUpdatable == Boolean.TRUE || getDeclaringView().isUpdatable())) { - if (hasSetter || isCollection && (cascadeTypes.contains(CascadeType.PERSIST) || attributeViewMapping.isCreatable())) { - boolean allowUpdatable; - if (isCollection) { - allowUpdatable = true; + if (attributeViewMapping != null) { + 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(); + } + // 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); } else { - allowUpdatable = !disallowOwnedUpdatableSubview || attributeViewMapping.isUpdatable(); + this.cascadeSubtypeMappings = Collections.emptyMap(); } - // But only if the attribute is explicitly or implicitly updatable - this.readOnlySubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), true, false); - this.cascadeSubtypeMappings = initializeDependentSubtypeMappingsAuto(context, attributeViewMapping.getEntityViewClass(), false, allowUpdatable); } 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); + } this.cascadeSubtypeMappings = Collections.emptyMap(); } } else { @@ -500,7 +509,7 @@ private Map initializeDependentSubtypeMappings(MetamodelBu return subtypeMappings; } - private Map initializeDependentSubtypeMappingsAuto(final MetamodelBuildingContext context, final Class clazz, boolean readOnly, boolean allowUpdatable) { + private Map initializeDependentSubtypeMappingsAuto(final MetamodelBuildingContext context, final Class clazz, boolean allowUpdatable, boolean allowCreatable) { Set> subtypes = context.findSubtypes(clazz); if (subtypes.size() == 0) { return Collections.emptyMap(); @@ -511,7 +520,7 @@ private Map initializeDependentSubtypeMappingsAuto(final M final ViewMapping subtypeMapping = context.getViewMapping(type); if (subtypeMapping == null) { unknownSubviewType(type); - } else if (!readOnly && (allowUpdatable && subtypeMapping.isUpdatable() || !subtypeMapping.isUpdatable()) || !subtypeMapping.isUpdatable() && !subtypeMapping.isCreatable()) { + } else if (allowSubtype(subtypeMapping, allowUpdatable, allowCreatable)) { // We can't initialize a potential subtype mapping here immediately, but have to wait until the current view mapping is fully initialized // This avoids access to partly initialized view mappings viewMapping.onInitializeViewMappingsFinished(new Runnable() { @@ -537,6 +546,16 @@ public void run() { return subtypeMappings; } + private static boolean allowSubtype(ViewMapping subtypeMapping, boolean allowUpdatable, boolean allowCreatable) { + if (allowUpdatable && subtypeMapping.isUpdatable()) { + return true; + } else if (allowCreatable && subtypeMapping.isCreatable()) { + return true; + } else { + return !subtypeMapping.isUpdatable() && !subtypeMapping.isCreatable(); + } + } + public MethodAttributeMapping handleReplacement(AttributeMapping original) { if (original == null) { return this; diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/SimpleUpdateContext.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/SimpleUpdateContext.java new file mode 100644 index 0000000000..59c12110ff --- /dev/null +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/SimpleUpdateContext.java @@ -0,0 +1,94 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.view.impl.update; + +import com.blazebit.persistence.view.impl.EntityViewManagerImpl; +import com.blazebit.persistence.view.impl.tx.TransactionSynchronizationStrategy; +import com.blazebit.persistence.view.impl.update.flush.PostFlushDeleter; + +import javax.persistence.EntityManager; +import java.util.List; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +public class SimpleUpdateContext implements UpdateContext { + + private final EntityViewManagerImpl evm; + private final EntityManager em; + + public SimpleUpdateContext(EntityViewManagerImpl evm, EntityManager em) { + this.evm = evm; + this.em = em; + } + + @Override + public EntityViewManagerImpl getEntityViewManager() { + return evm; + } + + @Override + public EntityManager getEntityManager() { + return em; + } + + @Override + public boolean containsEntity(Class entityClass, Object id) { + return evm.getJpaProvider().containsEntity(em, entityClass, id); + } + + @Override + public boolean isForceFull() { + return false; + } + + @Override + public boolean addVersionCheck(Class entityClass, Object id) { + return false; + } + + @Override + public boolean addRemovedObject(Object value) { + return false; + } + + @Override + public boolean isRemovedObject(Object value) { + return false; + } + + public TransactionSynchronizationStrategy getSynchronizationStrategy() { + return null; + } + + @Override + public InitialStateResetter getInitialStateResetter() { + return null; + } + + @Override + public List getOrphanRemovalDeleters() { + return null; + } + + @Override + public void removeOrphans(int orphanRemovalStartIndex) { + } + +} 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 6384e74fcb..26192c703a 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 @@ -60,6 +60,11 @@ */ public class CompositeAttributeFlusher extends CompositeAttributeFetchGraphNode> implements DirtyAttributeFlusher { + private static final Runnable EMPTY_RUNNABLE = new Runnable() { + @Override + public void run() { + } + }; private static final int FEATURE_SUPPORTS_QUERY_FLUSH = 0; private static final int FEATURE_HAS_PASS_THROUGH_FLUSHER = 1; private static final int FEATURE_IS_ANY_OPTIMISTIC_LOCK_PROTECTED = 2; @@ -420,7 +425,11 @@ public void flushQuery(UpdateContext context, String parameterPrefix, Query quer } } - private Object determineOldId(UpdateContext context, MutableStateTrackable updatableProxy, Runnable postReplaceListener) { + public Object getEntityIdCopy(UpdateContext context, EntityViewProxy updatableProxy) { + return determineOldId(context, updatableProxy, EMPTY_RUNNABLE); + } + + private Object determineOldId(UpdateContext context, EntityViewProxy updatableProxy, Runnable postReplaceListener) { if (updatableProxy.$$_getId() != null && postReplaceListener != null) { if (updatableProxy.$$_getId() instanceof EntityViewProxy) { // Copy the id view to preserve the original values diff --git a/integration/datanucleus-5.1/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleus51JpaMetamodelAccessor.java b/integration/datanucleus-5.1/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleus51JpaMetamodelAccessor.java index af41ac6fcd..21c924dbe0 100644 --- a/integration/datanucleus-5.1/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleus51JpaMetamodelAccessor.java +++ b/integration/datanucleus-5.1/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleus51JpaMetamodelAccessor.java @@ -78,12 +78,14 @@ public boolean isElementCollection(Attribute attribute) { if (attribute instanceof PluralAttribute) { Type.PersistenceType persistenceType = ((PluralAttribute) attribute).getElementType().getPersistenceType(); //CHECKSTYLE:OFF: FallThrough + //CHECKSTYLE:OFF: MissingSwitchDefault switch (persistenceType) { case BASIC: case EMBEDDABLE: return true; } //CHECKSTYLE:ON: FallThrough + //CHECKSTYLE:ON: MissingSwitchDefault } return false; } diff --git a/integration/datanucleus/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleusJpaMetamodelAccessor.java b/integration/datanucleus/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleusJpaMetamodelAccessor.java index 44321250c1..4016cf3057 100644 --- a/integration/datanucleus/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleusJpaMetamodelAccessor.java +++ b/integration/datanucleus/src/main/java/com/blazebit/persistence/integration/datanucleus/DataNucleusJpaMetamodelAccessor.java @@ -77,12 +77,14 @@ public boolean isElementCollection(Attribute attribute) { if (attribute instanceof PluralAttribute) { Type.PersistenceType persistenceType = ((PluralAttribute) attribute).getElementType().getPersistenceType(); //CHECKSTYLE:OFF: FallThrough + //CHECKSTYLE:OFF: MissingSwitchDefault switch (persistenceType) { case BASIC: case EMBEDDABLE: - return true; + return true; } //CHECKSTYLE:ON: FallThrough + //CHECKSTYLE:ON: MissingSwitchDefault } return false; } 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 a5d5f7820b..5617297635 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 @@ -78,6 +78,7 @@ public class HibernateJpaProvider implements JpaProvider { private static final Method GET_TYPE_NAME; + private static final Method IS_NULLABLE; private static final Logger LOG = Logger.getLogger(HibernateJpaProvider.class.getName()); protected final PersistenceUnitUtil persistenceUnitUtil; @@ -138,6 +139,14 @@ public class HibernateJpaProvider implements JpaProvider { } else { throw new IllegalStateException("Unknown Hibernate version in use, could not find AbstractType JPA metamodel class!"); } + + try { + Method isNullable = OneToOneType.class.getDeclaredMethod("isNullable"); + isNullable.setAccessible(true); + IS_NULLABLE = isNullable; + } catch (Exception ex) { + throw new IllegalStateException("Unknown Hibernate version in use, could not find isNullable method!", ex); + } } /** @@ -466,7 +475,10 @@ public boolean isForeignJoinColumn(EntityType ownerType, String attributeName Type propertyType = persister.getPropertyType(attributeName); if (propertyType instanceof OneToOneType) { - return ((OneToOneType) propertyType).getRHSUniqueKeyPropertyName() != null; + OneToOneType oneToOneType = (OneToOneType) propertyType; + // It is foreign if there is a mapped by attribute + // But as of Hibernate 5.4 we noticed that we have to treat nullable one-to-ones as "foreign" as well + return (oneToOneType).getRHSUniqueKeyPropertyName() != null || isNullable(oneToOneType); } // Every entity persister has "owned" properties on table number 0, others have higher numbers @@ -474,6 +486,14 @@ public boolean isForeignJoinColumn(EntityType ownerType, String attributeName return tableNumber >= persister.getEntityMetamodel().getSubclassEntityNames().size(); } + private boolean isNullable(OneToOneType oneToOneType) { + try { + return (boolean) IS_NULLABLE.invoke(oneToOneType); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override public boolean isColumnShared(EntityType ownerType, String attributeName) { AbstractEntityPersister persister = getEntityPersister(ownerType); @@ -531,6 +551,12 @@ public ConstraintType requiresTreatFilter(EntityType ownerType, String attrib return ConstraintType.NONE; } + protected boolean isForeignKeyDirectionToParent(org.hibernate.type.EntityType entityType) { + ForeignKeyDirection direction = entityType.getForeignKeyDirection(); + // Types changed between 4 and 5 so we check it like this. Essentially we check if the TO_PARENT direction is used + return direction.toString().regionMatches(true, 0, "to", 0, 2); + } + protected boolean isForeignKeyDirectionToParent(CollectionType collectionType) { ForeignKeyDirection direction = collectionType.getForeignKeyDirection(); // Types changed between 4 and 5 so we check it like this. Essentially we check if the TO_PARENT direction is used diff --git a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java index dcd3a96077..f9c324976a 100644 --- a/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java +++ b/integration/spring-data/1.x/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -26,7 +26,9 @@ import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryInformation; import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.intercept.MethodInterceptor; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; @@ -62,6 +64,7 @@ public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { private final CriteriaBuilderFactory cbf; private final EntityViewManager evm; private final QueryExtractor extractor; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; private List postProcessors; private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; @@ -72,6 +75,7 @@ public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBu this.cbf = cbf; this.evm = evm; addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor(evm)); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); } @Override @@ -126,6 +130,14 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return EntityViewAwareRepositoryImpl.class; } + @Override + public T getRepository(Class repositoryInterface, Object customImplementation) { + if (postProcessors != null && postProcessors.get(postProcessors.size() - 1) != entityViewReplacingMethodInterceptor) { + addRepositoryProxyPostProcessor(entityViewReplacingMethodInterceptor); + } + return super.getRepository(repositoryInterface, customImplementation); + } + @Override protected QueryLookupStrategy getQueryLookupStrategy(QueryLookupStrategy.Key key, EvaluationContextProvider evaluationContextProvider) { switch (key != null ? key : QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) { diff --git a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java index 19f2abc105..9bbce57673 100644 --- a/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java +++ b/integration/spring-data/2.0/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -26,7 +26,9 @@ import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.intercept.MethodInterceptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.beans.BeansException; @@ -81,6 +83,7 @@ public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { private final EntityViewManager evm; private final QueryExtractor extractor; private final Map repositoryInformationCache; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; private List postProcessors; private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; private Optional> repositoryBaseClass; @@ -103,6 +106,7 @@ public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBu this.evm = evm; addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor(evm)); this.repositoryBaseClass = Optional.empty(); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); } @Override @@ -337,6 +341,7 @@ public T getRepository(Class repositoryInterface, RepositoryComposition.R postProcessors.forEach(processor -> processor.postProcess(result, information)); + result.addAdvice(entityViewReplacingMethodInterceptor); result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); diff --git a/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java b/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java index 716f7ddee9..b75688cb25 100644 --- a/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java +++ b/integration/spring-data/2.1/src/main/java/com/blazebit/persistence/spring/data/impl/repository/BlazePersistenceRepositoryFactory.java @@ -26,7 +26,9 @@ import com.blazebit.persistence.spring.data.base.query.EntityViewAwareRepositoryMetadata; import com.blazebit.persistence.spring.data.impl.query.EntityViewAwareRepositoryMetadataImpl; import com.blazebit.persistence.spring.data.impl.query.PartTreeBlazePersistenceQuery; +import com.blazebit.persistence.spring.data.repository.EntityViewReplacingMethodInterceptor; import com.blazebit.persistence.view.EntityViewManager; +import org.aopalliance.intercept.MethodInterceptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; import org.springframework.beans.BeansException; @@ -82,6 +84,7 @@ public class BlazePersistenceRepositoryFactory extends JpaRepositoryFactory { private final EntityViewManager evm; private final QueryExtractor extractor; private final Map repositoryInformationCache; + private final EntityViewReplacingMethodInterceptor entityViewReplacingMethodInterceptor; private List postProcessors; private EntityViewAwareCrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor; private Optional> repositoryBaseClass; @@ -104,6 +107,7 @@ public BlazePersistenceRepositoryFactory(EntityManager entityManager, CriteriaBu this.evm = evm; addRepositoryProxyPostProcessor(this.crudMethodMetadataPostProcessor = new EntityViewAwareCrudMethodMetadataPostProcessor(evm)); this.repositoryBaseClass = Optional.empty(); + this.entityViewReplacingMethodInterceptor = new EntityViewReplacingMethodInterceptor(entityManager, evm); } @Override @@ -338,6 +342,7 @@ public T getRepository(Class repositoryInterface, RepositoryComposition.R postProcessors.forEach(processor -> processor.postProcess(result, information)); + result.addAdvice(entityViewReplacingMethodInterceptor); result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); diff --git a/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/repository/EntityViewReplacingMethodInterceptor.java b/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/repository/EntityViewReplacingMethodInterceptor.java new file mode 100644 index 0000000000..e5f950c06f --- /dev/null +++ b/integration/spring-data/base/src/main/java/com/blazebit/persistence/spring/data/repository/EntityViewReplacingMethodInterceptor.java @@ -0,0 +1,72 @@ +/* + * 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.spring.data.repository; + +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.spi.type.BasicDirtyTracker; +import com.blazebit.persistence.view.spi.type.EntityViewProxy; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; + +import javax.persistence.EntityManager; + +/** + * @author Christian Beikov + * @since 1.3.0 + */ +public class EntityViewReplacingMethodInterceptor implements MethodInterceptor, RepositoryProxyPostProcessor { + + private final EntityManager em; + private final EntityViewManager evm; + + public EntityViewReplacingMethodInterceptor(EntityManager em, EntityViewManager evm) { + this.em = em; + this.evm = evm; + } + + @Override + public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { + factory.addAdvice(this); + } + + @Override + public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable { + Object o = invocation.proceed(); + + if (invocation.getMethod().getName().startsWith("save")) { + Object[] arguments = invocation.getArguments(); + if (arguments.length == 1) { + arguments[0] = convertToEntity(arguments[0]); + } else { + return convertToEntity(o); + } + } + + return o; + } + + private Object convertToEntity(Object entityOrView) { + if (entityOrView instanceof BasicDirtyTracker) { + EntityViewProxy view = (EntityViewProxy) entityOrView; + return evm.getEntityReference(em, view); + } + return entityOrView; + } +}