From dceecefdf349901786e49ee94e0d1eb97be492e8 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 29 Mar 2020 20:26:08 -0700 Subject: [PATCH 01/16] fix: add ON condition on the right side of collection join fetch criteria --- graphql-jpa-query-schema/pom.xml | 6 + .../schema/impl/GraphQLJpaQueryFactory.java | 46 +- .../hql/internal/ast/HqlSqlWalker.java | 1489 +++++++++++++++++ .../GraphQLEnumVariableBindingsTests.java | 6 +- .../query/schema/GraphQLExecutorTests.java | 8 +- .../GraphQLWhereVariableBindingsTests.java | 19 +- .../schema/StarwarsQueryExecutorTests.java | 73 +- 7 files changed, 1605 insertions(+), 42 deletions(-) create mode 100644 graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java diff --git a/graphql-jpa-query-schema/pom.xml b/graphql-jpa-query-schema/pom.xml index 6695d9e19..1b6253f97 100644 --- a/graphql-jpa-query-schema/pom.xml +++ b/graphql-jpa-query-schema/pom.xml @@ -44,6 +44,12 @@ true + + org.hibernate + hibernate-core + true + + javax.persistence javax.persistence-api diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index bc70cea21..6219ca21c 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -275,7 +275,7 @@ protected TypedQuery getCountQuery(DataFetchingEnvironment environment, Fi query.select(cb.count(root)); List predicates = field.getArguments().stream() - .map(it -> getPredicate(cb, root, null, queryEnvironment, it)) + .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); @@ -305,7 +305,7 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F } List predicates = field.getArguments().stream() - .map(it -> getPredicate(cb, from, null, queryEnvironment, it)) + .map(it -> getPredicate(field, cb, from, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); @@ -456,6 +456,7 @@ protected final List getFieldPredicates(Field field, CriteriaQuery Optional optionalArgument = getArgument(selection, OPTIONAL); Optional whereArgument = getArgument(selection, WHERE); Boolean isOptional = null; + Boolean isFetchOn = false; // Build predicate arguments for singular attributes only if(fieldPath.getModel() instanceof SingularAttribute) { @@ -502,6 +503,8 @@ protected final List getFieldPredicates(Field field, CriteriaQuery // Let's apply fetch join to retrieve associated plural attributes fetch = reuseFetch(from, selection.getName(), isOptional); } + + isFetchOn = true; } // Let's build join fetch graph to avoid Hibernate error: // "query specified join fetching, but the owner of the fetched association was not present in the select list" @@ -522,8 +525,17 @@ protected final List getFieldPredicates(Field field, CriteriaQuery DataFetchingEnvironment fieldEnvironment = wherePredicateEnvironment(environment, fieldDefinition, fieldArguments); - - predicates.addAll(getFieldPredicates(selection, query, cb, root, fetch, fieldEnvironment)); + if(isFetchOn) { + Predicate[] fetchPredicates = getFieldPredicates(selection, + query, + cb, + root, + fetch, + fieldEnvironment).toArray(new Predicate[] {}); + ((Join) fetch).on(fetchPredicates); + } else { + predicates.addAll(getFieldPredicates(selection, query, cb, root, fetch, fieldEnvironment)); + } } }); @@ -531,7 +543,7 @@ protected final List getFieldPredicates(Field field, CriteriaQuery arguments.stream() .filter(this::isPredicateArgument) - .map(it -> getPredicate(cb, root, from, environment, it)) + .map(it -> getPredicate(field, cb, root, from, environment, it)) .filter(it -> it != null) .forEach(predicates::add); @@ -600,17 +612,17 @@ protected > R getAttribute(String attributeName) { } @SuppressWarnings( "unchecked" ) - protected Predicate getPredicate(CriteriaBuilder cb, Root from, From path, DataFetchingEnvironment environment, Argument argument) { + protected Predicate getPredicate(Field field, CriteriaBuilder cb, Root from, From path, DataFetchingEnvironment environment, Argument argument) { if(isLogicalArgument(argument) || isDistinctArgument(argument) || isPageArgument(argument) || isAfterArgument(argument) || isFirstArgument(argument) ) { return null; } - else if(isWhereArgument(argument)) { + else if(isWhereArgument(argument)) { return getWherePredicate(cb, from, path, argumentEnvironment(environment, argument), argument); } else if(!argument.getName().contains(".")) { - Attribute argumentEntityAttribute = getAttribute(environment, argument); + Attribute argumentEntityAttribute = getAttribute(environment, argument.getName()); // If the argument is a list, let's assume we need to join and do an 'in' clause if (argumentEntityAttribute instanceof PluralAttribute) { @@ -624,9 +636,9 @@ else if(!argument.getName().contains(".")) { return cb.equal(path.get(argument.getName()), convertValue(environment, argument, argument.getValue())); } else { if(!argument.getName().endsWith(".where")) { - Path field = getCompoundJoinedPath(path, argument.getName(), false); + Path argumentPath = getCompoundJoinedPath(path, argument.getName(), false); - return cb.equal(field, convertValue(environment, argument, argument.getValue())); + return cb.equal(argumentPath, convertValue(environment, argument, argument.getValue())); } else { String fieldName = argument.getName().split("\\.")[0]; @@ -698,7 +710,7 @@ protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From arguments ) { if(isEntityType(environment)) { - Attribute attribute = getAttribute(environment, argument); + Attribute attribute = getAttribute(environment, argument.getName()); if(attribute.isAssociation()) { GraphQLFieldDefinition fieldDefinition = getFieldDefinition(environment.getGraphQLSchema(), @@ -993,7 +1005,7 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From } else { args.put(logical.name(), environment.getArgument(fieldName)); - isOptional = isOptionalAttribute(getAttribute(environment, argument)); + isOptional = isOptionalAttribute(getAttribute(environment, argument.getName())); } return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional), @@ -1290,7 +1302,7 @@ private void setPropertyValue(Object javaBean, String propertyName, Object prope * @return Java class type */ protected Class getJavaType(DataFetchingEnvironment environment, Argument argument) { - Attribute argumentEntityAttribute = getAttribute(environment, argument); + Attribute argumentEntityAttribute = getAttribute(environment, argument.getName()); if (argumentEntityAttribute instanceof PluralAttribute) return ((PluralAttribute) argumentEntityAttribute).getElementType().getJavaType(); @@ -1305,12 +1317,14 @@ protected Class getJavaType(DataFetchingEnvironment environment, Argument arg * @param argument * @return JPA model attribute */ - private Attribute getAttribute(DataFetchingEnvironment environment, Argument argument) { + private Attribute getAttribute(DataFetchingEnvironment environment, String argument) { GraphQLObjectType objectType = getObjectType(environment); EntityType entityType = getEntityType(objectType); - return entityType.getAttribute(argument.getName()); + return entityType.getAttribute(argument); } + + private boolean isOptionalAttribute(Attribute attribute) { if(SingularAttribute.class.isInstance(attribute)) { diff --git a/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java new file mode 100644 index 000000000..61b8594d4 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java @@ -0,0 +1,1489 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.hql.internal.ast; + +import static org.hibernate.hql.spi.QueryTranslator.ERROR_LEGACY_ORDINAL_PARAMS_NO_LONGER_SUPPORTED; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.hibernate.QueryException; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.internal.JoinSequence; +import org.hibernate.engine.internal.ParameterBinder; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.CollectionProperties; +import org.hibernate.hql.internal.antlr.HqlSqlBaseWalker; +import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; +import org.hibernate.hql.internal.antlr.HqlTokenTypes; +import org.hibernate.hql.internal.antlr.SqlTokenTypes; +import org.hibernate.hql.internal.ast.tree.AggregateNode; +import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; +import org.hibernate.hql.internal.ast.tree.CastFunctionNode; +import org.hibernate.hql.internal.ast.tree.CollectionFunction; +import org.hibernate.hql.internal.ast.tree.ConstructorNode; +import org.hibernate.hql.internal.ast.tree.DeleteStatement; +import org.hibernate.hql.internal.ast.tree.DotNode; +import org.hibernate.hql.internal.ast.tree.EntityJoinFromElement; +import org.hibernate.hql.internal.ast.tree.FromClause; +import org.hibernate.hql.internal.ast.tree.FromElement; +import org.hibernate.hql.internal.ast.tree.FromElementFactory; +import org.hibernate.hql.internal.ast.tree.FromReferenceNode; +import org.hibernate.hql.internal.ast.tree.IdentNode; +import org.hibernate.hql.internal.ast.tree.IndexNode; +import org.hibernate.hql.internal.ast.tree.InsertStatement; +import org.hibernate.hql.internal.ast.tree.IntoClause; +import org.hibernate.hql.internal.ast.tree.MethodNode; +import org.hibernate.hql.internal.ast.tree.OperatorNode; +import org.hibernate.hql.internal.ast.tree.ParameterContainer; +import org.hibernate.hql.internal.ast.tree.ParameterNode; +import org.hibernate.hql.internal.ast.tree.QueryNode; +import org.hibernate.hql.internal.ast.tree.ResolvableNode; +import org.hibernate.hql.internal.ast.tree.RestrictableStatement; +import org.hibernate.hql.internal.ast.tree.ResultVariableRefNode; +import org.hibernate.hql.internal.ast.tree.SelectClause; +import org.hibernate.hql.internal.ast.tree.SelectExpression; +import org.hibernate.hql.internal.ast.tree.UpdateStatement; +import org.hibernate.hql.internal.ast.util.ASTPrinter; +import org.hibernate.hql.internal.ast.util.ASTUtil; +import org.hibernate.hql.internal.ast.util.AliasGenerator; +import org.hibernate.hql.internal.ast.util.JoinProcessor; +import org.hibernate.hql.internal.ast.util.LiteralProcessor; +import org.hibernate.hql.internal.ast.util.NodeTraverser; +import org.hibernate.hql.internal.ast.util.SessionFactoryHelper; +import org.hibernate.hql.internal.ast.util.SyntheticAndFactory; +import org.hibernate.hql.internal.ast.util.TokenPrinters; +import org.hibernate.hql.spi.QueryTranslator; +import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.log.DeprecationLogger; +import org.hibernate.internal.util.StringHelper; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.param.CollectionFilterKeyParameterSpecification; +import org.hibernate.param.NamedParameterSpecification; +import org.hibernate.param.ParameterSpecification; +import org.hibernate.param.PositionalParameterSpecification; +import org.hibernate.param.VersionTypeSeedParameterSpecification; +import org.hibernate.persister.collection.CollectionPropertyNames; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.Queryable; +import org.hibernate.sql.JoinType; +import org.hibernate.type.AssociationType; +import org.hibernate.type.CompositeType; +import org.hibernate.type.DbTimestampType; +import org.hibernate.type.Type; +import org.hibernate.type.VersionType; +import org.hibernate.usertype.UserVersionType; + +import antlr.ASTFactory; +import antlr.RecognitionException; +import antlr.SemanticException; +import antlr.collections.AST; + +/** + * Implements methods used by the HQL->SQL tree transform grammar (a.k.a. the second phase). + *
    + *
  • Isolates the Hibernate API-specific code from the ANTLR generated code.
  • + *
  • Handles the SQL fragments generated by the persisters in order to create the SELECT and FROM clauses, + * taking into account the joins and projections that are implied by the mappings (persister/queryable).
  • + *
  • Uses SqlASTFactory to create customized AST nodes.
  • + *
+ * + * @see SqlASTFactory + */ +public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, ParameterBinder.NamedParameterSource { + private static final CoreMessageLogger LOG = CoreLogging.messageLogger( HqlSqlWalker.class ); + + private final QueryTranslatorImpl queryTranslatorImpl; + private final HqlParser hqlParser; + private final SessionFactoryHelper sessionFactoryHelper; + private final Map tokenReplacements; + private final AliasGenerator aliasGenerator = new AliasGenerator(); + private final LiteralProcessor literalProcessor; + private final ParseErrorHandler parseErrorHandler; + private final String collectionFilterRole; + + private FromClause currentFromClause; + private SelectClause selectClause; + + /** + * Maps each top-level result variable to its SelectExpression; + * (excludes result variables defined in subqueries) + */ + private Map selectExpressionsByResultVariable = new HashMap<>(); + + private Set querySpaces = new HashSet<>(); + + private int parameterCount; + private Map namedParameters; + private Map positionalParameters; + + private ArrayList parameterSpecs = new ArrayList<>(); + private int numberOfParametersInSetClause; + + private ArrayList assignmentSpecifications = new ArrayList(); + + private JoinType impliedJoinType = JoinType.INNER_JOIN; + + private boolean inEntityGraph; + + /** + * Create a new tree transformer. + * + * @param qti Back pointer to the query translator implementation that is using this tree transform. + * @param sfi The session factory implementor where the Hibernate mappings can be found. + * @param parser A reference to the phase-1 parser + * @param tokenReplacements Registers the token replacement map with the walker. This map will + * be used to substitute function names and constants. + * @param collectionRole The collection role name of the collection used as the basis for the + * filter, NULL if this is not a collection filter compilation. + */ + public HqlSqlWalker( + QueryTranslatorImpl qti, + SessionFactoryImplementor sfi, + HqlParser parser, + Map tokenReplacements, + String collectionRole) { + setASTFactory( new SqlASTFactory( this ) ); + // Initialize the error handling delegate. + this.parseErrorHandler = new ErrorTracker( qti.getQueryString() ); + this.queryTranslatorImpl = qti; + this.sessionFactoryHelper = new SessionFactoryHelper( sfi ); + this.literalProcessor = new LiteralProcessor( this ); + this.tokenReplacements = tokenReplacements; + this.collectionFilterRole = collectionRole; + this.hqlParser = parser; + } + + // handle trace logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + private int traceDepth; + + @Override + public void traceIn(String ruleName, AST tree) { + if ( !LOG.isTraceEnabled() ) { + return; + } + if ( inputState.guessing > 0 ) { + return; + } + String prefix = StringHelper.repeat( '-', ( traceDepth++ * 2 ) ) + "-> "; + String traceText = ruleName + " (" + buildTraceNodeName( tree ) + ")"; + LOG.trace( prefix + traceText ); + } + + private String buildTraceNodeName(AST tree) { + return tree == null + ? "???" + : tree.getText() + " [" + TokenPrinters.SQL_TOKEN_PRINTER.getTokenTypeName( tree.getType() ) + "]"; + } + + @Override + public void traceOut(String ruleName, AST tree) { + if ( !LOG.isTraceEnabled() ) { + return; + } + if ( inputState.guessing > 0 ) { + return; + } + String prefix = "<-" + StringHelper.repeat( '-', ( --traceDepth * 2 ) ) + " "; + LOG.trace( prefix + ruleName ); + } + + @Override + protected void prepareFromClauseInputTree(AST fromClauseInput) { + if ( !isSubQuery() ) { +// // inject param specifications to account for dynamic filter param values +// if ( ! getEnabledFilters().isEmpty() ) { +// Iterator filterItr = getEnabledFilters().values().iterator(); +// while ( filterItr.hasNext() ) { +// FilterImpl filter = ( FilterImpl ) filterItr.next(); +// if ( ! filter.getFilterDefinition().getParameterNames().isEmpty() ) { +// Iterator paramItr = filter.getFilterDefinition().getParameterNames().iterator(); +// while ( paramItr.hasNext() ) { +// String parameterName = ( String ) paramItr.next(); +// // currently param filters *only* work with single-column parameter types; +// // if that limitation is ever lifted, this logic will need to change to account for that +// ParameterNode collectionFilterKeyParameter = ( ParameterNode ) astFactory.create( PARAM, "?" ); +// DynamicFilterParameterSpecification paramSpec = new DynamicFilterParameterSpecification( +// filter.getName(), +// parameterName, +// filter.getFilterDefinition().getParameterType( parameterName ), +// positionalParameterCount++ +// ); +// collectionFilterKeyParameter.setHqlParameterSpecification( paramSpec ); +// parameterSpecs.add( paramSpec ); +// } +// } +// } +// } + + if ( isFilter() ) { + // Handle collection-filter compilation. + // IMPORTANT NOTE: This is modifying the INPUT (HQL) tree, not the output tree! + QueryableCollection persister = sessionFactoryHelper.getCollectionPersister( collectionFilterRole ); + Type collectionElementType = persister.getElementType(); + if ( !collectionElementType.isEntityType() ) { + throw new QueryException( "collection of values in filter: this" ); + } + + String collectionElementEntityName = persister.getElementPersister().getEntityName(); + ASTFactory inputAstFactory = hqlParser.getASTFactory(); + AST fromElement = inputAstFactory.create( HqlTokenTypes.FILTER_ENTITY, collectionElementEntityName ); + ASTUtil.createSibling( inputAstFactory, HqlTokenTypes.ALIAS, "this", fromElement ); + fromClauseInput.addChild( fromElement ); + // Show the modified AST. + LOG.debug( "prepareFromClauseInputTree() : Filter - Added 'this' as a from element..." ); + queryTranslatorImpl.showHqlAst( hqlParser.getAST() ); + + // Create a parameter specification for the collection filter... + final Type collectionFilterKeyType = sessionFactoryHelper.requireQueryableCollection( collectionFilterRole ) + .getKeyType(); + final ParameterNode collectionFilterKeyParameter = (ParameterNode) astFactory.create( PARAM, "?" ); + final CollectionFilterKeyParameterSpecification collectionFilterKeyParameterSpec = new CollectionFilterKeyParameterSpecification( + collectionFilterRole, + collectionFilterKeyType + ); + parameterCount++; + collectionFilterKeyParameter.setHqlParameterSpecification( collectionFilterKeyParameterSpec ); + parameterSpecs.add( collectionFilterKeyParameterSpec ); + } + } + } + + public boolean isFilter() { + return collectionFilterRole != null; + } + + public String getCollectionFilterRole() { + return collectionFilterRole; + } + + public boolean isInEntityGraph() { + return inEntityGraph; + } + + public SessionFactoryHelper getSessionFactoryHelper() { + return sessionFactoryHelper; + } + + public Map getTokenReplacements() { + return tokenReplacements; + } + + public AliasGenerator getAliasGenerator() { + return aliasGenerator; + } + + public FromClause getCurrentFromClause() { + return currentFromClause; + } + + public ParseErrorHandler getParseErrorHandler() { + return parseErrorHandler; + } + + @Override + public void reportError(RecognitionException e) { + parseErrorHandler.reportError( e ); // Use the delegate. + } + + @Override + public void reportError(String s) { + parseErrorHandler.reportError( s ); // Use the delegate. + } + + @Override + public void reportWarning(String s) { + parseErrorHandler.reportWarning( s ); + } + + /** + * Returns the set of unique query spaces (a.k.a. + * table names) that occurred in the query. + * + * @return A set of table names (Strings). + */ + public Set getQuerySpaces() { + return querySpaces; + } + + @Override + protected AST createFromElement(String path, AST alias, AST propertyFetch) throws SemanticException { + FromElement fromElement = currentFromClause.addFromElement( path, alias ); + fromElement.setAllPropertyFetch( propertyFetch != null ); + return fromElement; + } + + @Override + protected AST createFromFilterElement(AST filterEntity, AST alias) throws SemanticException { + FromElement fromElement = currentFromClause.addFromElement( filterEntity.getText(), alias ); + FromClause fromClause = fromElement.getFromClause(); + QueryableCollection persister = sessionFactoryHelper.getCollectionPersister( collectionFilterRole ); + // Get the names of the columns used to link between the collection + // owner and the collection elements. + String[] keyColumnNames = persister.getKeyColumnNames(); + String fkTableAlias = persister.isOneToMany() + ? fromElement.getTableAlias() + : fromClause.getAliasGenerator().createName( collectionFilterRole ); + JoinSequence join = sessionFactoryHelper.createJoinSequence(); + join.setRoot( persister, fkTableAlias ); + if ( !persister.isOneToMany() ) { + join.addJoin( + (AssociationType) persister.getElementType(), + fromElement.getTableAlias(), + JoinType.INNER_JOIN, + persister.getElementColumnNames( fkTableAlias ) + ); + } + join.addCondition( fkTableAlias, keyColumnNames, " = ?" ); + fromElement.setJoinSequence( join ); + fromElement.setFilter( true ); + LOG.debug( "createFromFilterElement() : processed filter FROM element." ); + return fromElement; + } + + @Override + protected void createFromJoinElement( + AST path, + AST alias, + int joinType, + AST fetchNode, + AST propertyFetch, + AST with) throws SemanticException { + boolean fetch = fetchNode != null; + if ( fetch && isSubQuery() ) { + throw new QueryException( "fetch not allowed in subquery from-elements" ); + } + + + // the incoming "path" can be either: + // 1) an implicit join path (join p.address.city) + // 2) an entity-join (join com.acme.User) + // + // so make the proper interpretation here... + + final EntityPersister entityJoinReferencedPersister = resolveEntityJoinReferencedPersister( path ); + if ( entityJoinReferencedPersister != null ) { + // `path` referenced an entity + final EntityJoinFromElement join = createEntityJoin( + entityJoinReferencedPersister, + alias, + joinType, + propertyFetch, + with + ); + + ( (FromReferenceNode) path ).setFromElement( join ); + } + else { + if ( path.getType() != SqlTokenTypes.DOT ) { + throw new SemanticException( "Path expected for join!" ); + } + + DotNode dot = (DotNode) path; + JoinType hibernateJoinType = JoinProcessor.toHibernateJoinType( joinType ); + dot.setJoinType( hibernateJoinType ); // Tell the dot node about the join type. + dot.setFetch( fetch ); + // Generate an explicit join for the root dot node. The implied joins will be collected and passed up + // to the root dot node. + dot.resolve( true, false, alias == null ? null : alias.getText() ); + + final FromElement fromElement; + if ( dot.getDataType() != null && dot.getDataType().isComponentType() ) { + if ( dot.getDataType().isAnyType() ) { + throw new SemanticException( "An AnyType attribute cannot be join fetched" ); + // ^^ because the discriminator (aka, the "meta columns") must be known to the SQL in + // a non-parameterized way. + } + FromElementFactory factory = new FromElementFactory( + getCurrentFromClause(), + dot.getLhs().getFromElement(), + dot.getPropertyPath(), + alias == null ? null : alias.getText(), + null, + false + ); + fromElement = factory.createComponentJoin( (CompositeType) dot.getDataType() ); + } + else { + fromElement = dot.getImpliedJoin(); + fromElement.setAllPropertyFetch( propertyFetch != null ); + + if ( with != null ) { +// if ( fetch ) { +// throw new SemanticException( "with-clause not allowed on fetched associations; use filters" ); +// } + handleWithFragment( fromElement, with ); + } + } + + if ( LOG.isDebugEnabled() ) { + LOG.debug( + "createFromJoinElement() : " + + getASTPrinter().showAsString( fromElement, "-- join tree --" ) + ); + } + } + } + + private EntityPersister resolveEntityJoinReferencedPersister(AST path) { + if ( path.getType() == IDENT ) { + final IdentNode pathIdentNode = (IdentNode) path; + String name = path.getText(); + if ( name == null ) { + name = pathIdentNode.getOriginalText(); + } + return sessionFactoryHelper.findEntityPersisterByName( name ); + } + else if ( path.getType() == DOT ) { + final String pathText = ASTUtil.getPathText( path ); + return sessionFactoryHelper.findEntityPersisterByName( pathText ); + } + return null; + } + + @Override + protected void finishFromClause(AST fromClause) throws SemanticException { + ( (FromClause) fromClause ).finishInit(); + } + + private EntityJoinFromElement createEntityJoin( + EntityPersister entityPersister, + AST aliasNode, + int joinType, + AST propertyFetch, + AST with) throws SemanticException { + final String alias = aliasNode == null ? null : aliasNode.getText(); + LOG.debugf( "Creating entity-join FromElement [%s -> %s]", alias, entityPersister.getEntityName() ); + EntityJoinFromElement join = new EntityJoinFromElement( + this, + getCurrentFromClause(), + entityPersister, + JoinProcessor.toHibernateJoinType( joinType ), + propertyFetch != null, + alias + ); + + if ( with != null ) { + handleWithFragment( join, with ); + } + + return join; + } + + private void handleWithFragment(FromElement fromElement, AST hqlWithNode) throws SemanticException { + try { + withClause( hqlWithNode ); + AST hqlSqlWithNode = returnAST; + if ( LOG.isDebugEnabled() ) { + LOG.debug( + "handleWithFragment() : " + getASTPrinter().showAsString( + hqlSqlWithNode, + "-- with clause --" + ) + ); + } + WithClauseVisitor visitor = new WithClauseVisitor( fromElement, queryTranslatorImpl ); + NodeTraverser traverser = new NodeTraverser( visitor ); + traverser.traverseDepthFirst( hqlSqlWithNode ); + + SqlGenerator sql = new SqlGenerator( getSessionFactoryHelper().getFactory() ); + sql.whereExpr( hqlSqlWithNode.getFirstChild() ); + + fromElement.setWithClauseFragment( "(" + sql.getSQL() + ")" ); + } + catch (SemanticException e) { + throw e; + } + catch (InvalidWithClauseException e) { + throw e; + } + catch (Exception e) { + throw new SemanticException( e.getMessage() ); + } + } + + private static class WithClauseVisitor implements NodeTraverser.VisitationStrategy { + private final FromElement joinFragment; + private final QueryTranslatorImpl queryTranslatorImpl; + + private FromElement referencedFromElement; + private String joinAlias; + + public WithClauseVisitor(FromElement fromElement, QueryTranslatorImpl queryTranslatorImpl) { + this.joinFragment = fromElement; + this.queryTranslatorImpl = queryTranslatorImpl; + } + + @Override + public void visit(AST node) { + // TODO : currently expects that the individual with expressions apply to the same sql table join. + // This may not be the case for joined-subclass where the property values + // might be coming from different tables in the joined hierarchy. At some + // point we should expand this to support that capability. However, that has + // some difficulties: + // 1) the biggest is how to handle ORs when the individual comparisons are + // linked to different sql joins. + // 2) here we would need to track each comparison individually, along with + // the join alias to which it applies and then pass that information + // back to the FromElement so it can pass it along to the JoinSequence + if ( node instanceof DotNode ) { + DotNode dotNode = (DotNode) node; + FromElement fromElement = dotNode.getFromElement(); + if ( referencedFromElement != null ) { +// if ( fromElement != referencedFromElement ) { +// throw new HibernateException( "with-clause referenced two different from-clause elements" ); +// } + } + else { + referencedFromElement = fromElement; + joinAlias = extractAppliedAlias( dotNode ); + // TODO : temporary + // needed because currently persister is the one that + // creates and renders the join fragments for inheritance + // hierarchies... +// if ( !joinAlias.equals( referencedFromElement.getTableAlias() ) ) { +// throw new InvalidWithClauseException( +// "with clause can only reference columns in the driving table", +// queryTranslatorImpl.getQueryString() +// ); +// } + } + } + else if ( node instanceof ParameterNode ) { + applyParameterSpecification( ( (ParameterNode) node ).getHqlParameterSpecification() ); + } + else if ( node instanceof ParameterContainer ) { + applyParameterSpecifications( (ParameterContainer) node ); + } + } + + private void applyParameterSpecifications(ParameterContainer parameterContainer) { + if ( parameterContainer.hasEmbeddedParameters() ) { + ParameterSpecification[] specs = parameterContainer.getEmbeddedParameters(); + for ( ParameterSpecification spec : specs ) { + applyParameterSpecification( spec ); + } + } + } + + private void applyParameterSpecification(ParameterSpecification paramSpec) { + joinFragment.addEmbeddedParameter( paramSpec ); + } + + private String extractAppliedAlias(DotNode dotNode) { + return dotNode.getText().substring( 0, dotNode.getText().indexOf( '.' ) ); + } + + public FromElement getReferencedFromElement() { + return referencedFromElement; + } + + public String getJoinAlias() { + return joinAlias; + } + } + + /** + * Sets the current 'FROM' context. + * + * @param fromNode The new 'FROM' context. + * @param inputFromNode The from node from the input AST. + */ + @Override + protected void pushFromClause(AST fromNode, AST inputFromNode) { + FromClause newFromClause = (FromClause) fromNode; + newFromClause.setParentFromClause( currentFromClause ); + currentFromClause = newFromClause; + } + + /** + * Returns to the previous 'FROM' context. + */ + private void popFromClause() { + currentFromClause = currentFromClause.getParentFromClause(); + } + + @Override + protected void lookupAlias(AST aliasRef) + throws SemanticException { + FromElement alias = currentFromClause.getFromElement( aliasRef.getText() ); + FromReferenceNode aliasRefNode = (FromReferenceNode) aliasRef; + aliasRefNode.setFromElement( alias ); + } + + @Override + protected void setImpliedJoinType(int joinType) { + impliedJoinType = JoinProcessor.toHibernateJoinType( joinType ); + } + + public JoinType getImpliedJoinType() { + return impliedJoinType; + } + + @Override + protected AST lookupProperty(AST dot, boolean root, boolean inSelect) throws SemanticException { + DotNode dotNode = (DotNode) dot; + FromReferenceNode lhs = dotNode.getLhs(); + AST rhs = lhs.getNextSibling(); + + // this used to be a switch statement based on the rhs's node type + // expecting it to be SqlTokenTypes.ELEMENTS or + // SqlTokenTypes.INDICES in the cases where the re-arranging is needed + // + // In such cases it additionally expects the RHS to be a CollectionFunction node. + // + // However, in my experience these assumptions sometimes did not works as sometimes the node + // types come in with the node type WEIRD_IDENT. What this does now is to: + // 1) see if the LHS is a collection + // 2) see if the RHS is a reference to one of the "collection properties". + // if both are true, we log a deprecation warning + if ( lhs.getDataType() != null + && lhs.getDataType().isCollectionType() ) { + if ( CollectionProperties.isCollectionProperty( rhs.getText() ) ) { + DeprecationLogger.DEPRECATION_LOGGER.logDeprecationOfCollectionPropertiesInHql( + rhs.getText(), + lhs.getPath() + ); + } + + // perform the re-arrangement + if ( CollectionPropertyNames.COLLECTION_INDICES.equalsIgnoreCase( rhs.getText() ) + || CollectionPropertyNames.COLLECTION_ELEMENTS.equalsIgnoreCase( rhs.getText() ) ) { + if ( LOG.isDebugEnabled() ) { + LOG.debugf( + "lookupProperty() %s => %s(%s)", + dotNode.getPath(), + rhs.getText(), + lhs.getPath() + ); + } + + final CollectionFunction f; + if ( rhs instanceof CollectionFunction ) { + f = (CollectionFunction) rhs; + } + else { + f = new CollectionFunction(); + f.initialize( SqlTokenTypes.METHOD_CALL, rhs.getText() ); + f.initialize( this ); + } + + // Re-arrange the tree so that the collection function is the root and the lhs is the path. + f.setFirstChild( lhs ); + lhs.setNextSibling( null ); + dotNode.setFirstChild( f ); + resolve( lhs ); // Don't forget to resolve the argument! + f.resolve( inSelect ); // Resolve the collection function now. + return f; + } + } + + // otherwise, resolve the path and return it + dotNode.resolveFirstChild(); + return dotNode; + } + + @Override + protected boolean isNonQualifiedPropertyRef(AST ident) { + final String identText = ident.getText(); + if ( currentFromClause.isFromElementAlias( identText ) ) { + return false; + } + + List fromElements = currentFromClause.getExplicitFromElements(); + if ( fromElements.size() == 1 ) { + final FromElement fromElement = (FromElement) fromElements.get( 0 ); + try { + LOG.tracev( "Attempting to resolve property [{0}] as a non-qualified ref", identText ); + return fromElement.getPropertyMapping( identText ).toType( identText ) != null; + } + catch (QueryException e) { + // Should mean that no such property was found + } + } + + return false; + } + + @Override + protected AST lookupNonQualifiedProperty(AST property) throws SemanticException { + final FromElement fromElement = (FromElement) currentFromClause.getExplicitFromElements().get( 0 ); + AST syntheticDotNode = generateSyntheticDotNodeForNonQualifiedPropertyRef( property, fromElement ); + return lookupProperty( syntheticDotNode, false, getCurrentClauseType() == HqlSqlTokenTypes.SELECT ); + } + + private AST generateSyntheticDotNodeForNonQualifiedPropertyRef(AST property, FromElement fromElement) { + AST dot = getASTFactory().create( DOT, "{non-qualified-property-ref}" ); + // TODO : better way?!? + ( (DotNode) dot ).setPropertyPath( ( (FromReferenceNode) property ).getPath() ); + + IdentNode syntheticAlias = (IdentNode) getASTFactory().create( IDENT, "{synthetic-alias}" ); + syntheticAlias.setFromElement( fromElement ); + syntheticAlias.setResolved(); + + dot.setFirstChild( syntheticAlias ); + dot.addChild( property ); + + return dot; + } + + @Override + protected void processQuery(AST select, AST query) throws SemanticException { + if ( LOG.isDebugEnabled() ) { + LOG.debugf( "processQuery() : %s", query.toStringTree() ); + } + + try { + QueryNode qn = (QueryNode) query; + + // Was there an explicit select expression? + boolean explicitSelect = select != null && select.getNumberOfChildren() > 0; + + // Add in the EntityGraph attribute nodes. + if ( queryTranslatorImpl.getEntityGraphQueryHint() != null ) { + final boolean oldInEntityGraph = inEntityGraph; + try { + inEntityGraph = true; + qn.getFromClause().getFromElements().addAll( + queryTranslatorImpl.getEntityGraphQueryHint().toFromElements( qn.getFromClause(), this ) + ); + } + finally { + inEntityGraph = oldInEntityGraph; + } + } + + if ( !explicitSelect ) { + // No explicit select expression; render the id and properties + // projection lists for every persister in the from clause into + // a single 'token node'. + //TODO: the only reason we need this stuff now is collection filters, + // we should get rid of derived select clause completely! + createSelectClauseFromFromClause( qn ); + } + else { + // Use the explicitly declared select expression; determine the + // return types indicated by each select token + useSelectClause( select ); + } + + // After that, process the JOINs. + // Invoke a delegate to do the work, as this is farily complex. + JoinProcessor joinProcessor = new JoinProcessor( this ); + joinProcessor.processJoins( qn ); + + // Attach any mapping-defined "ORDER BY" fragments + Iterator itr = qn.getFromClause().getProjectionList().iterator(); + while ( itr.hasNext() ) { + final FromElement fromElement = (FromElement) itr.next(); +// if ( fromElement.isFetch() && fromElement.isCollectionJoin() ) { + if ( fromElement.isFetch() && fromElement.getQueryableCollection() != null ) { + // Does the collection referenced by this FromElement + // specify an order-by attribute? If so, attach it to + // the query's order-by + if ( fromElement.getQueryableCollection().hasOrdering() ) { + String orderByFragment = fromElement + .getQueryableCollection() + .getSQLOrderByString( fromElement.getCollectionTableAlias() ); + qn.getOrderByClause().addOrderFragment( orderByFragment ); + } + if ( fromElement.getQueryableCollection().hasManyToManyOrdering() ) { + String orderByFragment = fromElement.getQueryableCollection() + .getManyToManyOrderByString( fromElement.getTableAlias() ); + qn.getOrderByClause().addOrderFragment( orderByFragment ); + } + } + } + } + finally { + popFromClause(); + } + } + + protected void postProcessDML(RestrictableStatement statement) throws SemanticException { + statement.getFromClause().resolve(); + + FromElement fromElement = (FromElement) statement.getFromClause().getFromElements().get( 0 ); + Queryable persister = fromElement.getQueryable(); + // Make #@%$^#^&# sure no alias is applied to the table name + fromElement.setText( persister.getTableName() ); + +// // append any filter fragments; the EMPTY_MAP is used under the assumption that +// // currently enabled filters should not affect this process +// if ( persister.getDiscriminatorType() != null ) { +// new SyntheticAndFactory( getASTFactory() ).addDiscriminatorWhereFragment( +// statement, +// persister, +// java.util.Collections.EMPTY_MAP, +// fromElement.getTableAlias() +// ); +// } + if ( persister.getDiscriminatorType() != null || !queryTranslatorImpl.getEnabledFilters().isEmpty() ) { + new SyntheticAndFactory( this ).addDiscriminatorWhereFragment( + statement, + persister, + queryTranslatorImpl.getEnabledFilters(), + fromElement.getTableAlias() + ); + } + + } + + @Override + protected void postProcessUpdate(AST update) throws SemanticException { + UpdateStatement updateStatement = (UpdateStatement) update; + + postProcessDML( updateStatement ); + } + + @Override + protected void postProcessDelete(AST delete) throws SemanticException { + postProcessDML( (DeleteStatement) delete ); + } + + @Override + protected void postProcessInsert(AST insert) throws SemanticException, QueryException { + InsertStatement insertStatement = (InsertStatement) insert; + insertStatement.validate(); + + SelectClause selectClause = insertStatement.getSelectClause(); + Queryable persister = insertStatement.getIntoClause().getQueryable(); + + if ( !insertStatement.getIntoClause().isExplicitIdInsertion() ) { + // the insert did not explicitly reference the id. See if + // 1) that is allowed + // 2) whether we need to alter the SQL tree to account for id + final IdentifierGenerator generator = persister.getIdentifierGenerator(); + if ( !BulkInsertionCapableIdentifierGenerator.class.isInstance( generator ) ) { + throw new QueryException( + "Invalid identifier generator encountered for implicit id handling as part of bulk insertions" + ); + } + final BulkInsertionCapableIdentifierGenerator capableGenerator = + BulkInsertionCapableIdentifierGenerator.class.cast( generator ); + if ( !capableGenerator.supportsBulkInsertionIdentifierGeneration() ) { + throw new QueryException( + "Identifier generator reported it does not support implicit id handling as part of bulk insertions" + ); + } + + final String fragment = capableGenerator.determineBulkInsertionIdentifierGenerationSelectFragment( + sessionFactoryHelper.getFactory().getDialect() + ); + if ( fragment != null ) { + // we got a fragment from the generator, so alter the sql tree... + // + // first, wrap the fragment as a node + AST fragmentNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, fragment ); + // next, rearrange the SQL tree to add the fragment node as the first select expression + AST originalFirstSelectExprNode = selectClause.getFirstChild(); + selectClause.setFirstChild( fragmentNode ); + fragmentNode.setNextSibling( originalFirstSelectExprNode ); + // finally, prepend the id column name(s) to the insert-spec + insertStatement.getIntoClause().prependIdColumnSpec(); + } + } + + if ( sessionFactoryHelper.getFactory().getDialect().supportsParametersInInsertSelect() ) { + AST child = selectClause.getFirstChild(); + int i = 0; + while ( child != null ) { + if ( child instanceof ParameterNode ) { + // infer the parameter type from the type listed in the INSERT INTO clause + ( (ParameterNode) child ).setExpectedType( + insertStatement.getIntoClause() + .getInsertionTypes()[selectClause.getParameterPositions().get( i )] + ); + i++; + } + child = child.getNextSibling(); + } + } + + final boolean includeVersionProperty = persister.isVersioned() && + !insertStatement.getIntoClause().isExplicitVersionInsertion() && + persister.isVersionPropertyInsertable(); + if ( includeVersionProperty ) { + // We need to seed the version value as part of this bulk insert + VersionType versionType = persister.getVersionType(); + AST versionValueNode = null; + + if ( sessionFactoryHelper.getFactory().getDialect().supportsParametersInInsertSelect() ) { + int[] sqlTypes = versionType.sqlTypes( sessionFactoryHelper.getFactory() ); + if ( sqlTypes == null || sqlTypes.length == 0 ) { + throw new IllegalStateException( versionType.getClass() + ".sqlTypes() returns null or empty array" ); + } + if ( sqlTypes.length > 1 ) { + throw new IllegalStateException( + versionType.getClass() + + ".sqlTypes() returns > 1 element; only single-valued versions are allowed." + ); + } + versionValueNode = getASTFactory().create( HqlSqlTokenTypes.PARAM, "?" ); + ParameterSpecification paramSpec = new VersionTypeSeedParameterSpecification( versionType ); + ( (ParameterNode) versionValueNode ).setHqlParameterSpecification( paramSpec ); + parameterSpecs.add( 0, paramSpec ); + + if ( sessionFactoryHelper.getFactory().getDialect().requiresCastingOfParametersInSelectClause() ) { + // we need to wrtap the param in a cast() + MethodNode versionMethodNode = (MethodNode) getASTFactory().create( + HqlSqlTokenTypes.METHOD_CALL, + "(" + ); + AST methodIdentNode = getASTFactory().create( HqlSqlTokenTypes.IDENT, "cast" ); + versionMethodNode.addChild( methodIdentNode ); + versionMethodNode.initializeMethodNode( methodIdentNode, true ); + AST castExprListNode = getASTFactory().create( HqlSqlTokenTypes.EXPR_LIST, "exprList" ); + methodIdentNode.setNextSibling( castExprListNode ); + castExprListNode.addChild( versionValueNode ); + versionValueNode.setNextSibling( + getASTFactory().create( + HqlSqlTokenTypes.IDENT, + sessionFactoryHelper.getFactory().getDialect().getTypeName( sqlTypes[0] ) + ) + ); + processFunction( versionMethodNode, true ); + versionValueNode = versionMethodNode; + } + } + else { + if ( isIntegral( versionType ) ) { + try { + Object seedValue = versionType.seed( null ); + versionValueNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, seedValue.toString() ); + } + catch (Throwable t) { + throw new QueryException( "could not determine seed value for version on bulk insert [" + versionType + "]" ); + } + } + else if ( isDatabaseGeneratedTimestamp( versionType ) ) { + String functionName = sessionFactoryHelper.getFactory() + .getDialect() + .getCurrentTimestampSQLFunctionName(); + versionValueNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, functionName ); + } + else { + throw new QueryException( "cannot handle version type [" + versionType + "] on bulk inserts with dialects not supporting parameterSpecs in insert-select statements" ); + } + } + + AST currentFirstSelectExprNode = selectClause.getFirstChild(); + selectClause.setFirstChild( versionValueNode ); + versionValueNode.setNextSibling( currentFirstSelectExprNode ); + + insertStatement.getIntoClause().prependVersionColumnSpec(); + } + + if ( insertStatement.getIntoClause().isDiscriminated() ) { + String sqlValue = insertStatement.getIntoClause().getQueryable().getDiscriminatorSQLValue(); + AST discrimValue = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, sqlValue ); + insertStatement.getSelectClause().addChild( discrimValue ); + } + + } + + private boolean isDatabaseGeneratedTimestamp(Type type) { + // currently only the Hibernate-supplied DbTimestampType is supported here + return DbTimestampType.class.isAssignableFrom( type.getClass() ); + } + + private boolean isIntegral(Type type) { + return Long.class.isAssignableFrom( type.getReturnedClass() ) + || Integer.class.isAssignableFrom( type.getReturnedClass() ) + || long.class.isAssignableFrom( type.getReturnedClass() ) + || int.class.isAssignableFrom( type.getReturnedClass() ); + } + + private void useSelectClause(AST select) throws SemanticException { + selectClause = (SelectClause) select; + selectClause.initializeExplicitSelectClause( currentFromClause ); + } + + private void createSelectClauseFromFromClause(QueryNode qn) throws SemanticException { + AST select = astFactory.create( SELECT_CLAUSE, "{derived select clause}" ); + AST sibling = qn.getFromClause(); + qn.setFirstChild( select ); + select.setNextSibling( sibling ); + selectClause = (SelectClause) select; + selectClause.initializeDerivedSelectClause( currentFromClause ); + LOG.debug( "Derived SELECT clause created." ); + } + + @Override + protected void resolve(AST node) throws SemanticException { + resolve(node, null); + } + + @Override + protected void resolve(AST node, AST predicateNode) throws SemanticException { + if ( node != null ) { + // This is called when it's time to fully resolve a path expression. + ResolvableNode r = (ResolvableNode) node; + if ( isInFunctionCall() ) { + r.resolveInFunctionCall( false, true ); + } + else { + r.resolve( false, true, null, null, predicateNode ); // Generate implicit joins, only if necessary. + } + } + } + + @Override + protected void resolveSelectExpression(AST node) throws SemanticException { + // This is called when it's time to fully resolve a path expression. + int type = node.getType(); + switch ( type ) { + case DOT: { + DotNode dot = (DotNode) node; + dot.resolveSelectExpression(); + break; + } + case ALIAS_REF: { + // Notify the FROM element that it is being referenced by the select. + FromReferenceNode aliasRefNode = (FromReferenceNode) node; + //aliasRefNode.resolve( false, false, aliasRefNode.getText() ); //TODO: is it kosher to do it here? + aliasRefNode.resolve( false, false ); //TODO: is it kosher to do it here? + FromElement fromElement = aliasRefNode.getFromElement(); + if ( fromElement != null ) { + fromElement.setIncludeSubclasses( true ); + } + break; + } + default: { + break; + } + } + } + + @Override + protected void beforeSelectClause() throws SemanticException { + // Turn off includeSubclasses on all FromElements. + FromClause from = getCurrentFromClause(); + List fromElements = from.getFromElements(); + for ( Iterator iterator = fromElements.iterator(); iterator.hasNext(); ) { + FromElement fromElement = (FromElement) iterator.next(); + fromElement.setIncludeSubclasses( false ); + } + } + + @Override + protected AST generatePositionalParameter(AST delimiterNode, AST numberNode) throws SemanticException { + // todo : we check this multiple times + if ( getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() && namedParameters != null ) { + throw new SemanticException( + "Cannot mix positional and named parameters: " + queryTranslatorImpl.getQueryString() + ); + } + + if ( numberNode == null ) { + throw new QueryException( + String.format( + Locale.ROOT, + ERROR_LEGACY_ORDINAL_PARAMS_NO_LONGER_SUPPORTED, + queryTranslatorImpl.getQueryString() + ) + ); + } + final String positionString = numberNode.getText(); + final int label = Integer.parseInt( positionString ); + trackPositionalParameterPositions( label ); + + final ParameterNode parameter = (ParameterNode) astFactory.create( PARAM, positionString ); + parameter.setText( "?" ); + + final int queryParamtersPosition = isFilter() + ? label + : label - 1; + final PositionalParameterSpecification paramSpec = new PositionalParameterSpecification( + delimiterNode.getLine(), + delimiterNode.getColumn(), + label, + queryParamtersPosition + ); + parameter.setHqlParameterSpecification( paramSpec ); + parameterSpecs.add( paramSpec ); + + return parameter; + } + + @SuppressWarnings("unchecked") + private void trackPositionalParameterPositions(int label) { + if ( positionalParameters == null ) { + positionalParameters = new HashMap(); + } + + final Integer loc = parameterCount++; + + final Object existingValue = positionalParameters.get( label ); + if ( existingValue == null ) { + positionalParameters.put( label, loc ); + } + else if ( existingValue instanceof Integer ) { + final ArrayList list = new ArrayList(); + positionalParameters.put( label, list ); + list.add( existingValue ); + list.add( loc ); + } + else { + ( (List) existingValue ).add( loc ); + } + } + + @Override + protected AST generateNamedParameter(AST delimiterNode, AST nameNode) throws SemanticException { + if ( getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() && positionalParameters != null ) { + throw new SemanticException( + "Cannot mix positional and named parameters: " + queryTranslatorImpl.getQueryString() + ); + } + final String name = nameNode.getText(); + trackNamedParameterPositions( name ); + + // create the node initially with the param name so that it shows + // appropriately in the "original text" attribute + final ParameterNode parameter = (ParameterNode) astFactory.create( NAMED_PARAM, name ); + parameter.setText( "?" ); + + final NamedParameterSpecification paramSpec = new NamedParameterSpecification( + delimiterNode.getLine(), + delimiterNode.getColumn(), + name + ); + parameter.setHqlParameterSpecification( paramSpec ); + parameterSpecs.add( paramSpec ); + + return parameter; + } + + @SuppressWarnings("unchecked") + private void trackNamedParameterPositions(String name) { + if ( namedParameters == null ) { + namedParameters = new HashMap(); + } + + final Integer loc = parameterCount++; + + final Object existingValue = namedParameters.get( name ); + if ( existingValue == null ) { + namedParameters.put( name, loc ); + } + else if ( existingValue instanceof Integer ) { + ArrayList list = new ArrayList<>( 4 ); + list.add( (Integer) existingValue ); + list.add( loc ); + namedParameters.put( name, list ); + } + else { + ( (List) existingValue ).add( loc ); + } + } + + @Override + protected void processConstant(AST constant) throws SemanticException { + literalProcessor.processConstant( + constant, + true + ); // Use the delegate, resolve identifiers as FROM element aliases. + } + + @Override + protected void processBoolean(AST constant) throws SemanticException { + literalProcessor.processBoolean( constant ); // Use the delegate. + } + + @Override + protected void processNumericLiteral(AST literal) { + literalProcessor.processNumeric( literal ); + } + + @Override + protected void processIndex(AST indexOp) throws SemanticException { + IndexNode indexNode = (IndexNode) indexOp; + indexNode.resolve( true, true ); + } + + @Override + protected void processFunction(AST functionCall, boolean inSelect) throws SemanticException { + MethodNode methodNode = (MethodNode) functionCall; + methodNode.resolve( inSelect ); + } + + @Override + protected void processCastFunction(AST castFunctionCall, boolean inSelect) throws SemanticException { + CastFunctionNode castFunctionNode = (CastFunctionNode) castFunctionCall; + castFunctionNode.resolve( inSelect ); + } + + @Override + protected void processAggregation(AST node, boolean inSelect) throws SemanticException { + AggregateNode aggregateNode = (AggregateNode) node; + aggregateNode.resolve(); + } + + @Override + protected void processConstructor(AST constructor) throws SemanticException { + ConstructorNode constructorNode = (ConstructorNode) constructor; + constructorNode.prepare(); + } + + @Override + protected void setAlias(AST selectExpr, AST ident) { + ( (SelectExpression) selectExpr ).setAlias( ident.getText() ); + // only put the alias (i.e., result variable) in selectExpressionsByResultVariable + // if is not defined in a subquery. + if ( !isSubQuery() ) { + selectExpressionsByResultVariable.put( ident.getText(), (SelectExpression) selectExpr ); + } + } + + @Override + protected boolean isOrderExpressionResultVariableRef(AST orderExpressionNode) throws SemanticException { + // ORDER BY is not supported in a subquery + // TODO: should an exception be thrown if an ORDER BY is in a subquery? + if ( !isSubQuery() && + orderExpressionNode.getType() == IDENT && + selectExpressionsByResultVariable.containsKey( orderExpressionNode.getText() ) ) { + return true; + } + return false; + } + + @Override + protected void handleResultVariableRef(AST resultVariableRef) throws SemanticException { + if ( isSubQuery() ) { + throw new SemanticException( + "References to result variables in subqueries are not supported." + ); + } + ( (ResultVariableRefNode) resultVariableRef ).setSelectExpression( + selectExpressionsByResultVariable.get( resultVariableRef.getText() ) + ); + } + + /** + * Returns the locations of all occurrences of the named parameter. + */ + @Override + public int[] getNamedParameterLocations(String name) throws QueryException { + Object o = namedParameters.get( name ); + if ( o == null ) { + throw new QueryException( + QueryTranslator.ERROR_NAMED_PARAMETER_DOES_NOT_APPEAR + name, + queryTranslatorImpl.getQueryString() + ); + } + if ( o instanceof Integer ) { + return new int[] {(Integer) o}; + } + else { + return ArrayHelper.toIntArray( (ArrayList) o ); + } + } + + public void addQuerySpaces(Serializable[] spaces) { + querySpaces.addAll( Arrays.asList( spaces ) ); + } + + public Type[] getReturnTypes() { + return selectClause.getQueryReturnTypes(); + } + + public String[] getReturnAliases() { + return selectClause.getQueryReturnAliases(); + } + + public SelectClause getSelectClause() { + return selectClause; + } + + public FromClause getFinalFromClause() { + FromClause top = currentFromClause; + while ( top.getParentFromClause() != null ) { + top = top.getParentFromClause(); + } + return top; + } + + public boolean isShallowQuery() { + // select clauses for insert statements should alwasy be treated as shallow + return getStatementType() == INSERT || queryTranslatorImpl.isShallowQuery(); + } + + public Map getEnabledFilters() { + return queryTranslatorImpl.getEnabledFilters(); + } + + public LiteralProcessor getLiteralProcessor() { + return literalProcessor; + } + + public ASTPrinter getASTPrinter() { + return TokenPrinters.SQL_TOKEN_PRINTER; + } + + public ArrayList getParameterSpecs() { + return parameterSpecs; + } + + public int getNumberOfParametersInSetClause() { + return numberOfParametersInSetClause; + } + + @Override + protected void evaluateAssignment(AST eq) throws SemanticException { + prepareLogicOperator( eq ); + Queryable persister = getCurrentFromClause().getFromElement().getQueryable(); + evaluateAssignment( eq, persister, -1 ); + } + + private void evaluateAssignment(AST eq, Queryable persister, int targetIndex) { + if ( persister.isMultiTable() ) { + // no need to even collect this information if the persister is considered multi-table + AssignmentSpecification specification = new AssignmentSpecification( eq, persister ); + if ( targetIndex >= 0 ) { + assignmentSpecifications.add( targetIndex, specification ); + } + else { + assignmentSpecifications.add( specification ); + } + numberOfParametersInSetClause += specification.getParameters().length; + } + } + + public ArrayList getAssignmentSpecifications() { + return assignmentSpecifications; + } + + @Override + protected AST createIntoClause(String path, AST propertySpec) throws SemanticException { + Queryable persister = (Queryable) getSessionFactoryHelper().requireClassPersister( path ); + + IntoClause intoClause = (IntoClause) getASTFactory().create( INTO, persister.getEntityName() ); + intoClause.setFirstChild( propertySpec ); + intoClause.initialize( persister ); + + addQuerySpaces( persister.getQuerySpaces() ); + + return intoClause; + } + + @Override + protected void prepareVersioned(AST updateNode, AST versioned) throws SemanticException { + UpdateStatement updateStatement = (UpdateStatement) updateNode; + FromClause fromClause = updateStatement.getFromClause(); + if ( versioned != null ) { + // Make sure that the persister is versioned + Queryable persister = fromClause.getFromElement().getQueryable(); + if ( !persister.isVersioned() ) { + throw new SemanticException( "increment option specified for update of non-versioned entity" ); + } + + VersionType versionType = persister.getVersionType(); + if ( versionType instanceof UserVersionType ) { + throw new SemanticException( "user-defined version types not supported for increment option" ); + } + + AST eq = getASTFactory().create( HqlSqlTokenTypes.EQ, "=" ); + AST versionPropertyNode = generateVersionPropertyNode( persister ); + + eq.setFirstChild( versionPropertyNode ); + + AST versionIncrementNode = null; + if ( isTimestampBasedVersion( versionType ) ) { + versionIncrementNode = getASTFactory().create( HqlSqlTokenTypes.PARAM, "?" ); + ParameterSpecification paramSpec = new VersionTypeSeedParameterSpecification( versionType ); + ( (ParameterNode) versionIncrementNode ).setHqlParameterSpecification( paramSpec ); + parameterSpecs.add( 0, paramSpec ); + } + else { + // Not possible to simply re-use the versionPropertyNode here as it causes + // OOM errors due to circularity :( + versionIncrementNode = getASTFactory().create( HqlSqlTokenTypes.PLUS, "+" ); + versionIncrementNode.setFirstChild( generateVersionPropertyNode( persister ) ); + versionIncrementNode.addChild( getASTFactory().create( HqlSqlTokenTypes.IDENT, "1" ) ); + } + + eq.addChild( versionIncrementNode ); + + evaluateAssignment( eq, persister, 0 ); + + AST setClause = updateStatement.getSetClause(); + AST currentFirstSetElement = setClause.getFirstChild(); + setClause.setFirstChild( eq ); + eq.setNextSibling( currentFirstSetElement ); + } + } + + private boolean isTimestampBasedVersion(VersionType versionType) { + final Class javaType = versionType.getReturnedClass(); + return Date.class.isAssignableFrom( javaType ) + || Calendar.class.isAssignableFrom( javaType ); + } + + private AST generateVersionPropertyNode(Queryable persister) throws SemanticException { + String versionPropertyName = persister.getPropertyNames()[persister.getVersionProperty()]; + AST versionPropertyRef = getASTFactory().create( HqlSqlTokenTypes.IDENT, versionPropertyName ); + AST versionPropertyNode = lookupNonQualifiedProperty( versionPropertyRef ); + resolve( versionPropertyNode ); + return versionPropertyNode; + } + + @Override + protected void prepareLogicOperator(AST operator) throws SemanticException { + ( (OperatorNode) operator ).initialize(); + } + + @Override + protected void prepareArithmeticOperator(AST operator) throws SemanticException { + ( (OperatorNode) operator ).initialize(); + } + + @Override + protected void validateMapPropertyExpression(AST node) throws SemanticException { + try { + FromReferenceNode fromReferenceNode = (FromReferenceNode) node; + QueryableCollection collectionPersister = fromReferenceNode.getFromElement().getQueryableCollection(); + if ( !Map.class.isAssignableFrom( collectionPersister.getCollectionType().getReturnedClass() ) ) { + throw new SemanticException( "node did not reference a map" ); + } + } + catch (SemanticException se) { + throw se; + } + catch (Throwable t) { + throw new SemanticException( "node did not reference a map" ); + } + } + + public Set getTreatAsDeclarationsByPath(String path) { + return hqlParser.getTreatMap().get( path ); + } + + public Dialect getDialect() { + return sessionFactoryHelper.getFactory().getServiceRegistry().getService( JdbcServices.class ).getDialect(); + } + + public static void panic() { + throw new QueryException( "TreeWalker: panic" ); + } +} diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java index aa202b42f..0e2245283 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLEnumVariableBindingsTests.java @@ -191,9 +191,11 @@ public void queryEnumArrayVariableBindingInEmbeddedRelation() { + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" - + "]}" + + "]}, " + + "{id=4, name=Anton Chekhov, books=[]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; - + //when Object result = executor.execute(query, variables).getData(); diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index c774276a7..5f27fa81c 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -2049,11 +2049,13 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteria Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); String expected1 = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[]}, " + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " + "{id=7, title=Three Sisters, genre=PLAY}" - + "]}" + + "]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //then: @@ -2066,7 +2068,9 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteria + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" - + "]}" + + "]}, " + + "{id=4, name=Anton Chekhov, books=[]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //then: diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java index b9dcd9eb3..e9062d7ba 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLWhereVariableBindingsTests.java @@ -233,17 +233,14 @@ public void queryWithPropertyWhereVariableBinding() throws IOException { // then then(executionResult.getErrors()).isEmpty(); Map result = executionResult.getData(); - then(result) - .extracting("Authors") - .flatExtracting("select") - .extracting("name") - .containsOnly("Leo Tolstoy"); - then(result) - .extracting("Authors") - .flatExtracting("select") - .flatExtracting("books") - .extracting("genre") - .containsOnly(NOVEL); + + String expected = "{Authors={select=[" + + "{name=Leo Tolstoy, books=[{genre=NOVEL}, {genre=NOVEL}]}, " + + "{name=Anton Chekhov, books=[]}, " + + "{name=Igor Dianov, books=[]}" + + "]}}"; + + then(result.toString()).isEqualTo(expected); } @Test diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index c59eccde8..4dbac7261 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -118,6 +118,31 @@ public void queryManyToManyTester() { .extracting(Character::getName) .containsOnly("Luke Skywalker"); } + + @Test + @Transactional + public void queryOneToManyTesterLeftOuterJoinTester() { + // given: + Query query = em.createQuery("select distinct author from Author as author \n" + + "left join fetch author.books as books on books.title like '%War%' \n" + + "where ( author.id in (1L, 4L, 8L) ) \n" + + "order by author.id asc"); + + String qu = "select d1 from Department d1 left join fetch d1.manager where exists \n" + + "(select d2 from Department d2 where d2.manager is null and d1 = d2)"; + + query.setHint("hibernate.query.passDistinctThrough", false); + + + + // when: + List result = query.getResultList(); + + assertThat(result).isNotEmpty(); + } + + + @Test public void getsNamesOfAllDroids() { @@ -1029,7 +1054,7 @@ public void queryFilterNestedManyToOneToDo() { //when: Object result = executor.execute(query).getData(); - + //then: assertThat(result.toString()).isEqualTo(expected); } @@ -1386,8 +1411,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQ() { String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1413,8 +1440,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1440,8 +1469,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseIN() { String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1467,8 +1498,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseINArray() { String expected = "{Humans={select=[" + "{name=Luke Skywalker, friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Darth Vader, friends=[]}, " + "{name=Han Solo, friends=[{name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}" + + "{name=Leia Organa, friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + + "{name=Wilhuff Tarkin, friends=[]}" + "]}}"; //when: @@ -1493,7 +1526,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNE() { "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1519,10 +1555,13 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; - + //when: Object result = executor.execute(query).getData(); @@ -1546,7 +1585,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNIN() { "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1573,7 +1615,10 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; @@ -1593,10 +1638,10 @@ public void queryEmbeddedWhereWithPluralAssociations() { " select {" + " name" + " friends(where:{" + - " NOT_EXISTS:{ friends:{name:{EQ:\"R2-D2\"}}}" + + " NOT_EXISTS:{ friendsOf: {name:{EQ:\"R2-D2\"}}}" + " }) {" + " name" + - " friends {" + + " friendsOf {" + " name" + " }" + " }" + @@ -1606,13 +1651,16 @@ public void queryEmbeddedWhereWithPluralAssociations() { String expected = "{Droids={select=[" + "{name=C-3PO, friends=[" - + "{name=R2-D2, friends=[" + + "{name=R2-D2, friendsOf=[" + + "{name=C-3PO}, " + "{name=Han Solo}, " + "{name=Leia Organa}, " + "{name=Luke Skywalker}" + "]}" - + "]}]}}"; - + + "]}, " + + "{name=R2-D2, friends=[]}" + + "]}}"; + //when: Object result = executor.execute(query).getData(); @@ -1636,7 +1684,10 @@ public void queryEmbeddedWhereWithPluralAssociationsNOT_EXISTS() { "}"; String expected = "{Humans={select=[" + + "{name=Luke Skywalker, friends=[]}, " + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Han Solo, friends=[]}, " + + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; From ab45ae956632df47bf8f437c42687b887c640de0 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Thu, 2 Apr 2020 21:08:38 -0700 Subject: [PATCH 02/16] fix: remove Hibernate HqlSqlWalker hack --- .../hql/internal/ast/HqlSqlWalker.java | 1489 ----------------- 1 file changed, 1489 deletions(-) delete mode 100644 graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java diff --git a/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java deleted file mode 100644 index 61b8594d4..000000000 --- a/graphql-jpa-query-schema/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java +++ /dev/null @@ -1,1489 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ -package org.hibernate.hql.internal.ast; - -import static org.hibernate.hql.spi.QueryTranslator.ERROR_LEGACY_ORDINAL_PARAMS_NO_LONGER_SUPPORTED; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import org.hibernate.QueryException; -import org.hibernate.dialect.Dialect; -import org.hibernate.engine.internal.JoinSequence; -import org.hibernate.engine.internal.ParameterBinder; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.hql.internal.CollectionProperties; -import org.hibernate.hql.internal.antlr.HqlSqlBaseWalker; -import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; -import org.hibernate.hql.internal.antlr.HqlTokenTypes; -import org.hibernate.hql.internal.antlr.SqlTokenTypes; -import org.hibernate.hql.internal.ast.tree.AggregateNode; -import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; -import org.hibernate.hql.internal.ast.tree.CastFunctionNode; -import org.hibernate.hql.internal.ast.tree.CollectionFunction; -import org.hibernate.hql.internal.ast.tree.ConstructorNode; -import org.hibernate.hql.internal.ast.tree.DeleteStatement; -import org.hibernate.hql.internal.ast.tree.DotNode; -import org.hibernate.hql.internal.ast.tree.EntityJoinFromElement; -import org.hibernate.hql.internal.ast.tree.FromClause; -import org.hibernate.hql.internal.ast.tree.FromElement; -import org.hibernate.hql.internal.ast.tree.FromElementFactory; -import org.hibernate.hql.internal.ast.tree.FromReferenceNode; -import org.hibernate.hql.internal.ast.tree.IdentNode; -import org.hibernate.hql.internal.ast.tree.IndexNode; -import org.hibernate.hql.internal.ast.tree.InsertStatement; -import org.hibernate.hql.internal.ast.tree.IntoClause; -import org.hibernate.hql.internal.ast.tree.MethodNode; -import org.hibernate.hql.internal.ast.tree.OperatorNode; -import org.hibernate.hql.internal.ast.tree.ParameterContainer; -import org.hibernate.hql.internal.ast.tree.ParameterNode; -import org.hibernate.hql.internal.ast.tree.QueryNode; -import org.hibernate.hql.internal.ast.tree.ResolvableNode; -import org.hibernate.hql.internal.ast.tree.RestrictableStatement; -import org.hibernate.hql.internal.ast.tree.ResultVariableRefNode; -import org.hibernate.hql.internal.ast.tree.SelectClause; -import org.hibernate.hql.internal.ast.tree.SelectExpression; -import org.hibernate.hql.internal.ast.tree.UpdateStatement; -import org.hibernate.hql.internal.ast.util.ASTPrinter; -import org.hibernate.hql.internal.ast.util.ASTUtil; -import org.hibernate.hql.internal.ast.util.AliasGenerator; -import org.hibernate.hql.internal.ast.util.JoinProcessor; -import org.hibernate.hql.internal.ast.util.LiteralProcessor; -import org.hibernate.hql.internal.ast.util.NodeTraverser; -import org.hibernate.hql.internal.ast.util.SessionFactoryHelper; -import org.hibernate.hql.internal.ast.util.SyntheticAndFactory; -import org.hibernate.hql.internal.ast.util.TokenPrinters; -import org.hibernate.hql.spi.QueryTranslator; -import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; -import org.hibernate.id.IdentifierGenerator; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.log.DeprecationLogger; -import org.hibernate.internal.util.StringHelper; -import org.hibernate.internal.util.collections.ArrayHelper; -import org.hibernate.param.CollectionFilterKeyParameterSpecification; -import org.hibernate.param.NamedParameterSpecification; -import org.hibernate.param.ParameterSpecification; -import org.hibernate.param.PositionalParameterSpecification; -import org.hibernate.param.VersionTypeSeedParameterSpecification; -import org.hibernate.persister.collection.CollectionPropertyNames; -import org.hibernate.persister.collection.QueryableCollection; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.persister.entity.Queryable; -import org.hibernate.sql.JoinType; -import org.hibernate.type.AssociationType; -import org.hibernate.type.CompositeType; -import org.hibernate.type.DbTimestampType; -import org.hibernate.type.Type; -import org.hibernate.type.VersionType; -import org.hibernate.usertype.UserVersionType; - -import antlr.ASTFactory; -import antlr.RecognitionException; -import antlr.SemanticException; -import antlr.collections.AST; - -/** - * Implements methods used by the HQL->SQL tree transform grammar (a.k.a. the second phase). - *
    - *
  • Isolates the Hibernate API-specific code from the ANTLR generated code.
  • - *
  • Handles the SQL fragments generated by the persisters in order to create the SELECT and FROM clauses, - * taking into account the joins and projections that are implied by the mappings (persister/queryable).
  • - *
  • Uses SqlASTFactory to create customized AST nodes.
  • - *
- * - * @see SqlASTFactory - */ -public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, ParameterBinder.NamedParameterSource { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( HqlSqlWalker.class ); - - private final QueryTranslatorImpl queryTranslatorImpl; - private final HqlParser hqlParser; - private final SessionFactoryHelper sessionFactoryHelper; - private final Map tokenReplacements; - private final AliasGenerator aliasGenerator = new AliasGenerator(); - private final LiteralProcessor literalProcessor; - private final ParseErrorHandler parseErrorHandler; - private final String collectionFilterRole; - - private FromClause currentFromClause; - private SelectClause selectClause; - - /** - * Maps each top-level result variable to its SelectExpression; - * (excludes result variables defined in subqueries) - */ - private Map selectExpressionsByResultVariable = new HashMap<>(); - - private Set querySpaces = new HashSet<>(); - - private int parameterCount; - private Map namedParameters; - private Map positionalParameters; - - private ArrayList parameterSpecs = new ArrayList<>(); - private int numberOfParametersInSetClause; - - private ArrayList assignmentSpecifications = new ArrayList(); - - private JoinType impliedJoinType = JoinType.INNER_JOIN; - - private boolean inEntityGraph; - - /** - * Create a new tree transformer. - * - * @param qti Back pointer to the query translator implementation that is using this tree transform. - * @param sfi The session factory implementor where the Hibernate mappings can be found. - * @param parser A reference to the phase-1 parser - * @param tokenReplacements Registers the token replacement map with the walker. This map will - * be used to substitute function names and constants. - * @param collectionRole The collection role name of the collection used as the basis for the - * filter, NULL if this is not a collection filter compilation. - */ - public HqlSqlWalker( - QueryTranslatorImpl qti, - SessionFactoryImplementor sfi, - HqlParser parser, - Map tokenReplacements, - String collectionRole) { - setASTFactory( new SqlASTFactory( this ) ); - // Initialize the error handling delegate. - this.parseErrorHandler = new ErrorTracker( qti.getQueryString() ); - this.queryTranslatorImpl = qti; - this.sessionFactoryHelper = new SessionFactoryHelper( sfi ); - this.literalProcessor = new LiteralProcessor( this ); - this.tokenReplacements = tokenReplacements; - this.collectionFilterRole = collectionRole; - this.hqlParser = parser; - } - - // handle trace logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - private int traceDepth; - - @Override - public void traceIn(String ruleName, AST tree) { - if ( !LOG.isTraceEnabled() ) { - return; - } - if ( inputState.guessing > 0 ) { - return; - } - String prefix = StringHelper.repeat( '-', ( traceDepth++ * 2 ) ) + "-> "; - String traceText = ruleName + " (" + buildTraceNodeName( tree ) + ")"; - LOG.trace( prefix + traceText ); - } - - private String buildTraceNodeName(AST tree) { - return tree == null - ? "???" - : tree.getText() + " [" + TokenPrinters.SQL_TOKEN_PRINTER.getTokenTypeName( tree.getType() ) + "]"; - } - - @Override - public void traceOut(String ruleName, AST tree) { - if ( !LOG.isTraceEnabled() ) { - return; - } - if ( inputState.guessing > 0 ) { - return; - } - String prefix = "<-" + StringHelper.repeat( '-', ( --traceDepth * 2 ) ) + " "; - LOG.trace( prefix + ruleName ); - } - - @Override - protected void prepareFromClauseInputTree(AST fromClauseInput) { - if ( !isSubQuery() ) { -// // inject param specifications to account for dynamic filter param values -// if ( ! getEnabledFilters().isEmpty() ) { -// Iterator filterItr = getEnabledFilters().values().iterator(); -// while ( filterItr.hasNext() ) { -// FilterImpl filter = ( FilterImpl ) filterItr.next(); -// if ( ! filter.getFilterDefinition().getParameterNames().isEmpty() ) { -// Iterator paramItr = filter.getFilterDefinition().getParameterNames().iterator(); -// while ( paramItr.hasNext() ) { -// String parameterName = ( String ) paramItr.next(); -// // currently param filters *only* work with single-column parameter types; -// // if that limitation is ever lifted, this logic will need to change to account for that -// ParameterNode collectionFilterKeyParameter = ( ParameterNode ) astFactory.create( PARAM, "?" ); -// DynamicFilterParameterSpecification paramSpec = new DynamicFilterParameterSpecification( -// filter.getName(), -// parameterName, -// filter.getFilterDefinition().getParameterType( parameterName ), -// positionalParameterCount++ -// ); -// collectionFilterKeyParameter.setHqlParameterSpecification( paramSpec ); -// parameterSpecs.add( paramSpec ); -// } -// } -// } -// } - - if ( isFilter() ) { - // Handle collection-filter compilation. - // IMPORTANT NOTE: This is modifying the INPUT (HQL) tree, not the output tree! - QueryableCollection persister = sessionFactoryHelper.getCollectionPersister( collectionFilterRole ); - Type collectionElementType = persister.getElementType(); - if ( !collectionElementType.isEntityType() ) { - throw new QueryException( "collection of values in filter: this" ); - } - - String collectionElementEntityName = persister.getElementPersister().getEntityName(); - ASTFactory inputAstFactory = hqlParser.getASTFactory(); - AST fromElement = inputAstFactory.create( HqlTokenTypes.FILTER_ENTITY, collectionElementEntityName ); - ASTUtil.createSibling( inputAstFactory, HqlTokenTypes.ALIAS, "this", fromElement ); - fromClauseInput.addChild( fromElement ); - // Show the modified AST. - LOG.debug( "prepareFromClauseInputTree() : Filter - Added 'this' as a from element..." ); - queryTranslatorImpl.showHqlAst( hqlParser.getAST() ); - - // Create a parameter specification for the collection filter... - final Type collectionFilterKeyType = sessionFactoryHelper.requireQueryableCollection( collectionFilterRole ) - .getKeyType(); - final ParameterNode collectionFilterKeyParameter = (ParameterNode) astFactory.create( PARAM, "?" ); - final CollectionFilterKeyParameterSpecification collectionFilterKeyParameterSpec = new CollectionFilterKeyParameterSpecification( - collectionFilterRole, - collectionFilterKeyType - ); - parameterCount++; - collectionFilterKeyParameter.setHqlParameterSpecification( collectionFilterKeyParameterSpec ); - parameterSpecs.add( collectionFilterKeyParameterSpec ); - } - } - } - - public boolean isFilter() { - return collectionFilterRole != null; - } - - public String getCollectionFilterRole() { - return collectionFilterRole; - } - - public boolean isInEntityGraph() { - return inEntityGraph; - } - - public SessionFactoryHelper getSessionFactoryHelper() { - return sessionFactoryHelper; - } - - public Map getTokenReplacements() { - return tokenReplacements; - } - - public AliasGenerator getAliasGenerator() { - return aliasGenerator; - } - - public FromClause getCurrentFromClause() { - return currentFromClause; - } - - public ParseErrorHandler getParseErrorHandler() { - return parseErrorHandler; - } - - @Override - public void reportError(RecognitionException e) { - parseErrorHandler.reportError( e ); // Use the delegate. - } - - @Override - public void reportError(String s) { - parseErrorHandler.reportError( s ); // Use the delegate. - } - - @Override - public void reportWarning(String s) { - parseErrorHandler.reportWarning( s ); - } - - /** - * Returns the set of unique query spaces (a.k.a. - * table names) that occurred in the query. - * - * @return A set of table names (Strings). - */ - public Set getQuerySpaces() { - return querySpaces; - } - - @Override - protected AST createFromElement(String path, AST alias, AST propertyFetch) throws SemanticException { - FromElement fromElement = currentFromClause.addFromElement( path, alias ); - fromElement.setAllPropertyFetch( propertyFetch != null ); - return fromElement; - } - - @Override - protected AST createFromFilterElement(AST filterEntity, AST alias) throws SemanticException { - FromElement fromElement = currentFromClause.addFromElement( filterEntity.getText(), alias ); - FromClause fromClause = fromElement.getFromClause(); - QueryableCollection persister = sessionFactoryHelper.getCollectionPersister( collectionFilterRole ); - // Get the names of the columns used to link between the collection - // owner and the collection elements. - String[] keyColumnNames = persister.getKeyColumnNames(); - String fkTableAlias = persister.isOneToMany() - ? fromElement.getTableAlias() - : fromClause.getAliasGenerator().createName( collectionFilterRole ); - JoinSequence join = sessionFactoryHelper.createJoinSequence(); - join.setRoot( persister, fkTableAlias ); - if ( !persister.isOneToMany() ) { - join.addJoin( - (AssociationType) persister.getElementType(), - fromElement.getTableAlias(), - JoinType.INNER_JOIN, - persister.getElementColumnNames( fkTableAlias ) - ); - } - join.addCondition( fkTableAlias, keyColumnNames, " = ?" ); - fromElement.setJoinSequence( join ); - fromElement.setFilter( true ); - LOG.debug( "createFromFilterElement() : processed filter FROM element." ); - return fromElement; - } - - @Override - protected void createFromJoinElement( - AST path, - AST alias, - int joinType, - AST fetchNode, - AST propertyFetch, - AST with) throws SemanticException { - boolean fetch = fetchNode != null; - if ( fetch && isSubQuery() ) { - throw new QueryException( "fetch not allowed in subquery from-elements" ); - } - - - // the incoming "path" can be either: - // 1) an implicit join path (join p.address.city) - // 2) an entity-join (join com.acme.User) - // - // so make the proper interpretation here... - - final EntityPersister entityJoinReferencedPersister = resolveEntityJoinReferencedPersister( path ); - if ( entityJoinReferencedPersister != null ) { - // `path` referenced an entity - final EntityJoinFromElement join = createEntityJoin( - entityJoinReferencedPersister, - alias, - joinType, - propertyFetch, - with - ); - - ( (FromReferenceNode) path ).setFromElement( join ); - } - else { - if ( path.getType() != SqlTokenTypes.DOT ) { - throw new SemanticException( "Path expected for join!" ); - } - - DotNode dot = (DotNode) path; - JoinType hibernateJoinType = JoinProcessor.toHibernateJoinType( joinType ); - dot.setJoinType( hibernateJoinType ); // Tell the dot node about the join type. - dot.setFetch( fetch ); - // Generate an explicit join for the root dot node. The implied joins will be collected and passed up - // to the root dot node. - dot.resolve( true, false, alias == null ? null : alias.getText() ); - - final FromElement fromElement; - if ( dot.getDataType() != null && dot.getDataType().isComponentType() ) { - if ( dot.getDataType().isAnyType() ) { - throw new SemanticException( "An AnyType attribute cannot be join fetched" ); - // ^^ because the discriminator (aka, the "meta columns") must be known to the SQL in - // a non-parameterized way. - } - FromElementFactory factory = new FromElementFactory( - getCurrentFromClause(), - dot.getLhs().getFromElement(), - dot.getPropertyPath(), - alias == null ? null : alias.getText(), - null, - false - ); - fromElement = factory.createComponentJoin( (CompositeType) dot.getDataType() ); - } - else { - fromElement = dot.getImpliedJoin(); - fromElement.setAllPropertyFetch( propertyFetch != null ); - - if ( with != null ) { -// if ( fetch ) { -// throw new SemanticException( "with-clause not allowed on fetched associations; use filters" ); -// } - handleWithFragment( fromElement, with ); - } - } - - if ( LOG.isDebugEnabled() ) { - LOG.debug( - "createFromJoinElement() : " - + getASTPrinter().showAsString( fromElement, "-- join tree --" ) - ); - } - } - } - - private EntityPersister resolveEntityJoinReferencedPersister(AST path) { - if ( path.getType() == IDENT ) { - final IdentNode pathIdentNode = (IdentNode) path; - String name = path.getText(); - if ( name == null ) { - name = pathIdentNode.getOriginalText(); - } - return sessionFactoryHelper.findEntityPersisterByName( name ); - } - else if ( path.getType() == DOT ) { - final String pathText = ASTUtil.getPathText( path ); - return sessionFactoryHelper.findEntityPersisterByName( pathText ); - } - return null; - } - - @Override - protected void finishFromClause(AST fromClause) throws SemanticException { - ( (FromClause) fromClause ).finishInit(); - } - - private EntityJoinFromElement createEntityJoin( - EntityPersister entityPersister, - AST aliasNode, - int joinType, - AST propertyFetch, - AST with) throws SemanticException { - final String alias = aliasNode == null ? null : aliasNode.getText(); - LOG.debugf( "Creating entity-join FromElement [%s -> %s]", alias, entityPersister.getEntityName() ); - EntityJoinFromElement join = new EntityJoinFromElement( - this, - getCurrentFromClause(), - entityPersister, - JoinProcessor.toHibernateJoinType( joinType ), - propertyFetch != null, - alias - ); - - if ( with != null ) { - handleWithFragment( join, with ); - } - - return join; - } - - private void handleWithFragment(FromElement fromElement, AST hqlWithNode) throws SemanticException { - try { - withClause( hqlWithNode ); - AST hqlSqlWithNode = returnAST; - if ( LOG.isDebugEnabled() ) { - LOG.debug( - "handleWithFragment() : " + getASTPrinter().showAsString( - hqlSqlWithNode, - "-- with clause --" - ) - ); - } - WithClauseVisitor visitor = new WithClauseVisitor( fromElement, queryTranslatorImpl ); - NodeTraverser traverser = new NodeTraverser( visitor ); - traverser.traverseDepthFirst( hqlSqlWithNode ); - - SqlGenerator sql = new SqlGenerator( getSessionFactoryHelper().getFactory() ); - sql.whereExpr( hqlSqlWithNode.getFirstChild() ); - - fromElement.setWithClauseFragment( "(" + sql.getSQL() + ")" ); - } - catch (SemanticException e) { - throw e; - } - catch (InvalidWithClauseException e) { - throw e; - } - catch (Exception e) { - throw new SemanticException( e.getMessage() ); - } - } - - private static class WithClauseVisitor implements NodeTraverser.VisitationStrategy { - private final FromElement joinFragment; - private final QueryTranslatorImpl queryTranslatorImpl; - - private FromElement referencedFromElement; - private String joinAlias; - - public WithClauseVisitor(FromElement fromElement, QueryTranslatorImpl queryTranslatorImpl) { - this.joinFragment = fromElement; - this.queryTranslatorImpl = queryTranslatorImpl; - } - - @Override - public void visit(AST node) { - // TODO : currently expects that the individual with expressions apply to the same sql table join. - // This may not be the case for joined-subclass where the property values - // might be coming from different tables in the joined hierarchy. At some - // point we should expand this to support that capability. However, that has - // some difficulties: - // 1) the biggest is how to handle ORs when the individual comparisons are - // linked to different sql joins. - // 2) here we would need to track each comparison individually, along with - // the join alias to which it applies and then pass that information - // back to the FromElement so it can pass it along to the JoinSequence - if ( node instanceof DotNode ) { - DotNode dotNode = (DotNode) node; - FromElement fromElement = dotNode.getFromElement(); - if ( referencedFromElement != null ) { -// if ( fromElement != referencedFromElement ) { -// throw new HibernateException( "with-clause referenced two different from-clause elements" ); -// } - } - else { - referencedFromElement = fromElement; - joinAlias = extractAppliedAlias( dotNode ); - // TODO : temporary - // needed because currently persister is the one that - // creates and renders the join fragments for inheritance - // hierarchies... -// if ( !joinAlias.equals( referencedFromElement.getTableAlias() ) ) { -// throw new InvalidWithClauseException( -// "with clause can only reference columns in the driving table", -// queryTranslatorImpl.getQueryString() -// ); -// } - } - } - else if ( node instanceof ParameterNode ) { - applyParameterSpecification( ( (ParameterNode) node ).getHqlParameterSpecification() ); - } - else if ( node instanceof ParameterContainer ) { - applyParameterSpecifications( (ParameterContainer) node ); - } - } - - private void applyParameterSpecifications(ParameterContainer parameterContainer) { - if ( parameterContainer.hasEmbeddedParameters() ) { - ParameterSpecification[] specs = parameterContainer.getEmbeddedParameters(); - for ( ParameterSpecification spec : specs ) { - applyParameterSpecification( spec ); - } - } - } - - private void applyParameterSpecification(ParameterSpecification paramSpec) { - joinFragment.addEmbeddedParameter( paramSpec ); - } - - private String extractAppliedAlias(DotNode dotNode) { - return dotNode.getText().substring( 0, dotNode.getText().indexOf( '.' ) ); - } - - public FromElement getReferencedFromElement() { - return referencedFromElement; - } - - public String getJoinAlias() { - return joinAlias; - } - } - - /** - * Sets the current 'FROM' context. - * - * @param fromNode The new 'FROM' context. - * @param inputFromNode The from node from the input AST. - */ - @Override - protected void pushFromClause(AST fromNode, AST inputFromNode) { - FromClause newFromClause = (FromClause) fromNode; - newFromClause.setParentFromClause( currentFromClause ); - currentFromClause = newFromClause; - } - - /** - * Returns to the previous 'FROM' context. - */ - private void popFromClause() { - currentFromClause = currentFromClause.getParentFromClause(); - } - - @Override - protected void lookupAlias(AST aliasRef) - throws SemanticException { - FromElement alias = currentFromClause.getFromElement( aliasRef.getText() ); - FromReferenceNode aliasRefNode = (FromReferenceNode) aliasRef; - aliasRefNode.setFromElement( alias ); - } - - @Override - protected void setImpliedJoinType(int joinType) { - impliedJoinType = JoinProcessor.toHibernateJoinType( joinType ); - } - - public JoinType getImpliedJoinType() { - return impliedJoinType; - } - - @Override - protected AST lookupProperty(AST dot, boolean root, boolean inSelect) throws SemanticException { - DotNode dotNode = (DotNode) dot; - FromReferenceNode lhs = dotNode.getLhs(); - AST rhs = lhs.getNextSibling(); - - // this used to be a switch statement based on the rhs's node type - // expecting it to be SqlTokenTypes.ELEMENTS or - // SqlTokenTypes.INDICES in the cases where the re-arranging is needed - // - // In such cases it additionally expects the RHS to be a CollectionFunction node. - // - // However, in my experience these assumptions sometimes did not works as sometimes the node - // types come in with the node type WEIRD_IDENT. What this does now is to: - // 1) see if the LHS is a collection - // 2) see if the RHS is a reference to one of the "collection properties". - // if both are true, we log a deprecation warning - if ( lhs.getDataType() != null - && lhs.getDataType().isCollectionType() ) { - if ( CollectionProperties.isCollectionProperty( rhs.getText() ) ) { - DeprecationLogger.DEPRECATION_LOGGER.logDeprecationOfCollectionPropertiesInHql( - rhs.getText(), - lhs.getPath() - ); - } - - // perform the re-arrangement - if ( CollectionPropertyNames.COLLECTION_INDICES.equalsIgnoreCase( rhs.getText() ) - || CollectionPropertyNames.COLLECTION_ELEMENTS.equalsIgnoreCase( rhs.getText() ) ) { - if ( LOG.isDebugEnabled() ) { - LOG.debugf( - "lookupProperty() %s => %s(%s)", - dotNode.getPath(), - rhs.getText(), - lhs.getPath() - ); - } - - final CollectionFunction f; - if ( rhs instanceof CollectionFunction ) { - f = (CollectionFunction) rhs; - } - else { - f = new CollectionFunction(); - f.initialize( SqlTokenTypes.METHOD_CALL, rhs.getText() ); - f.initialize( this ); - } - - // Re-arrange the tree so that the collection function is the root and the lhs is the path. - f.setFirstChild( lhs ); - lhs.setNextSibling( null ); - dotNode.setFirstChild( f ); - resolve( lhs ); // Don't forget to resolve the argument! - f.resolve( inSelect ); // Resolve the collection function now. - return f; - } - } - - // otherwise, resolve the path and return it - dotNode.resolveFirstChild(); - return dotNode; - } - - @Override - protected boolean isNonQualifiedPropertyRef(AST ident) { - final String identText = ident.getText(); - if ( currentFromClause.isFromElementAlias( identText ) ) { - return false; - } - - List fromElements = currentFromClause.getExplicitFromElements(); - if ( fromElements.size() == 1 ) { - final FromElement fromElement = (FromElement) fromElements.get( 0 ); - try { - LOG.tracev( "Attempting to resolve property [{0}] as a non-qualified ref", identText ); - return fromElement.getPropertyMapping( identText ).toType( identText ) != null; - } - catch (QueryException e) { - // Should mean that no such property was found - } - } - - return false; - } - - @Override - protected AST lookupNonQualifiedProperty(AST property) throws SemanticException { - final FromElement fromElement = (FromElement) currentFromClause.getExplicitFromElements().get( 0 ); - AST syntheticDotNode = generateSyntheticDotNodeForNonQualifiedPropertyRef( property, fromElement ); - return lookupProperty( syntheticDotNode, false, getCurrentClauseType() == HqlSqlTokenTypes.SELECT ); - } - - private AST generateSyntheticDotNodeForNonQualifiedPropertyRef(AST property, FromElement fromElement) { - AST dot = getASTFactory().create( DOT, "{non-qualified-property-ref}" ); - // TODO : better way?!? - ( (DotNode) dot ).setPropertyPath( ( (FromReferenceNode) property ).getPath() ); - - IdentNode syntheticAlias = (IdentNode) getASTFactory().create( IDENT, "{synthetic-alias}" ); - syntheticAlias.setFromElement( fromElement ); - syntheticAlias.setResolved(); - - dot.setFirstChild( syntheticAlias ); - dot.addChild( property ); - - return dot; - } - - @Override - protected void processQuery(AST select, AST query) throws SemanticException { - if ( LOG.isDebugEnabled() ) { - LOG.debugf( "processQuery() : %s", query.toStringTree() ); - } - - try { - QueryNode qn = (QueryNode) query; - - // Was there an explicit select expression? - boolean explicitSelect = select != null && select.getNumberOfChildren() > 0; - - // Add in the EntityGraph attribute nodes. - if ( queryTranslatorImpl.getEntityGraphQueryHint() != null ) { - final boolean oldInEntityGraph = inEntityGraph; - try { - inEntityGraph = true; - qn.getFromClause().getFromElements().addAll( - queryTranslatorImpl.getEntityGraphQueryHint().toFromElements( qn.getFromClause(), this ) - ); - } - finally { - inEntityGraph = oldInEntityGraph; - } - } - - if ( !explicitSelect ) { - // No explicit select expression; render the id and properties - // projection lists for every persister in the from clause into - // a single 'token node'. - //TODO: the only reason we need this stuff now is collection filters, - // we should get rid of derived select clause completely! - createSelectClauseFromFromClause( qn ); - } - else { - // Use the explicitly declared select expression; determine the - // return types indicated by each select token - useSelectClause( select ); - } - - // After that, process the JOINs. - // Invoke a delegate to do the work, as this is farily complex. - JoinProcessor joinProcessor = new JoinProcessor( this ); - joinProcessor.processJoins( qn ); - - // Attach any mapping-defined "ORDER BY" fragments - Iterator itr = qn.getFromClause().getProjectionList().iterator(); - while ( itr.hasNext() ) { - final FromElement fromElement = (FromElement) itr.next(); -// if ( fromElement.isFetch() && fromElement.isCollectionJoin() ) { - if ( fromElement.isFetch() && fromElement.getQueryableCollection() != null ) { - // Does the collection referenced by this FromElement - // specify an order-by attribute? If so, attach it to - // the query's order-by - if ( fromElement.getQueryableCollection().hasOrdering() ) { - String orderByFragment = fromElement - .getQueryableCollection() - .getSQLOrderByString( fromElement.getCollectionTableAlias() ); - qn.getOrderByClause().addOrderFragment( orderByFragment ); - } - if ( fromElement.getQueryableCollection().hasManyToManyOrdering() ) { - String orderByFragment = fromElement.getQueryableCollection() - .getManyToManyOrderByString( fromElement.getTableAlias() ); - qn.getOrderByClause().addOrderFragment( orderByFragment ); - } - } - } - } - finally { - popFromClause(); - } - } - - protected void postProcessDML(RestrictableStatement statement) throws SemanticException { - statement.getFromClause().resolve(); - - FromElement fromElement = (FromElement) statement.getFromClause().getFromElements().get( 0 ); - Queryable persister = fromElement.getQueryable(); - // Make #@%$^#^&# sure no alias is applied to the table name - fromElement.setText( persister.getTableName() ); - -// // append any filter fragments; the EMPTY_MAP is used under the assumption that -// // currently enabled filters should not affect this process -// if ( persister.getDiscriminatorType() != null ) { -// new SyntheticAndFactory( getASTFactory() ).addDiscriminatorWhereFragment( -// statement, -// persister, -// java.util.Collections.EMPTY_MAP, -// fromElement.getTableAlias() -// ); -// } - if ( persister.getDiscriminatorType() != null || !queryTranslatorImpl.getEnabledFilters().isEmpty() ) { - new SyntheticAndFactory( this ).addDiscriminatorWhereFragment( - statement, - persister, - queryTranslatorImpl.getEnabledFilters(), - fromElement.getTableAlias() - ); - } - - } - - @Override - protected void postProcessUpdate(AST update) throws SemanticException { - UpdateStatement updateStatement = (UpdateStatement) update; - - postProcessDML( updateStatement ); - } - - @Override - protected void postProcessDelete(AST delete) throws SemanticException { - postProcessDML( (DeleteStatement) delete ); - } - - @Override - protected void postProcessInsert(AST insert) throws SemanticException, QueryException { - InsertStatement insertStatement = (InsertStatement) insert; - insertStatement.validate(); - - SelectClause selectClause = insertStatement.getSelectClause(); - Queryable persister = insertStatement.getIntoClause().getQueryable(); - - if ( !insertStatement.getIntoClause().isExplicitIdInsertion() ) { - // the insert did not explicitly reference the id. See if - // 1) that is allowed - // 2) whether we need to alter the SQL tree to account for id - final IdentifierGenerator generator = persister.getIdentifierGenerator(); - if ( !BulkInsertionCapableIdentifierGenerator.class.isInstance( generator ) ) { - throw new QueryException( - "Invalid identifier generator encountered for implicit id handling as part of bulk insertions" - ); - } - final BulkInsertionCapableIdentifierGenerator capableGenerator = - BulkInsertionCapableIdentifierGenerator.class.cast( generator ); - if ( !capableGenerator.supportsBulkInsertionIdentifierGeneration() ) { - throw new QueryException( - "Identifier generator reported it does not support implicit id handling as part of bulk insertions" - ); - } - - final String fragment = capableGenerator.determineBulkInsertionIdentifierGenerationSelectFragment( - sessionFactoryHelper.getFactory().getDialect() - ); - if ( fragment != null ) { - // we got a fragment from the generator, so alter the sql tree... - // - // first, wrap the fragment as a node - AST fragmentNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, fragment ); - // next, rearrange the SQL tree to add the fragment node as the first select expression - AST originalFirstSelectExprNode = selectClause.getFirstChild(); - selectClause.setFirstChild( fragmentNode ); - fragmentNode.setNextSibling( originalFirstSelectExprNode ); - // finally, prepend the id column name(s) to the insert-spec - insertStatement.getIntoClause().prependIdColumnSpec(); - } - } - - if ( sessionFactoryHelper.getFactory().getDialect().supportsParametersInInsertSelect() ) { - AST child = selectClause.getFirstChild(); - int i = 0; - while ( child != null ) { - if ( child instanceof ParameterNode ) { - // infer the parameter type from the type listed in the INSERT INTO clause - ( (ParameterNode) child ).setExpectedType( - insertStatement.getIntoClause() - .getInsertionTypes()[selectClause.getParameterPositions().get( i )] - ); - i++; - } - child = child.getNextSibling(); - } - } - - final boolean includeVersionProperty = persister.isVersioned() && - !insertStatement.getIntoClause().isExplicitVersionInsertion() && - persister.isVersionPropertyInsertable(); - if ( includeVersionProperty ) { - // We need to seed the version value as part of this bulk insert - VersionType versionType = persister.getVersionType(); - AST versionValueNode = null; - - if ( sessionFactoryHelper.getFactory().getDialect().supportsParametersInInsertSelect() ) { - int[] sqlTypes = versionType.sqlTypes( sessionFactoryHelper.getFactory() ); - if ( sqlTypes == null || sqlTypes.length == 0 ) { - throw new IllegalStateException( versionType.getClass() + ".sqlTypes() returns null or empty array" ); - } - if ( sqlTypes.length > 1 ) { - throw new IllegalStateException( - versionType.getClass() + - ".sqlTypes() returns > 1 element; only single-valued versions are allowed." - ); - } - versionValueNode = getASTFactory().create( HqlSqlTokenTypes.PARAM, "?" ); - ParameterSpecification paramSpec = new VersionTypeSeedParameterSpecification( versionType ); - ( (ParameterNode) versionValueNode ).setHqlParameterSpecification( paramSpec ); - parameterSpecs.add( 0, paramSpec ); - - if ( sessionFactoryHelper.getFactory().getDialect().requiresCastingOfParametersInSelectClause() ) { - // we need to wrtap the param in a cast() - MethodNode versionMethodNode = (MethodNode) getASTFactory().create( - HqlSqlTokenTypes.METHOD_CALL, - "(" - ); - AST methodIdentNode = getASTFactory().create( HqlSqlTokenTypes.IDENT, "cast" ); - versionMethodNode.addChild( methodIdentNode ); - versionMethodNode.initializeMethodNode( methodIdentNode, true ); - AST castExprListNode = getASTFactory().create( HqlSqlTokenTypes.EXPR_LIST, "exprList" ); - methodIdentNode.setNextSibling( castExprListNode ); - castExprListNode.addChild( versionValueNode ); - versionValueNode.setNextSibling( - getASTFactory().create( - HqlSqlTokenTypes.IDENT, - sessionFactoryHelper.getFactory().getDialect().getTypeName( sqlTypes[0] ) - ) - ); - processFunction( versionMethodNode, true ); - versionValueNode = versionMethodNode; - } - } - else { - if ( isIntegral( versionType ) ) { - try { - Object seedValue = versionType.seed( null ); - versionValueNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, seedValue.toString() ); - } - catch (Throwable t) { - throw new QueryException( "could not determine seed value for version on bulk insert [" + versionType + "]" ); - } - } - else if ( isDatabaseGeneratedTimestamp( versionType ) ) { - String functionName = sessionFactoryHelper.getFactory() - .getDialect() - .getCurrentTimestampSQLFunctionName(); - versionValueNode = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, functionName ); - } - else { - throw new QueryException( "cannot handle version type [" + versionType + "] on bulk inserts with dialects not supporting parameterSpecs in insert-select statements" ); - } - } - - AST currentFirstSelectExprNode = selectClause.getFirstChild(); - selectClause.setFirstChild( versionValueNode ); - versionValueNode.setNextSibling( currentFirstSelectExprNode ); - - insertStatement.getIntoClause().prependVersionColumnSpec(); - } - - if ( insertStatement.getIntoClause().isDiscriminated() ) { - String sqlValue = insertStatement.getIntoClause().getQueryable().getDiscriminatorSQLValue(); - AST discrimValue = getASTFactory().create( HqlSqlTokenTypes.SQL_TOKEN, sqlValue ); - insertStatement.getSelectClause().addChild( discrimValue ); - } - - } - - private boolean isDatabaseGeneratedTimestamp(Type type) { - // currently only the Hibernate-supplied DbTimestampType is supported here - return DbTimestampType.class.isAssignableFrom( type.getClass() ); - } - - private boolean isIntegral(Type type) { - return Long.class.isAssignableFrom( type.getReturnedClass() ) - || Integer.class.isAssignableFrom( type.getReturnedClass() ) - || long.class.isAssignableFrom( type.getReturnedClass() ) - || int.class.isAssignableFrom( type.getReturnedClass() ); - } - - private void useSelectClause(AST select) throws SemanticException { - selectClause = (SelectClause) select; - selectClause.initializeExplicitSelectClause( currentFromClause ); - } - - private void createSelectClauseFromFromClause(QueryNode qn) throws SemanticException { - AST select = astFactory.create( SELECT_CLAUSE, "{derived select clause}" ); - AST sibling = qn.getFromClause(); - qn.setFirstChild( select ); - select.setNextSibling( sibling ); - selectClause = (SelectClause) select; - selectClause.initializeDerivedSelectClause( currentFromClause ); - LOG.debug( "Derived SELECT clause created." ); - } - - @Override - protected void resolve(AST node) throws SemanticException { - resolve(node, null); - } - - @Override - protected void resolve(AST node, AST predicateNode) throws SemanticException { - if ( node != null ) { - // This is called when it's time to fully resolve a path expression. - ResolvableNode r = (ResolvableNode) node; - if ( isInFunctionCall() ) { - r.resolveInFunctionCall( false, true ); - } - else { - r.resolve( false, true, null, null, predicateNode ); // Generate implicit joins, only if necessary. - } - } - } - - @Override - protected void resolveSelectExpression(AST node) throws SemanticException { - // This is called when it's time to fully resolve a path expression. - int type = node.getType(); - switch ( type ) { - case DOT: { - DotNode dot = (DotNode) node; - dot.resolveSelectExpression(); - break; - } - case ALIAS_REF: { - // Notify the FROM element that it is being referenced by the select. - FromReferenceNode aliasRefNode = (FromReferenceNode) node; - //aliasRefNode.resolve( false, false, aliasRefNode.getText() ); //TODO: is it kosher to do it here? - aliasRefNode.resolve( false, false ); //TODO: is it kosher to do it here? - FromElement fromElement = aliasRefNode.getFromElement(); - if ( fromElement != null ) { - fromElement.setIncludeSubclasses( true ); - } - break; - } - default: { - break; - } - } - } - - @Override - protected void beforeSelectClause() throws SemanticException { - // Turn off includeSubclasses on all FromElements. - FromClause from = getCurrentFromClause(); - List fromElements = from.getFromElements(); - for ( Iterator iterator = fromElements.iterator(); iterator.hasNext(); ) { - FromElement fromElement = (FromElement) iterator.next(); - fromElement.setIncludeSubclasses( false ); - } - } - - @Override - protected AST generatePositionalParameter(AST delimiterNode, AST numberNode) throws SemanticException { - // todo : we check this multiple times - if ( getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() && namedParameters != null ) { - throw new SemanticException( - "Cannot mix positional and named parameters: " + queryTranslatorImpl.getQueryString() - ); - } - - if ( numberNode == null ) { - throw new QueryException( - String.format( - Locale.ROOT, - ERROR_LEGACY_ORDINAL_PARAMS_NO_LONGER_SUPPORTED, - queryTranslatorImpl.getQueryString() - ) - ); - } - final String positionString = numberNode.getText(); - final int label = Integer.parseInt( positionString ); - trackPositionalParameterPositions( label ); - - final ParameterNode parameter = (ParameterNode) astFactory.create( PARAM, positionString ); - parameter.setText( "?" ); - - final int queryParamtersPosition = isFilter() - ? label - : label - 1; - final PositionalParameterSpecification paramSpec = new PositionalParameterSpecification( - delimiterNode.getLine(), - delimiterNode.getColumn(), - label, - queryParamtersPosition - ); - parameter.setHqlParameterSpecification( paramSpec ); - parameterSpecs.add( paramSpec ); - - return parameter; - } - - @SuppressWarnings("unchecked") - private void trackPositionalParameterPositions(int label) { - if ( positionalParameters == null ) { - positionalParameters = new HashMap(); - } - - final Integer loc = parameterCount++; - - final Object existingValue = positionalParameters.get( label ); - if ( existingValue == null ) { - positionalParameters.put( label, loc ); - } - else if ( existingValue instanceof Integer ) { - final ArrayList list = new ArrayList(); - positionalParameters.put( label, list ); - list.add( existingValue ); - list.add( loc ); - } - else { - ( (List) existingValue ).add( loc ); - } - } - - @Override - protected AST generateNamedParameter(AST delimiterNode, AST nameNode) throws SemanticException { - if ( getSessionFactoryHelper().isStrictJPAQLComplianceEnabled() && positionalParameters != null ) { - throw new SemanticException( - "Cannot mix positional and named parameters: " + queryTranslatorImpl.getQueryString() - ); - } - final String name = nameNode.getText(); - trackNamedParameterPositions( name ); - - // create the node initially with the param name so that it shows - // appropriately in the "original text" attribute - final ParameterNode parameter = (ParameterNode) astFactory.create( NAMED_PARAM, name ); - parameter.setText( "?" ); - - final NamedParameterSpecification paramSpec = new NamedParameterSpecification( - delimiterNode.getLine(), - delimiterNode.getColumn(), - name - ); - parameter.setHqlParameterSpecification( paramSpec ); - parameterSpecs.add( paramSpec ); - - return parameter; - } - - @SuppressWarnings("unchecked") - private void trackNamedParameterPositions(String name) { - if ( namedParameters == null ) { - namedParameters = new HashMap(); - } - - final Integer loc = parameterCount++; - - final Object existingValue = namedParameters.get( name ); - if ( existingValue == null ) { - namedParameters.put( name, loc ); - } - else if ( existingValue instanceof Integer ) { - ArrayList list = new ArrayList<>( 4 ); - list.add( (Integer) existingValue ); - list.add( loc ); - namedParameters.put( name, list ); - } - else { - ( (List) existingValue ).add( loc ); - } - } - - @Override - protected void processConstant(AST constant) throws SemanticException { - literalProcessor.processConstant( - constant, - true - ); // Use the delegate, resolve identifiers as FROM element aliases. - } - - @Override - protected void processBoolean(AST constant) throws SemanticException { - literalProcessor.processBoolean( constant ); // Use the delegate. - } - - @Override - protected void processNumericLiteral(AST literal) { - literalProcessor.processNumeric( literal ); - } - - @Override - protected void processIndex(AST indexOp) throws SemanticException { - IndexNode indexNode = (IndexNode) indexOp; - indexNode.resolve( true, true ); - } - - @Override - protected void processFunction(AST functionCall, boolean inSelect) throws SemanticException { - MethodNode methodNode = (MethodNode) functionCall; - methodNode.resolve( inSelect ); - } - - @Override - protected void processCastFunction(AST castFunctionCall, boolean inSelect) throws SemanticException { - CastFunctionNode castFunctionNode = (CastFunctionNode) castFunctionCall; - castFunctionNode.resolve( inSelect ); - } - - @Override - protected void processAggregation(AST node, boolean inSelect) throws SemanticException { - AggregateNode aggregateNode = (AggregateNode) node; - aggregateNode.resolve(); - } - - @Override - protected void processConstructor(AST constructor) throws SemanticException { - ConstructorNode constructorNode = (ConstructorNode) constructor; - constructorNode.prepare(); - } - - @Override - protected void setAlias(AST selectExpr, AST ident) { - ( (SelectExpression) selectExpr ).setAlias( ident.getText() ); - // only put the alias (i.e., result variable) in selectExpressionsByResultVariable - // if is not defined in a subquery. - if ( !isSubQuery() ) { - selectExpressionsByResultVariable.put( ident.getText(), (SelectExpression) selectExpr ); - } - } - - @Override - protected boolean isOrderExpressionResultVariableRef(AST orderExpressionNode) throws SemanticException { - // ORDER BY is not supported in a subquery - // TODO: should an exception be thrown if an ORDER BY is in a subquery? - if ( !isSubQuery() && - orderExpressionNode.getType() == IDENT && - selectExpressionsByResultVariable.containsKey( orderExpressionNode.getText() ) ) { - return true; - } - return false; - } - - @Override - protected void handleResultVariableRef(AST resultVariableRef) throws SemanticException { - if ( isSubQuery() ) { - throw new SemanticException( - "References to result variables in subqueries are not supported." - ); - } - ( (ResultVariableRefNode) resultVariableRef ).setSelectExpression( - selectExpressionsByResultVariable.get( resultVariableRef.getText() ) - ); - } - - /** - * Returns the locations of all occurrences of the named parameter. - */ - @Override - public int[] getNamedParameterLocations(String name) throws QueryException { - Object o = namedParameters.get( name ); - if ( o == null ) { - throw new QueryException( - QueryTranslator.ERROR_NAMED_PARAMETER_DOES_NOT_APPEAR + name, - queryTranslatorImpl.getQueryString() - ); - } - if ( o instanceof Integer ) { - return new int[] {(Integer) o}; - } - else { - return ArrayHelper.toIntArray( (ArrayList) o ); - } - } - - public void addQuerySpaces(Serializable[] spaces) { - querySpaces.addAll( Arrays.asList( spaces ) ); - } - - public Type[] getReturnTypes() { - return selectClause.getQueryReturnTypes(); - } - - public String[] getReturnAliases() { - return selectClause.getQueryReturnAliases(); - } - - public SelectClause getSelectClause() { - return selectClause; - } - - public FromClause getFinalFromClause() { - FromClause top = currentFromClause; - while ( top.getParentFromClause() != null ) { - top = top.getParentFromClause(); - } - return top; - } - - public boolean isShallowQuery() { - // select clauses for insert statements should alwasy be treated as shallow - return getStatementType() == INSERT || queryTranslatorImpl.isShallowQuery(); - } - - public Map getEnabledFilters() { - return queryTranslatorImpl.getEnabledFilters(); - } - - public LiteralProcessor getLiteralProcessor() { - return literalProcessor; - } - - public ASTPrinter getASTPrinter() { - return TokenPrinters.SQL_TOKEN_PRINTER; - } - - public ArrayList getParameterSpecs() { - return parameterSpecs; - } - - public int getNumberOfParametersInSetClause() { - return numberOfParametersInSetClause; - } - - @Override - protected void evaluateAssignment(AST eq) throws SemanticException { - prepareLogicOperator( eq ); - Queryable persister = getCurrentFromClause().getFromElement().getQueryable(); - evaluateAssignment( eq, persister, -1 ); - } - - private void evaluateAssignment(AST eq, Queryable persister, int targetIndex) { - if ( persister.isMultiTable() ) { - // no need to even collect this information if the persister is considered multi-table - AssignmentSpecification specification = new AssignmentSpecification( eq, persister ); - if ( targetIndex >= 0 ) { - assignmentSpecifications.add( targetIndex, specification ); - } - else { - assignmentSpecifications.add( specification ); - } - numberOfParametersInSetClause += specification.getParameters().length; - } - } - - public ArrayList getAssignmentSpecifications() { - return assignmentSpecifications; - } - - @Override - protected AST createIntoClause(String path, AST propertySpec) throws SemanticException { - Queryable persister = (Queryable) getSessionFactoryHelper().requireClassPersister( path ); - - IntoClause intoClause = (IntoClause) getASTFactory().create( INTO, persister.getEntityName() ); - intoClause.setFirstChild( propertySpec ); - intoClause.initialize( persister ); - - addQuerySpaces( persister.getQuerySpaces() ); - - return intoClause; - } - - @Override - protected void prepareVersioned(AST updateNode, AST versioned) throws SemanticException { - UpdateStatement updateStatement = (UpdateStatement) updateNode; - FromClause fromClause = updateStatement.getFromClause(); - if ( versioned != null ) { - // Make sure that the persister is versioned - Queryable persister = fromClause.getFromElement().getQueryable(); - if ( !persister.isVersioned() ) { - throw new SemanticException( "increment option specified for update of non-versioned entity" ); - } - - VersionType versionType = persister.getVersionType(); - if ( versionType instanceof UserVersionType ) { - throw new SemanticException( "user-defined version types not supported for increment option" ); - } - - AST eq = getASTFactory().create( HqlSqlTokenTypes.EQ, "=" ); - AST versionPropertyNode = generateVersionPropertyNode( persister ); - - eq.setFirstChild( versionPropertyNode ); - - AST versionIncrementNode = null; - if ( isTimestampBasedVersion( versionType ) ) { - versionIncrementNode = getASTFactory().create( HqlSqlTokenTypes.PARAM, "?" ); - ParameterSpecification paramSpec = new VersionTypeSeedParameterSpecification( versionType ); - ( (ParameterNode) versionIncrementNode ).setHqlParameterSpecification( paramSpec ); - parameterSpecs.add( 0, paramSpec ); - } - else { - // Not possible to simply re-use the versionPropertyNode here as it causes - // OOM errors due to circularity :( - versionIncrementNode = getASTFactory().create( HqlSqlTokenTypes.PLUS, "+" ); - versionIncrementNode.setFirstChild( generateVersionPropertyNode( persister ) ); - versionIncrementNode.addChild( getASTFactory().create( HqlSqlTokenTypes.IDENT, "1" ) ); - } - - eq.addChild( versionIncrementNode ); - - evaluateAssignment( eq, persister, 0 ); - - AST setClause = updateStatement.getSetClause(); - AST currentFirstSetElement = setClause.getFirstChild(); - setClause.setFirstChild( eq ); - eq.setNextSibling( currentFirstSetElement ); - } - } - - private boolean isTimestampBasedVersion(VersionType versionType) { - final Class javaType = versionType.getReturnedClass(); - return Date.class.isAssignableFrom( javaType ) - || Calendar.class.isAssignableFrom( javaType ); - } - - private AST generateVersionPropertyNode(Queryable persister) throws SemanticException { - String versionPropertyName = persister.getPropertyNames()[persister.getVersionProperty()]; - AST versionPropertyRef = getASTFactory().create( HqlSqlTokenTypes.IDENT, versionPropertyName ); - AST versionPropertyNode = lookupNonQualifiedProperty( versionPropertyRef ); - resolve( versionPropertyNode ); - return versionPropertyNode; - } - - @Override - protected void prepareLogicOperator(AST operator) throws SemanticException { - ( (OperatorNode) operator ).initialize(); - } - - @Override - protected void prepareArithmeticOperator(AST operator) throws SemanticException { - ( (OperatorNode) operator ).initialize(); - } - - @Override - protected void validateMapPropertyExpression(AST node) throws SemanticException { - try { - FromReferenceNode fromReferenceNode = (FromReferenceNode) node; - QueryableCollection collectionPersister = fromReferenceNode.getFromElement().getQueryableCollection(); - if ( !Map.class.isAssignableFrom( collectionPersister.getCollectionType().getReturnedClass() ) ) { - throw new SemanticException( "node did not reference a map" ); - } - } - catch (SemanticException se) { - throw se; - } - catch (Throwable t) { - throw new SemanticException( "node did not reference a map" ); - } - } - - public Set getTreatAsDeclarationsByPath(String path) { - return hqlParser.getTreatMap().get( path ); - } - - public Dialect getDialect() { - return sessionFactoryHelper.getFactory().getServiceRegistry().getService( JdbcServices.class ).getDialect(); - } - - public static void panic() { - throw new QueryException( "TreeWalker: panic" ); - } -} From 8c546ee496f75ff628ab3bba17be3df68c550e9b Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Thu, 2 Apr 2020 21:12:05 -0700 Subject: [PATCH 03/16] fix: add GraphQLJpaOneToManyDataFetcher for inline query filter --- .../impl/GraphQLJpaOneToMayDataFetcher.java | 65 ++ .../schema/impl/GraphQLJpaQueryFactory.java | 492 +++++---- .../schema/impl/GraphQLJpaSchemaBuilder.java | 227 +++-- .../schema/StarwarsQueryExecutorTests.java | 954 +++++++++--------- 4 files changed, 906 insertions(+), 832 deletions(-) create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java new file mode 100644 index 000000000..80781a0de --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 IntroPro Ventures Inc. and/or its affiliates. + * + * 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.introproventures.graphql.jpa.query.schema.impl; + +import java.util.Optional; + +import javax.persistence.TypedQuery; +import javax.persistence.metamodel.PluralAttribute; + +import com.introproventures.graphql.jpa.query.support.GraphQLSupport; +import graphql.language.Argument; +import graphql.language.Field; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; + +/** + * One-To-Many DataFetcher that uses where argument to filter collection attributes + * + * @author Igor Dianov + * + */ +class GraphQLJpaOneToManyDataFetcher implements DataFetcher { + + private final PluralAttribute attribute; + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaOneToManyDataFetcher(GraphQLJpaQueryFactory queryFactory, + PluralAttribute attribute) { + this.queryFactory = queryFactory; + this.attribute = attribute; + } + + @Override + public Object get(DataFetchingEnvironment environment) { + Field field = environment.getField(); + + Object source = environment.getSource(); + Optional whereArg = GraphQLSupport.getWhereArgument(field); + + // Resolve collection query if where argument is present or any field in selection has orderBy argument + if (whereArg.isPresent()) { + TypedQuery query = queryFactory.getCollectionQuery(environment, field, true); + + return query.getResultList(); + } + + // Let hibernate resolve collection query + return queryFactory.getAttributeValue(source, attribute); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 6219ca21c..2db56561b 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -80,7 +80,6 @@ import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import com.introproventures.graphql.jpa.query.support.GraphQLSupport; - import graphql.GraphQLException; import graphql.execution.MergedField; import graphql.execution.ValuesResolver; @@ -116,10 +115,10 @@ public final class GraphQLJpaQueryFactory { private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaQueryFactory.class); - + protected static final String WHERE = "where"; protected static final String OPTIONAL = "optional"; - + protected static final String HIBERNATE_QUERY_PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough"; protected static final String ORG_HIBERNATE_CACHEABLE = "org.hibernate.cacheable"; protected static final String ORG_HIBERNATE_FETCH_SIZE = "org.hibernate.fetchSize"; @@ -146,33 +145,33 @@ private GraphQLJpaQueryFactory(Builder builder) { public DataFetchingEnvironment getQueryEnvironment(DataFetchingEnvironment environment, MergedField queryField) { - - // Override query environment with associated entity object type and select field + + // Override query environment with associated entity object type and select field return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .fieldType(getEntityObjectType()) .mergedField(queryField) .build(); } - - public List queryKeys(DataFetchingEnvironment environment, - int firstResult, + + public List queryKeys(DataFetchingEnvironment environment, + int firstResult, int maxResults) { MergedField queryField = resolveQueryField(environment.getField()); - // Override query environment with associated entity object type and + // Override query environment with associated entity object type and final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - - TypedQuery keysQuery = getKeysQuery(queryEnvironment, + + TypedQuery keysQuery = getKeysQuery(queryEnvironment, queryEnvironment.getField()); - + keysQuery.setFirstResult(firstResult) .setMaxResults(maxResults); if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Keys Query String:\n {}", getJPQLQueryString(keysQuery)); } - + return keysQuery.getResultList(); } @@ -188,25 +187,25 @@ public List queryResultList(DataFetchingEnvironment environment, maxResults); } - + protected Stream queryResultStream(DataFetchingEnvironment environment, int maxResults, List keys) { MergedField queryField = resolveQueryField(environment.getField()); - - // Override query environment with associated entity object type and + + // Override query environment with associated entity object type and final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); final int fetchSize = Integer.min(maxResults, defaultFetchSize); final boolean isDistinct = resolveDistinctArgument(queryEnvironment.getField()); - + final TypedQuery query = getQuery(queryEnvironment, queryEnvironment.getField(), isDistinct, keys.toArray()); - + // Let' try reduce overhead and disable all caching query.setHint(ORG_HIBERNATE_READ_ONLY, true); - query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize); + query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize); query.setHint(ORG_HIBERNATE_CACHEABLE, false); - + // Let's not pass distinct if enabled to have better performance if(isDistinct) { query.setHint(HIBERNATE_QUERY_PASS_DISTINCT_THROUGH, false); @@ -218,49 +217,49 @@ protected Stream queryResultStream(DataFetchingEnvironment environment, // Let's execute query and get wrap result into stream return query.getResultStream() - .peek(entityManager::detach); + .peek(entityManager::detach); } - + protected Object querySingleResult(final DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); - + final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - + TypedQuery query = getQuery(queryEnvironment, queryEnvironment.getField(), true); - + if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Single Result Query String:\n {}", getJPQLQueryString(query)); } - + return query.getSingleResult(); - } - + } + public Long queryTotalCount(DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); final DataFetchingEnvironment queryEnvironment = getQueryEnvironment(environment, queryField); - + TypedQuery countQuery = getCountQuery(queryEnvironment, queryEnvironment.getField()); - + if (logger.isDebugEnabled()) { logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(countQuery)); } - + return countQuery.getSingleResult(); - } + } protected TypedQuery getQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Object... keys) { DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .localContext(Boolean.TRUE) // Fetch mode .build(); - + CriteriaQuery criteriaQuery = getCriteriaQuery(queryEnvironment, field, isDistinct, keys); return entityManager.createQuery(criteriaQuery); } - + protected TypedQuery getCountQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Long.class); @@ -271,24 +270,24 @@ protected TypedQuery getCountQuery(DataFetchingEnvironment environment, Fi .localContext(Boolean.FALSE) // Join mode .build(); root.alias("root"); - + query.select(cb.count(root)); - + List predicates = field.getArguments().stream() .map(it -> getPredicate(field, cb, root, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); - + query.where(predicates.toArray(new Predicate[0])); return entityManager.createQuery(query); } - + protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, Field field) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery(Object.class); Root from = query.from(entityType); - + from.alias("root"); DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) @@ -303,12 +302,12 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F .collect(Collectors.toList()); query.multiselect(selection); } - + List predicates = field.getArguments().stream() .map(it -> getPredicate(field, cb, from, null, queryEnvironment, it)) .filter(it -> it != null) .collect(Collectors.toList()); - + query.where(predicates.toArray(new Predicate[0])); GraphQLSupport.fields(field.getSelectionSet()) @@ -319,46 +318,51 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F // Process the orderBy clause mayBeAddOrderBy(selection, query, cb, selectionPath, queryEnvironment); }); - + mayBeAddDefaultOrderBy(query, from, cb); - + return entityManager.createQuery(query); } - + @SuppressWarnings( { "rawtypes", "unchecked" } ) protected TypedQuery getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct) { - + Object source = environment.getSource(); - + SingularAttribute parentIdAttribute = entityType.getId(Object.class); - + Object parentIdValue = getAttributeValue(source, parentIdAttribute); - + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); Root from = query.from(entityType); - + + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.TRUE) + .build(); + from.alias("owner"); - + // Must use inner join in parent context Join join = from.join(field.getName()) .on(cb.in(from.get(parentIdAttribute.getName())) .value(parentIdValue)); - + query.select(join.alias(field.getName())); - - List predicates = getFieldPredicates(field, query, cb, from, join, environment); + + List predicates = getFieldPredicates(field, query, cb, from, join, queryEnvironment); query.where( predicates.toArray(new Predicate[0]) ); - - // optionally add default ordering + + // optionally add default ordering mayBeAddDefaultOrderBy(query, join, cb); - + return entityManager.createQuery(query.distinct(isDistinct)); } - + @SuppressWarnings("unchecked") protected CriteriaQuery getCriteriaQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Object... keys) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); @@ -376,7 +380,7 @@ protected CriteriaQuery getCriteriaQuery(DataFetchingEnvironment environm if (keys.length > 0) { if(hasIdAttribute()) { predicates.add(from.get(idAttributeName()).in(keys)); - } // array of idClass attributes + } // array of idClass attributes else if (hasIdClassAttribue()) { String[] names = idClassAttributeNames(); Map> idKeys = new HashMap<>(); @@ -388,20 +392,20 @@ else if (hasIdClassAttribue()) { .forEach(i -> { idKeys.computeIfAbsent(names[i], key -> new ArrayList<>()) .add(values[i]); - }); + }); }); - + List idPredicates = Stream.of(names) .map(name -> { return from.get(name) .in(idKeys.get(name).toArray(new Object[0])); }) .collect(Collectors.toList()); - - predicates.add(cb.and(idPredicates.toArray(new Predicate[0]))); + + predicates.add(cb.and(idPredicates.toArray(new Predicate[0]))); } } - + // Use AND clause to filter results if(!predicates.isEmpty()) query.where(predicates.toArray(new Predicate[0])); @@ -411,14 +415,14 @@ else if (hasIdClassAttribue()) { return query.distinct(isDistinct); } - + Stream zipped(List lista, List listb, BiFunction zipper){ int shortestLength = Math.min(lista.size(),listb.size()); return IntStream.range(0,shortestLength).mapToObj( i -> { return zipper.apply(lista.get(i), listb.get(i)); - }); - } - + }); + } + protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery query, CriteriaBuilder cb, Path fieldPath, DataFetchingEnvironment environment) { // Singular attributes only if (fieldPath.getModel() instanceof SingularAttribute) { @@ -429,18 +433,18 @@ protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery query, Crit .map(argument -> getOrderByValue(argument, environment)) .ifPresent(orderBy -> { List orders = new ArrayList<>(query.getOrderList()); - + if ("DESC".equals(orderBy.getName())) { orders.add(cb.desc(fieldPath)); } else { orders.add(cb.asc(fieldPath)); } - + query.orderBy(orders); }); } } - + protected final List getFieldPredicates(Field field, CriteriaQuery query, CriteriaBuilder cb, Root root, From from, DataFetchingEnvironment environment) { List arguments = new ArrayList<>(); @@ -450,13 +454,12 @@ protected final List getFieldPredicates(Field field, CriteriaQuery GraphQLSupport.fields(field.getSelectionSet()) .filter(selection -> isPersistent(environment, selection.getName())) .forEach(selection -> { - + Path fieldPath = from.get(selection.getName()); From fetch = null; Optional optionalArgument = getArgument(selection, OPTIONAL); Optional whereArgument = getArgument(selection, WHERE); Boolean isOptional = null; - Boolean isFetchOn = false; // Build predicate arguments for singular attributes only if(fieldPath.getModel() instanceof SingularAttribute) { @@ -468,7 +471,7 @@ protected final List getFieldPredicates(Field field, CriteriaQuery if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE ) { - // Let's do fugly conversion + // Let's do fugly conversion isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) .orElse(attribute.isOptional()); @@ -482,11 +485,11 @@ protected final List getFieldPredicates(Field field, CriteriaQuery .map(it -> new Argument(selection.getName() + "." + it.getName(), it.getValue())) .collect(Collectors.toList())); - + } } else { - // We must add plural attributes with explicit join fetch - // Let's do fugly conversion + // We must add plural attributes with explicit join fetch + // Let's do fugly conversion // the many end is a collection, and it is always optional by default (empty collection) isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) .orElse(toManyDefaultOptional); @@ -495,50 +498,45 @@ protected final List getFieldPredicates(Field field, CriteriaQuery EntityType entityType = getEntityType(objectType); PluralAttribute attribute = (PluralAttribute) entityType.getAttribute(selection.getName()); - + Optional whereArg = GraphQLSupport.getWhereArgument(selection); + // Let's join fetch element collections to avoid filtering their values used where search criteria if(PersistentAttributeType.ELEMENT_COLLECTION == attribute.getPersistentAttributeType()) { from.fetch(selection.getName(), JoinType.LEFT); - } else { + } else if(!whereArg.isPresent()) { // Let's apply fetch join to retrieve associated plural attributes fetch = reuseFetch(from, selection.getName(), isOptional); } - - isFetchOn = true; } - // Let's build join fetch graph to avoid Hibernate error: + // Let's build join fetch graph to avoid Hibernate error: // "query specified join fetching, but the owner of the fetched association was not present in the select list" if(selection.getSelectionSet() != null && fetch != null) { Map variables = environment.getVariables(); - + GraphQLFieldDefinition fieldDefinition = getFieldDefinition(environment.getGraphQLSchema(), this.getObjectType(environment), - selection); - + selection); + List values = whereArgument.map(Collections::singletonList) .orElse(Collections.emptyList()); - + Map fieldArguments = new ValuesResolver().getArgumentValues(fieldDefinition.getArguments(), values, variables); - - DataFetchingEnvironment fieldEnvironment = wherePredicateEnvironment(environment, - fieldDefinition, + + DataFetchingEnvironment fieldEnvironment = wherePredicateEnvironment(environment, + fieldDefinition, fieldArguments); - if(isFetchOn) { - Predicate[] fetchPredicates = getFieldPredicates(selection, - query, - cb, - root, - fetch, - fieldEnvironment).toArray(new Predicate[] {}); - ((Join) fetch).on(fetchPredicates); - } else { - predicates.addAll(getFieldPredicates(selection, query, cb, root, fetch, fieldEnvironment)); - } + + predicates.addAll(getFieldPredicates(selection, + query, + cb, + root, + fetch, + fieldEnvironment)); } }); - + arguments.addAll(field.getArguments()); arguments.stream() @@ -576,7 +574,7 @@ protected void mayBeAddDefaultOrderBy(CriteriaQuery query, From from, Cr } } else { AttributePropertyDescriptor attribute = attributePropertyDescriptor.get(); - + GraphQLDefaultOrderBy order = attribute.getDefaultOrderBy().get(); if (order.asc()) { query.orderBy(cb.asc(from.get(attribute.getName()))); @@ -590,7 +588,7 @@ protected void mayBeAddDefaultOrderBy(CriteriaQuery query, From from, Cr protected boolean isPredicateArgument(Argument argument) { return !isOrderByArgument(argument) && !isOptionalArgument(argument); } - + protected boolean isOrderByArgument(Argument argument) { return GraphQLJpaSchemaBuilder.ORDER_BY_PARAM_NAME.equals(argument.getName()); } @@ -598,29 +596,29 @@ protected boolean isOrderByArgument(Argument argument) { protected boolean isOptionalArgument(Argument argument) { return OPTIONAL.equals(argument.getName()); } - + protected Optional getArgument(Field selectedField, String argumentName) { return selectedField.getArguments() .stream() .filter(it -> it.getName() .equals(argumentName)) - .findFirst(); + .findFirst(); } - + protected > R getAttribute(String attributeName) { return (R) entityType.getAttribute(attributeName); } - + @SuppressWarnings( "unchecked" ) protected Predicate getPredicate(Field field, CriteriaBuilder cb, Root from, From path, DataFetchingEnvironment environment, Argument argument) { - if(isLogicalArgument(argument) || - isDistinctArgument(argument) || isPageArgument(argument) || + if(isLogicalArgument(argument) || + isDistinctArgument(argument) || isPageArgument(argument) || isAfterArgument(argument) || isFirstArgument(argument) ) { return null; - } + } else if(isWhereArgument(argument)) { return getWherePredicate(cb, from, path, argumentEnvironment(environment, argument), argument); - } + } else if(!argument.getName().contains(".")) { Attribute argumentEntityAttribute = getAttribute(environment, argument.getName()); @@ -628,7 +626,7 @@ else if(!argument.getName().contains(".")) { if (argumentEntityAttribute instanceof PluralAttribute) { // Apply left outer join to retrieve optional associations Boolean isFetch = environment.getLocalContext(); - + return (isFetch ? reuseFetch(from, argument.getName(), false) : reuseJoin(from, argument.getName(), false)) .in(convertValue(environment, argument, argument.getValue())); } @@ -660,23 +658,23 @@ else if(!argument.getName().contains(".")) { } } } - - + + @SuppressWarnings( "unchecked" ) private > R getValue(Argument argument, DataFetchingEnvironment environment) { Value value = argument.getValue(); - + if(VariableReference.class.isInstance(value)) { Object variableValue = getVariableReferenceValue((VariableReference) value, environment); - + GraphQLArgument graphQLArgument = environment.getExecutionStepInfo() .getFieldDefinition() .getArgument(argument.getName()); - + return (R) AstValueHelper.astFromValue(variableValue, graphQLArgument.getType()); } - + return (R) value; } @@ -700,17 +698,17 @@ protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From predicateArguments = new LinkedHashMap<>(); predicateArguments.put(logical.name(), environment.getArguments()); - + DataFetchingEnvironment predicateDataFetchingEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(predicateArguments) .build(); Argument predicateArgument = new Argument(logical.name(), whereValue); - + return getArgumentPredicate(cb, (path != null) ? path : root, predicateDataFetchingEnvironment, predicateArgument); } @@ -720,23 +718,23 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From from, ObjectValue whereValue = getValue(argument, environment); if (whereValue.getChildren().isEmpty()) - return cb.disjunction(); - + return cb.disjunction(); + Logical logical = extractLogical(argument); List predicates = new ArrayList<>(); whereValue.getObjectFields().stream() .filter(it -> Logical.names().contains(it.getName())) - .map(it -> { + .map(it -> { Map arguments = getFieldArguments(environment, it, argument); - + if(it.getValue() instanceof ArrayValue) { return getArgumentsPredicate(cb, from, argumentEnvironment(environment, arguments), new Argument(it.getName(), it.getValue())); } - + return getArgumentPredicate(cb, from, argumentEnvironment(environment, arguments), new Argument(it.getName(), it.getValue())); @@ -757,57 +755,57 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From from, return getCompoundPredicate(cb, predicates, logical); } - - - protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, - CriteriaBuilder cb, - From from, + + + protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, + CriteriaBuilder cb, + From from, Logical logical, - ObjectField objectField, - Argument argument, + ObjectField objectField, + Argument argument, Map arguments ) { if(isEntityType(environment)) { Attribute attribute = getAttribute(environment, argument.getName()); - + if(attribute.isAssociation()) { GraphQLFieldDefinition fieldDefinition = getFieldDefinition(environment.getGraphQLSchema(), this.getObjectType(environment), new Field(objectField.getName())); - + if(Arrays.asList(Logical.EXISTS, Logical.NOT_EXISTS).contains(logical) ) { AbstractQuery query = environment.getRoot(); Subquery subquery = query.subquery(attribute.getJavaType()); From correlation = Root.class.isInstance(from) ? subquery.correlate((Root) from) : subquery.correlate((Join) from); - + Join correlationJoin = correlation.join(objectField.getName()); - + DataFetchingEnvironment existsEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .root(subquery) - .build(); - + .build(); + Predicate restriction = getArgumentPredicate(cb, correlationJoin, wherePredicateEnvironment(existsEnvironment, fieldDefinition, arguments), argument); - - + + Predicate exists = cb.exists(subquery.select((Join) correlationJoin) .where(restriction)); - + return logical == Logical.EXISTS ? exists : cb.not(exists); } - + AbstractQuery query = environment.getRoot(); Boolean isFetch = environment.getLocalContext(); Boolean isOptional = isOptionalAttribute(attribute); - + From context = (isSubquery(query) || isCountQuery(query) || !isFetch) ? reuseJoin(from, objectField.getName(), isOptional) : reuseFetch(from, objectField.getName(), isOptional); - - return getArgumentPredicate(cb, + + return getArgumentPredicate(cb, context, wherePredicateEnvironment(environment, fieldDefinition, arguments), argument); @@ -825,7 +823,7 @@ protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, protected boolean isSubquery(AbstractQuery query) { return Subquery.class.isInstance(query); } - + protected boolean isCountQuery(AbstractQuery query) { return Optional.ofNullable(query.getSelection()) .map(Selection::getJavaType) @@ -845,13 +843,13 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, Logical logical = extractLogical(argument); List predicates = new ArrayList<>(); - + List> arguments = environment.getArgument(logical.name()); List values = whereValue.getValues() .stream() .map(ObjectValue.class::cast).collect(Collectors.toList()); - - List>> tuples = + + List>> tuples = IntStream.range(0, values.size()) .mapToObj(i -> new SimpleEntry>(values.get(i), arguments.get(i))) @@ -865,19 +863,19 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, .map(it -> { Map args = e.getValue(); Argument arg = new Argument(it.getName(), it.getValue()); - + if(ArrayValue.class.isInstance(it.getValue())) { return getArgumentsPredicate(cb, from, argumentEnvironment(environment, args), arg); } - + return getArgumentPredicate(cb, from, argumentEnvironment(environment, args), arg); - + })) .forEach(predicates::add); @@ -889,18 +887,18 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, .map(it -> { Map args = e.getValue(); Argument arg = new Argument(it.getName(), it.getValue()); - + return getObjectFieldPredicate(environment, cb, from, logical, it, arg, args); })) .filter(predicate -> predicate != null) .forEach(predicates::add); - + return getCompoundPredicate(cb, predicates, logical); } - + private Map getFieldArguments(DataFetchingEnvironment environment, ObjectField field, Argument argument) { Map arguments; - + if (environment.getArgument(argument.getName()) instanceof Collection) { Collection> list = environment.getArgument(argument.getName()); @@ -911,10 +909,10 @@ private Map getFieldArguments(DataFetchingEnvironment environmen } else { arguments = environment.getArgument(argument.getName()); } - + return arguments; } - + private Logical extractLogical(Argument argument) { return Optional.of(argument.getName()) .filter(it -> Logical.names().contains(it)) @@ -941,7 +939,7 @@ private Predicate getLogicalPredicates(String fieldName, .map(it -> { Map args = getFieldArguments(environment, it, argument); Argument arg = new Argument(it.getName(), it.getValue()); - + return getLogicalPredicate(it.getName(), cb, path, @@ -952,14 +950,14 @@ private Predicate getLogicalPredicates(String fieldName, .forEach(predicates::add); return getCompoundPredicate(cb, predicates, logical); - } - + } + private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From path, ObjectField objectField, DataFetchingEnvironment environment, Argument argument) { ObjectValue expressionValue; - + if(objectField.getValue() instanceof ObjectValue) expressionValue = (ObjectValue) objectField.getValue(); - else + else expressionValue = new ObjectValue(Arrays.asList(objectField)); if(expressionValue.getChildren().isEmpty()) @@ -972,22 +970,22 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From // Let's parse logical expressions, i.e. AND, OR expressionValue.getObjectFields().stream() .filter(it -> Logical.names().contains(it.getName())) - .map(it -> { + .map(it -> { Map args = getFieldArguments(environment, it, argument); Argument arg = new Argument(it.getName(), it.getValue()); - + if(it.getValue() instanceof ArrayValue) { return getLogicalPredicates(fieldName, cb, path, it, argumentEnvironment(environment, args), arg); } - + return getLogicalPredicate(fieldName, cb, path, it, argumentEnvironment(environment, args), arg); }) .forEach(predicates::add); - + // Let's parse relation criteria expressions if present, i.e. books, author, etc. if(expressionValue.getObjectFields() .stream() @@ -999,7 +997,7 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From Map args = new LinkedHashMap<>(); Argument arg = new Argument(logical.name(), expressionValue); boolean isOptional = false; - + if(Logical.names().contains(argument.getName())) { args.put(logical.name(), environment.getArgument(argument.getName())); } else { @@ -1007,13 +1005,13 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From isOptional = isOptionalAttribute(getAttribute(environment, argument.getName())); } - - return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional), + + return getArgumentPredicate(cb, reuseJoin(path, fieldName, isOptional), wherePredicateEnvironment(environment, fieldDefinition, args), arg); } - - // Let's parse simple Criteria expressions, i.e. EQ, LIKE, etc. + + // Let's parse simple Criteria expressions, i.e. EQ, LIKE, etc. JpaPredicateBuilder pb = new JpaPredicateBuilder(cb); expressionValue.getObjectFields() @@ -1030,15 +1028,15 @@ private Predicate getLogicalPredicate(String fieldName, CriteriaBuilder cb, From return getCompoundPredicate(cb, predicates, logical); } - + private Predicate getCompoundPredicate(CriteriaBuilder cb, List predicates, Logical logical) { if(predicates.isEmpty()) return cb.disjunction(); - + if(predicates.size() == 1) { return predicates.get(0); } - + return (logical == Logical.OR) ? cb.or(predicates.toArray(new Predicate[0])) : cb.and(predicates.toArray(new Predicate[0])); @@ -1047,16 +1045,16 @@ private Predicate getCompoundPredicate(CriteriaBuilder cb, List predi private PredicateFilter getPredicateFilter(ObjectField objectField, DataFetchingEnvironment environment, Argument argument) { EnumSet options = EnumSet.of(PredicateFilter.Criteria.valueOf(argument.getName())); - + Map valueArguments = new LinkedHashMap(); valueArguments.put(objectField.getName(), environment.getArgument(argument.getName())); - + DataFetchingEnvironment dataFetchingEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(valueArguments) .build(); - + Argument dataFetchingArgument = new Argument(objectField.getName(), argument.getValue()); - + Object filterValue = convertValue( dataFetchingEnvironment, dataFetchingArgument, argument.getValue() ); return new PredicateFilter(objectField.getName(), filterValue, options ); @@ -1067,10 +1065,10 @@ protected final DataFetchingEnvironment argumentEnvironment(DataFetchingEnvironm .arguments(arguments) .build(); } - + protected final DataFetchingEnvironment argumentEnvironment(DataFetchingEnvironment environment, Argument argument) { Map arguments = environment.getArgument(argument.getName()); - + return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) .arguments(arguments) .build(); @@ -1083,7 +1081,7 @@ protected final DataFetchingEnvironment wherePredicateEnvironment(DataFetchingEn .fieldType(fieldDefinition.getType()) .build(); } - + /** * @param fieldName * @return Path of compound field to the primitive type @@ -1146,7 +1144,7 @@ private From reuseJoin(From from, String fieldName, boolean outer) { } return outer ? from.join(fieldName, JoinType.LEFT) : from.join(fieldName); } - + // trying to find already existing fetch joins to reuse private From reuseFetch(From from, String fieldName, boolean outer) { @@ -1156,8 +1154,8 @@ private From reuseFetch(From from, String fieldName, boolean outer) { } } return outer ? (From) from.fetch(fieldName, JoinType.LEFT) : (From) from.fetch(fieldName); - } - + } + @SuppressWarnings( { "unchecked", "rawtypes" } ) protected Object convertValue(DataFetchingEnvironment environment, Argument argument, Value value) { if (value instanceof NullValue) { @@ -1179,15 +1177,15 @@ else if (value instanceof VariableReference) { if(javaType.isEnum()) { if(argumentValue instanceof Collection) { List values = new ArrayList<>(); - + Collection.class.cast(argumentValue) - .forEach(it -> values.add(Enum.valueOf(javaType, it.toString()))); + .forEach(it -> values.add(Enum.valueOf(javaType, it.toString()))); return values; } else { return Enum.valueOf(javaType, argumentValue.toString()); } - } - else { + } + else { // Get resolved variable in environment arguments return argumentValue; } @@ -1201,28 +1199,28 @@ else if (value instanceof VariableReference) { arrayValue = Collection.class.cast(arrayValue.iterator() .next()); } - + // Let's convert enum types, i.e. array of strings or EnumValue into Java type if(getJavaType(environment, argument).isEnum()) { Function objectValue = (obj) -> Value.class.isInstance(obj) ? Value.class.cast(obj) : new EnumValue(obj.toString()); - // Return real typed resolved array values converted into Java enums + // Return real typed resolved array values converted into Java enums return arrayValue.stream() - .map((it) -> convertValue(environment, - argument, + .map((it) -> convertValue(environment, + argument, objectValue.apply(it))) .collect(Collectors.toList()); - } - // Let's try handle Ast Value types + } + // Let's try handle Ast Value types else if(arrayValue.stream() .anyMatch(it->it instanceof Value)) { return arrayValue.stream() - .map(it -> convertValue(environment, - argument, + .map(it -> convertValue(environment, + argument, Value.class.cast(it))) .collect(Collectors.toList()); - } + } // Return real typed resolved array value, i.e. Date, UUID, Long else { return arrayValue; @@ -1247,7 +1245,7 @@ else if (value instanceof EnumValue) { } else if (value instanceof ObjectValue) { Class javaType = getJavaType(environment, argument); Map values = environment.getArgument(argument.getName()); - + try { return getJavaBeanValue(javaType, values); } catch (Exception cause) { @@ -1257,13 +1255,13 @@ else if (value instanceof EnumValue) { return value; } - + private Object getJavaBeanValue(Class javaType, Map values) throws Exception { Constructor constructor = javaType.getConstructor(); constructor.setAccessible(true); Object javaBean = constructor.newInstance(); - + values.entrySet() .stream() .forEach(entry -> { @@ -1271,11 +1269,11 @@ private Object getJavaBeanValue(Class javaType, Map values) t entry.getKey(), entry.getValue()); }); - + return javaBean; } - - private void setPropertyValue(Object javaBean, String propertyName, Object propertyValue) { + + private void setPropertyValue(Object javaBean, String propertyName, Object propertyValue) { try { BeanInfo bi = Introspector.getBeanInfo(javaBean.getClass()); PropertyDescriptor pds[] = bi.getPropertyDescriptors(); @@ -1283,7 +1281,7 @@ private void setPropertyValue(Object javaBean, String propertyName, Object prope if (pd.getName().equals(propertyName)) { Method setter = pd.getWriteMethod(); setter.setAccessible(true); - + if (setter != null) { setter.invoke(javaBean, new Object[] {propertyValue} ); } @@ -1323,8 +1321,8 @@ private Attribute getAttribute(DataFetchingEnvironment environment, String return entityType.getAttribute(argument); } - - + + private boolean isOptionalAttribute(Attribute attribute) { if(SingularAttribute.class.isInstance(attribute)) { @@ -1333,10 +1331,10 @@ private boolean isOptionalAttribute(Attribute attribute) { else if(PluralAttribute.class.isInstance(attribute)) { return true; } - + return false; } - + /** * Resolve JPA model entity type from GraphQL objectType * @@ -1352,13 +1350,13 @@ private EntityType getEntityType(GraphQLObjectType objectType) { .findFirst() .get(); } - + private boolean isEntityType(DataFetchingEnvironment environment) { GraphQLObjectType objectType = getObjectType(environment); return entityManager.getMetamodel() .getEntities().stream() .anyMatch(it -> it.getName().equals(objectType.getName())); - } + } /** * Resolve GraphQL object type from Argument output type. @@ -1404,16 +1402,16 @@ protected GraphQLFieldDefinition getFieldDefinition(GraphQLSchema schema, GraphQ } GraphQLFieldDefinition fieldDefinition = parentType.getFieldDefinition(field.getName()); - + if (fieldDefinition != null) { return fieldDefinition; } - + throw new GraphQLException("unknown field " + field.getName()); } - + protected final boolean isManagedType(Attribute attribute) { - return attribute.getPersistentAttributeType() != PersistentAttributeType.EMBEDDED + return attribute.getPersistentAttributeType() != PersistentAttributeType.EMBEDDED && attribute.getPersistentAttributeType() != PersistentAttributeType.BASIC && attribute.getPersistentAttributeType() != PersistentAttributeType.ELEMENT_COLLECTION; } @@ -1463,7 +1461,7 @@ protected final > R getObjectFieldValue(ObjectValue objectVal @SuppressWarnings( "unchecked" ) protected final T getArgumentValue(DataFetchingEnvironment environment, Argument argument, Class type) { Value value = argument.getValue(); - + if(VariableReference.class.isInstance(value)) { return (T) environment.getVariables() @@ -1484,7 +1482,7 @@ else if (FloatValue.class.isInstance(value)) { else if (NullValue.class.isInstance(value)) { return (T) null; } - + throw new IllegalArgumentException("Not supported"); } @@ -1492,7 +1490,7 @@ protected boolean isPersistent(DataFetchingEnvironment environment, String attributeName) { GraphQLObjectType objectType = getObjectType(environment); EntityType entityType = getEntityType(objectType); - + return isPersistent(entityType, attributeName); } @@ -1500,11 +1498,11 @@ protected boolean isPersistent(EntityType entityType, String attributeName) { try { return entityType.getAttribute(attributeName) != null; - } catch (Exception ignored) { } - + } catch (Exception ignored) { } + return false; } - + protected boolean isTransient(DataFetchingEnvironment environment, String attributeName) { return !isPersistent(environment, attributeName); @@ -1514,31 +1512,31 @@ protected boolean isTransient(EntityType entityType, String attributeName) { return !isPersistent(entityType, attributeName); } - - + + protected String getJPQLQueryString(TypedQuery query) { try { Object queryImpl = query.unwrap(TypedQuery.class); - + java.lang.reflect.Field queryStringField = ReflectionUtil.getField(queryImpl.getClass(), "queryString"); - + ReflectionUtil.forceAccess(queryStringField); - + return queryStringField.get(queryImpl) .toString(); - + } catch (Exception ignored) { logger.error("Error getting JPQL string", ignored); } - + return null; } protected boolean hasIdAttribute() { return entityType.getIdType() != null; } - + protected String idAttributeName() { return entityType.getId(entityType.getIdType() .getJavaType()).getName(); @@ -1547,7 +1545,7 @@ protected String idAttributeName() { protected boolean hasIdClassAttribue() { return entityType.getIdClassAttributes() != null; } - + protected String[] idClassAttributeNames() { return entityType.getIdClassAttributes() .stream() @@ -1556,7 +1554,7 @@ protected String[] idClassAttributeNames() { .collect(Collectors.toList()) .toArray(new String[0]); } - + /** * Fetches the value of the given SingularAttribute on the given * entity. @@ -1578,7 +1576,7 @@ protected FieldType getAttributeValue(EntityType entity, } catch (Exception e) { throw new RuntimeException(e); } - } + } /** * Fetches the value of the given SingularAttribute on the given @@ -1602,48 +1600,48 @@ protected FieldType getAttributeValue(EntityType entity, throw new RuntimeException(e); } } - + protected boolean resolveDistinctArgument(Field field) { - Argument distinctArg = extractArgument(field, - SELECT_DISTINCT_PARAM_NAME, + Argument distinctArg = extractArgument(field, + SELECT_DISTINCT_PARAM_NAME, new BooleanValue(defaultDistinct)); - + return BooleanValue.class.cast(distinctArg.getValue()) .isValue(); } - + public boolean isDefaultDistinct() { return defaultDistinct; } - + public String getSelectNodeName() { return selectNodeName; } public MergedField resolveQueryField(Field rootNode) { Optional recordsSelection = GraphQLSupport.searchByFieldName(rootNode, getSelectNodeName()); - + Field queryField = recordsSelection.map(selectNode -> Field.newField(selectNode.getName()) .selectionSet(selectNode.getSelectionSet()) .arguments(rootNode.getArguments()) .directives(selectNode.getDirectives()) .build()) - .orElse(rootNode); + .orElse(rootNode); return MergedField.newMergedField(queryField) .build(); } - + public GraphQLObjectType getEntityObjectType() { return entityObjectType; } - + public int getDefaultFetchSize() { return defaultFetchSize; } - + private MergedField flattenEmbeddedIdArguments(Field field) { // manage object arguments (EmbeddedId) final List argumentsWhereObjectsAreFlattened = field.getArguments() @@ -1659,11 +1657,11 @@ private MergedField flattenEmbeddedIdArguments(Field field) { } }) .collect(Collectors.toList()); - + return MergedField.newMergedField(field.transform(builder -> builder.arguments(argumentsWhereObjectsAreFlattened))) .build(); } - + /** * Creates builder to build {@link GraphQLJpaQueryFactory}. @@ -1826,5 +1824,5 @@ public IBuildStage withDefaultFetchSize(int defaultFetchSize) { public GraphQLJpaQueryFactory build() { return new GraphQLJpaQueryFactory(this); } - } + } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 1154f1213..56ddad6f3 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -411,34 +411,34 @@ private GraphQLArgument computeWhereArgument(ManagedType managedType) { .collect(Collectors.toList()) ) .build(); - + return GraphQLArgument.newArgument() .name(QUERY_WHERE_PARAM_NAME) .description("Where logical specification") .type(whereInputObject) .build(); - + } private String resolveWhereArgumentTypeName(ManagedType managedType) { String typeName=resolveTypeName(managedType); - + return namingStrategy.pluralize(typeName)+"CriteriaExpression"; } - + private String resolveSubqueryArgumentTypeName(ManagedType managedType) { String typeName=resolveTypeName(managedType); - + return namingStrategy.pluralize(typeName)+"SubqueryCriteriaExpression"; } private GraphQLInputObjectType getSubqueryInputType(ManagedType managedType) { return subqueryInputObjectCache.computeIfAbsent(managedType, this::computeSubqueryInputType); } - + private GraphQLInputObjectType computeSubqueryInputType(ManagedType managedType) { String type=resolveSubqueryArgumentTypeName(managedType); - + Builder whereInputObject = GraphQLInputObjectType.newInputObject() .name(type) .description("Where logical AND specification of the provided list of criteria expressions") @@ -458,13 +458,13 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType managedTy .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(new GraphQLTypeReference(type))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(new GraphQLTypeReference(type))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(Attribute::isAssociation) @@ -473,37 +473,37 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType managedTy .map(this::getWhereInputRelationField) .collect(Collectors.toList()) ); - + return whereInputObject.build(); - - } - + + } + 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"; - + } - + private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) { String type=resolveWhereInputTypeName(managedType); - + Builder whereInputObject = GraphQLInputObjectType.newInputObject() .name(type) .description("Where logical AND specification of the provided list of criteria expressions") @@ -523,13 +523,13 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(this::isValidInput) @@ -545,15 +545,15 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) .map(this::getWhereInputRelationField) .collect(Collectors.toList()) ); - - + + return whereInputObject.build(); - - } - + + } + private GraphQLInputObjectField getWhereInputRelationField(Attribute attribute) { ManagedType foreignType = getForeignType(attribute); - + String type = resolveWhereInputTypeName(foreignType); String description = getSchemaDescription(attribute); @@ -561,9 +561,9 @@ private GraphQLInputObjectField getWhereInputRelationField(Attribute attrib .name(attribute.getName()) .description(description) .type(new GraphQLTypeReference(type)) - .build(); + .build(); } - + private GraphQLInputObjectField getWhereInputField(Attribute attribute) { GraphQLInputType type = getWhereAttributeType(attribute); String description = getSchemaDescription(attribute); @@ -573,20 +573,20 @@ private GraphQLInputObjectField getWhereInputField(Attribute attribute) { .name(attribute.getName()) .description(description) .type(type) - .build(); + .build(); } throw new IllegalArgumentException("Attribute " + attribute.getName() + " cannot be mapped as an Input Argument"); } private Map whereAttributesMap = new HashMap<>(); - + private GraphQLInputType getWhereAttributeType(Attribute attribute) { String type = namingStrategy.singularize(attribute.getName())+attribute.getDeclaringType().getJavaType().getSimpleName()+"Criteria"; if(whereAttributesMap.containsKey(type)) return whereAttributesMap.get(type); - + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject() .name(type) .description("Criteria expression specification of "+namingStrategy.singularize(attribute.getName())+" attribute in entity " + attribute.getDeclaringType().getJavaType()) @@ -614,7 +614,7 @@ private GraphQLInputType getWhereAttributeType(Attribute attribute) { .type(getAttributeInputType(attribute)) .build() ); - + if(!attribute.getJavaType().isEnum()) { if(!attribute.getJavaType().equals(String.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() @@ -642,7 +642,7 @@ private GraphQLInputType getWhereAttributeType(Attribute attribute) { .build() ); } - + if(attribute.getJavaType().equals(String.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.LIKE.name()) @@ -704,10 +704,10 @@ private GraphQLInputType getWhereAttributeType(Attribute attribute) { .type(getAttributeInputType(attribute)) .build() ); - } - else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) + } + else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) && Field.class.cast(attribute.getJavaMember()) - .isAnnotationPresent(Convert.class)) + .isAnnotationPresent(Convert.class)) { builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.LOCATE.name()) @@ -716,7 +716,7 @@ else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) .build()); } } - + builder.field(GraphQLInputObjectField.newInputObjectField() .name(Criteria.IS_NULL.name()) .description("Is Null criteria") @@ -755,13 +755,13 @@ else if (attribute.getJavaMember().getClass().isAssignableFrom(Field.class) ); GraphQLInputType answer = builder.build(); - + whereAttributesMap.putIfAbsent(type, answer); - + return answer; - + } - + private GraphQLArgument getArgument(Attribute attribute) { GraphQLInputType type = getAttributeInputType(attribute); String description = getSchemaDescription(attribute); @@ -772,7 +772,7 @@ private GraphQLArgument getArgument(Attribute attribute) { .description(description) .build(); } - + private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean input) { if (input && embeddableInputCache.containsKey(embeddableType.getJavaType())) return embeddableInputCache.get(embeddableType.getJavaType()); @@ -807,16 +807,16 @@ private GraphQLType getEmbeddableType(EmbeddableType embeddableType, boolean } else{ embeddableOutputCache.putIfAbsent(embeddableType.getJavaType(), (GraphQLObjectType) graphQLType); } - + return graphQLType; } - + private GraphQLObjectType getObjectType(EntityType entityType) { return entityCache.computeIfAbsent(entityType, this::computeObjectType); } - - + + private GraphQLObjectType computeObjectType(EntityType entityType) { return GraphQLObjectType.newObject() .name(entityType.getName()) @@ -839,16 +839,16 @@ private List getTransientFields(ManagedType managedTy return EntityIntrospector.introspect(managedType) .getTransientPropertyDescriptors() .stream() - .filter(AttributePropertyDescriptor::isNotIgnored) - .map(this::getJavaFieldDefinition) + .filter(AttributePropertyDescriptor::isNotIgnored) + .map(this::getJavaFieldDefinition) .collect(Collectors.toList()); } - + @SuppressWarnings( { "rawtypes" } ) private GraphQLFieldDefinition getJavaFieldDefinition(AttributePropertyDescriptor propertyDescriptor) { GraphQLOutputType type = getGraphQLTypeFromJavaType(propertyDescriptor.getPropertyType()); DataFetcher dataFetcher = PropertyDataFetcher.fetching(propertyDescriptor.getName()); - + String description = propertyDescriptor.getSchemaDescription().orElse(null); return GraphQLFieldDefinition.newFieldDefinition() @@ -884,30 +884,43 @@ && isNotIgnoredOrder(attribute) ) { } // Get the fields that can be queried on (i.e. Simple Types, no Sub-Objects) - if (attribute instanceof SingularAttribute + if (attribute instanceof SingularAttribute && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC) { ManagedType foreignType = getForeignType(attribute); SingularAttribute singularAttribute = SingularAttribute.class.cast(attribute); // TODO fix page count query arguments.add(getWhereArgument(foreignType)); - - // to-one end could be optional + + // to-one end could be optional arguments.add(optionalArgument(singularAttribute.isOptional())); } // Get Sub-Objects fields queries via DataFetcher else if (attribute instanceof PluralAttribute - && (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY + && (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY)) { Assert.assertNotNull(baseEntity, "For attribute "+attribute.getName() + " cannot find declaring type!"); EntityType elementType = (EntityType) ((PluralAttribute) attribute).getElementType(); arguments.add(getWhereArgument(elementType)); - + // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); + + GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() + .name(baseEntity.getName()) + .build(); + + dataFetcher = new GraphQLJpaOneToManyDataFetcher(GraphQLJpaQueryFactory.builder() + .withEntityManager(entityManager) + .withEntityType(baseEntity) + .withEntityObjectType(entityObjectType) + .withSelectNodeName(entityObjectType.getName()) + .withDefaultDistinct(isDefaultDistinct) + .build(), + (PluralAttribute) attribute); } - + return GraphQLFieldDefinition.newFieldDefinition() .name(attribute.getName()) .description(getSchemaDescription(attribute)) @@ -916,7 +929,7 @@ else if (attribute instanceof PluralAttribute .arguments(arguments) .build(); } - + private GraphQLArgument optionalArgument(Boolean defaultValue) { return GraphQLArgument.newArgument() .name("optional") @@ -925,14 +938,14 @@ private GraphQLArgument optionalArgument(Boolean defaultValue) { .defaultValue(defaultValue) .build(); } - + protected ManagedType getForeignType(Attribute attribute) { if(SingularAttribute.class.isInstance(attribute)) return (ManagedType) ((SingularAttribute) attribute).getType(); else return (EntityType) ((PluralAttribute) attribute).getElementType(); } - + @SuppressWarnings( { "rawtypes" } ) private GraphQLInputObjectField getInputObjectField(Attribute attribute) { GraphQLInputType type = getAttributeInputType(attribute); @@ -949,7 +962,7 @@ private Stream> findBasicAttributes(Collection> at } private GraphQLInputType getAttributeInputType(Attribute attribute) { - + try { return (GraphQLInputType) getAttributeType(attribute, true); } catch (ClassCastException e){ @@ -970,27 +983,27 @@ private GraphQLType getAttributeType(Attribute attribute, boolean input) { if (isBasic(attribute)) { return getGraphQLTypeFromJavaType(attribute.getJavaType()); - } + } else if (isEmbeddable(attribute)) { EmbeddableType embeddableType = (EmbeddableType) ((SingularAttribute) attribute).getType(); return getEmbeddableType(embeddableType, input); - } + } else if (isToMany(attribute)) { EntityType foreignType = (EntityType) ((PluralAttribute) attribute).getElementType(); - + return input ? getWhereInputType(foreignType) : new GraphQLList(new GraphQLTypeReference(foreignType.getName())); - } + } else if (isToOne(attribute)) { EntityType foreignType = (EntityType) ((SingularAttribute) attribute).getType(); - + return input ? getWhereInputType(foreignType) : new GraphQLTypeReference(foreignType.getName()); - } + } else if (isElementCollection(attribute)) { Type foreignType = ((PluralAttribute) attribute).getElementType(); - + if(foreignType.getPersistenceType() == Type.PersistenceType.BASIC) { GraphQLType graphQLType = getGraphQLTypeFromJavaType(foreignType.getJavaType()); - + return input ? graphQLType : new GraphQLList(graphQLType); } } @@ -1005,15 +1018,15 @@ else if (isElementCollection(attribute)) { protected final boolean isEmbeddable(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED; } - + protected final boolean isBasic(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC; } - + protected final boolean isElementCollection(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ELEMENT_COLLECTION; } - + protected final boolean isToMany(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY; @@ -1022,12 +1035,12 @@ protected final boolean isToMany(Attribute attribute) { protected final boolean isOneToMany(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY; } - + protected final boolean isToOne(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE; } - + protected final boolean isValidInput(Attribute attribute) { return attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.BASIC || @@ -1040,7 +1053,7 @@ private String getSchemaDescription(Attribute attribute) { .getSchemaDescription(attribute.getName()) .orElse(null); } - + private String getSchemaDescription(EntityType entityType) { return EntityIntrospector.introspect(entityType) .getSchemaDescription() @@ -1052,11 +1065,11 @@ private String getSchemaDescription(EmbeddableType embeddableType) { .getSchemaDescription() .orElse(null); } - + private boolean isNotIgnored(EmbeddableType attribute) { return isNotIgnored(attribute.getJavaType()); } - + private boolean isNotIgnored(Attribute attribute) { return isNotIgnored(attribute.getJavaMember()) && isNotIgnored(attribute.getJavaType()); } @@ -1064,11 +1077,11 @@ private boolean isNotIgnored(Attribute attribute) { private boolean isIdentity(Attribute attribute) { return attribute instanceof SingularAttribute && ((SingularAttribute)attribute).isId(); } - + private boolean isNotIgnored(EntityType entityType) { return isNotIgnored(entityType.getJavaType()) && isNotIgnored(entityType.getJavaType().getName()); } - + private boolean isNotIgnored(String name) { return entityPaths.isEmpty() || entityPaths.stream() .anyMatch(prefix -> name.startsWith(prefix)); @@ -1112,14 +1125,14 @@ protected boolean isNotIgnoredOrder(Attribute attribute) { return false; } - + @SuppressWarnings( "unchecked" ) private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { if (clazz.isEnum()) { - + if (classCache.containsKey(clazz)) return classCache.get(clazz); - + GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(clazz.getSimpleName()); int ordinal = 0; for (Enum enumValue : ((Class>)clazz).getEnumConstants()) @@ -1129,7 +1142,7 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { setNoOpCoercing(enumType); classCache.putIfAbsent(clazz, enumType); - + return enumType; } else if (clazz.isArray()) { return GraphQLList.list(JavaScalars.of(clazz.getComponentType())); @@ -1139,21 +1152,21 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class clazz) { } protected GraphQLInputType getFieldsEnumType(EntityType entityType) { - + GraphQLEnumType.Builder enumBuilder = GraphQLEnumType.newEnum().name(entityType.getName()+"FieldsEnum"); final AtomicInteger ordinal = new AtomicInteger(); - + entityType.getAttributes().stream() .filter(this::isValidInput) .filter(this::isNotIgnored) .forEach(it -> enumBuilder.value(it.getName(), ordinal.incrementAndGet())); - + GraphQLInputType answer = enumBuilder.build(); setNoOpCoercing(answer); return answer; } - + /** * JPA will deserialize Enum's for us...we don't want GraphQL doing it. * @@ -1168,8 +1181,8 @@ private void setNoOpCoercing(GraphQLType type) { log.error("Unable to set coercing for " + type, e); } } - - private static final GraphQLArgument paginationArgument = + + private static final GraphQLArgument paginationArgument = newArgument().name(PAGE_PARAM_NAME) .description("Page object for pageble requests, specifying the requested start page and limit size.") .type(newInputObject().name("Page") @@ -1194,7 +1207,7 @@ private void setNoOpCoercing(GraphQLType type) { .value("DESC", "DESC", "Descending") .build(); - + /** * @return the name */ @@ -1208,7 +1221,7 @@ public String getName() { @Override public GraphQLJpaSchemaBuilder name(String name) { this.name = name; - + return this; } @@ -1255,7 +1268,7 @@ public GraphQLJpaSchemaBuilder defaultDistinct(boolean isDefaultDistinct) { return this; } - + /** * @param namingStrategy the namingStrategy to set */ @@ -1263,7 +1276,7 @@ public void setNamingStrategy(NamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } - + static class NoOpCoercing implements Coercing { @Override @@ -1285,21 +1298,21 @@ public Object parseLiteral(Object input) { @Override public GraphQLJpaSchemaBuilder entityPath(String path) { Assert.assertNotNull(path, "path is null"); - + entityPaths.add(path); - + return this; } @Override public GraphQLJpaSchemaBuilder namingStrategy(NamingStrategy instance) { Assert.assertNotNull(instance, "instance is null"); - + this.namingStrategy = instance; - + return this; } - + public boolean isToManyDefaultOptional() { return toManyDefaultOptional; } @@ -1311,10 +1324,10 @@ public void setToManyDefaultOptional(boolean toManyDefaultOptional) { public GraphQLJpaSchemaBuilder toManyDefaultOptional(boolean toManyDefaultOptional) { this.toManyDefaultOptional = toManyDefaultOptional; - + return this; } - + public boolean isEnableSubscription() { return enableSubscription; } @@ -1328,23 +1341,23 @@ public GraphQLJpaSchemaBuilder enableSubscription(boolean enableSubscription) { public boolean isEnableDeferDirective() { return enableDeferDirective; } - + public GraphQLJpaSchemaBuilder enableDeferDirective(boolean enableDeferDirective) { this.enableDeferDirective = enableDeferDirective; return this; } - + public boolean isEnableRelay() { return enableRelay; } - + public GraphQLJpaSchemaBuilder enableRelay(boolean enableRelay) { this.enableRelay = enableRelay; return this; } - + public int getDefaultMaxResults() { return defaultMaxResults; } @@ -1358,7 +1371,7 @@ public GraphQLJpaSchemaBuilder defaultMaxResults(int defaultMaxResults) { public int getDefaultPageLimitSize() { return defaultPageLimitSize; } - + public GraphQLJpaSchemaBuilder defaultPageLimitSize(int defaultPageLimitSize) { this.defaultPageLimitSize = defaultPageLimitSize; @@ -1368,11 +1381,11 @@ public GraphQLJpaSchemaBuilder defaultPageLimitSize(int defaultPageLimitSize) { public int getDefaultFetchSize() { return defaultFetchSize; } - + public GraphQLJpaSchemaBuilder defaultFetchSize(int defaultFetchSize) { this.defaultFetchSize = defaultFetchSize; return this; } - + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index 4dbac7261..fc7bce7c8 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -27,6 +27,7 @@ import javax.persistence.Query; import javax.transaction.Transactional; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -83,7 +84,7 @@ public void JPASampleTester() { assertThat(result).isNotEmpty(); assertThat(result).hasSize(13); } - + @Test @Transactional public void queryManyToManyTester() { @@ -105,11 +106,11 @@ public void queryManyToManyTester() { assertThat(result.get(0).getName()).isEqualTo("C-3PO"); assertThat(result.get(0).getFriends()).hasSize(3); assertThat(result.get(0).getFriends()).extracting(Character::getName) - .containsOnly("Han Solo", "Leia Organa", "R2-D2"); + .containsOnly("Han Solo", "Leia Organa", "R2-D2"); assertThat(result.get(0).getFriends()).flatExtracting(Character::getFriendsOf) .extracting(Character::getName) .containsOnly("Luke Skywalker"); - + assertThat(result.get(1).getName()).isEqualTo("R2-D2"); assertThat(result.get(1).getFriends()).hasSize(2); assertThat(result.get(1).getFriends()).extracting(Character::getName) @@ -118,31 +119,32 @@ public void queryManyToManyTester() { .extracting(Character::getName) .containsOnly("Luke Skywalker"); } - + @Test @Transactional + @Ignore public void queryOneToManyTesterLeftOuterJoinTester() { // given: - Query query = em.createQuery("select distinct author from Author as author \n" + - "left join fetch author.books as books on books.title like '%War%' \n" + - "where ( author.id in (1L, 4L, 8L) ) \n" + + Query query = em.createQuery("select distinct author from Author as author \n" + + "left join fetch author.books as books on books.title like '%War%' \n" + + "where ( author.id in (1L, 4L, 8L) ) \n" + "order by author.id asc"); - - String qu = "select d1 from Department d1 left join fetch d1.manager where exists \n" + + + String qu = "select d1 from Department d1 left join fetch d1.manager where exists \n" + "(select d2 from Department d2 where d2.manager is null and d1 = d2)"; - + query.setHint("hibernate.query.passDistinctThrough", false); - - + + // when: List result = query.getResultList(); - + assertThat(result).isNotEmpty(); } - - - + + + @Test public void getsNamesOfAllDroids() { @@ -294,7 +296,7 @@ public void queryDeepNesting() { + "{name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + "]}}"; - + //when: Object result = executor.execute(query).getData(); @@ -314,7 +316,7 @@ public void queryDeepNestingPlural() { + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + "]" + "}]}}"; - + //when: Object result = executor.execute(query).getData(); @@ -365,7 +367,7 @@ public void queryWhereRootPagedWithVariables() { put("limit", 2); }}; - + String expected = "{Humans={pages=3, total=5, select=[{name=Luke Skywalker}, {name=Darth Vader}]}}"; //when: @@ -374,7 +376,7 @@ public void queryWhereRootPagedWithVariables() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryPaginationWithoutRecords() { //given: @@ -592,14 +594,14 @@ public void queryWithTypenameDeepNesting() { + "{name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=Leia Organa, __typename=Character}, {name=Luke Skywalker, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}, " + "{name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO, __typename=Character}, {name=Han Solo, __typename=Character}, {name=Luke Skywalker, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}, " + "{name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{name=C-3PO, __typename=Character}, {name=Han Solo, __typename=Character}, {name=Leia Organa, __typename=Character}, {name=R2-D2, __typename=Character}], __typename=Character}], __typename=Droid}}"; - + //when: Object result = executor.execute(query).getData(); //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithTypenameSimple() { //given: @@ -615,7 +617,7 @@ public void queryWithTypenameSimple() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithStringBetweenPredicate() { //given: @@ -649,24 +651,24 @@ public void queryWithStringNotBetweenPredicate() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideManyToOneRelations() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: {appearsIn: {IN: [A_NEW_HOPE]}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: {appearsIn: {IN: [A_NEW_HOPE]}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -679,24 +681,24 @@ public void queryWithWhereInsideManyToOneRelations() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideManyToOneRelationsNotExisting() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: {appearsIn: {IN: [PHANTOM_MENACE]}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: {appearsIn: {IN: [PHANTOM_MENACE]}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[]}}"; @@ -706,30 +708,30 @@ public void queryWithWhereInsideManyToOneRelationsNotExisting() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsNotExisting() { //given: - String query = "query {" + - " Humans(where: {" + - " friends: {appearsIn: {EQ: PHANTOM_MENACE}}" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " primaryFunction { function }" + - " appearsIn" + - " }" + - " friends {" + - " id" + - " name" + - " appearsIn" + - " }" + - " }" + - " }" + + String query = "query {" + + " Humans(where: {" + + " friends: {appearsIn: {EQ: PHANTOM_MENACE}}" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " primaryFunction { function }" + + " appearsIn" + + " }" + + " friends {" + + " id" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[]}}"; @@ -739,40 +741,35 @@ public void queryWithWhereInsideOneToManyRelationsNotExisting() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering() { //given: - String query = "query {" + - " Characters(where: {" + - " friends: {appearsIn: {IN: A_NEW_HOPE}}" + - " }) {" + - " select {" + - " id" + - " name" + - " appearsIn" + - " friends(where: {name: {LIKE: \"Leia\"}}) {" + - " id" + - " name" + - " }" + - " }" + - " }" + + String query = "query {" + + " Characters(where: {" + + " friends: {appearsIn: {IN: A_NEW_HOPE}}" + + " }) {" + + " select {" + + " id" + + " name" + + " appearsIn" + + " friends(where: {name: {LIKE: \"Leia\"}}) {" + + " id" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Characters={select=[" - + "{id=1000, name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=1002, name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=2000, name=C-3PO, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}, " - + "{id=2001, name=R2-D2, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[" - + "{id=1003, name=Leia Organa}" - + "]}" + + "{id=1000, name=Luke Skywalker, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=1001, name=Darth Vader, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI], friends=[]}, " + + "{id=1002, name=Han Solo, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=1003, name=Leia Organa, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[]}, " + + "{id=1004, name=Wilhuff Tarkin, appearsIn=[A_NEW_HOPE], friends=[]}, " + + "{id=2000, name=C-3PO, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}, " + + "{id=2001, name=R2-D2, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS], friends=[{id=1003, name=Leia Organa}]}" + "]}}"; //when: @@ -780,32 +777,32 @@ public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering2() { //given: - String query = "query {" + - " Humans(where: {" + - " favoriteDroid: { id: {EQ: \"2000\"}}" + - " friends: {" + - " appearsIn: {IN: A_NEW_HOPE}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " id" + - " name" + - " primaryFunction { function }" + - " }" + - " friends(where: {name: {LIKE: \"Leia\"}}) {" + - " id" + - " name" + - " }" + - " }" + - " } " + + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: { id: {EQ: \"2000\"}}" + + " friends: {" + + " appearsIn: {IN: A_NEW_HOPE}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " id" + + " name" + + " primaryFunction { function }" + + " }" + + " friends(where: {name: {LIKE: \"Leia\"}}) {" + + " id" + + " name" + + " }" + + " }" + + " } " + "}"; String expected = "{Humans={select=[" @@ -817,25 +814,25 @@ public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering2() { //then: assertThat(result.toString()).isEqualTo(expected); - } - - + } + + @Test public void queryWithWhereInsideOneToManyRelations() { //given: String query = "query { " - + " Humans(where: {friends: {appearsIn: {IN: A_NEW_HOPE}} }) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " }" + - " friends {" + - " name" + - " appearsIn" + - " }" + - " }" + + + " Humans(where: {friends: {appearsIn: {IN: A_NEW_HOPE}} }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " }" + + " friends {" + + " name" + + " appearsIn" + + " }" + + " }" + " }" + "}"; @@ -880,21 +877,21 @@ public void queryWithWhereInsideOneToManyRelations() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWithFavoriteDroidDefaultOptionalTrue() { //given: String query = "query { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -911,21 +908,21 @@ public void queryHumansWithFavoriteDroidDefaultOptionalTrue() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWittFavorideDroidExplicitOptionalFalse() { //given: String query = "query { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid(optional: false) {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid(optional: false) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -939,23 +936,23 @@ public void queryHumansWittFavorideDroidExplicitOptionalFalse() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryHumansWittFavorideDroidExplicitOptionalFalseParameterBinding() { //given: String query = "query($optional: Boolean) { " - + "Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid(optional: $optional) {" + - " name" + - " }" + - " }" + - " }" + + + "Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid(optional: $optional) {" + + " name" + + " }" + + " }" + + " }" + "}"; - + Map variables = Collections.singletonMap("optional", false); String expected = "{Humans={select=[" @@ -968,8 +965,8 @@ public void queryHumansWittFavorideDroidExplicitOptionalFalseParameterBinding() //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryFilterManyToOneEmbdeddedCriteria() { //given: @@ -979,7 +976,7 @@ public void queryFilterManyToOneEmbdeddedCriteria() { "select=[{" + "name=R2-D2, " + "primaryFunction={function=Astromech}" + - "}]" + + "}]" + "}}"; //when: @@ -993,13 +990,13 @@ public void queryFilterManyToOneEmbdeddedCriteria() { public void queryFilterManyToOnRelationCriteria() { //given: String query = "query { " + - " Droids(where: {primaryFunction: { function: {EQ:\"Astromech\"}}}) { " + - " select {" + - " name " + - " primaryFunction {" + - " function" + - " }" + - " }" + + " Droids(where: {primaryFunction: { function: {EQ:\"Astromech\"}}}) { " + + " select {" + + " name " + + " primaryFunction {" + + " function" + + " }" + + " }" + " }" + "}"; @@ -1007,7 +1004,7 @@ public void queryFilterManyToOnRelationCriteria() { "select=[{" + "name=R2-D2, " + "primaryFunction={function=Astromech}" + - "}]" + + "}]" + "}}"; //when: @@ -1016,7 +1013,7 @@ public void queryFilterManyToOnRelationCriteria() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryFilterNestedManyToOneToDo() { //given: @@ -1054,11 +1051,11 @@ public void queryFilterNestedManyToOneToDo() { //when: Object result = executor.execute(query).getData(); - + //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryFilterNestedManyToOneRelationCriteria() { //given: @@ -1100,29 +1097,29 @@ public void queryFilterNestedManyToOneRelationCriteria() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryFilterNestedManyToManyRelationCriteria() { //given: String query = "query {" + - " Humans(where: {" + - " friends: { name: { LIKE: \"Leia\" } } " + - " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " primaryFunction {" + - " function" + - " }" + - " }" + - " friends {" + - " name" + - " }" + - " }" + + " Humans(where: {" + + " friends: { name: { LIKE: \"Leia\" } } " + + " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " friends {" + + " name" + + " }" + + " }" + " } " + "}"; @@ -1154,24 +1151,24 @@ public void queryFilterNestedManyToManyRelationCriteriaWithEXISTS() { String query = "query {" + " Humans(where: {" + " EXISTS: {" + - " friends: { name: { LIKE: \"Leia\" } } " + + " friends: { name: { LIKE: \"Leia\" } } " + " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + " }"+ - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " primaryFunction {" + - " function" + - " }" + - " }" + - " friends {" + - " name" + - " }" + - " }" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " friends {" + + " name" + + " }" + + " }" + " } " + "}"; @@ -1200,26 +1197,26 @@ public void queryFilterNestedManyToManyRelationCriteriaWithEXISTS() { assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithWhereInsideOneToManyRelationsShouldApplyFilterCriterias() { //given: String query = "query { " + " Humans(where: {" + "friends: {appearsIn: {IN: A_NEW_HOPE}} " - + "favoriteDroid: {name: {EQ: \"C-3PO\"}} " - + "}) {" + - " select {" + - " id" + - " name" + - " favoriteDroid {" + - " name" + - " }" + - " friends {" + - " name" + - " appearsIn" + - " }" + - " }" + + + "favoriteDroid: {name: {EQ: \"C-3PO\"}} " + + "}) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " }" + + " friends {" + + " name" + + " appearsIn" + + " }" + + " }" + " }" + "}"; @@ -1240,20 +1237,20 @@ public void queryWithWhereInsideOneToManyRelationsShouldApplyFilterCriterias() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithOneToManyRelationsShouldUseLeftOuterJoin() { //given: String query = "query { " + - " Humans {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + + " Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + " }" + "}"; @@ -1271,21 +1268,21 @@ public void queryWithOneToManyRelationsShouldUseLeftOuterJoin() { //then: assertThat(result.toString()).isEqualTo(expected); } - - + + @Test public void queryWithWhereOneToManyRelationsShouldUseLeftOuterJoinAndApplyCriteria() { //given: String query = "query { " + - " Humans(where: {favoriteDroid: {name: {EQ: \"C-3PO\"}}}) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " }" + + " Humans(where: {favoriteDroid: {name: {EQ: \"C-3PO\"}}}) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " }" + " }" + "}"; @@ -1304,14 +1301,14 @@ public void queryWithWhereOneToManyRelationsShouldUseLeftOuterJoinAndApplyCriter public void queryWithNestedWhereSearchCriteriaShouldFetchElementCollectionsAttributes() { //given: String query = "query { " + - " Characters (where: {" + - " appearsIn :{IN: THE_FORCE_AWAKENS}" + - " }) {" + - " select {" + - " id, " + - " name," + - " appearsIn" + - " }" + + " Characters (where: {" + + " appearsIn :{IN: THE_FORCE_AWAKENS}" + + " }) {" + + " select {" + + " id, " + + " name," + + " appearsIn" + + " }" + " }" + "}"; @@ -1329,25 +1326,25 @@ public void queryWithNestedWhereSearchCriteriaShouldFetchElementCollectionsAttri //then: assertThat(result.toString()).isEqualTo(expected); } - - + + @Test public void queryWithNestedWhereCompoundSearchCriteriaShouldFetchElementCollectionsAttributes() { //given: String query = "query { " + - " Humans(where:{" + - " favoriteDroid: {name: {EQ: \"C-3PO\"}} " + - " appearsIn: {IN: [THE_FORCE_AWAKENS]}" + - " }) {" + - " select {" + - " id" + - " name" + - " homePlanet" + - " favoriteDroid {" + - " name" + - " }" + - " appearsIn" + - " }" + + " Humans(where:{" + + " favoriteDroid: {name: {EQ: \"C-3PO\"}} " + + " appearsIn: {IN: [THE_FORCE_AWAKENS]}" + + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " }" + + " appearsIn" + + " }" + " }" + "}"; @@ -1365,19 +1362,19 @@ public void queryWithNestedWhereCompoundSearchCriteriaShouldFetchElementCollecti //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryShouldReturnDistictResultsByDefault() { //given: String query = "query { " + - " Humans (where: { " + - " appearsIn: {IN: [A_NEW_HOPE, EMPIRE_STRIKES_BACK]}" + - " }) {" + - " select {" + - " id" + - " name" + - " }" + + " Humans (where: { " + + " appearsIn: {IN: [A_NEW_HOPE, EMPIRE_STRIKES_BACK]}" + + " }) {" + + " select {" + + " id" + + " name" + + " }" + " }" + "}"; @@ -1394,19 +1391,19 @@ public void queryShouldReturnDistictResultsByDefault() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseEQ() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1422,20 +1419,20 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQ() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1451,20 +1448,20 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseIN() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {IN: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {IN: THE_FORCE_AWAKENS}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1481,19 +1478,19 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseIN() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseINArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {IN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {IN: [THE_FORCE_AWAKENS]}}) {" + + " name(orderBy: ASC)" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1509,20 +1506,20 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseINArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNE() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1538,20 +1535,20 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNE() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1561,27 +1558,27 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { + "{name=Leia Organa, friends=[]}, " + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + "]}}"; - + //when: Object result = executor.execute(query).getData(); //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNIN() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: THE_FORCE_AWAKENS}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: THE_FORCE_AWAKENS}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1597,21 +1594,21 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNIN() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1627,26 +1624,26 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryEmbeddedWhereWithPluralAssociations() { - + //given: String query = "{ " - + "Droids {" + - " select {" + - " name" + - " friends(where:{" + - " NOT_EXISTS:{ friendsOf: {name:{EQ:\"R2-D2\"}}}" + - " }) {" + - " name" + - " friendsOf {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + "Droids {" + + " select {" + + " name" + + " friends(where:{" + + " NOT_EXISTS:{ friendsOf: {name:{EQ:\"R2-D2\"}}}" + + " }) {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" @@ -1660,27 +1657,27 @@ public void queryEmbeddedWhereWithPluralAssociations() { + "]}, " + "{name=R2-D2, friends=[]}" + "]}}"; - + //when: Object result = executor.execute(query).getData(); //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithPluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Humans {" + - " select {" + - " name" + - " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + - " name" + - " }" + - " }" + - " }" + + + //given: + String query = "{" + + " Humans {" + + " select {" + + " name" + + " friends(where: {appearsIn: {NIN: [THE_FORCE_AWAKENS]}}) {" + + " name" + + " }" + + " }" + + " }" + "}"; String expected = "{Humans={select=[" @@ -1696,26 +1693,26 @@ public void queryEmbeddedWhereWithPluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithNestedPluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Droids {" + - " select {" + - " name" + - " friends(where:{" + - " NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}" + - " }) {" + - " name" + - " friends {" + - " name" + - " }" + - " }" + - " } " + - " }" + + + //given: + String query = "{" + + " Droids {" + + " select {" + + " name" + + " friends(where:{" + + " NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}" + + " }) {" + + " name" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + " }" + "}"; String expected = "{Droids={select=[" @@ -1740,26 +1737,26 @@ public void queryEmbeddedWhereWithNestedPluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithRecursivePluralAssociationsNOT_EXISTS() { - - //given: - String query = "{" + - " Droids(where: {" + - " friends: {NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}}" + - " }) {" + - " select {" + - " name" + - " friends {" + - " name" + - " friends {" + - " name" + - " }" + - " }" + - " } " + - " }" + + + //given: + String query = "{" + + " Droids(where: {" + + " friends: {NOT_EXISTS: {friends:{name:{EQ:\"Leia Organa\"}}}}" + + " }) {" + + " select {" + + " name" + + " friends {" + + " name" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + " }" + "}"; String expected = "{Droids={select=[" @@ -1784,26 +1781,26 @@ public void queryEmbeddedWhereWithRecursivePluralAssociationsNOT_EXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithManyToManyAssociations() { - - //given: - String query = "{" + - " Droids(where: {" + - " friends: {friendsOf:{name:{EQ:\"Leia Organa\"}}}" + - " }) {" + - " select {" + - " name" + - " friends {" + - " name" + - " friendsOf {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + //given: + String query = "{" + + " Droids(where: {" + + " friends: {friendsOf:{name:{EQ:\"Leia Organa\"}}}" + + " }) {" + + " select {" + + " name" + + " friends {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" @@ -1823,34 +1820,35 @@ public void queryEmbeddedWhereWithManyToManyAssociations() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryEmbeddedWhereWithManyToManyAssociationsUsingEXISTS() { - - //given: - String query = "{" + - " Droids {" + - " select {" + - " name" + - " friends(where: {EXISTS: {friendsOf:{name:{EQ:\"Leia Organa\"}}}}) {" + - " name" + - " friendsOf {" + - " name" + - " }" + - " }" + - " } " + - " } " + + + //given: + String query = "{" + + " Droids {" + + " select {" + + " name" + + " friends(where: {EXISTS: {friendsOf:{name:{EQ:\"Leia Organa\"}}}}) {" + + " name" + + " friendsOf {" + + " name" + + " }" + + " }" + + " } " + + " } " + "}"; String expected = "{Droids={select=[" + "{name=C-3PO, friends=[" - + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " + "{name=R2-D2, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=Luke Skywalker}]}" + "]}, " - + "{name=R2-D2, friends=[{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}, " - + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}" + + "{name=R2-D2, friends=[" + + "{name=Luke Skywalker, friendsOf=[{name=C-3PO}, {name=Han Solo}, {name=Leia Organa}, {name=R2-D2}]}, " + + "{name=Han Solo, friendsOf=[{name=C-3PO}, {name=Leia Organa}, {name=Luke Skywalker}, {name=R2-D2}]}" + "]}" + "]}}"; @@ -1859,5 +1857,5 @@ public void queryEmbeddedWhereWithManyToManyAssociationsUsingEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } } From cbce61c271b049d18d9451253b2336ce7df26425 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Thu, 2 Apr 2020 22:15:43 -0700 Subject: [PATCH 04/16] fix: optimize OneToManyDataFetcher using query stream result --- .../impl/GraphQLJpaOneToMayDataFetcher.java | 21 ++++++++++++++++--- .../schema/impl/GraphQLJpaQueryFactory.java | 9 ++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java index 80781a0de..d422d0170 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java @@ -17,10 +17,14 @@ package com.introproventures.graphql.jpa.query.schema.impl; import java.util.Optional; +import java.util.stream.Stream; import javax.persistence.TypedQuery; import javax.persistence.metamodel.PluralAttribute; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.support.GraphQLSupport; import graphql.language.Argument; import graphql.language.Field; @@ -35,6 +39,8 @@ */ class GraphQLJpaOneToManyDataFetcher implements DataFetcher { + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaOneToManyDataFetcher.class); + private final PluralAttribute attribute; private final GraphQLJpaQueryFactory queryFactory; @@ -50,16 +56,25 @@ public Object get(DataFetchingEnvironment environment) { Object source = environment.getSource(); Optional whereArg = GraphQLSupport.getWhereArgument(field); + boolean isDistinct = true; + int fetchSize = 100; // Resolve collection query if where argument is present or any field in selection has orderBy argument if (whereArg.isPresent()) { - TypedQuery query = queryFactory.getCollectionQuery(environment, field, true); + TypedQuery query = queryFactory.getCollectionQuery(environment, + field, isDistinct); + + Stream resultStream = queryFactory.getResultStream(query, + fetchSize, + isDistinct); - return query.getResultList(); + return ResultStreamWrapper.wrap(resultStream, + fetchSize); } // Let hibernate resolve collection query - return queryFactory.getAttributeValue(source, attribute); + return queryFactory.getAttributeValue(source, + attribute); } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 2db56561b..3eee5b25f 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -201,6 +201,14 @@ protected Stream queryResultStream(DataFetchingEnvironment environment, final TypedQuery query = getQuery(queryEnvironment, queryEnvironment.getField(), isDistinct, keys.toArray()); + // Let's execute query and get wrap result into stream + return getResultStream(query, fetchSize, isDistinct); + } + + protected Stream getResultStream(TypedQuery query, + int fetchSize, + boolean isDistinct) { + // Let' try reduce overhead and disable all caching query.setHint(ORG_HIBERNATE_READ_ONLY, true); query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize); @@ -220,6 +228,7 @@ protected Stream queryResultStream(DataFetchingEnvironment environment, .peek(entityManager::detach); } + protected Object querySingleResult(final DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); From 2fbbcb4082ea488de5da1442257aa8a463b374aa Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Thu, 2 Apr 2020 22:21:24 -0700 Subject: [PATCH 05/16] fix: update name to GraphQLJpaOneToManyDataFetcher --- ...taFetcher.java => GraphQLJpaOneToManyDataFetcher.java} | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) rename graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/{GraphQLJpaOneToMayDataFetcher.java => GraphQLJpaOneToManyDataFetcher.java} (92%) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java similarity index 92% rename from graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java rename to graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java index d422d0170..32b1129a5 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToMayDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java @@ -22,9 +22,6 @@ import javax.persistence.TypedQuery; import javax.persistence.metamodel.PluralAttribute; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.introproventures.graphql.jpa.query.support.GraphQLSupport; import graphql.language.Argument; import graphql.language.Field; @@ -39,8 +36,6 @@ */ class GraphQLJpaOneToManyDataFetcher implements DataFetcher { - private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaOneToManyDataFetcher.class); - private final PluralAttribute attribute; private final GraphQLJpaQueryFactory queryFactory; @@ -59,7 +54,8 @@ public Object get(DataFetchingEnvironment environment) { boolean isDistinct = true; int fetchSize = 100; - // Resolve collection query if where argument is present or any field in selection has orderBy argument + // Resolve collection query if where argument is present + // TODO or any field in selection has orderBy argument if (whereArg.isPresent()) { TypedQuery query = queryFactory.getCollectionQuery(environment, field, isDistinct); From 12d2982d04ffce5cfcbce78f545bf581766fd150 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 5 Apr 2020 18:49:45 -0700 Subject: [PATCH 06/16] feat: add OneToMany batch DataLoader support --- .../schema/impl/BatchLoaderRegistry.java | 38 ++++++ .../impl/GraphQLJpaExecutorContext.java | 76 ++++++++++-- .../GraphQLJpaExecutorContextFactory.java | 48 ++++++-- .../impl/GraphQLJpaOneToManyDataFetcher.java | 20 ++-- .../GraphQLJpaOneToManyMappedBatchLoader.java | 65 ++++++++++ .../schema/impl/GraphQLJpaQueryFactory.java | 63 ++++++++-- .../schema/impl/GraphQLJpaSchemaBuilder.java | 113 ++++++++++-------- .../jpa/query/support/GraphQLSupport.java | 64 +++++++--- .../schema/StarwarsQueryExecutorTests.java | 28 +---- 9 files changed, 385 insertions(+), 130 deletions(-) create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java new file mode 100644 index 000000000..4fe60d463 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java @@ -0,0 +1,38 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; + +public class BatchLoaderRegistry { + private final static Map>> mappedBatchLoaders = new LinkedHashMap<>(); + private static BatchLoaderRegistry instance = new BatchLoaderRegistry(); + + public static BatchLoaderRegistry getInstance() { + return instance; + } + + public static void register(String batchLoaderKey, MappedBatchLoaderWithContext> mappedBatchLoader) { + mappedBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); + } + + public static DataLoaderRegistry newDataLoaderRegistry(DataLoaderOptions dataLoaderOptions) { + DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); + + mappedBatchLoaders.entrySet() + .forEach(entry -> { + DataLoader> dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), + dataLoaderOptions); + dataLoaderRegistry.register(entry.getKey(), dataLoader); + }); + + return dataLoaderRegistry; + + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java index d1232f789..1afed04c4 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java @@ -16,26 +16,38 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import java.util.Arrays; +import java.util.List; import java.util.function.Supplier; +import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.schema.GraphQLExecutionInputFactory; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContext; - import graphql.ExecutionInput; import graphql.GraphQL; import graphql.GraphQLContext; +import graphql.execution.instrumentation.ChainedInstrumentation; import graphql.execution.instrumentation.Instrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLSchema; import graphql.schema.visibility.GraphqlFieldVisibility; public class GraphQLJpaExecutorContext implements GraphQLExecutorContext { - + + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaExecutorContext.class); + private final GraphQLSchema graphQLSchema; private final GraphQLExecutionInputFactory executionInputFactory; private final Supplier graphqlFieldVisibility; private final Supplier instrumentation; private final Supplier graphqlContext; + private final Supplier dataLoaderDispatcherInstrumentationOptions; + private final Supplier dataLoaderRegistry; private GraphQLJpaExecutorContext(Builder builder) { this.graphQLSchema = builder.graphQLSchema; @@ -43,28 +55,55 @@ private GraphQLJpaExecutorContext(Builder builder) { this.graphqlFieldVisibility = builder.graphqlFieldVisibility; this.instrumentation = builder.instrumentation; this.graphqlContext = builder.graphqlContext; + this.dataLoaderDispatcherInstrumentationOptions = builder.dataLoaderDispatcherInstrumentationOptions; + this.dataLoaderRegistry = builder.dataLoaderRegistry; } - + @Override public ExecutionInput.Builder newExecutionInput() { + DataLoaderRegistry dataLoaderRegistry = newDataLoaderRegistry(); + return executionInputFactory.create() + .dataLoaderRegistry(dataLoaderRegistry) .context(graphqlContext.get()); } @Override public GraphQL.Builder newGraphQL() { + Instrumentation instrumentation = newIstrumentation(); + return GraphQL.newGraphQL(getGraphQLSchema()) - .instrumentation(instrumentation.get()); + .instrumentation(instrumentation); + } + + public DataLoaderRegistry newDataLoaderRegistry() { + return dataLoaderRegistry.get(); + } + + public Instrumentation newIstrumentation() { + DataLoaderDispatcherInstrumentationOptions options = dataLoaderDispatcherInstrumentationOptions.get(); + + if (logger.isDebugEnabled()) { + options.includeStatistics(true); + } + + DataLoaderDispatcherInstrumentation dispatcherInstrumentation = new DataLoaderDispatcherInstrumentation(options); + + List list = Arrays.asList(dispatcherInstrumentation, + instrumentation.get()); + + return new ChainedInstrumentation(list); + } - + @Override public GraphQLSchema getGraphQLSchema() { GraphQLCodeRegistry codeRegistry = graphQLSchema.getCodeRegistry() .transform(builder -> builder.fieldVisibility(graphqlFieldVisibility.get())); - + return graphQLSchema.transform(builder -> builder.codeRegistry(codeRegistry)); } - + /** * Creates builder to build {@link GraphQLJpaExecutorContext}. * @return created builder @@ -85,9 +124,13 @@ public interface IBuildStage { public IBuildStage graphqlFieldVisibility(Supplier graphqlFieldVisibility); public IBuildStage instrumentation(Supplier instrumentation); - + public IBuildStage graphqlContext(Supplier graphqlContext); + public IBuildStage dataLoaderDispatcherInstrumentationOptions(Supplier dataLoaderDispatcherInstrumentationOptions); + + public IBuildStage dataLoaderRegistry(Supplier dataLoaderRegistry); + public GraphQLJpaExecutorContext build(); } @@ -101,6 +144,8 @@ public static final class Builder implements IGraphQLSchemaStage, IBuildStage { private Supplier graphqlFieldVisibility; private Supplier instrumentation; private Supplier graphqlContext; + private Supplier dataLoaderDispatcherInstrumentationOptions; + private Supplier dataLoaderRegistry; private Builder() { } @@ -135,10 +180,23 @@ public IBuildStage graphqlContext(Supplier graphqlContext) { return this; } + @Override + public IBuildStage dataLoaderDispatcherInstrumentationOptions(Supplier dataLoaderDispatcherInstrumentationOptions) { + this.dataLoaderDispatcherInstrumentationOptions = dataLoaderDispatcherInstrumentationOptions; + + return this; + } + + @Override + public IBuildStage dataLoaderRegistry(Supplier dataLoaderRegistry) { + this.dataLoaderRegistry = dataLoaderRegistry; + + return this; + } + @Override public GraphQLJpaExecutorContext build() { return new GraphQLJpaExecutorContext(this); } - } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java index fa6aead17..dfc609f85 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContextFactory.java @@ -18,27 +18,46 @@ import java.util.function.Supplier; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.introproventures.graphql.jpa.query.schema.GraphQLExecutionInputFactory; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContext; import com.introproventures.graphql.jpa.query.schema.GraphQLExecutorContextFactory; - import graphql.GraphQLContext; import graphql.execution.instrumentation.Instrumentation; import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.dataloader.DataLoaderDispatcherInstrumentationOptions; import graphql.schema.GraphQLSchema; import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.GraphqlFieldVisibility; public class GraphQLJpaExecutorContextFactory implements GraphQLExecutorContextFactory { - + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaExecutorContext.class); + private GraphQLExecutionInputFactory executionInputFactory = new GraphQLExecutionInputFactory() {}; private Supplier graphqlFieldVisibility = () -> DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; private Supplier instrumentation = () -> new SimpleInstrumentation(); private Supplier graphqlContext = () -> GraphQLContext.newContext().build(); - + private Supplier dataLoaderDispatcherInstrumentationOptions = () -> { + return DataLoaderDispatcherInstrumentationOptions.newOptions(); + }; + + private Supplier dataLoaderOptions = () -> DataLoaderOptions.newOptions(); + + private Supplier dataLoaderRegistry = () -> { + DataLoaderOptions options = dataLoaderOptions.get() + .setCachingEnabled(false); + + return BatchLoaderRegistry.newDataLoaderRegistry(options); + }; + + public GraphQLJpaExecutorContextFactory() { } - + @Override public GraphQLExecutorContext newExecutorContext(GraphQLSchema graphQLSchema) { return GraphQLJpaExecutorContext.builder() @@ -47,6 +66,8 @@ public GraphQLExecutorContext newExecutorContext(GraphQLSchema graphQLSchema) { .graphqlFieldVisibility(graphqlFieldVisibility) .instrumentation(instrumentation) .graphqlContext(graphqlContext) + .dataLoaderDispatcherInstrumentationOptions(dataLoaderDispatcherInstrumentationOptions) + .dataLoaderRegistry(dataLoaderRegistry) .build(); } @@ -55,13 +76,13 @@ public GraphQLJpaExecutorContextFactory withGraphqlFieldVisibility(Supplier instrumentation) { this.instrumentation = instrumentation; return this; } - + public GraphQLJpaExecutorContextFactory withExecutionInputFactory(GraphQLExecutionInputFactory executionInputFactory) { this.executionInputFactory = executionInputFactory; return this; @@ -71,11 +92,16 @@ public GraphQLJpaExecutorContextFactory withGraphqlContext(Supplier dataLoaderDispatcherInstrumentationOptions) { + this.dataLoaderDispatcherInstrumentationOptions = dataLoaderDispatcherInstrumentationOptions; + return this; + } + public GraphQLExecutionInputFactory getExecutionInputFactory() { return executionInputFactory; } - + public Supplier getGraphqlFieldVisibility() { return graphqlFieldVisibility; } @@ -84,9 +110,13 @@ public Supplier getInstrumentation() { return instrumentation; } - + public Supplier getGraphqlContext() { return graphqlContext; } + public Supplier getDataLoaderDispatcherInstrumentationOptions() { + return dataLoaderDispatcherInstrumentationOptions; + } + } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java index 32b1129a5..db8fbcb53 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java @@ -16,17 +16,19 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import java.util.List; import java.util.Optional; -import java.util.stream.Stream; -import javax.persistence.TypedQuery; import javax.persistence.metamodel.PluralAttribute; +import org.dataloader.DataLoader; + import com.introproventures.graphql.jpa.query.support.GraphQLSupport; import graphql.language.Argument; import graphql.language.Field; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLType; /** * One-To-Many DataFetcher that uses where argument to filter collection attributes @@ -48,24 +50,20 @@ public GraphQLJpaOneToManyDataFetcher(GraphQLJpaQueryFactory queryFactory, @Override public Object get(DataFetchingEnvironment environment) { Field field = environment.getField(); + GraphQLType parentType = environment.getParentType(); Object source = environment.getSource(); Optional whereArg = GraphQLSupport.getWhereArgument(field); - boolean isDistinct = true; - int fetchSize = 100; // Resolve collection query if where argument is present // TODO or any field in selection has orderBy argument if (whereArg.isPresent()) { - TypedQuery query = queryFactory.getCollectionQuery(environment, - field, isDistinct); + Object parentIdValue = queryFactory.getParentIdAttributeValue(source); + String dataLoaderKey = parentType.getName() + "." + attribute.getName(); - Stream resultStream = queryFactory.getResultStream(query, - fetchSize, - isDistinct); + DataLoader> dataLoader = environment.getDataLoader(dataLoaderKey); - return ResultStreamWrapper.wrap(resultStream, - fetchSize); + return dataLoader.load(parentIdValue, environment); } // Let hibernate resolve collection query diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java new file mode 100644 index 000000000..699a9fd02 --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java @@ -0,0 +1,65 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import static java.util.stream.Collectors.groupingBy; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; + +import javax.persistence.TypedQuery; + +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.MappedBatchLoaderWithContext; + +import com.introproventures.graphql.jpa.query.support.GraphQLSupport; +import graphql.language.Field; +import graphql.schema.DataFetchingEnvironment; + +// a batch loader function that will be called with N or more keys for batch loading +class GraphQLJpaOneToManyMappedBatchLoader implements MappedBatchLoaderWithContext> { + + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaOneToManyMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public CompletionStage>> load(Set keys, BatchLoaderEnvironment environment) { + Object key = keys.iterator().next(); + DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() + .get(key); + Field field = context.getField(); + + return CompletableFuture.supplyAsync(() -> { + TypedQuery query = queryFactory.getCollectionQuery(context, field, true, keys); + + List resultList = query.getResultList(); + + Map> batch = resultList.stream() + .collect(groupingBy(t -> t[0], + Collectors.mapping(t -> t[1], + GraphQLSupport.toResultList()))); + Map> resultMap = new LinkedHashMap<>(); + + keys.forEach(it -> { + List list = batch.getOrDefault(it, Collections.emptyList()); + + if (!list.isEmpty()) { + list = list.stream() + .filter(GraphQLSupport.distinctByKey(GraphQLSupport::identityToString)) + .collect(Collectors.toList()); + } + + resultMap.put(it, list); + }); + + return resultMap; + }); + } +}; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 3eee5b25f..89d016d28 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -45,6 +45,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; +import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -205,7 +206,7 @@ protected Stream queryResultStream(DataFetchingEnvironment environment, return getResultStream(query, fetchSize, isDistinct); } - protected Stream getResultStream(TypedQuery query, + protected Stream getResultStream(TypedQuery query, int fetchSize, boolean isDistinct) { @@ -334,16 +335,49 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F } @SuppressWarnings( { "rawtypes", "unchecked" } ) - protected TypedQuery getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct) { - - Object source = environment.getSource(); + protected TypedQuery getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set keys) { SingularAttribute parentIdAttribute = entityType.getId(Object.class); - Object parentIdValue = getAttributeValue(source, parentIdAttribute); + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + //CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); + CriteriaQuery query = cb.createQuery(Object[].class); + Root from = query.from(entityType); + + DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(query) + .localContext(Boolean.TRUE) + .build(); + + from.alias("owner"); + + // Must use inner join in parent context + Join join = from.join(field.getName()) + .on(from.get(parentIdAttribute.getName()).in(keys)); + + query.multiselect(from.get(parentIdAttribute.getName()), + join.alias(field.getName())); + + List predicates = getFieldPredicates(field, query, cb, from, join, queryEnvironment); + + query.where( + predicates.toArray(new Predicate[0]) + ); + + // optionally add default ordering + mayBeAddDefaultOrderBy(query, join, cb); + + return entityManager.createQuery(query.distinct(isDistinct)); + } + + @SuppressWarnings( { "rawtypes", "unchecked" } ) + protected TypedQuery getBatchCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set keys) { + + SingularAttribute parentIdAttribute = entityType.getId(Object.class); CriteriaBuilder cb = entityManager.getCriteriaBuilder(); - CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); + //CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); + CriteriaQuery query = cb.createQuery(); Root from = query.from(entityType); DataFetchingEnvironment queryEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) @@ -355,8 +389,7 @@ protected TypedQuery getCollectionQuery(DataFetchingEnvironment environm // Must use inner join in parent context Join join = from.join(field.getName()) - .on(cb.in(from.get(parentIdAttribute.getName())) - .value(parentIdValue)); + .on(from.get(parentIdAttribute.getName()).in(keys)); query.select(join.alias(field.getName())); @@ -372,6 +405,7 @@ protected TypedQuery getCollectionQuery(DataFetchingEnvironment environm return entityManager.createQuery(query.distinct(isDistinct)); } + @SuppressWarnings("unchecked") protected CriteriaQuery getCriteriaQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Object... keys) { CriteriaBuilder cb = entityManager.getCriteriaBuilder(); @@ -1564,6 +1598,13 @@ protected String[] idClassAttributeNames() { .toArray(new String[0]); } + + protected T getParentIdAttributeValue(T entity) { + SingularAttribute parentIdAttribute = entityType.getId(Object.class); + + return (T) getAttributeValue(entity, parentIdAttribute); + } + /** * Fetches the value of the given SingularAttribute on the given * entity. @@ -1571,14 +1612,14 @@ protected String[] idClassAttributeNames() { * http://stackoverflow.com/questions/7077464/how-to-get-singularattribute-mapped-value-of-a-persistent-object */ @SuppressWarnings("unchecked") - protected FieldType getAttributeValue(EntityType entity, SingularAttribute field) { + protected T getAttributeValue(T entity, SingularAttribute field) { try { Member member = field.getJavaMember(); if (member instanceof Method) { // this should be a getter method: - return (FieldType) ((Method)member).invoke(entity); + return (T) ((Method)member).invoke(entity); } else if (member instanceof java.lang.reflect.Field) { - return (FieldType) ((java.lang.reflect.Field)member).get(entity); + return (T) ((java.lang.reflect.Field)member).get(entity); } else { throw new IllegalArgumentException("Unexpected java member type. Expecting method or field, found: " + member); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 56ddad6f3..cc7b6d75c 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -44,6 +45,7 @@ import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; +import org.dataloader.MappedBatchLoaderWithContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,7 +58,6 @@ import com.introproventures.graphql.jpa.query.schema.impl.EntityIntrospector.EntityIntrospectionResult.AttributePropertyDescriptor; import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import com.introproventures.graphql.jpa.query.schema.relay.GraphQLJpaRelayDataFetcher; - import graphql.Assert; import graphql.Directives; import graphql.Scalars; @@ -80,7 +81,7 @@ /** * JPA specific schema builder implementation of {code #GraphQLSchemaBuilder} interface - * + * * @author Igor Dianov * */ @@ -93,30 +94,30 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { public static final String PAGE_START_PARAM_NAME = "start"; public static final String PAGE_LIMIT_PARAM_NAME = "limit"; - + public static final String QUERY_SELECT_PARAM_NAME = "select"; public static final String QUERY_WHERE_PARAM_NAME = "where"; public static final String QUERY_LOGICAL_PARAM_NAME = "logical"; public static final String SELECT_DISTINCT_PARAM_NAME = "distinct"; - + protected NamingStrategy namingStrategy = new NamingStrategy() {}; - + public static final String ORDER_BY_PARAM_NAME = "orderBy"; - + private Map, GraphQLOutputType> classCache = new HashMap<>(); private Map, GraphQLObjectType> entityCache = new HashMap<>(); private Map, GraphQLInputObjectType> inputObjectCache = new HashMap<>(); private Map, GraphQLInputObjectType> subqueryInputObjectCache = new HashMap<>(); private Map, GraphQLObjectType> embeddableOutputCache = new HashMap<>(); private Map, GraphQLInputObjectType> embeddableInputCache = new HashMap<>(); - + private static final Logger log = LoggerFactory.getLogger(GraphQLJpaSchemaBuilder.class); private EntityManager entityManager; - + private String name = "GraphQLJPA"; - + private String description = "GraphQL Schema for all entities in this JPA application"; private boolean isUseDistinctParameter = false; @@ -128,11 +129,15 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private int defaultMaxResults = 100; private int defaultFetchSize = 100; private int defaultPageLimitSize = 100; - + private final Relay relay = new Relay(); - + private final List entityPaths = new ArrayList<>(); - + + private Supplier batchLoadersMapProvider = () -> { + return BatchLoaderRegistry.getInstance(); + }; + public GraphQLJpaSchemaBuilder(EntityManager entityManager) { this.entityManager = entityManager; } @@ -144,7 +149,7 @@ public GraphQLJpaSchemaBuilder(EntityManager entityManager) { public GraphQLSchema build() { GraphQLSchema.Builder schema = GraphQLSchema.newSchema() .query(getQueryType()); - + if(enableSubscription) { schema.subscription(getSubscriptionType()); } @@ -152,16 +157,16 @@ public GraphQLSchema build() { if(enableDeferDirective) { schema.additionalDirective(Directives.DeferDirective); } - + if(enableRelay) { schema.additionalType(Relay.pageInfoType); } - + return schema.build(); } private GraphQLObjectType getQueryType() { - GraphQLObjectType.Builder queryType = + GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject() .name(this.name + "Query") .description(this.description); @@ -173,7 +178,7 @@ private GraphQLObjectType getQueryType() { .map(this::getQueryFieldByIdDefinition) .collect(Collectors.toList()) ); - + queryType.fields( entityManager.getMetamodel() .getEntities().stream() @@ -186,7 +191,7 @@ private GraphQLObjectType getQueryType() { } private GraphQLObjectType getSubscriptionType() { - GraphQLObjectType.Builder queryType = + GraphQLObjectType.Builder queryType = GraphQLObjectType.newObject() .name(this.name + "Subscription") .description(this.description); @@ -201,10 +206,10 @@ private GraphQLObjectType getSubscriptionType() { return queryType.build(); } - + private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityType) { GraphQLObjectType entityObjectType = getObjectType(entityType); - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -212,12 +217,12 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityT .withSelectNodeName(entityObjectType.getName()) .withToManyDefaultOptional(toManyDefaultOptional) .build(); - + DataFetcher dataFetcher = GraphQLJpaSimpleDataFetcher.builder() .withQueryFactory(queryFactory) .build(); String fieldName = entityType.getName(); - + return GraphQLFieldDefinition.newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description(getSchemaDescription(entityType)) @@ -232,10 +237,10 @@ private GraphQLFieldDefinition getQueryFieldByIdDefinition(EntityType entityT ) .build(); } - + private GraphQLObjectType getConnectionType(GraphQLObjectType nodeType) { GraphQLObjectType edgeType = relay.edgeType(nodeType.getName(), nodeType, null, Collections.emptyList()); - + return relay.connectionType(nodeType.getName(), edgeType, Collections.emptyList()); } @@ -244,7 +249,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit final GraphQLObjectType outputType = enableRelay ? getConnectionType(entityObjectType) : getSelectType(entityType); final DataFetcher dataFetcher; - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -254,7 +259,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit .withDefaultDistinct(isDefaultDistinct) .withDefaultFetchSize(defaultFetchSize) .build(); - + if(enableRelay) { dataFetcher = GraphQLJpaRelayDataFetcher.builder() .withQueryFactory(queryFactory) @@ -270,7 +275,7 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit } String fieldName = namingStrategy.pluralize(entityType.getName()); - + GraphQLFieldDefinition.Builder fieldDefinition = GraphQLFieldDefinition.newFieldDefinition() .name(enableRelay ? Introspector.decapitalize(fieldName) : fieldName) .description("Query request wrapper for " + entityType.getName() + " to request paginated data. " @@ -281,17 +286,17 @@ private GraphQLFieldDefinition getQueryFieldSelectDefinition(EntityType entit .dataFetcher(dataFetcher) .argument(getWhereArgument(entityType)) .arguments(enableRelay ? relay.getForwardPaginationConnectionFieldArguments() : Collections.singletonList(paginationArgument)); - + if (isUseDistinctParameter) { fieldDefinition.argument(distinctArgument(entityType)); } return fieldDefinition.build(); } - + private GraphQLObjectType getSelectType(EntityType entityType) { GraphQLObjectType selectObjectType = getObjectType(entityType); - + GraphQLObjectType selectPagedResultType = GraphQLObjectType.newObject() .name(namingStrategy.pluralize(entityType.getName())) .description("Query response wrapper object for " + entityType.getName() + ". When page is requested, this object will be returned with query metadata.") @@ -313,14 +318,14 @@ private GraphQLObjectType getSelectType(EntityType entityType) { .type(new GraphQLList(selectObjectType)) .build() ) - .build(); - + .build(); + return selectPagedResultType; } - + private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType entityType) { GraphQLObjectType entityObjectType = getObjectType(entityType); - + GraphQLJpaQueryFactory queryFactory = GraphQLJpaQueryFactory.builder() .withEntityManager(entityManager) .withEntityType(entityType) @@ -328,8 +333,8 @@ private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType entit .withSelectNodeName(SELECT_DISTINCT_PARAM_NAME) .withToManyDefaultOptional(toManyDefaultOptional) .withDefaultDistinct(isDefaultDistinct) - .build(); - + .build(); + DataFetcher dataFetcher = GraphQLJpaStreamDataFetcher.builder() .withQueryFactory(queryFactory) .build(); @@ -343,13 +348,13 @@ private GraphQLFieldDefinition getQueryFieldStreamDefinition(EntityType entit .dataFetcher(dataFetcher) .argument(paginationArgument) .argument(getWhereArgument(entityType)); - + if (isUseDistinctParameter) { fieldDefinition.argument(distinctArgument(entityType)); } return fieldDefinition.build(); - } + } private Map, GraphQLArgument> whereArgumentsMap = new HashMap<>(); @@ -365,7 +370,7 @@ private GraphQLArgument distinctArgument(EntityType entityType) { private GraphQLArgument getWhereArgument(ManagedType managedType) { return whereArgumentsMap.computeIfAbsent(managedType.getJavaType(), (javaType) -> computeWhereArgument(managedType)); } - + private GraphQLArgument computeWhereArgument(ManagedType managedType) { String type=resolveWhereArgumentTypeName(managedType); @@ -388,18 +393,18 @@ private GraphQLArgument computeWhereArgument(ManagedType managedType) { .name(Logical.EXISTS.name()) .description("Logical EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .field(GraphQLInputObjectField.newInputObjectField() .name(Logical.NOT_EXISTS.name()) .description("Logical NOT EXISTS subquery expression") .type(new GraphQLList(getSubqueryInputType(managedType))) - .build() + .build() ) .fields(managedType.getAttributes().stream() .filter(this::isValidInput) .filter(this::isNotIgnored) - .filter(this::isNotIgnoredFilter) + .filter(this::isNotIgnoredFilter) .map(this::getWhereInputField) .collect(Collectors.toList()) ) @@ -907,17 +912,27 @@ else if (attribute instanceof PluralAttribute // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); + GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() .name(baseEntity.getName()) .build(); - dataFetcher = new GraphQLJpaOneToManyDataFetcher(GraphQLJpaQueryFactory.builder() - .withEntityManager(entityManager) - .withEntityType(baseEntity) - .withEntityObjectType(entityObjectType) - .withSelectNodeName(entityObjectType.getName()) - .withDefaultDistinct(isDefaultDistinct) - .build(), + GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory.builder() + .withEntityManager(entityManager) + .withEntityType(baseEntity) + .withEntityObjectType(entityObjectType) + .withSelectNodeName(entityObjectType.getName()) + .withDefaultDistinct(isDefaultDistinct) + .build(); + + + String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); + MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(graphQLJpaQueryFactory); + + batchLoadersMapProvider.get() + .register(dataLoaderKey, mappedBatchLoader); + + dataFetcher = new GraphQLJpaOneToManyDataFetcher(graphQLJpaQueryFactory, (PluralAttribute) attribute); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java index 7d5256a98..8779f1d2d 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/support/GraphQLSupport.java @@ -4,20 +4,22 @@ import static com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder.QUERY_WHERE_PARAM_NAME; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import com.introproventures.graphql.jpa.query.schema.impl.PageArgument; - import graphql.language.Argument; import graphql.language.Field; import graphql.language.ObjectField; @@ -32,16 +34,16 @@ public static Stream fields(SelectionSet selectionSet) { .stream() .filter(Field.class::isInstance) .map(Field.class::cast); - + } public static Optional searchByFieldName(Field root, String fieldName) { Predicate matcher = (field) -> fieldName.equals(field.getName()); return search(root, matcher); - } + } + - public static Optional search(Field root, Predicate predicate) { Queue queue = new ArrayDeque<>(); queue.add(root); @@ -58,7 +60,7 @@ public static Optional search(Field root, Predicate predicate) { } return Optional.empty(); - } + } public static final Collection selections(Field field) { SelectionSet selectionSet = Optional.ofNullable(field.getSelectionSet()) @@ -67,7 +69,7 @@ public static final Collection selections(Field field) { return fields(selectionSet).collect(Collectors.toList()); } - + public static Optional getPageArgument(Field field) { return field.getArguments() .stream() @@ -81,13 +83,13 @@ public static Optional getWhereArgument(Field field) { .filter(it -> QUERY_WHERE_PARAM_NAME.equals(it.getName())) .findFirst(); } - + public static PageArgument extractPageArgument(DataFetchingEnvironment environment, Optional paginationRequest, int defaultPageLimitSize) { if (paginationRequest.isPresent()) { Map pagex = environment.getArgument(GraphQLJpaSchemaBuilder.PAGE_PARAM_NAME); - + Integer start = pagex.getOrDefault(GraphQLJpaSchemaBuilder.PAGE_START_PARAM_NAME, 1); Integer limit = pagex.getOrDefault(GraphQLJpaSchemaBuilder.PAGE_LIMIT_PARAM_NAME, defaultPageLimitSize); @@ -96,7 +98,7 @@ public static PageArgument extractPageArgument(DataFetchingEnvironment environm return new PageArgument(1, defaultPageLimitSize); } - + public static Field removeArgument(Field field, Optional argument) { if (!argument.isPresent()) { @@ -109,7 +111,7 @@ public static Field removeArgument(Field field, Optional argument) { return field.transform(builder -> builder.arguments(newArguments)); } - + public static Boolean isWhereArgument(Argument argument) { return GraphQLJpaSchemaBuilder.QUERY_WHERE_PARAM_NAME.equals(argument.getName()); } @@ -125,7 +127,7 @@ public static Boolean isFirstArgument(Argument argument) { public static Boolean isAfterArgument(Argument argument) { return "after".equals(argument.getName()); } - + public static Boolean isLogicalArgument(Argument argument) { return GraphQLJpaSchemaBuilder.QUERY_LOGICAL_PARAM_NAME.equals(argument.getName()); } @@ -133,7 +135,7 @@ public static Boolean isLogicalArgument(Argument argument) { public static Boolean isDistinctArgument(Argument argument) { return GraphQLJpaSchemaBuilder.SELECT_DISTINCT_PARAM_NAME.equals(argument.getName()); } - + public static final Optional getObjectField(ObjectValue objectValue, String fieldName) { return objectValue.getObjectFields().stream() .filter(it -> fieldName.equals(it.getName())) @@ -144,6 +146,40 @@ public static final Optional getSelectionField(Field field, String fieldN return GraphQLSupport.fields(field.getSelectionSet()) .filter(it -> fieldName.equals(it.getName())) .findFirst(); - } - + } + + public static Collector, List> toResultList() { + return Collector.of(ArrayList::new, + (list, item) -> { + if (item != null) { + list.add(item); + } + }, + (left, right) -> { + left.addAll(right); + return left; + }, + Collector.Characteristics.CONCURRENT); + } + + public static Predicate distinctByKey(Function keyExtractor) { + Map seen = new ConcurrentHashMap<>(); + + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + + public static String identityToString(final Object object) { + if (object == null) { + return null; + } + final String name = object.getClass().getName(); + final String hexString = Integer.toHexString(System.identityHashCode(object)); + final StringBuilder builder = new StringBuilder(name.length() + 1 + hexString.length()); + // @formatter:off + builder.append(name) + .append("@") + .append(hexString); + // @formatter:off + return builder.toString(); + } } diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index fc7bce7c8..10deaf2b3 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -27,7 +27,6 @@ import javax.persistence.Query; import javax.transaction.Transactional; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -41,6 +40,7 @@ import com.introproventures.graphql.jpa.query.schema.model.starwars.Droid; @SpringBootTest +@Transactional public class StarwarsQueryExecutorTests extends AbstractSpringBootTestSupport { @SpringBootApplication @@ -120,32 +120,6 @@ public void queryManyToManyTester() { .containsOnly("Luke Skywalker"); } - @Test - @Transactional - @Ignore - public void queryOneToManyTesterLeftOuterJoinTester() { - // given: - Query query = em.createQuery("select distinct author from Author as author \n" + - "left join fetch author.books as books on books.title like '%War%' \n" + - "where ( author.id in (1L, 4L, 8L) ) \n" + - "order by author.id asc"); - - String qu = "select d1 from Department d1 left join fetch d1.manager where exists \n" + - "(select d2 from Department d2 where d2.manager is null and d1 = d2)"; - - query.setHint("hibernate.query.passDistinctThrough", false); - - - - // when: - List result = query.getResultList(); - - assertThat(result).isNotEmpty(); - } - - - - @Test public void getsNamesOfAllDroids() { //given: From 75baf029312b8c5ae7de6d95f6ff0143b9579aa0 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 5 Apr 2020 19:07:47 -0700 Subject: [PATCH 07/16] fix: refactor GraphQLJpaOneToManyMappedBatchLoader --- .../GraphQLJpaOneToManyMappedBatchLoader.java | 36 ++----------------- .../schema/impl/GraphQLJpaQueryFactory.java | 30 ++++++++++++++++ .../schema/impl/GraphQLJpaSchemaBuilder.java | 7 ++-- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java index 699a9fd02..a7a88c3c5 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java @@ -1,23 +1,14 @@ package com.introproventures.graphql.jpa.query.schema.impl; -import static java.util.stream.Collectors.groupingBy; - -import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; - -import javax.persistence.TypedQuery; import org.dataloader.BatchLoaderEnvironment; import org.dataloader.MappedBatchLoaderWithContext; -import com.introproventures.graphql.jpa.query.support.GraphQLSupport; -import graphql.language.Field; import graphql.schema.DataFetchingEnvironment; // a batch loader function that will be called with N or more keys for batch loading @@ -34,32 +25,9 @@ public CompletionStage>> load(Set keys, BatchLo Object key = keys.iterator().next(); DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() .get(key); - Field field = context.getField(); - - return CompletableFuture.supplyAsync(() -> { - TypedQuery query = queryFactory.getCollectionQuery(context, field, true, keys); - - List resultList = query.getResultList(); - Map> batch = resultList.stream() - .collect(groupingBy(t -> t[0], - Collectors.mapping(t -> t[1], - GraphQLSupport.toResultList()))); - Map> resultMap = new LinkedHashMap<>(); - - keys.forEach(it -> { - List list = batch.getOrDefault(it, Collections.emptyList()); - - if (!list.isEmpty()) { - list = list.stream() - .filter(GraphQLSupport.distinctByKey(GraphQLSupport::identityToString)) - .collect(Collectors.toList()); - } + return CompletableFuture.supplyAsync(() -> queryFactory.loadMany(context, keys)); + } - resultMap.put(it, list); - }); - return resultMap; - }); - } }; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 89d016d28..1746de1f3 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -26,6 +26,7 @@ import static graphql.introspection.Introspection.SchemaMetaFieldDef; import static graphql.introspection.Introspection.TypeMetaFieldDef; import static graphql.introspection.Introspection.TypeNameMetaFieldDef; +import static java.util.stream.Collectors.groupingBy; import java.beans.BeanInfo; import java.beans.Introspector; @@ -334,6 +335,35 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F return entityManager.createQuery(query); } + protected Map> loadMany(DataFetchingEnvironment environment, + Set keys) { + Field field = environment.getField(); + + TypedQuery query = getCollectionQuery(environment, field, isDefaultDistinct(), keys); + + List resultList = query.getResultList(); + + Map> batch = resultList.stream() + .collect(groupingBy(t -> t[0], + Collectors.mapping(t -> t[1], + GraphQLSupport.toResultList()))); + Map> resultMap = new LinkedHashMap<>(); + + keys.forEach(it -> { + List list = batch.getOrDefault(it, Collections.emptyList()); + + if (!list.isEmpty()) { + list = list.stream() + .filter(GraphQLSupport.distinctByKey(GraphQLSupport::identityToString)) + .collect(Collectors.toList()); + } + + resultMap.put(it, list); + }); + + return resultMap; + } + @SuppressWarnings( { "rawtypes", "unchecked" } ) protected TypedQuery getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set keys) { diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index cc7b6d75c..15bc17676 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -134,7 +134,7 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private final List entityPaths = new ArrayList<>(); - private Supplier batchLoadersMapProvider = () -> { + private Supplier batchLoadersRegistry = () -> { return BatchLoaderRegistry.getInstance(); }; @@ -925,12 +925,11 @@ else if (attribute instanceof PluralAttribute .withDefaultDistinct(isDefaultDistinct) .build(); - String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(graphQLJpaQueryFactory); - batchLoadersMapProvider.get() - .register(dataLoaderKey, mappedBatchLoader); + batchLoadersRegistry.get() + .register(dataLoaderKey, mappedBatchLoader); dataFetcher = new GraphQLJpaOneToManyDataFetcher(graphQLJpaQueryFactory, (PluralAttribute) attribute); From 974705043371b4c639239762d6fdfde6f1f0cfeb Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Sun, 5 Apr 2020 22:46:14 -0700 Subject: [PATCH 08/16] feat: add GraphQLJpaToOneMappedBatchLoader support --- .../schema/impl/BatchLoaderRegistry.java | 30 ++- .../GraphQLJpaOneToManyMappedBatchLoader.java | 2 +- .../schema/impl/GraphQLJpaQueryFactory.java | 48 +++-- .../schema/impl/GraphQLJpaSchemaBuilder.java | 28 ++- .../impl/GraphQLJpaToOneDataFetcher.java | 75 +++++++ .../GraphQLJpaToOneMappedBatchLoader.java | 30 +++ .../schema/StarwarsQueryExecutorTests.java | 190 +++++++++++++++--- 7 files changed, 349 insertions(+), 54 deletions(-) create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java create mode 100644 graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java index 4fe60d463..31cc31de2 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/BatchLoaderRegistry.java @@ -10,26 +10,38 @@ import org.dataloader.MappedBatchLoaderWithContext; public class BatchLoaderRegistry { - private final static Map>> mappedBatchLoaders = new LinkedHashMap<>(); + private final static Map>> mappedToManyBatchLoaders = new LinkedHashMap<>(); + private final static Map> mappedToOneBatchLoaders = new LinkedHashMap<>(); private static BatchLoaderRegistry instance = new BatchLoaderRegistry(); public static BatchLoaderRegistry getInstance() { return instance; } - public static void register(String batchLoaderKey, MappedBatchLoaderWithContext> mappedBatchLoader) { - mappedBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); + public static void registerToMany(String batchLoaderKey, MappedBatchLoaderWithContext> mappedBatchLoader) { + mappedToManyBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); + } + + public static void registerToOne(String batchLoaderKey, MappedBatchLoaderWithContext mappedBatchLoader) { + mappedToOneBatchLoaders.putIfAbsent(batchLoaderKey, mappedBatchLoader); } public static DataLoaderRegistry newDataLoaderRegistry(DataLoaderOptions dataLoaderOptions) { DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry(); - mappedBatchLoaders.entrySet() - .forEach(entry -> { - DataLoader> dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), - dataLoaderOptions); - dataLoaderRegistry.register(entry.getKey(), dataLoader); - }); + mappedToManyBatchLoaders.entrySet() + .forEach(entry -> { + DataLoader> dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), + dataLoaderOptions); + dataLoaderRegistry.register(entry.getKey(), dataLoader); + }); + + mappedToOneBatchLoaders.entrySet() + .forEach(entry -> { + DataLoader dataLoader = DataLoader.newMappedDataLoader(entry.getValue(), + dataLoaderOptions); + dataLoaderRegistry.register(entry.getKey(), dataLoader); + }); return dataLoaderRegistry; diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java index a7a88c3c5..11e02d201 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java @@ -26,7 +26,7 @@ public CompletionStage>> load(Set keys, BatchLo DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() .get(key); - return CompletableFuture.supplyAsync(() -> queryFactory.loadMany(context, keys)); + return CompletableFuture.supplyAsync(() -> queryFactory.loadOneToMany(context, keys)); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 1746de1f3..7daca41be 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -156,8 +156,8 @@ public DataFetchingEnvironment getQueryEnvironment(DataFetchingEnvironment envir } public List queryKeys(DataFetchingEnvironment environment, - int firstResult, - int maxResults) { + int firstResult, + int maxResults) { MergedField queryField = resolveQueryField(environment.getField()); // Override query environment with associated entity object type and @@ -187,7 +187,6 @@ public List queryResultList(DataFetchingEnvironment environment, // Let's wrap stream into lazy list to pass it downstream return ResultStreamWrapper.wrap(resultStream, maxResults); - } protected Stream queryResultStream(DataFetchingEnvironment environment, @@ -208,8 +207,8 @@ protected Stream queryResultStream(DataFetchingEnvironment environment, } protected Stream getResultStream(TypedQuery query, - int fetchSize, - boolean isDistinct) { + int fetchSize, + boolean isDistinct) { // Let' try reduce overhead and disable all caching query.setHint(ORG_HIBERNATE_READ_ONLY, true); @@ -230,7 +229,6 @@ protected Stream getResultStream(TypedQuery query, .peek(entityManager::detach); } - protected Object querySingleResult(final DataFetchingEnvironment environment) { final MergedField queryField = flattenEmbeddedIdArguments(environment.getField()); @@ -335,11 +333,11 @@ protected TypedQuery getKeysQuery(DataFetchingEnvironment environment, F return entityManager.createQuery(query); } - protected Map> loadMany(DataFetchingEnvironment environment, + protected Map> loadOneToMany(DataFetchingEnvironment environment, Set keys) { Field field = environment.getField(); - TypedQuery query = getCollectionQuery(environment, field, isDefaultDistinct(), keys); + TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); List resultList = query.getResultList(); @@ -364,8 +362,31 @@ protected Map> loadMany(DataFetchingEnvironment environment return resultMap; } + protected Map loadManyToOne(DataFetchingEnvironment environment, + Set keys) { + Field field = environment.getField(); + + TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); + + List resultList = query.getResultList(); + + Map batch = new LinkedHashMap<>(); + + resultList.forEach(item -> batch.put(item[0], item[1])); + + Map resultMap = new LinkedHashMap<>(); + + keys.forEach(it -> { + Object list = batch.getOrDefault(it, null); + + resultMap.put(it, list); + }); + + return resultMap; + } + @SuppressWarnings( { "rawtypes", "unchecked" } ) - protected TypedQuery getCollectionQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set keys) { + protected TypedQuery getBatchQuery(DataFetchingEnvironment environment, Field field, boolean isDistinct, Set keys) { SingularAttribute parentIdAttribute = entityType.getId(Object.class); @@ -546,10 +567,12 @@ protected final List getFieldPredicates(Field field, CriteriaQuery ) { // Let's do fugly conversion isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(attribute.isOptional()); + .orElse(attribute.isOptional()); // Let's apply left outer join to retrieve optional associations - fetch = reuseFetch(from, selection.getName(), isOptional); + if(!isOptional || !whereArgument.isPresent()) { + fetch = reuseFetch(from, selection.getName(), isOptional); + } } else if(attribute.getPersistentAttributeType() == PersistentAttributeType.EMBEDDED) { // Process where arguments clauses. arguments.addAll(selection.getArguments() @@ -571,12 +594,11 @@ protected final List getFieldPredicates(Field field, CriteriaQuery EntityType entityType = getEntityType(objectType); PluralAttribute attribute = (PluralAttribute) entityType.getAttribute(selection.getName()); - Optional whereArg = GraphQLSupport.getWhereArgument(selection); // Let's join fetch element collections to avoid filtering their values used where search criteria if(PersistentAttributeType.ELEMENT_COLLECTION == attribute.getPersistentAttributeType()) { from.fetch(selection.getName(), JoinType.LEFT); - } else if(!whereArg.isPresent()) { + } else if(!whereArgument.isPresent()) { // Let's apply fetch join to retrieve associated plural attributes fetch = reuseFetch(from, selection.getName(), isOptional); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 15bc17676..24c694350 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -888,6 +888,7 @@ && isNotIgnoredOrder(attribute) ) { ); } + // Get the fields that can be queried on (i.e. Simple Types, no Sub-Objects) if (attribute instanceof SingularAttribute && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC) { @@ -900,6 +901,29 @@ && isNotIgnoredOrder(attribute) ) { // to-one end could be optional arguments.add(optionalArgument(singularAttribute.isOptional())); + GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() + .name(baseEntity.getName()) + .build(); + + GraphQLJpaQueryFactory graphQLJpaQueryFactory = GraphQLJpaQueryFactory.builder() + .withEntityManager(entityManager) + .withEntityType(baseEntity) + .withEntityObjectType(entityObjectType) + .withSelectNodeName(entityObjectType.getName()) + .withDefaultDistinct(isDefaultDistinct) + .build(); + + String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); + + MappedBatchLoaderWithContext mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(graphQLJpaQueryFactory); + + batchLoadersRegistry.get() + .registerToOne(dataLoaderKey, mappedBatchLoader); + + dataFetcher = new GraphQLJpaToOneDataFetcher(graphQLJpaQueryFactory, + (SingularAttribute) attribute); + + } // Get Sub-Objects fields queries via DataFetcher else if (attribute instanceof PluralAttribute && (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_MANY @@ -912,7 +936,6 @@ else if (attribute instanceof PluralAttribute // make it configurable via builder api arguments.add(optionalArgument(toManyDefaultOptional)); - GraphQLObjectType entityObjectType = GraphQLObjectType.newObject() .name(baseEntity.getName()) .build(); @@ -926,10 +949,11 @@ else if (attribute instanceof PluralAttribute .build(); String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); + MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(graphQLJpaQueryFactory); batchLoadersRegistry.get() - .register(dataLoaderKey, mappedBatchLoader); + .registerToMany(dataLoaderKey, mappedBatchLoader); dataFetcher = new GraphQLJpaOneToManyDataFetcher(graphQLJpaQueryFactory, (PluralAttribute) attribute); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java new file mode 100644 index 000000000..b169118bf --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017 IntroPro Ventures Inc. and/or its affiliates. + * + * 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.introproventures.graphql.jpa.query.schema.impl; + +import java.util.Optional; + +import javax.persistence.metamodel.Attribute.PersistentAttributeType; +import javax.persistence.metamodel.SingularAttribute; + +import org.dataloader.DataLoader; + +import graphql.language.Argument; +import graphql.language.Field; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLType; + +/** + * One-To-Many DataFetcher that uses where argument to filter collection attributes + * + * @author Igor Dianov + * + */ +class GraphQLJpaToOneDataFetcher implements DataFetcher { + + private final SingularAttribute attribute; + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToOneDataFetcher(GraphQLJpaQueryFactory queryFactory, + SingularAttribute attribute) { + this.queryFactory = queryFactory; + this.attribute = attribute; + } + + @Override + public Object get(DataFetchingEnvironment environment) { + Field field = environment.getField(); + GraphQLType parentType = environment.getParentType(); + + Object source = environment.getSource(); + Optional optionalArg = queryFactory.getArgument(field, "optional"); + Boolean isOptional = optionalArg.map(it -> queryFactory.getArgumentValue(environment, it, Boolean.class)) + .orElse(attribute.isOptional()); + + // Resolve collection query if where argument is present + // TODO or any field in selection has orderBy argument + if (isOptional && !PersistentAttributeType.EMBEDDED.equals(attribute.getPersistentAttributeType())) { + Object parentIdValue = queryFactory.getParentIdAttributeValue(source); + String dataLoaderKey = parentType.getName() + "." + attribute.getName(); + + DataLoader dataLoader = environment.getDataLoader(dataLoaderKey); + + return dataLoader.load(parentIdValue, environment); + } + + // Let hibernate resolve collection query + return queryFactory.getAttributeValue(source, + attribute); + } + +} diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java new file mode 100644 index 000000000..93fb4668b --- /dev/null +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneMappedBatchLoader.java @@ -0,0 +1,30 @@ +package com.introproventures.graphql.jpa.query.schema.impl; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.MappedBatchLoaderWithContext; + +import graphql.schema.DataFetchingEnvironment; + +// a batch loader function that will be called with N or more keys for batch loading +class GraphQLJpaToOneMappedBatchLoader implements MappedBatchLoaderWithContext { + + private final GraphQLJpaQueryFactory queryFactory; + + public GraphQLJpaToOneMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public CompletionStage> load(Set keys, BatchLoaderEnvironment environment) { + Object key = keys.iterator().next(); + DataFetchingEnvironment context = (DataFetchingEnvironment) environment.getKeyContexts() + .get(key); + + return CompletableFuture.supplyAsync(() -> queryFactory.loadManyToOne(context, keys)); + } +}; diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index 10deaf2b3..2e2013b7a 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -172,7 +172,44 @@ public void queryManyToOneJoinByIdWithVariables() { put("id", "2001"); }}; - String expected = "{Humans={select=[{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}]}}"; + String expected = "{Humans={select=[" + + "{name=Luke Skywalker, homePlanet=Tatooine, favoriteDroid=null}, " + + "{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}, " + + "{name=Han Solo, homePlanet=null, favoriteDroid=null}, " + + "{name=Leia Organa, homePlanet=Alderaan, favoriteDroid=null}, " + + "{name=Wilhuff Tarkin, homePlanet=null, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query,variables).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @SuppressWarnings("serial") + @Test + public void queryManyToOneJoinByIdWithVariablesOptionalFalse() { + //given: + String query = "query ($id: String!) {" + + " Humans {" + + " select {" + + " name" + + " homePlanet" + + " favoriteDroid(where: {id: {EQ: $id}}, optional: false) {" + + " name" + + " }" + + " }" + + " }" + + "}" + + ""; + Map variables = new HashMap() {{ + put("id", "2001"); + }}; + + String expected = "{Humans={select=[" + + "{name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2}}" + + "]}}"; //when: Object result = executor.execute(query,variables).getData(); @@ -451,14 +488,52 @@ public void queryByCollectionOfEnumsAtRootLevel() { @Test public void queryByRestrictingSubObject() { //given: - String query = "query { Humans { select { name gender(where:{ code: {EQ: \"Male\"}}) { description } } } }"; + String query = "query {" + + " Humans {" + + " select {" + + " name" + + " gender(where: {code: {EQ: \"Male\"}}) {" + + " description" + + " }" + + " }" + + " }" + + "}"; String expected = "{Humans={select=[" - + "{name=Luke Skywalker, gender={description=Male}}, " - + "{name=Darth Vader, gender={description=Male}}, " - + "{name=Han Solo, gender={description=Male}}, " - + "{name=Wilhuff Tarkin, gender={description=Male}}" - + "]}}"; + + "{name=Luke Skywalker, gender={description=Male}}, " + + "{name=Darth Vader, gender={description=Male}}, " + + "{name=Han Solo, gender={description=Male}}, " + + "{name=Leia Organa, gender=null}, " + + "{name=Wilhuff Tarkin, gender={description=Male}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryByRestrictingSubObjectOptionalFalse() { + //given: + String query = "query {" + + " Humans {" + + " select {" + + " name" + + " gender(where: {code: {EQ: \"Male\"}}, optional: false) {" + + " description" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{name=Luke Skywalker, gender={description=Male}}, " + + "{name=Darth Vader, gender={description=Male}}, " + + "{name=Han Solo, gender={description=Male}}, " + + "{name=Wilhuff Tarkin, gender={description=Male}}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -944,14 +1019,48 @@ public void queryHumansWittFavorideDroidExplicitOptionalFalseParameterBinding() @Test public void queryFilterManyToOneEmbdeddedCriteria() { //given: - String query = "query { Droids { select { name primaryFunction(where: {function: {EQ:\"Astromech\"}}) { function }}}}"; + String query = "query {" + + " Droids {" + + " select {" + + " name" + + " primaryFunction(where: {function: {EQ: \"Astromech\"}}) {" + + " function" + + " }" + + " }" + + " }" + + "}" + + ""; - String expected = "{Droids={" + - "select=[{" + - "name=R2-D2, " + - "primaryFunction={function=Astromech}" + - "}]" + - "}}"; + String expected = "{Droids={select=[" + + "{name=C-3PO, primaryFunction=null}, " + + "{name=R2-D2, primaryFunction={function=Astromech}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryFilterManyToOneEmbdeddedCriteriaOptionalFalse() { + //given: + String query = "query {" + + " Droids {" + + " select {" + + " name" + + " primaryFunction(where: {function: {EQ: \"Astromech\"}} optional: false) {" + + " function" + + " }" + + " }" + + " }" + + "}" + + ""; + + String expected = "{Droids={select=[" + + "{name=R2-D2, primaryFunction={function=Astromech}}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -1007,21 +1116,13 @@ public void queryFilterNestedManyToOneToDo() { " }" + "}"; - String expected = "{Humans={" + - "select=[" + - "{" + - "id=1001, " + - "name=Darth Vader, " + - "homePlanet=Tatooine, " + - "favoriteDroid={" + - "name=R2-D2, " + - "primaryFunction={" + - "function=Astromech" + - "}" + - "}" + - "}" + - "]" + - "}}"; + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, homePlanet=Tatooine, favoriteDroid={name=C-3PO, primaryFunction=null}}, " + + "{id=1001, name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}, " + + "{id=1002, name=Han Solo, homePlanet=null, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, homePlanet=Alderaan, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, homePlanet=null, favoriteDroid=null}" + + "]}}"; //when: Object result = executor.execute(query).getData(); @@ -1030,6 +1131,37 @@ public void queryFilterNestedManyToOneToDo() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryFilterNestedManyToOneToDoOptionalFalse() { + //given: + String query = "query {" + + " Humans {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction(where:{function:{EQ:\"Astromech\"}}, optional: false) {" + + " function" + + " }" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{id=1001, name=Darth Vader, homePlanet=Tatooine, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test public void queryFilterNestedManyToOneRelationCriteria() { //given: From 401b5398def9fc8db538c8b0075b5ac8a400a181 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 07:37:55 -0700 Subject: [PATCH 09/16] fix: add batch query logging --- .../jpa/query/schema/impl/GraphQLJpaQueryFactory.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 7daca41be..9ec27b0ac 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -339,6 +339,10 @@ protected Map> loadOneToMany(DataFetchingEnvironment enviro TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(query)); + } + List resultList = query.getResultList(); Map> batch = resultList.stream() @@ -368,6 +372,10 @@ protected Map loadManyToOne(DataFetchingEnvironment environment, TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); + if (logger.isDebugEnabled()) { + logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(query)); + } + List resultList = query.getResultList(); Map batch = new LinkedHashMap<>(); From 460dd60ac897a59e42256cfdb65e4b2881b004b9 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 15:26:29 -0700 Subject: [PATCH 10/16] fix: add support orderBy sort for collections --- .../impl/GraphQLJpaOneToManyDataFetcher.java | 5 +- .../schema/impl/GraphQLJpaQueryFactory.java | 46 ++++++++++++++----- .../query/schema/GraphQLExecutorTests.java | 39 +++++++++++++++- 3 files changed, 75 insertions(+), 15 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java index db8fbcb53..762f288ea 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java @@ -55,9 +55,8 @@ public Object get(DataFetchingEnvironment environment) { Object source = environment.getSource(); Optional whereArg = GraphQLSupport.getWhereArgument(field); - // Resolve collection query if where argument is present - // TODO or any field in selection has orderBy argument - if (whereArg.isPresent()) { + // Resolve collection query if where argument is present or any field in selection has orderBy argument + if (whereArg.isPresent() || queryFactory.hasAnySelectionOrderBy(field)) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); String dataLoaderKey = parentType.getName() + "." + attribute.getName(); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 9ec27b0ac..b9628a934 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -116,6 +116,8 @@ */ public final class GraphQLJpaQueryFactory { + private static final String DESC = "DESC"; + private final static Logger logger = LoggerFactory.getLogger(GraphQLJpaQueryFactory.class); protected static final String WHERE = "where"; @@ -340,7 +342,7 @@ protected Map> loadOneToMany(DataFetchingEnvironment enviro TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); if (logger.isDebugEnabled()) { - logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(query)); + logger.info("\nGraphQL JPQL Batch Query String:\n {}", getJPQLQueryString(query)); } List resultList = query.getResultList(); @@ -373,7 +375,7 @@ protected Map loadManyToOne(DataFetchingEnvironment environment, TypedQuery query = getBatchQuery(environment, field, isDefaultDistinct(), keys); if (logger.isDebugEnabled()) { - logger.info("\nGraphQL JPQL Count Query String:\n {}", getJPQLQueryString(query)); + logger.info("\nGraphQL JPQL Batch Query String:\n {}", getJPQLQueryString(query)); } List resultList = query.getResultList(); @@ -518,13 +520,6 @@ else if (hasIdClassAttribue()) { return query.distinct(isDistinct); } - Stream zipped(List lista, List listb, BiFunction zipper){ - int shortestLength = Math.min(lista.size(),listb.size()); - return IntStream.range(0,shortestLength).mapToObj( i -> { - return zipper.apply(lista.get(i), listb.get(i)); - }); - } - protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery query, CriteriaBuilder cb, Path fieldPath, DataFetchingEnvironment environment) { // Singular attributes only if (fieldPath.getModel() instanceof SingularAttribute) { @@ -536,7 +531,7 @@ protected void mayBeAddOrderBy(Field selectedField, CriteriaQuery query, Crit .ifPresent(orderBy -> { List orders = new ArrayList<>(query.getOrderList()); - if ("DESC".equals(orderBy.getName())) { + if (DESC.equals(orderBy.getName())) { orders.add(cb.desc(fieldPath)); } else { orders.add(cb.asc(fieldPath)); @@ -608,7 +603,9 @@ protected final List getFieldPredicates(Field field, CriteriaQuery from.fetch(selection.getName(), JoinType.LEFT); } else if(!whereArgument.isPresent()) { // Let's apply fetch join to retrieve associated plural attributes - fetch = reuseFetch(from, selection.getName(), isOptional); + if (!hasAnySelectionOrderBy(selection)) { + fetch = reuseFetch(from, selection.getName(), isOptional); + } } } // Let's build join fetch graph to avoid Hibernate error: @@ -1772,6 +1769,33 @@ private MergedField flattenEmbeddedIdArguments(Field field) { .build(); } + protected boolean hasAnySelectionOrderBy(Field field) { + + if (!hasSelectionSet(field)) + return false; + + // Loop through all of the fields being requested + return field.getSelectionSet() + .getSelections() + .stream() + .filter(Field.class::isInstance) + .map(Field.class::cast) + .anyMatch(selectedField -> { + + // Optional orderBy argument + Optional orderBy = selectedField.getArguments() + .stream() + .filter(this::isOrderByArgument) + .findFirst(); + + if (orderBy.isPresent()) { + return true; + } + + return false; + }); + + } /** * Creates builder to build {@link GraphQLJpaQueryFactory}. diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index 5f27fa81c..96eb98703 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -432,7 +432,44 @@ public void queryAuthorBooksWithExplictOptionalEXISTS() { // then assertThat(result.toString()).isEqualTo(expected); - } + } + + @Test + public void queryAuthorBooksWithCollectionOrderBy() { + //given + String query = "query { " + + "Authors {" + + " select {" + + " id" + + " name(orderBy: ASC)" + + " books {" + + " id" + + " title(orderBy: DESC)" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=4, name=Anton Chekhov, books=[" + + "{id=7, title=Three Sisters}, " + + "{id=6, title=The Seagull}, " + + "{id=5, title=The Cherry Orchard}" + + "]}, " + + "{id=8, name=Igor Dianov, books=[]}, " + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace}, " + + "{id=3, title=Anna Karenina}" + + "]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithIsNullId() { //given From 9adc8e7e38266ba6037f0d3164fc8da21dcc7f5e Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 15:27:37 -0700 Subject: [PATCH 11/16] fix: unexpected NPE in getJPQLQueryString --- .../jpa/query/schema/impl/GraphQLJpaQueryFactory.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index b9628a934..8c4326618 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -1623,8 +1623,10 @@ protected String getJPQLQueryString(TypedQuery query) { ReflectionUtil.forceAccess(queryStringField); - return queryStringField.get(queryImpl) - .toString(); + if(queryStringField != null) { + return queryStringField.get(queryImpl) + .toString(); + } } catch (Exception ignored) { logger.error("Error getting JPQL string", ignored); From 6ffd660412830b7dc5773ac8f22bde8bd3c9711d Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 15:29:49 -0700 Subject: [PATCH 12/16] fix: add support for inline optional to one assiciations --- .../schema/impl/GraphQLJpaQueryFactory.java | 27 +++++++++++-------- .../impl/GraphQLJpaToOneDataFetcher.java | 8 +++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java index 8c4326618..118b9c4b4 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryFactory.java @@ -47,7 +47,6 @@ import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -554,9 +553,7 @@ protected final List getFieldPredicates(Field field, CriteriaQuery Path fieldPath = from.get(selection.getName()); From fetch = null; - Optional optionalArgument = getArgument(selection, OPTIONAL); Optional whereArgument = getArgument(selection, WHERE); - Boolean isOptional = null; // Build predicate arguments for singular attributes only if(fieldPath.getModel() instanceof SingularAttribute) { @@ -569,8 +566,9 @@ protected final List getFieldPredicates(Field field, CriteriaQuery || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE ) { // Let's do fugly conversion - isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(attribute.isOptional()); + Boolean isOptional = getOptionalArgumentValue(environment, + selection, + attribute); // Let's apply left outer join to retrieve optional associations if(!isOptional || !whereArgument.isPresent()) { @@ -587,17 +585,17 @@ protected final List getFieldPredicates(Field field, CriteriaQuery } } else { - // We must add plural attributes with explicit join fetch - // Let's do fugly conversion - // the many end is a collection, and it is always optional by default (empty collection) - isOptional = optionalArgument.map(it -> getArgumentValue(environment, it, Boolean.class)) - .orElse(toManyDefaultOptional); - GraphQLObjectType objectType = getObjectType(environment); EntityType entityType = getEntityType(objectType); PluralAttribute attribute = (PluralAttribute) entityType.getAttribute(selection.getName()); + // We must add plural attributes with explicit join fetch + // the many end is a collection, and it is always optional by default (empty collection) + Boolean isOptional = getOptionalArgumentValue(environment, + selection, + attribute); + // Let's join fetch element collections to avoid filtering their values used where search criteria if(PersistentAttributeType.ELEMENT_COLLECTION == attribute.getPersistentAttributeType()) { from.fetch(selection.getName(), JoinType.LEFT); @@ -648,6 +646,13 @@ protected final List getFieldPredicates(Field field, CriteriaQuery return predicates; } + protected Boolean getOptionalArgumentValue(DataFetchingEnvironment environment, + Field selection, + Attribute attribute) { + return getArgument(selection, OPTIONAL).map(it -> getArgumentValue(environment, it, Boolean.class)) + .orElseGet(() -> isOptionalAttribute(attribute)); + } + /** * if query orders are empty, then apply default ascending ordering * by root id attribute to prevent paging inconsistencies diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java index b169118bf..9e4c3ff3d 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java @@ -52,12 +52,10 @@ public Object get(DataFetchingEnvironment environment) { GraphQLType parentType = environment.getParentType(); Object source = environment.getSource(); - Optional optionalArg = queryFactory.getArgument(field, "optional"); - Boolean isOptional = optionalArg.map(it -> queryFactory.getArgumentValue(environment, it, Boolean.class)) - .orElse(attribute.isOptional()); - + Boolean isOptional = queryFactory.getOptionalArgumentValue(environment, + field, + attribute); // Resolve collection query if where argument is present - // TODO or any field in selection has orderBy argument if (isOptional && !PersistentAttributeType.EMBEDDED.equals(attribute.getPersistentAttributeType())) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); String dataLoaderKey = parentType.getName() + "." + attribute.getName(); From 6756369abb78618d61e7a9b3536176aeebd31b61 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 15:51:07 -0700 Subject: [PATCH 13/16] fix: add inline optional support for singular attributes --- .../query/schema/GraphQLExecutorTests.java | 1122 +++++++++-------- .../schema/StarwarsQueryExecutorTests.java | 154 +++ 2 files changed, 743 insertions(+), 533 deletions(-) diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index 96eb98703..9c926508a 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -42,7 +42,6 @@ import com.introproventures.graphql.jpa.query.AbstractSpringBootTestSupport; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; - import graphql.ErrorType; import graphql.ExecutionResult; import graphql.GraphQLError; @@ -52,7 +51,7 @@ @SpringBootTest public class GraphQLExecutorTests extends AbstractSpringBootTestSupport { - + @SpringBootApplication static class Application { @Bean @@ -62,14 +61,14 @@ public GraphQLExecutor graphQLExecutor(final GraphQLSchemaBuilder graphQLSchemaB @Bean public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManager) { - + return new GraphQLJpaSchemaBuilder(entityManager) .name("GraphQLBooks") .description("Books JPA test schema"); } - + } - + @Autowired private GraphQLExecutor executor; @@ -77,12 +76,12 @@ public GraphQLSchemaBuilder graphQLSchemaBuilder(final EntityManager entityManag public static void init() { TimeZone.setDefault(TimeZone.getTimeZone("UTC")); } - + @Test public void contextLoads() { Assert.isAssignable(GraphQLExecutor.class, executor.getClass()); } - + @Test public void GetsAllThings() { //given @@ -94,14 +93,14 @@ public void GetsAllThings() { //then assertThat(result.toString()).isEqualTo(expected); - + } @Test public void queryForThingById() { //given String query = "query ThingByIdQuery { Thing(id: \"2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1\") { id type } }"; - + String expected = "{Thing={id=2d1ebc5b-7d27-4197-9cf0-e84451c5bbb1, type=Thing1}}"; //when @@ -139,7 +138,7 @@ public void queryEmptyParameter() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @SuppressWarnings( { "rawtypes", "unchecked", "serial" } ) @Test public void queryWithParameterNoResult() { @@ -221,14 +220,14 @@ public void queryWithAlias() { //then: assertThat(result.toString()).isEqualTo(expected); } - - + + // https://github.com/introproventures/graphql-jpa-query/issues/33 @Test public void queryForElementCollection() { //given String query = "{ Author(id: 1) { id name, phoneNumbers } }"; - + String expected = "{Author={id=1, name=Leo Tolstoy, phoneNumbers=[1-123-1234, 1-123-5678]}}"; //when @@ -242,7 +241,7 @@ public void queryForElementCollection() { public void queryForEnumIn() { //given String query = "{ Books(where: {genre: {IN: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " @@ -255,12 +254,12 @@ public void queryForEnumIn() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumInArray() { //given String query = "{ Books(where: {genre: {IN: [NOVEL, PLAY]}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}, " @@ -280,7 +279,7 @@ public void queryForEnumInArray() { public void queryForEnumNinArray() { //given String query = "{ Books(where: {genre: {NIN: [NOVEL]}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " + "{id=6, title=The Seagull, genre=PLAY}, " @@ -293,12 +292,12 @@ public void queryForEnumNinArray() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumEq() { //given String query = "{ Books(where: {genre: {EQ: NOVEL}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -315,7 +314,7 @@ public void queryForEnumEq() { public void queryForEnumNe() { //given String query = "{ Books(where: {genre: {NE: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -327,12 +326,12 @@ public void queryForEnumNe() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForEnumNin() { //given String query = "{ Books(where: {genre: {NIN: PLAY}}) { select { id title, genre } }}"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, genre=NOVEL}, " + "{id=3, title=Anna Karenina, genre=NOVEL}" @@ -344,12 +343,12 @@ public void queryForEnumNin() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForParentWithEnum() { //given String query = "{ Books { select { id title, author( where: { genre: { EQ: NOVEL } }) { name } } } }"; - + String expected = "{Books={select=[" + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}" @@ -361,32 +360,33 @@ public void queryForParentWithEnum() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryAuthorBooksWithExplictOptional() { //given String query = "query { " - + "Authors(" + - " where: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title(orderBy: ASC)" + - " genre" + - " }" + - " }" + + + "Authors(" + + " where: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + + "{id=3, title=Anna Karenina, genre=NOVEL}, " + "{id=2, title=War and Peace, genre=NOVEL}]}" + "]}}"; @@ -396,32 +396,32 @@ public void queryAuthorBooksWithExplictOptional() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryAuthorBooksWithExplictOptionalEXISTS() { //given String query = "query { " - + "Authors(" + + + "Authors(" + " where: {" + " EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title(orderBy: ASC)" + - " genre" + - " }" + - " }" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title(orderBy: ASC)" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[{id=3, title=Anna Karenina, genre=NOVEL}, " + "{id=2, title=War and Peace, genre=NOVEL}]}" @@ -470,29 +470,56 @@ public void queryAuthorBooksWithCollectionOrderBy() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryBooksAuthorWithImplicitOptionalFalse() { + //given + String query = "query { " + + "Books {" + + " select {" + + " id" + + " title" + + " author(where: {name: {LIKE: \"Leo\"}}) {" + + " name" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithIsNullId() { //given String query = "query { " - + "Authors(" + - " where: {" + - " books: {" + - " id: {IS_NULL: true}" + - " }" + - " }" + - " ) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(" + + " where: {" + + " books: {" + + " id: {IS_NULL: true}" + + " }" + + " }" + + " ) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; - + String expected = "{Authors={select=[{id=8, name=Igor Dianov, books=[]}]}}"; //when @@ -501,14 +528,43 @@ public void queryAuthorBooksWithIsNullId() { // then assertThat(result.toString()).isEqualTo(expected); } - - + + @Test + public void queryBooksAuthorWithExplictOptionalTrue() { + //given + String query = "query { " + + "Books {" + + " select {" + + " id" + + " title" + + " author(optional: true, where: {name: {LIKE: \"Leo\"}}) {" + + " name" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, author={name=Leo Tolstoy}}, " + + "{id=3, title=Anna Karenina, author={name=Leo Tolstoy}}, " + + "{id=5, title=The Cherry Orchard, author=null}, " + + "{id=6, title=The Seagull, author=null}, " + + "{id=7, title=Three Sisters, author=null}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + // https://github.com/introproventures/graphql-jpa-query/issues/30 @Test public void queryForEntityWithMappedSuperclass() { //given String query = "{ Car(id: \"1\") { id brand } }"; - + String expected = "{Car={id=1, brand=Ford}}"; //when @@ -523,7 +579,7 @@ public void queryForEntityWithMappedSuperclass() { public void queryForEntityWithEmbeddedIdAndEmbeddedField() { //given String query = "{ Boat(boatId: {id: \"1\" country: \"EN\"}) { boatId {id country} engine { identification } } }"; - + String expected = "{Boat={boatId={id=1, country=EN}, engine={identification=12345}}}"; //when @@ -546,7 +602,7 @@ public void queryForEntityWithEmbeddedFieldWithWhere() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNumericBetweenPredicate() { //given: @@ -615,8 +671,8 @@ public void queryWithDateNotBetweenPredicate() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForEntitiesWithWithEmbeddedIdWithWhere() { @@ -631,21 +687,21 @@ public void queryForEntitiesWithWithEmbeddedIdWithWhere() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForBooksWithWhereAuthorById() { //given String query = "query { " - + "Books(where: {author: {id: {EQ: 1}}}) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " }" + - " }" + + + "Books(where: {author: {id: {EQ: 1}}}) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " }" + + " }" + " }"+ "}"; @@ -659,20 +715,20 @@ public void queryForBooksWithWhereAuthorById() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForBooksWithWhereAuthorEqIdWithVariables() { //given String query = "query($authorId: Long ) { " - + " Books(where: {" + - " author: {id: {EQ: $authorId}}" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " }" + + + " Books(where: {" + + " author: {id: {EQ: $authorId}}" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " }" + " }"+ "}"; Map variables = new HashMap() {{ @@ -690,27 +746,27 @@ public void queryForBooksWithWhereAuthorEqIdWithVariables() { // then assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitle() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -726,28 +782,28 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitle() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDAuthorLIKEName() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " author: {name: {LIKE: \"Leo\"}}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " author: {name: {LIKE: \"Leo\"}}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -763,31 +819,31 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDAuthorLIKEName() { // then assertThat(result.toString()).isEqualTo(expected); - } + } + - @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDEXISTSAuthorLIKEName() { //given String query = "query { " - + " Authors(where: {" + - " EXISTS: {" + - " books: {" + - " EXISTS: {" + - " author: {name: {LIKE: \"Leo\"}} " + - " }" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + " Authors(where: {" + + " EXISTS: {" + + " books: {" + + " EXISTS: {" + + " author: {name: {LIKE: \"Leo\"}} " + + " }" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -803,28 +859,28 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleANDEXISTSAuthorLIKEName( // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorsWithWhereEXISTSBooksLIKETitleEmpty() { //given String query = "query { " - + "Authors(where: {" + - " EXISTS: {" + - " books: {" + - " author: {name: {LIKE: \"Anton\"}}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " author: {name: {LIKE: \"Anton\"}}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -836,25 +892,25 @@ public void queryForAuthorsWithWhereEXISTSBooksLIKETitleEmpty() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithWhereNOTEXISTSBooksLIKETitleWar() { //given String query = "query { " - + "Authors(where: {" + - " NOT_EXISTS: {" + - " books: {" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + + + "Authors(where: {" + + " NOT_EXISTS: {" + + " books: {" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + " }"+ " }"+ "}"; @@ -872,29 +928,29 @@ public void queryForAuthorsWithWhereNOTEXISTSBooksLIKETitleWar() { // then assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { //given String query = "query { " - + " Authors(where: {" + - " books: {" + - " NOT_EXISTS: {" + - " author: {" + - " name: {LIKE: \"Leo\"}" + - " }" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " }" + - " }" + + + " Authors(where: {" + + " books: {" + + " NOT_EXISTS: {" + + " author: {" + + " name: {LIKE: \"Leo\"}" + + " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " }" + + " }" + " }"+ "}"; @@ -992,16 +1048,16 @@ public void queryTotalForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { public void queryForAuthorssWithWhereBooksGenreEquals() { //given String query = "query { " - + "Authors(where: {books: {genre: {EQ: NOVEL}}}) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(where: {books: {genre: {EQ: NOVEL}}}) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1017,31 +1073,31 @@ public void queryForAuthorssWithWhereBooksGenreEquals() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryForAuthorssWithWhereBooksManyToOneRelationCriteria() { //given String query = "query { " + - " Authors(where: {" + - " books: {" + - " author: {" + - " name: {LIKE: \"Leo\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author {" + - " name" + - " }" + - " }" + - " }" + + " Authors(where: {" + + " books: {" + + " author: {" + + " name: {LIKE: \"Leo\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author {" + + " name" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1057,28 +1113,28 @@ public void queryForAuthorssWithWhereBooksManyToOneRelationCriteria() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsImplicitAND() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + - " genre: {IN: NOVEL}" + - " title: {LIKE: \"War\"}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors(where: {" + + " books: {" + + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1095,29 +1151,29 @@ public void queryWithWhereInsideOneToManyRelationsImplicitAND() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideOneToManyRelationsImplicitANDWithEXISTS() { //given: String query = "query { " - + "Authors(where: {" + + + "Authors(where: {" + " EXISTS: {" + " books: {" + - " genre: {IN: NOVEL}" + - " title: {LIKE: \"War\"}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1135,29 +1191,29 @@ public void queryWithWhereInsideOneToManyRelationsImplicitANDWithEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + + + "Authors(where: {" + + " books: {" + " AND: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1172,31 +1228,31 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { //given: String query = "query { " + "Authors(where: {" + " EXISTS: {" + - " books: {" + + " books: {" + " AND: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1211,29 +1267,29 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyRelationsWithExplictOR() { //given: String query = "query { " - + "Authors(where: {" + - " books: {" + + + "Authors(where: {" + + " books: {" + " OR: { "+ - " genre: {IN: NOVEL}" + + " genre: {IN: NOVEL}" + " title: {LIKE: \"War\"}" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1247,32 +1303,32 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictOR() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyNestedRelationsWithManyToOneAndOR() { //given: String query = "query { " + - " Authors(where: {" + - " books: {" + + " Authors(where: {" + + " books: {" + " author: {name: {LIKE:\"Leo\"}}" + " AND: {" + - " OR: {" + - " id: {EQ: 2}" + - " title: {LIKE: \"Anna\"}" + - " }" + + " OR: {" + + " id: {EQ: 2}" + + " title: {LIKE: \"Anna\"}" + + " }" + " }" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }" + "}"; @@ -1288,29 +1344,29 @@ public void queryWithWhereInsideOneToManyNestedRelationsWithManyToOneAndOR() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideOneToManyNestedRelationsWithOneToManyDeepSelect() { //given: String query = "query { " + - " Authors(where: {" + - " books: {" + - " author: {name: {LIKE:\"Leo\"}}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author {" + - " name" + - " }" + - " }" + - " }" + + " Authors(where: {" + + " books: {" + + " author: {name: {LIKE:\"Leo\"}}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author {" + + " name" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1326,34 +1382,34 @@ public void queryWithWhereInsideOneToManyNestedRelationsWithOneToManyDeepSelect( //then: assertThat(result.toString()).isEqualTo(expected); - } - - + } + + @Test public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFilter() { //given: String query = "query { " + - " Books(where: {" + - " title:{LIKE: \"War\"}" + - " author: {" + - " name:{LIKE: \"Leo\"}" + - " books: {title: {LIKE: \"Anna\"}}" + - " }" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + " Books(where: {" + + " title:{LIKE: \"War\"}" + + " author: {" + + " name:{LIKE: \"Leo\"}" + + " books: {title: {LIKE: \"Anna\"}}" + + " }" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1373,35 +1429,35 @@ public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFi //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFilterEXISTS() { //given: String query = "query { " + - " Books(where: {" + - " title:{LIKE: \"War\"}" + + " Books(where: {" + + " title:{LIKE: \"War\"}" + " EXISTS: {" + - " author: {" + - " name:{LIKE: \"Leo\"}" + - " books: {title: {LIKE: \"Anna\"}}" + - " }" + - " }" + - " }) {" + - " select {" + - " id" + - " title" + - " genre" + - " author {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + " author: {" + + " name:{LIKE: \"Leo\"}" + + " books: {title: {LIKE: \"Anna\"}}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " title" + + " genre" + + " author {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1422,25 +1478,25 @@ public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFi //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryWithOneToManyNestedRelationsWithManyToOneOptionalTrue() { //given: String query = "query { " + - " Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author(optional: true) {" + - " id" + - " }" + - " }" + - " }" + + " Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author(optional: true) {" + + " id" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1462,25 +1518,25 @@ public void queryWithOneToManyNestedRelationsWithManyToOneOptionalTrue() { //then: assertThat(result.toString()).isEqualTo(expected); - } + } @Test public void queryWithOneToManyNestedRelationsWithManyToOneOptionalFalse() { //given: String query = "query { " + - " Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " author(optional: false) {" + - " id" + - " }" + - " }" + - " }" + + " Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " author(optional: false) {" + + " id" + + " }" + + " }" + + " }" + " }" + "}"; @@ -1501,8 +1557,8 @@ public void queryWithOneToManyNestedRelationsWithManyToOneOptionalFalse() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void ignoreFilter() { //given @@ -1590,21 +1646,21 @@ public void titleOrder() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithDefaultOptionalBooks() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1627,21 +1683,21 @@ public void queryForAuthorsWithDefaultOptionalBooks() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithExlicitOptionalBooksFalse() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books(optional: false) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books(optional: false) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1663,21 +1719,21 @@ public void queryForAuthorsWithExlicitOptionalBooksFalse() { // then assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryForAuthorsWithExlicitOptionalBooksTrue() { //given String query = "query { " - + "Authors {" + - " select {" + - " id" + - " name" + - " books(optional: true) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + + + "Authors {" + + " select {" + + " id" + + " name" + + " books(optional: true) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + " }"+ "}"; @@ -1722,7 +1778,7 @@ public void queryForTransientMethodAnnotatedWithGraphQLIgnoreShouldFail() { .extracting("validationErrorType", "queryPath") .containsOnly(tuple(ValidationErrorType.FieldUndefined, list("Books", "select", "authorName"))); } - + @Test public void queryWithEQNotMatchingCase() { //given: @@ -1736,7 +1792,7 @@ public void queryWithEQNotMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithEQMatchingCase() { //given: @@ -1752,7 +1808,7 @@ public void queryWithEQMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLOWERNotMatchingCase() { //given: @@ -1768,7 +1824,7 @@ public void queryWithLOWERNotMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLOWERMatchingCase() { //given: @@ -1784,7 +1840,7 @@ public void queryWithLOWERMatchingCase() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithEQCaseInsensitive() { //given: @@ -1830,7 +1886,7 @@ public void queryWithEQCaseSensitiveNotMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNECaseInsensitive() { //given: @@ -1879,7 +1935,7 @@ public void queryWithNECaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithLIKECaseInsensitive() { //given: @@ -1925,7 +1981,7 @@ public void queryWithLIKECaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithSTARTSCaseInsensitive() { //given: @@ -1971,7 +2027,7 @@ public void queryWithSTARTSCaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithENDSCaseInsensitive() { //given: @@ -2002,7 +2058,7 @@ public void queryWithENDSCaseSensitive() { //then: assertThat(result.toString()).isEqualTo(expected); } - + public void queryWithENDSCaseSensitiveNonMatching() { //given: String query = "query { Books ( where: { title: {ENDS : \"peace\"}}) { select { id title} } }"; @@ -2014,32 +2070,32 @@ public void queryWithENDSCaseSensitiveNonMatching() { //then: assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriteriaExpression() { //given: - String query = "query ($genre: Genre) {" + - " Authors(where: { " + - " books: {" + - " genre: {EQ: $genre}" + - " }" + - " }) {" + - " select {" + - " id" + - " name" + - " books {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + String query = "query ($genre: Genre) {" + + " Authors(where: { " + + " books: {" + + " genre: {EQ: $genre}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + "}"; //when: 1st query Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); - + String expected1 = "{Authors={select=[" + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard, genre=PLAY}, " @@ -2050,10 +2106,10 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriter //then: assertThat(result1.toString()).isEqualTo(expected1); - + //when: 2nd query Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); - + String expected2 = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " @@ -2064,27 +2120,27 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForCollectionCriter //then: assertThat(result2.toString()).isEqualTo(expected2); } - + @Test public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteriaExpression() { //given: - String query = "query ($genre: Genre) {" + - " Authors {" + - " select {" + - " id" + - " name" + - " books(where:{ genre: {EQ: $genre} }) {" + - " id" + - " title" + - " genre" + - " }" + - " }" + - " }" + + String query = "query ($genre: Genre) {" + + " Authors {" + + " select {" + + " id" + + " name" + + " books(where:{ genre: {EQ: $genre} }) {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + "}"; //when: 1st query Object result1 = executor.execute(query, Collections.singletonMap("genre", "PLAY")).getData(); - + String expected1 = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[]}, " + "{id=4, name=Anton Chekhov, books=[" @@ -2097,10 +2153,10 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteria //then: assertThat(result1.toString()).isEqualTo(expected1); - + //when: 2nd query Object result2 = executor.execute(query, Collections.singletonMap("genre", "NOVEL")).getData(); - + String expected2 = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " @@ -2112,7 +2168,7 @@ public void shouldNotReturnStaleCacheResultsFromPreviousQueryForEmbeddedCriteria //then: assertThat(result2.toString()).isEqualTo(expected2); - } + } @Test public void queryWithEnumParameterShouldExecuteWithNoError() { @@ -2155,13 +2211,13 @@ public void queryWithEnumParameterShouldExecuteWithNoError() { "War and Peace") ); } - + // https://github.com/introproventures/graphql-jpa-query/issues/198 @Test public void queryOptionalElementCollections() { //given String query = "{ Author(id: 8) { id name phoneNumbers books { id title tags } } }"; - + String expected = "{Author={id=8, name=Igor Dianov, phoneNumbers=[], books=[]}}"; //when @@ -2169,19 +2225,19 @@ public void queryOptionalElementCollections() { // then assertThat(result.toString()).isEqualTo(expected); - } - + } + @Test public void queryElementCollectionsWithWhereCriteriaExpression() { //given: - String query = "query {" + - " Books(where: {tags: {EQ: \"war\"}}) {" + - " select {" + - " id" + - " title" + - " tags" + - " }" + - " }" + + String query = "query {" + + " Books(where: {tags: {EQ: \"war\"}}) {" + + " select {" + + " id" + + " title" + + " tags" + + " }" + + " }" + "}"; String expected = "{Books={select=[{id=2, title=War and Peace, tags=[piece, war]}]}}"; @@ -2192,12 +2248,12 @@ public void queryElementCollectionsWithWhereCriteriaExpression() { //then: assertThat(result.toString()).isEqualTo(expected); } - + @Test public void queryWithNullVarables() { //given String query = "{ Author(id: 1) { id name } }"; - + String expected = "{Author={id=1, name=Leo Tolstoy}}"; //when @@ -2206,5 +2262,5 @@ public void queryWithNullVarables() { // then assertThat(result.toString()).isEqualTo(expected); } - + } \ No newline at end of file diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java index 2e2013b7a..f67d6eeb0 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/StarwarsQueryExecutorTests.java @@ -1964,4 +1964,158 @@ public void queryEmbeddedWhereWithManyToManyAssociationsUsingEXISTS() { //then: assertThat(result.toString()).isEqualTo(expected); } + + @Test + public void queryWithInLineExplicitOptionalFalseForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional: false, where:{name:{LIKE:\"R%\"}}) {" + + " name" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2}}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineExplicitOptionalTrueForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional:true, where:{name:{LIKE:\"R%\"}}) {" + + " name" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid=null}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2}}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineImplictOptionalTrueForSingularAttribute() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2, primaryFunction={function=Astromech}}}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineImplictOptionalTrueForSingularAttributeAndWhereSearchCriteria() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(where: {primaryFunction: {function: {EQ: \"Protocol\"}}}) {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}, " + + "{id=1001, name=Darth Vader, favoriteDroid=null}, " + + "{id=1002, name=Han Solo, favoriteDroid=null}, " + + "{id=1003, name=Leia Organa, favoriteDroid=null}, " + + "{id=1004, name=Wilhuff Tarkin, favoriteDroid=null}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithInLineExplicitOptionalFalseForSingularAttributeAndWhereSearchCriteria() { + + //given: + String query = "{" + + " Humans {" + + " select {" + + " id" + + " name" + + " favoriteDroid(optional: false, where: {primaryFunction: {function: {EQ: \"Protocol\"}}}) {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, primaryFunction={function=Protocol}}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + } From c9960f28a0562ad84944a585c40a22cd4c2dc0a4 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 16:03:19 -0700 Subject: [PATCH 14/16] fix: add support for inline collections filter with alias --- .../impl/GraphQLJpaExecutorContext.java | 6 +- .../impl/GraphQLJpaOneToManyDataFetcher.java | 27 ++++++++- .../impl/GraphQLJpaToOneDataFetcher.java | 25 +++++++- .../query/schema/GraphQLExecutorTests.java | 60 +++++++++++++++++++ 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java index 1afed04c4..dae052159 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaExecutorContext.java @@ -63,9 +63,13 @@ private GraphQLJpaExecutorContext(Builder builder) { public ExecutionInput.Builder newExecutionInput() { DataLoaderRegistry dataLoaderRegistry = newDataLoaderRegistry(); + GraphQLContext context = graphqlContext.get(); + + context.put("dataLoaderRegistry", dataLoaderRegistry); + return executionInputFactory.create() .dataLoaderRegistry(dataLoaderRegistry) - .context(graphqlContext.get()); + .context(context); } @Override diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java index 762f288ea..12baf1164 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java @@ -22,8 +22,12 @@ import javax.persistence.metamodel.PluralAttribute; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; import com.introproventures.graphql.jpa.query.support.GraphQLSupport; +import graphql.GraphQLContext; import graphql.language.Argument; import graphql.language.Field; import graphql.schema.DataFetcher; @@ -58,9 +62,28 @@ public Object get(DataFetchingEnvironment environment) { // Resolve collection query if where argument is present or any field in selection has orderBy argument if (whereArg.isPresent() || queryFactory.hasAnySelectionOrderBy(field)) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); - String dataLoaderKey = parentType.getName() + "." + attribute.getName(); + String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) + .orElseGet(attribute::getName); + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); - DataLoader> dataLoader = environment.getDataLoader(dataLoaderKey); + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(queryFactory); + + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); + + DataLoader> dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } + + } + + DataLoader> dataLoader = dataLoaderRegistry.getDataLoader(dataLoaderKey); return dataLoader.load(parentIdValue, environment); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java index 9e4c3ff3d..c8166fc4b 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java @@ -22,8 +22,11 @@ import javax.persistence.metamodel.SingularAttribute; import org.dataloader.DataLoader; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.dataloader.MappedBatchLoaderWithContext; -import graphql.language.Argument; +import graphql.GraphQLContext; import graphql.language.Field; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; @@ -58,8 +61,26 @@ public Object get(DataFetchingEnvironment environment) { // Resolve collection query if where argument is present if (isOptional && !PersistentAttributeType.EMBEDDED.equals(attribute.getPersistentAttributeType())) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); - String dataLoaderKey = parentType.getName() + "." + attribute.getName(); + String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) + .orElseGet(attribute::getName); + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(queryFactory); + + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); + + DataLoader dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } + + } DataLoader dataLoader = environment.getDataLoader(dataLoaderKey); return dataLoader.load(parentIdValue, environment); diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java index 9c926508a..b2f621ec7 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorTests.java @@ -497,6 +497,66 @@ public void queryBooksAuthorWithImplicitOptionalFalse() { assertThat(result.toString()).isEqualTo(expected); } + @Test + public void queryAuthorBooksByAlliasesWithInlineCollections() { + //given + String query = "query { " + + " Authors {" + + " select {" + + " id" + + " name" + + " War: books(where: {title: {LIKE: \"War\"}}) {" + + " title" + + " }" + + " Anna: books(where: {title: {LIKE: \"Anna\"}}) {" + + " title" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, War=[{title=War and Peace}], Anna=[{title=Anna Karenina}]}, " + + "{id=4, name=Anton Chekhov, War=[], Anna=[]}, " + + "{id=8, name=Igor Dianov, War=[], Anna=[]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryAuthorBooksByAlliasesWithInlineWhereSearch() { + //given + String query = "query { " + + " Authors(where: {name: {LIKE: \"Leo\"}}) {" + + " select {" + + " id" + + " name" + + " War: books(where: {title: {LIKE: \"War\"}}) {" + + " title" + + " }" + + " Anna: books(where: {title: {LIKE: \"Anna\"}}) {" + + " title" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, War=[{title=War and Peace}], Anna=[{title=Anna Karenina}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + @Test public void queryAuthorBooksWithIsNullId() { //given From bff6145fe09821f7951c4e639f24c6a891a506c0 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 16:05:07 -0700 Subject: [PATCH 15/16] fix: remove hibernate-core dependency --- graphql-jpa-query-schema/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/graphql-jpa-query-schema/pom.xml b/graphql-jpa-query-schema/pom.xml index 1b6253f97..6695d9e19 100644 --- a/graphql-jpa-query-schema/pom.xml +++ b/graphql-jpa-query-schema/pom.xml @@ -44,12 +44,6 @@ true - - org.hibernate - hibernate-core - true - - javax.persistence javax.persistence-api From a2741f38fcdb03b573d244fba9bfd32012b4e032 Mon Sep 17 00:00:00 2001 From: Igor Dianov Date: Mon, 6 Apr 2020 19:50:37 -0700 Subject: [PATCH 16/16] fix: polish data loader data fetchers --- .../schema/impl/GraphQLJpaSchemaBuilder.java | 4 +- ....java => GraphQLJpaToManyDataFetcher.java} | 45 ++++++++++--------- ...=> GraphQLJpaToManyMappedBatchLoader.java} | 4 +- .../impl/GraphQLJpaToOneDataFetcher.java | 42 +++++++++-------- 4 files changed, 53 insertions(+), 42 deletions(-) rename graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/{GraphQLJpaOneToManyDataFetcher.java => GraphQLJpaToManyDataFetcher.java} (68%) rename graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/{GraphQLJpaOneToManyMappedBatchLoader.java => GraphQLJpaToManyMappedBatchLoader.java} (84%) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java index 24c694350..a1a1a102c 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaSchemaBuilder.java @@ -950,12 +950,12 @@ else if (attribute instanceof PluralAttribute String dataLoaderKey = baseEntity.getName() + "." + attribute.getName(); - MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(graphQLJpaQueryFactory); + MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaToManyMappedBatchLoader(graphQLJpaQueryFactory); batchLoadersRegistry.get() .registerToMany(dataLoaderKey, mappedBatchLoader); - dataFetcher = new GraphQLJpaOneToManyDataFetcher(graphQLJpaQueryFactory, + dataFetcher = new GraphQLJpaToManyDataFetcher(graphQLJpaQueryFactory, (PluralAttribute) attribute); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java similarity index 68% rename from graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java rename to graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java index 12baf1164..1ce48fa45 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyDataFetcher.java @@ -40,12 +40,12 @@ * @author Igor Dianov * */ -class GraphQLJpaOneToManyDataFetcher implements DataFetcher { +class GraphQLJpaToManyDataFetcher implements DataFetcher { private final PluralAttribute attribute; private final GraphQLJpaQueryFactory queryFactory; - public GraphQLJpaOneToManyDataFetcher(GraphQLJpaQueryFactory queryFactory, + public GraphQLJpaToManyDataFetcher(GraphQLJpaQueryFactory queryFactory, PluralAttribute attribute) { this.queryFactory = queryFactory; this.attribute = attribute; @@ -64,33 +64,38 @@ public Object get(DataFetchingEnvironment environment) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) .orElseGet(attribute::getName); - GraphQLContext context = environment.getContext(); - DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); - if (!dataLoaderRegistry.getKeys() - .contains(dataLoaderKey)) { - synchronized (dataLoaderRegistry) { - MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaOneToManyMappedBatchLoader(queryFactory); + DataLoader> dataLoader = getDataLoader(environment, + dataLoaderKey); - DataLoaderOptions options = DataLoaderOptions.newOptions() - .setCachingEnabled(false); + return dataLoader.load(parentIdValue, environment); + } - DataLoader> dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, - options); + // Let hibernate resolve collection query + return queryFactory.getAttributeValue(source, + attribute); + } - dataLoaderRegistry.register(dataLoaderKey, dataLoader); - } + protected DataLoader> getDataLoader(DataFetchingEnvironment environment, + String dataLoaderKey) { + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); - } + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext> mappedBatchLoader = new GraphQLJpaToManyMappedBatchLoader(queryFactory); - DataLoader> dataLoader = dataLoaderRegistry.getDataLoader(dataLoaderKey); + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); - return dataLoader.load(parentIdValue, environment); + DataLoader> dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } } - // Let hibernate resolve collection query - return queryFactory.getAttributeValue(source, - attribute); + return dataLoaderRegistry.getDataLoader(dataLoaderKey); } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java similarity index 84% rename from graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java rename to graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java index 11e02d201..279fdde66 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaOneToManyMappedBatchLoader.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToManyMappedBatchLoader.java @@ -12,11 +12,11 @@ import graphql.schema.DataFetchingEnvironment; // a batch loader function that will be called with N or more keys for batch loading -class GraphQLJpaOneToManyMappedBatchLoader implements MappedBatchLoaderWithContext> { +class GraphQLJpaToManyMappedBatchLoader implements MappedBatchLoaderWithContext> { private final GraphQLJpaQueryFactory queryFactory; - public GraphQLJpaOneToManyMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { + public GraphQLJpaToManyMappedBatchLoader(GraphQLJpaQueryFactory queryFactory) { this.queryFactory = queryFactory; } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java index c8166fc4b..ded77cbc6 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaToOneDataFetcher.java @@ -63,25 +63,9 @@ public Object get(DataFetchingEnvironment environment) { Object parentIdValue = queryFactory.getParentIdAttributeValue(source); String dataLoaderKey = parentType.getName() + "." + Optional.ofNullable(field.getAlias()) .orElseGet(attribute::getName); - GraphQLContext context = environment.getContext(); - DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); - if (!dataLoaderRegistry.getKeys() - .contains(dataLoaderKey)) { - synchronized (dataLoaderRegistry) { - MappedBatchLoaderWithContext mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(queryFactory); - - DataLoaderOptions options = DataLoaderOptions.newOptions() - .setCachingEnabled(false); - - DataLoader dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, - options); - - dataLoaderRegistry.register(dataLoaderKey, dataLoader); - } - - } - DataLoader dataLoader = environment.getDataLoader(dataLoaderKey); + DataLoader dataLoader = getDataLoader(environment, + dataLoaderKey); return dataLoader.load(parentIdValue, environment); } @@ -91,4 +75,26 @@ public Object get(DataFetchingEnvironment environment) { attribute); } + protected DataLoader getDataLoader(DataFetchingEnvironment environment, + String dataLoaderKey) { + GraphQLContext context = environment.getContext(); + DataLoaderRegistry dataLoaderRegistry = context.get("dataLoaderRegistry"); + + if (!dataLoaderRegistry.getKeys() + .contains(dataLoaderKey)) { + synchronized (dataLoaderRegistry) { + MappedBatchLoaderWithContext mappedBatchLoader = new GraphQLJpaToOneMappedBatchLoader(queryFactory); + + DataLoaderOptions options = DataLoaderOptions.newOptions() + .setCachingEnabled(false); + + DataLoader dataLoader = DataLoader.newMappedDataLoader(mappedBatchLoader, + options); + dataLoaderRegistry.register(dataLoaderKey, dataLoader); + } + } + + return environment.getDataLoader(dataLoaderKey); + } + }