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

How to call multiple DAO functions in a transaction

我正在寻找一种在事务中调用多个 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 中使用这样的方法,而是能够在我们的业务逻辑层中定义事务的开始和提交,就像伪代码中提到的那样以上。

我过去所做的是创建一个存储库对象,该对象采用数据库 API 并生成连接并将连接作为成员变量保存到它。 (以及数据库参考)

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

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

Repository myr = new Repository(datasource);  // let constructor create connection
myr.setAutoCommit(false); 
myr.DAOObject1(parms);   // method wrapper
myr.DAOObject2(parms);   // method wrapper

myr.commitwork();   // method in Repository that calles endAndCommitTransactionLogic 

然后我们获取了这个新对象,并创建了一个池,并在新线程中对其进行了管理和管理,而应用程序只是从池中请求了一个新的 "Repository".. 然后我们就开始了。

@JBNizet 的评论是正确的,但是...请三思您是否真的需要迁移到 EJB。甚至那里的事务也不是很直观:将异常包装到 javax.ejb.EJBException 中既不灵活也不可读。更不用说其他问题了,比如启动时间集成测试

从你的问题来看,你似乎只需要一个支持拦截器依赖注入框架。所以可能的方法:

  • Spring绝对是这个区最受欢迎的
  • CDI(Weld 或 OpenWebBeans)自 Java EE 6 发布以来出现 - 但完全可以在没有 Java EE Application Server 的情况下使用(我正在使用这种方法现在 - 并且效果很好).
  • Guice 也带有自己的 com.google.inject.persist.Transactional 注释。

上述所有三个框架都同样适合您的用例,但还应考虑其他因素,例如:

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

希望对您有所帮助。

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

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

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

我创建了一个 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 .

@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@PostConstructDAOObject@PostConstruct 之前被调用。这是正确且理想的行为,但我不明白为什么。我猜因为 DAOObject @Inject 是一个 ConnectionWrapper,它的 @PostConstruct 优先,因为它必须在 DAOObjects 开始之前完成,但是这个只是猜测。