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; 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 Dec 21, 2022
1 parent aaeda3e commit 0b56ce8
Show file tree
Hide file tree
Showing 29 changed files with 909 additions and 798 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import javax.persistence.EntityManagerFactory;
import javax.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,9 +1,12 @@
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 javax.annotation.Priority;
Expand All @@ -12,14 +15,24 @@
import javax.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 +44,15 @@
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.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 +61,18 @@ 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_IF_NEEDED = DotName.createSimple(WithSessionOnDemand.class.getName());
private static final DotName WITH_SESSION = DotName.createSimple(WithSession.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 +92,97 @@ 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_IF_NEEDED);
for (BeanInfo bean : validationPhase.getContext().beans().withAroundInvokeInterceptor()) {
for (Entry<MethodInfo, Set<AnnotationInstance>> e : bean.getInterceptedMethodsBindings().entrySet()) {
if (e.getKey().returnType().name().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 @ReactiveTransactional, @WithSession or @WithSessionIfNeeded must return Uni: "
+ e.getKey())));
}
}
}
}

@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("javax.ws.rs.GET"),
DotName.createSimple("javax.ws.rs.HEAD"),
DotName.createSimple("javax.ws.rs.DELETE"), DotName.createSimple("javax.ws.rs.OPTIONS"),
DotName.createSimple("javax.ws.rs.PATCH"), DotName.createSimple("javax.ws.rs.POST"),
DotName.createSimple("javax.ws.rs.PUT"));
List<DotName> bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_IF_NEEDED);

// 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().getAllKnownSubclasses(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 @WithSessionIfNeeded 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 or @WithSessionIfNeeded
context.transform().add(WITH_SESSION_IF_NEEDED).done();
}
}));
}
}

@BuildStep
void lookupNamedQueries(CombinedIndexBuildItem index,
BuildProducer<PanacheNamedQueryEntityClassBuildStep> namedQueries,
Expand Down
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 javax.interceptor.InterceptorBinding;

import org.hibernate.reactive.mutiny.Mutiny;

import io.smallrye.mutiny.Uni;

/**
* Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive
* {@link Mutiny.Session}. If a reactive session exists when the {@link 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 Uni} completes.
* <p>
* A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted
* must return {@link 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,30 @@
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 javax.interceptor.InterceptorBinding;

import org.hibernate.reactive.mutiny.Mutiny;

import io.smallrye.mutiny.Uni;

/**
* Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive
* {@link Mutiny.Session} (if needed). If a reactive session exists when the {@link 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 Uni} completes.
* <p>
* A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted
* must return {@link 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,33 @@
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 javax.interceptor.InterceptorBinding;

import org.hibernate.reactive.mutiny.Mutiny;
import org.hibernate.reactive.mutiny.Mutiny.SessionFactory;

import io.smallrye.mutiny.Uni;

/**
* Instructs Panache to trigger the {@link Uni} returned from the intercepted method within a scope of a reactive
* {@link Mutiny.Transaction}. If a reactive session exists when the {@link 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 Uni}
* completes.
* <p>
* A method annotated with this annotation must return {@link Uni}. If declared on a class then all methods that are intercepted
* must return {@link Uni}.
*
* @see 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 0b56ce8

Please sign in to comment.