Reputation: 405
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
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
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:
@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
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
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