I am new to spring boot and Java and I am trying to establish a follower-following relationship by self joining my tb_user
table.
However I am getting a stackoverflow error. I tried to solve it but using @JsonIgnore
and @JsonIdentityInfo
like what many answers suggested but it doesn't seem to work.
Below is my code:
User.java
package com.codaholic.shop.models.security.user;
import com.codaholic.shop.models.Article;
import com.fasterxml.jackson.annotation.*;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Set;
import java.util.UUID;
import java.util.List;
import java.util.Collection;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "tb_user")
public class User implements UserDetails{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String firstName;
private String lastName;
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
@Builder.Default
private UserStatus status = UserStatus.ACTIVE;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
@Override
public String getUsername() {
return email;
}
@Override
public String getPassword(){
return password;
}
@Override
public boolean isAccountNonExpired(){
return true;
}
@Override
public boolean isAccountNonLocked(){
return true;
}
@Override
public boolean isCredentialsNonExpired(){
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@OneToMany
private List<Article> articles;
@ManyToMany(
cascade={CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH},
fetch = FetchType.LAZY)
@JoinTable(
name = "tb_follower_following",
joinColumns = @JoinColumn(name="followee_id", referencedColumnName = "id"), // the owning side of the r/s is this user
inverseJoinColumns = @JoinColumn(name="follower_id", referencedColumnName = "id")
)
@JsonIgnore
private Set<User> followers;
@ManyToMany(mappedBy="followers")
@JsonIgnore
private Set<User> following;
}
UserRepository.java
package com.codaholic.shop.repository;
import com.codaholic.shop.models.security.user.User;
import com.codaholic.shop.models.security.user.UserStatus;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
import java.util.UUID;
public interface UserRepository extends JpaRepository<User, UUID> {
Optional<User> findByEmail(String email);
Optional<User> findById(UUID id);
Optional<User> findByIdAndStatus(@Param("id") UUID id, UserStatus status);
}
FollowingService.java
package com.codaholic.shop.services;
import com.codaholic.shop.dto.FollowingResponseDTO;
import com.codaholic.shop.exceptions.ResourceNotFoundException;
import com.codaholic.shop.models.security.user.FollowingRequest;
import com.codaholic.shop.models.security.user.User;
import com.codaholic.shop.models.security.user.UserStatus;
import com.codaholic.shop.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class FollowingService {
private final UserRepository userRepository;
FollowingService(UserRepository userRepository){
this.userRepository = userRepository;
}
public FollowingResponseDTO createFollowing(FollowingRequest request) throws ResourceNotFoundException {
UUID followerId = request.getFollowerId();
User follower = userRepository.findByIdAndStatus(followerId, UserStatus.ACTIVE)
.orElseThrow(
()-> new ResourceNotFoundException(String.format("User %s is not found", followerId))
);
UUID followingId = request.getFollowingId();
User followee = userRepository.findByIdAndStatus(followingId, UserStatus.ACTIVE)
.orElseThrow(
() -> new ResourceNotFoundException(String.format("User %s is not found", followingId))
);
// Next create a new relation by setting their status to ACTIVE
follower.getFollowing().add(followee);
followee.getFollowers().add(follower);
userRepository.save(follower);
userRepository.save(followee);
FollowingResponseDTO followingResult = new FollowingResponseDTO();
followingResult.setFollowerId(followerId);
followingResult.setFollowingId(followingId);
followingResult.setSuccess(true);
return followingResult;
}
}
FollowingController
package com.codaholic.shop.controllers;
import com.codaholic.shop.dto.FollowingResponseDTO;
import com.codaholic.shop.exceptions.ResourceNotFoundException;
import com.codaholic.shop.models.security.user.FollowingRequest;
import com.codaholic.shop.services.FollowingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/following")
public class FollowingController {
private final FollowingService followingService;
@Autowired
public FollowingController(FollowingService followingService) {
this.followingService = followingService;
}
@PostMapping
public ResponseEntity<FollowingResponseDTO> createFollowing(@RequestBody FollowingRequest request) throws ResourceNotFoundException {
FollowingResponseDTO result = followingService.createFollowing(request);
return ResponseEntity.ok().body(result);
}
}
Stack trace:
2023-09-21T23:41:46.787+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
u1_0.id,
u1_0.email,
u1_0.first_name,
u1_0.last_name,
u1_0.password,
u1_0.role,
u1_0.status
from
tb_user u1_0
where
u1_0.id=?
and u1_0.status=?
Hibernate:
select
u1_0.id,
u1_0.email,
u1_0.first_name,
u1_0.last_name,
u1_0.password,
u1_0.role,
u1_0.status
from
tb_user u1_0
where
u1_0.id=?
and u1_0.status=?
2023-09-21T23:41:46.791+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
u1_0.id,
u1_0.email,
u1_0.first_name,
u1_0.last_name,
u1_0.password,
u1_0.role,
u1_0.status
from
tb_user u1_0
where
u1_0.id=?
and u1_0.status=?
Hibernate:
select
u1_0.id,
u1_0.email,
u1_0.first_name,
u1_0.last_name,
u1_0.password,
u1_0.role,
u1_0.status
from
tb_user u1_0
where
u1_0.id=?
and u1_0.status=?
2023-09-21T23:41:46.794+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
f1_0.follower_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.followee_id
where
f1_0.follower_id=?
Hibernate:
select
f1_0.follower_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.followee_id
where
f1_0.follower_id=?
2023-09-21T23:41:46.796+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
f1_0.followee_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.follower_id
where
f1_0.followee_id=?
Hibernate:
select
f1_0.followee_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.follower_id
where
f1_0.followee_id=?
2023-09-21T23:41:46.798+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
f1_0.follower_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.followee_id
where
f1_0.follower_id=?
Hibernate:
select
f1_0.follower_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.followee_id
where
f1_0.follower_id=?
2023-09-21T23:41:46.800+08:00 DEBUG 75113 --- [nio-3000-exec-2] org.hibernate.SQL :
select
f1_0.followee_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.follower_id
where
f1_0.followee_id=?
Hibernate:
select
f1_0.followee_id,
f1_1.id,
f1_1.email,
f1_1.first_name,
f1_1.last_name,
f1_1.password,
f1_1.role,
f1_1.status
from
tb_follower_following f1_0
join
tb_user f1_1
on f1_1.id=f1_0.follower_id
where
f1_0.followee_id=?
2023-09-21T23:41:46.810+08:00 DEBUG 75113 --- [nio-3000-exec-2] o.s.web.servlet.DispatcherServlet : Failed to complete request: jakarta.servlet.ServletException: Handler dispatch failed: java.lang.StackOverflowError
2023-09-21T23:41:46.811+08:00 ERROR 75113 --- [nio-3000-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed: java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at org.hibernate.collection.spi.PersistentSet.hashCode(PersistentSet.java:413) ~[hibernate-core-6.2.7.Final.jar:6.2.7.Final]
at com.codaholic.shop.models.security.user.User.hashCode(User.java:16) ~[main/:na]
at java.base/java.util.AbstractSet.hashCode(AbstractSet.java:124) ~[na:na]
at org.hibernate.collection.spi.PersistentSet.hashCode(PersistentSet.java:413) ~[hibernate-core-6.2.7.Final.jar:6.2.7.Final]
at com.codaholic.shop.models.security.user.User.hashCode(User.java:16) ~[main/:na]
at java.base/java.util.AbstractSet.hashCode(AbstractSet.java:124) ~[na:na]
at org.hibernate.collection.spi.PersistentSet.hashCode(PersistentSet.java:413) ~[hibernate-core-6.2.7.Final.jar:6.2.7.Final]
Edit: As per @RomanC's explanation, I have since removed adding one side of the list and now the stackoverflow is resolved. i.e from
follower.getFollowing().add(followee);
followee.getFollowers().add(follower);
userRepository.save(follower);
userRepository.save(followee);
to
follower.getFollowing().add(followee);
userRepository.save(follower);
Similarly, I also made amendments to remove the mappedBy
as suggested since both the follower and following can "own" the relationship.
@ManyToMany(
cascade={CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH},
fetch = FetchType.LAZY)
@JoinTable(
name = "tb_follower_following",
joinColumns = @JoinColumn(name="following_id", referencedColumnName = "id"), // ie. the FK is the follower_id
inverseJoinColumns = @JoinColumn(name="follower_id", referencedColumnName = "id")
)
private Set<User> followers;
@ManyToMany
@JoinTable(
name = "tb_follower_following",
joinColumns = @JoinColumn(name="follower_id", referencedColumnName = "id"), // ie. the FK is the following_id
inverseJoinColumns = @JoinColumn(name="following_id", referencedColumnName = "id")
)
private Set<User> following;
Also realized that the @JsonIgnore was no longer necessary.
Your following attribute is mapped to followers, end the end they have the same value. But since you do this:
follower.getFollowing().add(followee);
followee.getFollowers().add(follower);
You add the followee as a following and the follower as a follower. But the follower already contains a followee. So when you will try to load each, It will try to load the other and so on until StackOverflow.
According to me, you don't really need to have both following and followers list. According to your design, if a user is your follower, you are also his follower. So you just have to keep one list. But if following and followers lists should be different, you have to remove the mappedBy attribute.