experimental support for reactive Jakarta Data repositories

This commit is contained in:
Gavin King 2024-03-25 14:41:55 +01:00
parent 7b189d0200
commit d52edeb0e5
16 changed files with 418 additions and 51 deletions

View File

@ -71,6 +71,7 @@ dependencies {
quarkusOrmPanacheImplementation "io.quarkus:quarkus-hibernate-orm-panache:3.6.2"
quarkusHrPanacheImplementation "io.quarkus:quarkus-hibernate-reactive-panache:3.6.2"
jakartaDataImplementation "jakarta.data:jakarta.data-api:1.0.0-SNAPSHOT"
jakartaDataImplementation "org.hibernate.reactive:hibernate-reactive-core:2.2.2.Final"
}
// The source set gets a custom configuration which extends the normal test implementation config

View File

@ -0,0 +1,6 @@
package org.hibernate.processor.test.data.reactive;
import jakarta.persistence.Embeddable;
@Embeddable
public record Address(String street, String city, String postcode) {}

View File

@ -0,0 +1,23 @@
package org.hibernate.processor.test.data.reactive;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import java.util.Set;
@Entity
public class Author {
@Id
String ssn;
@Basic(optional = false)
String name;
Address address;
@ManyToMany
Set<Book> books;
}

View File

@ -0,0 +1,59 @@
package org.hibernate.processor.test.data.reactive;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import org.hibernate.annotations.NaturalId;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.Set;
@Entity
public class Book {
@Id
String isbn;
@NaturalId
String title;
@NaturalId
LocalDate publicationDate;
String text;
@Enumerated(EnumType.STRING)
@Basic(optional = false)
Type type = Type.Book;
@ManyToOne(optional = false)
Publisher publisher;
@ManyToMany(mappedBy = "books")
Set<Author> authors;
@Basic(optional = false)
int pages ;
BigDecimal price;
BigInteger quantitySold;
public Book(String isbn, String title, String text) {
this.isbn = isbn;
this.title = title;
this.text = text;
}
protected Book() {}
@Override
public String toString() {
return isbn + " : " + title + " [" + type + "]";
}
}

View File

@ -0,0 +1,106 @@
package org.hibernate.processor.test.data.reactive;
import io.smallrye.mutiny.Uni;
import jakarta.data.Limit;
import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.data.page.Page;
import jakarta.data.page.PageRequest;
import jakarta.data.repository.By;
import jakarta.data.repository.Delete;
import jakarta.data.repository.Find;
import jakarta.data.repository.Insert;
import jakarta.data.repository.OrderBy;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import jakarta.data.repository.Save;
import jakarta.data.repository.Update;
import org.hibernate.reactive.mutiny.Mutiny;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface Library {
Mutiny.StatelessSession session(); //required
@Find
Uni<Book> book(String isbn);
@Find
Uni<List<Book>> books(@By("isbn") List<String> isbns);
@Find
Uni<Book> book(String title, LocalDate publicationDate);
@Find
Uni<List<Book>> publications(Type type, Sort<Book> sort);
@Find
@OrderBy("title")
Uni<List<Book>> booksByPublisher(String publisher_name);
@Query("where title like :titlePattern")
@OrderBy("title")
Uni<List<Book>> booksByTitle(String titlePattern);
// not required by Jakarta Data
record BookWithAuthor(Book book, Author author) {}
@Query("select b, a from Book b join b.authors a order by b.isbn, a.ssn")
Uni<List<BookWithAuthor>> booksWithAuthors();
@Insert
Uni<Void> create(Book book);
@Insert
Uni<Void> create(Book[] book);
@Update
Uni<Void> update(Book book);
@Update
Uni<Void> update(Book[] books);
@Delete
Uni<Void> delete(Book book);
@Delete
Uni<Void> delete(Book[] book);
@Save
Uni<Void> upsert(Book book);
@Find
Uni<Author> author(String ssn);
@Insert
Uni<Void> create(Author author);
@Update
Uni<Void> update(Author author);
@Insert
Uni<Publisher[]> insertAll(Publisher[] publishers);
@Save
Uni<Publisher> save(Publisher publisher);
@Delete
Uni<Publisher> delete(Publisher publisher);
@Find
@OrderBy("isbn")
Uni<Page<Book>> allBooks(PageRequest<Book> pageRequest);
@Find
@OrderBy("name")
@OrderBy("address.city")
Uni<List<Author>> allAuthors(Order<Author> order, Limit limit);
@Find
Uni<List<Author>> authorsByCity(@By("address.city") String city);
@Find
Uni<List<Author>> authorsByCityAndPostcode(String address_city, String address_postcode);
}

View File

@ -0,0 +1,19 @@
package org.hibernate.processor.test.data.reactive;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.Set;
@Entity
public class Publisher {
@Id long id;
@Basic(optional = false)
String name;
@OneToMany(mappedBy = "publisher")
Set<Book> books;
}

View File

@ -0,0 +1,36 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.processor.test.data.reactive;
import org.hibernate.processor.test.util.CompilationTest;
import org.hibernate.processor.test.util.WithClasses;
import org.junit.Test;
import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor;
import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString;
/**
* @author Gavin King
*/
public class ReactiveTest extends CompilationTest {
@Test
@WithClasses({ Publisher.class, Author.class, Address.class, Book.class, Library.class })
public void test() {
System.out.println( getMetaModelSourceAsString( Author.class ) );
System.out.println( getMetaModelSourceAsString( Book.class ) );
System.out.println( getMetaModelSourceAsString( Author.class, true ) );
System.out.println( getMetaModelSourceAsString( Book.class, true ) );
System.out.println( getMetaModelSourceAsString( Library.class ) );
assertMetamodelClassGeneratedFor( Author.class, true );
assertMetamodelClassGeneratedFor( Book.class, true );
assertMetamodelClassGeneratedFor( Publisher.class, true );
assertMetamodelClassGeneratedFor( Author.class );
assertMetamodelClassGeneratedFor( Book.class );
assertMetamodelClassGeneratedFor( Publisher.class );
assertMetamodelClassGeneratedFor( Library.class );
}
}

View File

@ -0,0 +1,3 @@
package org.hibernate.processor.test.data.reactive;
public enum Type { Book, Magazine, Journal }

View File

@ -7,12 +7,12 @@
package org.hibernate.processor.annotation;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.processor.util.Constants;
import java.util.List;
import java.util.Locale;
import static org.hibernate.processor.util.Constants.HIB_SESSION;
import static org.hibernate.processor.util.Constants.UNI;
import static org.hibernate.processor.util.StringUtil.getUpperUnderscoreCaseFromLowerCamelCase;
/**
@ -194,7 +194,7 @@ public abstract class AbstractFinderMethod extends AbstractQueryMethod {
private void entityType(StringBuilder declaration) {
if ( isReactive() ) {
declaration
.append(annotationMetaEntity.importType(Constants.UNI))
.append(annotationMetaEntity.importType(UNI))
.append('<');
}
declaration

View File

@ -180,11 +180,13 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
}
boolean isUsingStatelessSession() {
return HIB_STATELESS_SESSION.equals(sessionType);
return HIB_STATELESS_SESSION.equals(sessionType)
|| MUTINY_STATELESS_SESSION.equals(sessionType);
}
boolean isReactive() {
return MUTINY_SESSION.equals(sessionType)
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|| UNI_MUTINY_SESSION.equals(sessionType);
}
@ -440,7 +442,7 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
declaration
.append('\t');
if ( isJakartaCursoredPage(containerType)
|| isJakartaPage(containerType) ) {
|| isJakartaPage(containerType) && !isReactive() ) {
if ( returnTypeName != null && isUsingEntityManager() ) {
// this is necessary to avoid losing the type
// after unwrapping the Query object
@ -467,18 +469,24 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
private void totalResults(StringBuilder declaration, List<String> paramTypes) {
declaration
.append("\tlong _totalResults = \n\t\t\t\t")
.append(parameterName(JD_PAGE_REQUEST, paramTypes, paramNames))
.append(".requestTotal()\n\t\t\t\t\t\t? ");
createQuery( declaration );
setParameters( declaration, paramTypes, "\t\t\t\t\t");
if ( isUsingEntityManager() ) {
declaration
.append("\t\t\t\t\t");
.append("\tlong _totalResults = \n\t\t\t\t");
if ( isReactive() ) {
declaration.append("-1;\n"); //TODO: add getResultCount() to HR
}
else {
declaration
.append(parameterName(JD_PAGE_REQUEST, paramTypes, paramNames))
.append(".requestTotal()\n\t\t\t\t\t\t? ");
createQuery( declaration );
setParameters( declaration, paramTypes, "\t\t\t\t\t");
if ( isUsingEntityManager() ) {
declaration
.append("\t\t\t\t\t");
}
unwrapQuery( declaration, !isUsingEntityManager() );
declaration
.append("\t\t\t\t\t\t\t\t.getResultCount()\n\t\t\t\t\t\t: -1;\n");
}
unwrapQuery( declaration, !isUsingEntityManager() );
declaration
.append("\t\t\t\t\t\t\t\t.getResultCount()\n\t\t\t\t\t\t: -1;\n");
}
void collectOrdering(StringBuilder declaration, List<String> paramTypes) {
@ -646,13 +654,28 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
.append(");");
break;
case JD_PAGE:
if ( isReactive() ) {
declaration
.append("\t\t\t.getResultList()\n")
.append("\t\t\t.map(_results -> ");
}
else {
declaration
.append("\t\t\t.getResultList();\n")
.append("\t\treturn ");
}
declaration
.append("\t\t\t.getResultList();\n")
.append("\t\treturn new ")
.append("new ")
.append(annotationMetaEntity.importType("jakarta.data.page.impl.PageRecord"))
.append('(')
.append(parameterName(JD_PAGE_REQUEST, paramTypes, paramNames))
.append(", _results, _totalResults);");
.append(", _results, _totalResults)");
if ( isReactive() ) {
declaration
.append(')');
}
declaration
.append(';');
break;
case JD_CURSORED_PAGE:
if ( returnTypeName == null ) {
@ -716,4 +739,10 @@ public abstract class AbstractQueryMethod implements MetaAttribute {
&& containerType.startsWith("org.hibernate");
}
boolean isUnifiableReturnType(@Nullable String containerType) {
return containerType == null
|| LIST.equals(containerType)
|| JD_PAGE.equals(containerType)
|| JD_CURSORED_PAGE.equals(containerType);
}
}

View File

@ -668,6 +668,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return name.contentEquals(HIB_SESSION)
|| name.contentEquals(HIB_STATELESS_SESSION)
|| name.contentEquals(MUTINY_SESSION)
|| name.contentEquals(MUTINY_STATELESS_SESSION)
|| name.contentEquals(ENTITY_MANAGER);
}
}
@ -1149,8 +1150,9 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
private void addLifecycleMethod(ExecutableElement method) {
final TypeMirror returnType = method.getReturnType();
if ( !HIB_STATELESS_SESSION.equals(sessionType) ) {
final TypeMirror returnType = ununi(method.getReturnType());
if ( !HIB_STATELESS_SESSION.equals(sessionType)
&& !MUTINY_STATELESS_SESSION.equals(sessionType) ) {
context.message( method,
"repository must be backed by a 'StatelessSession'",
Diagnostic.Kind.ERROR );
@ -1167,7 +1169,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
Diagnostic.Kind.ERROR );
}
else {
final boolean returnArgument = returnType.getKind() != TypeKind.VOID;
final boolean returnArgument = !isVoid(returnType);
final String operation = lifecycleOperation( method );
final VariableElement parameter = method.getParameters().get(0);
final TypeMirror declaredParameterType = parameter.asType();
@ -1203,6 +1205,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
methodName,
parameter.getSimpleName().toString(),
getSessionVariableName(),
sessionType,
operation,
context.addNonnullAnnotation(),
declaredType != parameterType,
@ -1213,6 +1216,19 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
}
private static boolean isVoid(TypeMirror returnType) {
switch (returnType.getKind()) {
case VOID:
return true;
case DECLARED:
final DeclaredType declaredType = (DeclaredType) returnType;
final TypeElement typeElement = (TypeElement) declaredType.asElement();
return typeElement.getQualifiedName().contentEquals(Void.class.getName());
default:
return false;
}
}
private @Nullable DeclaredType entityType(TypeMirror parameterType) {
final Types types = context.getTypeUtils();
switch ( parameterType.getKind() ) {
@ -1576,6 +1592,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
case HIB_SESSION:
case HIB_STATELESS_SESSION:
case MUTINY_SESSION:
case MUTINY_STATELESS_SESSION:
return "session";
default:
return sessionGetter;
@ -2604,10 +2621,12 @@ public class AnnotationMetaEntity extends AnnotationMeta {
private boolean usingReactiveSession(String sessionType) {
return MUTINY_SESSION.equals(sessionType)
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|| UNI_MUTINY_SESSION.equals(sessionType);
}
private boolean usingStatelessSession(String sessionType) {
return HIB_STATELESS_SESSION.equals(sessionType);
return HIB_STATELESS_SESSION.equals(sessionType)
|| MUTINY_STATELESS_SESSION.equals(sessionType);
}
}

View File

@ -12,8 +12,6 @@ import org.hibernate.processor.util.Constants;
import java.util.List;
import static org.hibernate.processor.util.Constants.LIST;
/**
* @author Gavin King
*/
@ -110,8 +108,7 @@ public class CriteriaFinderMethod extends AbstractCriteriaMethod {
type.append(annotationMetaEntity.importType(returnTypeName)).append("[]");
}
else {
boolean returnsUni = isReactive()
&& (containerType == null || LIST.equals(containerType));
final boolean returnsUni = isReactive() && isUnifiableReturnType(containerType);
if ( returnsUni ) {
type.append(annotationMetaEntity.importType(Constants.UNI)).append('<');
}

View File

@ -13,6 +13,10 @@ import org.hibernate.processor.util.Constants;
import static org.hibernate.processor.util.Constants.ENTITY_MANAGER_FACTORY;
import static org.hibernate.processor.util.Constants.HIB_SESSION_FACTORY;
import static org.hibernate.processor.util.Constants.MUTINY_SESSION;
import static org.hibernate.processor.util.Constants.MUTINY_SESSION_FACTORY;
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
/**
* Used by the container to instantiate a Jakarta Data repository.
@ -45,6 +49,12 @@ public class DefaultConstructor implements MetaAttribute {
this.addInjectAnnotation = addInjectAnnotation;
}
private boolean isReactive() {
return MUTINY_SESSION.equals(sessionTypeName)
|| MUTINY_STATELESS_SESSION.equals(sessionTypeName)
|| UNI_MUTINY_SESSION.equals(sessionTypeName);
}
@Override
public boolean hasTypedAttribute() {
return true;
@ -84,9 +94,14 @@ public class DefaultConstructor implements MetaAttribute {
.append(" = ")
.append(sessionVariableName)
.append("Factory.unwrap(")
.append(annotationMetaEntity.importType(HIB_SESSION_FACTORY))
.append(".class).openStatelessSession();")
.append("\n}\n\n");
.append(annotationMetaEntity.importType(isReactive() ? MUTINY_SESSION_FACTORY : HIB_SESSION_FACTORY))
.append(".class).openStatelessSession()");
if ( isReactive() ) {
declaration
.append(".await().indefinitely()");
}
declaration
.append(";\n}\n\n");
declaration.append('@')
.append(annotationMetaEntity.importType("jakarta.annotation.PreDestroy"))
.append("\nprivate void closeSession() {")

View File

@ -9,12 +9,18 @@ package org.hibernate.processor.annotation;
import org.hibernate.processor.model.MetaAttribute;
import org.hibernate.processor.model.Metamodel;
import static org.hibernate.processor.util.Constants.MUTINY_SESSION;
import static org.hibernate.processor.util.Constants.MUTINY_STATELESS_SESSION;
import static org.hibernate.processor.util.Constants.UNI;
import static org.hibernate.processor.util.Constants.UNI_MUTINY_SESSION;
public class LifecycleMethod implements MetaAttribute {
private final AnnotationMetaEntity annotationMetaEntity;
private final String entity;
private final String methodName;
private final String parameterName;
private final String sessionName;
private final String sessionType;
private final String operationName;
private final boolean addNonnullAnnotation;
private final boolean iterateParameter;
@ -26,6 +32,7 @@ public class LifecycleMethod implements MetaAttribute {
String methodName,
String parameterName,
String sessionName,
String sessionType,
String operationName,
boolean addNonnullAnnotation,
boolean iterateParameter,
@ -35,6 +42,7 @@ public class LifecycleMethod implements MetaAttribute {
this.methodName = methodName;
this.parameterName = parameterName;
this.sessionName = sessionName;
this.sessionType = sessionType;
this.operationName = operationName;
this.addNonnullAnnotation = addNonnullAnnotation;
this.iterateParameter = iterateParameter;
@ -79,39 +87,66 @@ public class LifecycleMethod implements MetaAttribute {
private void returnArgument(StringBuilder declaration) {
if ( returnArgument ) {
declaration
.append("\t\treturn ")
if ( isReactive() ) {
declaration
.append(".replaceWith(")
.append(parameterName)
.append(")");
}
else {
declaration
.append("\t\treturn ")
.append(parameterName);
}
declaration
.append(";\n");
}
else {
if ( isReactive() ) {
declaration
.append(";\n");
}
}
}
private void delegateCall(StringBuilder declaration) {
if ( iterateParameter ) {
if ( isReactive() ) {
declaration
.append("\t\tfor (var entity : ")
.append("\t\treturn ")
.append(sessionName)
.append('.')
.append(operationName)
.append('(')
.append(parameterName)
.append(") {\n\t");
.append(')');
}
declaration
.append("\t\t")
.append(sessionName)
.append('.')
.append(operationName)
.append('(')
.append(iterateParameter ? "entity" : parameterName)
.append(')')
.append(";\n");
if ( iterateParameter ) {
else {
if ( iterateParameter ) {
declaration
.append("\t\tfor (var _entity : ")
.append(parameterName)
.append(") {\n\t");
}
declaration
.append("\t\t}\n");
.append("\t\t")
.append(sessionName)
.append('.')
.append(operationName)
.append('(')
.append(iterateParameter ? "_entity" : parameterName)
.append(')')
.append(";\n");
if ( iterateParameter ) {
declaration
.append("\t\t}\n");
}
}
}
private void preamble(StringBuilder declaration) {
declaration
.append("\n@Override\npublic ")
.append(returnArgument ? annotationMetaEntity.importType(entity) : "void")
.append(returnType())
.append(' ')
.append(methodName)
.append('(');
@ -124,6 +159,19 @@ public class LifecycleMethod implements MetaAttribute {
.append(" {\n");
}
private String returnType() {
final String entityType = annotationMetaEntity.importType(entity);
if ( isReactive() ) {
return annotationMetaEntity.importType(UNI)
+ '<' + (returnArgument ? entityType : "Void") + '>';
}
else {
return returnArgument
? entityType
: "void";
}
}
private void nullCheck(StringBuilder declaration) {
declaration
.append("\tif (")
@ -177,4 +225,10 @@ public class LifecycleMethod implements MetaAttribute {
public Metamodel getHostingEntity() {
return annotationMetaEntity;
}
private boolean isReactive() {
return MUTINY_SESSION.equals(sessionType)
|| MUTINY_STATELESS_SESSION.equals(sessionType)
|| UNI_MUTINY_SESSION.equals(sessionType);
}
}

View File

@ -9,12 +9,11 @@ package org.hibernate.processor.annotation;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.AssertionFailure;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.processor.util.Constants;
import java.util.List;
import static org.hibernate.processor.util.Constants.LIST;
import static org.hibernate.processor.util.Constants.QUERY;
import static org.hibernate.processor.util.Constants.UNI;
import static org.hibernate.processor.util.StringUtil.getUpperUnderscoreCaseFromLowerCamelCase;
/**
@ -215,10 +214,9 @@ public class QueryMethod extends AbstractQueryMethod {
type.append(annotationMetaEntity.importType(returnTypeName)).append("[]");
}
else {
boolean returnsUni = isReactive()
&& (containerType == null || LIST.equals(containerType));
final boolean returnsUni = isReactive() && isUnifiableReturnType(containerType);
if ( returnsUni ) {
type.append(annotationMetaEntity.importType(Constants.UNI)).append('<');
type.append(annotationMetaEntity.importType(UNI)).append('<');
}
if ( containerType != null ) {
type.append(annotationMetaEntity.importType(containerType));

View File

@ -98,7 +98,9 @@ public final class Constants {
public static final String HIB_SESSION = "org.hibernate.Session";
public static final String HIB_SESSION_FACTORY = "org.hibernate.SessionFactory";
public static final String HIB_STATELESS_SESSION = "org.hibernate.StatelessSession";
public static final String MUTINY_SESSION_FACTORY = "org.hibernate.reactive.mutiny.Mutiny.SessionFactory";
public static final String MUTINY_SESSION = "org.hibernate.reactive.mutiny.Mutiny.Session";
public static final String MUTINY_STATELESS_SESSION = "org.hibernate.reactive.mutiny.Mutiny.StatelessSession";
public static final String QUARKUS_SESSION_OPERATIONS = "io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations";
public static final String TUPLE = "jakarta.persistence.Tuple";