Introduce `VirtualIdEmbeddable` and `IdClassEmbeddable` + instantiators

Prep work for EmbeddableInstantiator - initializer

Still need to
  - integrate EmbeddableInstantiator work
  - integrate embedded forms.  `VirtualIdEmbeddable` does not really need it as it can use the id-mapping itself as the embedded form.  But `IdClassEmbedded` should really be integrated
  - integrate `VirtualKeyEmbeddable` and `VirtualKeyEmbedded` for use as inverse composite fks
  - share `#finishInit` handling for `EmbeddableMappingType`, `VirtualIdEmbeddable` and `IdClassEmbeddable`
  - ability to use the containing composite owner as the parent of a composite (legacy behavior is to always use the "first" entity
This commit is contained in:
Steve Ebersole 2021-11-30 13:42:16 -06:00
parent 01c6b144ce
commit 29ed0a0566
10 changed files with 386 additions and 39 deletions

View File

@ -48,7 +48,7 @@ public final class EntityKey implements Serializable {
public EntityKey(Object id, EntityPersister persister) { public EntityKey(Object id, EntityPersister persister) {
this.persister = persister; this.persister = persister;
if ( id == null ) { if ( id == null ) {
throw new AssertionFailure( "null identifier" ); throw new AssertionFailure( "null identifier (" + persister.getEntityName() + ")" );
} }
this.identifier = id; this.identifier = id;
this.hashCode = generateHashCode(); this.hashCode = generateHashCode();

View File

@ -6,6 +6,7 @@
*/ */
package org.hibernate.metamodel.mapping; package org.hibernate.metamodel.mapping;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.property.access.spi.PropertyAccess;
import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.Fetchable;
@ -39,11 +40,22 @@ public interface AttributeMapping extends ModelPart, ValueMapping, Fetchable, Pr
* Convenient access to getting the value for this attribute from the "owner" * Convenient access to getting the value for this attribute from the "owner"
*/ */
default Object getValue(Object container, SharedSessionContractImplementor session) { default Object getValue(Object container, SharedSessionContractImplementor session) {
return getValue( container, session.getSessionFactory() );
}
/**
* Convenient access to getting the value for this attribute from the "owner"
*/
default Object getValue(Object container, SessionFactoryImplementor sessionFactory) {
return getPropertyAccess().getGetter().get( container ); return getPropertyAccess().getGetter().get( container );
} }
default void setValue(Object container, Object value, SharedSessionContractImplementor session) { default void setValue(Object container, Object value, SharedSessionContractImplementor session) {
getPropertyAccess().getSetter().set( container, value, session.getSessionFactory() ); setValue( container, value, session.getSessionFactory() );
}
default void setValue(Object container, Object value, SessionFactoryImplementor sessionFactory) {
getPropertyAccess().getSetter().set( container, value, sessionFactory );
} }
/** /**

View File

@ -811,12 +811,8 @@ public class EmbeddableMappingTypeImpl implements EmbeddableMappingType, Selecta
} }
for ( int i = 0; i < attributeMappings.size(); i++ ) { for ( int i = 0; i < attributeMappings.size(); i++ ) {
attributeMappings.get( i ) final AttributeMapping attributeMapping = attributeMappings.get( i );
.getAttributeMetadataAccess() attributeMapping.setValue( compositeInstance, resolvedValues[i], sessionFactory );
.resolveAttributeMetadata( null )
.getPropertyAccess()
.getSetter()
.set( compositeInstance, resolvedValues[i], sessionFactory );
} }
} }

View File

@ -29,12 +29,14 @@ import org.hibernate.sql.results.graph.Fetch;
import org.hibernate.sql.results.graph.FetchParentAccess; import org.hibernate.sql.results.graph.FetchParentAccess;
import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.collection.CollectionInitializer; import org.hibernate.sql.results.graph.collection.CollectionInitializer;
import org.hibernate.sql.results.graph.entity.AbstractEntityInitializer;
import org.hibernate.sql.results.graph.entity.EntityInitializer; import org.hibernate.sql.results.graph.entity.EntityInitializer;
import org.hibernate.sql.results.internal.NullValueAssembler; import org.hibernate.sql.results.internal.NullValueAssembler;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
import org.hibernate.type.descriptor.java.JavaType;
import org.hibernate.type.descriptor.java.spi.EntityJavaTypeDescriptor; import org.hibernate.type.descriptor.java.spi.EntityJavaTypeDescriptor;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList;
/** /**
@ -85,6 +87,10 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
this.resolvedValues = new Object[ numOfAttrs ]; this.resolvedValues = new Object[ numOfAttrs ];
this.assemblers = arrayList( numOfAttrs ); this.assemblers = arrayList( numOfAttrs );
// todo (6.0) - why not just use the "maps id" form right here?
// ( (CompositeIdentifierMapping) embedded ).getMappedIdEmbeddableTypeDescriptor()
// in theory this should let us avoid the explicit maps-id handling via `setPropertyValuesOnTarget`
embeddableTypeDescriptor.visitFetchables( embeddableTypeDescriptor.visitFetchables(
stateArrayContributor -> { stateArrayContributor -> {
final Fetch fetch = resultDescriptor.findFetch( stateArrayContributor ); final Fetch fetch = resultDescriptor.findFetch( stateArrayContributor );
@ -126,12 +132,21 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
} }
@Override @Override
public void resolveKey(RowProcessingState rowProcessingState) { public FetchParentAccess findFirstEntityDescriptorAccess() {
return getFetchParentAccess().findFirstEntityDescriptorAccess();
}
@Override
public void resolveKey(RowProcessingState processingState) {
// nothing to do // nothing to do
} }
@Override @Override
public void resolveInstance(RowProcessingState rowProcessingState) { public void resolveInstance(RowProcessingState processingState) {
reallyResolve( processingState );
}
private void reallyResolve(RowProcessingState processingState) {
if ( compositeInstance != null ) { if ( compositeInstance != null ) {
return; return;
} }
@ -140,14 +155,25 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
// which we access through the fetch parent access. // which we access through the fetch parent access.
// If this model part is an identifier, we must construct the instance as this is called during resolveKey // If this model part is an identifier, we must construct the instance as this is called during resolveKey
final EmbeddableMappingType embeddableTypeDescriptor = embedded.getEmbeddableTypeDescriptor(); final EmbeddableMappingType embeddableTypeDescriptor = embedded.getEmbeddableTypeDescriptor();
final JavaType<?> embeddableJtd = embeddableTypeDescriptor.getMappedJavaTypeDescriptor();
if ( fetchParentAccess != null && embeddableTypeDescriptor.getMappedJavaTypeDescriptor().getJavaTypeClass() if ( fetchParentAccess != null &&
.isAssignableFrom( fetchParentAccess.getInitializedPart().getJavaTypeDescriptor().getJavaTypeClass() ) embeddableJtd.getJavaTypeClass().isAssignableFrom( fetchParentAccess.getInitializedPart().getJavaTypeDescriptor().getJavaTypeClass() )
&& embeddableTypeDescriptor.getMappedJavaTypeDescriptor() instanceof EntityJavaTypeDescriptor<?> && embeddableJtd instanceof EntityJavaTypeDescriptor<?>
&& !( embedded instanceof CompositeIdentifierMapping ) && !( embedded instanceof CompositeIdentifierMapping )
&& !EntityIdentifierMapping.ROLE_LOCAL_NAME.equals( embedded.getFetchableName() ) ) { && !EntityIdentifierMapping.ROLE_LOCAL_NAME.equals( embedded.getFetchableName() ) ) {
fetchParentAccess.resolveInstance( rowProcessingState ); EmbeddableLoadingLogger.INSTANCE.debugf(
"Linking composite instance to fetch-parent [%s] - %s",
navigablePath,
fetchParentAccess
);
fetchParentAccess.resolveInstance( processingState );
compositeInstance = fetchParentAccess.getInitializedInstance(); compositeInstance = fetchParentAccess.getInitializedInstance();
EmbeddableLoadingLogger.INSTANCE.debugf(
"Done linking composite instance to fetch-parent [%s] - %s",
navigablePath,
compositeInstance
);
} }
if ( compositeInstance == null ) { if ( compositeInstance == null ) {
@ -164,37 +190,24 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
@Override @Override
public void initializeInstance(RowProcessingState processingState) { public void initializeInstance(RowProcessingState processingState) {
final Initializer initializer = processingState.resolveInitializer( navigablePath.getParent() );
handleParentInjection( processingState );
EmbeddableLoadingLogger.INSTANCE.debugf( EmbeddableLoadingLogger.INSTANCE.debugf(
"Initializing composite instance [%s]", "Initializing composite instance [%s]",
navigablePath navigablePath
); );
boolean areAllValuesNull = true; handleParentInjection( processingState );
for ( int i = 0; i < assemblers.size(); i++ ) {
final DomainResultAssembler<?> assembler = assemblers.get( i );
final Object contributorValue = assembler.assemble(
processingState,
processingState.getJdbcValuesSourceProcessingState().getProcessingOptions()
);
resolvedValues[i] = contributorValue; extractRowState( processingState );
if ( contributorValue != null ) {
areAllValuesNull = false;
}
}
if ( !createEmptyCompositesEnabled && areAllValuesNull ) { if ( !createEmptyCompositesEnabled && allValuesNull == TRUE ) {
compositeInstance = null; compositeInstance = null;
} }
else { else {
notifyResolutionListeners( compositeInstance ); notifyResolutionListeners( compositeInstance );
if ( compositeInstance instanceof HibernateProxy ) { if ( compositeInstance instanceof HibernateProxy ) {
if ( initializer != this ) { final Initializer parentInitializer = processingState.resolveInitializer( navigablePath.getParent() );
( (AbstractEntityInitializer) initializer ).registerResolutionListener( if ( parentInitializer != this ) {
( (FetchParentAccess) parentInitializer ).registerResolutionListener(
entity -> setPropertyValuesOnTarget( entity, processingState.getSession() ) entity -> setPropertyValuesOnTarget( entity, processingState.getSession() )
); );
} }
@ -214,11 +227,28 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
// A possible alternative could be to initialize the resolved values for primitive fields to their default value, // A possible alternative could be to initialize the resolved values for primitive fields to their default value,
// but that might cause unexpected outcomes for Hibernate 5 users that use createEmptyCompositesEnabled when updating. // but that might cause unexpected outcomes for Hibernate 5 users that use createEmptyCompositesEnabled when updating.
// You can see the need for this by running EmptyCompositeEquivalentToNullTest // You can see the need for this by running EmptyCompositeEquivalentToNullTest
else if ( !areAllValuesNull ) { else if ( allValuesNull == FALSE ) {
setPropertyValuesOnTarget( compositeInstance, processingState.getSession() ); setPropertyValuesOnTarget( compositeInstance, processingState.getSession() );
} }
} }
} }
private void extractRowState(RowProcessingState processingState) {
allValuesNull = true;
for ( int i = 0; i < assemblers.size(); i++ ) {
final DomainResultAssembler<?> assembler = assemblers.get( i );
final Object contributorValue = assembler.assemble(
processingState,
processingState.getJdbcValuesSourceProcessingState().getProcessingOptions()
);
resolvedValues[i] = contributorValue;
if ( contributorValue != null ) {
allValuesNull = false;
}
}
}
private void handleParentInjection(RowProcessingState processingState) { private void handleParentInjection(RowProcessingState processingState) {
final PropertyAccess parentInjectionAccess = embedded.getParentInjectionAttributePropertyAccess(); final PropertyAccess parentInjectionAccess = embedded.getParentInjectionAttributePropertyAccess();
if ( parentInjectionAccess == null ) { if ( parentInjectionAccess == null ) {
@ -229,8 +259,7 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
// todo (6.0) : should we initialize the composite instance if we get here and it is null (not NULL_MARKER)? // todo (6.0) : should we initialize the composite instance if we get here and it is null (not NULL_MARKER)?
// we want to avoid injection for `NULL_MARKER` // we want to avoid injection for `NULL_MARKER`
final Object compositeInstance = getCompositeInstance(); if ( compositeInstance == null || compositeInstance == NULL_MARKER ) {
if ( compositeInstance == null ) {
EmbeddableLoadingLogger.INSTANCE.debugf( EmbeddableLoadingLogger.INSTANCE.debugf(
"Skipping parent injection for null embeddable [%s]", "Skipping parent injection for null embeddable [%s]",
navigablePath navigablePath
@ -342,11 +371,12 @@ public abstract class AbstractEmbeddableInitializer extends AbstractFetchParentA
@Override @Override
public void finishUpRow(RowProcessingState rowProcessingState) { public void finishUpRow(RowProcessingState rowProcessingState) {
compositeInstance = null; compositeInstance = null;
allValuesNull = null;
clearResolutionListeners(); clearResolutionListeners();
} }
@Override @Override
public FetchParentAccess findFirstEntityDescriptorAccess() { public String toString() {
return getFetchParentAccess().findFirstEntityDescriptorAccess(); return getClass().getSimpleName() + "(" + navigablePath + ") : `" + getInitializedPart().getJavaTypeDescriptor().getJavaTypeClass() + "`";
} }
} }

View File

@ -0,0 +1,44 @@
/*
* 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.cid.idclass; /**
* @author Steve Ebersole
*/
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.Id;
import jakarta.persistence.Basic;
@Entity(name = "Customer")
@Table(name = "customers")
public class Customer {
@Id
public Integer id;
@Basic
public String name;
protected Customer() {
// for Hibernate use
}
public Customer(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

@ -0,0 +1,103 @@
/*
* 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.cid.idclass;
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.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = {
Customer.class,
Order.class,
Payment.class
})
@SessionFactory
public class NestedIdClassTests {
@Test
public void smokeTest(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String qry = "from Payment p join fetch p.id.order o join fetch o.id.customer";
session.createQuery( qry ).list();
});
scope.inTransaction( (session) -> {
final String qry = "from Payment p join fetch p.order o join fetch o.customer";
session.createQuery( qry ).list();
});
}
@Test
public void smokeTest2(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final String qry = "from Payment p";
final Payment payment = session.createQuery( qry, Payment.class ).uniqueResult();
assertThat( payment ).isNotNull();
assertThat( payment.accountNumber ).isNotNull();
assertThat( payment.order ).isNotNull();
assertThat( payment.order.orderNumber ).isNotNull();
assertThat( payment.order.customer ).isNotNull();
assertThat( payment.order.customer.id ).isNotNull();
assertThat( payment.order.customer.name ).isNotNull();
});
scope.inTransaction( (session) -> {
final String qry = "from Payment p join fetch p.order o join fetch o.customer";
session.createQuery( qry ).list();
});
}
@Test
public void smokeTest3(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Payment payment = session.get( Payment.class, new PaymentId( new OrderId( 1, 1 ), "123" ) );
assertThat( payment ).isNotNull();
assertThat( payment.accountNumber ).isNotNull();
assertThat( payment.order ).isNotNull();
assertThat( payment.order.orderNumber ).isNotNull();
assertThat( payment.order.customer ).isNotNull();
assertThat( payment.order.customer.id ).isNotNull();
assertThat( payment.order.customer.name ).isNotNull();
});
}
@BeforeEach
public void createTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
final Customer acme = new Customer( 1, "acme" );
final Customer spacely = new Customer( 2, "spacely" );
session.persist( acme );
session.persist( spacely );
final Order acmeOrder1 = new Order( acme, 1, 123F );
final Order acmeOrder2 = new Order( acme, 2, 123F );
session.persist( acmeOrder1 );
session.persist( acmeOrder2 );
final Payment payment = new Payment( acmeOrder1, "123" );
session.persist( payment );
} );
}
@AfterEach
public void dropTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.createQuery( "delete Payment" ).executeUpdate();
session.createQuery( "delete Order" ).executeUpdate();
session.createQuery( "delete Customer" ).executeUpdate();
} );
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.cid.idclass; /**
* @author Steve Ebersole
*/
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity(name = "Order")
@Table(name = "orders")
@IdClass( OrderId.class )
public class Order {
@Id
@ManyToOne
public Customer customer;
@Id
public Integer orderNumber;
public Float amount;
protected Order() {
// for Hibernate use
}
public Order(Customer customer, Integer orderNumber, Float amount) {
this.customer = customer;
this.orderNumber = orderNumber;
this.amount = amount;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public Integer getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(Integer orderNumber) {
this.orderNumber = orderNumber;
}
public Float getAmount() {
return amount;
}
public void setAmount(Float amount) {
this.amount = amount;
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.cid.idclass;
/**
* @author Steve Ebersole
*/
public class OrderId {
Integer customer;
Integer orderNumber;
public OrderId() {
}
public OrderId(Integer customer, Integer orderNumber) {
this.customer = customer;
this.orderNumber = orderNumber;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.cid.idclass; /**
* @author Steve Ebersole
*/
import jakarta.persistence.Basic;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity(name = "Payment")
@Table(name = "payments")
@IdClass( PaymentId.class )
public class Payment {
@Id
@ManyToOne
public Order order;
@Basic
public String accountNumber;
protected Payment() {
// for Hibernate use
}
public Payment(Order order, String accountNumber) {
this.order = order;
this.accountNumber = accountNumber;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public String getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(String accountNumber) {
this.accountNumber = accountNumber;
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.cid.idclass;
/**
* @author Steve Ebersole
*/
public class PaymentId {
private OrderId order;
private String accountNumber;
public PaymentId() {
}
public PaymentId(OrderId order, String accountNumber) {
this.order = order;
this.accountNumber = accountNumber;
}
}