treat a multivalued param of @Find method as an 'in' condition

exactly as I'm proposing for Jakarta Data
This commit is contained in:
Gavin King 2024-03-18 11:30:34 +01:00
parent ba442f5d18
commit 008090b60e
5 changed files with 188 additions and 71 deletions

View File

@ -41,12 +41,18 @@ public interface BookAuthorRepository {
@Find
Book book(String isbn);
@Find
Book[] books(@By("isbn") String[] isbns);
@Find
Optional<Book> bookIfAny(String isbn);
@Find
Author author(String ssn);
@Find
List<Author> authors(@By("ssn") String[] ssns);
@Find
Book byTitleAndDate(String title, LocalDate publicationDate);

View File

@ -249,7 +249,7 @@ public class HibernateProcessor extends AbstractProcessor {
}
catch (Exception e) {
final StringWriter stack = new StringWriter();
e.printStackTrace( new PrintWriter( stack) );
e.printStackTrace( new PrintWriter(stack) );
final Throwable cause = e.getCause();
final String message =
cause != null && cause != e

View File

@ -45,6 +45,7 @@ import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
@ -62,6 +63,7 @@ import static java.beans.Introspector.decapitalize;
import static java.lang.Boolean.FALSE;
import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.util.ElementFilter.fieldsIn;
import static javax.lang.model.util.ElementFilter.methodsIn;
import static org.hibernate.internal.util.StringHelper.qualify;
@ -1002,11 +1004,13 @@ public class AnnotationMetaEntity extends AnnotationMeta {
final List<String> paramTypes = parameterTypes( method );
final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes );
final String methodKey = methodName + paramTypes;
final List<Boolean> multivalued = new ArrayList<>();
for ( VariableElement parameter : method.getParameters() ) {
if ( isFinderParameterMappingToAttribute( parameter ) ) {
validateFinderParameter( entity, parameter );
multivalued.add( validateFinderParameter( entity, parameter ) == FieldType.MULTIVALUED );
}
else {
multivalued.add( false );
final Types types = context.getTypeUtils();
final TypeMirror parameterType = parameter.asType();
final String type = parameterType.toString();
@ -1044,6 +1048,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
paramNames,
paramTypes,
parameterNullability(method, entity),
multivalued,
repository,
sessionType[0],
sessionType[1],
@ -1211,8 +1216,20 @@ public class AnnotationMetaEntity extends AnnotationMeta {
final List<String> paramTypes = parameterTypes( method );
final String[] sessionType = sessionTypeFromParameters( paramNames, paramTypes );
final String methodKey = methodName + paramTypes;
if ( !usingStatelessSession(sessionType[0]) // no byNaturalId() lookup API for SS
&& matchesNaturalKey( method, entity ) ) {
final List<Boolean> multivalued = new ArrayList<>();
final List<@Nullable FieldType> fieldTypes = new ArrayList<>();
for ( VariableElement parameter : method.getParameters() ) {
if ( isFinderParameterMappingToAttribute( parameter ) ) {
final FieldType fieldType = validateFinderParameter(entity, parameter);
fieldTypes.add( fieldType );
multivalued.add( fieldType == FieldType.MULTIVALUED );
}
else {
multivalued.add( false );
}
}
if ( !usingStatelessSession( sessionType[0] ) // no byNaturalId() lookup API for SS
&& matchesNaturalKey( entity, fieldTypes ) ) {
putMember( methodKey,
new NaturalIdFinderMethod(
this,
@ -1240,6 +1257,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
paramNames,
paramTypes,
parameterNullability(method, entity),
multivalued,
repository,
sessionType[0],
sessionType[1],
@ -1302,6 +1320,7 @@ public class AnnotationMetaEntity extends AnnotationMeta {
);
break;
case BASIC:
case MULTIVALUED:
putMember( methodKey,
new CriteriaFinderMethod(
this,
@ -1311,6 +1330,10 @@ public class AnnotationMetaEntity extends AnnotationMeta {
paramNames,
paramTypes,
parameterNullability(method, entity),
method.getParameters().stream()
.map(param -> isFinderParameterMappingToAttribute(param)
&& fieldType == FieldType.MULTIVALUED)
.collect(toList()),
repository,
sessionType[0],
sessionType[1],
@ -1326,55 +1349,41 @@ public class AnnotationMetaEntity extends AnnotationMeta {
}
private FieldType pickStrategy(FieldType fieldType, String sessionType, List<String> profiles) {
switch (fieldType) {
case ID:
// no byId() API for SS or M.S, only get()
return (usingStatelessSession(sessionType) || usingReactiveSession(sessionType)) && !profiles.isEmpty()
? FieldType.BASIC : FieldType.ID;
case NATURAL_ID:
// no byNaturalId() lookup API for SS
// no byNaturalId() in M.S, but we do have Identifier workaround
return usingStatelessSession(sessionType) || (usingReactiveSession(sessionType) && !profiles.isEmpty())
? FieldType.BASIC : FieldType.NATURAL_ID;
default:
return FieldType.BASIC;
if ( ( usingStatelessSession(sessionType) || usingReactiveSession(sessionType) )
&& !profiles.isEmpty() ) {
// no support for passing fetch profiles i.e. IdentifierLoadAccess
// in SS or M.S except via Query.enableFetchProfile()
return FieldType.BASIC;
}
else {
switch (fieldType) {
case ID:
// no byId() API for SS or M.S, only get()
return FieldType.ID;
case NATURAL_ID:
// no byNaturalId() lookup API for SS
// no byNaturalId() in M.S, but we do have Identifier workaround
return FieldType.NATURAL_ID;
default:
return FieldType.BASIC;
}
}
}
private boolean matchesNaturalKey(ExecutableElement method, TypeElement entity) {
boolean result = true;
final List<? extends VariableElement> parameters = method.getParameters();
int count = 0;
for ( VariableElement param : parameters ) {
if ( isFinderParameterMappingToAttribute( param ) ) {
count ++;
if ( validateFinderParameter( entity, param ) != FieldType.NATURAL_ID ) {
// no short-circuit here because we want to validate
// all of them and get the nice error report
result = false;
}
}
}
return result && countNaturalIdFields( entity ) == count;
private boolean matchesNaturalKey(TypeElement entity, List<@Nullable FieldType> fieldTypes) {
return fieldTypes.stream().allMatch(type -> type == FieldType.NATURAL_ID)
&& entity.getEnclosedElements().stream()
.filter(member -> hasAnnotation(member, NATURAL_ID))
.count() == fieldTypes.size();
}
enum FieldType {
ID, NATURAL_ID, BASIC
}
private int countNaturalIdFields(TypeElement entity) {
int count = 0;
for ( Element member : entity.getEnclosedElements() ) {
if ( containsAnnotation( member, Constants.NATURAL_ID ) ) {
count ++;
}
}
return count;
ID, NATURAL_ID, BASIC, MULTIVALUED
}
private @Nullable FieldType validateFinderParameter(TypeElement entityType, VariableElement param) {
final Element member = memberMatchingPath( entityType, parameterName( param ) );
if ( member != null) {
if ( member != null ) {
if ( containsAnnotation( member, MANY_TO_MANY, ONE_TO_MANY, ELEMENT_COLLECTION ) ) {
context.message( param,
"matching field is a collection",
@ -1382,16 +1391,19 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return null;
}
final String memberType = memberType( member ).toString();
final String paramType = param.asType().toString();
if ( !isLegalAssignment( paramType, memberType ) ) {
context.message( param,
"matching field has type '" + memberType
+ "' in entity class '" + entityType + "'",
Diagnostic.Kind.ERROR );
}
// final String memberType = attributeType.toString();
// final String paramType = parameterType.toString();
// if ( !isLegalAssignment( paramType, memberType ) ) {
// context.message( param,
// "matching field has type '" + memberType
// + "' in entity class '" + entityType + "'",
// Diagnostic.Kind.ERROR );
// }
if ( containsAnnotation( member, ID, EMBEDDED_ID ) ) {
if ( checkParameterType( entityType, param, memberType( member ) ) ) {
return FieldType.MULTIVALUED;
}
else if ( containsAnnotation( member, ID, EMBEDDED_ID ) ) {
return FieldType.ID;
}
else if ( containsAnnotation( member, NATURAL_ID ) ) {
@ -1401,22 +1413,82 @@ public class AnnotationMetaEntity extends AnnotationMeta {
return FieldType.BASIC;
}
}
else {
final AnnotationMirror idClass = getAnnotationMirror( entityType, ID_CLASS );
if ( idClass != null ) {
final Object value = getAnnotationValue( idClass, "value" );
if ( value instanceof TypeMirror ) {
if ( context.getTypeUtils().isSameType( param.asType(), (TypeMirror) value ) ) {
return FieldType.ID;
}
}
}
final AnnotationMirror idClass = getAnnotationMirror( entityType, ID_CLASS );
if ( idClass != null ) {
final Object value = getAnnotationValue( idClass, "value" );
if ( value instanceof TypeMirror ) {
if ( context.getTypeUtils().isSameType( param.asType(), (TypeMirror) value ) ) {
return FieldType.ID;
context.message( param,
"no matching field named '" + parameterName( param )
+ "' in entity class '" + entityType + "'",
Diagnostic.Kind.ERROR );
return null;
}
}
/**
* Check the type of a parameter of a {@code @Find} method against the field type
* in the entity class.
* @return true if the parameter is multivalued (i.e. it's an {@code in} condition)
*/
private boolean checkParameterType(TypeElement entityType, VariableElement param, TypeMirror attributeType) {
final Types types = context.getTypeUtils();
if ( entityType.getKind() == CLASS ) { // do no checks if the entity type is a type variable
TypeMirror parameterType = param.asType();
if ( types.isSameType( parameterType, attributeType ) ) {
return false;
}
else {
final TypeKind kind = parameterType.getKind();
switch (kind) {
case TYPEVAR:
final TypeVariable typeVariable = (TypeVariable) parameterType;
parameterType = typeVariable.getUpperBound();
// INTENTIONAL FALL-THROUGH
case DECLARED:
final TypeElement iterable = context.getTypeElementForFullyQualifiedName(ITERABLE);
if ( types.isAssignable( parameterType, types.getDeclaredType( iterable, attributeType) ) ) {
return true;
}
else {
parameterTypeError( entityType, param, attributeType );
return false;
}
case ARRAY:
if ( !types.isSameType( parameterType, types.getArrayType(attributeType) ) ) {
parameterTypeError( entityType, param, attributeType );
}
return true;
default:
if ( kind.isPrimitive() ) {
if ( !types.isSameType( types.unboxedType(parameterType), attributeType) ) {
parameterTypeError( entityType, param, attributeType );
}
return false;
}
else {
// probably impossible
return false;
}
}
}
}
else {
return false;
}
}
context.message( param,
"no matching field named '" + parameterName( param )
private void parameterTypeError(TypeElement entityType, VariableElement param, TypeMirror attributeType) {
context.message(param,
"matching field has type '" + attributeType
+ "' in entity class '" + entityType + "'",
Diagnostic.Kind.ERROR );
return null;
}
private boolean finderParameterNullable(TypeElement entity, VariableElement param) {

View File

@ -11,6 +11,8 @@ import org.hibernate.processor.util.Constants;
import java.util.List;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import static org.hibernate.processor.util.Constants.LIST;
import static org.hibernate.processor.util.TypeUtils.isPrimitive;
@ -22,6 +24,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
private final @Nullable String containerType;
private final List<Boolean> paramNullability;
private final List<Boolean> multivalued;
CriteriaFinderMethod(
AnnotationMetaEntity annotationMetaEntity,
@ -29,6 +32,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
@Nullable String containerType,
List<String> paramNames, List<String> paramTypes,
List<Boolean> paramNullability,
List<Boolean> multivalued,
boolean belongsToDao,
String sessionType,
String sessionName,
@ -40,6 +44,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
paramNames, paramTypes, orderBys, addNonnullAnnotation, dataRepository );
this.containerType = containerType;
this.paramNullability = paramNullability;
this.multivalued = multivalued;
}
@Override
@ -164,14 +169,14 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
declaration
.append(", ");
}
parameter(declaration, i, paramName, paramType );
condition(declaration, i, paramName, paramType );
}
}
declaration
.append("\n\t);");
}
private void parameter(StringBuilder declaration, int i, String paramName, String paramType) {
private void condition(StringBuilder declaration, int i, String paramName, String paramType) {
declaration
.append("\n\t\t\t");
final String parameterName = paramName.replace('.', '$');
@ -186,14 +191,42 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
.append(".isNull()")
.append("\n\t\t\t\t: ");
}
declaration
.append("_builder.equal(_entity");
path( declaration, paramName );
declaration
.append(", ")
//TODO: only safe if we are binding literals as parameters!!!
.append(parameterName)
.append(')');
if ( multivalued.get(i) ) {
declaration
.append("_entity");
path( declaration, paramName );
declaration
.append(".in(");
if ( paramType.endsWith("[]") ) {
declaration
//TODO: only safe if we are binding literals as parameters!!!
.append(parameterName);
}
else {
declaration
.append(annotationMetaEntity.staticImport(StreamSupport.class.getName(), "stream"))
.append('(')
//TODO: only safe if we are binding literals as parameters!!!
.append(parameterName)
.append(".spliterator(), false).collect(") // ugh, very ugly!
.append(annotationMetaEntity.staticImport(Collectors.class.getName(), "toList"))
.append("())");
}
declaration
.append(")");
}
else {
//TODO: change to use Expression.equalTo() in JPA 3.2
declaration
.append("_builder.equal(_entity");
path( declaration, paramName );
declaration
.append(", ")
//TODO: only safe if we are binding literals as parameters!!!
.append(parameterName)
.append(')');
}
}
private void path(StringBuilder declaration, String paramName) {

View File

@ -22,6 +22,12 @@ public interface Dao {
@Find
Book getBook(String isbn);
@Find
Book[] getBooks(String[] isbn);
@Find
List<Book> getBooks(List<String> isbn);
@Find
Optional<Book> getBookIfAny(String isbn);