Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play + Ebean + JPA: Cascade a delete on a OneToOne mapping

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?

like image 333
user1995521 Avatar asked Jun 20 '13 20:06

user1995521


1 Answers

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.

like image 196
rtruszk Avatar answered Sep 23 '22 08:09

rtruszk