Skip to content
Advertisement

java.lang.StackOverflowError: null [Spring Boot, Hibernate]

I have two classes User.java and Address.java and there is a one-to-one bi-directional mapping between them.

But when I try to get the address using the User class I get an “java.lang.StackOverflowError: null” exception.

The same thing happens when I try to get the User from the Address class.

User.java

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    private String name;
    private String email;
    private String phone;
    private String password;
    private String imageUrl;
    
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address")
    private Address address;

Address.java

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    @OneToOne(cascade = CascadeType.ALL, mappedBy = "address")
    private User user;
    
    private String country;
    private String state;
    private String city;
    private String street;
    private String pincode;

MainController.java

@Controller
public class MainController {
    @Autowired
    private UserDao userDao;
    
    @Autowired
    private AddressDao addressDao;
    
    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        User user = new User();
        user.setName("name");
        user.setEmail("email");
        user.setPhone("phone");
        user.setPassword("password");
        user.setImageUrl("imageUrl");
        
        Address address = new Address();
        address.setCountry("country");
        address.setState("state");
        address.setCity("city");
        address.setStreet("street");
        address.setPincode("123456");
        
        user.setAddress(address);
        userDao.save(user);
        
        return "working";
    }
    
    @RequestMapping("/fetch")
    @ResponseBody
    public String fetch() {
        User user = userDao.getById((long) 1);
        System.out.println(user.getAddress());
        
        return "working";
    }
}

I am using the test() function to put data in the database and it is working fine. database image

But when I call the fetch() function I am getting the following error

java.lang.StackOverflowError: null
    at org.hibernate.proxy.pojo.BasicLazyInitializer.invoke(BasicLazyInitializer.java:58) ~[hibernate-core-5.6.5.Final.jar:5.6.5.Final]
    at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:43) ~[hibernate-core-5.6.5.Final.jar:5.6.5.Final]
    at 

Updated MainController.java

package com.demo.controller;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.demo.dao.AddressDao;
import com.demo.dao.UserDao;
import com.demo.entity.Address;
import com.demo.entity.User;

@Controller
public class MainController {
    @Autowired
    private UserDao userDao;
    
    @Autowired
    private AddressDao addressDao;
    
    @RequestMapping("/test")
    @ResponseBody
    public String test() {
        User user = new User();
        user.setName("name");
        user.setEmail("email");
        user.setPhone("phone");
        user.setPassword("password");
        user.setImageUrl("imageUrl");
        
        userDao.save(user);
        
        Address address = new Address();
        address.setCountry("country");
        address.setState("state");
        address.setCity("city");
        address.setStreet("street");
        address.setPincode("123456");
        
        addressDao.save(address);
        
        user.setAddress(address);
        userDao.save(user);
        
        return "working";
    }
    
    @RequestMapping("/fetch")
    @ResponseBody
    public String fetch() {
        Optional<User> op = userDao.findById((long) 1);
        User user = op.get();
        
        // working
        System.out.println(user.getName() + " " + user.getEmail() + " " + user.getPhone());
    
        // java.lang.StackOverflowError:null
        System.out.println(user.getAddress());
        
        return "working";
    }
}

Advertisement

Answer

TLDR: you aren’t actually saving anything anywhere, but it’s easy to fix. Here’s my code and my explanation:

MainController.java:

@RestController
public class MainController {
    private final UserRepository userRepository;
    private final AddressRepository addressRepository;

    public MainController(UserRepository userRepository, AddressRepository addressRepository){
        this.userRepository = userRepository;
        this.addressRepository = addressRepository;
    }

    @GetMapping("/test")
    public String test() {
        User user = new User();
        user.setName("name");
        user.setEmail("email");
        user.setPhone("phone");
        user.setPassword("password");
        user.setImageUrl("imageUrl");

        user = userRepository.save(user);
        System.out.println("saved user");

        Address address = new Address();
        address.setCountry("country");
        address.setState("state");
        address.setCity("city");
        address.setStreet("street");
        address.setPincode("123456");

        address = addressRepository.save(address);
        System.out.println("saved address");

        user.setAddress(address);
        userRepository.save(user);

        System.out.println("set user's address");
        return "working";
    }

    @GetMapping("/fetch")
    public String fetch() {
        Optional<User> optionalUser = userRepository.findById((long) 1);
        if(optionalUser.isPresent()){
            User user = optionalUser.get();
            System.out.println(user.getAddress());

            boolean addressExists = addressRepository.existsById((long) 1);
            System.out.println(addressExists);
            System.out.println(user.getAddress().getCountry());

            return "working";
        }

        System.out.println("Error: user with id 1 not found!");
        return "failing";
    }
}

User.java:

@Entity
@Table(name = "user")
public class User {

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

    private String name;
    private String email;
    private String phone;
    private String password;
    private String imageUrl;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;

    //getters and setters omitted for brevity
}

Address.java:

@Entity
@Table(name = "address")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @OneToOne(mappedBy = "address")
    private User user;

    private String country;
    private String state;
    private String city;
    private String street;
    private String pincode;
    
    //getters and setters omitted for brevity
}

AddressRepository.java:

public interface AddressRepository extends CrudRepository<Address, Long> {

}

UserRepository.java:

public interface UserRepository extends CrudRepository<User, Long> {

}

UserDAO.java:

public class UserDAO {
    private final String name;
    private final String email;
    private final String phone;
    private final String imageUrl;
    
    public UserDAO(User user) {
        name = user.getName();
        email = user.getEmail();
        phone = user.getPhone();
        imageUrl = user.getImageUrl();
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getPhone() {
        return phone;
    }

    public String getImageUrl() {
        return imageUrl;
    }
}

A DAO has no connection to the database, it’s intent is what the acronym stands for, simply to transfer data, and that’s it. When you make a repository, you can stick your objects there by saving them in the repository. Notice that by extending the CrudRepository with correct generics, you don’t even need to implement the methods yourself. The save method actually saves the POJO, and returns the saved version, which is why I did user = userRepository.save(user), which may seem counterintuitive at first, but it simply helps ensure that everything is as you expect. If you then want to send the UserDAO object as a response, you can create it using the user object that is returned from the database, maybe something like:

UserDAO dao = new UserDAO(userRepository.save(user));

Please take notice of what is happening inside the test method in MainController. First, we create the POJO User object and set its fields. Then we have to save it to the repository, it is only persisted after you call save method of the repository. Please note that the user object is saved again once it is updated with the address.

This is a very crude way to do things, it is best to create a service layer and do this there with the @Transactional annotation, which would mean that everything is rolled back in case something goes wrong inside a method annotated as @Transactional.

Also, using CascadeType.ALL may be not what you want, please refer to this answer.

Inside fetch method, I ensure that the user indeed exists, which is not guaranteed. To avoid 500 errors, it’s important to have a fallback mechanism for when something doesn’t work.

As a final side note, you shouldn’t be storing raw passwords like that, you should at least use hashing with salt and pepper, or use one of the many available libraries for implementing such functionality (although it can be quite fun getting down and dirty with the code itself). You should also consider how much information you are revealing when something does go wrong, as you don’t want to give away too much information which could be used to deanonimise a specific user, or even learn more about your code and the system architecture.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement