Two relationships to single-table inheritance sub classes failing with Hibernate 5.3.*

Hello all.

I have a simple single table mapping, a ship with images and documents (both sub classes of an attachment) attached to it:

@Entity
@Table(name = "SHIPS")
public class Ship implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@Column(name = "ID")
	private Long id;

	@Basic(optional = false)
	@Column(name = "NAME")
	private String name;

	@OneToMany(mappedBy = "ship", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	// @Where(clause = "DISCR = 'I'")
	private List<Image> images = new ArrayList<>();

	@OneToMany(mappedBy = "ship", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
	// @Where(clause = "DISCR = 'D'")
	private List<Document> documents = new ArrayList<>();

	public Ship() {}

	public Ship(Long id, String name) {
		this.id = id;
		this.name = name;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List<Image> getImages() {
		return images;
	}

	public void setImages(List<Image> images) {
		this.images = images;
	}

	public List<Document> getDocuments() {
		return documents;
	}

	public void setDocuments(List<Document> documents) {
		this.documents = documents;
	}
}
@Entity
@Table(name = "ATTACHMENTS")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCR")
public abstract class BaseAttachment implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@Column(name = "ID")
	protected Long id;

	@Basic(optional = false)
	@Column(name = "DISCR", insertable = false, updatable = false)
	protected String discr;

	@Basic(optional = false)
	@Column(name = "FILE_NAME")
	protected String fileName;

//	@Lob
//	@Basic(optional = false)
//	@Column(name = "ORIGINAL_DATA")
//	protected byte[] originalData;

	@ManyToOne(optional = false, fetch = FetchType.LAZY)
	@JoinColumn(name = "SHIP_ID", referencedColumnName = "ID")
	protected Ship ship;

	public BaseAttachment() {}

	public BaseAttachment(Long id, String fileName) {
		this.id = id;
		this.fileName = fileName;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getDiscr() {
		return discr;
	}

	public void setDiscr(String discr) {
		this.discr = discr;
	}

	public String getFileName() {
		return fileName;
	}

	public void setFileName(String fileName) {
		this.fileName = fileName;
	}

//	public byte[] getOriginalData() {
//		return originalData;
//	}
//
//	public void setOriginalData(byte[] originalData) {
//		this.originalData = originalData;
//	}

	public Ship getShip() {
		return ship;
	}

	public void setShip(Ship ship) {
		this.ship = ship;
	}
}
@Entity
@DiscriminatorValue("I")
public class Image extends BaseAttachment {

	private static final long serialVersionUID = 1L;

	@Column(name = "ALTERNATIVE_TEXT")
	private String alternativeText;

	public Image() {}

	public Image(Long id, String fileName) {
		super(id, fileName);
	}

	public String getAlternativeText() {
		return alternativeText;
	}

	public void setAlternativeText(String alternativeText) {
		this.alternativeText = alternativeText;
	}
}
@Entity
@DiscriminatorValue("D")
public class Document extends BaseAttachment {

	private static final long serialVersionUID = 1L;

	public Document() {}

	public Document(Long id, String fileName) {
		super(id, fileName);
	}
}

I created a test case:

@TestForIssue( jiraKey = "HHH-99999")
public class TwoRelationshipsToSubclassTest extends BaseEntityManagerFunctionalTestCase {

    @Override
    public Class<?>[] getAnnotatedClasses() {
        return new Class<?>[]{Ship.class, BaseAttachment.class, Image.class, Document.class};
    }

    @Override
    protected void addConfigOptions(Map options) {
        options.put( AvailableSettings.USE_SECOND_LEVEL_CACHE, "false" );
        options.put( AvailableSettings.SHOW_SQL, "true" );
        options.put( AvailableSettings.FORMAT_SQL, "true" );
        options.put( AvailableSettings.ENABLE_LAZY_LOAD_NO_TRANS, "true" );
    }

    @Before
    public void prepare() {
        doInJPA( this::entityManagerFactory, em -> {
            Ship ship = new Ship(1L, "Titanic");
            // images
            Image image1 = new Image(1L, "abc.png");
            image1.setShip(ship);
            Image image2 = new Image(2L, "def.jpg");
            image2.setShip(ship);
            // documents
            Document document1 = new Document(3L, "ghi.docx");
            document1.setShip(ship);
            Document document2 = new Document(4L, "jkl.pdf");
            document2.setShip(ship);
            Document document3 = new Document(5L, "mno.txt");
            document3.setShip(ship);
            // images relationship
            ship.getImages().add(image1);
            ship.getImages().add(image2);
            // documents relationship
            ship.getDocuments().add(document1);
            ship.getDocuments().add(document2);
            ship.getDocuments().add(document3);
            // persist
            em.persist( ship );
        });
    }

    /**
     * Use --debug to see the SQL being run + the sysouts.
     */
    @Test
    public void test() {
        doInJPA( this::entityManagerFactory, em -> {
            Ship ship = em.find( Ship.class, 1L );
            Assert.assertNotNull("Ship not found!", ship);
            List<Image> images = ship.getImages();
            Assert.assertEquals(2, images.size());
            List<Document> documents = ship.getDocuments();
            Assert.assertEquals(3, documents.size());
        } );
    }
}

Fails with:


expected:<2> but was:<5>
Expected :2
Actual   :5
<Click to see difference>

java.lang.AssertionError: expected:<2> but was:<5>
	at org.junit.Assert.fail(Assert.java:88)
	at org.junit.Assert.failNotEquals(Assert.java:834)
	at org.junit.Assert.assertEquals(Assert.java:645)
	at org.junit.Assert.assertEquals(Assert.java:631)
	at org.hibernate.jpa.test.inheritance.singletable.TwoRelationshipsToSubclassTest.lambda$test$3(TwoRelationshipsToSubclassTest.java:68)
	at org.hibernate.testing.transaction.TransactionUtil.doInJPA(TransactionUtil.java:206)
	at org.hibernate.testing.transaction.TransactionUtil.doInJPA(TransactionUtil.java:247)
	at org.hibernate.jpa.test.inheritance.singletable.TwoRelationshipsToSubclassTest.test(TwoRelationshipsToSubclassTest.java:64)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:564)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.hibernate.testing.junit4.ExtendedFrameworkMethod.invokeExplosively(ExtendedFrameworkMethod.java:45)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:298)
	at org.junit.internal.runners.statements.FailOnTimeout$CallableStatement.call(FailOnTimeout.java:292)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.lang.Thread.run(Thread.java:832)


org.hibernate.jpa.test.inheritance.singletable.TwoRelationshipsToSubclassTest > test FAILED
    java.lang.AssertionError at TwoRelationshipsToSubclassTest.java:68
1 test completed, 1 failed
> Task :hibernate-core:test FAILED
> Task :hibernate-core:buildDashboard UP-TO-DATE
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':hibernate-core:test'.
> There were failing tests. See the report at: file:///C:/projects/hibernate-orm/hibernate-core/target/reports/tests/test/index.html
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org

What happens here (Hibernate 5.3.25.Final branch) is that the subclass instances for ALL types are fetched, which is wrong.

EDIT: this test fails with all 5.x branches!

I’ve also tested this with the main branch (Hibernate 6.0.0.RC1) where this test is OK.

Does anyone happen to know which issue HHH-??? fixed this?

We currently have to add @Where(clause = "DISCR = 'D'") and @Where(clause = "DISCR = 'I'") to get around this, which is… um… kinda stoopid, ya know.

Thanks
Karsten

I think you can achieve the same by annotating @DiscriminatorOptions(force = true) on the entity type. I don’t know which HHH fixed this or if we even tagged a commit properly. We just figured that this was wrong in Hibernate 5.x so we fixed it at some point.

Thanks for the answer.

Are there any plans to fix this for the 5.x branches?

We frequently have SINGLE_TABLE mappings and relationships to sub classes, so this would be a pity not to be backported.

I could create an HHH-xxxxx issue and add the test case to any 5.x branch if desired, but someone/you would have to find the commits etc. to fix this (if possible).

Unless someone from the community steps up to fix this, I don’t think this will be backported as the Hibernate Team will not be investing a lot into 5.x anymore.