HHH-16463 fix use of @PrimaryKeyJoinColumn with @MapsId

supporting this mapping is required by JPA
This commit is contained in:
Gavin King 2024-02-14 09:44:30 +01:00
parent 2c85e5d190
commit 914227de93
6 changed files with 461 additions and 3 deletions

View File

@ -21,7 +21,6 @@ import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.ImplicitJoinColumnNameSource;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.ImplicitPrimaryKeyJoinColumnNameSource;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.boot.model.relational.Database;
import org.hibernate.boot.model.source.spi.AttributePath;
import org.hibernate.boot.spi.InFlightMetadataCollector;

View File

@ -6,6 +6,10 @@
*/
package org.hibernate.boot.model.internal;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.MapsId;
import jakarta.persistence.PrimaryKeyJoinColumn;
import jakarta.persistence.PrimaryKeyJoinColumns;
import org.hibernate.AnnotationException;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Formula;
@ -27,6 +31,8 @@ import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import java.lang.annotation.Annotation;
import static org.hibernate.boot.model.internal.AnnotatedColumn.buildColumnFromAnnotation;
import static org.hibernate.boot.model.internal.AnnotatedColumn.buildColumnFromNoAnnotation;
import static org.hibernate.boot.model.internal.AnnotatedColumn.buildColumnsFromAnnotations;
@ -266,6 +272,32 @@ class ColumnsBuilder {
}
return joinColumn;
}
else if ( property.isAnnotationPresent( MapsId.class ) ) {
// inelegant solution to HHH-16463, let the PrimaryKeyJoinColumn
// masquerade as a regular JoinColumn (when a @OneToOne maps to
// the primary key of the child table, it's more elegant and more
// spec-compliant to map the association with @PrimaryKeyJoinColumn)
if ( property.isAnnotationPresent( PrimaryKeyJoinColumn.class ) ) {
final PrimaryKeyJoinColumn column = property.getAnnotation( PrimaryKeyJoinColumn.class );
return new JoinColumn[] { new JoinColumnAdapter( column ) };
}
else if ( property.isAnnotationPresent( PrimaryKeyJoinColumns.class ) ) {
final PrimaryKeyJoinColumns primaryKeyJoinColumns = property.getAnnotation( PrimaryKeyJoinColumns.class );
final JoinColumn[] joinColumns = new JoinColumn[primaryKeyJoinColumns.value().length];
final PrimaryKeyJoinColumn[] columns = primaryKeyJoinColumns.value();
if ( columns.length == 0 ) {
throw new AnnotationException( "Property '" + getPath( propertyHolder, inferredData)
+ "' has an empty '@PrimaryKeyJoinColumns' annotation" );
}
for ( int i = 0; i < columns.length; i++ ) {
joinColumns[i] = new JoinColumnAdapter( columns[i] );
}
return joinColumns;
}
else {
return null;
}
}
else {
return null;
}
@ -288,4 +320,62 @@ class ColumnsBuilder {
}
}
@SuppressWarnings("ClassExplicitlyAnnotation")
private static final class JoinColumnAdapter implements JoinColumn {
private final PrimaryKeyJoinColumn column;
public JoinColumnAdapter(PrimaryKeyJoinColumn column) {
this.column = column;
}
@Override
public String name() {
return column.name();
}
@Override
public String referencedColumnName() {
return column.referencedColumnName();
}
@Override
public boolean unique() {
return false;
}
@Override
public boolean nullable() {
return false;
}
@Override
public boolean insertable() {
return false;
}
@Override
public boolean updatable() {
return false;
}
@Override
public String columnDefinition() {
return column.columnDefinition();
}
@Override
public String table() {
return "";
}
@Override
public ForeignKey foreignKey() {
return column.foreignKey();
}
@Override
public Class<? extends Annotation> annotationType() {
return JoinColumn.class;
}
}
}

View File

@ -49,7 +49,6 @@ import org.hibernate.boot.model.IdentifierGeneratorDefinition;
import org.hibernate.boot.spi.AccessType;
import org.hibernate.boot.spi.MetadataBuildingContext;
import org.hibernate.boot.spi.PropertyData;
import org.hibernate.boot.spi.SecondPass;
import org.hibernate.engine.OptimisticLockStyle;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.mapping.Collection;
@ -58,7 +57,6 @@ import org.hibernate.mapping.GeneratorCreator;
import org.hibernate.mapping.Join;
import org.hibernate.mapping.KeyValue;
import org.hibernate.mapping.MappedSuperclass;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.RootClass;
import org.hibernate.mapping.SimpleValue;

View File

@ -0,0 +1,123 @@
/*
* 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.onetoone.pkjoincolumn;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PrimaryKeyJoinColumn;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;
import java.io.Serializable;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@JiraKey("HHH-16463")
@Jpa(annotatedClasses = {OneToOneJoinColumnTest.Parent.class, OneToOneJoinColumnTest.Child.class},
useCollectingStatementInspector = true)
public class OneToOneJoinColumnTest {
static final String PARENT_ID = "parent_key";
static final String CHILD_ID = "child_key";
@Test
public void test(EntityManagerFactoryScope scope) {
SQLStatementInspector collectingStatementInspector = scope.getCollectingStatementInspector();
scope.inTransaction(
entityManager -> {
Parent parent = new Parent();
parent.setId(2L);
Child child = new Child();
parent.setChild(child);
child.setParent(parent);
entityManager.persist(parent);
}
);
collectingStatementInspector.clear();
scope.inTransaction(
entityManager -> {
Parent parent = entityManager.find( Parent.class, 2L );
assertNotNull(parent.getChild());
}
);
collectingStatementInspector
.assertNumberOfJoins(0, 1);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, CHILD_ID, 2);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, PARENT_ID, 3);
}
@Entity(name = "Parent")
static class Parent implements Serializable {
@Id
@Column(name = PARENT_ID)
private Long id;
//@PrimaryKeyJoinColumn
@OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST)
private Child child;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Child getChild() {
return child;
}
public void setChild(Child optionalData) {
this.child = optionalData;
}
}
@Entity(name = "Child")
static class Child implements Serializable {
@Id
private Long id;
// this is the thing we always allowed in the past,
// but really @PrimaryKeyJoinColumn is more elegant
@MapsId
@OneToOne
@JoinColumn(name = CHILD_ID)
private Parent parent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
}

View File

@ -0,0 +1,126 @@
/*
* 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.onetoone.pkjoincolumn;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;
import java.io.Serializable;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@JiraKey("HHH-16463")
@Jpa(annotatedClasses = {OneToOnePkJoinColumnNoMapsIdTest.Parent.class, OneToOnePkJoinColumnNoMapsIdTest.Child.class},
useCollectingStatementInspector = true)
public class OneToOnePkJoinColumnNoMapsIdTest {
static final String PARENT_ID = "parent_key";
static final String CHILD_ID = "child_key";
@Test
public void test(EntityManagerFactoryScope scope) {
SQLStatementInspector collectingStatementInspector = scope.getCollectingStatementInspector();
scope.inTransaction(
entityManager -> {
Parent parent = new Parent();
parent.setId(2L);
Child child = new Child();
child.setId(2L);
parent.setChild(child);
child.setParent(parent);
entityManager.persist(parent);
}
);
collectingStatementInspector.clear();
scope.inTransaction(
entityManager -> {
Parent parent = entityManager.find( Parent.class, 2L );
assertNotNull(parent.getChild());
}
);
collectingStatementInspector
.assertNumberOfJoins(0, 1);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, CHILD_ID, 2);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, PARENT_ID, 3);
}
@Entity(name = "Parent")
static class Parent implements Serializable {
@Id
@Column(name = PARENT_ID)
private Long id;
//@PrimaryKeyJoinColumn
@OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST)
private Child child;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Child getChild() {
return child;
}
public void setChild(Child optionalData) {
this.child = optionalData;
}
}
@Entity(name = "Child")
static class Child implements Serializable {
@Id
@Column(name = CHILD_ID)
private Long id;
// this is an alternative to @MapsId, and was
// the way to do it in older versions of JPA,
// but has the disadvantages that:
// a) you need to map the column twice, and
// b) you need to manually assign the id
@OneToOne(optional = false)
@JoinColumn(name = CHILD_ID)
private Parent parent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.onetoone.pkjoincolumn;
import java.io.Serializable;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PrimaryKeyJoinColumn;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@JiraKey("HHH-16463")
@Jpa(annotatedClasses = {OneToOnePkJoinColumnTest.Parent.class, OneToOnePkJoinColumnTest.Child.class},
useCollectingStatementInspector = true)
public class OneToOnePkJoinColumnTest {
static final String PARENT_ID = "parent_key";
static final String CHILD_ID = "child_key";
@Test
public void test(EntityManagerFactoryScope scope) {
SQLStatementInspector collectingStatementInspector = scope.getCollectingStatementInspector();
scope.inTransaction(
entityManager -> {
Parent parent = new Parent();
parent.setId(2L);
Child child = new Child();
parent.setChild(child);
child.setParent(parent);
entityManager.persist(parent);
}
);
collectingStatementInspector.clear();
scope.inTransaction(
entityManager -> {
Parent parent = entityManager.find( Parent.class, 2L );
assertNotNull(parent.getChild());
}
);
collectingStatementInspector
.assertNumberOfJoins(0, 1);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, CHILD_ID, 2);
collectingStatementInspector
.assertNumberOfOccurrenceInQueryNoSpace(0, PARENT_ID, 3);
}
@Entity(name = "Parent")
static class Parent implements Serializable {
@Id
@Column(name = PARENT_ID)
private Long id;
//@PrimaryKeyJoinColumn
@OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST)
private Child child;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Child getChild() {
return child;
}
public void setChild(Child optionalData) {
this.child = optionalData;
}
}
@Entity(name = "Child")
static class Child implements Serializable {
@Id
private Long id;
// this is the preferred way to do it I believe,
// but Hibernate never recognized this mapping
@MapsId
@OneToOne
@PrimaryKeyJoinColumn(name = CHILD_ID)
private Parent parent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
}