Jdbi: 使 @CreateSqlObject 不那么混乱。

创建于 2018-02-01  ·  20评论  ·  资料来源: jdbi/jdbi

问题
JDBI 官方文档让开发人员相信@CreateSqlObject注解是为不同的 DAO 添加事务支持的机制。

http://jdbi.org/#__createsqlobject

这似乎有点误导实际行为,当使用首选的jdbi.onDemand创建机制时可能导致状态错误。

论坛中有几条消息说明了这种困惑:

要求
不确定这个问题的最佳解决方案是什么。 想到的一些:

  • 弃用
  • 关于缺点的更好的文档
  • 在多个 DAO 中拆分查询的另一种解决方案,但允许逻辑事务语义将它们联系在一起。 具体来说,支持只读/写事务的混合。
cleanup improvement

最有用的评论

很抱歉再次对此发表评论。 你的路线图中有什么可以帮助解决这个问题吗?

完全没有问题。 这显然是造成混乱的根源,也是我们想要解决的问题——我们只是真的想确保这次做对了,因为我们上次没有:)

(正如@svlada所提到的,在其他框架中在更高级别使用@Transactional很常见)

是的,但据我了解,这一切都是通过您的 DI 框架(如 Spring)中的 AOP 挂钩完成的。 由于 JDBI 不会“包装”您的所有服务对象,因此我们没有机会为不是我们自己创建的对象提供类似 AOP 的解决方案。 即使是旧的cglib实现也只会在 daos 本身上注意到@Transactional ,它永远不会在单独的服务类上工作。

如果有帮助,我们可以考虑添加例如 Spring 和/或 Guice AOP 绑定以允许更高级别的事务。 这不是core的一部分,而是spring扩展的一部分。 这不太可能是核心开发人员所关注的事情,但会认真考虑将贡献纳入其中。 也许这以“更清洁”的方式解决了您的问题?

我们决定从cglib切换到Proxy主要是出于兼容性原因 - Proxy是受支持的 jdk api,而cglib (或者更具体地说是asm )往往会随着每个主要版本(它在 8、9、11 ......

所有20条评论

感谢您报告此事。 我怀疑我们最终会在这里进行技术上的重大更改,但我认为现有的行为很糟糕,以至于我们在 3.x 发布周期的早期进行了技术上的重大更改。 如果您依赖于现有行为或不同意可能在此处发布小的重大更改,请标记此问题。

仅就 _why_ @CreateSqlObject的上下文而言,点播效果不佳:

  • 按需是核心级别的抽象,使用代理实现。 当您在按需代理上调用方法时,会创建一个真正的 SQL 对象,并将方法调用委托给真正的 SQL 对象。 在委托方法调用返回后,支持该真实实例的句柄将关闭。
  • @CreateSqlObject是使用handle.attach(sqlObjectType)实现的,带有原始 SQL 对象的支持句柄。

因此,创建的 SQL 对象的支持句柄在 SQL 对象甚至可以返回之前就关闭了。

以上都不是一成不变的——这就是它现在的实施方式。

我不相信我们必须打破兼容性来解决这个问题。

抱歉,我真的不明白... onDemand 和 CreateSqlObject 到底有什么问题? 我从来没有遇到过任何问题,关于@qualidafial的解释只是让我的思绪开始倾斜......

image

为调用fooProxy.usecase获取连接。 Foo.usecase调用 createSqlObject 方法bar ,该方法在将新Bar附加到当前( usecase的)句柄后返回。 当用返回时,用例的句柄关闭。 只要您没有对返回的Bar做任何异步操作, bar的句柄怎么会过早过期? bar的生命周期和使用仅限于Foo.usecase的主体,句柄也是如此......

我不能谈论 JDBI 的内部,但是我可以评论我们在生产中看到的行为。

基本上,在我们较低的环境中,一切都很好。 但是随着生产中负载的增加,我们不断地看到 SQL 语句没有针对正确的数据库节点执行。 例如,只读选择会命中主节点,而插入会命中只读副本。 此外,我们会在操作过程中遇到连接关闭错误。

从本质上讲, @CreateSqlObject似乎创建了一个线程安全问题——只读标志被竞争请求更改。

为了“解决”这个问题 - 我们删除了@CreateSqlObject的所有用法,最终得到了这样的结果:

  • class FooDao
  • class BarDao
  • class CombinedDao extends FooDao, BarDao

然后我们jdbi.onDemand(CombinedDao)并且只使用通过它访问。
切换到这种模式虽然不那么漂亮,但消除了上述生产中的所有错误。

仅供参考,我正在考虑回到 JDBI 2,因为目前我所有的代码都使用带有抽象类的 onDemand,而且我不知道如何以我满意的方式进行重组!

我倾向于使用服务接口,抽象实现类包含逻辑但不包含 SQL,方法注释为事务性,使用 CreateSqlObject 提供对纯 SQL 的 DAO 类的访问。 一个简化的例子:

界面

public interface AccountService {
    void addAccount(Account account, User user);
}  

执行

public abstract class AccountServiceJdbi implements AccountService {

    <strong i="11">@Override</strong>  
    <strong i="12">@Transaction</strong>  
    public final void addAccount(@BindBean() Account account, User user) {
        long accountId =  accountDao().insertAccount(account);
        accountDao().linkAccountToOwner(accountId, user.getId());
    }

    <strong i="13">@CreateSqlObject</strong>
    abstract AccountDao accountDao();
}

(你可以想象道)

这为逻辑和数据访问提供了非常好的分离,同时允许通过多个 DAO 方法进行事务。 通过实现 DAO 接口,服务的单元测试很容易编写和理解。

我尝试在 JDBI 3 中做类似的事情,但我认为这意味着我的服务类的实现必须是一个接口,其中包含逻辑的默认方法。 默认方法不能成为最终方法,因此代码感觉不够简洁,并且让我无法控制如何使用我的类。

有什么方法可以在 JDBI 3 中构建我的代码以拥有最终的事务方法?

您是正确的,没有办法将接口方法定义为最终方法。

但是,您_可以_ 定义自己的 SQL 方法注释,与@SqlQuery@SqlUpdate等类似,并为具有该注释的方法提供静态实现。

但是,您_可以_ 定义自己的 SQL 方法注释,与@SqlQuery@SqlUpdate等类似,并为具有该注释的方法提供静态实现。

感谢您的回复 - 我不太明白这对我有什么帮助。 在多个 DAO 中进行查询但在同一个事务中执行它们的推荐方法是什么?

如何在多个 DAO 中进行查询,但在同一个事务中执行它们?

我个人使用类似的东西

interface Dao1 {
  @SqlQuery("...")
  void query1();
}

interface Dao2 {
  @SqlQuery("...")
  void query2();
}

interface JdbiServiceImpl extends Service {
  <strong i="8">@CreateSqlObject</strong>
  Dao1 dao1();
  <strong i="9">@CreateSqlObject</strong>
  Dao2 dao2();

  <strong i="10">@Transaction</strong>
  <strong i="11">@Override</strong>
  void businessCase() {
    dao1().query1();
    dao2().query2();
  }
}

Service service = handle.attach(JdbiServiceImpl.class);
service.businessCase();

这种方式对我很有用。 @CreateSqlObject基本上就像 Spring 的 getter/setter 依赖注入。 我将 onDemand 实例作为 bean 放在我的 spring 上下文中,因此它的工作方式与常规服务完全一样,调用者不需要了解 jdbi 或实现接口。

image

您看到的反模式 StockReductionCase 是使用 CreateSqlObject 嵌套 jdbi“依赖注入”的示例。 它内部有自己的依赖关系,就像服务实现一样。 它基本上是一个独立的服务,我只是将它称为 Case 而不是 Service 以避免循环依赖,方法是只将 Cases(在多个地方需要)放入 Services(顶级不可重用逻辑)和 Queries 中。

image

我听说过使用@CreateSqlObject成功使交易正确范围的混合报告,尤其是与onDemand()结合使用时。

在同一个事务中执行多个 SQL 对象的最可靠方法是通过Handle.inTransaction()Jdbi.inTransaction()运行事务。 在回调内部,任何通过Handle.attach()创建的 DAO 都将成为句柄事务的一部分:

jdbi.useTransaction(handle -> {
  Dao1 dao1 = handle.attach(Dao1.class);
  Dao2 dao2 = handle.attach(Dao2.class);

  dao1.doStuff();
  dao2.doMoreStuff();
});

感谢您的回复
@qualidafial这看起来像是我必须走的路,但它确实使为服务类编写单元测试变得困难。 当它实例化 Daos 本身时,我不能用模拟 Daos 代替单元测试。

@TheRealMarnes也感谢 - 这看起来与我尝试过的相似,我只是不喜欢使用默认方法,因为它们可以被覆盖。

@qualidafial我想和你确认一下,我们不能在调用多个 dao 方法的服务方法上使用 jdbi 的@Transaction注释?

如果这个假设是正确的,那么如果没有正确记录为官方文档的一部分,这种行为是非常危险的。 大量开发人员都有使用 Spring 堆栈的经验,并且使用@Transactional注释是支持跨多个 DAO 事务的非常标准的方式。

@svlada @Transaction注释 _only_ 适用于 SQL 对象的方法。 当您说“服务方法”时,我的印象是您在 Jdbi 影响之外的某个对象上使用注释,例如注入的类。

我们的测试套件中的一个示例:

<strong i="9">@Test</strong>
public void testInsertAndFind() {
    Foo foo = handle.attach(Foo.class);
    Something s = foo.insertAndFind(1, "Stephane");
    assertThat(s).isEqualTo(new Something(1, "Stephane"));
}

<strong i="10">@Test</strong>
public void testTransactionPropagates() {
    Foo foo = dbRule.getJdbi().open().attach(Foo.class);

    assertThatExceptionOfType(Exception.class)
        .isThrownBy(() -> foo.insertAndFail(1, "Jeff"));

    Something n = foo.createBar().findById(1);
    assertThat(n).isNull();
}

public interface Foo {
    <strong i="11">@CreateSqlObject</strong>
    Bar createBar();

    @SqlUpdate("insert into something (id, name) values (:id, :name)")
    int insert(@Bind("id") int id, @Bind("name") String name);

    <strong i="12">@Transaction</strong>
    default Something insertAndFind(int id, String name) {
        insert(id, name);
        return createBar().findById(id);
    }

    <strong i="13">@Transaction</strong>
    default Something insertAndFail(int id, String name) {
        insert(id, name);
        return createBar().explode();
    }
}

public interface Bar {
    @SqlQuery("select id, name from something where id = :id")
    Something findById(@Bind("id") int id);

    default Something explode() {
        throw new RuntimeException();
    }
}

我还想澄清一下onDemand() + @CreateSqlObject

  • @CreateSqlObject DAO 只能从创建它们的 DAO 的内部方法中使用——如上例所示。
  • 调用fooDao.createBar().findById()将抛出异常,说明连接已关闭。

很抱歉再次对此发表评论。 你的路线图中有什么可以帮助解决这个问题吗?

我仍然非常怀念能够在抽象类中使用 CreateSqlObject。 必须在接口中做所有事情仍然感觉有限制; 通常我想让“服务”类型的方法具有事务性,但这意味着我的服务类也必须是使用默认方法的接口,我开始失去关注点分离、具体最终方法等的清晰度。

@tamslinn就您使用接口而不是抽象类的问题而言,我想我可以肯定地说,我们不会在不久的将来从该决定中恢复过来。 jdbi3使用jdk Proxies,只支持接口。

我不认为在 cglib 或提供此功能的其他模块上构建的单独贡献模块是完全不可能的,但现在这不是问题。

如果 sqlobject 公理非常适合您,您总是可以将您的服务重构为不被 jdbi 增强的常规类,而是注入一个Jdbi实例来使用流式 API。

至于我们的事务支持、onDemand 等之间的行为的原始主题,这正变得越来越成为工作的重点,尽管我认为我们还没有真正具体的计划。 但我们正在实现它。

非常感谢您的更新。 代理完全有意义。
对我来说,具体限制是无法在 JDBI 类之外启动/结束事务。
(正如@svlada所提到的,在其他框架中使用@Transactional在更高级别上很常见)
不过,我真的很喜欢 JDBI 的其他一切,所以我会坚持下去,也许我会想出一个我对我的服务类感到满意的设计模式 :)

很抱歉再次对此发表评论。 你的路线图中有什么可以帮助解决这个问题吗?

完全没有问题。 这显然是造成混乱的根源,也是我们想要解决的问题——我们只是真的想确保这次做对了,因为我们上次没有:)

(正如@svlada所提到的,在其他框架中在更高级别使用@Transactional很常见)

是的,但据我了解,这一切都是通过您的 DI 框架(如 Spring)中的 AOP 挂钩完成的。 由于 JDBI 不会“包装”您的所有服务对象,因此我们没有机会为不是我们自己创建的对象提供类似 AOP 的解决方案。 即使是旧的cglib实现也只会在 daos 本身上注意到@Transactional ,它永远不会在单独的服务类上工作。

如果有帮助,我们可以考虑添加例如 Spring 和/或 Guice AOP 绑定以允许更高级别的事务。 这不是core的一部分,而是spring扩展的一部分。 这不太可能是核心开发人员所关注的事情,但会认真考虑将贡献纳入其中。 也许这以“更清洁”的方式解决了您的问题?

我们决定从cglib切换到Proxy主要是出于兼容性原因 - Proxy是受支持的 jdk api,而cglib (或者更具体地说是asm )往往会随着每个主要版本(它在 8、9、11 ......

你好,

感谢@stevenschlansker 的信息。 我实际上并没有在我当前的项目中使用 Spring 和 JDBI,只是为了比较而提到的。 我完全理解 AOP 的东西,只是想找到一种方法来解决它,这意味着我可以按照我的意愿拆分代码 - 抽象类曾经为我工作,但远离cglib是完全有意义的

我现在在想,我会在需要时构建我的服务类,这样我就可以在事务中构建它们。

         try (Handle handle = jdbi.open()) {
                handle.useTransaction(h -> {
                    AccountService accountService = new AccountServiceImpl(h.attach(AccountDao.class));                    
                    accountService.addAccount(a, u);
                });
          }

我认为这意味着我可以使用 Mock Dao 对服务进行单元测试,使服务方法保持最终状态,并且不依赖接口中的默认方法进行事务管理,并且服务类实例化的额外开销不应产生重大影响。

只需快速回复您上一条评论中的代码示例:您可以跳过try块并直接调用jdbi.useTransaction() 。 此方法分配一个临时句柄,当您的回调返回时该句柄会自动关闭。

大家好,有一个 PR #1579——从 jdbi 3.10.0 开始,CreateSqlObject 和 onDemand 应该(最终)很好地协同工作。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

mcarabolante picture mcarabolante  ·  4评论

stevenschlansker picture stevenschlansker  ·  4评论

kkrgwbj picture kkrgwbj  ·  4评论

electrum picture electrum  ·  3评论

agavrilov76 picture agavrilov76  ·  5评论