hibernate/hibernate-orm · error · AnnotationException

Association '%s' of entity '%s' is 'mappedBy' a different en

Error message

Association '%s' of entity '%s' is 'mappedBy' a different entity and may not explicitly specify the '@JoinColumn'

What it means

This association is the INVERSE side of a bidirectional relationship ('mappedBy' points at the owning side), yet it also declares an explicit '@JoinColumn'. In a bidirectional association only the owning side defines the join column; the inverse side mirrors it, so Hibernate forbids the duplicate declaration.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedJoinColumn.java:114

//		formulaColumn.setContext( buildingContext );
//		formulaColumn.setPropertyHolder( propertyHolder );
//		formulaColumn.setPropertyName( getRelativePath( propertyHolder, propertyName ) );
//		formulaColumn.setJoins( joins );
		formulaColumn.setParent( parent );
		formulaColumn.bind();
		return formulaColumn;
	}

	static AnnotatedJoinColumn buildJoinColumn(
			JoinColumn joinColumn,
			String mappedBy,
			AnnotatedJoinColumns parent,
			PropertyHolder propertyHolder,
			PropertyData inferredData,
			String defaultColumnSuffix) {
		if ( joinColumn != null ) {
			if ( mappedBy != null ) {
				throw new AnnotationException(
						String.format(
								Locale.ROOT,
								"Association '%s' of entity '%s' is 'mappedBy' a different entity and may not explicitly specify the '@JoinColumn'",
								inferredData.getPropertyName(),
								propertyHolder.getEntityName() )
				);
			}
			return explicitJoinColumn( joinColumn, parent, inferredData, defaultColumnSuffix );
		}
		else {
			return implicitJoinColumn( parent, inferredData, defaultColumnSuffix );
		}
	}

	private static AnnotatedJoinColumn explicitJoinColumn(
			JoinColumn joinColumn,
			AnnotatedJoinColumns parent,
			PropertyData inferredData,

View on GitHub (pinned to fad1729dce)

Solutions

  1. Delete the @JoinColumn from the mappedBy (inverse) side named in the message.
  2. Put the @JoinColumn with the desired FK name on the OWNING side (the @ManyToOne/@OneToOne that does NOT use mappedBy).
  3. Verify the mappedBy string actually names the owning-side property, so the mapping is a valid bidirectional pair.

Example fix

// before
@Entity
class Order {
    @OneToMany(mappedBy = "order")
    @JoinColumn(name = "order_id") // illegal on inverse side
    List<OrderItem> items;
}

// after
@Entity
class Order {
    @OneToMany(mappedBy = "order")
    List<OrderItem> items;
}

@Entity
class OrderItem {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id") // owning side owns the column
    Order order;
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard: inverse (mappedBy) sides must not declare join columns
for (Field f : cls.getDeclaredFields()) {
    OneToMany otm = f.getAnnotation(OneToMany.class);
    ManyToMany mtm = f.getAnnotation(ManyToMany.class);
    boolean inverse = (otm != null && !otm.mappedBy().isEmpty())
                   || (mtm != null && !mtm.mappedBy().isEmpty());
    if (inverse && (f.isAnnotationPresent(JoinColumn.class) || f.isAnnotationPresent(JoinColumns.class))) {
        throw new IllegalStateException(cls.getName() + "." + f.getName()
            + " is mappedBy and must not declare @JoinColumn");
    }
}

Try / catch

try {
    factory = cfg.buildSessionFactory();
} catch (AnnotationException e) {
    if (e.getMessage().contains("may not explicitly specify the '@JoinColumn'")) {
        // strip the @JoinColumn from the field named in the message
        fixInverseSideJoinColumn(e.getMessage());
    }
    throw e;
}

Prevention

When it happens

Trigger: '@OneToMany(mappedBy = "order") @JoinColumn(name = "oid")' on the collection side while the @ManyToOne side owns the FK; a @ManyToMany(mappedBy=...) with a @JoinTable/@JoinColumn; mappedBy set to a non-null value and joinColumn != null when buildJoinColumn is invoked.

Common situations: Adding @JoinColumn to the 'child list' side believing it fixes a FK name (it does not — name it on the @ManyToOne side); converting unidirectional to bidirectional and forgetting to delete the old @JoinColumn; copy-paste between owning and inverse sides.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/f0dce07ee2ceab16. Report an issue: GitHub.