Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for lazy loaded attribute fetching with JPA entity graph query hint #387

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions build/pom.xml
Expand Up @@ -15,6 +15,13 @@

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-platform</artifactId>
<version>${hibernate.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
Expand Down Expand Up @@ -75,6 +82,11 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
Expand Down
1 change: 1 addition & 0 deletions dependencies/pom.xml
Expand Up @@ -20,6 +20,7 @@
<joda-time.version>2.12.5</joda-time.version>
<graphql-java-extended-scalars.version>20.2</graphql-java-extended-scalars.version>
<jakarta.persistence-api.version>3.1.0</jakarta.persistence-api.version>
<hibernate.version>6.2.3.Final</hibernate.version>
</properties>

<dependencyManagement>
Expand Down
Expand Up @@ -139,6 +139,7 @@
newScalarType("SqlTimestamp", "SQL Timestamp type", new GraphQLSqlTimestampCoercing())
);
scalarsRegistry.put(Byte[].class, newScalarType("ByteArray", "ByteArray type", new GraphQLLOBCoercing()));
scalarsRegistry.put(byte[].class, newScalarType("ByteArray", "ByteArray type", new GraphQLLOBCoercing()));
scalarsRegistry.put(Instant.class, newScalarType("Instant", "Instant type", new GraphQLInstantCoercing()));
scalarsRegistry.put(
ZonedDateTime.class,
Expand All @@ -158,6 +159,10 @@
return Collections.unmodifiableCollection(scalarsRegistry.values());
}

public static boolean contains(Class<?> key) {
return scalarsRegistry.containsKey(key);

Check warning on line 163 in scalars/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java

View check run for this annotation

Codecov / codecov/patch

scalars/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java#L163

Added line #L163 was not covered by tests
}

public static GraphQLScalarType of(Class<?> key) {
return scalarsRegistry.computeIfAbsent(key, JavaScalars::computeGraphQLScalarType);
}
Expand Down Expand Up @@ -669,7 +674,7 @@
@Override
public Object serialize(Object input) {
if (input.getClass() == byte[].class) {
return input;
return new String((byte[]) input, StandardCharsets.UTF_8);

Check warning on line 677 in scalars/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java

View check run for this annotation

Codecov / codecov/patch

scalars/src/main/java/com/introproventures/graphql/jpa/query/schema/JavaScalars.java#L677

Added line #L677 was not covered by tests
}
return null;
}
Expand Down
Expand Up @@ -22,6 +22,7 @@
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.isLogicalArgument;
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.isPageArgument;
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.isWhereArgument;
import static com.introproventures.graphql.jpa.query.support.GraphQLSupport.selections;
import static graphql.introspection.Introspection.SchemaMetaFieldDef;
import static graphql.introspection.Introspection.TypeMetaFieldDef;
import static graphql.introspection.Introspection.TypeNameMetaFieldDef;
Expand Down Expand Up @@ -60,7 +61,9 @@
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Subgraph;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.AbstractQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
Expand Down Expand Up @@ -121,6 +124,7 @@ public final class GraphQLJpaQueryFactory {
private static final String DESC = "DESC";

private static final Logger logger = LoggerFactory.getLogger(GraphQLJpaQueryFactory.class);
public static final String JAKARTA_PERSISTENCE_FETCHGRAPH = "jakarta.persistence.fetchgraph";
private static Function<Object, Object> unproxy;

static {
Expand Down Expand Up @@ -260,15 +264,26 @@ protected Stream<Object> queryResultStream(DataFetchingEnvironment environment,
keys.toArray()
);

// Let's create entity graph from selection
var entityGraph = createEntityGraph(queryEnvironment);

// Let's execute query and get wrap result into stream
return getResultStream(query, fetchSize, isDistinct);
return getResultStream(query, fetchSize, isDistinct, entityGraph);
}

protected <T> Stream<T> getResultStream(TypedQuery<T> query, int fetchSize, boolean isDistinct) {
protected <T> Stream<T> getResultStream(
TypedQuery<T> query,
int fetchSize,
boolean isDistinct,
EntityGraph<?> entityGraph
) {
// Let' try reduce overhead and disable all caching
query.setHint(ORG_HIBERNATE_READ_ONLY, true);
query.setHint(ORG_HIBERNATE_FETCH_SIZE, fetchSize);
query.setHint(ORG_HIBERNATE_CACHEABLE, false);
if (entityGraph != null) {
query.setHint(JAKARTA_PERSISTENCE_FETCHGRAPH, entityGraph);
}

if (logger.isDebugEnabled()) {
logger.info("\nGraphQL JPQL Fetch Query String:\n {}", getJPQLQueryString(query));
Expand Down Expand Up @@ -441,7 +456,9 @@ protected Map<Object, List<Object>> loadOneToMany(DataFetchingEnvironment enviro

TypedQuery<Object[]> query = getBatchQuery(environment, field, isDefaultDistinct(), keys);

List<Object[]> resultList = getResultList(query);
var entityGraph = createEntityGraph(environment);

List<Object[]> resultList = getResultList(query, entityGraph);

if (logger.isTraceEnabled()) {
logger.trace(
Expand Down Expand Up @@ -477,7 +494,9 @@ protected Map<Object, Object> loadManyToOne(DataFetchingEnvironment environment,

TypedQuery<Object[]> query = getBatchQuery(environment, field, isDefaultDistinct(), keys);

List<Object[]> resultList = getResultList(query);
var entityGraph = createEntityGraph(environment);

List<Object[]> resultList = getResultList(query, entityGraph);

Map<Object, Object> resultMap = new LinkedHashMap<>(resultList.size());

Expand All @@ -486,7 +505,7 @@ protected Map<Object, Object> loadManyToOne(DataFetchingEnvironment environment,
return resultMap;
}

protected <T> List<T> getResultList(TypedQuery<T> query) {
protected <T> List<T> getResultList(TypedQuery<T> query, EntityGraph<?> entityGraph) {
if (logger.isDebugEnabled()) {
logger.info("\nGraphQL JPQL Batch Query String:\n {}", getJPQLQueryString(query));
}
Expand All @@ -496,6 +515,10 @@ protected <T> List<T> getResultList(TypedQuery<T> query) {
query.setHint(ORG_HIBERNATE_FETCH_SIZE, defaultFetchSize);
query.setHint(ORG_HIBERNATE_CACHEABLE, false);

if (entityGraph != null) {
query.setHint(JAKARTA_PERSISTENCE_FETCHGRAPH, entityGraph);
}

return query.getResultList();
}

Expand Down Expand Up @@ -1693,8 +1716,10 @@ private EmbeddableType<?> computeEmbeddableType(GraphQLObjectType objectType) {
* @return resolved GraphQL object type or null if no output type is provided
*/
private GraphQLObjectType getObjectType(DataFetchingEnvironment environment) {
GraphQLType outputType = environment.getFieldType();
return getObjectType(environment.getFieldType());
}

private GraphQLObjectType getObjectType(GraphQLType outputType) {
if (outputType instanceof GraphQLList) outputType = ((GraphQLList) outputType).getWrappedType();

if (outputType instanceof GraphQLObjectType) return (GraphQLObjectType) outputType;
Expand Down Expand Up @@ -1976,6 +2001,88 @@ private <T> T detach(T entity) {
return entity;
}

EntityGraph<?> createEntityGraph(DataFetchingEnvironment environment) {
Field root = environment.getMergedField().getSingleField();
GraphQLObjectType fieldType = getObjectType(environment);
EntityType<?> entityType = getEntityType(fieldType);

EntityGraph<?> entityGraph = entityManager.createEntityGraph(entityType.getJavaType());

var entityDescriptor = EntityIntrospector.introspect(entityType);

selections(root)
.forEach(selectedField -> {
var propertyDescriptor = entityDescriptor.getPropertyDescriptor(selectedField.getName());

propertyDescriptor
.flatMap(AttributePropertyDescriptor::getAttribute)
.ifPresent(attribute -> {
if (
isManagedType(attribute) && hasSelectionSet(selectedField) && hasNoArguments(selectedField)
) {
var attributeFieldDefinition = fieldType.getFieldDefinition(attribute.getName());
entityGraph.addAttributeNodes(attribute.getName());
addSubgraph(
selectedField,
attributeFieldDefinition,
entityGraph.addSubgraph(attribute.getName())
);
} else if (isBasic(attribute)) {
entityGraph.addAttributeNodes(attribute.getName());
}
});
});

return entityGraph;
}

void addSubgraph(Field field, GraphQLFieldDefinition fieldDefinition, Subgraph<?> subgraph) {
var fieldObjectType = getObjectType(fieldDefinition.getType());
var fieldEntityType = getEntityType(fieldObjectType);
var fieldEntityDescriptor = EntityIntrospector.introspect(fieldEntityType);

selections(field)
.forEach(selectedField -> {
var propertyDescriptor = fieldEntityDescriptor.getPropertyDescriptor(selectedField.getName());

propertyDescriptor
.flatMap(AttributePropertyDescriptor::getAttribute)
.ifPresent(attribute -> {
var selectedName = selectedField.getName();

if (
hasSelectionSet(selectedField) && isManagedType(attribute) && hasNoArguments(selectedField)
) {
var selectedFieldDefinition = fieldObjectType.getFieldDefinition(selectedName);
subgraph.addAttributeNodes(selectedName);
addSubgraph(selectedField, selectedFieldDefinition, subgraph.addSubgraph(selectedName));
} else if (isBasic(attribute)) {
subgraph.addAttributeNodes(selectedName);
}
});
});
}

static boolean isManagedType(Attribute<?, ?> attribute) {
return (
attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.EMBEDDED &&
attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC &&
attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.ELEMENT_COLLECTION
);
}

static boolean isBasic(Attribute<?, ?> attribute) {
return !isManagedType(attribute);
}

static boolean hasNoArguments(Field field) {
return !hasArguments(field);
}

static boolean hasArguments(Field field) {
return field.getArguments() != null && !field.getArguments().isEmpty();
}

/**
* Creates builder to build {@link GraphQLJpaQueryFactory}.
* @return created builder
Expand Down
Expand Up @@ -1412,7 +1412,7 @@ private GraphQLOutputType getGraphQLTypeFromJavaType(Class<?> clazz) {
classCache.putIfAbsent(clazz, enumType);

return enumType;
} else if (clazz.isArray()) {
} else if (clazz.isArray() && !JavaScalars.contains(clazz)) {
return GraphQLList.list(JavaScalars.of(clazz.getComponentType()));
}

Expand Down
Expand Up @@ -792,6 +792,59 @@ public void queryBooksAuthorWithExplictOptionalTrue() {
assertThat(result.toString()).isEqualTo(expected);
}

@Test
public void queryAuthorsWithLazyLoadProfilePicture() {
//given
String query =
"""
query {
Authors {
select {
id
name
profilePicture
}
}
}
""";

String expected =
"""
{Authors={select=[{id=1, name=Leo Tolstoy, profilePicture=base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==}, {id=4, name=Anton Chekhov, profilePicture=base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==}, {id=8, name=Igor Dianov, profilePicture=base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==}]}}
""";

//when
Object result = executor.execute(query).getData();

// then
assertThat(result.toString()).isEqualTo(expected.strip());
}

@Test
public void queryAuthorsWithNoProfilePicture() {
//given
String query =
"""
query {
Authors {
select {
id
name
}
}
}
""";

String expected =
"{Authors={select=[{id=1, name=Leo Tolstoy}, {id=4, name=Anton Chekhov}, {id=8, name=Igor Dianov}]}}";

//when
Object result = executor.execute(query).getData();

// then
assertThat(result.toString()).isEqualTo(expected.strip());
}

// https://github.com/introproventures/graphql-jpa-query/issues/30
@Test
public void queryForEntityWithMappedSuperclass() {
Expand Down
8 changes: 4 additions & 4 deletions schema/src/test/resources/data.sql
Expand Up @@ -112,19 +112,19 @@ insert into thing (id, type) values
('2D1EBC5B7D2741979CF0E84451C5BBB1', 'Thing1');

-- Books
insert into author (id, name, genre) values (1, 'Leo Tolstoy', 'NOVEL');
insert into author (id, name, genre, profile_picture) values (1, 'Leo Tolstoy', 'NOVEL', 'base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==');
insert into book (id, title, author_id, genre, publication_date, description)
values (2, 'War and Peace', 1, 'NOVEL', '1869-01-01', 'The novel chronicles the history of the French invasion of Russia and the impact of the Napoleonic era on Tsarist society through the stories of five Russian aristocratic families.');
insert into book (id, title, author_id, genre, publication_date, description)
values (3, 'Anna Karenina', 1, 'NOVEL', '1877-04-01', 'A complex novel in eight parts, with more than a dozen major characters, it is spread over more than 800 pages (depending on the translation), typically contained in two volumes.');
insert into author (id, name, genre) values (4, 'Anton Chekhov', 'PLAY');
insert into author (id, name, genre, profile_picture) values (4, 'Anton Chekhov', 'PLAY', 'base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==');
insert into book (id, title, author_id, genre, publication_date, description)
values (5, 'The Cherry Orchard', 4, 'PLAY', '1904-01-17', 'The play concerns an aristocratic Russian landowner who returns to her family estate (which includes a large and well-known cherry orchard) just before it is auctioned to pay the mortgage.');
insert into book (id, title, author_id, genre, publication_date, description)
values (6, 'The Seagull', 4, 'PLAY', '1896-10-17', 'It dramatises the romantic and artistic conflicts between four characters');
insert into book (id, title, author_id, genre, publication_date, description)
values (7, 'Three Sisters', 4, 'PLAY', '1900-01-01', 'The play is sometimes included on the short list of Chekhov''s outstanding plays, along with The Cherry Orchard, The Seagull and Uncle Vanya.[1]');
insert into author (id, name, genre) values (8, 'Igor Dianov', 'JAVA');
insert into author (id, name, genre, profile_picture) values (8, 'Igor Dianov', 'JAVA', 'base64:iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlElEQVR4nO2UQQrEIAxFe/8jSRUrKtZFEasIQs7yh3RRKEOnixmYjYsQ0OT5E2ImIsIvbRpAjB7iq7GptcIYg947rLUopbwl5JzPGPattXtgSgkhBAghsG3b4fncOQfv/RnDIL7jB+d5/qxQKYV1XS9ArTWWZbkoZHsExhgPICfdlcxKpZTY9/25ZBp/mcb6on9s7Bc+TJAvSO7XjwAAAABJRU5ErkJggg==');

insert into book_tags (book_id, tags) values (2, 'war'), (2, 'piece');
insert into book_tags (book_id, tags) values (3, 'anna'), (3, 'karenina');
Expand Down Expand Up @@ -163,4 +163,4 @@ insert into calculated_entity (id, title, info) values
-- FloatingThing
insert into floating_thing (id, float_value, double_value, big_decimal_value) values
(1, 4.55, 4.55, 4.55),
(2, -0.44, -0.44, -0.44)
(2, -0.44, -0.44, -0.44)
20 changes: 20 additions & 0 deletions tests/models/books/pom.xml
Expand Up @@ -24,4 +24,24 @@
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<executions>
<execution>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>