Search code examples
jakarta-eejpaejbentitymanaged

JPA and EJB managed vs. non-managed


I am a little bit confused: I have a Java EE application with JSF, EJB and JPA.

I have a UserService which is an EJB.

@Stateless
public class UserService {
    public User create(User u) throws ProcessingException {
        if (!exists(u)) {
            u = userDao.create(u);
            addRole(u, RoleType.USER);
            return u;
        } else {
            throw new ProcessingException("User " + u.getUsername() + " already exists");
        }
    }   


    public boolean hasRole(User u, RoleType r) {
        if (u == null || r == null) {
            return false;
        }

        if (!userDao.isManaged(u)) {
            u = userDao.find(u.getId());
        }

        Set<Role> roles = u.getRoles();
        ...
    }
}

I had some problems and did some debugging and found out that sometimes in hasRole, the User is not in managed-state, why I do userDao.isManaged(u). However I can't understand why it is sometimes not managed. Can you explain why?

Example:

@Test
public void test() throws ProcessingException {
    Client c = clientBuilder.build();
    User u = new User();
    u.setClient(c);
    userService.create(u);

    userService.addRole(u, RoleType.APPROVER);

When addRole(u, RoleType.APPROVER) is called, then u has state unmanaged. But why?!

Do I always have to add checks to my methods in order to be sure that the entity is managed?


Solution

  • An entity is only managed in the very same transaction as where it's obtained from DB.

    In a @Stateless EJB a single method call from the client counts by default as a single full transaction. All nested EJB method calls take place in the same transaction. But once the EJB method call from the client returns to the client (e.g., the JSF/CDI managed bean), the transaction ends. When that method returns an entity, then it becomes unmanaged.

    When you pass that very same unmanaged entity back into the service layer, then it's still unmanaged until you call em.merge() on it, or get a fresh one from DB by em.find() on @Id.

    In your specific case, you could change the create() service method like below:

    public User create(User user, RoleType... roles) {
        // ...
    
        for (RoleType role : roles) {
            addRole(user, role);
        }
    
        return user;
    }
    

    So you can just perform the job in a single transaction

    userService.create(u, RoleType.APPROVER);
    

    instead of in two transactions

    userService.create(u);
    userService.addRole(u, RoleType.APPROVER);
    

    The EJB methods should be designed in such way that the client (the JSF/CDI managed bean) doesn't need to call multiple different methods in a succession from a single action method. Instead, refactor and/or merge such a specific sequence of multiple different service methods into a single service method. This guarantees that the job will take place in a single transaction.

    See also: