treat a multivalued param of @Find method as an 'in' condition
exactly as I'm proposing for Jakarta Data
This commit is contained in:
parent
ba442f5d18
commit
008090b60e
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
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( method, entity ) ) {
|
||||
&& 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,50 +1349,36 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
}
|
||||
|
||||
private FieldType pickStrategy(FieldType fieldType, String sessionType, List<String> profiles) {
|
||||
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 (usingStatelessSession(sessionType) || usingReactiveSession(sessionType)) && !profiles.isEmpty()
|
||||
? FieldType.BASIC : FieldType.ID;
|
||||
return 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;
|
||||
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) {
|
||||
|
@ -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,7 +1413,7 @@ 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" );
|
||||
|
@ -1418,6 +1430,66 @@ public class AnnotationMetaEntity extends AnnotationMeta {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void parameterTypeError(TypeElement entityType, VariableElement param, TypeMirror attributeType) {
|
||||
context.message(param,
|
||||
"matching field has type '" + attributeType
|
||||
+ "' in entity class '" + entityType + "'",
|
||||
Diagnostic.Kind.ERROR );
|
||||
}
|
||||
|
||||
private boolean finderParameterNullable(TypeElement entity, VariableElement param) {
|
||||
final Element member = memberMatchingPath( entity, parameterName( param ) );
|
||||
|
|
|
@ -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,6 +191,33 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
|
|||
.append(".isNull()")
|
||||
.append("\n\t\t\t\t: ");
|
||||
}
|
||||
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 );
|
||||
|
@ -195,6 +227,7 @@ public class CriteriaFinderMethod extends AbstractFinderMethod {
|
|||
.append(parameterName)
|
||||
.append(')');
|
||||
}
|
||||
}
|
||||
|
||||
private void path(StringBuilder declaration, String paramName) {
|
||||
final StringTokenizer tokens = new StringTokenizer(paramName, ".");
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue