问题
JDBI 官方文档让开发人员相信@CreateSqlObject注解是为不同的 DAO 添加事务支持的机制。
http://jdbi.org/#__createsqlobject
这似乎有点误导实际行为,当使用首选的jdbi.onDemand
创建机制时可能导致状态错误。
论坛中有几条消息说明了这种困惑:
https://groups.google.com/forum/#!searchin/jdbi/CreateSqlObject %7 Csort:date/jdbi/FPI1l1OBLU0/slXj_8hJBwAJ
https://groups.google.com/forum/#!searchin/jdbi/CreateSqlObject %7 Csort:date/jdbi/b1rWVQ7JNAg/2R2c9okAAgAJ
要求
不确定这个问题的最佳解决方案是什么。 想到的一些:
感谢您报告此事。 我怀疑我们最终会在这里进行技术上的重大更改,但我认为现有的行为很糟糕,以至于我们在 3.x 发布周期的早期进行了技术上的重大更改。 如果您依赖于现有行为或不同意可能在此处发布小的重大更改,请标记此问题。
仅就 _why_ @CreateSqlObject
的上下文而言,点播效果不佳:
@CreateSqlObject
是使用handle.attach(sqlObjectType)
实现的,带有原始 SQL 对象的支持句柄。因此,创建的 SQL 对象的支持句柄在 SQL 对象甚至可以返回之前就关闭了。
以上都不是一成不变的——这就是它现在的实施方式。
我不相信我们必须打破兼容性来解决这个问题。
抱歉,我真的不明白... onDemand 和 CreateSqlObject 到底有什么问题? 我从来没有遇到过任何问题,关于@qualidafial的解释只是让我的思绪开始倾斜......
为调用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 或实现接口。
您看到的反模式 StockReductionCase 是使用 CreateSqlObject 嵌套 jdbi“依赖注入”的示例。 它内部有自己的依赖关系,就像服务实现一样。 它基本上是一个独立的服务,我只是将它称为 Case 而不是 Service 以避免循环依赖,方法是只将 Cases(在多个地方需要)放入 Services(顶级不可重用逻辑)和 Queries 中。
我听说过使用@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 应该(最终)很好地协同工作。
最有用的评论
完全没有问题。 这显然是造成混乱的根源,也是我们想要解决的问题——我们只是真的想确保这次做对了,因为我们上次没有:)
是的,但据我了解,这一切都是通过您的 DI 框架(如 Spring)中的 AOP 挂钩完成的。 由于 JDBI 不会“包装”您的所有服务对象,因此我们没有机会为不是我们自己创建的对象提供类似 AOP 的解决方案。 即使是旧的
cglib
实现也只会在 daos 本身上注意到@Transactional
,它永远不会在单独的服务类上工作。如果有帮助,我们可以考虑添加例如 Spring 和/或 Guice AOP 绑定以允许更高级别的事务。 这不是
core
的一部分,而是spring
扩展的一部分。 这不太可能是核心开发人员所关注的事情,但会认真考虑将贡献纳入其中。 也许这以“更清洁”的方式解决了您的问题?我们决定从
cglib
切换到Proxy
主要是出于兼容性原因 -Proxy
是受支持的 jdk api,而cglib
(或者更具体地说是asm
)往往会随着每个主要版本(它在 8、9、11 ......