Skip to content

HHH-19688 @IdClass with single @Id @OneToOne field #10741

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,11 @@ private static PropertyData getUniqueIdPropertyFromBaseClass(
return baseClassElements.get( 0 );
}

/**
* Given the id class of the current entity, as specified by an
* {@link IdClass} annotation, determine if it's actually the
* identifier type or {@link IdClass} of some associated entity.
*/
private boolean isIdClassPrimaryKeyOfAssociatedEntity(
ElementsToProcess elementsToProcess,
ClassDetails compositeClass,
Expand All @@ -639,26 +644,58 @@ private boolean isIdClassPrimaryKeyOfAssociatedEntity(
Map<ClassDetails, InheritanceState> inheritanceStates,
MetadataBuildingContext context) {
if ( elementsToProcess.getIdPropertyCount() == 1 ) {
// There's only one @Id field, so it might be the @EmbeddedId of an associated
// entity referenced via a @ManyToOne or @OneToOne association
final PropertyData idPropertyOnBaseClass =
getUniqueIdPropertyFromBaseClass( inferredData, baseInferredData, propertyAccessor, context );
final InheritanceState state =
inheritanceStates.get( idPropertyOnBaseClass.getClassOrElementType().determineRawClass() );
final TypeDetails idPropertyType = idPropertyOnBaseClass.getClassOrElementType();
final InheritanceState state = inheritanceStates.get( idPropertyType.determineRawClass() );
if ( state == null ) {
return false; //while it is likely a user error, let's consider it is something that might happen
}
final ClassDetails associatedClassWithIdClass = state.getClassWithIdClass( true );
if ( associatedClassWithIdClass == null ) {
//we cannot know for sure here unless we try and find the @EmbeddedId
//Let's not do this thorough checking but do some extra validation
return hasToOneAnnotation( idPropertyOnBaseClass.getAttributeMember() );

// Likely a user error, but treat it as something that might happen
return false;
}
else {
final IdClass idClass = associatedClassWithIdClass.getAnnotationUsage( IdClass.class, modelsContext() );
return compositeClass.getName().equals( idClass.value().getName() );
final ClassDetails associatedClassWithIdClass = state.getClassWithIdClass( true );
if ( associatedClassWithIdClass == null ) {
// If annotated @OneToOne or @ManyToOne, it's an association to another entity
return hasToOneAnnotation( idPropertyOnBaseClass.getAttributeMember() )
// determine if the @Id or @EmbeddedId tpe is the same
&& isIdClassOfAssociatedEntity( compositeClass, propertyAccessor, context, idPropertyType );
}
else {
// The associated entity has an @IdClass, so check if it's the same
final IdClass idClass =
associatedClassWithIdClass.getAnnotationUsage( IdClass.class, modelsContext() );
return compositeClass.getName().equals( idClass.value().getName() );
}
}
}
else {
// There are multiple @Id fields, so we know for sure that the id class of
// this entity can't be the identifier type of the associated entity
return false;
}
}

private static boolean isIdClassOfAssociatedEntity(
ClassDetails compositeClass,
AccessType propertyAccessor,
MetadataBuildingContext context,
TypeDetails idPropertyType) {
// Determine the @Id type or @EmbeddedId class of the associated entity
final var propertyContainer =
new PropertyContainer( idPropertyType.determineRawClass(), idPropertyType, propertyAccessor );
final List<PropertyData> idProperties = new ArrayList<>();
final int idPropertyCount = addElementsOfClass( idProperties, propertyContainer, context, 0 );
if ( idPropertyCount == 1 ) {
// Exactly one @Id or @EmbeddedId attribute
final PropertyData idPropertyOfAssociatedEntity = idProperties.get( 0 );
return compositeClass.getName()
.equals( idPropertyOfAssociatedEntity.getPropertyType().getName() );
}
else {
// No id property found in the associated class,
// or multiple id properties but no @IdClass
return false;
}
}
Expand Down
104 changes: 104 additions & 0 deletions hibernate-core/src/test/java/x/IdClassSingleOneToOneTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package x;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.OneToOne;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;

import java.util.Objects;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;


@DomainModel( annotatedClasses = {
IdClassSingleOneToOneTest.EntityA.class,
IdClassSingleOneToOneTest.EntityB.class,
} )
@SessionFactory
public class IdClassSingleOneToOneTest {

@Test
public void test(SessionFactoryScope scope) {
scope.getSessionFactory();
scope.inTransaction( session -> {
EntityA entityA = new EntityA(3);
EntityB entityB = new EntityB( entityA );
entityA.entityB = entityB;
session.persist( entityA );
session.persist( entityB );
assertEquals( new EntityBId(3),
session.getIdentifier( entityB ) );
} );
scope.inTransaction( session -> {
EntityB entityB = session.find( EntityB.class, new EntityBId(3) );
assertNotNull( entityB );
} );
}

@Entity( name = "EntityA" )
static class EntityA {

@Id
private Integer id;

@OneToOne( mappedBy = "entityA", fetch = FetchType.LAZY )
private EntityB entityB;

public EntityA() {
}

public EntityA(Integer id) {
this.id = id;
}

}

@IdClass( EntityBId.class )
@Entity( name = "EntityB" )
static class EntityB {

@Id
@OneToOne( fetch = FetchType.LAZY )
private EntityA entityA;

public EntityB() {
}

public EntityB(EntityA entityA) {
this.entityA = entityA;
}

}

static class EntityBId {
private final Integer entityA;

EntityBId() {
entityA = null;
}
EntityBId(Integer entityA) {
this.entityA = entityA;
}

@Override
public boolean equals(Object o) {
return o instanceof EntityBId entityBId
&& Objects.equals( entityA, entityBId.entityA );
}

@Override
public int hashCode() {
return Objects.hashCode( entityA );
}
}
}