smallrye-graphql icon indicating copy to clipboard operation
smallrye-graphql copied to clipboard

Problem with PersistentSet and Hibernate

Open mouseas opened this issue 3 years ago • 18 comments

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.

mouseas avatar Nov 22 '21 23:11 mouseas

Hi @mouseas . Thanks for this detailed issue. Do you perhaps have a small reproducer I can look at ?

phillip-kruger avatar Nov 23 '21 06:11 phillip-kruger

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?

t1 avatar Nov 23 '21 06:11 t1

@phillip-kruger: Hah, you where a bit faster, but GitHub didn't tell me ;-)

t1 avatar Nov 23 '21 06:11 t1

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.

mouseas avatar Nov 23 '21 17:11 mouseas

@phillip-kruger, @t1 I put together a reproducer, were you able to get it to reproduce the issue for you?

mouseas avatar Dec 02 '21 14:12 mouseas

@mouseas - not yet, but it;s on my TODO list :)

phillip-kruger avatar Dec 02 '21 14:12 phillip-kruger

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?

mouseas avatar Dec 03 '21 21:12 mouseas

Nope, that workaround doesn't work either. JPA tries to persist changes if I assign a new HashSet.

mouseas avatar Dec 07 '21 22:12 mouseas

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?

mouseas avatar Feb 25 '22 18:02 mouseas

You can inject @Context from smallrye and get the requested fields from there

phillip-kruger avatar Feb 25 '22 21:02 phillip-kruger

@javax.ws.rs.core.Context? ...Doesn't seem right. Can I get an example?

mouseas avatar Feb 25 '22 21:02 mouseas

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

phillip-kruger avatar Feb 25 '22 21:02 phillip-kruger

​import​ ​io.smallrye.graphql.api.Context​;

phillip-kruger avatar Feb 25 '22 21:02 phillip-kruger

Also see this : https://www.phillip-kruger.com/post/experimental_graphql/

phillip-kruger avatar Feb 25 '22 21:02 phillip-kruger

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.

mouseas avatar Mar 01 '22 18:03 mouseas

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

phillip-kruger avatar Mar 01 '22 19:03 phillip-kruger

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
    }
}

mouseas avatar Mar 01 '22 21:03 mouseas

try: @Inject HttpSession session;

also this should work:

@Resource Principal principal;

phillip-kruger avatar Mar 01 '22 21:03 phillip-kruger