improve error messages when association targets non-entity type

This commit is contained in:
Gavin King 2024-02-19 10:18:19 +01:00
parent 34374c0c70
commit e4632107d7
6 changed files with 57 additions and 30 deletions

View File

@ -44,7 +44,6 @@ import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.mapping.Any;
import org.hibernate.mapping.AttributeContainer;
import org.hibernate.mapping.BasicValue;

View File

@ -15,6 +15,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.function.Supplier;
import jakarta.persistence.Entity;
import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.FetchMode;
@ -97,7 +98,6 @@ import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.mapping.Any;
import org.hibernate.mapping.Backref;
import org.hibernate.mapping.BasicValue;
import org.hibernate.mapping.CheckConstraint;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Column;
@ -2556,8 +2556,8 @@ public abstract class CollectionBinder {
boolean isCollectionOfEntities) {
if ( !isCollectionOfEntities) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ "' targets the type '" + elementType.getName() + "' which is not an '@Entity' type" );
throw new AnnotationException( "Association '" + safeCollectionRole() + "'"
+ targetEntityMessage( elementType ) );
}
joinColumns.setManyToManyOwnerSideEntityName( collectionEntity.getEntityName() );
@ -2594,8 +2594,8 @@ public abstract class CollectionBinder {
if ( !isCollectionOfEntities) {
if ( property.isAnnotationPresent( ManyToMany.class ) || property.isAnnotationPresent( OneToMany.class ) ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ "' targets the type '" + elementType.getName() + "' which is not an '@Entity' type" );
throw new AnnotationException( "Association '" + safeCollectionRole() + "'"
+ targetEntityMessage( elementType ) );
}
else if (isManyToAny) {
if ( propertyHolder.getJoinTable( property ) == null ) {
@ -2607,13 +2607,20 @@ public abstract class CollectionBinder {
final JoinTable joinTableAnn = propertyHolder.getJoinTable( property );
if ( joinTableAnn != null && joinTableAnn.inverseJoinColumns().length > 0 ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ " has a '@JoinTable' with 'inverseJoinColumns' and targets the type '"
+ elementType.getName() + "' which is not an '@Entity' type" );
+ " has a '@JoinTable' with 'inverseJoinColumns' and"
+ targetEntityMessage( elementType ) );
}
}
}
}
static String targetEntityMessage(XClass elementType) {
final String problem = elementType.isAnnotationPresent( Entity.class )
? " which does not belong to the same persistence unit"
: " which is not an '@Entity' type";
return " targets the type '" + elementType.getName() + "'" + problem;
}
private Class<? extends EmbeddableInstantiator> resolveCustomInstantiator(
XProperty property,
XClass propertyClass,

View File

@ -246,8 +246,8 @@ public class MapBinder extends CollectionBinder {
String mapKeyPropertyName) {
final PersistentClass associatedClass = persistentClasses.get( elementType.getName() );
if ( associatedClass == null ) {
throw new AnnotationException( "Association '" + safeCollectionRole()
+ "' targets the type '" + elementType.getName() + "' which is not an '@Entity' type" );
throw new AnnotationException( "Association '" + safeCollectionRole() + "'"
+ targetEntityMessage( elementType ) );
}
final Property mapProperty = findPropertyByName( associatedClass, mapKeyPropertyName );
if ( mapProperty == null ) {

View File

@ -13,7 +13,6 @@ import org.hibernate.MappingException;
import org.hibernate.annotations.LazyGroup;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.annotations.OnDeleteAction;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XProperty;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData;
@ -37,7 +36,6 @@ import static org.hibernate.boot.model.internal.BinderHelper.findPropertyByName;
import static org.hibernate.boot.model.internal.BinderHelper.getPath;
import static org.hibernate.boot.model.internal.ToOneBinder.bindForeignKeyNameAndDefinition;
import static org.hibernate.boot.model.internal.ToOneBinder.defineFetchingStrategy;
import static org.hibernate.boot.model.internal.ToOneBinder.getReferenceEntityName;
import static org.hibernate.internal.util.StringHelper.qualify;
import static org.hibernate.type.ForeignKeyDirection.FROM_PARENT;
import static org.hibernate.type.ForeignKeyDirection.TO_PARENT;
@ -53,19 +51,20 @@ public class OneToOneSecondPass implements SecondPass {
private final PropertyHolder propertyHolder;
private final NotFoundAction notFoundAction;
private final PropertyData inferredData;
private final XClass targetEntity;
private final OnDeleteAction onDeleteAction;
private final boolean optional;
private final String cascadeStrategy;
private final AnnotatedJoinColumns joinColumns;
private final String referencedEntityName;
private final boolean annotatedEntity;
public OneToOneSecondPass(
String mappedBy,
String ownerEntity,
@SuppressWarnings("unused") String ownerProperty,
PropertyHolder propertyHolder,
PropertyData inferredData,
XClass targetEntity,
String referencedEntityName,
boolean annotatedEntity,
NotFoundAction notFoundAction,
OnDeleteAction onDeleteAction,
boolean optional,
@ -75,10 +74,11 @@ public class OneToOneSecondPass implements SecondPass {
this.ownerEntity = ownerEntity;
this.mappedBy = mappedBy;
this.propertyHolder = propertyHolder;
this.referencedEntityName = referencedEntityName;
this.buildingContext = buildingContext;
this.notFoundAction = notFoundAction;
this.inferredData = inferredData;
this.targetEntity = targetEntity;
this.annotatedEntity = annotatedEntity;
this.onDeleteAction = onDeleteAction;
this.optional = optional;
this.cascadeStrategy = cascadeStrategy;
@ -94,7 +94,6 @@ public class OneToOneSecondPass implements SecondPass {
);
final String propertyName = inferredData.getPropertyName();
value.setPropertyName( propertyName );
final String referencedEntityName = getReferenceEntityName( inferredData, targetEntity, buildingContext );
value.setReferencedEntityName( referencedEntityName );
XProperty property = inferredData.getProperty();
defineFetchingStrategy( value, property, inferredData, propertyHolder );
@ -136,10 +135,14 @@ public class OneToOneSecondPass implements SecondPass {
private void bindUnowned(Map<String, PersistentClass> persistentClasses, OneToOne oneToOne, Property property) {
oneToOne.setMappedByProperty( mappedBy );
final PersistentClass targetEntity = persistentClasses.get( oneToOne.getReferencedEntityName() );
final String targetEntityName = oneToOne.getReferencedEntityName();
final PersistentClass targetEntity = persistentClasses.get( targetEntityName );
if ( targetEntity == null ) {
final String problem = annotatedEntity
? " which does not belong to the same persistence unit"
: " which is not an '@Entity' type";
throw new MappingException( "Association '" + getPath( propertyHolder, inferredData )
+ "' targets unknown entity type '" + oneToOne.getReferencedEntityName() + "'" );
+ "' targets the type '" + targetEntityName + "'" + problem );
}
final Property targetProperty = targetProperty( oneToOne, targetEntity );
if ( targetProperty.getValue() instanceof OneToOne ) {
@ -151,7 +154,7 @@ public class OneToOneSecondPass implements SecondPass {
else {
throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData )
+ "' is 'mappedBy' a property named '" + mappedBy
+ "' of the target entity type '" + oneToOne.getReferencedEntityName()
+ "' of the target entity type '" + targetEntityName
+ "' which is not a '@OneToOne' or '@ManyToOne' association" );
}
checkMappedByType(
@ -235,13 +238,18 @@ public class OneToOneSecondPass implements SecondPass {
+ "' which does not exist in the target entity type '" + oneToOne.getReferencedEntityName() + "'" );
}
private void bindOwned(Map<String, PersistentClass> persistentClasses, OneToOne oneToOne, String propertyName, Property property) {
private void bindOwned(
Map<String, PersistentClass> persistentClasses,
OneToOne oneToOne,
String propertyName,
Property property) {
final ToOneFkSecondPass secondPass = new ToOneFkSecondPass(
oneToOne,
joinColumns,
true,
annotatedEntity,
propertyHolder.getPersistentClass(),
qualify( propertyHolder.getPath(), propertyName),
qualify( propertyHolder.getPath(), propertyName ),
buildingContext
);
secondPass.doSecondPass(persistentClasses);

View File

@ -9,6 +9,7 @@ package org.hibernate.boot.model.internal;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Entity;
import org.hibernate.AnnotationException;
import org.hibernate.AssertionFailure;
import org.hibernate.FetchMode;
@ -36,7 +37,6 @@ import org.hibernate.mapping.SimpleValue;
import org.hibernate.mapping.ToOne;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.Id;
@ -49,8 +49,6 @@ import jakarta.persistence.OneToOne;
import jakarta.persistence.PrimaryKeyJoinColumn;
import jakarta.persistence.PrimaryKeyJoinColumns;
import static jakarta.persistence.ConstraintMode.NO_CONSTRAINT;
import static jakarta.persistence.ConstraintMode.PROVIDER_DEFAULT;
import static jakarta.persistence.FetchType.EAGER;
import static jakarta.persistence.FetchType.LAZY;
import static org.hibernate.boot.model.internal.BinderHelper.getCascadeStrategy;
@ -229,6 +227,7 @@ public class ToOneBinder {
value,
joinColumns,
unique,
isTargetAnnotatedEntity( targetEntity, property, context ),
propertyHolder.getPersistentClass(),
fullPath,
context
@ -254,6 +253,11 @@ public class ToOneBinder {
);
}
static boolean isTargetAnnotatedEntity(XClass targetEntity, XProperty property, MetadataBuildingContext context) {
final XClass target = isDefault( targetEntity, context ) ? property.getType() : targetEntity;
return target.isAnnotationPresent( Entity.class );
}
private static boolean handleSpecjSyntax(
AnnotatedJoinColumns columns,
PropertyData inferredData,
@ -490,6 +494,7 @@ public class ToOneBinder {
notFoundAction,
onDelete == null ? null : onDelete.action(),
getTargetEntity( inferredData, context ),
property,
propertyHolder,
inferredData,
nullIfEmpty( oneToOne.mappedBy() ),
@ -510,6 +515,7 @@ public class ToOneBinder {
NotFoundAction notFoundAction,
OnDeleteAction cascadeOnDelete,
XClass targetEntity,
XProperty annotatedProperty,
PropertyHolder propertyHolder,
PropertyData inferredData,
String mappedBy,
@ -528,10 +534,10 @@ public class ToOneBinder {
final OneToOneSecondPass secondPass = new OneToOneSecondPass(
mappedBy,
propertyHolder.getEntityName(),
propertyName,
propertyHolder,
inferredData,
targetEntity,
getReferenceEntityName( inferredData, targetEntity, context ),
isTargetAnnotatedEntity( targetEntity, annotatedProperty, context ),
notFoundAction,
cascadeOnDelete,
optional,

View File

@ -34,11 +34,13 @@ public class ToOneFkSecondPass extends FkSecondPass {
private final boolean unique;
private final String path;
private final String entityClassName;
private final boolean annotatedEntity;
public ToOneFkSecondPass(
ToOne value,
AnnotatedJoinColumns columns,
boolean unique,
boolean annotatedEntity,
PersistentClass persistentClass,
String path,
MetadataBuildingContext buildingContext) {
@ -48,6 +50,7 @@ public class ToOneFkSecondPass extends FkSecondPass {
this.unique = unique;
this.entityClassName = persistentClass.getClassName();
this.path = entityClassName != null ? path.substring( entityClassName.length() + 1 ) : path;
this.annotatedEntity = annotatedEntity;
}
@Override
@ -95,13 +98,17 @@ public class ToOneFkSecondPass extends FkSecondPass {
@Override
public void doSecondPass(java.util.Map<String, PersistentClass> persistentClasses) throws MappingException {
if ( value instanceof ManyToOne ) {
//TODO: move this validation logic to a separate ManyToOnSecondPass
//TODO: move this validation logic to a separate ManyToOneSecondPass
// for consistency with how this is handled for OneToOnes
final ManyToOne manyToOne = (ManyToOne) value;
final PersistentClass targetEntity = persistentClasses.get( manyToOne.getReferencedEntityName() );
final String targetEntityName = manyToOne.getReferencedEntityName();
final PersistentClass targetEntity = persistentClasses.get( targetEntityName );
if ( targetEntity == null ) {
final String problem = annotatedEntity
? " which does not belong to the same persistence unit"
: " which is not an '@Entity' type";
throw new AnnotationException( "Association '" + qualify( entityClassName, path )
+ "' targets an unknown entity named '" + manyToOne.getReferencedEntityName() + "'" );
+ "' targets the type '" + targetEntityName + "'" + problem );
}
manyToOne.setPropertyName( path );
createSyntheticPropertyReference(