jimmayhem
jimmayhem

Reputation: 405

How to solve circular reference in JPA associations?

I know this kind of question was answered many times and there are solutions to it, however, none of them worked for me. I tried @JsonIgnore, @JsonIgnoreProperties @JsonManagedReference/@JsonBackedReference, yet still the debugger shows that user has reference to authority, which has reference to user, which has reference to authority, which has reference to user ... Most importantly it doesn't throw any exceptions. However, I still wonder why does this happen, why it doesn't throw exceptions, and does it affect productivity

My entities are simple: there is a User

@Entity
@Table(name = "users_tb")
@NoArgsConstructor
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Authority> authorities;
}

and Authority

@Entity
@Table(name = "authorities_tb")
@NoArgsConstructor
@Getter
@Setter
public class Authority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

the code to retrieve users using JpaRepository<T, V>

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        var user = userRepository.findUserByUsername(username).orElseThrow(
              () -> new UsernameNotFoundException(ERR_USERNAME_NOT_FOUND));

        return new CustomUserDetails(user);
    }

The debugger output state before return from loadUserByUsername:

user = {User@10781}
    > id = {Long@10784}
    > username = "John"
    > password = "$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG" 
    > authorities = {PersistentBag@10788} size = 2
        > 0 = {Authority@10818}
            > id = {Long@10784}
            > name = "READ"
            > user = {User@10784}
                > id = {User@10784}
                > username = "John"
                > password = "$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG" 
                > authorities = {PersistentBag@10788} size = 2
                    > 0 = {Authority@10818}
                    ...

Upvotes: 4

Views: 3925

Answers (4)

Darkie
Darkie

Reputation: 1

This is probably a late response, but is still useful for late comers. The solution is to make use of Access Level from the official documentation.

@Entity
@Table(name = "users_tb")
@NoArgsConstructor
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;
    
    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Authority> authorities;
}

and similarly,

@Entity
@Table(name = "authorities_tb")
@NoArgsConstructor
@Getter
@Setter
public class Authority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Setter(AccessLevel.NONE)
    @Getter(AccessLevel.NONE)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

Upvotes: 0

Jens Schauder
Jens Schauder

Reputation: 81907

Circular dependencies aren't a problem in themselves with JPA.

There are two potential problems with them:

From a software design perspective circular dependencies create a cluster of classes that you can't easily break up. You can easily get rid of them in your case by making the relationship a unidirectional one and replace the other direction by a query, if you really have to. Is it worth it in your case? It depends how closely your two entities are really related. I'd try to avoid bidirectional relationships, because it is easy to make mistakes, like not keeping both sides of the relationship in sync. But in most cases I wouldn't sweat it. Most software as way more serious design issues.

The other problem occurs when something tries to navigate this loop until its end, which obviously doesn't work. The typical scenarios are:

  • rendering it into JSON (or XML). This is what @JsonIgnore & Co takes care of by not including properties in the JSON.
  • equals, hashCode, toString are often implemented to call the respective methods of all referenced objects. Just as the JSON rendering this will lead to stack overflows. So make sure to break the cycle in these methods as well.

JPA itself doesn't have a problem with cycles because it will look up entities in the first level cache. Assuming you load an Authority and everything is eagerly loaded, JPA will put it in the first level cache, before checking the referenced user id. If it is present in the cache it will use that instance. If not it will load it from the database, put it in the cache and then check for the authorities ids in the cache. It will use the ones found and load the rest. For those it will again check the user id, but those are the user we just loaded, so it is certainly in the cache. Therefore JPA is done and won't get lost in a cycle. It will just skip the annotated

Upvotes: 2

Enfield Li
Enfield Li

Reputation: 2530

You can simply annotate the duplicated field with @ToString.Exclude

In you case:

@Data // this includes getter/setter and toString
@Entity
@Table(name = "users_tb")
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @ToString.Exclude
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Authority> authorities;
}

@Data
@Entity
@Table(name = "authorities_tb")
@NoArgsConstructor
public class Authority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ToString.Exclude
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

More info: Lombok @Data and @ToString.Exclude

Upvotes: 2

samivic
samivic

Reputation: 1310

Try not to use the Lombok annotation @Getter and @Setter. Then generate manually getters and setters and use @JsonIgnore on the class member field and the getter, and @JsonProperty on the setter.

@JsonIgnore
private List<Authority> authorities;

@JsonIgnore
// Getter for authorities

@JsonProperty
// Setter for authorities

Upvotes: 0

Related Questions