Transactions don't rollback

2019-01-24 09:38发布

问题:

I am call two methods, the first one update a table and the next one insert a record in another table. When the second transaction fails the EJB is not doing the rollback of the first transaction.

This is my backing bean:

@ManagedBean
@ViewScoped
public class TransactionTestBean implements Serializable {

    @EJB
    private TransactionTestService service;

    public String loadView() {
        return "/test/transactionTest";
    }

    public void test() {
        try {
            service.updateTest();
        } catch (Exception e) {
        }
    }
}

The EJB interface:

@Local
public interface TransactionTestService {

    void updateTest() throws CustomException;
}

The EJB class:

@Stateless
@TransactionManagement
public class TransactionTestServiceImpl implements TransactionTestService {

    @Resource(mappedName = "java:jboss/datasources/xxxxxDS", shareable = true)
    public DataSource dataSource;

    private TransactionTestDAO dao;

    @PostConstruct
    public void init() {
        dao = new TransactionTestDAOImpl();
    }

    @PreDestroy
    public void destroy() {
        dao = null;
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void updateTest() throws CustomException {

        try (Connection connection = dataSource.getConnection()) {
            dao.updateRecord(connection);
            // dao.saveRecord(connection);
        } catch (SQLException exception) {
            throw new CustomException(exception, exception.getMessage());
        }
    }
}

And my custom exception:

@ApplicationException(rollback = true)
public class CustomException extends Exception {

    public CustomException(Throwable cause, String message) {
        super(message, cause);
    }
}

EDITED:

Added the DAO Class:

public class TransactionTestDAOImpl implements TransactionTestDAO {

    @Override
    public void updateRecord(Connection connection) throws CustomException {

        PreparedStatement preparedStatement = null;

        try {
            preparedStatement = connection.prepareStatement("UPDATE table_x SET field_x = ? WHERE field_y = 1");
            preparedStatement.setInt(1, 1);
            preparedStatement.executeUpdate();
        } catch (Exception exception) {
            throw new CustomException(exception, exception.getMessage());
        } finally {
            if (preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException sqlException) {
                }
            }
        }
    }
}

And the DAO Interface:

public interface TransactionTestDAO {

    void updateRecord(Connection connection) throws CustomException;
}

回答1:

The key issue in this case was bad default in datasources in some JBoss versions. Original code was fine and was working correctly in other application servers (WebSphere App Server and lightweight WebSphere Liberty).

Datasources created in JBoss are not JTA - in admin console the Use JTA setting is unchecked and in xml related setting is <datasource jta="false" .... Changing this setting to true fixed the problem. (JohnB, you wrote that defining xa-datasource fixed that, but since I didn't see your original xml with datasource definition, I believe that during changing datasource you've also change this flawed jta="false" setting). It will work for non xa-datasources also, as Grzesiek tested.

This is a very bad default, since it causes transactions not to be managed by container and causes flawed transaction behavior in connections got in EJB components.

Big thanks to Grzesiek D. who helped me in diagnosing this issue.



回答2:

Please try this

@Override
public void updateTest() throws CustomException {

    Connection connection = dataSource.getConnection();
    try {
        connection.setAutoCommit(false);  // this should be the key

        dao.updateRecord(connection);
        dao.saveRecord(connection);

        connection.commit();

    } catch(Exception ex) {
        connection.rollback();
        throw new CustomException(ex, ex.getMessage());

    } finally {
        if(connection != null) {
            connection.close();
        }
    }
}

Hope it will help this time.


UPDATE
My answer above has a mistake, because above code assumes that BMT (Bean-Managed Transactions) is used. But as we can see, you are using CMT (Container-Managed Transactions).
Because @TransactionManagement is equivalent of @TransactionManagement(TransactionManagementType.CONTAINER)).

Above code snippet will only work with BMT. With CMT you should get error like below:

Caused by: java.sql.SQLException: You cannot set autocommit during a managed transaction!

But :-) my mistake turned to sth good in the end, because when you wrote

This works very well (...)

then we found an answer: you think that your EJB bean use CMT with JTA, but due to some error, it does not.


In comments below, I've also advised you to use JPA, but in this simple case JDBC is good enough. CMT transactions can be freely used also with JDBC.

Also type of the data source doesn't matter here. CMT can be freely used with a non-XA datasource (also called local datasource) and XA datasource as well.


UPDATE 2

User @Gas solved the problem in the following comment. Kudos for him.

Basically: there were nothing wrong with the original code. Problem lies in the configuration of the datasource (has to be JTA enabled). So edit Datasource configuration via JBoss Administration console and set a checkbox "Use JTA".

Good luck.



回答3:

UPDATE: bad guess, see answer below with updates.


I'm almost sure that your problem is caused because you are creating your DAO by hand with new keyword:

@PostConstruct
public void init() {
    dao = new XxxxDAOImpl();
}

When you are doing such things, you ejb container cannot manage that object lifecycle and transaction boundaries.
You should let the container to create and manage the dao (and inject it for you). In that way, you will gain a proper transaction propagation across all your EJB methods - and thus your problem will be solved.

To achieve that, you can simply annotate your DAO class with @Stateless and inject it in your TransactionTestServiceImpl class with:

@EJB
private XxxxDAO dao;

And then, of course, remove init and destroy methods.

Personal advice

Why to use a separate, additional dao layer at all? In Java EE world the most convenient option is to just use an EntityManager. Entity Manager plays role of dao very well in most use cases. Altough JDBC in your example is good enough, JPA is (IMHO) simpler.

Good luck



回答4:

I think the problem is that the Connection/DataSource is simply not part of your current transaction. Instead of injecting an JDBC connection I suggest to:

  1. Create a PersistenceUnit for the defined DataSoruce in your persistence.xml of the JTA type.
  2. Inject the corresponding EntityManager in your EJB.
  3. Unwrap the Connection from the EntityManager injected in step 2. There is no standard way to it, check this answer.