Skip to content

fix: Apply left outer join to retrieve optional associations [WIP] #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class Book {
@GraphQLIgnoreFilter
String description;

@ManyToOne(fetch=FetchType.LAZY)
@ManyToOne(fetch=FetchType.LAZY, optional = false)
Author author;

@Enumerated(EnumType.STRING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToOne;

import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription;

import lombok.Data;
import lombok.EqualsAndHashCode;

Expand All @@ -32,7 +31,7 @@
@EqualsAndHashCode(callSuper=true)
public class Droid extends Character {

@ManyToOne(fetch = FetchType.LAZY)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "primary_function")
DroidFunction primaryFunction;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@
import javax.persistence.Convert;
import javax.persistence.EntityManager;
import javax.persistence.Transient;
import javax.persistence.metamodel.*;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.PluralAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.Type;

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

private GraphQLArgument getWhereArgument(ManagedType<?> managedType) {
String typeName="";
if (managedType instanceof EmbeddableType){
typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType";
} else if (managedType instanceof EntityType) {
typeName = ((EntityType<?>)managedType).getName();
}

String type = namingStrategy.pluralize(typeName)+"CriteriaExpression";

GraphQLArgument whereArgument = whereArgumentsMap.get(managedType.getJavaType());

if(whereArgument != null)
return whereArgument;

return whereArgumentsMap.computeIfAbsent(managedType.getJavaType(), (javaType) -> computeWhereArgument(managedType));
}

private GraphQLArgument computeWhereArgument(ManagedType<?> managedType) {
String type=resolveWhereArgumentTypeName(managedType);

GraphQLInputObjectType whereInputObject = GraphQLInputObjectType.newInputObject()
.name(type)
.description("Where logical AND specification of the provided list of criteria expressions")
Expand Down Expand Up @@ -266,29 +264,38 @@ private GraphQLArgument getWhereArgument(ManagedType<?> managedType) {
)
.build();

whereArgument = GraphQLArgument.newArgument()
.name(QUERY_WHERE_PARAM_NAME)
.description("Where logical specification")
.type(whereInputObject)
.build();

whereArgumentsMap.put(managedType.getJavaType(), whereArgument);

return whereArgument;
return GraphQLArgument.newArgument()
.name(QUERY_WHERE_PARAM_NAME)
.description("Where logical specification")
.type(whereInputObject)
.build();

}

private GraphQLInputObjectType getWhereInputType(ManagedType<?> managedType) {
return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType);
private String resolveWhereArgumentTypeName(ManagedType<?> managedType) {
String typeName=resolveTypeName(managedType);

return namingStrategy.pluralize(typeName)+"CriteriaExpression";
}

private String resolveWhereInputTypeName(ManagedType<?> managedType) {
private String resolveTypeName(ManagedType<?> managedType) {
String typeName="";

if (managedType instanceof EmbeddableType){
typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType";
} else if (managedType instanceof EntityType) {
typeName = ((EntityType<?>)managedType).getName();
}

return typeName;
}

private GraphQLInputObjectType getWhereInputType(ManagedType<?> managedType) {
return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType);
}

private String resolveWhereInputTypeName(ManagedType<?> managedType) {
String typeName=resolveTypeName(managedType);

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

Expand Down Expand Up @@ -633,6 +640,8 @@ && isNotIgnoredOrder(attribute) ) {

// TODO fix page count query
arguments.add(getWhereArgument(foreignType));

arguments.add(optionalArgument(SingularAttribute.class.cast(attribute)));

} // Get Sub-Objects fields queries via DataFetcher
else if (attribute instanceof PluralAttribute
Expand All @@ -644,7 +653,7 @@ else if (attribute instanceof PluralAttribute
arguments.add(getWhereArgument(elementType));
dataFetcher = new GraphQLJpaOneToManyDataFetcher(entityManager, baseEntity, (PluralAttribute) attribute);
}

return GraphQLFieldDefinition.newFieldDefinition()
.name(attribute.getName())
.description(getSchemaDescription(attribute.getJavaMember()))
Expand All @@ -653,6 +662,15 @@ else if (attribute instanceof PluralAttribute
.argument(arguments)
.build();
}

private GraphQLArgument optionalArgument(SingularAttribute<?,?> attribute) {
return GraphQLArgument.newArgument()
.name("optional")
.description("Optional association specification")
.type(Scalars.GraphQLBoolean)
.defaultValue(attribute.isOptional())
.build();
}

protected ManagedType<?> getForeignType(Attribute<?,?> attribute) {
if(SingularAttribute.class.isInstance(attribute))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
*/
class QraphQLJpaBaseDataFetcher implements DataFetcher<Object> {

private static final String OPTIONAL = "optional";

// "__typename" is part of the graphql introspection spec and has to be ignored
private static final String TYPENAME = "__typename";

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

// 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
// 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
if (fieldPath.getModel() instanceof SingularAttribute) {
SingularAttribute<?,?> attribute = (SingularAttribute<?,?>) fieldPath.getModel();
if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE
|| attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE
) {
reuseJoin(from, selectedField.getName(), false);
// Let's apply left outer join to retrieve optional associations
Optional<Argument> optionalArgument = getArgument(selectedField, OPTIONAL);

// Let's do fugly conversion
Boolean isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class))
.orElse(attribute.isOptional());

reuseJoin(from, selectedField.getName(), isOptional);
}
}
} else {
// We must add plural attributes with explicit fetch to avoid Hibernate error:
} else {
// We must add plural attributes with explicit join to avoid Hibernate error:
// "query specified join fetching, but the owner of the fetched association was not present in the select list"
// TODO Let's try detect optional relation and apply join type
reuseJoin(from, selectedField.getName(), false);
PluralAttribute<?, ?, ?> attribute = getAttribute(selectedField.getName());

// Let's apply left outer join to retrieve optional many-to-many associations
boolean isOptional = (PersistentAttributeType.MANY_TO_MANY == attribute.getPersistentAttributeType());

reuseJoin(from, selectedField.getName(), isOptional);
}
}
}
Expand Down Expand Up @@ -251,6 +264,22 @@ protected boolean isOrderByArgument(Argument argument) {
return GraphQLJpaSchemaBuilder.ORDER_BY_PARAM_NAME.equals(argument.getName());
}

protected boolean isOptionalArgument(Argument argument) {
return OPTIONAL.equals(argument.getName());
}

protected Optional<Argument> getArgument(Field selectedField, String argumentName) {
return selectedField.getArguments()
.stream()
.filter(it -> it.getName()
.equals(argumentName))
.findFirst();
}

protected <R extends Attribute<?,?>> R getAttribute(String attributeName) {
return (R) entityType.getAttribute(attributeName);
}

@SuppressWarnings( "unchecked" )
protected Predicate getPredicate(CriteriaBuilder cb, Root<?> from, From<?,?> path, DataFetchingEnvironment environment, Argument argument) {

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

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

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

if(Logical.names().contains(argument.getName()))
if(Logical.names().contains(argument.getName())) {
arguments.put(logical.name(), environment.getArgument(argument.getName()));
else
} else {
arguments.put(logical.name(), environment.getArgument(fieldName));

isOptional = isOptionalAttribute(getAttribute(environment, argument));
}

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

for (Join<?,?> join : path.getJoins()) {
if (join.getAttribute().getName().equals(fieldName)) {
if ((join.getJoinType() == JoinType.LEFT) == outer) {
return join;
}
return join;
}
}
return outer ? path.join(fieldName, JoinType.LEFT) : path.join(fieldName);
Expand Down Expand Up @@ -629,6 +661,14 @@ private Attribute<?,?> getAttribute(DataFetchingEnvironment environment, Argumen
return entityType.getAttribute(argument.getName());
}

private boolean isOptionalAttribute(Attribute<?,?> attribute) {
if(SingularAttribute.class.isInstance(attribute)) {
return SingularAttribute.class.cast(attribute).isOptional();
}

return false;
}

/**
* Resolve JPA model entity type from GraphQL objectType
*
Expand Down Expand Up @@ -777,14 +817,38 @@ protected final Stream<Field> flatten(Field field) {


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

@SuppressWarnings( "unchecked" )
protected final <R> R getArgumentValue(Argument argument) {
return (R) argument.getValue();
protected final <T> T getArgumentValue(DataFetchingEnvironment environment, Argument argument, Class<T> type) {
Value<?> value = argument.getValue();

if(VariableReference.class.isInstance(value)) {
return (T)
environment.getExecutionContext()
.getVariables()
.get(VariableReference.class.cast(value).getName());
}
else if (BooleanValue.class.isInstance(value)) {
return (T) new Boolean(BooleanValue.class.cast(value).isValue());
}
else if (IntValue.class.isInstance(value)) {
return (T) IntValue.class.cast(value).getValue();
}
else if (StringValue.class.isInstance(value)) {
return (T) StringValue.class.cast(value).getValue();
}
else if (FloatValue.class.isInstance(value)) {
return (T) FloatValue.class.cast(value).getValue();
}
else if (NullValue.class.isInstance(value)) {
return (T) null;
}

throw new IllegalArgumentException("Not supported");
}

protected final Optional<ObjectField> getObjectField(ObjectValue objectValue, String fieldName) {
Expand Down
Loading