smallrye-graphql
smallrye-graphql copied to clipboard
Problem with PersistentSet and Hibernate
I'm running into an issue with GraphQL not playing nice with Hibernate and JPA.
I've got an AppUser
class with a @ManyToMany
join to a set of AppRole
. I'm using the SmallRye GraphQL client v1.3.3 as the UI, and smallrye graphql v1.1.0 on the back end. If I request basic fields from the AppUser
without accessing the set of roles, it works fine. But if I try to access the roles then I get the following exception:
15:37:36,580 ERROR [io.smallrye.graphql] (default task-1) SRGQL012000: Data Fetching Error: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: gov.utah.health.model.user.AppUser.roles, could not initialize proxy - no Session
at [email protected]//org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
at [email protected]//org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
at [email protected]//org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581)
at [email protected]//org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148)
at [email protected]//org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:188)
at [email protected]//io.smallrye.graphql.execution.datafetcher.helper.AbstractHelper.recursiveTransformCollection(AbstractHelper.java:205)
at [email protected]//io.smallrye.graphql.execution.datafetcher.helper.AbstractHelper.recursiveTransform(AbstractHelper.java:72)
at [email protected]//io.smallrye.graphql.execution.datafetcher.helper.FieldHelper.transformResponse(FieldHelper.java:32)
at [email protected]//io.smallrye.graphql.execution.datafetcher.PropertyDataFetcher.get(PropertyDataFetcher.java:36)
at [email protected]//graphql.execution.ExecutionStrategy.fetchField(ExecutionStrategy.java:270)
at [email protected]//graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:203)
at [email protected]//graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:60)
at [email protected]//graphql.execution.ExecutionStrategy.completeValueForObject(ExecutionStrategy.java:646)
at [email protected]//graphql.execution.ExecutionStrategy.completeValue(ExecutionStrategy.java:438)
at [email protected]//graphql.execution.ExecutionStrategy.completeField(ExecutionStrategy.java:390)
at [email protected]//graphql.execution.ExecutionStrategy.lambda$resolveFieldWithInfo$1(ExecutionStrategy.java:205)
at java.base/java.util.concurrent.CompletableFuture.uniApplyNow(CompletableFuture.java:680)
at java.base/java.util.concurrent.CompletableFuture.uniApplyStage(CompletableFuture.java:658)
at java.base/java.util.concurrent.CompletableFuture.thenApply(CompletableFuture.java:2094)
at [email protected]//graphql.execution.ExecutionStrategy.resolveFieldWithInfo(ExecutionStrategy.java:204)
at [email protected]//graphql.execution.AsyncExecutionStrategy.execute(AsyncExecutionStrategy.java:60)
at [email protected]//graphql.execution.Execution.executeOperation(Execution.java:165)
at [email protected]//graphql.execution.Execution.execute(Execution.java:104)
at [email protected]//graphql.GraphQL.execute(GraphQL.java:557)
at [email protected]//graphql.GraphQL.parseValidateAndExecute(GraphQL.java:482)
at [email protected]//graphql.GraphQL.executeAsync(GraphQL.java:446)
at [email protected]//graphql.GraphQL.execute(GraphQL.java:377)
at [email protected]//io.smallrye.graphql.execution.ExecutionService.execute(ExecutionService.java:123)
at [email protected]//io.smallrye.graphql.servlet.ExecutionServlet.handleInput(ExecutionServlet.java:93)
at [email protected]//io.smallrye.graphql.servlet.ExecutionServlet.handleInput(ExecutionServlet.java:88)
at [email protected]//io.smallrye.graphql.servlet.ExecutionServlet.doPost(ExecutionServlet.java:78)
at [email protected]//javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
at [email protected]//javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
at [email protected]//io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
at [email protected]//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
at io.opentracing.contrib.opentracing-jaxrs2//io.opentracing.contrib.jaxrs2.server.SpanFinishingFilter.doFilter(SpanFinishingFilter.java:52)
at [email protected]//io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at [email protected]//io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at [email protected]//io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at [email protected]//io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at [email protected]//io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
at [email protected]//io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at [email protected]//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.lambda$handleRequest$1(ElytronRunAsHandler.java:68)
at [email protected]//org.wildfly.security.auth.server.FlexibleIdentityAssociation.runAsFunctionEx(FlexibleIdentityAssociation.java:103)
at [email protected]//org.wildfly.security.auth.server.Scoped.runAsFunctionEx(Scoped.java:161)
at [email protected]//org.wildfly.security.auth.server.Scoped.runAs(Scoped.java:73)
at [email protected]//org.wildfly.elytron.web.undertow.server.ElytronRunAsHandler.handleRequest(ElytronRunAsHandler.java:67)
at [email protected]//io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
at [email protected]//io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
at [email protected]//io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at [email protected]//io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at [email protected]//io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at [email protected]//io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at org.wildfly.security.elytron-web.undertow-server-servlet@1.9.1.Final//org.wildfly.elytron.web.undertow.server.servlet.CleanUpHandler.handleRequest(CleanUpHandler.java:38)
at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at [email protected]//org.wildfly.extension.undertow.security.jacc.JACCContextIdHandler.handleRequest(JACCContextIdHandler.java:61)
at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at [email protected]//org.wildfly.extension.undertow.deployment.GlobalRequestControllerHandler.handleRequest(GlobalRequestControllerHandler.java:68)
at [email protected]//io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
at [email protected]//io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:280)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:79)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:134)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:131)
at [email protected]//io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
at [email protected]//io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1544)
at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1544)
at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1544)
at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1544)
at [email protected]//org.wildfly.extension.undertow.deployment.UndertowDeploymentInfoService$UndertowThreadSetupAction.lambda$create$0(UndertowDeploymentInfoService.java:1544)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:260)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:79)
at [email protected]//io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:100)
at [email protected]//io.undertow.server.Connectors.executeRootHandler(Connectors.java:387)
at [email protected]//io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:852)
at [email protected]//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
at [email protected]//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
at [email protected]//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
at [email protected]//org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1280)
at java.base/java.lang.Thread.run(Thread.java:829)
This might sound familiar. Issue #71 and Issue #95 are both similar. #95 seems like it should have solved this back with the v1.0.1 release. The exception seems to be a little different, though.
Now if I tell JPA to eager-load rather than lazy-load the AppRole
set, of course it works, but then it's always loading all data whether it's needed or not. I can also avoid an exception if I set <property name="hibernate.enable_lazy_load_no_trans" value="true"/>
in persistence.xml, but that's just hitting the database every time the roles are accessed, not suitable for production code.
I've tried annotating alternately my @GraphQLApi public class AppUserService
and the individual @Query("user") public AppUser getUser(...)
method with javax.transaction.Transactional
, but that doesn't seem to have made any difference. The getUser()
method finishes executing and I assume the transaction ends there before reaching the graphql code where the exception is thrown.
Is there something I'm missing?
Edit: I also just gave it a try with smallrye-graphql v1.3.5 (latest) and got the same error.
Hi @mouseas . Thanks for this detailed issue. Do you perhaps have a small reproducer I can look at ?
As this kind of problem has many moving parts, it's very difficult to reproduce. Could you provide a reproducer, so we can concentrate on the solution and not on trying out all the different possible combinations?
@phillip-kruger: Hah, you where a bit faster, but GitHub didn't tell me ;-)
Created it at https://github.com/mouseas/smallrye-issue-reproducer
I'm hoping the wildfly setup isn't too far from what you already have on hand.
I am not sure whether it matters if the database is PostgreSQL or if it can be just any type of sql database.
@phillip-kruger, @t1 I put together a reproducer, were you able to get it to reproduce the issue for you?
@mouseas - not yet, but it;s on my TODO list :)
My current workaround is to copy the contents of collections to, e.g. HashSet and replacing the JPA-created collections before leaving my code. Why is graphql copying collections in the first place?
Nope, that workaround doesn't work either. JPA tries to persist changes if I assign a new HashSet.
Is there a way I can access which fields need to be loaded from within a @GraphQLApi
and @Query
context so I can make sure the requested fields have been explicitly loaded? Can I get the HttpRequest object somehow?
You can inject @Context from smallrye and get the requested fields from there
@javax.ws.rs.core.Context
? ...Doesn't seem right. Can I get an example?
No this one https://github.com/phillip-kruger/graphql-experimental/blob/05cd20c6e9944471143f883939b105c676d49421/context-example/src/main/java/com/github/phillipkruger/service/PersonService.java#L30
See this example https://github.com/phillip-kruger/graphql-experimental/tree/main/context-example
import io.smallrye.graphql.api.Context;
Also see this : https://www.phillip-kruger.com/post/experimental_graphql/
Ok, I was able to resolve lazy loading errors using the io.smallrye.graphql.api.Context
and and reflection to call the getters for any requested fields before leaving the database query context.
Is there a way to also get the http session or request? I have the server set up so graphql requests require an OAuth bearer token which is validated against a user database, but within a @Query("getThing")
context I don't know how to access the data about the user making the request to see if they can access the data they're requesting.
It would be better to use Roles for something like this, then you can annotate the method with RolesAllowed. You should also be able to inject the request and the session and the user as request scoped vars in your class
This is roughly what I've got, but the session
is null when I access it.
@Named
@GraphQLApi
public class PersonService implements Serializable {
@javax.enterprise.context.RequestScoped
HttpSession session;
@Query("findPeople")
@RolesAllowed("user")
public Person findPeople(String firstName, String lastName, Context context) {
System.out.println(session.toString);
// do stuff
}
}
try:
@Inject HttpSession session;
also this should work:
@Resource Principal principal;