Hibernate creating wrong entity subtype in relationship
Asked Answered
T

3

8

I have a strange issue where hibernate does not create the expected entity type in a many to one relataionship. We have the following entities with subclass hierarchy (simplified):

@Entity
@Table(name = "A")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING, length = 1)
public abstract class A {

    @Id
    ...
    public Long getId() { ... }
    ...
}

@Entity
@DiscriminatorValue("1")
public class A1 extends A {
    ...
}

@Entity
@DiscriminatorValue("2")
public class A2 extends A {
    ...
}


@Entity
@Table(name = "B")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING, length = 1)
public abstract class B<AClass extends A> {

    protected AClass a;

    @Id
    ...
    public Long getId() { ... }
    ...

    public abstract AClass getA();
    public void setA(AClass a) { ... }
}

@Entity
@DiscriminatorValue("1")
public class B1 extends B<A1> {
    ...

    @Override
    @ManyToOne(fetch = EAGER)
    @JoinColumn(name = "A_ID")
    public A1 getA() { ... }
}

@Entity
@DiscriminatorValue("2")
public class B2 extends B<A2> {
    ...

    @Override
    @ManyToOne(fetch = EAGER)
    @JoinColumn(name = "A_ID")
    public A2 getA() { ... }
}

In persistence.xml both entities are declared in the order

A2
A1
B2
B1

Now I create instances of A1 and B1 in the DB:

A1 a1 = new A1();
entityManager.persist(a1);
B1 b1 = new B1();
b1.setA(a1);
entityManager.persist(b1);

I can see the instances are saved to the DB correctly each have ID 1, DISCRIMINATOR is also 1, A_ID in B is also 1.

When I now try to get the B (in another hibernate session):

B b = entityManager.find(B.class, 1L);

I get the exception:

org.hibernate.PropertyAccessException: Exception occurred inside getter of B
Caused by: java.lang.ClassCastException: A2 cannot be cast to A1
at B1.getA(B1.java:61)
... 108 more 

With debugging I found out that hibernate is creating the correct entity of type B1 and creates an incorrect entity of type A2 for the relationship to A. The correct type A1 is created if the order in the persistence.xml is changed. It seems like hibernate doesn't take the DISCRIMINATOR column of A table into account in this case but always creates the first subtype declared in the configuration. How can this be fixed? Is there something wrong with the annotations?

(I also had the concrete implementation of method getA() with its annotations in the supertype B at first, but this leads to similar problems.)

Telefilm answered 28/9, 2015 at 18:32 Comment(0)
L
5

With Hibernate 5.0.2.Final I was able to make your example work using @ManyToOne(..., targetEntity = A.class). I also replaced public abstract AClass getA(); with an ordinary getter.

@Entity
@Table(name = "B")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING, length = 1)
public abstract class B<AClass extends A> {
    private Long id;
    private AClass a;

    @Id
    @GeneratedValue
    public Long getId() {
        return id;
    }

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

    @ManyToOne(fetch = FetchType.EAGER, targetEntity = A.class)
    @JoinColumn(name = "A_ID")
    public AClass getA() {
        return a;
    }

    public void setA(AClass a) {
        this.a = a;
    }
}
@Entity
@DiscriminatorValue("1")
public class B1 extends B<A1> {
    // no need to override getA()
}
@Entity
@DiscriminatorValue("2")
public class B2 extends B<A2> {
    // no need to override getA()
}

I did not find anything about this behavior in the documentation. So I have only my observations:

  • Without targetEntity = A.class Hibernate didn't even query the DISCRIMINATOR column of table A when eagerly fetching rows from A along with B, like it already made a decision about actual type of A.
  • When I added targetEntity = A.class, A.DISCRIMINATOR appeared in the queries, and objects were created with the right sub-classes of class A.
Lionellionello answered 4/10, 2015 at 5:12 Comment(2)
Thanks for your answer. Yesterday I had the chance to test this and it works in the scenario of the question :)Telefilm
targetEntity = A.class did the trick. You saved my day.. Thank you :)Octavus
L
3

You are using the same join column (A_ID) in both B1 and B2 subclasses.

Use different one in each subclass:

@Entity
@DiscriminatorValue("1")
public class B1 extends B<A1> {
    @Override
    @ManyToOne(fetch = EAGER)
    @JoinColumn(name = "A1_ID")
    public A1 getA() { ... }
}

@Entity
@DiscriminatorValue("2")
public class B2 extends B<A2> {
    @Override
    @ManyToOne(fetch = EAGER)
    @JoinColumn(name = "A2_ID")
    public A2 getA() { ... }
}

Although it may make sense to reuse the column (with different columns one will anyway be null for each record depending on the subclass), it seems that Hibernate uses column names internally to uniquely identify some mapping elements within the same table. That's why it probably ignores the definition of the many-to-one mapping in B1 and uses the one from B2 for it as well (because B2 is defined before B1 in the persistence.xml).

Lassie answered 2/10, 2015 at 15:39 Comment(1)
Thanks Dragan for your answer! :) I have not had the time to test if it works because the other answer works and has some advantages: 1. Less code. 2. Less DB columns 3. We can keep the not null and foreign key constraints on the DB column.Telefilm
M
1

Late to this one but just to add that Hibernate throws the same error (returning wrong subtype) when you name the subclass fields with the same name in classes that have a relationship with them e.g.

@Entity
public abstract class Box {
    ...
}

@Entity
public class LargeBox extends Box {
    ...
}

@Entity
public class SmallBox extends Box {
    ...
}

@Entity
public class A {

    @ManyToOne
    private LargeBox box;

}

@Entity
public class B {

    @ManyToOne
    private SmallBox box;
}

The above will throw an error when reading instances of class B out of the database as box gets cast to LargeBox. Updating to:

@Entity
public class A {

    @ManyToOne
    private LargeBox largeBox;

}

@Entity
public class B {

    @ManyToOne
    private SmallBox smallBox;
}

...fixed it for me. Note: example is brief, you need to update getter signatures accordingly too.

Megathere answered 14/6, 2019 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.