【问题标题】:How to call multiple DAO functions in a transaction如何在一个事务中调用多个 DAO 函数
【发布时间】:2015-12-28 16:39:57
【问题描述】:

我正在寻找一种在事务中调用多个 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
}

我们想要的是避免在我们的“数据库 api”.jar 中使用此类方法,而是能够在我们的业务逻辑层中定义事务的开始和提交,就像上面的伪代码中提到的那样。

【问题讨论】:

  • 使用 EJB(或 Spring),您将拥有比您想要的更简单、更安全和可重用的东西。无需通过代码启动、提交和回滚事务。如果事务方法抛出运行时异常,事务将自动回滚。
  • 是的,但我正在寻找一种实现事务逻辑的方法。我可以在调用多个 DAO 方法的函数上添加 @Transactional 并让它在运行时异常上重新滚动所有数据库更改吗?例如,我有一个创建对象的方法,然后将信息写入 2 个不同的表(总共 3 个 DAO 方法)。如果第三种方法抛出运行时异常,我想回滚所有内容,甚至是对象创建。
  • 是的,这正是 EJB 和 Spring 事务方法所做的。
  • 好的,谢谢,我去看看
  • @Konstantine JBNizet 的回答是正确的,但是......你真的需要迁移到 EJB 吗?甚至事务在那里也不是很直观:将您的异常包装到javax.ejb.EJBException 既不灵活也不可读。更不用说其他问题了,比如启动时间或集成测试。

标签: java transactions ejb cdi jta


【解决方案1】:

我过去所做的是创建一个接收数据库 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 

然后我们获取了这个新对象,并创建了一个已准备好的池,并在一个新线程中进行管理,而应用程序只是从池中请求了一个新的“存储库”......然后我们就走了。

【讨论】:

    【解决方案2】:

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

    从您的问题来看,您似乎只需要一个支持 InterceptorsDependency Injection 框架。所以可能的方法:

    • Spring绝对是本区人气最高的
    • CDI(Weld 或 OpenWebBeans)自 Java EE 6 发布以来出现 - 但完全可以在没有 Java EE 应用程序服务器的情况下使用 (我现在正在使用这种方法 - 效果很好)。李>
    • Guice 还带有自己的 com.google.inject.persist.Transactional 注释。

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

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

    希望对你有所帮助。

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

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

    【讨论】:

    • 我们的应用程序已经投入生产,我们正在开发/扩展它(不好的做法,但我不是老板)。我们不能从没有框架迁移到 Spring。此外,我们还有一个由不同团队开发的移动版本(使用 EJB 和 JPA)。现在我们的目标是将我们的(Web 应用程序的)数据库层打包成一个适用于所有环境的通用库。这意味着移动团队将从 JPA 迁移到自定义数据库实现。但是要做到这一点,我们需要解决上述问题。 CDI 似乎是要走的路,也是最容易从 EJB 迁移的方法。
    • 如果您已经在使用 Java EE 6+ 服务器,那么可能是的。
    • 我并没有真正看到如何使用拦截器来实现我正在寻找的东西......我将如何启动事务然后调用 DAO 函数然后提交/回滚结果?请记住,DAO bean 只能访问公共数据源,但不能访问公共连接对象
    • 你可以看一下这种拦截器的一个非常简单的版本,例如here.
    • 如上所述,我们没有使用任何持久化 API,因此我们无权访问 EntityManager。我们使用容器的DataSource 对象来获取连接。让我澄清一下,“dblayer api”的每个方法都从数据源获取自己的连接。这就是为什么我们不确定如何执行交易。现在,可以包含在事务中的每个方法都接受一个Connection 参数并从业务层函数接收一个Connection 对象。我们想要的是在业务层声明一个事务,而不传递一个公共的Connection 对象。
    【解决方案3】:

    @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 资源,该资源@Injects 这些对象并启动并提交事务,而无需传递任何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 @Injects 和 ConnectionWrapper,它的 @PostConstruct 优先,因为它必须在 DAOObjects 甚至可以开始之前完成,但这只是一个猜测。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-01-18
      • 2018-07-29
      • 2021-08-07
      • 1970-01-01
      • 1970-01-01
      • 2011-04-24
      相关资源
      最近更新 更多