hibernate/hibernate-orm · error · AnnotationException

Association '{}' in entity '{}' is annotated '@MapsId' but r

Error message

Association '{}' in entity '{}' is annotated '@MapsId' but refers to a property '{}' which has an explicit column mapping

What it means

'@MapsId' tells Hibernate to derive the association's join column from the mapped identifier property's column. That derivation only works when the id property uses the default/implicit column name; if the named id property has an explicit @Column mapping, the derived join-column name would silently disagree with it, so Hibernate (as a non-spec convenience it controls strictly) throws instead.

Source

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

				mappingColumn != null && mappingColumn.isUnique(),
				false
		);
	}

	private String defaultColumnName(int columnIndex, PersistentClass referencedEntity, String logicalReferencedColumn) {
		final var parent = getParent();
		if ( parent.hasMapsId() ) {
			// infer the join column of the association
			// from the name of the mapped primary key
			// column (this is not required by the JPA
			// spec) and is arguably backwards, given
			// the name of the @MapsId annotation, but
			// it's better than just having two different
			// column names which disagree
			final var column = parent.resolveMapsId().getValue().getColumns().get( columnIndex );
//			return column.getQuotedName();
			if ( column.isExplicit() ) {
				throw new AnnotationException( "Association '" + parent.getPropertyName()
						+ "' in entity '" + parent.getPropertyHolder().getEntityName()
						+ "' is annotated '@MapsId' but refers to a property '"
						+ parent.getMapsId() + "' which has an explicit column mapping" );
			}
		}
//		else {
			return parent.buildDefaultColumnName( referencedEntity, logicalReferencedColumn );
//		}
	}

	private String defaultAnyKeyColumnName() {
		final var context = getBuildingContext();
		final var buildingOptions = context.getBuildingOptions();
		final Identifier logicalColumnName =
				buildingOptions.getImplicitNamingStrategy()
						.determineAnyKeyColumnName( new ImplicitAnyKeyColumnNameSource() {
							private final AttributePath attributePath =
									AttributePath.parse( getParent().getPropertyName() );

View on GitHub (pinned to fad1729dce)

Solutions

  1. Remove the explicit @Column from the identifier property named in the message and let the association's @JoinColumn define the shared column name.
  2. Or keep the explicit @Column and drop @MapsId, mapping the FK manually: '@Column(name="cust_id", insertable=false, updatable=false)' on the id plus a plain @OneToOne with @JoinColumn.
  3. Make sure only ONE side names the column — either the id property or the association's @JoinColumn, never both.

Example fix

// before
@Entity
class Person {
    @Id
    @Column(name = "cust_id") // explicit -> conflicts with @MapsId derivation
    Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    Customer customer;
}

// after
@Entity
class Person {
    @Id
    Long id; // implicit name; derived from the join column

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = "cust_id")
    Customer customer;
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard: an explicitly mapped id property must not be @MapsId-derived
for (Field f : cls.getDeclaredFields()) {
    if (f.isAnnotationPresent(ManyToOne.class) || f.isAnnotationPresent(OneToOne.class)) {
        MapsId mapsId = f.getAnnotation(MapsId.class);
        if (mapsId != null && !mapsId.value().isEmpty()) {
            Field idField = cls.getDeclaredField(mapsId.value());
            if (idField != null && idField.getAnnotation(Column.class) != null
                    && idField.getAnnotation(Column.class).name() != null
                    && !idField.getAnnotation(Column.class).name().isEmpty()) {
                throw new IllegalStateException("Explicit @Column on id conflicts with @MapsId on " + f.getName());
            }
        }
    }
}

Try / catch

try {
    Metadata md = sources.buildMetadata();
} catch (AnnotationException e) {
    // 'explicit column mapping' -> remove @Column from the id property or drop @MapsId
    throw newConfigurationException("Derived-id column conflict", e);
}

Prevention

When it happens

Trigger: '@Id @Column(name = "cust_id") Long id;' together with '@OneToOne @MapsId Customer customer;' on the same entity; @MapsId pointing at an @EmbeddedId attribute that declares an explicit @Column. Detected in AnnotatedJoinColumn when parent.hasMapsId() and the resolved mapped column isExplicit().

Common situations: Shared-primary-key (@OneToOne derived-id) patterns where the id column was renamed explicitly; retrofitting @MapsId onto an existing entity that already had a custom id column name; following a tutorial that adds both @Column on the id and @MapsId on the association.

Related errors


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