HHH-16592 infer join column names using @MapsId
This commit is contained in:
parent
03273eadeb
commit
b1116c8b71
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue