0

When attempting to delete an entity, a StackOverflowError occurs. The only time this happens is when I have a composite key containing 2 foreign key references. I created a minimal example below:

First object containing a @OneToMany collection:

@Entity
public class Foo {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @OneToMany(mappedBy = "id.foo", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Baz> baz;

    // Getters and setters omitted.
}

Second object containing an @OneToMany reference:

@Entity
public class Bar {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @OneToMany(mappedBy = "id.bar", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Baz> baz;

    // Getters and setters omitted.
}

Object containing both of the objects above (Foo and Bar) as a composite key:

@Entity
public class Baz {

    @EmbeddedId
    private BazId id;

    // Getters and setters omitted.
}

And finally, the composite key class for Baz:

@Embeddable
public class BazId implements Serializable {

    private static final long serialVersionUID = -8869241450494423817L;

    @ManyToOne
    private Foo foo;

    @ManyToOne
    private Bar bar;

    // Getters and setters omitted
}

When I perform the test below, I get a java.lang.StackOverflowError:

// Spring Data JPA Repositories (extend CrudRepository)
@Autowired private FooRepository fooRepository;
@Autowired private BarRepository barRepository;
@Autowired private BazRepository bazRepository;

@Test
public void test() {
    final Foo foo = new Foo();
    fooRepository.save(foo);

    final Bar bar = new Bar();
    barRepository.save(bar);

    final Baz baz = new Baz(new BazId(foo, bar));
    bazRepository.save(baz);

    foo.getBaz().add(baz);
    bar.getBaz().add(baz);

    // These two deletes cause the StackOverflowError
    fooRepository.delete(foo);
    barRepository.delete(bar);
}

What I've tried:

  • Overriding equals(Object o), toString(), and hashCode().

I'm not sure what else to try other than redoing my mapping entirely. I need the mapping to be setup this way as in my real project, I have a User following a Business so this needs to be a composite key between two entities. Why is this causing infinite recursion?

EDIT

Stack trace if it helps (part of it. Whole thing wont fit):

java.lang.StackOverflowError
    at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:131)
    at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:108)
    at org.apache.tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.java:81)
    at com.sun.proxy.$Proxy73.prepareStatement(Unknown Source)
    at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$5.doPrepare(StatementPreparerImpl.java:146)
    at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$StatementPreparationTemplate.prepareStatement(StatementPreparerImpl.java:172)
    at org.hibernate.engine.jdbc.internal.StatementPreparerImpl.prepareQueryStatement(StatementPreparerImpl.java:148)
    at org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:1929)
    at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1898)
    at org.hibernate.loader.Loader.executeQueryStatement(Loader.java:1876)
    at org.hibernate.loader.Loader.doQuery(Loader.java:919)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:336)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:306)
    at org.hibernate.loader.Loader.loadEntity(Loader.java:2204)
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:60)
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:50)
    at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3956)
    at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508)
    at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478)
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219)
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278)
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121)
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
    at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1129)
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1022)
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:632)
    at org.hibernate.type.EntityType.resolve(EntityType.java:424)
    at org.hibernate.type.ComponentType.resolve(ComponentType.java:687)
    at org.hibernate.loader.Loader.extractKeysFromResultSet(Loader.java:848)
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:714)
    at org.hibernate.loader.Loader.processResultSet(Loader.java:972)
    at org.hibernate.loader.Loader.doQuery(Loader.java:930)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:336)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:306)
    at org.hibernate.loader.Loader.loadEntity(Loader.java:2204)
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:60)
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:50)
    at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3956)
    at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:508)
    at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:478)
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:219)
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278)
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121)
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
    at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1129)
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1022)
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:632)
    at org.hibernate.type.EntityType.resolve(EntityType.java:424)
    at org.hibernate.type.ComponentType.resolve(ComponentType.java:687)
    at org.hibernate.loader.Loader.extractKeysFromResultSet(Loader.java:848)
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:714)
    at org.hibernate.loader.Loader.processResultSet(Loader.java:972)
    at org.hibernate.loader.Loader.doQuery(Loader.java:930)
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:336)
Jake Miller
  • 2,432
  • 2
  • 24
  • 39
  • Are you sure you need `cascade = CascadeType.ALL` right after `mappedBy = "id.bar"` ? – Mike Nakis Feb 22 '17 at 22:15
  • Stacktrace makes it look like you are having a recursive loading back and forth. Try lazy? – Christopher Oezbek Feb 22 '17 at 22:16
  • Related: http://stackoverflow.com/questions/13027214/jpa-manytoone-with-cascadetype-all – Christopher Oezbek Feb 22 '17 at 22:17
  • @MikeNakis Replace `Foo` with `User`, `Bar` with `Business` and `Baz` with `Follow`. If a `User` is deleted, then the `Follow` association should be deleted. If the `Business` is deleted, then the `Follow` association should be deleted as well. That's why both `Foo` and `Bar` are cascading. – Jake Miller Feb 22 '17 at 22:19
  • @ChristopherOezbek So the problem is, when I go to delete `Foo`, it tries to delete `Baz` as well? And inside `Baz`, there's `Bar`. So it checks `Bar`'s relationships and finds a `OneToMany` association on `Baz`. So it checks `Baz` and finds `Foo`, and then the cycle repeats until a `StackOverflowError` occurs? Or am I not understanding it properly? – Jake Miller Feb 22 '17 at 22:21
  • @JakeMiller: I think this is what is happening... Probably the cascade is at fault. – Christopher Oezbek Feb 22 '17 at 22:29
  • @ChristopherOezbek Check my other comment regarding my actual set up in my real project (User, Business and Follow). How would you suggest I set that up? Remove the cascading and simply delete everything manually? – Jake Miller Feb 22 '17 at 22:31
  • This is not a valid use of an Embeddable. Check the JPA spec, section 11.1.17 `Relationship mappings defined within an embedded id class are not supported` – Will Dazey Feb 23 '17 at 00:12

2 Answers2

1

Having cascade = CascadeType.ALL on the Baz collection within Foo AND Bar causes infinite recursion when attempting to delete. Remove cascading on both of the collections and delete Baz relationships manually instead of cascading to fix this issue.

Jake Miller
  • 2,432
  • 2
  • 24
  • 39
1

Tho Hibernate does claim support for directly modeling ManyToOne relationship mappings in the PK class, whether EmbeddedId or IdClass. I would suggest not designing to a specific persistence provider's implementation. The following example is designed to the JPA 2.0/2.1 specification and offers good portability.

Foo

@Entity
public class Foo {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @OneToMany(mappedBy = "foo", cascade = CascadeType.ALL)
    private List<Baz> baz;
}

Bar

@Entity
public class Bar {
    @Id @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @OneToMany(mappedBy = "bar", cascade = CascadeType.ALL)
    private List<Baz> baz;
}

Baz

@Entity
public class Baz {
    @EmbeddedId
    private BazId id;

    @ManyToOne 
    @MapsId("fooid")
    private Foo foo;

    @ManyToOne 
    @MapsId("barid")
    private Bar bar;
}

BazId

@Embeddable
public class BazId implements Serializable {

    private long fooid;

    private long barid;
}
Will Dazey
  • 253
  • 2
  • 13
  • It's supported in Hibernate. That's not the problem. But do you recommend I approach it this way in order to be JPA 2.0 compliant? Approaching it this way will allow me to switch ORM vendors without having to overhaul my mapping right? – Jake Miller Feb 23 '17 at 02:30
  • I was not aware that Hibernate did in fact support this, but I reviewed the Hibernate doc and they do note that Hibernate supports directly modeling the ManyToOne in the PK class. I will update my answer to remove the spec quote since it does not apply for this provider. Tho, I do stand by my suggestion. – Will Dazey Feb 23 '17 at 04:28