9090 */
9191class 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