diff --git a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc index 849ad0b8d768c..dd742e6823b63 100644 --- a/docs/src/main/asciidoc/hibernate-reactive-panache.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive-panache.adoc @@ -20,6 +20,8 @@ What we're doing in Panache allows you to write your Hibernate Reactive entities [source,java] ---- +import io.quarkus.hibernate.reactive.panache.PanacheEntity; + @Entity public class Person extends PanacheEntity { public String name; @@ -43,9 +45,9 @@ public class Person extends PanacheEntity { You have noticed how much more compact and readable the code is? Does this look interesting? Read on! -NOTE: the `list()` method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code. +NOTE: The `list()` method might be surprising at first. It takes fragments of HQL (JP-QL) queries and contextualizes the rest. That makes for very concise but yet readable code. -NOTE: what was described above is essentially the link:https://www.martinfowler.com/eaaCatalog/activeRecord.html[active record pattern], sometimes just called the entity pattern. +NOTE: What was described above is essentially the link:https://www.martinfowler.com/eaaCatalog/activeRecord.html[active record pattern], sometimes just called the entity pattern. Hibernate with Panache also allows for the use of the more classical link:https://martinfowler.com/eaaCatalog/repository.html[repository pattern] via `PanacheRepository`. == Solution @@ -718,16 +720,25 @@ PanacheQuery query = Person.find("select new MyView(d.race, AVG(d.we Hibernate Reactive in Quarkus currently does not support multiple persistence units. -== Transactions +[[transactions]] +== Sessions and Transactions + +First of all, most of the methods of a Panache entity must be invoked within the scope of a reactive `Mutiny.Session`. +In some cases, the session is opened automatically on demand. +For example, if a Panache entity method is invoked in a JAX-RS resource method in an application that includes the `quarkus-resteasy-reactive` extension. +For other cases, there are both a declarative and a programmatic way to ensure the session is opened. +You can annotate a CDI business method that returns `Uni` with the `@WithSession` annotation. +The method will be intercepted and the returned `Uni` will be triggered within a scope of a reactive session. +Alternatively, you can use the `Panache.withSession()` method to achieve the same effect. -Make sure to wrap methods modifying your database (e.g. `entity.persist()`) within a transaction. Marking a -CDI bean method `@ReactiveTransactional` will do that for you and make that method a transaction boundary. Alternatively, -you can use `Panache.withTransaction()` for the same effect. We recommend doing -so at your application entry point boundaries like your REST endpoint controllers. +NOTE: Note that a Panache entity may not be used from a blocking thread. See also xref:getting-started-reactive.adoc[Getting Started With Reactive] guide that explains the basics of reactive principles in Quarkus. -NOTE: You cannot use `@Transactional` with Hibernate Reactive for your transactions: you must use `@ReactiveTransactional`, -and your annotated method must return a `Uni` to be non-blocking. Otherwise, it needs be called from a non-`VertxThread` thread -and will become blocking. +Also make sure to wrap methods that modify the database or involve multiple queries (e.g. `entity.persist()`) within a transaction. +You can annotate a CDI business method that returns `Uni` with the `@WithTransaction` annotation. +The method will be intercepted and the returned `Uni` is triggered within a transaction boundary. +Alternatively, you can use the `Panache.withTransaction()` method for the same effect. + +IMPORTANT: You cannot use the `@Transactional` annotation with Hibernate Reactive for your transactions: you must use `@WithTransaction`, and your annotated method must return a `Uni` to be non-blocking. JPA batches changes you make to your entities and sends changes (it is called flush) at the end of the transaction or before a query. This is usually a good thing as it is more efficient. @@ -738,9 +749,9 @@ And your transaction still has to be committed. Here is an example of the usage of the flush method to allow making a specific action in case of `PersistenceException`: [source,java] ---- -@ReactiveTransactional +@WithTransaction public Uni create(Person person){ - //Here I use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes. + // Here we use the persistAndFlush() shorthand method on a Panache repository to persist to database then flush the changes. return person.persistAndFlush() .onFailure(PersistenceException.class) .recoverWithItem(() -> { @@ -752,12 +763,11 @@ public Uni create(Person person){ } ---- -The `@ReactiveTransactional` annotation will also work for testing. +The `@WithTransaction` annotation will also work for testing. This means that changes done during the test will be propagated to the database. If you want any changes made to be rolled back at the end of the test you can use the `io.quarkus.test.TestReactiveTransaction` annotation. -This will run the test method in a transaction, but roll it back once the test method is -complete to revert any database changes. +This will run the test method in a transaction, but roll it back once the test method is complete to revert any database changes. == Lock management @@ -841,6 +851,11 @@ public class PersonRepository implements PanacheRepositoryBase { } ---- +== Testing + +Testing reactive Panache entities in a `@QuarkusTest` is slightly more complicated than testing regular Panache entities due to the asynchronous nature of the APIs and the fact that all operations need to run on a Vert.x event loop. +The usage of `@RunOnVertxContext`, `@TestReactiveTransaction` and `UniAsserter` is described in the xref:hibernate-reactive.adoc#testing[Hibernate Reactive] guide. + == Mocking === Using the active record pattern @@ -886,81 +901,75 @@ You can write your mocking test like this: [source,java] ---- +import io.quarkus.test.vertx.UniAsserter; +import io.quarkus.test.vertx.RunOnVertxContext; + @QuarkusTest public class PanacheFunctionalityTest { + @RunOnVertxContext // <1> @Test - public void testPanacheMocking() { - PanacheMock.mock(Person.class); + public void testPanacheMocking(UniAsserter asserter) { // <2> + asserter.execute(() -> PanacheMock.mock(Person.class)); // Mocked classes always return a default value - Assertions.assertEquals(0, Person.count().await().indefinitely()); + asserter.assertEquals(() -> Person.count(), 0l); // Now let's specify the return value - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, Person.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> Person.count(), 23l); // Now let's change the return value - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, Person.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> Person.count(), 42l); // Now let's call the original method - Mockito.when(Person.count()).thenCallRealMethod(); - Assertions.assertEquals(0, Person.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod()); + asserter.assertEquals(() -> Person.count(), 0l); // Check that we called it 4 times - PanacheMock.verify(Person.class, Mockito.times(4)).count();// <1> + asserter.execute(() -> { + PanacheMock.verify(Person.class, Mockito.times(4)).count(); // <3> + }); // Mock only with specific parameters - Person p = new Person(); - Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, Person.findById(12l).await().indefinitely()); - Assertions.assertNull(Person.findById(42l).await().indefinitely()); + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); + asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> Person.findById(42l)); // Mock throwing - Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); - try { - Person.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } + asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return Person.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); // We can even mock your custom methods - Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty()); - - PanacheMock.verify(Person.class).findOrdered(); - PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); - PanacheMock.verifyNoMoreInteractions(Person.class); - } -} ----- -<1> Be sure to call your `verify` and `do*` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know -what mock object to pass. - -==== Mocking `Mutiny.Session` and entity instance methods - -If you need to mock entity instance methods, such as `persist()` you can do it by mocking the Hibernate Reactive `Mutiny.Session` object: + asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty()); -[source,java] ----- -@QuarkusTest -public class PanacheMockingTest { - - @InjectMock - Mutiny.Session session; - - @Test - public void testPanacheSessionMocking() { - Person p = new Person(); - // mocked via Mutiny.Session mocking - p.persist().await().indefinitely(); - Assertions.assertNull(p.id); + asserter.execute(() -> { + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + }); - Mockito.verify(session, Mockito.times(1)).persist(Mockito.any()); + // IMPORTANT: We need to execute the asserter within a reactive session + asserter.surroundWith(u -> Panache.withSession(() -> u)); } } ---- +<1> Make sure the test method is run on the Vert.x event loop. +<2> The injected `UniAsserter` agrument is used to make assertions. +<3> Be sure to call your `verify` and `do*` methods on `PanacheMock` rather than `Mockito`, otherwise you won't know +what mock object to pass. === Using the repository pattern @@ -1014,56 +1023,76 @@ You can write your mocking test like this: [source,java] ---- +import io.quarkus.test.vertx.UniAsserter; +import io.quarkus.test.vertx.RunOnVertxContext; + @QuarkusTest public class PanacheFunctionalityTest { @InjectMock PersonRepository personRepository; + @RunOnVertxContext // <1> @Test - public void testPanacheRepositoryMocking() throws Throwable { + public void testPanacheRepositoryMocking(UniAsserter asserter) { // <2> + // Mocked classes always return a default value - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); // Now let's specify the return value - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 23l); // Now let's change the return value - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 42l); // Now let's call the original method - Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod()); + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); // Check that we called it 4 times - Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + asserter.execute(() -> { + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + }); // Mock only with specific parameters - Person p = new Person(); - Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely()); - Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely()); - + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); + asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> mockablePersonRepository.findById(42l)); + // Mock throwing - Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); - try { - mockablePersonRepository.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } + asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return mockablePersonRepository.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); // We can even mock your custom methods - Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty()); - - Mockito.verify(mockablePersonRepository).findOrdered(); - Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); - Mockito.verify(mockablePersonRepository).persist(Mockito. any()); - Mockito.verifyNoMoreInteractions(mockablePersonRepository); + asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered()) + .thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty()); + + asserter.execute(() -> { + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verify(mockablePersonRepository).persist(Mockito. any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + }); + + // IMPORTANT: We need to execute the asserter within a reactive session + asserter.surroundWith(u -> Panache.withSession(() -> u)); } } ---- +<1> Make sure the test method is run on the Vert.x event loop. +<2> The injected `UniAsserter` agrument is used to make assertions. == How and why we simplify Hibernate Reactive mappings diff --git a/docs/src/main/asciidoc/hibernate-reactive.adoc b/docs/src/main/asciidoc/hibernate-reactive.adoc index 8a64046d61581..8816dbcd6c04f 100644 --- a/docs/src/main/asciidoc/hibernate-reactive.adoc +++ b/docs/src/main/asciidoc/hibernate-reactive.adoc @@ -216,6 +216,7 @@ You can also inject an instance of `Uni` using the exact same me Uni 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. diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java index 0b7fecbf95405..e474441059d26 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/ReactiveSessionFactoryProducer.java @@ -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; @@ -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)) { diff --git a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java index cd9f5a983c1b3..fecb66017602c 100644 --- a/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java +++ b/extensions/panache/hibernate-reactive-panache-common/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheJpaCommonResourceProcessor.java @@ -1,10 +1,14 @@ 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; @@ -12,14 +16,24 @@ 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; @@ -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 { @@ -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 generatedBeanBuildItemBuildProducer, BuildProducer additionalBeans) { @@ -61,12 +95,101 @@ void testTx(BuildProducer generatedBeanBuildItemBuildPro } @BuildStep - void registerInterceptor(BuildProducer additionalBeans) { + void registerInterceptors(BuildProducer 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 errors) { + List bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_ON_DEMAND, WITH_TRANSACTION); + for (BeanInfo bean : validationPhase.getContext().beans().withAroundInvokeInterceptor()) { + for (Entry> 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 annotationsTransformer) { + if (capabilities.isPresent(Capability.RESTEASY_REACTIVE)) { + // Custom request method designators are not supported + List 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 bindings = List.of(REACTIVE_TRANSACTIONAL, WITH_SESSION, WITH_SESSION_ON_DEMAND, WITH_TRANSACTION); + + // Collect all panache entities + Set 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 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 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 namedQueries, diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java new file mode 100644 index 0000000000000..d9099daa3a3c5 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSession.java @@ -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}. + *

+ * 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. + *

+ * 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 { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java new file mode 100644 index 0000000000000..3ebc4f4ac668f --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithSessionOnDemand.java @@ -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 when needed and eventually closed when the + * {@link io.smallrye.mutiny.Uni} completes. + *

+ * 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 { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java new file mode 100644 index 0000000000000..71f063d778e0f --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/WithTransaction.java @@ -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. + *

+ * 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 { + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java index 5fdf02d6d39de..15740abcabe2a 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractJpaOperations.java @@ -5,28 +5,19 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -import jakarta.enterprise.inject.spi.Bean; import jakarta.persistence.LockModeType; import org.hibernate.internal.util.LockModeConverter; import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.mutiny.Mutiny.Session; -import io.quarkus.arc.Arc; import io.quarkus.panache.common.Parameters; import io.quarkus.panache.common.Sort; import io.quarkus.panache.hibernate.common.runtime.PanacheJpaUtil; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -import io.vertx.core.Vertx; public abstract class AbstractJpaOperations { @@ -34,29 +25,6 @@ public abstract class AbstractJpaOperations { static final long TIMEOUT_MS = 5000; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; - private static void executeInVertxEventLoop(Runnable runnable) { - Vertx vertx = Arc.container().instance(Vertx.class).get(); - // this needs to be sync - CompletableFuture cf = new CompletableFuture<>(); - vertx.runOnContext(v -> { - try { - runnable.run(); - cf.complete(null); - } catch (Throwable t) { - cf.completeExceptionally(t); - } - }); - try { - cf.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { - throw new RuntimeException(e); - } - } - - private static Session lookupSessionFromArc() { - return Arc.container().instance(Session.class).get(); - } - protected abstract PanacheQueryType createPanacheQuery(Uni session, String query, String orderBy, Object paramsArrayOrMap); @@ -109,62 +77,14 @@ public Uni delete(Object entity) { } public boolean isPersistent(Object entity) { - // only attempt to look up the request context session if it's already there: do not - // run the producer method otherwise, before we know which thread we're on - Session requestSession = isInRequestContext(Mutiny.Session.class) ? lookupSessionFromArc() - : null; - if (requestSession != null) { - return requestSession.contains(entity); - } else { - return false; - } + Mutiny.Session current = SessionOperations.getCurrentSession(); + return current != null ? current.contains(entity) : false; } public Uni flush() { return getSession().chain(Session::flush); } - // - // Private stuff - - public static Uni getSession() { - // Always check if we're running on the event loop: if not, - // we need to delegate the execution of all tasks on it. - if (io.vertx.core.Context.isOnEventLoopThread()) { - return Uni.createFrom().item(lookupSessionFromArc()); - } else { - // FIXME: we may need context propagation - final Executor executor = AbstractJpaOperations::executeInVertxEventLoop; - return Uni.createFrom().item(AbstractJpaOperations::lookupSessionFromArc) - .runSubscriptionOn(executor); - } - } - - private static boolean isInRequestContext(Class klass) { - Set> beans = Arc.container().beanManager().getBeans(klass); - if (beans.isEmpty()) - return false; - return Arc.container().requestContext().get(beans.iterator().next()) != null; - } - - public static Mutiny.Query bindParameters(Mutiny.Query query, Object[] params) { - if (params == null || params.length == 0) - return query; - for (int i = 0; i < params.length; i++) { - query.setParameter(i + 1, params[i]); - } - return query; - } - - public static Mutiny.Query bindParameters(Mutiny.Query query, Map params) { - if (params == null || params.size() == 0) - return query; - for (Entry entry : params.entrySet()) { - query.setParameter(entry.getKey(), entry.getValue()); - } - return query; - } - public int paramCount(Object[] params) { return params != null ? params.length : 0; } @@ -421,22 +341,6 @@ public IllegalStateException implementationInjectionMissing() { "This method is normally automatically overridden in subclasses: did you forget to annotate your entity with @Entity?"); } - public static Uni executeUpdate(String query, Object... params) { - return getSession().chain(session -> { - Mutiny.Query jpaQuery = session.createQuery(query); - bindParameters(jpaQuery, params); - return jpaQuery.executeUpdate(); - }); - } - - public static Uni executeUpdate(String query, Map params) { - return getSession().chain(session -> { - Mutiny.Query jpaQuery = session.createQuery(query); - bindParameters(jpaQuery, params); - return jpaQuery.executeUpdate(); - }); - } - public Uni executeUpdate(Class entityClass, String query, Object... params) { if (PanacheJpaUtil.isNamedQuery(query)) @@ -474,4 +378,45 @@ public Uni update(Class entityClass, String query, Parameters params public Uni update(Class entityClass, String query, Object... params) { return executeUpdate(entityClass, query, params); } + + // + // Static helpers + + public static Uni getSession() { + return SessionOperations.getSession(); + } + + public static Mutiny.Query bindParameters(Mutiny.Query query, Object[] params) { + if (params == null || params.length == 0) + return query; + for (int i = 0; i < params.length; i++) { + query.setParameter(i + 1, params[i]); + } + return query; + } + + public static Mutiny.Query bindParameters(Mutiny.Query query, Map params) { + if (params == null || params.size() == 0) + return query; + for (Entry entry : params.entrySet()) { + query.setParameter(entry.getKey(), entry.getValue()); + } + return query; + } + + public static Uni executeUpdate(String query, Object... params) { + return getSession().chain(session -> { + Mutiny.Query jpaQuery = session.createQuery(query); + bindParameters(jpaQuery, params); + return jpaQuery.executeUpdate(); + }); + } + + public static Uni executeUpdate(String query, Map params) { + return getSession().chain(session -> { + Mutiny.Query jpaQuery = session.createQuery(query); + bindParameters(jpaQuery, params); + return jpaQuery.executeUpdate(); + }); + } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java new file mode 100644 index 0000000000000..2b9b1ea23923c --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/AbstractUniInterceptor.java @@ -0,0 +1,18 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import jakarta.interceptor.InvocationContext; + +import io.smallrye.mutiny.Uni; + +abstract class AbstractUniInterceptor { + + @SuppressWarnings("unchecked") + protected Uni proceedUni(InvocationContext context) { + try { + return ((Uni) context.proceed()); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java index 83afdbcd48230..9b8bb4be979ae 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactional.java @@ -10,18 +10,17 @@ import org.hibernate.reactive.mutiny.Mutiny; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; import io.smallrye.mutiny.Uni; -import io.vertx.core.impl.VertxThread; /** * Use this annotation on your method to run them in a reactive {@link Mutiny.Transaction}. + *

+ * The annotated method must return a {@link Uni}. * - * If the annotated method returns a {@link Uni}, this has exactly the same behaviour as if the method - * was enclosed in a call to {@link Mutiny.Session#withTransaction(java.util.function.Function)}. - * - * Otherwise, invocations are only allowed when not running from a {@link VertxThread} and the behaviour - * will be the same as a blocking call to {@link Mutiny.Session#withTransaction(java.util.function.Function)}. + * @deprecated Use {@link WithTransaction} instead. */ +@Deprecated(forRemoval = true) @Inherited @InterceptorBinding @Target({ ElementType.TYPE, ElementType.METHOD }) diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java index 74e11668f926f..fe3e3f797ef3a 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptor.java @@ -1,11 +1,20 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; -@Interceptor @ReactiveTransactional +@Interceptor @Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) -public class ReactiveTransactionalInterceptor extends ReactiveTransactionalInterceptorBase { +public class ReactiveTransactionalInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @ReactiveTransactional are validated at build time + // The build fails if the method does not return Uni + return SessionOperations.withTransaction(() -> proceedUni(context)); + } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java deleted file mode 100644 index 3e51fcc1aad5f..0000000000000 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/ReactiveTransactionalInterceptorBase.java +++ /dev/null @@ -1,134 +0,0 @@ -package io.quarkus.hibernate.reactive.panache.common.runtime; - -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.function.Function; - -import jakarta.interceptor.AroundInvoke; -import jakarta.interceptor.InvocationContext; - -import org.hibernate.reactive.mutiny.Mutiny.Transaction; - -import io.smallrye.mutiny.Uni; - -public abstract class ReactiveTransactionalInterceptorBase { - private static final String JUNIT_TEST_ANN = "org.junit.jupiter.api.Test"; - private static final String JUNIT_BEFORE_EACH_ANN = "org.junit.jupiter.api.BeforeEach"; - private static final String JUNIT_AFTER_EACH_ANN = "org.junit.jupiter.api.AfterEach"; - private static final String UNI_ASSERTER_CLASS = "io.quarkus.test.vertx.UniAsserter"; - - @SuppressWarnings("unchecked") - @AroundInvoke - public Object intercept(InvocationContext ic) throws Exception { - Class returnType = ic.getMethod().getReturnType(); - if (returnType == Uni.class) { - return AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - try { - return (Uni) ic.proceed(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - })); - } else if (io.vertx.core.Context.isOnVertxThread()) { - if (isSpecialTestMethod(ic)) { - return handleSpecialTestMethod(ic); - } - throw new RuntimeException("Unsupported return type " + returnType + " in method " + ic.getMethod() - + ": only Uni is supported when using @ReactiveTransaction if you are running on a VertxThread"); - } else { - // we're not on a Vert.x thread, we can block, and we assume the intercepted method is blocking - // FIXME: should we require a @Blocking annotation? - Uni ret = AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - try { - return Uni.createFrom().item(ic.proceed()); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - })); - return ret.await().atMost(Duration.ofMillis(AbstractJpaOperations.TIMEOUT_MS)); - } - } - - protected boolean isSpecialTestMethod(InvocationContext ic) { - Method method = ic.getMethod(); - return hasParameter(UNI_ASSERTER_CLASS, method) - && (hasAnnotation(JUNIT_TEST_ANN, method) - || hasAnnotation(JUNIT_BEFORE_EACH_ANN, method) - || hasAnnotation(JUNIT_AFTER_EACH_ANN, method)); - } - - protected Object handleSpecialTestMethod(InvocationContext ic) { - // let's not deal with generics/erasure - Class[] parameterTypes = ic.getMethod().getParameterTypes(); - Object uniAsserter = null; - Class uniAsserterClass = null; - for (int i = 0; i < parameterTypes.length; i++) { - Class klass = parameterTypes[i]; - if (klass.getName().equals(UNI_ASSERTER_CLASS)) { - uniAsserter = ic.getParameters()[i]; - uniAsserterClass = klass; - break; - } - } - if (uniAsserter == null) { - throw new AssertionError("We could not find the right UniAsserter parameter, please file a bug report"); - } - try { - Method execute = uniAsserterClass.getMethod("surroundWith", Function.class); - // here our execution differs: we can run the test method first, which uses the UniAsserter, and all its code is deferred - // by pushing execution into the asserter pipeline - try { - ic.proceed(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { - throw new RuntimeException(e); - } - // Now the pipeline is set up, we need to surround it with our transaction - execute.invoke(uniAsserter, new Function, Uni>() { - @Override - public Uni apply(Uni t) { - return AbstractJpaOperations.getSession().flatMap(session -> session.withTransaction(tx -> { - inTransactionCallback(tx); - return t; - })); - } - - }); - return null; - } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { - throw new AssertionError("Reflective call to UniAsserter parameter failed, please file a bug report", e); - } - } - - private boolean hasParameter(String parameterType, Method method) { - // let's not deal with generics/erasure - for (Class klass : method.getParameterTypes()) { - if (klass.getName().equals(parameterType)) { - return true; - } - } - return false; - } - - private boolean hasAnnotation(String annotationName, Method method) { - for (Annotation annotation : method.getAnnotations()) { - if (annotation.annotationType().getName().equals(annotationName)) { - return true; - } - } - return false; - } - - protected void inTransactionCallback(Transaction tx) { - } -} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java new file mode 100644 index 0000000000000..7f5655acf0c59 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/SessionOperations.java @@ -0,0 +1,208 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.reactive.common.spi.Implementor; +import org.hibernate.reactive.context.Context.Key; +import org.hibernate.reactive.context.impl.BaseKey; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.Mutiny.Session; +import org.hibernate.reactive.mutiny.Mutiny.SessionFactory; +import org.hibernate.reactive.mutiny.Mutiny.Transaction; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.impl.LazyValue; +import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; + +/** + * Static util methods for {@link Mutiny.Session}. + */ +public final class SessionOperations { + + private static final String ERROR_MSG = "Hibernate Reactive Panache requires a safe (isolated) Vert.x sub-context, but the current context hasn't been flagged as such."; + + private static final LazyValue SESSION_FACTORY = new LazyValue<>( + new Supplier() { + @Override + public SessionFactory get() { + // Note that Mutiny.SessionFactory is @ApplicationScoped bean - it's safe to use the cached client proxy + Mutiny.SessionFactory sessionFactory = Arc.container().instance(Mutiny.SessionFactory.class).get(); + if (sessionFactory == null) { + throw new IllegalStateException("Mutiny.SessionFactory bean not found"); + } + return sessionFactory; + } + }); + + private static final LazyValue> SESSION_KEY = new LazyValue<>( + new Supplier>() { + + @Override + public Key get() { + return new BaseKey<>(Mutiny.Session.class, ((Implementor) SESSION_FACTORY.get()).getUuid()); + } + }); + + // This key is used to indicate that a reactive session should be opened lazily (when needed) in the current vertx context + private static final String SESSION_ON_DEMAND_KEY = "hibernate.reactive.panache.sessionOnDemand"; + + /** + * Marks the current vertx duplicated context as "lazy" which indicates that a reactive session should be opened lazily if + * needed. The opened session is eventually closed and the marking key is removed when the provided {@link Uni} completes. + * + * @param + * @param work + * @return a new {@link Uni} + * @see #getSession() + */ + static Uni withSessionOnDemand(Supplier> work) { + Context context = vertxContext(); + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { + // context already marked - no need to set the key and close the session + return work.get(); + } else { + // mark the lazy session + context.putLocal(SESSION_ON_DEMAND_KEY, true); + // perform the work and eventually close the session and remove the key + return work.get().eventually(() -> { + context.removeLocal(SESSION_ON_DEMAND_KEY); + return closeSession(); + }); + } + } + + /** + * Performs the work in the scope of a reactive transaction. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withTransaction(Supplier> work) { + return withSession(s -> { + return s.withTransaction(t -> work.get()); + }); + } + + /** + * Performs the work in the scope of a reactive transaction. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withTransaction(Function> work) { + return withSession(s -> { + return s.withTransaction(t -> work.apply(t)); + }); + } + + /** + * Performs the work in the scope of a reactive session. An existing session is reused if possible. + * + * @param + * @param work + * @return a new {@link Uni} + */ + public static Uni withSession(Function> work) { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + // reactive session exists - reuse this session + return work.apply(current); + } else { + // reactive session does not exist - open a new one and close it when the returned Uni completes + return getSessionFactory() + .openSession() + .invoke(s -> context.putLocal(key, s)) + .chain(s -> work.apply(s)) + .eventually(() -> closeSession()); + } + } + + /** + * If there is a reactive session stored in the current Vert.x duplicated context then this session is reused. + *

+ * However, if there is no reactive session found then: + *

    + *
  1. if the current vertx duplicated context is marked as "lazy" then a new session is opened and stored it in the + * context
  2. + *
  3. otherwise an exception thrown
  4. + *
+ * + * @throws IllegalStateException If no reactive session was found in the context and the context was not marked to open a + * new session lazily + * @return the {@link Mutiny.Session} + */ + public static Uni getSession() { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + // reuse the existing reactive session + return Uni.createFrom().item(current); + } else { + if (context.getLocal(SESSION_ON_DEMAND_KEY) != null) { + // open a new reactive session and store it in the vertx duplicated context + // the context was marked as "lazy" which means that the session will be eventually closed + return getSessionFactory().openSession().invoke(s -> context.putLocal(key, s)); + } else { + throw new IllegalStateException("No current Mutiny.Session found" + + "\n\t- no reactive session was found in the context and the context was not marked to open a new session lazily" + + "\n\t- you might need to annotate the business method with @WithSession"); + } + } + } + + /** + * @return the current reactive session stored in the context, or {@code null} if no session exists + */ + public static Mutiny.Session getCurrentSession() { + Context context = vertxContext(); + Mutiny.Session current = context.getLocal(getSessionKey()); + if (current != null && current.isOpen()) { + return current; + } + return null; + } + + /** + * + * @return the current vertx duplicated context + * @throws IllegalStateException If no vertx context is found or is not a safe context as mandated by the + * {@link VertxContextSafetyToggle} + */ + private static Context vertxContext() { + Context context = Vertx.currentContext(); + if (context != null) { + VertxContextSafetyToggle.validateContextIfExists(ERROR_MSG, ERROR_MSG); + return context; + } else { + throw new IllegalStateException("No current Vertx context found"); + } + } + + static Uni closeSession() { + Context context = vertxContext(); + Key key = getSessionKey(); + Mutiny.Session current = context.getLocal(key); + if (current != null && current.isOpen()) { + return current.close().eventually(() -> context.removeLocal(key)); + } + return Uni.createFrom().voidItem(); + } + + static Key getSessionKey() { + return SESSION_KEY.get(); + } + + static Mutiny.SessionFactory getSessionFactory() { + return SESSION_FACTORY.get(); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java index db626fb07ac8d..bd496d144831e 100644 --- a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/TestReactiveTransactionalInterceptor.java @@ -1,12 +1,102 @@ package io.quarkus.hibernate.reactive.panache.common.runtime; -import org.hibernate.reactive.mutiny.Mutiny.Transaction; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.function.Function; -public class TestReactiveTransactionalInterceptor extends ReactiveTransactionalInterceptorBase { +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; - @Override - protected void inTransactionCallback(Transaction tx) { - tx.markForRollback(); +import io.smallrye.mutiny.Uni; + +public class TestReactiveTransactionalInterceptor { + + private static final String JUNIT_TEST_ANN = "org.junit.jupiter.api.Test"; + private static final String JUNIT_BEFORE_EACH_ANN = "org.junit.jupiter.api.BeforeEach"; + private static final String JUNIT_AFTER_EACH_ANN = "org.junit.jupiter.api.AfterEach"; + private static final String UNI_ASSERTER_CLASS = "io.quarkus.test.vertx.UniAsserter"; + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + if (isSpecialTestMethod(context)) { + return handleSpecialTestMethod(context); + } + // TODO validate this requirement during build + throw new IllegalStateException( + "A test method annotated with @TestReactiveTransaction must accept io.quarkus.test.vertx.UniAsserter"); + } + + protected boolean isSpecialTestMethod(InvocationContext ic) { + Method method = ic.getMethod(); + return hasParameter(UNI_ASSERTER_CLASS, method) + && (hasAnnotation(JUNIT_TEST_ANN, method) + || hasAnnotation(JUNIT_BEFORE_EACH_ANN, method) + || hasAnnotation(JUNIT_AFTER_EACH_ANN, method)); + } + + protected Object handleSpecialTestMethod(InvocationContext ic) { + // let's not deal with generics/erasure + Class[] parameterTypes = ic.getMethod().getParameterTypes(); + Object uniAsserter = null; + Class uniAsserterClass = null; + for (int i = 0; i < parameterTypes.length; i++) { + Class klass = parameterTypes[i]; + if (klass.getName().equals(UNI_ASSERTER_CLASS)) { + uniAsserter = ic.getParameters()[i]; + uniAsserterClass = klass; + break; + } + } + if (uniAsserter == null) { + throw new AssertionError("We could not find the right UniAsserter parameter, please file a bug report"); + } + try { + Method execute = uniAsserterClass.getMethod("surroundWith", Function.class); + // here our execution differs: we can run the test method first, which uses the UniAsserter, and all its code is deferred + // by pushing execution into the asserter pipeline + try { + ic.proceed(); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException(e); + } + // Now the pipeline is set up, we need to surround it with our transaction + execute.invoke(uniAsserter, new Function, Uni>() { + @Override + public Uni apply(Uni t) { + return SessionOperations.withTransaction(tx -> { + tx.markForRollback(); + return t; + }); + } + + }); + return null; + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new AssertionError("Reflective call to UniAsserter parameter failed, please file a bug report", e); + } + } + + private boolean hasParameter(String parameterType, Method method) { + // let's not deal with generics/erasure + for (Class klass : method.getParameterTypes()) { + if (klass.getName().equals(parameterType)) { + return true; + } + } + return false; + } + + private boolean hasAnnotation(String annotationName, Method method) { + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType().getName().equals(annotationName)) { + return true; + } + } + return false; } } diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java new file mode 100644 index 0000000000000..a3457b40b18f9 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithSession; + +@WithSession +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithSessionInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithSession are validated at build time + // The build fails if a method does not return Uni + return SessionOperations.withSession(s -> proceedUni(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java new file mode 100644 index 0000000000000..8969e0a38653f --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithSessionOnDemandInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithSessionOnDemand; + +@WithSessionOnDemand +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithSessionOnDemandInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithSessionOnDemand are validated at build time + // The build fails if a method does not return Uni + return SessionOperations.withSessionOnDemand(() -> proceedUni(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java new file mode 100644 index 0000000000000..1063f47b99a09 --- /dev/null +++ b/extensions/panache/hibernate-reactive-panache-common/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/common/runtime/WithTransactionInterceptor.java @@ -0,0 +1,22 @@ +package io.quarkus.hibernate.reactive.panache.common.runtime; + +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; + +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; + +@WithTransaction +@Interceptor +@Priority(Interceptor.Priority.PLATFORM_BEFORE + 200) +public class WithTransactionInterceptor extends AbstractUniInterceptor { + + @AroundInvoke + public Object intercept(InvocationContext context) throws Exception { + // Note that intercepted methods annotated with @WithTransaction are validated at build time + // The build fails if the method does not return Uni + return SessionOperations.withTransaction(() -> proceedUni(context)); + } + +} diff --git a/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java b/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java index 635fde1265f35..194f123bf06e5 100644 --- a/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java +++ b/extensions/panache/hibernate-reactive-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/panache/common/deployment/PanacheHibernateResourceProcessor.java @@ -7,7 +7,6 @@ import java.util.stream.Collectors; import jakarta.persistence.Id; -import jakarta.persistence.Transient; import org.hibernate.reactive.mutiny.Mutiny; import org.jboss.jandex.AnnotationInstance; @@ -53,7 +52,6 @@ public final class PanacheHibernateResourceProcessor { private static final DotName DOTNAME_ID = DotName.createSimple(Id.class.getName()); protected static final String META_INF_PANACHE_ARCHIVE_MARKER = "META-INF/panache-archive.marker"; - private static final DotName DOTNAME_TRANSIENT = DotName.createSimple(Transient.class.getName()); private static final DotName DOTNAME_UNI = DotName.createSimple(Uni.class.getName()); private static final DotName DOTNAME_MULTI = DotName.createSimple(Multi.class.getName()); diff --git a/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java b/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java index 789e187629d11..17cbce5d4fe0c 100644 --- a/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java +++ b/extensions/panache/hibernate-reactive-panache/runtime/src/main/java/io/quarkus/hibernate/reactive/panache/Panache.java @@ -6,6 +6,7 @@ import org.hibernate.reactive.mutiny.Mutiny; import io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations; +import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations; import io.quarkus.panache.common.Parameters; import io.smallrye.mutiny.Uni; @@ -16,13 +17,25 @@ */ public class Panache { + /** + * Obtains a {@link Uni} within the scope of a reactive session. If a reactive session exists then it is reused. If it + * does not exist not exist then open a new session that is automatically closed when the provided {@link Uni} completes. + * + * @param + * @param uniSupplier + * @return a new {@link Uni} + */ + public static Uni withSession(Supplier> uniSupplier) { + return SessionOperations.withSession(s -> uniSupplier.get()); + } + /** * Returns the current {@link Mutiny.Session} * * @return the current {@link Mutiny.Session} */ public static Uni getSession() { - return AbstractJpaOperations.getSession(); + return SessionOperations.getSession(); } /** @@ -36,7 +49,7 @@ public static Uni getSession() { * @see Panache#currentTransaction() */ public static Uni withTransaction(Supplier> work) { - return getSession().flatMap(session -> session.withTransaction(t -> work.get())); + return SessionOperations.withTransaction(() -> work.get()); } /** diff --git a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java index d55a8a35d0495..d89ca47d709cc 100644 --- a/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java +++ b/extensions/panache/hibernate-reactive-rest-data-panache/deployment/src/main/java/io/quarkus/hibernate/reactive/rest/data/panache/deployment/ResourceImplementor.java @@ -20,7 +20,8 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import io.quarkus.rest.data.panache.deployment.Constants; @@ -82,6 +83,7 @@ String implement(ClassOutput classOutput, DataAccessImplementor dataAccessImplem private void implementList(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class); + methodCreator.addAnnotation(WithSession.class); ResultHandle page = methodCreator.getMethodParam(0); ResultHandle sort = methodCreator.getMethodParam(1); ResultHandle columns = methodCreator.invokeVirtualMethod(ofMethod(Sort.class, "getColumns", List.class), sort); @@ -97,6 +99,7 @@ private void implementList(ClassCreator classCreator, DataAccessImplementor data private void implementListWithQuery(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("list", Uni.class, Page.class, Sort.class, String.class, Map.class); + methodCreator.addAnnotation(WithSession.class); ResultHandle page = methodCreator.getMethodParam(0); ResultHandle sort = methodCreator.getMethodParam(1); ResultHandle query = methodCreator.getMethodParam(2); @@ -118,6 +121,7 @@ private void implementListWithQuery(ClassCreator classCreator, DataAccessImpleme */ private void implementCount(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("count", Uni.class); + methodCreator.addAnnotation(WithSession.class); methodCreator.returnValue(dataAccessImplementor.count(methodCreator)); methodCreator.close(); } @@ -129,6 +133,7 @@ private void implementCount(ClassCreator classCreator, DataAccessImplementor dat private void implementListPageCount(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator(Constants.PAGE_COUNT_METHOD_PREFIX + "list", Uni.class, Page.class); + methodCreator.addAnnotation(WithSession.class); ResultHandle page = methodCreator.getMethodParam(0); methodCreator.returnValue(dataAccessImplementor.pageCount(methodCreator, page)); methodCreator.close(); @@ -136,6 +141,7 @@ private void implementListPageCount(ClassCreator classCreator, DataAccessImpleme private void implementGet(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("get", Uni.class, Object.class); + methodCreator.addAnnotation(WithSession.class); ResultHandle id = methodCreator.getMethodParam(0); methodCreator.returnValue(dataAccessImplementor.findById(methodCreator, id)); methodCreator.close(); @@ -144,7 +150,7 @@ private void implementGet(ClassCreator classCreator, DataAccessImplementor dataA private void implementAdd(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor, ResourceMethodListenerImplementor resourceMethodListenerImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("add", Uni.class, Object.class); - methodCreator.addAnnotation(ReactiveTransactional.class); + methodCreator.addAnnotation(WithTransaction.class); ResultHandle entity = methodCreator.getMethodParam(0); resourceMethodListenerImplementor.onBeforeAdd(methodCreator, entity); ResultHandle uni = dataAccessImplementor.persist(methodCreator, entity); @@ -156,7 +162,7 @@ private void implementAdd(ClassCreator classCreator, DataAccessImplementor dataA private void implementUpdate(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor, String entityType, ResourceMethodListenerImplementor resourceMethodListenerImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("update", Uni.class, Object.class, Object.class); - methodCreator.addAnnotation(ReactiveTransactional.class); + methodCreator.addAnnotation(WithTransaction.class); ResultHandle id = methodCreator.getMethodParam(0); ResultHandle entity = methodCreator.getMethodParam(1); // Set entity ID before executing an update to make sure that a requested object ID matches a given entity ID. @@ -171,7 +177,7 @@ private void implementUpdate(ClassCreator classCreator, DataAccessImplementor da private void implementDelete(ClassCreator classCreator, DataAccessImplementor dataAccessImplementor, ResourceMethodListenerImplementor resourceMethodListenerImplementor) { MethodCreator methodCreator = classCreator.getMethodCreator("delete", Uni.class, Object.class); - methodCreator.addAnnotation(ReactiveTransactional.class); + methodCreator.addAnnotation(WithTransaction.class); ResultHandle id = methodCreator.getMethodParam(0); resourceMethodListenerImplementor.onBeforeDelete(methodCreator, id); ResultHandle uni = dataAccessImplementor.deleteById(methodCreator, id); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index b1d40056dff4c..7cf76961b8464 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -14,7 +14,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -63,10 +62,10 @@ public class BeanInfo implements InjectionTargetInfo { private final DisposerInfo disposer; - private final Map interceptedMethods; - private final Map decoratedMethods; - - private final Map lifecycleInterceptors; + // These maps are initialized during BeanDeployment.init() + private volatile Map interceptedMethods; + private volatile Map decoratedMethods; + private volatile Map lifecycleInterceptors; private final boolean alternative; private final Integer priority; @@ -144,9 +143,9 @@ public class BeanInfo implements InjectionTargetInfo { this.params = params; // Identifier must be unique for a specific deployment this.identifier = Hashes.sha1(toString()); - this.interceptedMethods = new ConcurrentHashMap<>(); - this.decoratedMethods = new ConcurrentHashMap<>(); - this.lifecycleInterceptors = new ConcurrentHashMap<>(); + this.interceptedMethods = Collections.emptyMap(); + this.decoratedMethods = Collections.emptyMap(); + this.lifecycleInterceptors = Collections.emptyMap(); this.forceApplicationClass = forceApplicationClass; this.targetPackageName = targetPackageName; } @@ -294,6 +293,30 @@ Map getDecoratedMethods() { return decoratedMethods; } + /** + * @return {@code true} if the bean has an associated interceptor with the given binding, {@code false} otherwise + */ + public boolean hasAroundInvokeInterceptorWithBinding(DotName binding) { + if (interceptedMethods.isEmpty()) { + return false; + } + for (InterceptionInfo interception : interceptedMethods.values()) { + if (Annotations.contains(interception.bindings, binding)) { + return true; + } + } + return false; + } + + /** + * + * @return an immutable map of intercepted methods to the set of interceptor bindings + */ + public Map> getInterceptedMethodsBindings() { + return interceptedMethods.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Entry::getKey, e -> Collections.unmodifiableSet(e.getValue().bindings))); + } + List getInterceptedOrDecoratedMethods() { Set methods = new HashSet<>(interceptedMethods.keySet()); methods.addAll(decoratedMethods.keySet()); @@ -550,10 +573,11 @@ void init(List errors, Consumer bytecodeTransfor if (disposer != null) { disposer.init(errors); } - interceptedMethods.putAll(initInterceptedMethods(errors, bytecodeTransformerConsumer, transformUnproxyableClasses)); - decoratedMethods.putAll(initDecoratedMethods()); + interceptedMethods = Map + .copyOf(initInterceptedMethods(errors, bytecodeTransformerConsumer, transformUnproxyableClasses)); + decoratedMethods = Map.copyOf(initDecoratedMethods()); if (errors.isEmpty()) { - lifecycleInterceptors.putAll(initLifecycleInterceptors()); + lifecycleInterceptors = Map.copyOf(initLifecycleInterceptors()); } } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java index 106cdce96be40..a3c155faee4d4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java @@ -185,6 +185,24 @@ public BeanStream withName() { return this; } + /** + * + * @return the new stream of beans that have an associated interceptor + */ + public BeanStream withAroundInvokeInterceptor() { + stream = stream.filter(BeanInfo::hasAroundInvokeInterceptors); + return this; + } + + /** + * + * @return the new stream of beans that have an associated lifecycle interceptor + */ + public BeanStream withLifecycleInterceptor() { + stream = stream.filter(BeanInfo::hasLifecycleInterceptors); + return this; + } + /** * * @param id diff --git a/integration-tests/grpc-hibernate-reactive/src/main/java/com/example/reactive/ReactiveService.java b/integration-tests/grpc-hibernate-reactive/src/main/java/com/example/reactive/ReactiveService.java index 4ef14e229842d..a86a636012740 100644 --- a/integration-tests/grpc-hibernate-reactive/src/main/java/com/example/reactive/ReactiveService.java +++ b/integration-tests/grpc-hibernate-reactive/src/main/java/com/example/reactive/ReactiveService.java @@ -10,6 +10,7 @@ import io.quarkus.arc.Arc; import io.quarkus.arc.ManagedContext; import io.quarkus.grpc.GrpcService; +import io.quarkus.hibernate.reactive.panache.Panache; import io.smallrye.common.vertx.ContextLocals; import io.smallrye.common.vertx.VertxContext; import io.smallrye.mutiny.Multi; @@ -56,8 +57,10 @@ public Multi watch(Test.Empty request) { Multi cached = broadcast.cache(); cached.subscribe().with(i -> { }); - Multi existing = Item. streamAll() - .map(item -> Test.Item.newBuilder().setText(item.text).build()); + + Multi existing = Panache.withSession(() -> Item. listAll()).toMulti().flatMap(list -> { + return Multi.createFrom().iterable(list); + }).map(item -> Test.Item.newBuilder().setText(item.text).build()); return Multi.createBy().concatenating() .streams(existing, cached.map(i -> i.text) .map(Test.Item.newBuilder()::setText) diff --git a/integration-tests/hibernate-reactive-panache-blocking/pom.xml b/integration-tests/hibernate-reactive-panache-blocking/pom.xml deleted file mode 100644 index 422231187646e..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/pom.xml +++ /dev/null @@ -1,317 +0,0 @@ - - - - quarkus-integration-tests-parent - io.quarkus - 999-SNAPSHOT - - 4.0.0 - - quarkus-integration-test-hibernate-reactive-panache-blocking - Quarkus - Integration Tests - Hibernate Reactive with Panache in Blocking mode - To test proper error reporting and safeguards when Panache Reactive is being used in combination with blocking technologies - - - vertx-reactive:postgresql://localhost:5432/hibernate_orm_test - - - - - io.quarkus - quarkus-hibernate-reactive-panache - - - io.quarkus - quarkus-resteasy - - - io.quarkus - quarkus-resteasy-jackson - - - io.quarkus - quarkus-core - - - io.quarkus - quarkus-reactive-pg-client - - - org.junit.jupiter - junit-jupiter-api - compile - - - - - io.quarkus - quarkus-junit5 - test - - - io.quarkus - quarkus-junit5-internal - test - - - io.rest-assured - rest-assured - test - - - io.quarkus - quarkus-test-h2 - test - - - io.quarkus - quarkus-panache-mock - test - - - net.bytebuddy - byte-buddy - - - - - org.assertj - assertj-core - test - - - org.awaitility - awaitility - test - - - - io.quarkus - quarkus-core-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-hibernate-reactive-panache-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-reactive-pg-client-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-resteasy-deployment - ${project.version} - pom - test - - - * - * - - - - - io.quarkus - quarkus-resteasy-jackson-deployment - ${project.version} - pom - test - - - * - * - - - - - - - - - src/main/resources - true - - - - - maven-surefire-plugin - - true - - - - maven-failsafe-plugin - - true - - - - io.quarkus - quarkus-maven-plugin - - - - build - - - - - - - - - - test-postgresql - - - test-containers - - - - - - maven-surefire-plugin - - false - - - - - prod-mode - test - - test - - - **/*PMT.java - - - - - - maven-failsafe-plugin - - false - - - - - - - - docker-postgresql - - - start-containers - - - - vertx-reactive:postgresql://localhost:5431/hibernate_orm_test - - - - - io.fabric8 - docker-maven-plugin - - - - ${postgres.image} - postgresql - - - hibernate_orm_test - hibernate_orm_test - hibernate_orm_test - - - 5431:5432 - - - - mapped - - 5432 - - - - - - - - - true - - - - docker-start - compile - - stop - start - - - - docker-stop - post-integration-test - - stop - - - - - - org.codehaus.mojo - exec-maven-plugin - - - docker-prune - generate-resources - - exec - - - ${docker-prune.location} - - - - - - - - - diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java b/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java deleted file mode 100644 index 9f9cf94e297c2..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/Fruit.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import jakarta.persistence.Entity; - -import io.quarkus.hibernate.reactive.panache.PanacheEntity; - -@Entity -public class Fruit extends PanacheEntity { - - public String name; - public String color; - - public Fruit(String name, String color) { - this.name = name; - this.color = color; - } - - public Fruit() { - } - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java b/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java deleted file mode 100644 index b42a1fb9cae7f..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/java/io/quarkus/it/panache/reactive/TestEndpoint.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import java.util.List; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; - -import io.quarkus.hibernate.reactive.panache.Panache; -import io.quarkus.hibernate.reactive.panache.PanacheEntityBase; - -/** - * These tests cover for "mixed mode" usage of Panache Reactive from a blocking thread; - * this is known to be tricky as Hibernate Reactive requires running on the event loop, - * while Panache relies on the notion of "current Session" being stored in the current - * CDI context. - */ -@Path("test") -public class TestEndpoint { - - @GET - @Path("store3fruits") - public String testStorage() { - Fruit apple = new Fruit("apple", "red"); - Fruit orange = new Fruit("orange", "orange"); - Fruit banana = new Fruit("banana", "yellow"); - - Panache.withTransaction(() -> Fruit.persist(apple, orange, banana)).subscribeAsCompletionStage().join(); - - //We wants this same request to also perform a read, so to trigger a second lookup of the Mutiny.Session from ArC - return verifyStored(); - } - - @GET - @Path("load3fruits") - public String verifyStored() { - final List fruitsList = Panache.withTransaction(() -> Fruit.find("select name, color from Fruit") - .list()) - .subscribeAsCompletionStage() - .join(); - return fruitsList.size() == 3 ? "OK" : "KO"; - } - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties b/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties deleted file mode 100644 index 6d0922b28d2fe..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/main/resources/application.properties +++ /dev/null @@ -1,6 +0,0 @@ -quarkus.datasource.db-kind=postgresql -quarkus.datasource.username=hibernate_orm_test -quarkus.datasource.password=hibernate_orm_test -quarkus.datasource.reactive.url=${postgres.reactive.url} - -quarkus.hibernate-orm.database.generation=drop-and-create diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java b/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java deleted file mode 100644 index 6f88d80421ba3..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityInGraalITCase.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import io.quarkus.test.junit.QuarkusIntegrationTest; - -/** - * Repeat all tests from {@link PanacheFunctionalityTest} in native image mode. - */ -@QuarkusIntegrationTest -public class PanacheFunctionalityInGraalITCase extends PanacheFunctionalityTest { - -} diff --git a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java b/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java deleted file mode 100644 index fac550debcebd..0000000000000 --- a/integration-tests/hibernate-reactive-panache-blocking/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.quarkus.it.panache.reactive; - -import static org.hamcrest.Matchers.is; - -import org.junit.jupiter.api.Test; - -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; - -/** - * Test various Panache operations running in Quarkus - */ -@QuarkusTest -public class PanacheFunctionalityTest { - - @Test - public void tests() { - RestAssured.when().get("/test/store3fruits").then().body(is("OK")); - RestAssured.when().get("/test/load3fruits").then().body(is("OK")); - } - -} diff --git a/integration-tests/hibernate-reactive-panache-kotlin/src/test/kotlin/io/quarkus/it/panache/reactive/kotlin/PanacheFunctionalityTest.kt b/integration-tests/hibernate-reactive-panache-kotlin/src/test/kotlin/io/quarkus/it/panache/reactive/kotlin/PanacheFunctionalityTest.kt index a327e7222a566..7deefbbcf1936 100644 --- a/integration-tests/hibernate-reactive-panache-kotlin/src/test/kotlin/io/quarkus/it/panache/reactive/kotlin/PanacheFunctionalityTest.kt +++ b/integration-tests/hibernate-reactive-panache-kotlin/src/test/kotlin/io/quarkus/it/panache/reactive/kotlin/PanacheFunctionalityTest.kt @@ -2,6 +2,8 @@ package io.quarkus.it.panache.reactive.kotlin import com.fasterxml.jackson.databind.ObjectMapper import io.quarkus.hibernate.reactive.panache.Panache +import io.quarkus.hibernate.reactive.panache.common.WithSession +import io.quarkus.hibernate.reactive.panache.common.WithTransaction import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional import io.quarkus.test.TestReactiveTransaction import io.quarkus.test.junit.DisabledOnIntegrationTest @@ -15,15 +17,13 @@ import io.smallrye.mutiny.Uni import jakarta.json.bind.JsonbBuilder import jakarta.persistence.PersistenceException import org.hamcrest.Matchers.`is` -import org.hibernate.reactive.mutiny.Mutiny import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.MethodOrderer import org.junit.jupiter.api.Order import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder -import kotlin.time.Duration.Companion.minutes -import kotlin.time.toJavaDuration +import java.util.function.Supplier @QuarkusTest @TestMethodOrder(MethodOrderer.OrderAnnotation::class) @@ -55,14 +55,10 @@ open class PanacheFunctionalityTest { } @Test + @RunOnVertxContext @DisabledOnIntegrationTest - fun testPanacheInTest() { - assertEquals( - 0, - Person.count() - .await() - .atMost(5.minutes.toJavaDuration()) - ) + fun testPanacheInTest(asserter: UniAsserter) { + asserter.assertEquals({ Panache.withSession { Person.count() } }, 0L) } @Test @@ -164,52 +160,47 @@ open class PanacheFunctionalityTest { } @Test - @ReactiveTransactional + @RunOnVertxContext @DisabledOnIntegrationTest - fun testTransaction(): Uni { - val transaction: Mutiny.Transaction? = Panache.currentTransaction() - .await() - .atMost(5.minutes.toJavaDuration()) - Assertions.assertNotNull(transaction) - return Uni.createFrom().nullItem() + fun testTransaction(asserter: UniAsserter) { + asserter.assertNotNull { Panache.withTransaction { Panache.currentTransaction() } } } @Test + @RunOnVertxContext @DisabledOnIntegrationTest - fun testNoTransaction() { - val transaction: Mutiny.Transaction? = Panache.currentTransaction() - .await() - .atMost(5.minutes.toJavaDuration()) - Assertions.assertNull(transaction) + fun testNoTransaction(asserter: UniAsserter) { + asserter.assertNull { Panache.withSession { Panache.currentTransaction() } } } @Test + @RunOnVertxContext @DisabledOnIntegrationTest - fun testBug7102() { - createBug7102() - .flatMap { person: Person -> - getBug7102(person.id!!) - .flatMap { person1: Person -> - assertEquals("pero", person1.name) - updateBug7102(person.id!!) - }.flatMap { _ -> getBug7102(person.id!!) } - .map { person2: Person -> - assertEquals("jozo", person2.name) - null - } - }.flatMap { Person.deleteAll() } - .await() - .atMost(5.minutes.toJavaDuration()) + fun testBug7102(asserter: UniAsserter) { + asserter.execute { + createBug7102() + .flatMap { person: Person -> + getBug7102(person.id!!) + .flatMap { person1: Person -> + assertEquals("pero", person1.name) + updateBug7102(person.id!!) + }.flatMap { _ -> getBug7102(person.id!!) } + .map { person2: Person -> + assertEquals("jozo", person2.name) + null + } + }.flatMap { Panache.withSession { Person.deleteAll() } } + } } - @ReactiveTransactional + @WithTransaction fun createBug7102(): Uni { val person = Person() person.name = "pero" return person.persistAndFlush() } - @ReactiveTransactional + @WithTransaction fun updateBug7102(id: Long): Uni { return Person.findById(id) .map { person: Person? -> @@ -218,7 +209,7 @@ open class PanacheFunctionalityTest { } } - @ReactiveTransactional + @WithSession fun getBug7102(id: Long): Uni { return Person.findById(id) .map { it!! } @@ -247,40 +238,59 @@ open class PanacheFunctionalityTest { @Test @Order(200) - @ReactiveTransactional + @RunOnVertxContext @DisabledOnIntegrationTest fun testReactiveTransactional(asserter: UniAsserter) { - asserter.assertNotNull { Panache.currentTransaction() } - asserter.assertEquals({ Person.count() }, 0L) - asserter.assertNotNull { Person().persist() } - asserter.assertEquals({ Person.count() }, 1L) + asserter.assertEquals({ reactiveTransactional() }, 1L) + } + + @WithTransaction + fun reactiveTransactional(): Uni { + return Panache.currentTransaction() + .invoke { tx -> Assertions.assertNotNull(tx) } + .chain { tx -> Person.count() } + .invoke { count -> assertEquals(0L, count) } + .call(Supplier { Person().persist() }) + .chain { tx -> Person.count() } } @Test @Order(201) - @ReactiveTransactional + @RunOnVertxContext @DisabledOnIntegrationTest fun testReactiveTransactional2(asserter: UniAsserter) { - asserter.assertNotNull { Panache.currentTransaction() } - // make sure the previous one was NOT rolled back - asserter.assertEquals({ Person.count() }, 1L) - // now delete everything and cause a rollback - asserter.assertEquals({ Person.deleteAll() }, 1L) - asserter.execute { - Panache.currentTransaction().invoke { tx: Mutiny.Transaction -> tx.markForRollback() } - } + asserter.assertTrue { reactiveTransactional2() } + } + + @WithTransaction + fun reactiveTransactional2(): Uni { + return Panache.currentTransaction() + .invoke { tx -> Assertions.assertNotNull(tx) } + .chain(Supplier { Person.count() }) + .invoke { count -> assertEquals(1L, count) } + .chain(Supplier { Person.deleteAll() }) + .invoke { count -> assertEquals(1L, count) } + .chain(Supplier { Panache.currentTransaction() }) + .invoke { tx -> tx.markForRollback() } + .map { tx -> true } } @Test @Order(202) - @ReactiveTransactional + @RunOnVertxContext @DisabledOnIntegrationTest fun testReactiveTransactional3(asserter: UniAsserter) { - asserter.assertNotNull { Panache.currentTransaction() } - // make sure it was rolled back - asserter.assertEquals({ Person.count() }, 1L) - // and clean up - asserter.assertEquals({ Person.deleteAll() }, 1L) + asserter.assertEquals({ testReactiveTransactional3() }, 1L) + } + + @ReactiveTransactional + fun testReactiveTransactional3(): Uni { + return Panache.currentTransaction() + .invoke { tx -> Assertions.assertNotNull(tx) } + .chain { tx -> Person.count() } + // make sure it was rolled back + .invoke { count -> assertEquals(1L, count) } + .call(Supplier { Person.deleteAll() }) } @Test @@ -288,6 +298,6 @@ open class PanacheFunctionalityTest { @RunOnVertxContext @DisabledOnIntegrationTest fun testPersistenceException(asserter: UniAsserter) { - asserter.assertFailedWith({ Person().delete() }, PersistenceException::class.java) + asserter.assertFailedWith({ Panache.withSession { Person().delete() } }, PersistenceException::class.java) } } diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java index 5efc721fa7777..a3388fb0590be 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheFunctionalityTest.java @@ -2,12 +2,12 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.persistence.PersistenceException; -import org.hibernate.reactive.mutiny.Mutiny.Transaction; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; @@ -18,6 +18,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; +import io.quarkus.hibernate.reactive.panache.common.WithTransaction; import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional; import io.quarkus.test.TestReactiveTransaction; import io.quarkus.test.junit.DisabledOnIntegrationTest; @@ -65,9 +67,10 @@ public void testPanacheSerialisation() { } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - public void testPanacheInTest() { - Assertions.assertEquals(0, Person.count().await().indefinitely()); + public void testPanacheInTest(UniAsserter asserter) { + asserter.assertEquals(() -> Panache.withSession(() -> Person.count()), 0l); } @Test @@ -157,25 +160,24 @@ public void testSortByNullPrecedence() { } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test - Uni testTransaction() { - Transaction transaction = Panache.currentTransaction().await().indefinitely(); - Assertions.assertNotNull(transaction); - return Uni.createFrom().nullItem(); + void testTransaction(UniAsserter asserter) { + asserter.assertNotNull(() -> Panache.withTransaction(() -> Panache.currentTransaction())); } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - void testNoTransaction() { - Transaction transaction = Panache.currentTransaction().await().indefinitely(); - Assertions.assertNull(transaction); + void testNoTransaction(UniAsserter asserter) { + asserter.assertNull(() -> Panache.withSession(() -> Panache.currentTransaction())); } @DisabledOnIntegrationTest + @RunOnVertxContext @Test - public void testBug7102() { - createBug7102() + public void testBug7102(UniAsserter asserter) { + asserter.execute(() -> createBug7102() .flatMap(person -> { return getBug7102(person.id) .flatMap(person1 -> { @@ -187,18 +189,17 @@ public void testBug7102() { Assertions.assertEquals("jozo", person2.name); return null; }); - }).flatMap(v -> Person.deleteAll()) - .await().indefinitely(); + }).flatMap(v -> Panache.withTransaction(() -> Person.deleteAll()))); } - @ReactiveTransactional + @WithTransaction Uni createBug7102() { Person personPanache = new Person(); personPanache.name = "pero"; return personPanache.persistAndFlush().map(v -> personPanache); } - @ReactiveTransactional + @WithTransaction Uni updateBug7102(Long id) { return Person. findById(id) .map(person -> { @@ -207,7 +208,7 @@ Uni updateBug7102(Long id) { }); } - @ReactiveTransactional + @WithSession Uni getBug7102(Long id) { return Person.findById(id); } @@ -234,39 +235,60 @@ public void testTestTransaction2(UniAsserter asserter) { } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(200) public void testReactiveTransactional(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - asserter.assertEquals(() -> Person.count(), 0l); - asserter.assertNotNull(() -> new Person().persist()); - asserter.assertEquals(() -> Person.count(), 1l); + asserter.assertEquals(() -> reactiveTransactional(), 1l); + } + + @WithTransaction + Uni reactiveTransactional() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + .invoke(count -> assertEquals(0l, count)) + .call(() -> new Person().persist()) + .chain(tx -> Person.count()); } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(201) public void testReactiveTransactional2(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - // make sure the previous one was NOT rolled back - asserter.assertEquals(() -> Person.count(), 1l); - // now delete everything and cause a rollback - asserter.assertEquals(() -> Person.deleteAll(), 1l); - asserter.execute(() -> Panache.currentTransaction().invoke(tx -> tx.markForRollback())); + asserter.assertTrue(() -> reactiveTransactional2()); + } + + @WithTransaction + Uni reactiveTransactional2() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + .invoke(count -> assertEquals(1l, count)) + .chain(() -> Person.deleteAll()) + .invoke(count -> assertEquals(1l, count)) + .chain(() -> Panache.currentTransaction()) + .invoke(tx -> tx.markForRollback()) + .map(tx -> true); } @DisabledOnIntegrationTest - @ReactiveTransactional + @RunOnVertxContext @Test @Order(202) public void testReactiveTransactional3(UniAsserter asserter) { - asserter.assertNotNull(() -> Panache.currentTransaction()); - // make sure it was rolled back - asserter.assertEquals(() -> Person.count(), 1l); - // and clean up - asserter.assertEquals(() -> Person.deleteAll(), 1l); + asserter.assertEquals(() -> testReactiveTransactional3(), 1l); + } + + @ReactiveTransactional + Uni testReactiveTransactional3() { + return Panache.currentTransaction() + .invoke(tx -> assertNotNull(tx)) + .chain(tx -> Person.count()) + // make sure it was rolled back + .invoke(count -> assertEquals(1l, count)) + .call(() -> Person.deleteAll()); } @DisabledOnIntegrationTest @@ -274,6 +296,6 @@ public void testReactiveTransactional3(UniAsserter asserter) { @Test @Order(300) public void testPersistenceException(UniAsserter asserter) { - asserter.assertFailedWith(() -> new Person().delete(), PersistenceException.class); + asserter.assertFailedWith(() -> Panache.withTransaction(() -> new Person().delete()), PersistenceException.class); } } diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java index 78931c8cdbc42..d8a87e05176ca 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/PanacheMockingTest.java @@ -1,5 +1,8 @@ package io.quarkus.it.panache.reactive; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + import java.util.Collections; import jakarta.inject.Inject; @@ -11,57 +14,75 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase; import io.quarkus.panache.mock.PanacheMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; +import io.quarkus.test.vertx.RunOnVertxContext; +import io.quarkus.test.vertx.UniAsserter; import io.smallrye.mutiny.Uni; @QuarkusTest public class PanacheMockingTest { + @SuppressWarnings("static-access") @Test + @RunOnVertxContext @Order(1) - public void testPanacheMocking() { - PanacheMock.mock(Person.class); - - Assertions.assertEquals(0, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, Person.count().await().indefinitely()); - - Mockito.when(Person.count()).thenCallRealMethod(); - Assertions.assertEquals(0, Person.count().await().indefinitely()); - - PanacheMock.verify(Person.class, Mockito.times(4)).count(); - - Person p = new Person(); - Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, Person.findById(12l).await().indefinitely()); - Assertions.assertNull(Person.findById(42l).await().indefinitely()); - - Person.persist(p).await().indefinitely(); - Assertions.assertNull(p.id); - - Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException()); - try { - Person.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } - - Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(Person.findOrdered().await().indefinitely().isEmpty()); - - PanacheMock.verify(Person.class).findOrdered(); - PanacheMock.verify(Person.class).persist(Mockito. any(), Mockito. any()); - PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); - PanacheMock.verifyNoMoreInteractions(Person.class); - - Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + public void testPanacheMocking(UniAsserter asserter) { + String key = "person"; + + asserter.execute(() -> PanacheMock.mock(Person.class)); + asserter.assertEquals(() -> Person.count(), 0l); + + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> Person.count(), 23l); + + asserter.execute(() -> Mockito.when(Person.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> Person.count(), 42l); + + asserter.execute(() -> Mockito.when(Person.count()).thenCallRealMethod()); + asserter.assertEquals(() -> Person.count(), 0l); + + asserter.execute(() -> { + // use block lambda here, otherwise mutiny fails with NPE + PanacheMock.verify(Person.class, Mockito.times(4)).count(); + }); + + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(Person.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); + asserter.assertThat(() -> Person.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> Person.findById(42l)); + + asserter.execute(() -> Person.persist(asserter.getData(key))); + asserter.execute(() -> assertNull(((Person) asserter.getData(key)).id)); + + asserter.execute(() -> Mockito.when(Person.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return Person.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); + + asserter.execute(() -> Mockito.when(Person.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> Person.findOrdered(), list -> list.isEmpty()); + + asserter.execute(() -> { + PanacheMock.verify(Person.class).findOrdered(); + PanacheMock.verify(Person.class).persist(Mockito. any(), Mockito. any()); + PanacheMock.verify(Person.class, Mockito.atLeastOnce()).findById(Mockito.any()); + PanacheMock.verifyNoMoreInteractions(Person.class); + Assertions.assertEquals(0, Person.methodWithPrimitiveParams(true, (byte) 0, (short) 0, 0, 2, 2.0f, 2.0, 'c')); + }); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> Panache.withSession(() -> u)); } @Test @@ -73,63 +94,85 @@ public void testPanacheMockingWasCleared() { @InjectMock MockablePersonRepository mockablePersonRepository; + @RunOnVertxContext @Test - public void testPanacheRepositoryMocking() throws Throwable { - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); - - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l)); - Assertions.assertEquals(23, mockablePersonRepository.count().await().indefinitely()); - - Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l)); - Assertions.assertEquals(42, mockablePersonRepository.count().await().indefinitely()); - - Mockito.when(mockablePersonRepository.count()).thenCallRealMethod(); - Assertions.assertEquals(0, mockablePersonRepository.count().await().indefinitely()); - - Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); - - Person p = new Person(); - Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); - Assertions.assertSame(p, mockablePersonRepository.findById(12l).await().indefinitely()); - Assertions.assertNull(mockablePersonRepository.findById(42l).await().indefinitely()); - - mockablePersonRepository.persist(p).await().indefinitely(); - Assertions.assertNull(p.id); - - Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException()); - try { - mockablePersonRepository.findById(12l); - Assertions.fail(); - } catch (WebApplicationException x) { - } - - Mockito.when(mockablePersonRepository.findOrdered()).thenReturn(Uni.createFrom().item(Collections.emptyList())); - Assertions.assertTrue(mockablePersonRepository.findOrdered().await().indefinitely().isEmpty()); - - Mockito.verify(mockablePersonRepository).findOrdered(); - Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); - Mockito.verify(mockablePersonRepository).persist(Mockito. any()); - Mockito.verifyNoMoreInteractions(mockablePersonRepository); + public void testPanacheRepositoryMocking(UniAsserter asserter) throws Throwable { + String key = "person"; + + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(23l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 23l); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenReturn(Uni.createFrom().item(42l))); + asserter.assertEquals(() -> mockablePersonRepository.count(), 42l); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.count()).thenCallRealMethod()); + asserter.assertEquals(() -> mockablePersonRepository.count(), 0l); + + asserter.execute(() -> { + // use block lambda here, otherwise mutiny fails with NPE + Mockito.verify(mockablePersonRepository, Mockito.times(4)).count(); + }); + + asserter.execute(() -> { + Person p = new Person(); + Mockito.when(mockablePersonRepository.findById(12l)).thenReturn(Uni.createFrom().item(p)); + asserter.putData(key, p); + }); + + asserter.assertThat(() -> mockablePersonRepository.findById(12l), p -> Assertions.assertSame(p, asserter.getData(key))); + asserter.assertNull(() -> mockablePersonRepository.findById(42l)); + + asserter.execute(() -> mockablePersonRepository.persist((Person) asserter.getData(key))); + asserter.execute(() -> assertNull(((Person) asserter.getData(key)).id)); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.findById(12l)).thenThrow(new WebApplicationException())); + asserter.assertFailedWith(() -> { + try { + return mockablePersonRepository.findById(12l); + } catch (Exception e) { + return Uni.createFrom().failure(e); + } + }, t -> assertEquals(WebApplicationException.class, t.getClass())); + + asserter.execute(() -> Mockito.when(mockablePersonRepository.findOrdered()) + .thenReturn(Uni.createFrom().item(Collections.emptyList()))); + asserter.assertThat(() -> mockablePersonRepository.findOrdered(), list -> list.isEmpty()); + + asserter.execute(() -> { + Mockito.verify(mockablePersonRepository).findOrdered(); + Mockito.verify(mockablePersonRepository, Mockito.atLeastOnce()).findById(Mockito.any()); + Mockito.verify(mockablePersonRepository).persist(Mockito. any()); + Mockito.verifyNoMoreInteractions(mockablePersonRepository); + }); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> Panache.withSession(() -> u)); } @Inject PersonRepository realPersonRepository; + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RunOnVertxContext @Test - public void testPanacheRepositoryBridges() { + public void testPanacheRepositoryBridges(UniAsserter asserter) { // normal method call - Assertions.assertNull(realPersonRepository.findById(0l).await().indefinitely()); + asserter.assertNull(() -> realPersonRepository.findById(0l)); // bridge call - Assertions.assertNull(((PanacheRepositoryBase) realPersonRepository).findById(0l).await().indefinitely()); + asserter.assertNull(() -> ((PanacheRepositoryBase) realPersonRepository).findById(0l)); // normal method call - Assertions.assertNull(realPersonRepository.findById(0l, LockModeType.NONE).await().indefinitely()); + asserter.assertNull(() -> realPersonRepository.findById(0l, LockModeType.NONE)); // bridge call - Assertions.assertNull( - ((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE).await().indefinitely()); + asserter.assertNull(() -> ((PanacheRepositoryBase) realPersonRepository).findById(0l, LockModeType.NONE)); // normal method call - Assertions.assertEquals(false, realPersonRepository.deleteById(0l).await().indefinitely()); + asserter.assertFalse(() -> realPersonRepository.deleteById(0l)); // bridge call - Assertions.assertEquals(false, ((PanacheRepositoryBase) realPersonRepository).deleteById(0l).await().indefinitely()); + asserter.assertFalse(() -> ((PanacheRepositoryBase) realPersonRepository).deleteById(0l)); + + // Execute the asserter within a reactive session + asserter.surroundWith(u -> Panache.withSession(() -> u)); } } diff --git a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java index 2a40905c07e42..241f7bf51e6e5 100644 --- a/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java +++ b/integration-tests/hibernate-reactive-panache/src/test/java/io/quarkus/it/panache/reactive/TestReactiveTransactionTest.java @@ -6,17 +6,20 @@ import io.quarkus.hibernate.reactive.panache.Panache; import io.quarkus.test.TestReactiveTransaction; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.vertx.RunOnVertxContext; import io.quarkus.test.vertx.UniAsserter; @QuarkusTest public class TestReactiveTransactionTest { + @RunOnVertxContext @TestReactiveTransaction @Test public void testTestTransaction(UniAsserter asserter) { asserter.assertNotNull(() -> Panache.currentTransaction()); } + @RunOnVertxContext @TestReactiveTransaction @BeforeEach public void beforeEach(UniAsserter asserter) { diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 0268d4630a39d..4a4c9e171debe 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -189,7 +189,6 @@ hibernate-reactive-postgresql hibernate-reactive-panache hibernate-reactive-panache-kotlin - hibernate-reactive-panache-blocking hibernate-search-orm-elasticsearch hibernate-search-orm-elasticsearch-coordination-outbox-polling hibernate-search-orm-opensearch diff --git a/integration-tests/reactive-messaging-hibernate-reactive/src/main/java/io/quarkus/it/kafka/KafkaReceivers.java b/integration-tests/reactive-messaging-hibernate-reactive/src/main/java/io/quarkus/it/kafka/KafkaReceivers.java index e916c017a691d..c42b69dfd641e 100644 --- a/integration-tests/reactive-messaging-hibernate-reactive/src/main/java/io/quarkus/it/kafka/KafkaReceivers.java +++ b/integration-tests/reactive-messaging-hibernate-reactive/src/main/java/io/quarkus/it/kafka/KafkaReceivers.java @@ -15,6 +15,7 @@ import org.hibernate.reactive.mutiny.Mutiny; import io.quarkus.hibernate.reactive.panache.Panache; +import io.quarkus.hibernate.reactive.panache.common.WithSession; import io.smallrye.common.vertx.ContextLocals; import io.smallrye.common.vertx.VertxContext; import io.smallrye.mutiny.Uni; @@ -72,6 +73,7 @@ public CompletionStage consume(Message msg) { return msg.ack(); } + @WithSession public Uni> getFruits() { return Fruit.listAll(); } diff --git a/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java b/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java index 597f3dfcd5c5e..a348c0a83c430 100644 --- a/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java +++ b/test-framework/vertx/src/main/java/io/quarkus/test/vertx/RunOnVertxContextTestMethodInvoker.java @@ -41,10 +41,6 @@ public Object methodParamInstance(String paramClassName) { public boolean supportsMethod(Class originalTestClass, Method originalTestMethod) { return hasAnnotation(RunOnVertxContext.class, originalTestMethod.getAnnotations()) || hasAnnotation(RunOnVertxContext.class, originalTestClass.getAnnotations()) - || hasAnnotation("io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional", - originalTestMethod.getAnnotations()) - || hasAnnotation("io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional", - originalTestClass.getAnnotations()) || hasAnnotation(TestReactiveTransaction.class, originalTestMethod.getAnnotations()) || hasAnnotation(TestReactiveTransaction.class, originalTestClass.getAnnotations()); } @@ -102,7 +98,8 @@ private boolean shouldContextBeDuplicated(Class c, Method m) { runOnVertxContext = c.getAnnotation(RunOnVertxContext.class); } if (runOnVertxContext == null) { - return false; + // Use duplicated context if @TestReactiveTransaction is present + return m.isAnnotationPresent(TestReactiveTransaction.class); } else { return runOnVertxContext.duplicateContext(); }