Skip to content

Commit 7ff25ae

Browse files
authored
fix: Apply left outer join to retrieve optional associations [WIP] (#105)
* fix: Apply left outer join to retrieve optional associations * chore: polish GraphQLJPASchemaBuilder * chore: clean DriodFunction orphan * fix: change primaryFunction association type to @OnetoOne * feat: first crack at optional argument join handling * fix: set author association type to optional=false * fix: add optional unit tests * fix: compile error
1 parent 6e9da5b commit 7ff25ae

File tree

6 files changed

+341
-79
lines changed

6 files changed

+341
-79
lines changed

graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/DroidFunction.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

graphql-jpa-query-example-model-books/src/main/java/com/introproventures/graphql/jpa/query/schema/model/book/Book.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class Book {
4343
@GraphQLIgnoreFilter
4444
String description;
4545

46-
@ManyToOne(fetch=FetchType.LAZY)
46+
@ManyToOne(fetch=FetchType.LAZY, optional = false)
4747
Author author;
4848

4949
@Enumerated(EnumType.STRING)

graphql-jpa-query-example-model-starwars/src/main/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Droid.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
import javax.persistence.Entity;
2020
import javax.persistence.FetchType;
2121
import javax.persistence.JoinColumn;
22-
import javax.persistence.ManyToOne;
22+
import javax.persistence.OneToOne;
2323

2424
import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription;
25-
2625
import lombok.Data;
2726
import lombok.EqualsAndHashCode;
2827

@@ -32,7 +31,7 @@
3231
@EqualsAndHashCode(callSuper=true)
3332
public class Droid extends Character {
3433

35-
@ManyToOne(fetch = FetchType.LAZY)
34+
@OneToOne(fetch = FetchType.LAZY)
3635
@JoinColumn(name = "primary_function")
3736
DroidFunction primaryFunction;
3837

graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@
3636
import javax.persistence.Convert;
3737
import javax.persistence.EntityManager;
3838
import javax.persistence.Transient;
39-
import javax.persistence.metamodel.*;
39+
import javax.persistence.metamodel.Attribute;
40+
import javax.persistence.metamodel.EmbeddableType;
41+
import javax.persistence.metamodel.EntityType;
42+
import javax.persistence.metamodel.ManagedType;
43+
import javax.persistence.metamodel.PluralAttribute;
44+
import javax.persistence.metamodel.SingularAttribute;
45+
import javax.persistence.metamodel.Type;
4046

4147
import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription;
4248
import com.introproventures.graphql.jpa.query.annotation.GraphQLIgnore;
@@ -221,20 +227,12 @@ private GraphQLArgument distinctArgument(EntityType<?> entityType) {
221227
}
222228

223229
private GraphQLArgument getWhereArgument(ManagedType<?> managedType) {
224-
String typeName="";
225-
if (managedType instanceof EmbeddableType){
226-
typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType";
227-
} else if (managedType instanceof EntityType) {
228-
typeName = ((EntityType<?>)managedType).getName();
229-
}
230-
231-
String type = namingStrategy.pluralize(typeName)+"CriteriaExpression";
232-
233-
GraphQLArgument whereArgument = whereArgumentsMap.get(managedType.getJavaType());
234-
235-
if(whereArgument != null)
236-
return whereArgument;
237-
230+
return whereArgumentsMap.computeIfAbsent(managedType.getJavaType(), (javaType) -> computeWhereArgument(managedType));
231+
}
232+
233+
private GraphQLArgument computeWhereArgument(ManagedType<?> managedType) {
234+
String type=resolveWhereArgumentTypeName(managedType);
235+
238236
GraphQLInputObjectType whereInputObject = GraphQLInputObjectType.newInputObject()
239237
.name(type)
240238
.description("Where logical AND specification of the provided list of criteria expressions")
@@ -266,29 +264,38 @@ private GraphQLArgument getWhereArgument(ManagedType<?> managedType) {
266264
)
267265
.build();
268266

269-
whereArgument = GraphQLArgument.newArgument()
270-
.name(QUERY_WHERE_PARAM_NAME)
271-
.description("Where logical specification")
272-
.type(whereInputObject)
273-
.build();
274-
275-
whereArgumentsMap.put(managedType.getJavaType(), whereArgument);
276-
277-
return whereArgument;
267+
return GraphQLArgument.newArgument()
268+
.name(QUERY_WHERE_PARAM_NAME)
269+
.description("Where logical specification")
270+
.type(whereInputObject)
271+
.build();
278272

279273
}
280274

281-
private GraphQLInputObjectType getWhereInputType(ManagedType<?> managedType) {
282-
return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType);
275+
private String resolveWhereArgumentTypeName(ManagedType<?> managedType) {
276+
String typeName=resolveTypeName(managedType);
277+
278+
return namingStrategy.pluralize(typeName)+"CriteriaExpression";
283279
}
284280

285-
private String resolveWhereInputTypeName(ManagedType<?> managedType) {
281+
private String resolveTypeName(ManagedType<?> managedType) {
286282
String typeName="";
283+
287284
if (managedType instanceof EmbeddableType){
288285
typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType";
289286
} else if (managedType instanceof EntityType) {
290287
typeName = ((EntityType<?>)managedType).getName();
291288
}
289+
290+
return typeName;
291+
}
292+
293+
private GraphQLInputObjectType getWhereInputType(ManagedType<?> managedType) {
294+
return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType);
295+
}
296+
297+
private String resolveWhereInputTypeName(ManagedType<?> managedType) {
298+
String typeName=resolveTypeName(managedType);
292299

293300
return namingStrategy.pluralize(typeName)+"RelationCriteriaExpression";
294301

@@ -633,6 +640,8 @@ && isNotIgnoredOrder(attribute) ) {
633640

634641
// TODO fix page count query
635642
arguments.add(getWhereArgument(foreignType));
643+
644+
arguments.add(optionalArgument(SingularAttribute.class.cast(attribute)));
636645

637646
} // Get Sub-Objects fields queries via DataFetcher
638647
else if (attribute instanceof PluralAttribute
@@ -644,7 +653,7 @@ else if (attribute instanceof PluralAttribute
644653
arguments.add(getWhereArgument(elementType));
645654
dataFetcher = new GraphQLJpaOneToManyDataFetcher(entityManager, baseEntity, (PluralAttribute) attribute);
646655
}
647-
656+
648657
return GraphQLFieldDefinition.newFieldDefinition()
649658
.name(attribute.getName())
650659
.description(getSchemaDescription(attribute.getJavaMember()))
@@ -653,6 +662,15 @@ else if (attribute instanceof PluralAttribute
653662
.argument(arguments)
654663
.build();
655664
}
665+
666+
private GraphQLArgument optionalArgument(SingularAttribute<?,?> attribute) {
667+
return GraphQLArgument.newArgument()
668+
.name("optional")
669+
.description("Optional association specification")
670+
.type(Scalars.GraphQLBoolean)
671+
.defaultValue(attribute.isOptional())
672+
.build();
673+
}
656674

657675
protected ManagedType<?> getForeignType(Attribute<?,?> attribute) {
658676
if(SingularAttribute.class.isInstance(attribute))

graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@
9090
*/
9191
class QraphQLJpaBaseDataFetcher implements DataFetcher<Object> {
9292

93+
private static final String OPTIONAL = "optional";
94+
9395
// "__typename" is part of the graphql introspection spec and has to be ignored
9496
private static final String TYPENAME = "__typename";
9597

@@ -167,24 +169,35 @@ protected final List<Argument> getFieldArguments(Field field, CriteriaQuery<?> q
167169
// Process where arguments clauses.
168170
arguments.addAll(selectedField.getArguments()
169171
.stream()
170-
.filter(it -> !isOrderByArgument(it))
172+
.filter(it -> !isOrderByArgument(it) && !isOptionalArgument(it))
171173
.map(it -> new Argument(selectedField.getName() + "." + it.getName(), it.getValue()))
172174
.collect(Collectors.toList()));
173175

174-
// Check if it's an object and the foreign side is One. Then we can eagerly fetch causing an inner join instead of 2 queries
176+
// Check if it's an object and the foreign side is One. Then we can eagerly join causing an inner join instead of 2 queries
175177
if (fieldPath.getModel() instanceof SingularAttribute) {
176178
SingularAttribute<?,?> attribute = (SingularAttribute<?,?>) fieldPath.getModel();
177179
if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE
178180
|| attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE
179181
) {
180-
reuseJoin(from, selectedField.getName(), false);
182+
// Let's apply left outer join to retrieve optional associations
183+
Optional<Argument> optionalArgument = getArgument(selectedField, OPTIONAL);
184+
185+
// Let's do fugly conversion
186+
Boolean isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class))
187+
.orElse(attribute.isOptional());
188+
189+
reuseJoin(from, selectedField.getName(), isOptional);
181190
}
182191
}
183-
} else {
184-
// We must add plural attributes with explicit fetch to avoid Hibernate error:
192+
} else {
193+
// We must add plural attributes with explicit join to avoid Hibernate error:
185194
// "query specified join fetching, but the owner of the fetched association was not present in the select list"
186-
// TODO Let's try detect optional relation and apply join type
187-
reuseJoin(from, selectedField.getName(), false);
195+
PluralAttribute<?, ?, ?> attribute = getAttribute(selectedField.getName());
196+
197+
// Let's apply left outer join to retrieve optional many-to-many associations
198+
boolean isOptional = (PersistentAttributeType.MANY_TO_MANY == attribute.getPersistentAttributeType());
199+
200+
reuseJoin(from, selectedField.getName(), isOptional);
188201
}
189202
}
190203
}
@@ -251,6 +264,22 @@ protected boolean isOrderByArgument(Argument argument) {
251264
return GraphQLJpaSchemaBuilder.ORDER_BY_PARAM_NAME.equals(argument.getName());
252265
}
253266

267+
protected boolean isOptionalArgument(Argument argument) {
268+
return OPTIONAL.equals(argument.getName());
269+
}
270+
271+
protected Optional<Argument> getArgument(Field selectedField, String argumentName) {
272+
return selectedField.getArguments()
273+
.stream()
274+
.filter(it -> it.getName()
275+
.equals(argumentName))
276+
.findFirst();
277+
}
278+
279+
protected <R extends Attribute<?,?>> R getAttribute(String attributeName) {
280+
return (R) entityType.getAttribute(attributeName);
281+
}
282+
254283
@SuppressWarnings( "unchecked" )
255284
protected Predicate getPredicate(CriteriaBuilder cb, Root<?> from, From<?,?> path, DataFetchingEnvironment environment, Argument argument) {
256285

@@ -259,6 +288,7 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root<?> from, From<?,?> pat
259288

260289
// If the argument is a list, let's assume we need to join and do an 'in' clause
261290
if (argumentEntityAttribute instanceof PluralAttribute) {
291+
// Apply left outer join to retrieve optional associations
262292
return reuseJoin(from, argument.getName(), false)
263293
.in(convertValue(environment, argument, argument.getValue()));
264294
}
@@ -272,7 +302,7 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root<?> from, From<?,?> pat
272302
} else {
273303
String fieldName = argument.getName().split("\\.")[0];
274304

275-
From<?,?> join = getCompoundJoin(path, argument.getName(), false);
305+
From<?,?> join = getCompoundJoin(path, argument.getName(), true);
276306
Argument where = new Argument("where", argument.getValue());
277307
Map<String, Object> variables = Optional.ofNullable(environment.getContext())
278308
.filter(it -> it instanceof Map)
@@ -388,13 +418,17 @@ private Predicate getFieldPredicate(String fieldName, CriteriaBuilder cb, From<?
388418
this.getObjectType(environment, argument),
389419
new Field(fieldName));
390420
Map<String, Object> arguments = new LinkedHashMap<>();
421+
boolean isOptional = false;
391422

392-
if(Logical.names().contains(argument.getName()))
423+
if(Logical.names().contains(argument.getName())) {
393424
arguments.put(logical.name(), environment.getArgument(argument.getName()));
394-
else
425+
} else {
395426
arguments.put(logical.name(), environment.getArgument(fieldName));
427+
428+
isOptional = isOptionalAttribute(getAttribute(environment, argument));
429+
}
396430

397-
return getArgumentPredicate(cb, reuseJoin(path, fieldName, false),
431+
return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional),
398432
wherePredicateEnvironment(environment, fieldDefinition, arguments),
399433
new Argument(logical.name(), expressionValue));
400434
}
@@ -519,9 +553,7 @@ private Join<?,?> reuseJoin(From<?, ?> path, String fieldName, boolean outer) {
519553

520554
for (Join<?,?> join : path.getJoins()) {
521555
if (join.getAttribute().getName().equals(fieldName)) {
522-
if ((join.getJoinType() == JoinType.LEFT) == outer) {
523-
return join;
524-
}
556+
return join;
525557
}
526558
}
527559
return outer ? path.join(fieldName, JoinType.LEFT) : path.join(fieldName);
@@ -629,6 +661,14 @@ private Attribute<?,?> getAttribute(DataFetchingEnvironment environment, Argumen
629661
return entityType.getAttribute(argument.getName());
630662
}
631663

664+
private boolean isOptionalAttribute(Attribute<?,?> attribute) {
665+
if(SingularAttribute.class.isInstance(attribute)) {
666+
return SingularAttribute.class.cast(attribute).isOptional();
667+
}
668+
669+
return false;
670+
}
671+
632672
/**
633673
* Resolve JPA model entity type from GraphQL objectType
634674
*
@@ -777,14 +817,38 @@ protected final Stream<Field> flatten(Field field) {
777817

778818

779819
@SuppressWarnings( "unchecked" )
780-
protected final <R extends Value> R getObjectFieldValue(ObjectValue objectValue, String fieldName) {
820+
protected final <R extends Value<?>> R getObjectFieldValue(ObjectValue objectValue, String fieldName) {
781821
return (R) getObjectField(objectValue, fieldName).map(it-> it.getValue())
782822
.orElse(new NullValue());
783823
}
784824

785825
@SuppressWarnings( "unchecked" )
786-
protected final <R> R getArgumentValue(Argument argument) {
787-
return (R) argument.getValue();
826+
protected final <T> T getArgumentValue(DataFetchingEnvironment environment, Argument argument, Class<T> type) {
827+
Value<?> value = argument.getValue();
828+
829+
if(VariableReference.class.isInstance(value)) {
830+
return (T)
831+
environment.getExecutionContext()
832+
.getVariables()
833+
.get(VariableReference.class.cast(value).getName());
834+
}
835+
else if (BooleanValue.class.isInstance(value)) {
836+
return (T) new Boolean(BooleanValue.class.cast(value).isValue());
837+
}
838+
else if (IntValue.class.isInstance(value)) {
839+
return (T) IntValue.class.cast(value).getValue();
840+
}
841+
else if (StringValue.class.isInstance(value)) {
842+
return (T) StringValue.class.cast(value).getValue();
843+
}
844+
else if (FloatValue.class.isInstance(value)) {
845+
return (T) FloatValue.class.cast(value).getValue();
846+
}
847+
else if (NullValue.class.isInstance(value)) {
848+
return (T) null;
849+
}
850+
851+
throw new IllegalArgumentException("Not supported");
788852
}
789853

790854
protected final Optional<ObjectField> getObjectField(ObjectValue objectValue, String fieldName) {

0 commit comments

Comments
 (0)