Play + Ebean + JPA: Cascade a delete on a OneToOne mapping
Asked Answered
G

1

6

This is related to this question, but the example below is shorter, so I figured another question on this would make sense.

I have two entities, A and B, in a one-to-one relationship. For an A, a B is optional, and every B must have an A. I want to cascade deletes from A to B. Here's my first attempt:

@Entity
public class A extends Model {

    @Id
    private Long id;

    @OneToOne(optional = true, mappedBy = "a", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private B b;

}

@Entity
public class B extends Model {

    @Id
    private Long id;

    @OneToOne(optional = false)
    private A a;

}

However, it seems like Ebean ignores the "optional" annotation, because when I execute a find for a saved A with id 1, the following SQL is executed:

select t0.id c0, t1.id c1 
from a t0
join b t1 on t1.a_id = t0.id 
where t0.id = 1  

In other words, it does an inner instead of a left join, which causes the find to fail when there's no associated B. I've tried various combinations of @JoinColumn, etc. to no avail. The only somewhat satisfactory workaround I've found is to model A-to-B as a "fake" one-to-many relationship. Is there a better solution? Is this a bug or is it a known/stated limitation of Ebean?

Guv answered 20/6, 2013 at 20:29 Comment(0)
T
2

I have found the solution. I switched the direction of this mapping. So I removed mappedBy = "a" from A class and added mappedBy = "b" in B class.
So the code now looks this way:

@Entity
public class A extends Model {

    @Id
    private Long id;

    @OneToOne(optional = true, cascade = CascadeType.REMOVE, orphanRemoval = true)
    private B b;

 ...
}


@Entity
public class B extends Model {

    @Id
    private Long id;

    @OneToOne(optional = false, mappedBy = "b")
    private A a;

    private String name;

    ...
}

I added name field in B class to make this test more interesting.

My test medhod:

@Test
public void abTest () {
    FakeApplication app = Helpers.fakeApplication(Helpers.inMemoryDatabase());
    Helpers.start(app);

    A a = new A();
    B b = new B();
    a.setId(1L);
    b.setId(2L);
    a.setB(b);
    b.setA(a);
    b.setName("bbb");

    Ebean.save(b);
    Ebean.save(a);

    A fa = Ebean.find(A.class, 1L);
    System.out.println("a.id: "+fa.getId());
    System.out.println("a.b.id: "+fa.getB());
    System.out.println("a.b.name: "+fa.getB().getName());

    A a1 = new A();
    a1.setId(3L);
    Ebean.save(a1);
    A fa1 = Ebean.find(A.class, 3L);
    System.out.println("a1.id: "+fa1.getId());
    System.out.println("a1.b.id: "+fa1.getB());

    B fb = Ebean.find(B.class, 2L);
    System.out.println("b.id: "+fb.getId());
    System.out.println("b.name: "+fb.getName());
    System.out.println("b.a.id: "+fb.getA().getId());
}

And the result of this test is:

[debug] c.j.b.PreparedStatementHandle - insert into b (id, name) values (2,'bbb')
[debug] c.j.b.PreparedStatementHandle - insert into a (id, b_id) values (1,2)
[debug] c.j.b.PreparedStatementHandle - select t0.id c0, t0.b_id c1 from a t0 where t0.id = 1
a.id: 1
a.b.id: models.B@2
[debug] c.j.b.PreparedStatementHandle - select t0.id c0, t0.name c1, t1.id c2 from b t0 left outer join a t1 on t1.b_id = t0.id  where t0.id = 2
a.b.name: bbb
[debug] c.j.b.PreparedStatementHandle - insert into a (id, b_id) values (3,'[SQL NULL of type -5]')
[debug] c.j.b.PreparedStatementHandle - select t0.id c0, t0.b_id c1 from a t0 where t0.id = 3
a1.id: 3
a1.b.id: null
[debug] c.j.b.PreparedStatementHandle - select t0.id c0, t0.name c1, t1.id c2 from b t0 left outer join a t1 on t1.b_id = t0.id  where t0.id = 2
b.id: 2
b.name: bbb
b.a.id: 1

So this code works good no matter if A.b is null or not. As we can see in log there is now left outer join instead of join.

Tyranny answered 2/12, 2014 at 18:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.