support for queries defined in intermediate classes

for our work on the Jakarta Data TCK

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-04-02 00:27:15 +02:00
parent e21d139a84
commit f51d8dbe0d
7 changed files with 259 additions and 78 deletions

View File

@ -0,0 +1,21 @@
package org.hibernate.processor.test.data.namedquery;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import java.util.Set;
@Entity
public class Author {
@Id
String ssn;
String name;
// @Embedded
// Address address;
@ManyToMany
Set<Book> books;
}

View File

@ -0,0 +1,43 @@
package org.hibernate.processor.test.data.namedquery;
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import org.hibernate.annotations.NaturalId;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
@Entity
@Table(name = "books")
public class Book {
@Id
String isbn;
@NaturalId
@Basic(optional = false)
String title;
@Basic(optional = false)
String text;
@NaturalId
LocalDate publicationDate;
@ManyToMany(mappedBy = "books")
Set<Author> authors;
BigDecimal price;
int pages;
public Book(String isbn, String title, String text) {
this.isbn = isbn;
this.title = title;
this.text = text;
}
Book() {}
}

View File

@ -0,0 +1,13 @@
package org.hibernate.processor.test.data.namedquery;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import java.util.List;
@Repository(dataStore = "myds")
public interface BookAuthorRepository$ extends BookAuthorRepository {
@Override
@Query("from Book where title like :title")
List<Book> findByTitleLike(String title);
}

View File

@ -0,0 +1,30 @@
package org.hibernate.processor.test.data.namedquery;
import jakarta.data.Limit;
import jakarta.data.Order;
import jakarta.data.Sort;
import jakarta.data.page.CursoredPage;
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.Param;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import jakarta.data.repository.Save;
import jakarta.data.repository.Update;
import org.hibernate.StatelessSession;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@Repository(dataStore = "myds")
public interface BookAuthorRepository {
List<Book> findByTitleLike(String title);
}

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.namedquery;
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.assertNoMetamodelClassGeneratedFor;
import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString;
/**
* @author Gavin King
*/
public class NamedQueryTest extends CompilationTest {
@Test
@WithClasses({ Author.class, Book.class, BookAuthorRepository.class, BookAuthorRepository$.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( BookAuthorRepository.class ) );
assertMetamodelClassGeneratedFor( Author.class, true );
assertMetamodelClassGeneratedFor( Book.class, true );
assertMetamodelClassGeneratedFor( Author.class );
assertMetamodelClassGeneratedFor( Book.class );
assertMetamodelClassGeneratedFor( BookAuthorRepository.class );
assertNoMetamodelClassGeneratedFor( BookAuthorRepository$.class );
}
}

View File

@ -314,7 +314,10 @@ public class HibernateProcessor extends AbstractProcessor {
context.logMessage( Diagnostic.Kind.OTHER, "Processing repository class '" + element + "'" );
final AnnotationMetaEntity metaEntity =
AnnotationMetaEntity.create( typeElement, context );
context.addMetaAuxiliary( metaEntity.getQualifiedName(), metaEntity );
if ( metaEntity.isInitialized() ) {
context.addMetaAuxiliary( metaEntity.getQualifiedName(), metaEntity );
}
// otherwise discard it (assume it has query by magical method name stuff)
}
}
else {

View File

@ -10,6 +10,7 @@ import org.antlr.v4.runtime.Token;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.hibernate.AssertionFailure;
import org.hibernate.grammars.hql.HqlLexer;
import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.processor.Context;
import org.hibernate.processor.ImportContextImpl;
import org.hibernate.processor.ProcessLaterException;
@ -21,7 +22,6 @@ import org.hibernate.processor.util.AccessTypeInformation;
import org.hibernate.processor.util.Constants;
import org.hibernate.processor.validation.ProcessorSessionFactory;
import org.hibernate.processor.validation.Validation;
import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.query.criteria.JpaEntityJoin;
import org.hibernate.query.criteria.JpaRoot;
import org.hibernate.query.criteria.JpaSelection;
@ -212,9 +212,29 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return jakartaDataStaticModel;
}
public boolean isInitialized() {
return initialized;
}
@Override
public final String getSimpleName() {
return element.getSimpleName().toString();
return removeDollar( element.getSimpleName().toString() );
}
private String getConstructorName() {
return getSimpleName() + '_';
}
/**
* If this is an "intermediate" class providing {@code @Query}
* annotations for the query by magical method name crap, then
* by convention it will be named with a trailing $ sign. Strip
* that off, so we get the standard constructor.
*/
private static String removeDollar(String simpleName) {
return simpleName.endsWith("$")
? simpleName.substring(0, simpleName.length()-1)
: simpleName;
}
@Override
@ -368,7 +388,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
else if ( containsAnnotation( method, JD_INSERT, JD_UPDATE, JD_SAVE ) ) {
lifecycleMethods.add( method );
}
else if ( hasAnnotation( method, JD_DELETE) ) {
else if ( hasAnnotation( method, JD_DELETE ) ) {
if ( isDeleteLifecycle(method) ) {
lifecycleMethods.add( method );
}
@ -376,6 +396,16 @@ public class AnnotationMetaEntity extends AnnotationMeta {
queryMethods.add( method );
}
}
else if ( !isSessionGetter(method)
&& !method.getModifiers().contains(Modifier.DEFAULT) ) {
final String companionClassName = element.getQualifiedName().toString() + '$';
if ( context.getElementUtils().getTypeElement(companionClassName) == null ) {
message( method, "repository method cannot be implemented",
Diagnostic.Kind.ERROR );
}
// NOTE EARLY EXIT with initialized = false
return;
}
}
primaryEntity = primaryEntity( lifecycleMethods );
@ -483,10 +513,9 @@ public class AnnotationMetaEntity extends AnnotationMeta {
private void addDefaultConstructor() {
final String sessionVariableName = getSessionVariableName(sessionType);
final String typeName = element.getSimpleName().toString() + '_';
putMember("_", new DefaultConstructor(
this,
typeName,
getConstructorName(),
sessionVariableName,
sessionType,
sessionVariableName,
@ -633,13 +662,12 @@ public class AnnotationMetaEntity extends AnnotationMeta {
final String sessionType = method == null ? this.sessionType : method.getReturnType().toString();
final String sessionVariableName = getSessionVariableName( sessionType );
final String name = method == null ? sessionVariableName : method.getSimpleName().toString();
final String typeName = element.getSimpleName().toString() + '_';
if ( method == null || !method.isDefault() ) {
putMember( name,
new RepositoryConstructor(
this,
typeName,
getConstructorName(),
name,
sessionType,
sessionVariableName,
@ -664,18 +692,15 @@ public class AnnotationMetaEntity extends AnnotationMeta {
* and in HR, we define the static session getter.
*/
private String setupQuarkusDaoConstructor() {
final String typeName = element.getSimpleName().toString() + '_';
final String sessionVariableName = getSessionVariableName( sessionType );
if ( context.usesQuarkusOrm() ) {
String name = "getEntityManager";
putMember( name,
new RepositoryConstructor(
this,
typeName,
getConstructorName(),
name,
sessionType,
sessionVariableName,
getSessionVariableName( sessionType ),
dataStore(),
context.addInjectAnnotation(),
context.addNonnullAnnotation(),
@ -1047,7 +1072,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
.asMemberOf((DeclaredType) element.asType(), method);
final TypeMirror returnType = methodType.getReturnType();
final TypeKind kind = returnType.getKind();
if ( kind == TypeKind.VOID || kind == TypeKind.ARRAY || kind.isPrimitive() ) {
if ( kind == TypeKind.VOID || kind == TypeKind.ARRAY || kind.isPrimitive() ) {
addQueryMethod( method, returnType, null );
}
else if ( kind == TypeKind.DECLARED ) {
@ -2093,81 +2118,91 @@ public class AnnotationMetaEntity extends AnnotationMeta {
@Nullable TypeElement containerType,
AnnotationMirror mirror,
boolean isNative) {
// The following is quite fragile!
final String containerTypeName;
if ( containerType == null ) {
if ( returnType != null && returnType.getKind() == TypeKind.ARRAY ) {
final ArrayType arrayType = (ArrayType) returnType;
final TypeMirror componentType = arrayType.getComponentType();
final TypeElement object = context.getElementUtils().getTypeElement(JAVA_OBJECT);
if ( !context.getTypeUtils().isSameType( object.asType(), componentType ) ) {
returnType = componentType;
containerTypeName = "[]";
}
else {
// assume it's returning a single tuple as Object[]
containerTypeName = null;
}
}
else {
containerTypeName = null;
}
}
else {
containerTypeName = containerType.getQualifiedName().toString();
}
final AnnotationValue value = getAnnotationValue( mirror, "value" );
if ( value != null ) {
final Object query = value.getValue();
if ( query instanceof String ) {
final String queryString = (String) query;
final Object queryString = value.getValue();
if ( queryString instanceof String ) {
addQueryMethod(method, returnType, containerTypeName, mirror, isNative, value, (String) queryString);
}
}
}
// The following is quite fragile!
final String containerTypeName;
if ( containerType == null ) {
if ( returnType != null && returnType.getKind() == TypeKind.ARRAY ) {
final ArrayType arrayType = (ArrayType) returnType;
final TypeMirror componentType = arrayType.getComponentType();
final TypeElement object = context.getElementUtils().getTypeElement(JAVA_OBJECT);
if ( !context.getTypeUtils().isSameType( object.asType(), componentType ) ) {
returnType = componentType;
containerTypeName = "[]";
}
else {
// assume it's returning a single tuple as Object[]
containerTypeName = null;
}
}
else {
containerTypeName = null;
}
}
else {
containerTypeName = containerType.getQualifiedName().toString();
}
private void addQueryMethod(
ExecutableElement method,
@Nullable TypeMirror returnType,
@Nullable String containerTypeName,
AnnotationMirror mirror,
boolean isNative,
AnnotationValue value,
String queryString) {
final List<String> paramNames = parameterNames( method );
final List<String> paramTypes = parameterTypes( method );
final List<String> paramNames = parameterNames(method);
final List<String> paramTypes = parameterTypes(method);
// now check that the query has a parameter for every method parameter
checkParameters( method, returnType, paramNames, paramTypes, mirror, value, queryString );
// now check that the query has a parameter for every method parameter
checkParameters(method, returnType, paramNames, paramTypes, mirror, value, queryString);
final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes );
final DeclaredType resultType = resultType( method, returnType, mirror, value );
final List<OrderBy> orderBys = resultType == null
final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes );
final DeclaredType resultType = resultType(method, returnType, mirror, value);
final List<OrderBy> orderBys =
resultType == null
? emptyList()
: orderByList( method, (TypeElement) resultType.asElement() );
final String processedQuery;
if ( isNative ) {
processedQuery = queryString;
validateSql( method, mirror, processedQuery, paramNames, value );
}
else {
processedQuery = addFromClauseIfNecessary( queryString, implicitEntityName(resultType) );
validateHql( method, returnType, mirror, value, processedQuery, paramNames, paramTypes );
}
final QueryMethod attribute =
new QueryMethod(
this, method,
method.getSimpleName().toString(),
processedQuery,
returnType == null ? null : returnType.toString(),
returnType == null ? null : returnTypeClass( returnType ),
containerTypeName,
paramNames,
paramTypes,
isInsertUpdateDelete( queryString ),
isNative,
repository,
sessionType[0],
sessionType[1],
orderBys,
context.addNonnullAnnotation(),
jakartaDataRepository
);
putMember( attribute.getPropertyName() + paramTypes, attribute );
}
final String processedQuery;
if (isNative) {
processedQuery = queryString;
validateSql(method, mirror, processedQuery, paramNames, value);
}
else {
processedQuery = addFromClauseIfNecessary( queryString, implicitEntityName(resultType) );
validateHql(method, returnType, mirror, value, processedQuery, paramNames, paramTypes);
}
final QueryMethod attribute =
new QueryMethod(
this, method,
method.getSimpleName().toString(),
processedQuery,
returnType == null ? null : returnType.toString(),
returnType == null ? null : returnTypeClass(returnType),
containerTypeName,
paramNames,
paramTypes,
isInsertUpdateDelete(queryString),
isNative,
repository,
sessionType[0],
sessionType[1],
orderBys,
context.addNonnullAnnotation(),
jakartaDataRepository
);
putMember( attribute.getPropertyName() + paramTypes, attribute );
}
private static String returnTypeClass(TypeMirror returnType) {