-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,12 +4,16 @@ | |
*/ | ||
package org.hibernate.boot.model.internal; | ||
|
||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
||
import org.hibernate.AnnotationException; | ||
import org.hibernate.boot.spi.MetadataBuildingContext; | ||
import org.hibernate.boot.spi.PropertyData; | ||
import org.hibernate.internal.util.StringHelper; | ||
import org.hibernate.mapping.AggregateColumn; | ||
import org.hibernate.mapping.Component; | ||
import org.hibernate.mapping.Join; | ||
|
@@ -68,6 +72,7 @@ public class ComponentPropertyHolder extends AbstractPropertyHolder { | |
|
||
private final String embeddedAttributeName; | ||
private final Map<String,AttributeConversionInfo> attributeConversionInfoMap; | ||
private final List<AnnotatedColumn> annotatedColumns; | ||
|
||
public ComponentPropertyHolder( | ||
Component component, | ||
|
@@ -94,6 +99,12 @@ public ComponentPropertyHolder( | |
this.embeddedAttributeName = ""; | ||
this.attributeConversionInfoMap = processAttributeConversions( inferredData.getClassOrElementType() ); | ||
} | ||
|
||
if ( parent instanceof ComponentPropertyHolder parentHolder ) { | ||
this.annotatedColumns = parentHolder.annotatedColumns; | ||
} else { | ||
this.annotatedColumns = new ArrayList<>(); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -221,26 +232,45 @@ public String getEntityName() { | |
|
||
@Override | ||
public void addProperty(Property property, MemberDetails attributeMemberDetails, AnnotatedColumns columns, ClassDetails declaringClass) { | ||
//Ejb3Column.checkPropertyConsistency( ); //already called earlier | ||
//AnnotatedColumns.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 | ||
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)" | ||
); | ||
} | ||
} | ||
if ( columns != null ) { | ||
final Table table = columns.getTable(); | ||
if ( !table.equals( getTable() ) ) { | ||
if ( component.getPropertySpan() == 0 ) { | ||
component.setTable( table ); | ||
} | ||
else { | ||
annotatedColumns.addAll( columns.getColumns() ); | ||
} | ||
addProperty( property, attributeMemberDetails, declaringClass ); | ||
} | ||
|
||
public void checkPropertyConsistency() { | ||
if ( annotatedColumns.size() > 1 ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't you just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no we cannot because the Column does not contains the secondaryTable information here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @beikov ok I see what you are saying, here we construct this annotatedColumns as part of the component and its nested components so we have the full list of columns so we can compare: in our case firstName, lastName and age. Now i am on the debugger and you are right, the component.getColumns() indeed contains the three columns and i can do the same check here without the need of this annotatedColumns. Let me try to make this work, including the negative test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @beikov ok so I tried this idea, but the table is already set previously so they are all resolving to Person and the negative test fails because no exception is thrown here. I know this annotatedColumns is not ideal but i could not find a better way here. |
||
for ( int currentIndex = 1; currentIndex < annotatedColumns.size(); currentIndex++ ) { | ||
final AnnotatedColumn current = annotatedColumns.get( currentIndex ); | ||
final AnnotatedColumn previous = annotatedColumns.get( currentIndex - 1 ); | ||
if ( !Objects.equals( | ||
StringHelper.nullIfEmpty( current.getExplicitTableName() ), | ||
StringHelper.nullIfEmpty( previous.getExplicitTableName() ) ) ) { | ||
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)" | ||
+ "' has properties mapped to two different tables" | ||
+ " (all properties of the embeddable class must map to the same table)" | ||
Comment on lines
-237
to
+269
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind reverting this whitespace change (yes, I know IntelliJ makes this change since a recent release, but I'm not keen on it, and I wish there was a way to revert to the old behavior to stay consistent with the existing code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gavinking which whitespace ? in the EmbeddableBinder ? those ?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, look at the quoted lines, which removed two tabs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gavinking its actually an optical illusion, the AnnotatedException is threw twice here (i could not really find a better way of doing this) and git says there is a change in whitespace but if you pull the PR there is actually none. For the squash, I create a new PR based on main correct ? And close this one ? Or there is a way to squash inside this branch ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gavinking ok nevermind i force pushed the squash. In github do i update with merge commit or update with rebase ? Also should i create backport for Hibernate 7 and 6 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Always use rebase. Let's first try to get this PR through the finish line. Afterwards we can do the backports. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @beikov thanks, I did that, do you think we can go ahead ? Or you want some changes ? Did I answered your concerns ? |
||
); | ||
} | ||
} | ||
} | ||
addProperty( property, attributeMemberDetails, declaringClass ); | ||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* Copyright Red Hat Inc. and Hibernate Authors | ||
*/ | ||
package org.hibernate.orm.test.records; | ||
|
||
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 jakarta.persistence.Table; | ||
import org.hibernate.AnnotationException; | ||
import org.hibernate.boot.MetadataSources; | ||
import org.hibernate.boot.registry.StandardServiceRegistry; | ||
import org.hibernate.testing.orm.junit.DomainModel; | ||
import org.hibernate.testing.orm.junit.JiraKey; | ||
import org.hibernate.testing.orm.junit.ServiceRegistryScope; | ||
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; | ||
import static org.assertj.core.api.Assertions.fail; | ||
|
||
@JiraKey("HHH-19542") | ||
@DomainModel(annotatedClasses = { | ||
RecordNestedEmbeddedWithASecondaryTableTest.UserEntity.class | ||
}) | ||
@SessionFactory | ||
class RecordNestedEmbeddedWithASecondaryTableTest { | ||
|
||
private UserEntity user; | ||
|
||
@BeforeAll | ||
void prepare(SessionFactoryScope scope) { | ||
scope.inTransaction( session -> { | ||
Person person = new Person( new FullName( "Sylvain", "Lecoy" ), 38 ); | ||
user = new UserEntity( person ); | ||
session.persist( user ); | ||
} ); | ||
} | ||
|
||
@Test | ||
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" ); | ||
}); | ||
} | ||
|
||
@Test | ||
void test(ServiceRegistryScope scope) { | ||
final StandardServiceRegistry registry = scope.getRegistry(); | ||
final MetadataSources sources = new MetadataSources( registry ).addAnnotatedClass( UserEntity1.class ); | ||
|
||
try { | ||
sources.buildMetadata(); | ||
fail( "Expecting to fail" ); | ||
} catch (AnnotationException expected) { | ||
assertThat( expected ).hasMessageContaining( "all properties of the embeddable class must map to the same table" ); | ||
} | ||
} | ||
|
||
@Entity | ||
@Table(name = "UserEntity") | ||
@SecondaryTable(name = "Person") | ||
static class UserEntity { | ||
@Id | ||
@GeneratedValue | ||
private Integer id; | ||
private Person person; | ||
|
||
public UserEntity( | ||
final Person person) { | ||
this.person = person; | ||
} | ||
|
||
protected UserEntity() { | ||
|
||
} | ||
} | ||
|
||
@Embeddable | ||
record Person( | ||
FullName fullName, | ||
@Column(table = "Person") | ||
Integer age) { | ||
|
||
} | ||
|
||
@Embeddable | ||
record FullName( | ||
@Column(table = "Person") | ||
String firstName, | ||
@Column(table = "Person") | ||
String lastName) { | ||
|
||
} | ||
|
||
@Entity | ||
@Table(name = "UserEntity") | ||
@SecondaryTable(name = "Person") | ||
public static class UserEntity1 { | ||
@Id | ||
@GeneratedValue | ||
private Integer id; | ||
private Person1 person; | ||
|
||
public UserEntity1( | ||
final Person1 person) { | ||
this.person = person; | ||
} | ||
|
||
protected UserEntity1() { | ||
|
||
} | ||
} | ||
|
||
@Embeddable | ||
public record Person1( | ||
FullName1 fullName, | ||
@Column(table = "Person") | ||
Integer age) { | ||
|
||
} | ||
|
||
@Embeddable | ||
public record FullName1( | ||
@Column(table = "Person") | ||
String firstName, | ||
String lastName) { | ||
|
||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 theAnnotatedColumns
?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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 thegetTable()
resolve to thecomponent.getTable()
, at this stage, thePerson
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 themakePropertyValueAndBind()
//used when the value is provided and the binding is done elsewhere
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the full test suit passes, and i also make this work:
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while this work, i feel i should find a way to pass columns directly, the solution proposed here is actually messy.
My idea is - just like the AnnotatedJoinColumns works - create a "virtual column parent AnnotatedColumns for the component. That way the checkPropertyConsistency will do its work
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have changed the approach completely using my idea but improve to check on the fly and i am pretty happy with the results now.
Checked that every combination of mapping works, and when remove one override or one mapping for 1 or 2 fields, it fails as excepted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gavinking, @mbellade, its in your hands now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@beikov you were right, I found out that the last version did not threw an exception when the columns did not matched. its now resolved