Skip to content

Commit

Permalink
Hibernate reactive panache refactoring
Browse files Browse the repository at this point in the history
- do not store the current reactive session in the CDI request context
but instead in the vertx duplicated context
- do not offload execution of a panache entity method on the current
vertx context but instead validate that the method is executed on the
vetx duplicated context
- introduce WithSession, WithSessionOnDemand and WithTransaction
bindings and interceptors
- deprecate ReactiveTransactional
- ReactiveTransactionalInterceptor can only be used for methods that
return Uni/Multi; this is validated at build time
- if resteasy-reactive is present then automatically add WithSessionOnDemand binding to all resource methods on classes that use a panache entity
- also remove the quarkus-integration-test-hibernate-reactive-panache-blocking module
- quarkus-test-vertx - run the test method on a duplicated vertx context even if the RunOnVertxContext is not present but the TestReactiveTransaction is
  • Loading branch information
mkouba committed Feb 6, 2023
1 parent 117817b commit 47b10ae
Show file tree
Hide file tree
Showing 36 changed files with 1,125 additions and 968 deletions.
213 changes: 121 additions & 92 deletions docs/src/main/asciidoc/hibernate-reactive-panache.adoc

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/src/main/asciidoc/hibernate-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ You can also inject an instance of `Uni<Mutiny.Session>` using the exact same me
Uni<Mutiny.Session> session;
----

[[testing]]
=== Testing

Using Hibernate Reactive in a `@QuarkusTest` is slightly more involved than using Hibernate ORM due to the asynchronous nature of the APIs and the fact that all operations need to run on a Vert.x Event Loop.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceUnit;

import org.hibernate.reactive.common.spi.Implementor;
import org.hibernate.reactive.common.spi.MutinyImplementor;
import org.hibernate.reactive.mutiny.Mutiny;
import org.hibernate.reactive.mutiny.impl.MutinySessionFactoryImpl;
Expand All @@ -28,7 +29,7 @@ public class ReactiveSessionFactoryProducer {
@ApplicationScoped
@DefaultBean
@Unremovable
@Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class })
@Typed({ Mutiny.SessionFactory.class, MutinyImplementor.class, Implementor.class })
public MutinySessionFactoryImpl mutinySessionFactory() {
if (jpaConfig.getDeactivatedPersistenceUnitNames()
.contains(HibernateReactive.DEFAULT_REACTIVE_PERSISTENCE_UNIT_NAME)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,39 @@
package io.quarkus.hibernate.reactive.panache.common.deployment;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;

import jakarta.annotation.Priority;
import jakarta.interceptor.Interceptor;
import jakarta.persistence.NamedQueries;
import jakarta.persistence.NamedQuery;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -31,9 +45,16 @@
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.hibernate.orm.deployment.HibernateOrmEnabled;
import io.quarkus.hibernate.orm.deployment.JpaModelBuildItem;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.quarkus.hibernate.reactive.panache.common.runtime.PanacheHibernateRecorder;
import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional;
import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactionalInterceptor;
import io.quarkus.hibernate.reactive.panache.common.runtime.TestReactiveTransactionalInterceptor;
import io.quarkus.hibernate.reactive.panache.common.runtime.WithSessionInterceptor;
import io.quarkus.hibernate.reactive.panache.common.runtime.WithSessionOnDemandInterceptor;
import io.smallrye.mutiny.Uni;

@BuildSteps(onlyIf = HibernateOrmEnabled.class)
public final class PanacheJpaCommonResourceProcessor {
Expand All @@ -42,6 +63,19 @@ public final class PanacheJpaCommonResourceProcessor {
private static final DotName DOTNAME_NAMED_QUERIES = DotName.createSimple(NamedQueries.class.getName());
private static final String TEST_REACTIVE_TRANSACTION = "io.quarkus.test.TestReactiveTransaction";

private static final DotName REACTIVE_TRANSACTIONAL = DotName.createSimple(ReactiveTransactional.class.getName());
private static final DotName WITH_SESSION_ON_DEMAND = DotName.createSimple(WithSessionOnDemand.class.getName());
private static final DotName WITH_SESSION = DotName.createSimple(WithSession.class.getName());
private static final DotName WITH_TRANSACTION = DotName.createSimple(WithTransaction.class.getName());
private static final DotName UNI = DotName.createSimple(Uni.class.getName());
private static final DotName PANACHE_ENTITY_BASE = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntityBase");
private static final DotName PANACHE_ENTITY = DotName.createSimple("io.quarkus.hibernate.reactive.panache.PanacheEntity");
private static final DotName PANACHE_KOTLIN_ENTITY_BASE = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntityBase");
private static final DotName PANACHE_KOTLIN_ENTITY = DotName
.createSimple("io.quarkus.hibernate.reactive.panache.kotlin.PanacheEntity");

@BuildStep(onlyIf = IsTest.class)
void testTx(BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildProducer,
BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
Expand All @@ -61,12 +95,101 @@ void testTx(BuildProducer<GeneratedBeanBuildItem> generatedBeanBuildItemBuildPro
}

@BuildStep
void registerInterceptor(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
void registerInterceptors(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder();
builder.addBeanClass(WithSessionOnDemandInterceptor.class);
builder.addBeanClass(WithSessionInterceptor.class);
builder.addBeanClass(ReactiveTransactionalInterceptor.class);
additionalBeans.produce(builder.build());
}

@BuildStep
void validateInterceptedMethods(ValidationPhaseBuildItem validationPhase,
BuildProducer<ValidationErrorBuildItem> errors) {
List<DotName> bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_ON_DEMAND, WITH_TRANSACTION);
for (BeanInfo bean : validationPhase.getContext().beans().withAroundInvokeInterceptor()) {
for (Entry<MethodInfo, Set<AnnotationInstance>> e : bean.getInterceptedMethodsBindings().entrySet()) {
DotName returnTypeName = e.getKey().returnType().name();
if (returnTypeName.equals(UNI)) {
// Method returns Uni - no need to iterate over the bindings
continue;
}
if (Annotations.containsAny(e.getValue(), bindings)) {
errors.produce(new ValidationErrorBuildItem(
new IllegalStateException(
"A method annotated with "
+ bindings.stream().map(b -> "@" + b.withoutPackagePrefix())
.collect(Collectors.toList())
+ " must return Uni: "
+ e.getKey() + " declared on " + e.getKey().declaringClass())));
}
}
}
}

@BuildStep
void transformResourceMethods(CombinedIndexBuildItem index, Capabilities capabilities,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer) {
if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) {
// Custom request method designators are not supported
List<DotName> designators = List.of(DotName.createSimple("jakarta.ws.rs.GET"),
DotName.createSimple("jakarta.ws.rs.HEAD"),
DotName.createSimple("jakarta.ws.rs.DELETE"), DotName.createSimple("jakarta.ws.rs.OPTIONS"),
DotName.createSimple("jakarta.ws.rs.PATCH"), DotName.createSimple("jakarta.ws.rs.POST"),
DotName.createSimple("jakarta.ws.rs.PUT"));
List<DotName> bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_ON_DEMAND, WITH_TRANSACTION);

// Collect all panache entities
Set<DotName> entities = new HashSet<>();
for (ClassInfo subclass : index.getIndex().getAllKnownSubclasses(PANACHE_ENTITY_BASE)) {
if (!subclass.name().equals(PANACHE_ENTITY)) {
entities.add(subclass.name());
}
}
for (ClassInfo subclass : index.getIndex().getAllKnownImplementors(PANACHE_KOTLIN_ENTITY_BASE)) {
if (!subclass.name().equals(PANACHE_KOTLIN_ENTITY)) {
entities.add(subclass.name());
}
}
Set<DotName> entityUsers = new HashSet<>();
for (DotName entity : entities) {
for (ClassInfo user : index.getIndex().getKnownUsers(entity)) {
entityUsers.add(user.name());
}
}

annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {
@Override
public boolean appliesTo(Kind kind) {
return kind == Kind.METHOD;
}

@Override
public void transform(TransformationContext context) {
MethodInfo method = context.getTarget().asMethod();
Collection<AnnotationInstance> annotations = context.getAnnotations();
if (method.isSynthetic()
|| Modifier.isStatic(method.flags())
|| method.declaringClass().isInterface()
|| !method.returnType().name().equals(UNI)
|| !entityUsers.contains(method.declaringClass().name())
|| !Annotations.containsAny(annotations, designators)
|| Annotations.containsAny(annotations, bindings)) {
return;
}
// Add @WithSessionOnDemand to a method that
// - is not static
// - is not synthetic
// - returns Uni
// - is declared in a class that uses a panache entity
// - is annotated with @GET, @POST, @PUT, @DELETE ,@PATCH ,@HEAD or @OPTIONS
// - is not annotated with @ReactiveTransactional, @WithSession, @WithSessionOnDemand, or @WithTransaction
context.transform().add(WITH_SESSION_ON_DEMAND).done();
}
}));
}
}

@BuildStep
void lookupNamedQueries(CombinedIndexBuildItem index,
BuildProducer<PanacheNamedQueryEntityClassBuildStep> namedQueries,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkus.hibernate.reactive.panache.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.interceptor.InterceptorBinding;

/**
* Instructs Panache to perform the work represented by the {@link io.smallrye.mutiny.Uni} returned from the intercepted method
* within a scope of a reactive {@link org.hibernate.reactive.mutiny.Mutiny.Session}.
* <p>
* If a reactive session exists when the {@link io.smallrye.mutiny.Uni} returned from the annotated method is
* triggered, then this session is reused. Otherwise, a new session is opened and eventually closed when the
* {@link io.smallrye.mutiny.Uni} completes.
* <p>
* A method annotated with this annotation must return either {@link io.smallrye.mutiny.Uni}. If declared on a class then all
* methods that are intercepted must return {@link io.smallrye.mutiny.Uni}.
*/
@Inherited
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WithSession {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.quarkus.hibernate.reactive.panache.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.interceptor.InterceptorBinding;

/**
* Instructs Panache to trigger the {@link io.smallrye.mutiny.Uni} returned from the intercepted method within a scope of a
* reactive {@link org.hibernate.reactive.mutiny.Mutiny.Session} (if needed). If a reactive session exists when the
* {@link io.smallrye.mutiny.Uni} returned from the annotated method is triggered, then this session is reused. Otherwise, a new
* session is opened <b>when needed</b> and eventually closed when the
* {@link io.smallrye.mutiny.Uni} completes.
* <p>
* A method annotated with this annotation must return {@link io.smallrye.mutiny.Uni}. If declared on a class then all methods
* that are intercepted must return {@link io.smallrye.mutiny.Uni}.
*/
@Inherited
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WithSessionOnDemand {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.hibernate.reactive.panache.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import jakarta.interceptor.InterceptorBinding;

/**
* Instructs Panache to trigger the {@link io.smallrye.mutiny.Uni} returned from the intercepted method within a scope of a
* reactive {@link org.hibernate.reactive.mutiny.Mutiny.Transaction}. If a reactive session exists when the
* {@link io.smallrye.mutiny.Uni} returned from the annotated method is triggered, then this session is reused. Otherwise, a new
* session is opened and eventually closed when the
* {@link io.smallrye.mutiny.Uni} completes.
* <p>
* A method annotated with this annotation must return {@link io.smallrye.mutiny.Uni}. If declared on a class then all methods
* that are intercepted must return {@link io.smallrye.mutiny.Uni}.
*
* @see org.hibernate.reactive.mutiny.Mutiny.SessionFactory#withTransaction(java.util.function.Function)
*/
@Inherited
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(value = RetentionPolicy.RUNTIME)
public @interface WithTransaction {

}
Loading

0 comments on commit 47b10ae

Please sign in to comment.