Skip to content

HHH-19542 embeddable property order #10356

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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 @@ -221,23 +221,21 @@ public String getEntityName() {

@Override
public void addProperty(Property property, MemberDetails attributeMemberDetails, AnnotatedColumns columns, ClassDetails declaringClass) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are not using columns at all i wonder if in term of design i should keep the logic in this method at all here ? let me know

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @itsmoonrack, this solution looks much better!

There's one last thing that makes me wonder: if the property.getValue().getTable() method returns the expected result (i.e. the @AttributeOverride's table for nested embeddables), why was it not the case in the AnnotatedColumns?

Could you please have a look at the binders logic and check if there might be something missing there? It would be great for consistency to understand why this is happening as well.

Copy link
Author

@itsmoonrack itsmoonrack Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @mbellade, thanks !

That's indeed a good question. From what I have seen earlier the property is already constructed from those columns (higher in the hierarchy call). So in effect the property contains the same informations as the AnnotatedColumns.

What I found however, is that in the special event of an embeddable in an embeddable (our nested use case), that the columns were missing (columns = null), effectively skipping the whole logic of replacing a table. I have ran the full test suit to ensure non regression and it passed. So conceptually I am a bit bugged about this AnnotatedColumns versus Property, for me, after analysing, the later contains the former;

I thought about doing the following,

columns != null ? columns.getTable() : property.getValue().getTable()

But eventually rolled back as I did not find any value of doing so.

In both cases the full test suit passed.

Please correct me if I'm wrong or if I missed something obvious. We can put back the proposed line in this comment if we are more confident with it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you mean why the columns were empty in the first place ?

That's a good question, I will investigate this evening.

Copy link
Author

@itsmoonrack itsmoonrack Jun 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mbellade I tried to pass the columns to the EmbeddableBinder.createEmbeddedProperty to see what happen. Now the AnnotatedColumns are correctly passed, however the getTable() resolve to the component.getTable(), at this stage, the Person component getTable() returns UserEntity, while the property.getValue().getTable() returns Person. The later being correct.

So we cannot blatantly passes the columns to the PropertyBinder.

I am also reading that when we have an underlying value we use the makePropertyAndBind(), instead of the makePropertyValueAndBind() //used when the value is provided and the binding is done elsewhere

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @mbellade, the fact that the columns are null is because in the method createEmbeddedProperty() from the EmbeddableBinder simple don't set columns at all before makePropertyAndBind() is called. But it looks like this is by design since "the value is provided and the binding is done elsewhere".

From now since the value is provided, I feel that the code proposed in this PR does solve our issue.

Do you think its acceptable or you want to dig further ? I feel that the changeset would be larger if we go this other route and my (limited) knowledge is impeding me from finding a satisfactory solution other than that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow this discussion for the last proposition @gavinking

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when the column mappings for different fields disagree on the mapped table (via @AttributeOverride or otherwise) ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gavinking from what i've seen, when I remove the first column @AttributeOverride mapping, to have a disagrement, we have the exception saying all properties of the component must map to the same table.

The propert.getValue().getTable() already resolve to the correct table because the overrides are already made at this point.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to understand the binding logic upstream but just ended not knowing what to do to solve the issue of having columns null here.

//Ejb3Column.checkPropertyConsistency( ); //already called earlier
//AnnotatedColumn.checkPropertyConsistency( ); //already called earlier
// Check table matches between the component and the columns
// if not, change the component table if no properties are set
// if a property is set already the core cannot support that
if ( columns != null ) {
final Table table = columns.getTable();
if ( !table.equals( getTable() ) ) {
if ( component.getPropertySpan() == 0 ) {
component.setTable( table );
}
else {
throw new AnnotationException(
"Embeddable class '" + component.getComponentClassName()
+ "' has properties mapped to two different tables"
+ " (all properties of the embeddable class must map to the same table)"
);
}
final Table table = property.getValue().getTable();
if ( !table.equals( getTable() ) ) {
if ( component.getPropertySpan() == 0 ) {
component.setTable( table );
}
else {
throw new AnnotationException(
"Embeddable class '" + component.getComponentClassName()
+ "' has properties mapped to two different tables"
+ " (all properties of the embeddable class must map to the same table)"
);
Comment on lines +236 to +238
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ "' has properties mapped to two different tables"
+ " (all properties of the embeddable class must map to the same table)"
);
+ "' has properties mapped to two different tables"
+ " '" + table.getName() + "' and '" + getTable().getName() + "'"
+ " (all properties of the embeddable class must map to the same table)"
);

}
}
addProperty( property, attributeMemberDetails, declaringClass );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class EmbeddableA {
@AttributeOverrides({@AttributeOverride(name = "embedAttrB" , column = @Column(table = "TableB"))})
private EmbeddableB embedB;

@Column(table = "TableB")
private String embedAttrA;

public EmbeddableB getEmbedB() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import jakarta.persistence.Table;
import org.hibernate.boot.MetadataSources;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.NotImplementedYet;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
import org.junit.jupiter.api.Test;
Expand All @@ -32,7 +31,6 @@
class NestedEmbeddedObjectWithASecondaryTableTest {

@Test
@NotImplementedYet
void testNestedEmbeddedAndSecondaryTables(ServiceRegistryScope registryScope) {
final MetadataSources metadataSources = new MetadataSources( registryScope.getRegistry() )
.addAnnotatedClasses( Book.class, Author.class, House.class );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.records;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.SecondaryTable;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

@JiraKey("HHH-19542")
@DomainModel(annotatedClasses = {
RecordNestedEmbeddedWithASecondaryTableTest.UserEntity.class
})
@SessionFactory
public class RecordNestedEmbeddedWithASecondaryTableTest {

private UserEntity user;

@BeforeAll
public void prepare(SessionFactoryScope scope) {
scope.inTransaction( session -> {
Person person = new Person( new FullName( "Sylvain", "Lecoy" ), 38 );
user = new UserEntity( person );
session.persist( user );
} );
}

@Test
public void test(SessionFactoryScope scope) {
scope.inTransaction(session -> {
UserEntity entity = session.find( UserEntity.class, user.id );
assertThat( entity ).isNotNull();
assertThat( entity.id ).isEqualTo( user.id );
assertThat( entity.person ).isNotNull();
assertThat( entity.person.age ).isEqualTo( 38 );
assertThat( entity.person.fullName.firstName ).isEqualTo( "Sylvain" );
assertThat( entity.person.fullName.lastName ).isEqualTo( "Lecoy" );
});
}

@Entity
@SecondaryTable(name = "Person")
public static class UserEntity {
@Id
@GeneratedValue
private Integer id;
private Person person;

public UserEntity(
final Person person) {
this.person = person;
}

protected UserEntity() {

}
}

@Embeddable
public record Person(
@AttributeOverride(name = "firstName", column = @Column(table = "Person"))
@AttributeOverride(name = "lastName", column = @Column(table = "Person"))
FullName fullName,
@Column(table = "Person")
Integer age) {

}

@Embeddable
public record FullName(
String firstName,
String lastName) {

}
}