diff --git a/src/main/java/org/hibernate/boot/models/bind/internal/binders/EntityTypeBinder.java b/src/main/java/org/hibernate/boot/models/bind/internal/binders/EntityTypeBinder.java index 0640846..36fe513 100644 --- a/src/main/java/org/hibernate/boot/models/bind/internal/binders/EntityTypeBinder.java +++ b/src/main/java/org/hibernate/boot/models/bind/internal/binders/EntityTypeBinder.java @@ -9,6 +9,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -52,6 +53,7 @@ import org.hibernate.mapping.JoinedSubclass; import org.hibernate.mapping.MappedSuperclass; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.PrimaryKey; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SingleTableSubclass; import org.hibernate.mapping.Subclass; @@ -68,7 +70,10 @@ import jakarta.persistence.DiscriminatorType; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; import jakarta.persistence.SharedCacheMode; import static org.hibernate.boot.models.bind.ModelBindingLogging.MODEL_BINDING_LOGGER; @@ -139,7 +144,6 @@ public EntityTypeBinder( final IdentifiableTypeBinder superTypeBinder = getSuperTypeBinder(); final EntityTypeBinder superEntityBinder = getSuperEntityBinder(); if ( binding instanceof RootClass rootClass ) { - assert superEntityBinder == null; if ( superTypeBinder != null ) { @@ -385,8 +389,8 @@ private UnionSubclass createUnionSubclass(IdentifiableTypeClass superTypeBinding else { assert superTypeBinding instanceof MappedSuperclass; - final PersistentClass superEntity = resolveSuperEntity( superTypeBinding ); - final UnionSubclass binding = new UnionSubclass( + final var superEntity = resolveSuperEntity( superTypeBinding ); + final var binding = new UnionSubclass( superEntity, getBindingState().getMetadataBuildingContext() ); @@ -396,20 +400,114 @@ private UnionSubclass createUnionSubclass(IdentifiableTypeClass superTypeBinding } private JoinedSubclass createJoinedSubclass(IdentifiableTypeClass superTypeBinding) { + final JoinedSubclass joinedSubclass; + + final var superEntityTypeBinding = getSuperEntityBinder().getTypeBinding(); if ( superTypeBinding instanceof PersistentClass superEntity ) { - return new JoinedSubclass( superEntity, getBindingState().getMetadataBuildingContext() ); + joinedSubclass = new JoinedSubclass( + superEntity, + getBindingState().getMetadataBuildingContext() + ); } else { assert superTypeBinding instanceof MappedSuperclass; - final PersistentClass superEntity = resolveSuperEntity( superTypeBinding ); - final JoinedSubclass binding = new JoinedSubclass( - superEntity, + joinedSubclass = new JoinedSubclass( + superEntityTypeBinding, getBindingState().getMetadataBuildingContext() ); - binding.setSuperMappedSuperclass( (MappedSuperclass) superTypeBinding ); - return binding; + joinedSubclass.setSuperMappedSuperclass( (MappedSuperclass) superTypeBinding ); } + + final var joinTableReference = modelBinders.getTableBinder().bindPrimaryTable( + getManagedType(), + EntityHierarchy.HierarchyRelation.SUB + ); + joinedSubclass.setTable( joinTableReference.binding() ); + + final PrimaryKey primaryKey = new PrimaryKey( joinTableReference.binding() ); + joinTableReference.binding().setPrimaryKey( primaryKey ); + + final var targetTable = superEntityTypeBinding.getIdentityTable(); + if ( targetTable.getPrimaryKey() != null && targetTable.getPrimaryKey().getColumnSpan() > 0 ) { + // we can create the foreign key immediately + final var joinTableAnn = getManagedType().getClassDetails().getAnnotationUsage( JoinTable.class ); + + final List> joinColumnAnns = BindingHelper.getValue( + joinTableAnn, + "joinColumns", + Collections.emptyList() + ); + final List> inverseJoinColumnAnns = BindingHelper.getValue( + joinTableAnn, + "inverseJoinColumns", + Collections.emptyList() + ); + + for ( int i = 0; i < targetTable.getPrimaryKey().getColumnSpan(); i++ ) { + final Column targetColumn = targetTable.getPrimaryKey().getColumns().get( i ); + final Column pkColumn; + if ( !inverseJoinColumnAnns.isEmpty() ) { + final var joinColumnAnn = resolveMatchingJoinColumnAnn( + inverseJoinColumnAnns, + targetColumn, + joinColumnAnns + ); + pkColumn = ColumnBinder.bindColumn( joinColumnAnn, targetColumn::getName, true, false ); + } + else { + pkColumn = ColumnBinder.bindColumn( null, targetColumn::getName, true, false ); + } + primaryKey.addColumn( pkColumn ); + } + + final AnnotationUsage foreignKeyAnn = BindingHelper.getValue( joinTableAnn, "foreignKey", (AnnotationUsage) null ); + final String foreignKeyName = foreignKeyAnn == null + ? "" + : BindingHelper.getString( foreignKeyAnn, "name", ForeignKey.class, getBindingContext() ); + final String foreignKeyDefinition = foreignKeyAnn == null + ? "" + : BindingHelper.getString( foreignKeyAnn, "foreignKeyDefinition", ForeignKey.class, getBindingContext() ); + + final org.hibernate.mapping.ForeignKey foreignKey = targetTable.createForeignKey( + foreignKeyName, + primaryKey.getColumns(), + findSuperEntity().getEntityName(), + foreignKeyDefinition, + targetTable.getPrimaryKey().getColumns() + ); + foreignKey.setReferencedTable( targetTable ); + } + else { + throw new UnsupportedOperationException( "Delayed foreign key creation not yet implemented" ); + } + + // todo : bind foreign-key + // todo : do same for secondary tables + // - in both cases we can immediately process the fk if + + return joinedSubclass; + } + + private AnnotationUsage resolveMatchingJoinColumnAnn( + List> inverseJoinColumnAnns, + Column pkColumn, + List> joinColumnAnns) { + int matchPosition = -1; + for ( int j = 0; j < inverseJoinColumnAnns.size(); j++ ) { + final var inverseJoinColumnAnn = inverseJoinColumnAnns.get( j ); + final String name = inverseJoinColumnAnn.getString( "name" ); + if ( pkColumn.getName().equals( name ) ) { + matchPosition = j; + break; + } + } + + if ( matchPosition == -1 ) { + throw new MappingException( "Unable to match primary key column [" + pkColumn.getName() + "] to any inverseJoinColumn - " + getManagedType().getEntityName() ); + } + + return joinColumnAnns.get( matchPosition ); } private SingleTableSubclass createSingleTableSubclass(IdentifiableTypeClass superTypeBinding) { diff --git a/src/main/java/org/hibernate/boot/models/bind/internal/binders/TableBinder.java b/src/main/java/org/hibernate/boot/models/bind/internal/binders/TableBinder.java index a7cd824..da32987 100644 --- a/src/main/java/org/hibernate/boot/models/bind/internal/binders/TableBinder.java +++ b/src/main/java/org/hibernate/boot/models/bind/internal/binders/TableBinder.java @@ -43,6 +43,7 @@ import org.hibernate.models.spi.ClassDetails; import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinTable; import jakarta.persistence.SecondaryTable; /** @@ -87,41 +88,70 @@ public TableBinder( public TableReference bindPrimaryTable(EntityTypeMetadata type, EntityHierarchy.HierarchyRelation hierarchyRelation) { final ClassDetails typeClassDetails = type.getClassDetails(); final AnnotationUsage tableAnn = typeClassDetails.getAnnotationUsage( jakarta.persistence.Table.class ); + final AnnotationUsage joinTableAnn = typeClassDetails.getAnnotationUsage( JoinTable.class ); final AnnotationUsage subselectAnn = typeClassDetails.getAnnotationUsage( Subselect.class ); + if ( tableAnn != null && joinTableAnn != null ) { + throw new AnnotationPlacementException( "Illegal combination of @Table and @JoinTable on " + typeClassDetails.getName() ); + } + if ( tableAnn != null && subselectAnn != null ) { + throw new AnnotationPlacementException( "Illegal combination of @Table and @Subselect on " + typeClassDetails.getName() ); + } + if ( joinTableAnn != null && subselectAnn != null ) { + throw new AnnotationPlacementException( "Illegal combination of @JoinTable and @Subselect on " + typeClassDetails.getName() ); + } + final TableReference tableReference; if ( type.getHierarchy().getInheritanceType() == InheritanceType.TABLE_PER_CLASS ) { assert subselectAnn == null; if ( hierarchyRelation == EntityHierarchy.HierarchyRelation.ROOT ) { - tableReference = bindPhysicalTable( type, tableAnn, true ); + tableReference = bindPhysicalTable( type, tableAnn, jakarta.persistence.Table.class, true ); } else { tableReference = bindUnionTable( type, tableAnn ); } } - else { - if ( subselectAnn != null ) { - if ( tableAnn != null ) { - throw new AnnotationPlacementException( "Illegal combination of @Table and @Subselect on " + typeClassDetails.getName() ); - } - tableReference = bindVirtualTable( type, subselectAnn ); + else if ( type.getHierarchy().getInheritanceType() == InheritanceType.SINGLE_TABLE ) { + if ( hierarchyRelation == EntityHierarchy.HierarchyRelation.ROOT ) { + tableReference = normalTableDetermination( type, subselectAnn, tableAnn, jakarta.persistence.Table.class, typeClassDetails ); } else { - // either an explicit or implicit @Table - tableReference = bindPhysicalTable( type, tableAnn, true ); + tableReference = null; } } + else { + tableReference = normalTableDetermination( type, subselectAnn, joinTableAnn, JoinTable.class, typeClassDetails ); + } - bindingState.addTable( type, tableReference ); + if ( tableReference != null ) { + bindingState.addTable( type, tableReference ); - final PrimaryKey primaryKey = new PrimaryKey( tableReference.binding() ); - tableReference.binding().setPrimaryKey( primaryKey ); + final PrimaryKey primaryKey = new PrimaryKey( tableReference.binding() ); + tableReference.binding().setPrimaryKey( primaryKey ); + } return tableReference; } + private TableReference normalTableDetermination( + EntityTypeMetadata type, + AnnotationUsage subselectAnn, + AnnotationUsage tableAnn, + Class annotationType, + ClassDetails typeClassDetails) { + final TableReference tableReference; + if ( subselectAnn != null ) { + tableReference = bindVirtualTable( type, subselectAnn ); + } + else { + // either an explicit or implicit @Table + tableReference = bindPhysicalTable( type, tableAnn, annotationType, true ); + } + return tableReference; + } + private TableReference bindUnionTable( EntityTypeMetadata type, AnnotationUsage tableAnn) { @@ -206,12 +236,13 @@ public MetadataBuildingContext getBuildingContext() { ); } - private PhysicalTableReference bindPhysicalTable( + private PhysicalTableReference bindPhysicalTable( EntityTypeMetadata type, - AnnotationUsage tableAnn, + AnnotationUsage tableAnn, + Class annotationType, boolean isPrimary) { if ( tableAnn != null ) { - return bindExplicitPhysicalTable( type, tableAnn, isPrimary ); + return bindExplicitPhysicalTable( type, tableAnn, annotationType, isPrimary ); } else { return bindImplicitPhysicalTable( type, isPrimary ); @@ -292,22 +323,23 @@ public MetadataBuildingContext getBuildingContext() { ); } - private PhysicalTable bindExplicitPhysicalTable( + private PhysicalTable bindExplicitPhysicalTable( EntityTypeMetadata type, - AnnotationUsage tableAnn, + AnnotationUsage tableAnn, + Class annotationType, boolean isPrimary) { final Identifier logicalName = determineLogicalName( type, tableAnn ); final Identifier logicalSchemaName = resolveDatabaseIdentifier( tableAnn, "schema", - jakarta.persistence.Table.class, + annotationType, bindingOptions.getDefaultSchemaName(), QuotedIdentifierTarget.SCHEMA_NAME ); final Identifier logicalCatalogName = resolveDatabaseIdentifier( tableAnn, "catalog", - jakarta.persistence.Table.class, + annotationType, bindingOptions.getDefaultCatalogName(), QuotedIdentifierTarget.CATALOG_NAME ); diff --git a/src/main/java/org/hibernate/boot/models/bind/internal/binders/VersionBinder.java b/src/main/java/org/hibernate/boot/models/bind/internal/binders/VersionBinder.java index f220e41..8b42e16 100644 --- a/src/main/java/org/hibernate/boot/models/bind/internal/binders/VersionBinder.java +++ b/src/main/java/org/hibernate/boot/models/bind/internal/binders/VersionBinder.java @@ -35,6 +35,7 @@ public static void bindVersion( final Property property = new Property(); property.setName( attributeMetadata.getName() ); typeBinding.setVersion( property ); + typeBinding.addProperty( property ); final BasicValue basicValue = new BasicValue( bindingState.getMetadataBuildingContext(), diff --git a/src/test/java/org/hibernate/models/orm/bind/joined/SimpleJoinedTests.java b/src/test/java/org/hibernate/models/orm/bind/joined/SimpleJoinedTests.java new file mode 100644 index 0000000..f457bb0 --- /dev/null +++ b/src/test/java/org/hibernate/models/orm/bind/joined/SimpleJoinedTests.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.models.orm.bind.joined; + +import org.hibernate.mapping.JoinedSubclass; +import org.hibernate.mapping.RootClass; + +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.models.orm.bind.BindingTestingHelper.checkDomainModel; + +/** + * @author Steve Ebersole + */ +public class SimpleJoinedTests { + /** + * Allowing for something like: + * + * primaryKeyBinder.whenResolved( (primaryKey) -> ... ) + * + */ + @SuppressWarnings("JUnitMalformedDeclaration") + @Test + @ServiceRegistry + @FailureExpected( + reason = "Binding the primary key is done twice by 2 'owners' overwriting details. " + + "Might be case for distinct, sequential root key and secondary key (secondary tables, subclass tables) binding phases." + ) + void simpleTest(ServiceRegistryScope scope) { + checkDomainModel( + (context) -> { + final var metadataCollector = context.getMetadataCollector(); + final RootClass rootBinding = (RootClass) metadataCollector.getEntityBinding( Root.class.getName() ); + final JoinedSubclass subBinding = (JoinedSubclass) metadataCollector.getEntityBinding( Sub.class.getName() ); + + assertThat( rootBinding.getTable() ).isNotNull(); + assertThat( rootBinding.getTable() ).isSameAs( rootBinding.getIdentityTable() ); + assertThat( rootBinding.getTable() ).isSameAs( rootBinding.getRootTable() ); + assertThat( rootBinding.getDiscriminator() ).isNotNull(); + assertThat( rootBinding.getDiscriminator().getColumns() ).hasSize( 1 ); + assertThat( rootBinding.getDiscriminatorValue() ).isEqualTo( "R" ); + + assertThat( subBinding.getTable() ).isNotNull(); + assertThat( subBinding.getTable() ).isNotSameAs( rootBinding.getIdentityTable() ); + assertThat( subBinding.getTable() ).isNotSameAs( rootBinding.getRootTable() ); + assertThat( subBinding.getIdentityTable() ).isSameAs( rootBinding.getRootTable() ); + assertThat( subBinding.getRootTable() ).isSameAs( rootBinding.getRootTable() ); + assertThat( subBinding.getDiscriminatorValue() ).isEqualTo( "S" ); + + assertThat( rootBinding.getTable().getPrimaryKey() ).isNotNull(); + assertThat( rootBinding.getTable().getPrimaryKey().getColumns() ).hasSize( 1 ); + + assertThat( subBinding.getTable().getPrimaryKey() ).isNotNull(); + assertThat( subBinding.getTable().getPrimaryKey().getColumns() ).hasSize( 1 ); + }, + scope.getRegistry(), + Root.class, + Sub.class + ); + } + + @Entity(name="Root") + @Table(name="Root") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(discriminatorType = DiscriminatorType.CHAR) + @DiscriminatorValue("R") + public static class Root { + @Id + private Integer id; + private String name; + } + + @Entity(name="Sub") + @Table(name="Sub") + @DiscriminatorValue("S") + public static class Sub extends Root { + @Id + private Integer id; + private String name; + } +}