Search code examples
javaspringhibernatemany-to-manylazy-initialization

Lazy initialization @ManyToMany. What is the error?


Please, Help me implement lazy initialization.

I wrote Java code in Spring Core, Spring Security, and Hibernate. There are two user and Role entities. I link them with @ManyToMany(fetch = FetchType.EAGER). But for the task, I need to implement lazy initialization.

If I set @ManyToMany(fetch = FetchType.LAZY), an error occurs after successful authorization:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: web.model.User.roles, could not initialize proxy - no Session

Google directed to these links: First Link / Second Link

It suggests adding the user.getAuthorities () method.size();.

Isn't this a workaround???

Then authorization is successful, but with lazy initialization, I can't access the roles. And they don't appear on the jsp page.

org.apache.jasper.JasperException: An error occurred during processing [/WEB-INF/pages/admin_panel.jsp] in line [71]

68:             <td>${user.lastName}</td>
69:             <td>${user.username}</td>
70:             <td>${user.password}</td>
71:             <td>${user.roles}</td>
72:             <td>


Stacktrace:
    org.apache.jasper.servlet.JspServletWrapper.handleJspException(JspServletWrapper.java:617)

Below I will present the main parts of the code. If something is missing, then let me know.

My code:

User

@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "firstName")
private String firstName;

@Column(name = "lastName")
private String lastName;

@Column(name = "username")
private String username;

@Column(name = "password")
private String password;

@ManyToMany(fetch = FetchType.LAZY) // there was an EAGER here.
@JoinTable(name="user_role",
        joinColumns={@JoinColumn(name="userId")},
        inverseJoinColumns={@JoinColumn(name="roleId")})
private Set<Role> roles;

public User() {
}
...

class UserServiceImp

@Service
public class UserServiceImp implements UserService {

    @Autowired
    private UserDao userDao;

    @Transactional
    @Override
    public void add(User user) {
        userDao.add(user);
    }

    @Transactional
    public List<User> listUsers() {
        return userDao.listUsers();
    }

    @Transactional
    public boolean deleteUserById(long id) {
        return userDao.deleteUserById(id);
    }

    @Transactional(readOnly = true)
    public User getUserById(long id) {
        return userDao.getUserById(id);
    }

    @Transactional
    public void update(User user) {
        userDao.update(user);
    }

    @Transactional
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.getUserByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        int size = user.getAuthorities().size(); //added this line as indicated in the links
        return user;
    }
}

page *.jsp

...
<c:forEach items="${users}" var="user">
    <tr>
        <td>${user.id}</td>
        <td>${user.firstName}</td>
        <td>${user.lastName}</td>
        <td>${user.username}</td>
        <td>${user.password}</td>
        <td>${user.roles}</td>
        <td>
            <a href="<c:url value='${pageContext.request.contextPath}/admin/editUser?id=${user.id}'/>" >Edit</a>
            |
            <a href="<c:url value='${pageContext.request.contextPath}/admin/deleteUser/${user.id}'/>" >Delete</a>
        </td>
    </tr>
</c:forEach>
...

UserDaoImp

@Repository
public class UserDaoImp implements UserDao {

    @PersistenceContext
    private EntityManager manager;

   ...

    @Override
    @SuppressWarnings("unchecked")
    public User getUserByName(String username) {
        Query query = manager.createQuery("FROM User u WHERE u.username = : username");
        query.setParameter("username", username);
        try {
            return (User) query.getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }
...

Solution

  • What FetchType.LAZY does is that it doesn't take the entity collection (or entity in some cases e.g. @OneToOne) it is applied to when the entity containing it is returned from the db. It makes an additional request to fetch it later if its getter method is called in the same transaction. The workaround you found works because you call the getter getRoles() within getAuthorities() (although you haven't added that to the question) therefore fetching the set of roles explicitly.

    After that you try to access a list of users which I assume you get from public List<User> listUsers() and if you understood the above paragraph you probably already know what I am going to say. In that method you don't fetch the roles, leave the transaction and then try to access the roles with <td>${user.roles}</td>. Now there are a few possible solutions to this problem.

    • First off it is usually not a great idea to send entities to the frontend because that way you introduce unnecessary coupling. What I would suggest is you make a UserDto class which contains all the fields you will need to display (but not a single one more) which has a Set of RoleDto to which the same things apply and you convert your list of users you get from the db into a list of user dtos in the listUsers() method. That way you solve 3 problems -> 1. your code is decoupled 2. you eliminate data redundancy (e.g. unnecessary for the frontend db fields) 3. you fetch every user's roles when converting from users to user dtos.
    • Another option would be to separate the User entity from the class implementing UserDetails by adding a new class to implement it. You can then add to this class on login all the information spring needs which basically means to transfer the necessary information from the User class to this new UserDetailsImpl class in the loadUserByUsername(String username) method so that all methods inherited by the UserDetails interface could be adequately implemented. From that point on any validation of roles or authorities will pass through the non-entity UserDetailsImpl which will eliminate the need for your workaround and the class will never throw a LazyInitializationException because it's not an entity. But copying fields like firstName and lastName from User is highly discouraged because of the possible data discrepancy.
    • (NOT recommended) If you are feeling really lazy you can always just do the exact same workaround in the listUsers() method and just iterate trough the users and call their getRoles() method to fetch the roles.
    • And lastly I understand that for whatever reason you can't do this and that's why I left this point for last but if you have to on many occasions access a collection of an entity outside of a transaction then FetchType.EAGER is clearly the easiest and best option. This is literally the problem it's made to solve.