HHH-15393 - Improve write-paths to use mapping model

This commit is contained in:
Steve Ebersole 2022-12-01 20:00:21 -06:00
parent 33ce6a3d79
commit ee1788c3c3
7 changed files with 537 additions and 30 deletions

View File

@ -14,7 +14,7 @@ import org.hibernate.engine.spi.IdentifierValue;
*
* @author Andrea Boriero
*/
public interface CompositeIdentifierMapping extends EntityIdentifierMapping {
public interface CompositeIdentifierMapping extends EntityIdentifierMapping, EmbeddableValuedModelPart {
@Override
default IdentifierValue getUnsavedStrategy() {

View File

@ -63,6 +63,27 @@ public class EmbeddedForeignKeyDescriptor implements ForeignKeyDescriptor {
private final AssociationKey associationKey;
private final boolean hasConstraint;
public EmbeddedForeignKeyDescriptor(
String keyTable,
SelectableMappings keySelectableMappings,
EmbeddableValuedModelPart keyMappingType,
String targetTable,
SelectableMappings targetSelectableMappings,
EmbeddableValuedModelPart targetMappingType,
boolean hasConstraint,
MappingModelCreationProcess creationProcess) {
this(
keyMappingType,
targetMappingType,
keyTable,
keySelectableMappings,
targetTable,
targetSelectableMappings,
hasConstraint,
creationProcess
);
}
public EmbeddedForeignKeyDescriptor(
EmbeddableValuedModelPart keyMappingType,
EmbeddableValuedModelPart targetMappingType,

View File

@ -9,7 +9,6 @@ package org.hibernate.metamodel.mapping.internal;
import java.util.Locale;
import java.util.function.Consumer;
import org.hibernate.NotYetImplementedFor6Exception;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
@ -24,6 +23,7 @@ import org.hibernate.mapping.Value;
import org.hibernate.metamodel.mapping.AssociationKey;
import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.CompositeIdentifierMapping;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityAssociationMapping;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
@ -34,6 +34,7 @@ import org.hibernate.metamodel.mapping.ModelPartContainer;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.SelectableConsumer;
import org.hibernate.metamodel.mapping.SelectableMapping;
import org.hibernate.metamodel.mapping.SelectableMappings;
import org.hibernate.metamodel.mapping.VirtualModelPart;
import org.hibernate.persister.collection.BasicCollectionPersister;
import org.hibernate.persister.collection.CollectionPersister;
@ -53,6 +54,8 @@ import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.type.EntityType;
import static java.util.Objects.requireNonNullElse;
import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart;
import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder;
/**
* Entity-valued collection-part mapped through a join table. Models both <ul>
@ -375,8 +378,12 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
final String collectionTableName = ( (BasicCollectionPersister) collectionDescriptor ).getTableName();
foreignKey = createJoinTablePartForeignKey( collectionTableName, elementDescriptor, creationProcess );
// this fk will refer to the associated entity's id. if that id is not ready yet, delay this creation
if ( getAssociatedEntityMappingType().getIdentifierMapping() == null ) {
return false;
}
foreignKey = createJoinTablePartForeignKey( collectionTableName, elementDescriptor, creationProcess );
creationProcess.registerForeignKey( this, foreignKey );
}
else {
@ -421,21 +428,28 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
private ForeignKeyDescriptor createJoinTablePartForeignKey(
String collectionTableName,
ManyToOne elementDescriptor,
ManyToOne elementBootDescriptor,
MappingModelCreationProcess creationProcess) {
final EntityIdentifierMapping identifierMapping = getAssociatedEntityMappingType().getIdentifierMapping();
if ( identifierMapping.getNature() == EntityIdentifierMapping.Nature.SIMPLE ) {
final BasicEntityIdentifierMapping basicIdMapping = (BasicEntityIdentifierMapping) identifierMapping;
final EntityMappingType associatedEntityMapping = getAssociatedEntityMappingType();
final EntityIdentifierMapping associatedIdMapping = associatedEntityMapping.getIdentifierMapping();
assert associatedIdMapping != null;
assert elementDescriptor.getColumns().size() == 1;
final Column keyColumn = elementDescriptor.getColumns().get( 0 );
// NOTE : `elementBootDescriptor` describes the key side of the fk
// NOTE : `associatedIdMapping` is the target side model-part
// collectionTableName.keyColumnName -> targetTableName.targetColumnName
// we have the fk target model-part and selectables via the associated entity's id mapping
// and need to create the inverse (key) selectable-mappings and composite model-part
if ( associatedIdMapping.getNature() == EntityIdentifierMapping.Nature.SIMPLE ) {
final BasicEntityIdentifierMapping targetModelPart = (BasicEntityIdentifierMapping) associatedIdMapping;
assert elementBootDescriptor.getColumns().size() == 1;
final Column keyColumn = elementBootDescriptor.getColumns().get( 0 );
final SelectableMapping keySelectableMapping = SelectableMappingImpl.from(
collectionTableName,
keyColumn,
basicIdMapping.getJdbcMapping(),
targetModelPart.getJdbcMapping(),
creationProcess.getCreationContext().getTypeConfiguration(),
true,
false,
@ -444,9 +458,9 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
);
final BasicAttributeMapping keyModelPart = BasicAttributeMapping.withSelectableMapping(
getAssociatedEntityMappingType(),
basicIdMapping,
basicIdMapping.getPropertyAccess(),
associatedEntityMapping,
targetModelPart,
targetModelPart.getPropertyAccess(),
true,
false,
keySelectableMapping
@ -456,17 +470,45 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple
// the key
keyModelPart,
// the target
basicIdMapping,
targetModelPart,
// refers to primary key
true,
// has a constraint
true,
!elementBootDescriptor.isNullable(),
// do not swap the sides
false
);
}
else {
throw new NotYetImplementedFor6Exception( getClass() );
final CompositeIdentifierMapping targetModelPart = (CompositeIdentifierMapping) associatedIdMapping;
final SelectableMappings keySelectableMappings = SelectableMappingsImpl.from(
collectionTableName,
elementBootDescriptor,
getPropertyOrder( elementBootDescriptor, creationProcess ),
creationProcess.getCreationContext().getSessionFactory(),
creationProcess.getCreationContext().getTypeConfiguration(),
elementBootDescriptor.getColumnInsertability(),
elementBootDescriptor.getColumnUpdateability(),
creationProcess.getCreationContext().getSessionFactory().getJdbcServices().getDialect(),
creationProcess.getSqmFunctionRegistry()
);
return new EmbeddedForeignKeyDescriptor(
collectionTableName,
keySelectableMappings,
createInverseModelPart(
targetModelPart,
associatedEntityMapping,
this,
keySelectableMappings,
creationProcess
),
targetModelPart.getContainingTableExpression(),
targetModelPart.getPartMappingType(),
targetModelPart,
!elementBootDescriptor.isNullable(),
creationProcess
);
}
}

View File

@ -1225,7 +1225,7 @@ public class MappingModelCreationHelper {
}
}
private static int[] getPropertyOrder(Value bootValueMapping, MappingModelCreationProcess creationProcess) {
public static int[] getPropertyOrder(Value bootValueMapping, MappingModelCreationProcess creationProcess) {
final ComponentType componentType;
final boolean sorted;
if ( bootValueMapping instanceof Collection ) {

View File

@ -0,0 +1,260 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.mapping.manytoone.jointable;
import java.util.List;
import java.util.stream.Collectors;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.internal.EmbeddedForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.internal.ManyToManyCollectionPart;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.persister.entity.EntityPersister;
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 jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Asserts the runtime model descriptors related to the inverse side
* of a many-to-one with join-table.
* <p/>
* This tests simple keys. See {@link InverseManyToOneJoinTableSimpleIdTest} for
* simple id testing
*
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = {
InverseManyToOneJoinTableCompositeIdTest.Book.class,
InverseManyToOneJoinTableCompositeIdTest.Author.class
} )
@SessionFactory
public class InverseManyToOneJoinTableCompositeIdTest {
@Test
public void assertModel(SessionFactoryScope scope) {
final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( Author.class );
final PluralAttributeMapping books = (PluralAttributeMapping) entityDescriptor.findAttributeMapping( "books" );
final ManyToManyCollectionPart booksElementDescriptor = (ManyToManyCollectionPart) books.getElementDescriptor();
final EmbeddedForeignKeyDescriptor booksFk = (EmbeddedForeignKeyDescriptor) booksElementDescriptor.getForeignKeyDescriptor();
assertThat( booksFk.getKeyTable() ).isEqualTo( "book_authors" );
booksFk.getKeyPart().forEachSelectable( (selectionIndex, selectableMapping) -> {
final String expectedColumnName;
if ( selectionIndex == 0 ) {
expectedColumnName = "book_int_key";
}
else {
assert selectionIndex == 1;
expectedColumnName = "book_char_key";
}
assertThat( selectableMapping.getSelectionExpression() ).isEqualTo( expectedColumnName );
} );
assertThat( booksFk.getTargetTable() ).isEqualTo( "books" );
booksFk.getTargetPart().forEachSelectable( (selectionIndex, selectableMapping) -> {
final String expectedColumnName;
if ( selectionIndex == 0 ) {
expectedColumnName = "int_key";
}
else {
assert selectionIndex == 1;
expectedColumnName = "char_key";
}
assertThat( selectableMapping.getSelectionExpression() ).isEqualTo( expectedColumnName );
} );
}
@Test
public void usageSmokeTest(SessionFactoryScope scope) {
createTestData( scope );
try {
scope.inTransaction( (session) -> {
final Author stephenKing = session.get( Author.class, 1 );
verifyStephenKingBooks( stephenKing );
} );
scope.inTransaction( (session) -> {
final Author stephenKing = session
.createSelectionQuery( "from Author a join fetch a.books where a.id = 1", Author.class )
.getSingleResult();
verifyStephenKingBooks( stephenKing );
} );
}
finally {
dropTestData( scope );
}
}
private void verifyStephenKingBooks(Author author) {
final List<String> bookNames = author.books.stream().map( Book::getName ).collect( Collectors.toList() );
assertThat( bookNames ).contains( "It", "The Shining" );
}
private void createTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Author stephenKing = new Author( 1, "Stephen King" );
final Author johnMilton = new Author( 2, "John Milton" );
session.persist( stephenKing );
session.persist( johnMilton );
session.persist( new Book( new BookPk( 1, "king" ), "It", stephenKing ) );
session.persist( new Book( new BookPk( 2, "king" ), "The Shining", stephenKing ) );
session.persist( new Book( new BookPk( 1, "milton" ), "Paradise Lost", johnMilton ) );
} );
}
private void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createQuery( "from Author", Author.class ).list().forEach( session::remove );
} );
scope.inTransaction( (session) -> {
final Long bookCount = session.createSelectionQuery( "select count(1) from Book", Long.class ).uniqueResult();
assertThat( bookCount ).isEqualTo( 0L );
} );
}
@Entity( name = "Book" )
@Table( name = "books" )
public static class Book {
@EmbeddedId
private BookPk id;
@Basic
private String name;
@ManyToOne
@JoinTable(
name = "book_authors",
joinColumns = {
@JoinColumn(name = "book_int_key", referencedColumnName = "int_key"),
@JoinColumn(name = "book_char_key", referencedColumnName = "char_key")
},
inverseJoinColumns = @JoinColumn(name="author_id",nullable = false)
)
private Author author;
private Book() {
// for use by Hibernate
}
public Book(BookPk id, String name, Author author) {
this.id = id;
this.name = name;
this.author = author;
}
public BookPk getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
}
@Embeddable
public static class BookPk {
@Column( name="int_key" )
private Integer key1;
@Column( name="char_key" )
private String key2;
private BookPk() {
// for Hibernate
}
public BookPk(Integer key1, String key2) {
this.key1 = key1;
this.key2 = key2;
}
public Integer getKey1() {
return key1;
}
public String getKey2() {
return key2;
}
}
@Entity( name = "Author" )
@Table( name = "authors" )
public static class Author {
@Id
private Integer id;
@Basic
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "author")
private List<Book> books;
private Author() {
// for use by Hibernate
}
public Author(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Embeddable
public static class AuthorPk {
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
}
}

View File

@ -0,0 +1,184 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.mapping.manytoone.jointable;
import java.util.List;
import java.util.stream.Collectors;
import org.hibernate.metamodel.mapping.PluralAttributeMapping;
import org.hibernate.metamodel.mapping.internal.ManyToManyCollectionPart;
import org.hibernate.metamodel.mapping.internal.SimpleForeignKeyDescriptor;
import org.hibernate.metamodel.spi.MappingMetamodelImplementor;
import org.hibernate.persister.entity.EntityPersister;
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 jakarta.persistence.Basic;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Asserts the runtime model descriptors related to the inverse side
* of a many-to-one with join-table.
* <p/>
* This tests simple keys. See {@link InverseManyToOneJoinTableCompositeIdTest} for
* composite id testing
*
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = { InverseManyToOneJoinTableSimpleIdTest.Book.class, InverseManyToOneJoinTableSimpleIdTest.Author.class } )
@SessionFactory
public class InverseManyToOneJoinTableSimpleIdTest {
@Test
public void assertModel(SessionFactoryScope scope) {
final MappingMetamodelImplementor mappingMetamodel = scope.getSessionFactory()
.getRuntimeMetamodels()
.getMappingMetamodel();
final EntityPersister entityDescriptor = mappingMetamodel.getEntityDescriptor( Author.class );
final PluralAttributeMapping books = (PluralAttributeMapping) entityDescriptor.findAttributeMapping( "books" );
final ManyToManyCollectionPart booksElementDescriptor = (ManyToManyCollectionPart) books.getElementDescriptor();
final SimpleForeignKeyDescriptor booksFk = (SimpleForeignKeyDescriptor) booksElementDescriptor.getForeignKeyDescriptor();
assertThat( booksFk.getKeyTable() ).isEqualTo( "book_authors" );
assertThat( booksFk.getKeyPart().getSelectionExpression() ).isEqualTo( "book_id" );
assertThat( booksFk.getTargetTable() ).isEqualTo( "books" );
assertThat( booksFk.getTargetPart().getSelectionExpression() ).isEqualTo( "id" );
}
@Test
public void usageSmokeTest(SessionFactoryScope scope) {
createTestData( scope );
try {
scope.inTransaction( (session) -> {
final Author stephenKing = session.get( Author.class, 1 );
verifyStephenKingBooks( stephenKing );
} );
scope.inTransaction( (session) -> {
final Author stephenKing = session
.createSelectionQuery( "from Author a join fetch a.books where a.id = 1", Author.class )
.getSingleResult();
verifyStephenKingBooks( stephenKing );
} );
}
finally {
dropTestData( scope );
}
}
private void verifyStephenKingBooks(Author author) {
final List<String> bookNames = author.books.stream().map( Book::getName ).collect( Collectors.toList() );
assertThat( bookNames ).contains( "It", "The Shining" );
}
private void createTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Author stephenKing = new Author( 1, "Stephen King" );
final Author johnMilton = new Author( 2, "John Milton" );
session.persist( stephenKing );
session.persist( johnMilton );
session.persist( new Book( 1, "It", stephenKing ) );
session.persist( new Book( 2, "The Shining", stephenKing ) );
session.persist( new Book( 3, "Paradise Lost", johnMilton ) );
} );
}
private void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createQuery( "from Author", Author.class ).list().forEach( session::remove );
} );
scope.inTransaction( (session) -> {
final Long bookCount = session.createSelectionQuery( "select count(1) from Book", Long.class ).uniqueResult();
assertThat( bookCount ).isEqualTo( 0L );
} );
}
@Entity( name = "Book" )
@Table( name = "books" )
public static class Book {
@Id
private Integer id;
@Basic
private String name;
@ManyToOne
@JoinTable(name = "book_authors",
joinColumns = @JoinColumn(name = "book_id"),
inverseJoinColumns = @JoinColumn(name="author_id",nullable = false))
private Author author;
private Book() {
// for use by Hibernate
}
public Book(Integer id, String name, Author author) {
this.id = id;
this.name = name;
this.author = author;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity( name = "Author" )
@Table( name = "authors" )
public static class Author {
@Id
private Integer id;
@Basic
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "author")
private List<Book> books;
private Author() {
// for use by Hibernate
}
public Author(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@ -1,18 +1,10 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.mapping.manytoone;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
package org.hibernate.orm.test.mapping.manytoone.jointable;
import org.hibernate.metamodel.mapping.ForeignKeyDescriptor;
import org.hibernate.metamodel.mapping.ModelPart;
@ -25,6 +17,14 @@ import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;