HHH-18826 mappedBy validation in Processor

tolerate a mappedBy which refers to a parent id field rather than an association

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-11-08 00:09:05 +01:00
parent 356ea205ff
commit 3457b2d283
1 changed files with 60 additions and 50 deletions

View File

@ -75,6 +75,7 @@ import static org.hibernate.grammars.hql.HqlLexer.HAVING;
import static org.hibernate.grammars.hql.HqlLexer.ORDER; import static org.hibernate.grammars.hql.HqlLexer.ORDER;
import static org.hibernate.grammars.hql.HqlLexer.WHERE; import static org.hibernate.grammars.hql.HqlLexer.WHERE;
import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.internal.util.StringHelper.unqualify;
import static org.hibernate.processor.annotation.AbstractQueryMethod.isSessionParameter; import static org.hibernate.processor.annotation.AbstractQueryMethod.isSessionParameter;
import static org.hibernate.processor.annotation.AbstractQueryMethod.isSpecialParam; import static org.hibernate.processor.annotation.AbstractQueryMethod.isSpecialParam;
import static org.hibernate.processor.annotation.QueryMethod.isOrderParam; import static org.hibernate.processor.annotation.QueryMethod.isOrderParam;
@ -966,8 +967,9 @@ public class AnnotationMetaEntity extends AnnotationMeta {
final TypeElement assocTypeElement = (TypeElement) assocDeclaredType.asElement(); final TypeElement assocTypeElement = (TypeElement) assocDeclaredType.asElement();
if ( hasAnnotation(assocTypeElement, ENTITY) ) { if ( hasAnnotation(assocTypeElement, ENTITY) ) {
final AnnotationValue mappedBy = getAnnotationValue(annotation, "mappedBy"); final AnnotationValue mappedBy = getAnnotationValue(annotation, "mappedBy");
final String propertyName = mappedBy == null ? null : mappedBy.getValue().toString(); if ( mappedBy != null ) {
validateBidirectionalMapping(memberOfClass, annotation, propertyName, assocTypeElement); validateBidirectionalMapping(memberOfClass, annotation, mappedBy, assocTypeElement);
}
} }
else { else {
message(memberOfClass, "type '" + assocTypeElement.getSimpleName() message(memberOfClass, "type '" + assocTypeElement.getSimpleName()
@ -983,30 +985,27 @@ public class AnnotationMetaEntity extends AnnotationMeta {
} }
private void validateBidirectionalMapping( private void validateBidirectionalMapping(
Element memberOfClass, AnnotationMirror annotation, @Nullable String mappedBy, TypeElement assocTypeElement) { Element memberOfClass, AnnotationMirror annotation, AnnotationValue annotationVal, TypeElement assocTypeElement) {
if ( mappedBy != null && !mappedBy.isEmpty() ) { final String mappedBy = annotationVal.getValue().toString();
if ( mappedBy.equals("<error>") ) { if ( mappedBy != null && !mappedBy.isEmpty()
return; // this happens for a typesafe ref, e.g. Page_BOOK
// throw new ProcessLaterException(); // TODO: we should queue it to validate it later somehow
} && !mappedBy.equals( "<error>" ) ) {
if ( mappedBy.indexOf('.')>0 ) { if ( mappedBy.indexOf( '.' ) > 0 ) {
//we don't know how to handle paths yet //we don't know how to handle paths yet
return; return;
} }
final AnnotationValue annotationVal = for ( Element member : context.getAllMembers( assocTypeElement ) ) {
castNonNull(getAnnotationValue(annotation, "mappedBy")); if ( propertyName( this, member ).contentEquals( mappedBy )
for ( Element member : context.getAllMembers(assocTypeElement) ) { && compatibleAccess( assocTypeElement, member ) ) {
if ( propertyName(this, member).contentEquals(mappedBy) validateBackRef( memberOfClass, annotation, assocTypeElement, member, annotationVal );
&& compatibleAccess(assocTypeElement, member) ) {
validateBackRef(memberOfClass, annotation, assocTypeElement, member, annotationVal);
return; return;
} }
} }
// not found // not found
message(memberOfClass, annotation, message( memberOfClass, annotation, annotationVal,
annotationVal,
"no matching member in '" + assocTypeElement.getSimpleName() + "'", "no matching member in '" + assocTypeElement.getSimpleName() + "'",
Diagnostic.Kind.ERROR); Diagnostic.Kind.ERROR );
} }
} }
@ -1024,53 +1023,64 @@ public class AnnotationMetaEntity extends AnnotationMeta {
Element memberOfClass, Element memberOfClass,
AnnotationMirror annotation, AnnotationMirror annotation,
TypeElement assocTypeElement, TypeElement assocTypeElement,
Element member, Element referencedMember,
AnnotationValue annotationVal) { AnnotationValue annotationVal) {
final TypeMirror backType; final TypeMirror backType;
final String expectedMappingAnnotation;
switch ( annotation.getAnnotationType().asElement().toString() ) { switch ( annotation.getAnnotationType().asElement().toString() ) {
case ONE_TO_ONE: case ONE_TO_ONE:
backType = attributeType(member); backType = attributeType(referencedMember);
if ( !hasAnnotation(member, ONE_TO_ONE) ) { expectedMappingAnnotation = ONE_TO_ONE;
message(memberOfClass, annotation, annotationVal,
"member '" + member.getSimpleName()
+ "' of '" + assocTypeElement.getSimpleName()
+ "' is not annotated '@OneToOne'",
Diagnostic.Kind.WARNING);
}
break; break;
case ONE_TO_MANY: case ONE_TO_MANY:
backType = attributeType(member); backType = attributeType(referencedMember);
if ( !hasAnnotation(member, MANY_TO_ONE) ) { expectedMappingAnnotation = MANY_TO_ONE;
message(memberOfClass, annotation, annotationVal,
"member '" + member.getSimpleName()
+ "' of '" + assocTypeElement.getSimpleName()
+ "' is not annotated '@ManyToOne'",
Diagnostic.Kind.WARNING);
}
break; break;
case MANY_TO_MANY: case MANY_TO_MANY:
backType = elementType( attributeType(member) ); backType = elementType( attributeType(referencedMember) );
if ( !hasAnnotation(member, MANY_TO_MANY) ) { expectedMappingAnnotation = MANY_TO_MANY;
message(memberOfClass, annotation, annotationVal,
"member '" + member.getSimpleName()
+ "' of '" + assocTypeElement.getSimpleName()
+ "' is not annotated '@ManyToMany'",
Diagnostic.Kind.WARNING);
}
break; break;
default: default:
throw new AssertionFailure("should not have a mappedBy"); throw new AssertionFailure("should not have a mappedBy");
} }
if ( backType!=null if ( backType != null ) {
&& !context.getTypeUtils().isSameType(backType, element.asType()) ) { final Element idMember = getIdMember();
message(memberOfClass, annotation, annotationVal, final Types typeUtils = context.getTypeUtils();
"member '" + member.getSimpleName() if ( idMember != null && typeUtils.isSameType( backType, idMember.asType() ) ) {
+ "' of '" + assocTypeElement.getSimpleName() // mappedBy references a regular field of the same type as the entity id
+ "' is not of type '" + element.getSimpleName() + "'", //TODO: any other validation to do here??
Diagnostic.Kind.WARNING); }
else if ( typeUtils.isSameType( backType, element.asType() ) ) {
// mappedBy references a field of the same type as the entity
// it needs to be mapped as the appropriate sort of association
if ( !hasAnnotation( referencedMember, expectedMappingAnnotation ) ) {
message(memberOfClass, annotation, annotationVal,
"member '" + referencedMember.getSimpleName()
+ "' of '" + assocTypeElement.getSimpleName()
+ "' is not annotated '@" + unqualify(expectedMappingAnnotation) + "'",
Diagnostic.Kind.WARNING);
}
}
else {
// mappedBy references a field which seems to be of the wrong type
message( memberOfClass, annotation, annotationVal,
"member '" + referencedMember.getSimpleName()
+ "' of '" + assocTypeElement.getSimpleName()
+ "' is not of type '" + element.getSimpleName() + "'",
Diagnostic.Kind.WARNING );
}
} }
} }
private @Nullable Element getIdMember() {
for ( Element e : element.getEnclosedElements() ) {
if ( hasAnnotation( e, ID, EMBEDDED_ID ) ) {
return e;
}
}
return null;
}
private boolean isPersistent(Element memberOfClass, AccessType membersKind) { private boolean isPersistent(Element memberOfClass, AccessType membersKind) {
return ( entityAccessTypeInfo.getAccessType() == membersKind return ( entityAccessTypeInfo.getAccessType() == membersKind
|| determineAnnotationSpecifiedAccessType( memberOfClass ) != null ) || determineAnnotationSpecifiedAccessType( memberOfClass ) != null )