Skip to content

Commit

Permalink
[#679] Add support for getting entity reference fro entity view and f…
Browse files Browse the repository at this point in the history
…ix support for spring data domain events
  • Loading branch information
beikov committed Nov 8, 2018
1 parent 637f19a commit 62d5717
Show file tree
Hide file tree
Showing 20 changed files with 386 additions and 25 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ 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`
* Add support for DeltaSpike Data 1.9
* 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

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ public static int findJoinStartIndex(CharSequence sqlSb, int tokenEnd, Set<JoinT
do {
tokenRange = SqlUtils.rtrimBackwardsToFirstWhitespace(sqlSb, tokenEnd);
tokenEnd = tokenRange[0] - 1;
JoinToken token = JoinToken.valueOf(sqlSb.subSequence(tokenRange[0], tokenRange[1]).toString().trim().toUpperCase());
JoinToken token = JoinToken.of(sqlSb.subSequence(tokenRange[0], tokenRange[1]).toString().trim().toUpperCase());
if (allowedTokens.contains(token)) {
allowedTokens = token.previous();
} else {
Expand All @@ -527,6 +527,7 @@ public static int findJoinStartIndex(CharSequence sqlSb, int tokenEnd, Set<JoinT
* @since 1.3.0
*/
enum JoinToken {
COMMA,
LEFT,
INNER,
RIGHT,
Expand All @@ -547,6 +548,15 @@ Set<JoinToken> previous() {
Set<JoinToken> 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) {
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Long> cb = cbf.create(em, Long.class)
.from(Document.class, "d")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tuple> cb = cbf.create(em, Tuple.class);
cb.fromValues(NameObjectContainer.class, "embeddable", Collections.singleton(new NameObjectContainer("test", new NameObject("abc", "123"))));
Expand Down Expand Up @@ -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<Tuple> cb = cbf.create(em, Tuple.class);
cb.fromValues(Document.class, "names", "t", Collections.singleton(new NameObject("123", "abc")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.<Cat>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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,6 @@ include::13_deltaspike_data.adoc[]

include::14_metamodel.adoc[]

include::15_configuration.adoc[]
include::15_configuration.adoc[]

include::16_faq.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ public interface EntityViewManager {
*/
public <T> T getReference(Class<T> 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 <T> The type of the entity class
* @return An entity reference for given entity view object
* @since 1.3.0
*/
public <T> T getEntityReference(EntityManager entityManager, Object entityView);

/**
* Gives access to the change model of the entity view instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -273,6 +275,20 @@ public <T> T getReference(Class<T> entityViewClass, Object id) {
}
}

@Override
public <T> 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> T create(Class<T> entityViewClass) {
Constructor<T> constructor = (Constructor<T>) createConstructorCache.get(entityViewClass);
Expand Down
Loading

0 comments on commit 62d5717

Please sign in to comment.