質問

I have a question about how transactions are controlled by an EE container. This is pseudo code to give some context to my question. This is not how I code, so please stay on the question and do not evolve the topic into other stuff.

Consider the following two services and related controller. Both services have injected EntityManager and both have methods that need to run within a transaction. One service has a method that does not need any transaction support.

@Stateless
class UserService {
    @PersistenceContext private EntityManager em;

    public void saveUser(User user) {
        em.merge(user);
    }

    public String getFullName(User user) {
        return user.getFirstName() + " " + user.getLastName();
    }
}

@Stateless
class LogService {
    @PersistenceContext private EntityManager em;

    public void logEvent(String eventText) {
        Event event=new Event();
        event.setText(eventText);
        event.setTime(new Date());
        em.persist(event);
    }
}


@Named
class UserController {
    User user;

    @Inject UserService userService;
    @Inject LogService logService;

    public void updateUser(user) { // button posts to this method
        String fullName=userService.getFullName(user);  // 1
        if(fullName.startsWith("X")) return;            // 2
        userService.saveUser(user);                     // 3
        logService.logEvent("Saved user " + fullName);  // 4
    }
}

Now, imagine there's a button that posts a form to userController.updateUser.

My assumption is that the UserController.updateUser() will execute userService.saveUser(user); and logService.logEvent("Saved user " + fullName); within the same transaction. So if the call to logService.logEvent() fails with an SQL Exception, the user entity will not be updated. Also, my assumption is that call to userService.getFullName(user) is not run within any transaction and if we prematurely exit the method when user's name starts with an X, then no transaction is created. But clearly, these are just guesses.

Can someone explain what the Java EE container will do to support UserController.updateUser() method with transaction and what actually triggers the transaction? Also, any further reading you can point me to, would be much appreciated. I've seen some material online but still I'm missing something here and didn't get any help asking around at work, either. So I'm certainly not the only one who's got a gap on this.

役に立ちましたか?

解決

In your case 3 independent transactions will be started. Each by one of your @Stateless beans methods. It's because session EJBs have transacional methods with transaction type TransactionAttribute.REQUIRED by default. This means that if a transaction is not already running the new one will be created before method invocation.

To run all of your session EJBs methods in one transaction you must wrap them in one transaction. In your case you can do this by annotating updateUser(...) method with @Transactional

他のヒント

You need to change your @Inject annotations to @EJB, then by default with CMT (container managed transactions) each call by the CDI bean will be in its own TX scope. If you do not want one of the method calls to invoke a TX then add @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) to the method.

From the looks of your business logic you could really simplify things by changing @Named to @Stateless, change @Inject to @EJB and add @TransactionalAttribute(TransactionAttributeType.NOT_SUPPORTED) on the getFullName(..) if you don't want that method to run in a TX. With those changes you will get the TX behavior you are looking for.

If you want UserController to be a JSF managed bean then along with the other mods I suggested I would change @Named to @ManagedBean and simply add @Stateless under @ManagedBean instead of changing @Named to @Stateless.

Transactions in Java EE must be explicitly controlled, either using the UserTransaction from JNDI or using deployment descriptor/annotations on EJBs. For a CDI component, here the UserController, no transaction is started by default. (EDIT Transactions for EJB methods are enabled by default, if nothing is specified.)

So to start, your assumption:

the UserController.updateUser() will execute userService.saveUser(user); and logService.logEvent("Saved user " + fullName); within the same transaction

is wrong! I believe that a new transaction will be created and committed on each em.persist()/em.merge() call.

In order to wrap the calls saveUser() and logEvent() in the same transaction you can manually use the UserTransaction:

public void updateUser(user) {
    InitialContext ic = new InitialContext();
    UserTransaction utx = (UserTransaction) ic.lookup("java:comp/UserTransaction");
    utx.begin();
    ...
    utx.commit(); // or rollback(), wrap them in try...finally
}

Or the friendlier:

@Resource UserTransaction utx;
...
public void updateUser(user) {
    utx.begin();
    ...
    utx.commit(); // or rollback(), wrap them in try...finally
}

Or even better the @Transactional annotation, either with Java EE 7 or in Java EE 6 with the DeltaSpike JPA extension (or any other similar interceptor).

@Transactional
public void updateUser(user) {
    ...
}

You could specify that the EJB methods are transactional using the javax.ejb.TransactionAttribute annotation. There would still be 2 transactions in this case. Alternatively you could move the "business" logic from the web-tier to the EJB-tier in a method annotated with @TransactionAttribute and achieve running the DB methods in a single transaction.

As for further reading, check out the "Support for Transactions" chapter in the EJB 3 spec.

For anynone still having this problem, the accepted answer ( Flying Dumpling) is wrong.

What actually happens is this. The container has the TransactionAttributeType = REQUIRED by defeult. Which means if you do not annotate any of your beans, they will always be REQUIRED.

Now what happens here is this:

You call method UserController.updateUser() and when you do, a transaction will be created (by default if there are no annotations indicating otherwise, the container creates a transaction everytime a method is executed, and finishes it as soon as the execution is over).

When you invoke userService.getFullName(user), since this method is REQUIRED, what will happen is that the same transaction started initially when you first called UserController.updateUser(), will be used again here. Then the container comes back to he first bean and invokes another method, userService.saveUser(user), again since the transaction type is REQUIRED, then the same transaction will be used. And when it comes back and invokes the third method, logService.logEvent("Saved user " + fullName), uses the same one.

In this case, if you want to make sure every operation is runned in a separate transaction to avoid rolling back everything if one of them fails, you can use REQUIRES_NEW in every method that interacts with the DB. That way you ensure that everytime you run a method, a new transaction is created and there is no harm if one of them fails and you wish to go ahead with the others.

More information can be found here: https://docs.oracle.com/javaee/5/tutorial/doc/bncij.html

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top