diff --git a/graphql-jpa-query-example-model-starwars/src/main/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Character.java b/graphql-jpa-query-example-model-starwars/src/main/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Character.java index 4188b5307..bc5ea35a9 100644 --- a/graphql-jpa-query-example-model-starwars/src/main/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Character.java +++ b/graphql-jpa-query-example-model-starwars/src/main/java/com/introproventures/graphql/jpa/query/schema/model/starwars/Character.java @@ -30,6 +30,7 @@ import javax.persistence.OrderBy; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -40,7 +41,7 @@ @Getter @Setter @ToString -@EqualsAndHashCode(exclude={"appearsIn","friends"}) // Fixes NPE in Hibernate when initializing loaded collections #1 +@EqualsAndHashCode(exclude={"appearsIn","friends", "friendsOf"}) // Fixes NPE in Hibernate when initializing loaded collections #1 public abstract class Character { @Id @@ -58,6 +59,10 @@ public abstract class Character { @OrderBy("name ASC") Set friends; + @ManyToMany(fetch=FetchType.LAZY, mappedBy = "friends") + @OrderBy("name ASC") + Set friendsOf; + @GraphQLDescription("What Star Wars episodes does this character appear in") @ElementCollection(targetClass = Episode.class, fetch=FetchType.LAZY) @Enumerated(EnumType.ORDINAL) 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 da08f9079..1814dd03a 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 @@ -332,7 +332,7 @@ private GraphQLInputObjectType computeSubqueryInputType(ManagedType managedTy .build() ) .fields(managedType.getAttributes().stream() - .filter(this::isValidAssociation) + .filter(Attribute::isAssociation) .filter(this::isNotIgnored) .filter(this::isNotIgnoredFilter) .map(this::getWhereInputRelationField) @@ -404,7 +404,7 @@ private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) .collect(Collectors.toList()) ) .fields(managedType.getAttributes().stream() - .filter(this::isValidAssociation) + .filter(Attribute::isAssociation) .filter(this::isNotIgnored) .filter(this::isNotIgnoredFilter) .map(this::getWhereInputRelationField) @@ -853,7 +853,9 @@ else if (isElementCollection(attribute)) { Type foreignType = ((PluralAttribute) attribute).getElementType(); if(foreignType.getPersistenceType() == Type.PersistenceType.BASIC) { - return new GraphQLList(getGraphQLTypeFromJavaType(foreignType.getJavaType())); + GraphQLType graphQLType = getGraphQLTypeFromJavaType(foreignType.getJavaType()); + + return input ? graphQLType : new GraphQLList(graphQLType); } } @@ -897,11 +899,6 @@ protected final boolean isValidInput(Attribute attribute) { attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED; } - protected final boolean isValidAssociation(Attribute attribute) { - return isOneToMany(attribute) || isToOne(attribute); - } - - private String getSchemaDescription(Attribute attribute) { return EntityIntrospector.introspect(attribute.getDeclaringType()) .getSchemaDescription(attribute.getName()) diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java index 2bf9b9ab2..21556930d 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/JpaPredicateBuilder.java @@ -30,6 +30,7 @@ import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -37,7 +38,6 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Expression; import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.metamodel.EntityType; @@ -730,27 +730,21 @@ else if(Collection.class.isAssignableFrom(type)) { // collection join for plural attributes if(PluralAttribute.class.isInstance(from.getModel()) || EntityType.class.isInstance(from.getModel())) { + Expression> expression = from.get(filter.getField()); Predicate predicate; - - if(EntityType.class.isInstance(from.getModel())) { - From join = null; - for(Join fetch: from.getJoins()) { - if(fetch.getAttribute().getName().equals(filter.getField())) - join = (From) fetch; - } + if(Collection.class.isInstance(filter.getValue())) { + List restrictions = new ArrayList<>(); - if(join == null) { - join = (From) from.join(filter.getField()); - } + Collection.class.cast(filter.getValue()) + .forEach(v -> restrictions.add(cb.isMember(v, expression))); + + predicate = cb.and(restrictions.toArray(new Predicate[] {})); - predicate = join.in(value); } else { - Expression> expression = from.get(filter.getField()); - predicate = cb.isMember(filter.getValue(), expression); } - + if(filter.anyMatch(Criteria.NIN, Criteria.NE)) { return cb.not(predicate); } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java index 717992ad1..3949ace24 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/QraphQLJpaBaseDataFetcher.java @@ -52,6 +52,7 @@ import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.persistence.criteria.Selection; import javax.persistence.criteria.Subquery; import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.Attribute.PersistentAttributeType; @@ -471,61 +472,90 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From from, Map args = getFieldArguments(environment, it, argument); Argument arg = new Argument(it.getName(), it.getValue()); - if(isEntityType(environment)) { - Attribute attribute = getAttribute(environment, arg); - - if(attribute.isAssociation()) { - GraphQLFieldDefinition fieldDefinition = getFieldDef(environment.getGraphQLSchema(), - this.getObjectType(environment), - new Field(it.getName())); - - Boolean isOptional = false; - - 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(it.getName()); - - DataFetchingEnvironment existsEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) - .root(subquery) - .build(); - - Predicate restriction = getArgumentPredicate(cb, - correlationJoin, - wherePredicateEnvironment(existsEnvironment, fieldDefinition, args), - arg); - - - Predicate exists = cb.exists(subquery.select((Join) correlationJoin) - .where(restriction)); - - return logical == Logical.EXISTS ? exists : cb.not(exists); - } - - return getArgumentPredicate(cb, reuseJoin(from, it.getName(), isOptional), - wherePredicateEnvironment(environment, fieldDefinition, args), - arg); - } - } - - return getLogicalPredicate(it.getName(), - cb, - from, - it, - argumentEnvironment(environment, args), - arg); + return getObjectFieldPredicate(environment, cb, from, logical, it, arg, args); }) .filter(predicate -> predicate != null) .forEach(predicates::add); return getCompoundPredicate(cb, predicates, logical); } + + + protected Predicate getObjectFieldPredicate(DataFetchingEnvironment environment, + CriteriaBuilder cb, + From from, + Logical logical, + ObjectField it, + Argument arg, + Map args + ) { + if(isEntityType(environment)) { + Attribute attribute = getAttribute(environment, arg); + + if(attribute.isAssociation()) { + GraphQLFieldDefinition fieldDefinition = getFieldDef(environment.getGraphQLSchema(), + this.getObjectType(environment), + new Field(it.getName())); + + Boolean isOptional = false; + + 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(it.getName()); + + DataFetchingEnvironment existsEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .root(subquery) + .build(); + + Predicate restriction = getArgumentPredicate(cb, + correlationJoin, + wherePredicateEnvironment(existsEnvironment, fieldDefinition, args), + arg); + + + Predicate exists = cb.exists(subquery.select((Join) correlationJoin) + .where(restriction)); + + return logical == Logical.EXISTS ? exists : cb.not(exists); + } + + AbstractQuery query = environment.getRoot(); + + From context = (isSubquery(query) || isCountQuery(query)) + ? reuseJoin(from, it.getName(), isOptional) + : reuseFetch(from, it.getName(), isOptional); + + return getArgumentPredicate(cb, context, + wherePredicateEnvironment(environment, fieldDefinition, args), + arg); + } + } + + return getLogicalPredicate(it.getName(), + cb, + from, + it, + argumentEnvironment(environment, args), + arg); + } + + protected boolean isSubquery(AbstractQuery query) { + return Subquery.class.isInstance(query); + } + + protected boolean isCountQuery(AbstractQuery query) { + return Optional.ofNullable(query.getSelection()) + .map(Selection::getJavaType) + .map(Long.class::equals) + .orElse(false); + } protected Predicate getArgumentsPredicate(CriteriaBuilder cb, - From path, + From from, DataFetchingEnvironment environment, Argument argument) { ArrayValue whereValue = getValue(argument, environment); @@ -559,13 +589,13 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, if(ArrayValue.class.isInstance(it.getValue())) { return getArgumentsPredicate(cb, - path, + from, argumentEnvironment(environment, args), arg); } return getArgumentPredicate(cb, - path, + from, argumentEnvironment(environment, args), arg); @@ -581,53 +611,7 @@ protected Predicate getArgumentsPredicate(CriteriaBuilder cb, Map args = e.getValue(); Argument arg = new Argument(it.getName(), it.getValue()); - if(isEntityType(environment)) { - Attribute attribute = getAttribute(environment, arg); - - if(attribute.isAssociation()) { - - GraphQLFieldDefinition fieldDefinition = getFieldDef(environment.getGraphQLSchema(), - this.getObjectType(environment), - new Field(it.getName())); - boolean isOptional = false; - - 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(path) ? subquery.correlate((Root) path) - : subquery.correlate((Join) path); - - Join correlationJoin = correlation.join(it.getName()); - - DataFetchingEnvironment existsEnvironment = DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) - .root(subquery) - .build(); - - Predicate restriction = getArgumentPredicate(cb, - correlationJoin, - wherePredicateEnvironment(existsEnvironment, fieldDefinition, args), - arg); - - - Predicate exists = cb.exists(subquery.select((Join) correlationJoin) - .where(restriction)); - - return logical == Logical.EXISTS ? exists : cb.not(exists); - } - - - return getArgumentPredicate(cb, reuseJoin(path, it.getName(), isOptional), - wherePredicateEnvironment(environment, fieldDefinition, args), - arg); - } - } - - return getLogicalPredicate(it.getName(), - cb, - path, - it, - argumentEnvironment(environment, args), - arg); + return getObjectFieldPredicate(environment, cb, from, logical, it, arg, args); })) .filter(predicate -> predicate != null) .forEach(predicates::add); diff --git a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorListCriteriaTests.java b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorListCriteriaTests.java index 029b39704..e9b3d3622 100644 --- a/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorListCriteriaTests.java +++ b/graphql-jpa-query-schema/src/test/java/com/introproventures/graphql/jpa/query/schema/GraphQLExecutorListCriteriaTests.java @@ -20,8 +20,6 @@ import javax.persistence.EntityManager; -import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; -import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +31,9 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.Assert; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment=WebEnvironment.NONE) @@ -89,6 +90,45 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { " }" + "}"; + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace, genre=NOVEL}" + + "]" + + "}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { + //given: + String query = "query { " + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " AND: [{ "+ + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }]" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + + "}"; + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " @@ -437,8 +477,8 @@ public void queryWithWhereANDListEXISTSMixedCriteria() { String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" - + "{id=2, title=War and Peace}, " - + "{id=3, title=Anna Karenina}]}" + + "{id=2, title=War and Peace}" + + "]}" + "]}}"; //when: @@ -479,7 +519,8 @@ public void queryWithWhereBooksANDListEXISTSANDImplicitCriteria() { String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace}, " - + "{id=3, title=Anna Karenina}]}" + + "{id=3, title=Anna Karenina}]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //when: 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 ba03321a5..7fb7300f0 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 @@ -384,7 +384,7 @@ public void queryAuthorBooksWithExplictOptional() { + "}"; String expected = "{Authors={select=[" - + "{id=1, name=Leo Tolstoy, books=[{id=3, title=Anna Karenina, genre=NOVEL}, " + + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}]}" + "]}}"; @@ -394,7 +394,43 @@ public void queryAuthorBooksWithExplictOptional() { // then assertThat(result.toString()).isEqualTo(expected); } + + @Test + public void queryAuthorBooksWithExplictOptionalEXISTS() { + //given + String query = "query { " + + "Authors(" + + " where: {" + + " EXISTS: {" + + " 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}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } @Test public void queryAuthorBooksWithIsNullId() { //given @@ -418,7 +454,7 @@ public void queryAuthorBooksWithIsNullId() { " }" + "}"; - String expected = "{Authors={select=[]}}"; + String expected = "{Authors={select=[{id=8, name=Igor Dianov, books=[]}]}}"; //when Object result = executor.execute(query).getData(); @@ -827,7 +863,8 @@ public void queryForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard}, " + "{id=6, title=The Seagull}, " - + "{id=7, title=Three Sisters}]}" + + "{id=7, title=Three Sisters}]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //when @@ -901,7 +938,8 @@ public void queryTotalForAuthorsWithWhereBooksNOTEXISTSAuthorLIKENameLeo() { + "{id=4, name=Anton Chekhov, books=[" + "{id=5, title=The Cherry Orchard}, " + "{id=6, title=The Seagull}, " - + "{id=7, title=Three Sisters}]}" + + "{id=7, title=Three Sisters}]}, " + + "{id=8, name=Igor Dianov, books=[]}" + "]}}"; //when @@ -1005,6 +1043,45 @@ public void queryWithWhereInsideOneToManyRelationsImplicitAND() { " }" + "}"; + String expected = "{Authors={select=[{" + + "id=1, " + + "name=Leo Tolstoy, " + + "books=[" + + "{id=2, title=War and Peace, genre=NOVEL}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideOneToManyRelationsImplicitANDWithEXISTS() { + //given: + String query = "query { " + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + + "}"; + String expected = "{Authors={select=[{" + "id=1, " + "name=Leo Tolstoy, " @@ -1045,6 +1122,45 @@ public void queryWithWhereInsideOneToManyRelationsWithExplictAND() { " }" + "}"; + String expected = "{Authors={select=[" + + "{id=1, name=Leo Tolstoy, books=[" + + "{id=2, title=War and Peace, genre=NOVEL}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideOneToManyRelationsWithExplictANDEXISTS() { + //given: + String query = "query { " + + "Authors(where: {" + + " EXISTS: {" + + " books: {" + + " AND: { "+ + " genre: {IN: NOVEL}" + + " title: {LIKE: \"War\"}" + + " }" + + " }" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " books {" + + " id" + + " title" + + " genre" + + " }" + + " }" + + " }" + + "}"; + String expected = "{Authors={select=[" + "{id=1, name=Leo Tolstoy, books=[" + "{id=2, title=War and Peace, genre=NOVEL}, " @@ -1202,6 +1318,54 @@ public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFi " }" + "}"; + String expected = "{Books={select=[{" + + "id=2, " + + "title=War and Peace, genre=NOVEL, " + + "author={" + + "id=1, " + + "name=Leo Tolstoy, " + + "books=[" + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}" + + "}]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideManyToOneNestedRelationsWithOnToManyCollectionFilterEXISTS() { + //given: + String query = "query { " + + " 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" + + " }" + + " }" + + " }" + + " }" + + "}"; + String expected = "{Books={select=[{" + "id=2, " + "title=War and Peace, genre=NOVEL, " 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 c8da4e382..e12862322 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 @@ -38,6 +38,8 @@ import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; +import com.introproventures.graphql.jpa.query.schema.model.starwars.Character; +import com.introproventures.graphql.jpa.query.schema.model.starwars.Droid; @RunWith(SpringRunner.class) @SpringBootTest @@ -85,6 +87,41 @@ public void JPASampleTester() { assertThat(result).isNotEmpty(); assertThat(result).hasSize(13); } + + @Test + @Transactional + public void queryManyToManyTester() { + // given: + Query query = em.createQuery("select distinct droid from Droid as droid " + + "left join fetch droid.friends as generatedAlias0 " + + "inner join fetch generatedAlias0.friendsOf as generatedAlias1 " + + "where generatedAlias1.name='Luke Skywalker' " + + "order by droid.id asc"); + + query.setHint("hibernate.query.passDistinctThrough", false); + + // when: + List result = query.getResultList(); + + // then: + assertThat(result).hasSize(2); + + 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"); + 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) + .containsOnly("Han Solo", "Leia Organa"); + assertThat(result.get(1).getFriends()).flatExtracting(Character::getFriendsOf) + .extracting(Character::getName) + .containsOnly("Luke Skywalker"); + } @Test public void getsNamesOfAllDroids() { @@ -1068,6 +1105,55 @@ public void queryFilterNestedManyToManyRelationCriteria() { " } " + "}"; + String expected = "{Humans={select=[{" + + "id=1000, " + + "name=Luke Skywalker, " + + "homePlanet=Tatooine, " + + "favoriteDroid={" + + "name=C-3PO, " + + "primaryFunction={" + + "function=Protocol" + + "}" + + "}, " + + "friends=[" + + "{name=Leia Organa}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryFilterNestedManyToManyRelationCriteriaWithEXISTS() { + //given: + String query = "query {" + + " Humans(where: {" + + " EXISTS: {" + + " friends: { name: { LIKE: \"Leia\" } } " + + " favoriteDroid: { primaryFunction: { function: { EQ: \"Protocol\" } } }" + + " }"+ + " }) {" + + " select {" + + " id" + + " name" + + " homePlanet" + + " favoriteDroid {" + + " name" + + " primaryFunction {" + + " function" + + " }" + + " }" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + "}"; + String expected = "{Humans={select=[{" + "id=1000, " + "name=Luke Skywalker, " @@ -1278,8 +1364,7 @@ public void queryShouldReturnDistictResultsByDefault() { + "{id=1000, name=Luke Skywalker}, " + "{id=1001, name=Darth Vader}, " + "{id=1002, name=Han Solo}, " - + "{id=1003, name=Leia Organa}, " - + "{id=1004, name=Wilhuff Tarkin}" + + "{id=1003, name=Leia Organa}" + "]}}"; //when: @@ -1323,7 +1408,7 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseEQArray() { " Humans {" + " select {" + " name" + - " friends(where: {appearsIn: {EQ: [THE_FORCE_AWAKENS]}}) {" + + " friends(where: {appearsIn: {EQ: THE_FORCE_AWAKENS}}) {" + " name" + " }" + " }" + @@ -1430,7 +1515,7 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNEArray() { " Humans {" + " select {" + " name" + - " friends(where: {appearsIn: {NE: [THE_FORCE_AWAKENS]}}) {" + + " friends(where: {appearsIn: {NE: THE_FORCE_AWAKENS}}) {" + " name" + " }" + " }" + @@ -1502,5 +1587,230 @@ public void querySetOfEnumsWithinEmbeddedSelectClauseNINArray() { //then: assertThat(result.toString()).isEqualTo(expected); } + + @Test + public void queryEmbeddedWhereWithPluralAssociations() { + + //given: + String query = "{ " + + "Droids {" + + " select {" + + " name" + + " friends(where:{" + + " NOT_EXISTS:{ friends:{name:{EQ:\"R2-D2\"}}}" + + " }) {" + + " name" + + " friends {" + + " name" + + " }" + + " }" + + " } " + + " } " + + "}"; + + String expected = "{Droids={select=[" + + "{name=C-3PO, friends=[" + + "{name=R2-D2, friends=[" + + "{name=Han Solo}, " + + "{name=Leia Organa}, " + + "{name=Luke Skywalker}" + + "]}" + + "]}]}}"; + + //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" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{name=Darth Vader, friends=[{name=Wilhuff Tarkin}]}, " + + "{name=Wilhuff Tarkin, friends=[{name=Darth Vader}]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " }" + + " }" + + " } " + + " }" + + "}"; + + String expected = "{Droids={select=[" + + "{name=C-3PO, friends=[" + + "{name=Leia Organa, friends=[" + + "{name=C-3PO}, " + + "{name=Han Solo}, " + + "{name=Luke Skywalker}, " + + "{name=R2-D2}" + + "]}]}, " + + "{name=R2-D2, friends=[" + + "{name=Leia Organa, friends=[" + + "{name=C-3PO}, " + + "{name=Han Solo}, " + + "{name=Luke Skywalker}, " + + "{name=R2-D2}]}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " }" + + " }" + + " } " + + " }" + + "}"; + + String expected = "{Droids={select=[" + + "{name=C-3PO, friends=[" + + "{name=Leia Organa, friends=[" + + "{name=C-3PO}, " + + "{name=Han Solo}, " + + "{name=Luke Skywalker}, " + + "{name=R2-D2}" + + "]}]}, " + + "{name=R2-D2, friends=[" + + "{name=Leia Organa, friends=[" + + "{name=C-3PO}, " + + "{name=Han Solo}, " + + "{name=Luke Skywalker}, " + + "{name=R2-D2}]}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " }" + + " }" + + " } " + + " } " + + "}"; + + String expected = "{Droids={select=[" + + "{name=C-3PO, friends=[" + + "{name=Han Solo, friendsOf=[{name=Leia Organa}]}, " + + "{name=Luke Skywalker, friendsOf=[{name=Leia Organa}]}, " + + "{name=R2-D2, friendsOf=[{name=Leia Organa}]}" + + "]}, " + + "{name=R2-D2, friends=[" + + "{name=Han Solo, friendsOf=[{name=Leia Organa}]}, " + + "{name=Luke Skywalker, friendsOf=[{name=Leia Organa}]}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " }" + + " }" + + " } " + + " } " + + "}"; + + 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=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}]}" + + "]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } }