1
votes

I have an issue with a manytomany bi-directional JPA mapping that is causing me a stack overflow error While I have a total of 5 entities in my application I believe the issue is only related to 2 of them which are described below for simplicity.

Entities:

Application

User

An Application can have many developers

A User can develop many applications

Here is how they are mapped:

//Application Entity:

@ManyToMany
private List<Users> users

//User Entity

@ManyToMany(mappedBy = "users", fetch = FetchType.LAZY)

private List <Applications> applications;

This results in 3 tables being created in the database:

APPLICATIONS < ID APPNAME

APPLICATION_USERS < Join Table contains USERS_ID APPLICATIONS_ID

USERS < ID USER NAME

It is a circular reference that keeps running until there is a stack overflow.

The application works fine when it is first deployed with empty tables. A user registers for an application and this creates a row in the APPLICATIONS table (if the Application does not exist) A row is also created in the USERS table (if the user does not exist) and the join table APPLICATION_USERS is populated with the ID from the APPLICATION Table called APPLICATIONS_ID and the ID from the USERS table called USERS_ID.

You can add as many applications or users as you wish and the application works perfectly. I have verified that data is being loaded and persisted into the 3 tables exactly as expected Here is an example of the data in the tables after a user registers an Application:

APPLICATIONS

ID 51

APPLICATION_USERS
USERS_ID APPLICATIONS_ID

1       51

USERS
ID 1

Now when the server is stopped and restarted or when the application is re-deployed using create-tables (vs drop-and-create-tables) (and data is present in the tables) then I get a stack overflow at each entities toString() function. I have run this in debug with breakpoints on the Applications toString() function and on the Users toString() function and I can click resume and watch each toString() function get called over and over until the stack overflow results.

Here is the console log:

(Entity query being executed)

[EL Fine]: 2014-01-21 14:48:44.383--ServerSession(1615948530)--Connection(49767657)--Thread(Thread[http-bio-8080-exec-9,5,main])--SELECT t1.ID, t1.APPIDENTIFIER, t1.DATECREATED, t1.DATEMODIFIED, t1.DEVICETYPE FROM APPLICATIONS_Users t0, APPLICATIONS t1 WHERE ((t0.users_ID = ?) AND (t1.ID = t0.applications_ID))

(second entity query is invoked)

[EL Fine]: 2014-01-21 14:50:02.444--ServerSession(1615948530)--Connection(1871047709)--Thread(Thread[http-bio-8080-exec-9,5,main])--SELECT t1.ID, t1.DATECREATED, t1.DATEMODIFIED, t1.EMAIL, t1.FIRSTNAME, t1.FULLNAME, t1.LASTLOGINDATE, t1.LASTNAME, t1.USERNAME FROM APPLICATIONS_Users t0, Users t1 WHERE ((t0.applications_ID = ?) AND (t1.ID = t0.users_ID))

[EL Finest]: 2014-01-21 14:50:02.471--ServerSession(1615948530)--Connection(1601422824)--Thread(Thread[http-bio-8080-exec-9,5,main])--Connection released to connection pool [read].

java.lang.StackOverflowError

   at java.util.Vector.get(Vector.java:693)

   at java.util.AbstractList$Itr.next(AbstractList.java:345)

   at java.util.AbstractCollection.toString(AbstractCollection.java:421)

   at java.util.Vector.toString(Vector.java:940)

   at org.eclipse.persistence.indirection.IndirectList.toString(IndirectList.java:797)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at com.sap.crashlogserver.dao.entities.Applications.toString(Applications.java:150)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at java.util.AbstractCollection.toString(AbstractCollection.java:422)

   at java.util.Vector.toString(Vector.java:940)

   at org.eclipse.persistence.indirection.IndirectList.toString(IndirectList.java:797)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at com.sap.crashlogserver.dao.entities.Users.toString(Users.java:168)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at java.util.AbstractCollection.toString(AbstractCollection.java:422)

   at java.util.Vector.toString(Vector.java:940)

   at org.eclipse.persistence.indirection.IndirectList.toString(IndirectList.java:797)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at com.sap.crashlogserver.dao.entities.Applications.toString(Applications.java:150)

   at java.lang.String.valueOf(String.java:2826)

   at java.lang.StringBuilder.append(StringBuilder.java:115)

   at java.util.AbstractCollection.toString(AbstractCollection.java:422)

   at java.util.Vector.toString(Vector.java:940)

Based on a number of threads I have read I tried: 1. Reversing the mappings, 2. Adding @JsonIgnore to some of the entity fields 3. Using fetch = FetchType.LAZY

and many other config tweaks but none of them resolved this issue. Some of the suggestions like using transient fields. I am not sure this is supported in my JPA implementation of eclipselink.

I also read a thread suggestion me to implement gson.ExclusionStrategy. Have not tried this yet.

So that's the story. I am a newbie at Java and JPA. This is a very difficult issue for me to figure out. Any suggestions you may have to help me to resolve it would be greatly appreciated.

3

3 Answers

1
votes

Maybe it would help someone.

I got same problem. I use annotation @InvisibleJson with custom ExclusionStrategy to hide these fields

public class SafeDataExclusionStrategy implements ExclusionStrategy {

    @Override
    public boolean shouldSkipClass(Class<?> c) {
        return false;
    }

    @Override
    public boolean shouldSkipField(FieldAttributes c) {
        return c.getAnnotation(InvisibleJson.class) != null;
    }

}

and

@Retention(RetentionPolicy.RUNTIME)
@Target(value = ElementType.FIELD)
public @interface InvisibleJson {}

like

@InvisibleJson
@ManyToMany(mappedBy="roles")
public List<Staff> staff;

so GSON doesn't dig inside staff object.

0
votes

Your objects toString method is recursively calling toString on the entire model. The stack doesn't show why it is being called at first, but it might be due to some logging setting. Fix your toString method and turn off logging,

0
votes

Very simple solution, use the annotation @Expose over all the variables and lists you wish to allow GSON serialize/deserialise.

A simple example that would basically only allow GSON to use name:

@Expose
private String name;

@ManyToMany
private List<Users> users 

Then where you are using GsonBuilder, do the following:

GsonBuilder builder = new GsonBuilder().disableHtmlEscaping();
builder.excludeFieldsWithoutExposeAnnotation();  //<-- This tells GSON look for @Expose
gson = builder.create();

Also you could put @Expose over the list but you would need go into the users entity and make sure to place @Expose on the stuff in that entity for when GSON digs into it.