diff --git a/.project b/.project deleted file mode 100644 index 11c63df2c..000000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - graphql-jpa-query - - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.m2e.core.maven2Nature - - diff --git a/README.md b/README.md index 06a219a7b..24d18fcdc 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ Will return: Query Wrapper with Where Criteria Expressions ------------------------------------- -This library supports flexible type safe criteria expressions with user-friendly SQL query syntax semantics using `where` arguments and `select` field to specify the entity graph query with entiy attribute names as a combination of logical expressions like OR, AND, EQ, NE, GT, GE, LT, LR, IN, NIN, IS_NULL, NOT_NULL, BETWEEN, NOT_BETWEEN. +This library supports flexible type safe criteria expressions with user-friendly SQL query syntax semantics using `where` arguments and `select` field to specify the entity graph query with entiy attribute names as a combination of logical expressions like EQ, NE, GT, GE, LT, LR, IN, NIN, IS_NULL, NOT_NULL, BETWEEN, NOT_BETWEEN. You can use logical AND/OR combinations in SQL criteria expressions to specify complex criterias to fetch your data from SQL database. If you omit, where argument, all entities will be returned. For Example: @@ -173,7 +173,200 @@ Will return: } } -You can use familiar SQL criteria expressions to specify complex criterias to fetch your data from SQL database. If you omit, where argument, all entities will be returned. +Relation Attributes in Where Criteria Expressions: +---------------------------- +It is also possible to specify complex filters using many-to-one and one-to-many entity attributes in where criteria expressions with variable parameter bindings, i.e. + +Given the following query with many-to-one relation with variables `{"authorId": 1 }` : + +``` +query($authorId: Long) { + Books(where: { + author: {id: {EQ: $authorId}} + }) { + select { + id + title + genre + author { + id + name + } + } + } +} +``` + +will result in + +``` +{ + "data": { + "Books": { + "select": [ + { + "id": 2, + "title": "War and Peace", + "genre": "NOVEL", + "author": { + "id": 1, + "name": "Leo Tolstoy" + } + }, + { + "id": 3, + "title": "Anna Karenina", + "genre": "NOVEL", + "author": { + "id": 1, + "name": "Leo Tolstoy" + } + } + ] + } + } +} +``` + +And given the following query with one-to-many relation: + +``` +query { + Authors(where: { + books: {genre: {IN: NOVEL}} + }) { + select { + id + name + books { + id + title + genre + } + } + } +} +``` + +will result in + +``` +{ + "data": { + "Authors": { + "select": [ + { + "id": 1, + "name": "Leo Tolstoy", + "books": [ + { + "id": 2, + "title": "War and Peace", + "genre": "NOVEL" + }, + { + "id": 3, + "title": "Anna Karenina", + "genre": "NOVEL" + } + ] + } + ] + } + } +} +``` + +It is possible to use compound criterias in where search expressions given: + +``` +query { + Authors(where: { + books: { + genre: {IN: NOVEL} + title: {LIKE: "War"} + } + }) { + select { + id + name + books { + id + title + genre + } + } + } +} +``` + +Will return filtered inner collection result: + +``` +{ + "data": { + "Authors": { + "select": [ + { + "id": 1, + "name": "Leo Tolstoy", + "books": [ + { + "id": 2, + "title": "War and Peace", + "genre": "NOVEL" + } + ] + } + ] + } + } +} +``` + +It is also possible to filter inner collections as follows: + +``` +query { + Authors(where: { + books: {genre: {IN: NOVEL}} + }) { + select { + id + name + books(where: {title: {LIKE: "War"}}) { + id + title + genre + } + } + } +} +``` + +will result in + +``` +{ + "data": { + "Authors": { + "select": [ + { + "id": 1, + "name": "Leo Tolstoy", + "books": [ + { + "id": 2, + "title": "War and Peace", + "genre": "NOVEL" + } + ] + } + ] + } + } +} +``` Collection Filtering -------------------- diff --git a/graphql-jpa-query-boot-starter/.project b/graphql-jpa-query-boot-starter/.project deleted file mode 100644 index 703ec2a93..000000000 --- a/graphql-jpa-query-boot-starter/.project +++ /dev/null @@ -1,56 +0,0 @@ - - - graphql-jpa-query-boot-starter - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.wst.common.project.facet.core.builder - - - - - org.hibernate.eclipse.console.hibernateBuilder - - - - - org.springframework.ide.eclipse.core.springbuilder - - - - - org.springframework.ide.eclipse.boot.validation.springbootbuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - org.springframework.ide.eclipse.boot.validation.springbootbuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.springframework.ide.eclipse.core.springnature - org.eclipse.m2e.core.maven2Nature - org.eclipse.wst.common.project.facet.core.nature - org.eclipse.jdt.core.javanature - org.hibernate.eclipse.console.hibernateNature - - diff --git a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Author.java b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Author.java index 43f297e02..6bd10b220 100644 --- a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Author.java +++ b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Author.java @@ -19,6 +19,7 @@ import java.util.Collection; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.OneToMany; @@ -32,6 +33,6 @@ public class Author { String name; - @OneToMany(mappedBy="author") + @OneToMany(mappedBy="author", fetch=FetchType.LAZY) Collection books; } diff --git a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Book.java b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Book.java index d5636c9ed..a81493125 100644 --- a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Book.java +++ b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/books/Book.java @@ -19,6 +19,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.ManyToOne; @@ -32,7 +33,7 @@ public class Book { String title; - @ManyToOne + @ManyToOne(fetch=FetchType.LAZY) Author author; @Enumerated(EnumType.STRING) diff --git a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java index 8536a88fa..cc9bc37f3 100644 --- a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java +++ b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java @@ -22,6 +22,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; @@ -29,6 +30,7 @@ import javax.persistence.OrderBy; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -50,14 +52,14 @@ public abstract class Character { String name; @GraphQLDescription("Who are the known friends to this character") - @ManyToMany - @JoinTable(name="character_friends", + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name="character_friends", joinColumns=@JoinColumn(name="source_id", referencedColumnName="id"), inverseJoinColumns=@JoinColumn(name="friend_id", referencedColumnName="id")) Set friends; @GraphQLDescription("What Star Wars episodes does this character appear in") - @ElementCollection(targetClass = Episode.class) + @ElementCollection(targetClass = Episode.class, fetch = FetchType.LAZY) @Enumerated(EnumType.ORDINAL) @OrderBy Set appearsIn; diff --git a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java index 809c0cc9d..fc7574efe 100644 --- a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java +++ b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java @@ -17,11 +17,13 @@ package com.introproventures.graphql.jpa.query.example.starwars; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; + import lombok.Data; @Entity @@ -39,7 +41,7 @@ public class CodeList { boolean active; String description; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") CodeList parent; diff --git a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java index aa0af3640..b4d7bbc9d 100644 --- a/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java +++ b/graphql-jpa-query-example-merge/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java @@ -35,7 +35,7 @@ public class Human extends Character { @JoinColumn(name = "favorite_droid_id") Droid favoriteDroid; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "gender_code_id") CodeList gender; diff --git a/graphql-jpa-query-example-merge/src/main/resources/application.yml b/graphql-jpa-query-example-merge/src/main/resources/application.yml index be0aa8936..b23f02b12 100644 --- a/graphql-jpa-query-example-merge/src/main/resources/application.yml +++ b/graphql-jpa-query-example-merge/src/main/resources/application.yml @@ -2,11 +2,14 @@ spring: h2: console.enabled: true + jpa: + open-in-view: false + graphql: jpa: query: - name: GraphQLJpaQueryStarwars - description: GraphQL Jpa Query Starwars Schema Example + name: Query + description: Combined GraphQL Jpa Query for Starwars and Books Example enabled: true starwars: diff --git a/graphql-jpa-query-example-merge/src/main/resources/hibernate.properties b/graphql-jpa-query-example-merge/src/main/resources/hibernate.properties new file mode 100644 index 000000000..34f5b4c38 --- /dev/null +++ b/graphql-jpa-query-example-merge/src/main/resources/hibernate.properties @@ -0,0 +1,8 @@ +hibernate.generate_statistics=true +org.hibernate.stat=DEBUG +hibernate.show_sql=true +hibernate.format_sql=true + +logging.level.org.hibernate=debug +#logging.level.org.hibernate.type.descriptor.sql=trace +#logging.level.org.hibernate.SQL=debug diff --git a/graphql-jpa-query-example/.project b/graphql-jpa-query-example/.project deleted file mode 100644 index dc1942712..000000000 --- a/graphql-jpa-query-example/.project +++ /dev/null @@ -1,56 +0,0 @@ - - - graphql-jpa-query-boot-demo - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.wst.common.project.facet.core.builder - - - - - org.hibernate.eclipse.console.hibernateBuilder - - - - - org.springframework.ide.eclipse.core.springbuilder - - - - - org.springframework.ide.eclipse.boot.validation.springbootbuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - org.springframework.ide.eclipse.boot.validation.springbootbuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.springframework.ide.eclipse.core.springnature - org.eclipse.m2e.core.maven2Nature - org.eclipse.wst.common.project.facet.core.nature - org.eclipse.jdt.core.javanature - org.hibernate.eclipse.console.hibernateNature - - diff --git a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java index 8536a88fa..8535d8e55 100644 --- a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java +++ b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Character.java @@ -22,6 +22,7 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; @@ -29,6 +30,7 @@ import javax.persistence.OrderBy; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -50,14 +52,14 @@ public abstract class Character { String name; @GraphQLDescription("Who are the known friends to this character") - @ManyToMany + @ManyToMany(fetch=FetchType.LAZY ) @JoinTable(name="character_friends", joinColumns=@JoinColumn(name="source_id", referencedColumnName="id"), inverseJoinColumns=@JoinColumn(name="friend_id", referencedColumnName="id")) Set friends; @GraphQLDescription("What Star Wars episodes does this character appear in") - @ElementCollection(targetClass = Episode.class) + @ElementCollection(targetClass = Episode.class, fetch=FetchType.LAZY) @Enumerated(EnumType.ORDINAL) @OrderBy Set appearsIn; diff --git a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java index 809c0cc9d..fc7574efe 100644 --- a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java +++ b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/CodeList.java @@ -17,11 +17,13 @@ package com.introproventures.graphql.jpa.query.example.starwars; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import com.introproventures.graphql.jpa.query.annotation.GraphQLDescription; + import lombok.Data; @Entity @@ -39,7 +41,7 @@ public class CodeList { boolean active; String description; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parent_id") CodeList parent; diff --git a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java index aa0af3640..b4d7bbc9d 100644 --- a/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java +++ b/graphql-jpa-query-example/src/main/java/com/introproventures/graphql/jpa/query/example/starwars/Human.java @@ -35,7 +35,7 @@ public class Human extends Character { @JoinColumn(name = "favorite_droid_id") Droid favoriteDroid; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "gender_code_id") CodeList gender; diff --git a/graphql-jpa-query-example/src/main/resources/application.yml b/graphql-jpa-query-example/src/main/resources/application.yml index 2ce70ac9a..50b4c99c7 100644 --- a/graphql-jpa-query-example/src/main/resources/application.yml +++ b/graphql-jpa-query-example/src/main/resources/application.yml @@ -2,6 +2,7 @@ spring: jpa: hibernate.ddl-auto: create-drop show-sql: true + open-in-view: false h2: console.enabled: true diff --git a/graphql-jpa-query-example/src/main/resources/data.sql b/graphql-jpa-query-example/src/main/resources/data.sql new file mode 100644 index 000000000..39032979c --- /dev/null +++ b/graphql-jpa-query-example/src/main/resources/data.sql @@ -0,0 +1,104 @@ +-- Insert Code Lists +insert into code_list (id, type, code, description, sequence, active, parent_id) values + (0, 'org.crygier.graphql.model.starwars.Gender', 'Male', 'Male', 1, true, null), + (1, 'org.crygier.graphql.model.starwars.Gender', 'Female', 'Female', 2, true, null); + +-- Insert Droids +insert into character (id, name, primary_function, dtype) values + ('2000', 'C-3PO', 'Protocol', 'Droid'), + ('2001', 'R2-D2', 'Astromech', 'Droid'); + +-- Insert Humans +insert into character (id, name, home_planet, favorite_droid_id, dtype, gender_code_id) values + ('1000', 'Luke Skywalker', 'Tatooine', '2000', 'Human', 0), + ('1001', 'Darth Vader', 'Tatooine', '2001', 'Human', 0), + ('1002', 'Han Solo', NULL, NULL, 'Human', 0), + ('1003', 'Leia Organa', 'Alderaan', NULL, 'Human', 1), + ('1004', 'Wilhuff Tarkin', NULL, NULL, 'Human', 0); + +-- Luke's friends +insert into character_friends (source_id, friend_id) values + ('1000', '1002'), + ('1000', '1003'), + ('1000', '2000'), + ('1000', '2001'); + +-- Luke Appears in +insert into character_appears_in (character_id, appears_in) values + ('1000', 3), + ('1000', 4), + ('1000', 5), + ('1000', 6); + +-- Vader's friends +insert into character_friends (source_id, friend_id) values + ('1001', '1004'); + +-- Vader Appears in +insert into character_appears_in (character_id, appears_in) values + ('1001', 3), + ('1001', 4), + ('1001', 5); + +-- Solo's friends +insert into character_friends (source_id, friend_id) values + ('1002', '1000'), + ('1002', '1003'), + ('1002', '2001'); + +-- Solo Appears in +insert into character_appears_in (character_id, appears_in) values + ('1002', 3), + ('1002', 4), + ('1002', 5), + ('1002', 6); + +-- Leia's friends +insert into character_friends (source_id, friend_id) values + ('1003', '1000'), + ('1003', '1002'), + ('1003', '2000'), + ('1003', '2001'); + +-- Leia Appears in +insert into character_appears_in (character_id, appears_in) values + ('1003', 3), + ('1003', 4), + ('1003', 5), + ('1003', 6); + +-- Wilhuff's friends +insert into character_friends (source_id, friend_id) values + ('1004', '1001'); + +-- Wilhuff Appears in +insert into character_appears_in (character_id, appears_in) values + ('1004', 3); + +-- C3PO's friends +insert into character_friends (source_id, friend_id) values + ('2000', '1000'), + ('2000', '1002'), + ('2000', '1003'), + ('2000', '2001'); + +-- C3PO Appears in +insert into character_appears_in (character_id, appears_in) values + ('2000', 3), + ('2000', 4), + ('2000', 5), + ('2000', 6); + +-- R2's friends +insert into character_friends (source_id, friend_id) values + ('2001', '1000'), + ('2001', '1002'), + ('2001', '1003'); + +-- R2 Appears in +insert into character_appears_in (character_id, appears_in) values + ('2001', 3), + ('2001', 4), + ('2001', 5), + ('2001', 6); + diff --git a/graphql-jpa-query-example/src/main/resources/hibernate.properties b/graphql-jpa-query-example/src/main/resources/hibernate.properties new file mode 100644 index 000000000..34f5b4c38 --- /dev/null +++ b/graphql-jpa-query-example/src/main/resources/hibernate.properties @@ -0,0 +1,8 @@ +hibernate.generate_statistics=true +org.hibernate.stat=DEBUG +hibernate.show_sql=true +hibernate.format_sql=true + +logging.level.org.hibernate=debug +#logging.level.org.hibernate.type.descriptor.sql=trace +#logging.level.org.hibernate.SQL=debug 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 693f1b611..acaddfaae 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 @@ -66,6 +66,10 @@ public Object get(DataFetchingEnvironment environment) { //EntityGraph entityGraph = buildEntityGraph(new Field("select", new SelectionSet(Arrays.asList(field)))); + // Let's clear session persistent context to avoid getting stale objects cached in the same session + // between requests with different search criteria. This looks like a Hibernate bug... + entityManager.clear(); + return getQuery(environment, field, true) //.setHint("javax.persistence.fetchgraph", entityGraph) // TODO: fix runtime exception .getResultList(); @@ -114,23 +118,21 @@ protected TypedQuery getQuery(DataFetchingEnvironment environment, Field fiel CriteriaBuilder cb = entityManager.getCriteriaBuilder(); CriteriaQuery query = cb.createQuery((Class) entityType.getJavaType()); - //CriteriaQuery query = cb.createTupleQuery(); Root from = query.from(entityType); from.alias("owner"); // Must use inner join in parent context Join join = from.join(attribute.getName()) - .on(cb.in(from.get(parentIdAttribute.getName())).value(parentIdValue)); + .on(cb.in(from.get(parentIdAttribute.getName())) + .value(parentIdValue)); query.select(join.alias(attribute.getName())); - //query.multiselect(from.alias("owner"), join.alias(attribute.getName())); - - List predicates = getFieldArguments(field, query, cb, join).stream() - .map(it -> getPredicate(cb, from, join, environment, it)) - .filter(it -> it != null) - .collect(Collectors.toList()); + List predicates = getFieldArguments(field, query, cb, join, environment).stream() + .map(it -> getPredicate(cb, from, join, environment, it)) + .filter(it -> it != null) + .collect(Collectors.toList()); query.where( predicates.toArray(new Predicate[predicates.size()]) ); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java index 4d8917566..5c5277148 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/GraphQLJpaQueryDataFetcher.java @@ -36,7 +36,7 @@ import graphql.language.BooleanValue; import graphql.language.Field; import graphql.schema.DataFetchingEnvironment; -import graphql.schema.DataFetchingEnvironmentImpl; +import graphql.schema.DataFetchingEnvironmentBuilder; import graphql.schema.GraphQLObjectType; /** @@ -47,6 +47,12 @@ */ class GraphQLJpaQueryDataFetcher extends QraphQLJpaBaseDataFetcher { + private static final String HIBERNATE_QUERY_PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough"; + private static final String ORG_HIBERNATE_CACHEABLE = "org.hibernate.cacheable"; + private static final String ORG_HIBERNATE_FETCH_SIZE = "org.hibernate.fetchSize"; + private static final String ORG_HIBERNATE_READ_ONLY = "org.hibernate.readOnly"; + private static final String JAVAX_PERSISTENCE_FETCHGRAPH = "javax.persistence.fetchgraph"; + public GraphQLJpaQueryDataFetcher(EntityManager entityManager, EntityType entityType) { super(entityManager, entityType); } @@ -78,26 +84,17 @@ public Object get(DataFetchingEnvironment environment) { Optional.of(getFieldDef(environment.getGraphQLSchema(), (GraphQLObjectType)environment.getParentType(), field)) .map(it -> (GraphQLObjectType) it.getType()) .map(it -> it.getFieldDefinition(GraphQLJpaSchemaBuilder.QUERY_SELECT_PARAM_NAME)) - .map(it -> (DataFetchingEnvironment) - new DataFetchingEnvironmentImpl( - environment.getSource(), - environment.getArguments(), - environment.getContext(), - environment.getRoot(), - environment.getFieldDefinition(), - environment.getFields(), - it.getType(), - environment.getParentType(), - environment.getGraphQLSchema(), - environment.getFragmentsByName(), - environment.getExecutionId(), - environment.getSelectionSet(), - environment.getExecutionStepInfo(), - environment.getExecutionContext() - )).orElse(environment); + .map(it -> DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .fieldType(it.getType()) + .build() + ).orElse(environment); queryField = new Field(fieldName, field.getArguments(), recordsSelection.get().getSelectionSet()); + // Let's clear session persistent context to avoid getting stale objects cached in the same session + // between requests with different search criteria. This looks like a Hibernate bug... + entityManager.clear(); + TypedQuery query = getQuery(queryEnvironment, queryField, isDistinct); // Let's apply page only if present @@ -113,12 +110,13 @@ public Object get(DataFetchingEnvironment environment) { // reports on certain objects and you don't want a lot of the stuff that's normally flagged to // load via eager annotations. EntityGraph graph = buildEntityGraph(queryField); - query.setHint("javax.persistence.fetchgraph", graph); + query.setHint(JAVAX_PERSISTENCE_FETCHGRAPH, graph); - // Let' try reduce overhead - query.setHint("org.hibernate.readOnly", true); - query.setHint("org.hibernate.fetchSize", 1000); - query.setHint("org.hibernate.cacheable", true); + // Let' try reduce overhead and disable all caching + query.setHint(ORG_HIBERNATE_READ_ONLY, true); + query.setHint(ORG_HIBERNATE_FETCH_SIZE, 1000); + query.setHint(ORG_HIBERNATE_CACHEABLE, false); + query.setHint(HIBERNATE_QUERY_PASS_DISTINCT_THROUGH, false); result.put(GraphQLJpaSchemaBuilder.QUERY_SELECT_PARAM_NAME, query.getResultList()); } @@ -146,9 +144,7 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root root, From pat return null; if(isWhereArgument(argument)) - return getWherePredicate(cb, root, path, - new ArgumentEnvironment(environment, argument.getName()), - argument); + return getWherePredicate(cb, root, path, argumentEnvironment(environment, argument.getName()), argument); return super.getPredicate(cb, root, path, environment, argument); } 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 72947aabf..fc8a873d7 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 @@ -80,6 +80,8 @@ */ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { + private static final String AND = "AND"; + private static final String OR = "OR"; public static final String PAGE_PARAM_NAME = "page"; public static final String PAGE_TOTAL_PARAM_NAME = "total"; public static final String PAGE_PAGES_PARAM_NAME = "pages"; @@ -99,6 +101,7 @@ public class GraphQLJpaSchemaBuilder implements GraphQLSchemaBuilder { private Map, GraphQLOutputType> classCache = new HashMap<>(); private Map, GraphQLObjectType> entityCache = new HashMap<>(); + private Map, GraphQLInputObjectType> inputObjectCache = new HashMap<>(); private Map, GraphQLObjectType> embeddableOutputCache = new HashMap<>(); private Map, GraphQLInputObjectType> embeddableInputCache = new HashMap<>(); @@ -224,22 +227,34 @@ private GraphQLArgument getWhereArgument(ManagedType managedType) { .name(type) .description("Where logical AND specification of the provided list of criteria expressions") .field(GraphQLInputObjectField.newInputObjectField() - .name("OR") - .description("Logical operation for expressions") - .type(new GraphQLTypeReference(type)) - .build() + .name(OR) + .description("Logical operation for expressions") + .type(new GraphQLTypeReference(type)) + .build() ) .field(GraphQLInputObjectField.newInputObjectField() - .name("AND") - .description("Logical operation for expressions") - .type(new GraphQLTypeReference(type)) - .build() + .name(AND) + .description("Logical operation for expressions") + .type(new GraphQLTypeReference(type)) + .build() ) .fields(managedType.getAttributes().stream() - .filter(this::isValidInput) - .filter(this::isNotIgnored) - .map(this::getWhereInputField) - .collect(Collectors.toList()) + .filter(this::isValidInput) + .filter(this::isNotIgnored) + .map(this::getWhereInputField) + .collect(Collectors.toList()) + ) + .fields(managedType.getAttributes().stream() + .filter(this::isToOne) + .filter(this::isNotIgnored) + .map(this::getInputObjectField) + .collect(Collectors.toList()) + ) + .fields(managedType.getAttributes().stream() + .filter(this::isToMany) + .filter(this::isNotIgnored) + .map(this::getInputObjectField) + .collect(Collectors.toList()) ) .build(); @@ -254,7 +269,48 @@ private GraphQLArgument getWhereArgument(ManagedType managedType) { return whereArgument; } + + private GraphQLInputObjectType getWhereInputType(ManagedType managedType) { + return inputObjectCache.computeIfAbsent(managedType, this::computeWhereInputType); + } + private GraphQLInputObjectType computeWhereInputType(ManagedType managedType) { + String typeName=""; + if (managedType instanceof EmbeddableType){ + typeName = managedType.getJavaType().getSimpleName()+"EmbeddableType"; + } else if (managedType instanceof EntityType) { + typeName = ((EntityType)managedType).getName(); + } + + String type = namingStrategy.pluralize(typeName)+"RelationCriteriaExpression"; + + GraphQLInputObjectType whereInputObject = GraphQLInputObjectType.newInputObject() + .name(type) + .description("Where logical AND specification of the provided list of criteria expressions") + .field(GraphQLInputObjectField.newInputObjectField() + .name(OR) + .description("Logical operation for expressions") + .type(new GraphQLTypeReference(type)) + .build() + ) + .field(GraphQLInputObjectField.newInputObjectField() + .name(AND) + .description("Logical operation for expressions") + .type(new GraphQLTypeReference(type)) + .build() + ) + .fields(managedType.getAttributes().stream() + .filter(this::isValidInput) + .filter(this::isNotIgnored) + .map(this::getWhereInputField) + .collect(Collectors.toList()) + ) + .build(); + + return whereInputObject; + + } + private GraphQLInputObjectField getWhereInputField(Attribute attribute) { GraphQLInputType type = getWhereAttributeType(attribute); String description = getSchemaDescription(attribute.getJavaMember()); @@ -273,23 +329,22 @@ private GraphQLInputObjectField getWhereInputField(Attribute attribute) { private Map whereAttributesMap = new HashMap<>(); private GraphQLInputType getWhereAttributeType(Attribute attribute) { - //String type = namingStrategy.singularize(attribute.getName())+((EntityType)attribute.getDeclaringType()).getName()+"Criteria"; - String type = namingStrategy.singularize(attribute.getName())+attribute.getDeclaringType().getJavaType().getSimpleName()+"Criteria"; + String type = namingStrategy.singularize(attribute.getName())+attribute.getDeclaringType().getJavaType().getSimpleName()+"Criteria"; - if(whereAttributesMap.containsKey(type)) + if(whereAttributesMap.containsKey(type)) return whereAttributesMap.get(type); - GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject() + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject() .name(type) .description("Criteria expression specification of "+namingStrategy.singularize(attribute.getName())+" attribute in entity " + attribute.getDeclaringType().getJavaType()) .field(GraphQLInputObjectField.newInputObjectField() - .name("OR") + .name(OR) .description("Logical OR criteria expression") .type(new GraphQLTypeReference(type)) .build() ) .field(GraphQLInputObjectField.newInputObjectField() - .name("AND") + .name(AND) .description("Logical AND criteria expression") .type(new GraphQLTypeReference(type)) .build() @@ -525,7 +580,7 @@ private GraphQLFieldDefinition getObjectField(Attribute 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) { - ManagedType foreignType = (ManagedType) ((SingularAttribute) attribute).getType(); + ManagedType foreignType = getForeignType(attribute); // TODO fix page count query arguments.add(getWhereArgument(foreignType)); @@ -550,6 +605,10 @@ else if (attribute instanceof PluralAttribute .build(); } + protected ManagedType getForeignType(Attribute attribute) { + return (ManagedType) ((SingularAttribute) attribute).getType(); + } + @SuppressWarnings( { "rawtypes" } ) private GraphQLInputObjectField getInputObjectField(Attribute attribute) { GraphQLInputType type = getAttributeInputType(attribute); @@ -566,7 +625,8 @@ private Stream> findBasicAttributes(Collection> at } private GraphQLInputType getAttributeInputType(Attribute attribute) { - try{ + + try { return (GraphQLInputType) getAttributeType(attribute, true); } catch (ClassCastException e){ throw new IllegalArgumentException("Attribute " + attribute + " cannot be mapped as an Input Argument"); @@ -576,7 +636,7 @@ private GraphQLInputType getAttributeInputType(Attribute attribute) { private GraphQLOutputType getAttributeOutputType(Attribute attribute) { try { return (GraphQLOutputType) getAttributeType(attribute, false); - } catch (ClassCastException e){ + } catch (ClassCastException e) { throw new IllegalArgumentException("Attribute " + attribute + " cannot be mapped as an Output Argument"); } } @@ -593,11 +653,13 @@ else if (isEmbeddable(attribute)) { } else if (isToMany(attribute)) { EntityType foreignType = (EntityType) ((PluralAttribute) attribute).getElementType(); - return new GraphQLList(new GraphQLTypeReference(foreignType.getName())); + + return input ? getWhereInputType(foreignType) : new GraphQLList(new GraphQLTypeReference(foreignType.getName())); } else if (isToOne(attribute)) { EntityType foreignType = (EntityType) ((SingularAttribute) attribute).getType(); - return new GraphQLTypeReference(foreignType.getName()); + + return input ? getWhereInputType(foreignType) : new GraphQLTypeReference(foreignType.getName()); } else if (isElementCollection(attribute)) { Type foreignType = ((PluralAttribute) attribute).getElementType(); @@ -631,6 +693,10 @@ protected final boolean isToMany(Attribute attribute) { || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_MANY; } + 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; @@ -643,6 +709,7 @@ protected final boolean isValidInput(Attribute attribute) { attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.EMBEDDED; } + private String getSchemaDescription(Member member) { if (member instanceof AnnotatedElement) { String desc = getSchemaDescription((AnnotatedElement) member); diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java index bdf4f3cd2..d95eeb40f 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/Logical.java @@ -16,7 +16,19 @@ package com.introproventures.graphql.jpa.query.schema.impl; +import java.util.EnumSet; +import java.util.Set; +import java.util.stream.Collectors; + enum Logical { - AND, - OR + AND, OR; + + private static Set names = EnumSet.allOf(Logical.class) + .stream() + .map(it -> it.name().toString()) + .collect(Collectors.toSet()); + + public static Set names() { + return names; + } } diff --git a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PredicateFilter.java b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PredicateFilter.java index b50db8eb6..7e14bcbbc 100644 --- a/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PredicateFilter.java +++ b/graphql-jpa-query-schema/src/main/java/com/introproventures/graphql/jpa/query/schema/impl/PredicateFilter.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.EnumSet; import java.util.Set; +import java.util.stream.Collectors; class PredicateFilter implements Comparable, Serializable { @@ -102,7 +103,16 @@ public enum Criteria { /** * Not Between condition */ - NOT_BETWEEN + NOT_BETWEEN; + + private static Set names = EnumSet.allOf(Criteria.class) + .stream() + .map(it -> it.name().toString()) + .collect(Collectors.toSet()); + + public static Set names() { + return names; + } } private final String field; 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 a901ff915..f6d80eb1d 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 @@ -51,6 +51,7 @@ import javax.persistence.metamodel.SingularAttribute; import com.introproventures.graphql.jpa.query.annotation.GraphQLDefaultOrderBy; +import com.introproventures.graphql.jpa.query.schema.impl.PredicateFilter.Criteria; import graphql.GraphQLException; import graphql.execution.ValuesResolver; @@ -73,7 +74,7 @@ import graphql.language.VariableReference; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; -import graphql.schema.DataFetchingEnvironmentImpl; +import graphql.schema.DataFetchingEnvironmentBuilder; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; @@ -120,7 +121,7 @@ protected TypedQuery getQuery(DataFetchingEnvironment environment, Field fiel from.alias(from.getModel().getName()); // Build predicates from query arguments - List predicates = getFieldArguments(field, query, cb, from) + List predicates = getFieldArguments(field, query, cb, from, environment) .stream() .map(it -> getPredicate(cb, from, from, environment, it)) .filter(it -> it != null) @@ -136,7 +137,7 @@ protected TypedQuery getQuery(DataFetchingEnvironment environment, Field fiel return entityManager.createQuery(query.distinct(isDistinct)); } - protected final List getFieldArguments(Field field, CriteriaQuery query, CriteriaBuilder cb, From from) { + protected final List getFieldArguments(Field field, CriteriaQuery query, CriteriaBuilder cb, From from, DataFetchingEnvironment environment) { List arguments = new ArrayList<>(); @@ -177,11 +178,14 @@ protected final List getFieldArguments(Field field, CriteriaQuery q if (attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.MANY_TO_ONE || attribute.getPersistentAttributeType() == Attribute.PersistentAttributeType.ONE_TO_ONE ) { - from.fetch(selectedField.getName()); + reuseJoin(from, selectedField.getName(), false); } } } else { - // Do nothing + // We must add plural attributes with explicit join to avoid Hibernate error: + // "query specified join fetching, but the owner of the fetched association was not present in the select list" + // TODO Let's try detect many-to-many relation and reuse outer join + reuseJoin(from, selectedField.getName(), false); } } } @@ -256,7 +260,7 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root from, From pat // If the argument is a list, let's assume we need to join and do an 'in' clause if (argumentEntityAttribute instanceof PluralAttribute) { - return from.join(argument.getName()) + return reuseJoin(from, argument.getName(), false) .in(convertValue(environment, argument, argument.getValue())); } @@ -287,7 +291,7 @@ protected Predicate getPredicate(CriteriaBuilder cb, Root from, From pat .getArgumentValues(fieldDef.getArguments(), Collections.singletonList(where), variables) .get("where"); - return getWherePredicate(cb, from, join, new WherePredicateEnvironment(environment, fieldDef, arguments), where); + return getWherePredicate(cb, from, join, wherePredicateEnvironment(environment, fieldDef, arguments), where); } } } @@ -297,34 +301,23 @@ private R getValue(Argument argument) { return (R) argument.getValue(); } - - @SuppressWarnings( "serial" ) protected Predicate getWherePredicate(CriteriaBuilder cb, Root root, From path, DataFetchingEnvironment environment, Argument argument) { ObjectValue whereValue = getValue(argument); if(whereValue.getChildren().isEmpty()) return cb.conjunction(); + + Logical logical = extractLogical(argument); + + Map 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, - new DataFetchingEnvironmentImpl( - environment.getSource(), - new LinkedHashMap() {{ - put(Logical.AND.name(), environment.getArguments()); - }}, - environment.getContext(), - environment.getRoot(), - environment.getFieldDefinition(), - environment.getFields(), - environment.getFieldType(), - environment.getParentType(), - environment.getGraphQLSchema(), - environment.getFragmentsByName(), - environment.getExecutionId(), - environment.getSelectionSet(), - environment.getExecutionStepInfo(), - environment.getExecutionContext() - ), new Argument(Logical.AND.name(), whereValue) - ); + return getArgumentPredicate(cb, (path != null) ? path : root, predicateDataFetchingEnvironment, predicateArgument); } protected Predicate getArgumentPredicate(CriteriaBuilder cb, From path, @@ -332,26 +325,23 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From path, ObjectValue whereValue = getValue(argument); if (whereValue.getChildren().isEmpty()) - return cb.disjunction(); - - Logical logical = Optional.of(argument.getName()) - .filter(it -> Arrays.asList("AND", "OR").contains(it)) - .map(it -> Logical.valueOf(it)) - .orElse(Logical.AND); + return cb.disjunction(); + + Logical logical = extractLogical(argument); List predicates = new ArrayList<>(); whereValue.getObjectFields().stream() - .filter(it -> Arrays.asList("AND", "OR").contains(it.getName())) + .filter(it -> Logical.names().contains(it.getName())) .map(it -> getArgumentPredicate(cb, path, - new ArgumentEnvironment(environment, argument.getName()), + argumentEnvironment(environment, argument.getName()), new Argument(it.getName(), it.getValue()))) .forEach(predicates::add); whereValue.getObjectFields().stream() - .filter(it -> !Arrays.asList("AND", "OR").contains(it.getName())) + .filter(it -> !Logical.names().contains(it.getName())) .map(it -> getFieldPredicate(it.getName(), cb, path, it, - new ArgumentEnvironment(environment, argument.getName()), + argumentEnvironment(environment, argument.getName()), new Argument(it.getName(), it.getValue()))) .filter(predicate -> predicate != null) .forEach(predicates::add); @@ -363,34 +353,69 @@ protected Predicate getArgumentPredicate(CriteriaBuilder cb, From path, ? cb.or(predicates.toArray(new Predicate[predicates.size()])) : cb.and(predicates.toArray(new Predicate[predicates.size()])); } + + private Logical extractLogical(Argument argument) { + return Optional.of(argument.getName()) + .filter(it -> Logical.names().contains(it)) + .map(it -> Logical.valueOf(it)) + .orElse(Logical.AND); + } private Predicate getFieldPredicate(String fieldName, CriteriaBuilder cb, From path, ObjectField objectField, DataFetchingEnvironment environment, Argument argument) { - ObjectValue expressionValue = (ObjectValue) objectField.getValue(); + ObjectValue expressionValue; + + if(objectField.getValue() instanceof ObjectValue) + expressionValue = (ObjectValue) objectField.getValue(); + else + expressionValue = new ObjectValue(Arrays.asList(objectField)); if(expressionValue.getChildren().isEmpty()) return cb.disjunction(); - Logical logical = Optional.of(argument.getName()) - .filter(it -> Arrays.asList("AND","OR").contains(it)) - .map(it -> Logical.valueOf(it)) - .orElse(Logical.AND); + Logical logical = extractLogical(argument); List predicates = new ArrayList<>(); expressionValue.getObjectFields().stream() - .filter(it -> Arrays.asList("AND","OR").contains(it.getName())) + .filter(it -> Logical.names().contains(it.getName())) .map(it -> getFieldPredicate(fieldName, cb, path, it, - new ArgumentEnvironment(environment, argument.getName()), + argumentEnvironment(environment, argument.getName()), new Argument(it.getName(), it.getValue())) ) .forEach(predicates::add); + + // Let's parse relation criteria expressions if present + expressionValue.getObjectFields().stream() + .filter(it -> !Logical.names().contains(it.getName()) && !Criteria.names().contains(it.getName())) + .map(it -> { + GraphQLFieldDefinition fieldDefinition = getFieldDef(environment.getGraphQLSchema(), + this.getObjectType(environment, argument), + new Field(fieldName)); + + Map arguments = new LinkedHashMap<>(); + + arguments.put(logical.name(), environment.getArgument(fieldName)); + + return getArgumentPredicate(cb, reuseJoin(path, fieldName, false), + wherePredicateEnvironment(environment, fieldDefinition, arguments), + new Argument(logical.name(), expressionValue)); + } + ) + .forEach(predicates::add); + Optional relationPredicate = predicates.stream().findFirst(); + + // Let's check if relation criteria predicate exists, to avoid adding duplicate predicates in the query + if(relationPredicate.isPresent()) { + return relationPredicate.get(); + } + JpaPredicateBuilder pb = new JpaPredicateBuilder(cb, EnumSet.of(Logical.AND)); expressionValue.getObjectFields().stream() - .filter(it -> !Arrays.asList("AND","OR").contains(it.getName())) + .filter(it -> Criteria.names().contains(it.getName())) .map(it -> getPredicateFilter(new ObjectField(fieldName, it.getValue()), - new ArgumentEnvironment(environment, argument.getName()), + argumentEnvironment(environment, argument.getName()), new Argument(it.getName(), it.getValue())) ) .sorted() @@ -404,80 +429,38 @@ private Predicate getFieldPredicate(String fieldName, CriteriaBuilder cb, From options = EnumSet.of(PredicateFilter.Criteria.valueOf(argument.getName())); - - Object filterValue = convertValue( new DataFetchingEnvironmentImpl( - environment.getSource(), - new LinkedHashMap() {{ - put(objectField.getName(), environment.getArgument(argument.getName())); - }}, - environment.getContext(), - environment.getRoot(), - environment.getFieldDefinition(), - environment.getFields(), - environment.getFieldType(), - environment.getParentType(), - environment.getGraphQLSchema(), - environment.getFragmentsByName(), - environment.getExecutionId(), - environment.getSelectionSet(), - environment.getExecutionStepInfo(), - environment.getExecutionContext() - ), - new Argument(objectField.getName(), argument.getValue()), argument.getValue() ); + + 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 ); - } - class ArgumentEnvironment extends DataFetchingEnvironmentImpl { - - public ArgumentEnvironment(DataFetchingEnvironment environment, String argumentName) { - super( - environment.getSource(), - environment.getArgument(argumentName), - environment.getContext(), - environment.getRoot(), - environment.getFieldDefinition(), - environment.getFields(), - environment.getFieldType(), - environment.getParentType(), - environment.getGraphQLSchema(), - environment.getFragmentsByName(), - environment.getExecutionId(), - environment.getSelectionSet(), - environment.getExecutionStepInfo(), - environment.getExecutionContext() - ); - } + protected final DataFetchingEnvironment argumentEnvironment(DataFetchingEnvironment environment, String argumentName) { + return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .arguments(environment.getArgument(argumentName)) + .build(); } - class WherePredicateEnvironment extends DataFetchingEnvironmentImpl { - - public WherePredicateEnvironment(DataFetchingEnvironment environment, GraphQLFieldDefinition fieldDefinition, Map arguments) { - super( - environment.getSource(), - arguments, - environment.getContext(), - environment.getRoot(), - fieldDefinition, //environment.getFieldDefinition(), - environment.getFields(), - fieldDefinition.getType(), // environment.getFieldType(), - environment.getParentType(), - environment.getGraphQLSchema(), - environment.getFragmentsByName(), - environment.getExecutionId(), - environment.getSelectionSet(), - environment.getExecutionStepInfo(), - environment.getExecutionContext() - ); - } + protected final DataFetchingEnvironment wherePredicateEnvironment(DataFetchingEnvironment environment, GraphQLFieldDefinition fieldDefinition, Map arguments) { + return DataFetchingEnvironmentBuilder.newDataFetchingEnvironment(environment) + .arguments(arguments) + .fieldDefinition(fieldDefinition) + .fieldType(fieldDefinition.getType()) + .build(); } - - + /** * @param fieldName * @return Path of compound field to the primitive type @@ -536,7 +519,6 @@ private Join reuseJoin(From path, String fieldName, boolean outer) { for (Join join : path.getJoins()) { if (join.getAttribute().getName().equals(fieldName)) { if ((join.getJoinType() == JoinType.LEFT) == outer) { - //logger.debug("Reusing existing join for field " + fieldName); return join; } } @@ -641,10 +623,10 @@ private Attribute getAttribute(DataFetchingEnvironment environment, Argumen */ private EntityType getEntityType(GraphQLObjectType objectType) { return entityManager.getMetamodel() - .getEntities().stream() - .filter(it -> it.getName().equals(objectType.getName())) - .findFirst() - .get(); + .getEntities().stream() + .filter(it -> it.getName().equals(objectType.getName())) + .findFirst() + .get(); } /** @@ -667,16 +649,15 @@ private GraphQLObjectType getObjectType(DataFetchingEnvironment environment, Arg } protected Optional extractArgument(DataFetchingEnvironment environment, Field field, String argumentName) { - return field.getArguments() - .stream() - .filter(it -> argumentName.equals(it.getName())) - .findFirst(); + return field.getArguments().stream() + .filter(it -> argumentName.equals(it.getName())) + .findFirst(); } protected Argument extractArgument(DataFetchingEnvironment environment, Field field, String argumentName, Value defaultValue) { return extractArgument(environment, field, argumentName) - .orElse(new Argument(argumentName, defaultValue)); + .orElse(new Argument(argumentName, defaultValue)); } protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { @@ -760,10 +741,9 @@ protected final Stream selections(Field field) { ? field.getSelectionSet() : new SelectionSet(Collections.emptyList()); - return selectionSet.getSelections() - .stream() - .filter(it -> it instanceof Field) - .map(it -> (Field) it); + return selectionSet.getSelections().stream() + .filter(it -> it instanceof Field) + .map(it -> (Field) it); } protected final Stream flatten(Field field) { @@ -773,19 +753,17 @@ protected final Stream flatten(Field field) { return Stream.concat( Stream.of(field), - selectionSet.getSelections() - .stream() - .filter(it -> it instanceof Field) - .flatMap(selection -> this.flatten((Field) selection)) + selectionSet.getSelections().stream() + .filter(it -> it instanceof Field) + .flatMap(selection -> this.flatten((Field) selection)) ); } @SuppressWarnings( "unchecked" ) protected final R getObjectFieldValue(ObjectValue objectValue, String fieldName) { - return (R) getObjectField(objectValue, fieldName) - .map(it-> it.getValue()) - .orElse(new NullValue()); + return (R) getObjectField(objectValue, fieldName).map(it-> it.getValue()) + .orElse(new NullValue()); } @SuppressWarnings( "unchecked" ) @@ -795,19 +773,22 @@ protected final R getArgumentValue(Argument argument) { protected final Optional getObjectField(ObjectValue objectValue, String fieldName) { return objectValue.getObjectFields().stream() - .filter(it -> fieldName.equals(it.getName())) - .findFirst(); + .filter(it -> fieldName.equals(it.getName())) + .findFirst(); } protected final Optional getSelectionField(Field field, String fieldName) { return field.getSelectionSet().getSelections().stream() - .filter(it -> it instanceof Field) - .map(it -> (Field) it) - .filter(it -> fieldName.equals(it.getName())) - .findFirst(); + .filter(it -> it instanceof Field) + .map(it -> (Field) it) + .filter(it -> fieldName.equals(it.getName())) + .findFirst(); } + @SuppressWarnings("rawtypes") class NullValue implements Value { + + private static final long serialVersionUID = 1L; @Override public List getChildren() { 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 c1e97465a..7f1e88731 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 @@ -19,11 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.HashMap; +import java.util.Map; 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; @@ -35,6 +34,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) @TestPropertySource({"classpath:hibernate.properties"}) @@ -473,5 +475,201 @@ 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" + + " }" + + " }" + + " }"+ + "}"; + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL, author={id=1, name=Leo Tolstoy}}, " + + "{id=3, title=Anna Karenina, genre=NOVEL, author={id=1, name=Leo Tolstoy}}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // 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" + + " }" + + " }"+ + "}"; + Map variables = new HashMap() {{ + put("authorId", 1L); + }}; + + + String expected = "{Books={select=[" + + "{id=2, title=War and Peace, genre=NOVEL}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}" + + "]}}"; + + //when + Object result = executor.execute(query, variables).getData(); + + // then + assertThat(result.toString()).isEqualTo(expected); + } + + + + @Test + public void queryForAuthorssWithWhereBooksGenreEquals() { + //given + String query = "query { " + + "Authors(where: {books: {genre: {EQ: NOVEL}}}) {" + + " 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}, {id=3, title=Anna Karenina, genre=NOVEL}]}" + + "]}}"; + + //when + Object result = executor.execute(query).getData(); + + // 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" + + " }" + + " }" + + " }" + + "}"; + + 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 queryWithWhereInsideOneToManyRelationsWithExplictAND() { + //given: + String query = "query { " + + "Authors(where: {" + + " 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}]}" + + "]}}"; + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideOneToManyRelationsWithExplictOR() { + //given: + String query = "query { " + + "Authors(where: {" + + " books: {" + + " OR: { "+ + " 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}, " + + "{id=3, title=Anna Karenina, genre=NOVEL}]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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 e033dbc7c..bfcb4b0d0 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 @@ -26,8 +26,6 @@ import javax.persistence.Query; import javax.transaction.Transactional; -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; @@ -37,6 +35,9 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaExecutor; +import com.introproventures.graphql.jpa.query.schema.impl.GraphQLJpaSchemaBuilder; + @RunWith(SpringRunner.class) @SpringBootTest @TestPropertySource({"classpath:hibernate.properties"}) @@ -591,4 +592,163 @@ public void queryWithStringNotBetweenPredicate() { 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 expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS]}}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2, appearsIn=[A_NEW_HOPE, EMPIRE_STRIKES_BACK, RETURN_OF_THE_JEDI, THE_FORCE_AWAKENS]}}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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 expected = "{Humans={select=[]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " appearsIn" + + " }" + + " friends {" + + " id" + + " name" + + " appearsIn" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + + @Test + public void queryWithWhereInsideCompositeRelationsAndCollectionFiltering() { + //given: + String query = "query {" + + " Humans(where: {" + + " favoriteDroid: { id: {EQ: \"2000\"}}" + + " friends: {" + + " appearsIn: {IN: A_NEW_HOPE}" + + " }" + + " }) {" + + " select {" + + " id" + + " name" + + " favoriteDroid {" + + " id" + + " name" + + " primaryFunction" + + " }" + + " friends(where: {name: {LIKE: \"Leia\"}}) {" + + " id" + + " name" + + " }" + + " }" + + " } " + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={id=2000, name=C-3PO, primaryFunction=Protocol}, friends=[{id=1003, name=Leia Organa}]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //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" + + " }" + + " }" + + " }" + + "}"; + + String expected = "{Humans={select=[" + + "{id=1000, name=Luke Skywalker, favoriteDroid={name=C-3PO}, friends=[{name=C-3PO, appearsIn=[A_NEW_HOPE]}, {name=Han Solo, appearsIn=[A_NEW_HOPE]}, {name=Leia Organa, appearsIn=[A_NEW_HOPE]}, {name=R2-D2, appearsIn=[A_NEW_HOPE]}]}, " + + "{id=1001, name=Darth Vader, favoriteDroid={name=R2-D2}, friends=[{name=Wilhuff Tarkin, appearsIn=[A_NEW_HOPE]}]}" + + "]}}"; + + //when: + Object result = executor.execute(query).getData(); + + //then: + assertThat(result.toString()).isEqualTo(expected); + } + }