HHH-16592 infer join column names using @MapsId

This commit is contained in:
Gavin King 2023-09-07 23:02:55 +02:00
parent 03273eadeb
commit b1116c8b71
9 changed files with 239 additions and 25 deletions

View File

@ -292,7 +292,7 @@ public class AnnotatedJoinColumn extends AnnotatedColumn {
throw new AssertionFailure( "Building implicit column but the column is not implicit" );
}
for ( Column synthCol: referencedValue.getColumns() ) {
this.linkValueUsingDefaultColumnNaming( synthCol, referencedEntity, value );
linkValueUsingDefaultColumnNaming( synthCol, referencedEntity, value );
}
//reset for the future
setMappingColumn( null );
@ -302,9 +302,18 @@ public class AnnotatedJoinColumn extends AnnotatedColumn {
Column referencedColumn,
PersistentClass referencedEntity,
SimpleValue value) {
int columnIndex = getParent().getJoinColumns().indexOf(this);
linkValueUsingDefaultColumnNaming( columnIndex, referencedColumn, referencedEntity, value );
}
public void linkValueUsingDefaultColumnNaming(
int columnIndex,
Column referencedColumn,
PersistentClass referencedEntity,
SimpleValue value) {
final String logicalReferencedColumn = getBuildingContext().getMetadataCollector()
.getLogicalColumnName( referencedEntity.getTable(), referencedColumn.getQuotedName() );
final String columnName = getParent().buildDefaultColumnName( referencedEntity, logicalReferencedColumn );
final String columnName = defaultColumnName( columnIndex, referencedEntity, logicalReferencedColumn );
//yuk side effect on an implicit column
setLogicalColumnName( columnName );
setReferencedColumn( logicalReferencedColumn );
@ -324,6 +333,23 @@ public class AnnotatedJoinColumn extends AnnotatedColumn {
linkWithValue( value );
}
private String defaultColumnName(int columnIndex, PersistentClass referencedEntity, String logicalReferencedColumn) {
final AnnotatedJoinColumns 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
return parent.resolveMapsId().getValue().getColumns().get( columnIndex ).getQuotedName();
}
else {
return parent.buildDefaultColumnName( referencedEntity, logicalReferencedColumn );
}
}
public void addDefaultJoinColumnName(PersistentClass referencedEntity, String logicalReferencedColumn) {
final String columnName = getParent().buildDefaultColumnName( referencedEntity, logicalReferencedColumn );
getMappingColumn().setName( columnName );

View File

@ -31,7 +31,9 @@ import org.hibernate.boot.spi.PropertyData;
import org.hibernate.cfg.RecoverableException;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.Join;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Selectable;
@ -64,9 +66,10 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
private String propertyName; // this is really a .-separated property path
private String mappedBy;
//property name on the owning side if any
private String mapsId;
//property name on the owning side if any
private String mappedByPropertyName;
//table name on the mapped by side if any
//table name on the mapped by side if any
private String mappedByTableName;
private String mappedByEntityName;
private boolean elementCollection;
@ -116,7 +119,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
public static AnnotatedJoinColumns buildJoinColumns(
JoinColumn[] joinColumns,
// Comment comment,
// Comment comment,
String mappedBy,
Map<String, Join> joins,
PropertyHolder propertyHolder,
@ -124,7 +127,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
MetadataBuildingContext buildingContext) {
return buildJoinColumnsWithDefaultColumnSuffix(
joinColumns,
// comment,
// comment,
mappedBy,
joins,
propertyHolder,
@ -136,7 +139,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
public static AnnotatedJoinColumns buildJoinColumnsWithDefaultColumnSuffix(
JoinColumn[] joinColumns,
// Comment comment,
// Comment comment,
String mappedBy,
Map<String, Join> joins,
PropertyHolder propertyHolder,
@ -157,7 +160,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
if ( actualColumns == null || actualColumns.length == 0 ) {
AnnotatedJoinColumn.buildJoinColumn(
null,
// comment,
// comment,
mappedBy,
parent,
propertyHolder,
@ -170,7 +173,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
for ( JoinColumn actualColumn : actualColumns ) {
AnnotatedJoinColumn.buildJoinColumn(
actualColumn,
// comment,
// comment,
mappedBy,
parent,
propertyHolder,
@ -209,6 +212,27 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
return parent;
}
Property resolveMapsId() {
final PersistentClass persistentClass = getPropertyHolder().getPersistentClass();
final KeyValue identifier = persistentClass.getIdentifier();
try {
if ( identifier instanceof Component) {
// an @EmbeddedId
final Component embeddedIdType = (Component) identifier;
return embeddedIdType.getProperty( getMapsId() );
}
else {
// a simple id or an @IdClass
return persistentClass.getProperty( getMapsId() );
}
}
catch (MappingException me) {
throw new AnnotationException( "Identifier field '" + getMapsId()
+ "' named in '@MapsId' does not exist in entity '" + persistentClass.getEntityName() + "'",
me );
}
}
public List<AnnotatedJoinColumn> getJoinColumns() {
return columns;
}
@ -237,8 +261,8 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
/**
* @return true if the association mapping annotation did specify
* {@link jakarta.persistence.OneToMany#mappedBy() mappedBy},
* meaning that this {@code @JoinColumn} mapping belongs to an
* unowned many-valued association.
* meaning that this {@code @JoinColumn} mapping belongs to an
* unowned many-valued association.
*/
public boolean hasMappedBy() {
return mappedBy != null;
@ -304,7 +328,7 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
}
}
final Table table = table( columnOwner );
// final List<Selectable> keyColumns = referencedEntity.getKey().getSelectables();
// final List<Selectable> keyColumns = referencedEntity.getKey().getSelectables();
final List<? extends Selectable> keyColumns = table.getPrimaryKey() == null
? referencedEntity.getKey().getSelectables()
: table.getPrimaryKey().getColumns();
@ -364,9 +388,9 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
Identifier columnIdentifier;
if ( mappedBySide ) {
// NOTE : While it is completely misleading here to allow for the combination
// of a "JPA ElementCollection" to be mappedBy, the code that uses this
// class relies on this behavior for handling the inverse side of
// many-to-many mappings
// of a "JPA ElementCollection" to be mappedBy, the code that uses this
// class relies on this behavior for handling the inverse side of
// many-to-many mappings
columnIdentifier = implicitNamingStrategy.determineJoinColumnName(
new UnownedImplicitJoinColumnNameSource( referencedEntity, logicalReferencedColumn )
);
@ -456,6 +480,18 @@ public class AnnotatedJoinColumns extends AnnotatedColumns {
}
}
public boolean hasMapsId() {
return mapsId != null;
}
public String getMapsId() {
return mapsId;
}
public void setMapsId(String mapsId) {
this.mapsId = nullIfEmpty( mapsId );
}
private class UnownedImplicitJoinColumnNameSource implements ImplicitJoinColumnNameSource {
final AttributePath attributePath;
final Nature implicitNamingNature;

View File

@ -177,7 +177,7 @@ class ColumnsBuilder {
);
}
else {
OneToOne oneToOneAnn = property.getAnnotation( OneToOne.class );
final OneToOne oneToOneAnn = property.getAnnotation( OneToOne.class );
return AnnotatedJoinColumns.buildJoinColumns(
null,
// comment,

View File

@ -310,7 +310,7 @@ public class TableBinder {
final Identifier logicalName;
if ( isJPA2ElementCollection ) {
logicalName = buildingContext.getBuildingOptions().getImplicitNamingStrategy().determineCollectionTableName(
logicalName = buildingContext.getBuildingOptions().getImplicitNamingStrategy().determineCollectionTableName(
new ImplicitCollectionTableNameSource() {
private final EntityNaming owningEntityNaming = new EntityNaming() {
@Override
@ -746,9 +746,10 @@ public class TableBinder {
final List<Column> idColumns = referencedEntity instanceof JoinedSubclass
? referencedEntity.getKey().getColumns()
: referencedEntity.getIdentifier().getColumns();
for ( Column column: idColumns ) {
for ( int i = 0; i < idColumns.size(); i++ ) {
final Column column = idColumns.get(i);
final AnnotatedJoinColumn firstColumn = joinColumns.getJoinColumns().get(0);
firstColumn.linkValueUsingDefaultColumnNaming( column, referencedEntity, value);
firstColumn.linkValueUsingDefaultColumnNaming( i, column, referencedEntity, value );
firstColumn.overrideFromReferencedColumnIfNecessary( column );
}
}
@ -788,8 +789,21 @@ public class TableBinder {
SimpleValue simpleValue) {
final List<Column> valueColumns = value.getColumns();
final List<AnnotatedJoinColumn> columns = joinColumns.getJoinColumns();
final boolean mapsId = joinColumns.hasMapsId();
final List<Column> idColumns = mapsId ? joinColumns.resolveMapsId().getColumns() : null;
for ( int i = 0; i < columns.size(); i++ ) {
final AnnotatedJoinColumn joinColumn = columns.get(i);
if ( mapsId ) {
// infer the names of the primary key column
// from the join column of the association
// as (sorta) required by the JPA spec
final Column column = idColumns.get(i);
final String logicalColumnName = joinColumn.getLogicalColumnName();
if ( logicalColumnName != null ) {
column.setName( logicalColumnName );
simpleValue.getTable().columnRenamed( column);
}
}
final Column synthCol = valueColumns.get(i);
if ( joinColumn.isNameDeferred() ) {
//this has to be the default value

View File

@ -205,11 +205,14 @@ public class ToOneBinder {
}
if ( property.isAnnotationPresent( MapsId.class ) ) {
final MapsId mapsId = property.getAnnotation(MapsId.class);
final List<AnnotatedJoinColumn> joinColumnList = joinColumns.getJoinColumns();
//read only
for ( AnnotatedJoinColumn column : joinColumns.getJoinColumns() ) {
for ( AnnotatedJoinColumn column : joinColumnList ) {
column.setInsertable( false );
column.setUpdatable( false );
}
joinColumns.setMapsId( mapsId.value() );
}
boolean hasSpecjManyToOne = handleSpecjSyntax( joinColumns, inferredData, context, property );

View File

@ -298,6 +298,17 @@ public class Table implements Serializable, ContributableDatabaseObject {
}
}
@Internal
public void columnRenamed(Column column) {
for ( Map.Entry<String, Column> entry : columns.entrySet() ) {
if ( entry.getValue() == column ) {
columns.remove( entry.getKey() );
columns.put( column.getCanonicalName(), column );
break;
}
}
}
public int getColumnSpan() {
return columns.size();
}

View File

@ -7,7 +7,6 @@ import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToMany;
@ -124,7 +123,7 @@ public class MapsEmbeddedIdTest {
@ManyToOne
@MapsId("exLoanId")
@JoinColumn(name = "EX_LOAN_ID")
// @JoinColumn(name = "EX_LOAN_ID")
private Loan loan;
}
}

View File

@ -0,0 +1,126 @@
package org.hibernate.orm.test.annotations.mapsid;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToMany;
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.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SessionFactory
@DomainModel(annotatedClasses = {MapsIdSpecTest.Loan.class, MapsIdSpecTest.Extension.class})
public class MapsIdSpecTest {
@Test void test(SessionFactoryScope scope) {
ExtensionId eid = scope.fromTransaction( s -> {
Loan loan = new Loan();
loan.id = 999L;
Extension extension = new Extension();
extension.exLoanId = loan.id;
extension.loan = loan;
extension.exNo = 1;
extension.exExtensionDays = 30;
loan.extensions.add(extension);
extension = new Extension();
extension.exLoanId = loan.id;
extension.loan = loan;
extension.exNo = 2;
extension.exExtensionDays = 14;
loan.extensions.add(extension);
s.persist(loan);
return new ExtensionId(extension.exLoanId, extension.exNo );
});
scope.inSession( s -> {
List<Extension> extensions = s.createQuery("from Extension", Extension.class).getResultList();
assertEquals(2, extensions.size());
} );
scope.inSession( s -> {
Extension extension = s.find(Extension.class, eid);
assertEquals(14, extension.exExtensionDays);
assertEquals(2, extension.exNo);
assertEquals(999L, extension.exLoanId);
assertNotNull( extension.loan );
});
scope.inSession( s -> {
Loan loan = s.find(Loan.class, eid.exLoanId);
Extension extension = loan.extensions.get(0);
assertEquals(1, extension.exNo);
assertEquals(30, extension.exExtensionDays);
assertEquals(999L, extension.exLoanId);
assertEquals(loan, extension.loan);
});
}
@Entity(name = "Loan")
static class Loan {
@Id
@Column(name = "LOAN_ID")
private Long id;
private BigDecimal amount = BigDecimal.ZERO;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan")
private List<Extension> extensions = new ArrayList<>();
}
static class ExtensionId {
private Long exLoanId;
private int exNo;
public ExtensionId(Long exLoanId, int exNo) {
this.exLoanId = exLoanId;
this.exNo = exNo;
}
public ExtensionId() {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ExtensionId)) return false;
ExtensionId that = (ExtensionId) o;
return exNo == that.exNo && Objects.equals(exLoanId, that.exLoanId);
}
@Override
public int hashCode() {
return Objects.hash(exLoanId, exNo);
}
}
@Entity(name = "Extension")
@IdClass(ExtensionId.class)
static class Extension {
@Id
// @Column(name = "EX_LOAN_ID")
private Long exLoanId;
@Id
@Column(name = "EX_NO")
private int exNo;
@Column(name = "EX_EXTENSION_DAYS")
private int exExtensionDays;
@ManyToOne
@MapsId("exLoanId")
@JoinColumn(name = "EX_LOAN_ID")
private Loan loan;
}
}

View File

@ -5,7 +5,6 @@ import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToMany;
@ -120,8 +119,8 @@ public class MapsIdTest {
private int exExtensionDays;
@ManyToOne
@MapsId
@JoinColumn(name = "EX_LOAN_ID")
@MapsId("exLoanId")
// @JoinColumn(name = "EX_LOAN_ID")
private Loan loan;
}
}