如何在事务中调用多个DAO函数

时间:2015-12-28 16:39:57

标签: java transactions ejb cdi jta

我正在寻找一种在事务中调用多个DAO函数的方法,但我不使用spring或任何此类框架。我们实际拥有的是数据库api类型.jar,它使用已使用的数据源进行初始化。我想要实现的是让我的业务逻辑级代码执行以下操作:

Connection conn = datasource.getConnection();
conn.setAutoCommit(false);
DAOObject1.query1(params, conn);
DAOObject2.query4(params, conn);
conn.commit();
conn.setAutoCommit(false);

但是我想避免在每个函数中传递连接对象,因为这不是正确的方法。现在,在我们使用的少量事务中,我们正在寻找一种方法来停止将连接对象传递给数据库层,甚至在它之外创建它。我正在寻找以下内容:

//Pseudocode
try{
  Datasource.startTransactionLogic();
  DAO1.query(params);
  DAO2.query(params);
  Datasource.endAndCommitTransactionLogic();
}
catch(SQLException e){
  Datasource.rollbackTransaction();
}

我可以通过EJB实现这一目标吗?现在我们没有通过注入使用DAO,我们手动创建它们但我们即将迁移到EJB并开始通过容器使用它们。我听说EJB执行的所有查询都是事务性的,但是它如何知道要回滚到什么?通过savepoints

修改

让我指出,现在每个DAO对象的方法都获得了自己的连接对象。以下是我们DAO类的示例:

public class DAO {
public DTO exampleQueryMethod(Integer id) {
    DTO object = null;
    String sql = "SELECT * FROM TABLE_1 WHERE ID = ?";
    try (
        Connection connection = datasourceObject.getConnection();
        PreparedStatement statement = connection.prepareStatement(sql)
    ) {
        statement.setInt(1,  id);
        try (ResultSet resultSet = statement.executeQuery()) {
            if (resultSet.next()) {
                object = DAO.map(resultSet);
            }
        }
    }
    return object;
}
}

现在我们正在为需要处理事务的方法做的事情是获得一个接收Connection对象的第二个副本:

public void exampleUpdateMethod(DTO object, Connection connection) {
    //table update logic
}

我们想要的是避免在我们的'database api'.jar中使用这样的方法,而是能够在我们的业务逻辑层中定义事务的开始和提交,就像上面的伪代码中提到的那样。 / p>

3 个答案:

答案 0 :(得分:1)

我过去所做的是创建一个Repository Object,它接受Database API并生成连接并将连接保存为成员变量。 (以及数据库参考)

然后,我将所有业务层调用作为方法从此存储库对象挂起,以方便调用者。

这样..你可以调用,混合,匹配任何调用并使用底层连接,执行回滚,提交等等。

using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;

public class Foo
{

    public Foo(IHttpContextAccessor contextAccessor)
    {
        _context = contextAccessor.HttpContext;
    }

    private readonly HttpContext _context;

    public bool TokenCorrect()
    {
        ISessionFeature sessionFeature = _context.Features.Get<ISessionFeature>();
        if(sessionFeature != null)
        {
            int? token = sessionFeature.Session.GetInt32("Token");
            if(token.HasValue)
            {
                // do whatever check you are doing
            }
        }


        return false;
    }

}

然后我们接受了这个新对象,并创建了一个已经准备好的池,并在一个新线程中进行管理,而Application只是从池中请求了一个新的“Repository”...然后我们就去了。

答案 1 :(得分:1)

@JBNizet评论是正确的,但是......请三思而后行,是否真的需要迁移到EJB。即使交易也不那么直观:将您的例外包装到javax.ejb.EJBException中既不灵活也不可读。更不用说其他问题,例如启动时间集成测试

从您的问题来看,似乎所有您需要的是依赖注入框架,并支持拦截器。可能的方法:

  • Spring绝对是这个领域最受欢迎的
  • CDI(Weld或OpenWebBeans)自Java EE 6发布以来 - 但完全可以在没有Java EE Application Server的情况下使用(我现在正在使用这种方法 - 它运行良好)
  • Guice还附带了自己的com.google.inject.persist.Transactional注释。

以上所有三个框架对您的用例同样有用,但还有其他因素需要考虑,例如:

  • 你和哪一个你的团队熟悉
  • 学习曲线
  • 您的应用程序未来可能的需求
  • 框架的社区规模
  • 框架的当前发展速度

希望它对你有所帮助。

编辑:澄清您的疑虑:
您可以创建自己的Transaction类,该类将包含从Connection获取的datasource.getConnection()。此类事务应该是@RequestScoped CDI bean,并包含方法方法,如begin()commit()rollback() - 它们将调用 connection.commit / rollback 引擎盖下。然后你可以编写一个简单的拦截器,如this one,它将使用提到的事务并在需要的地方启动/提交/回滚它(当然禁用AutoCommit)。

这是可行的,但要记住,它应该精心设计。这就是为什么几乎每个DI平台/框架都已经提供了用于交易的拦截器。

答案 2 :(得分:1)

@G。 Demecki有正确的想法,但我遵循了不同的实现。 Interceptors无法解决问题(至少从我看到的问题),因为它们需要附加到应该使用它们的每个函数。一旦拦截了一个拦截器,调用该函数将始终拦截它,这不是我的目标。我希望能够明确定义事务的开始和结束,并使这两个语句之间执行的每个sql都成为SAME事务的一部分,而无需通过参数访问数据库的相关对象(如连接,事务等)通过。我能够实现这一目标的方式(在我看来相当优雅)如下:

我创建了一个ConnectionWrapper对象,如下所示:

@RequestScoped
public class ConnectionWrapper {

@Resource(lookup = "java:/MyDBName")
private DataSource dataSource;

private Connection connection;

@PostConstruct
public void init() throws SQLException {
    this.connection = dataSource.getConnection();
}

@PreDestroy
public void destroy() throws SQLException {
    this.connection.close();
}

public void begin() throws SQLException {
    this.connection.setAutoCommit(false);
}

public void commit() throws SQLException {
    this.connection.commit();
    this.connection.setAutoCommit(true);
}

public void rollback() throws SQLException {
    this.connection.rollback();
    this.connection.setAutoCommit(true);
}

public Connection getConnection() {
    return connection;
}
}

我的DAO个对象本身遵循以下模式:

@RequestScoped
public class DAOObject implements Serializable {

private Logger LOG = Logger.getLogger(getClass().getName());

@Inject
private ConnectionWrapper wrapper;

private Connection connection;

@PostConstruct
public void init() {
    connection = wrapper.getConnection();
}

public void query(DTOObject dto) throws SQLException {
    String sql = "INSERT INTO DTO_TABLE VALUES (?)";
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        statement.setString(1, dto.getName());
        statement.executeUpdate();
    }
}
}

现在,我可以轻松拥有一个jax-rs资源@Inject这些对象并启动并提交一个事务,而不必传递任何ConnectionUserTransaction。< / p>

@Path("test")
@RequestScoped
public class TestResource {

@Inject
ConnectionWrapper wrapper;

@Inject
DAOObject dao;

@Inject
DAOObject2 dao2;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Response testMethod() throws Exception {
    try {
        wrapper.begin();
        DTOObject dto = new DTOObject();
        dto.setName("Name_1");
        dao.query(dto);
        DTOObject2 dto2 = new DTOObject2();
        dto2.setName("Name_2");
        dao2.query2(dto2);
        wrapper.commit();
    } catch (SQLException e) {
        wrapper.rollback();
    }
    return Response.ok("ALL OK").build();
}
}

一切都很完美。没有Interceptors或环顾InvocationContext等等。

只有两件事困扰着我:

  1. 我还没有找到在@Resource(lookup = "java:/MyDBName")上拥有动态JNDI名称的方法,这让我很烦恼。在我们的AppServer中,我们定义了许多数据源,应用程序使用的数据源是根据与war一起打包的.xml资源文件动态选择的。这意味着我无法在编译时知道数据源JNDI。有通过InitialContext()环境变量获取数据源的解决方案,但我希望能够从服务器获取它作为资源。我也可以创建一个@Produces生产者并以这种方式注入它但仍然。
  2. 我不确定为什么在ConnectionWrapper @PostConstruct之前调用DAOObject @PostConstruct DAOObject。这是正确和理想的行为,但我不明白为什么。我猜测@Inject ConnectionWrapper sa @PostConstruct后,其DAOObjects优先,因为它必须在/* Unit test task */ gulp.task('pre-test', function() { return gulp.src(['src/app/**/*.js']) .pipe(plumber()) .pipe(istanbul({ instrumenter: isparta.Instrumenter, includeUntested: true })) .pipe(istanbul.hookRequire()); }); gulp.task('test:unit', ['pre-test'], function() { return gulp.src(['src/test/unit/**/*.js']) .pipe(mocha({ reporter: 'spec' })) .pipe(istanbul.writeReports({})); }); 甚至可以开始之前完成但是这个只是一个猜测。