Hi Nick, Thanks for your detailed message! Great to hear feedback about the new jOOQ 3.8 async API.
First off, the disappointment. Asynchronous database access is a lie, inevitably. For two reasons: 1. JDBC is blocking 2. Most databases are blocking (except for SQL Server and PostgreSQL to my knowledge, which ship with obscure, nonstandard, nonblocking APIs, but not JDBC) And if you want, there's a third argument, which isn't as strict 3. Most Java APIs (Spring, JTA, etc.) assume thread-bound transactions. It is *very* unusual to retrieve a Connection from a pool and pass it around between threads. With this in mind, let's review your code. 2016-05-26 2:12 GMT+02:00 <[email protected]>: > Neither in the jOOQ 3.8 manual, nor anywhere online, can I find examples > of how to use the new transactionAsync() and transactionResultAsync() > methods. > True, that's still a TODO, unfortunately. > > I have an existing method that I'd like to transition to the async > paradigm. It goes something like this: > > Mutator<User> mutator = u -> { ... }; > User updatedUser = > DSL.using(configuration).transactionResult(transactionConfiguration -> { > UserRecord userRecord = DSL.using(transactionConfiguration) > .selectFrom(USER) > .where(USER.USER_ID.equal(userId)) > .forUpdate() > .fetchOne(); > > if (userRecord == null) { > throw new ObjectNotFoundException("User(userId = %d) not found", > userId); > } > else { > User user = new User(userRecord); > mutator.mutate(user); > userRecord.store(); > return user; > } > }); > > fetchOne(), store(), and transactionResult() are all blocking calls. The > first obvious step is to use the new transactionResultAsync() method, > which looks like this: > > CompletionStage<User> updatedUserStage > = > DSL.using(configuration).transactionResultAsync(transactionConfiguration -> > { > > UserRecord userRecord = DSL.using(transactionConfiguration) > .selectFrom(USER) > .where(USER.USER_ID.equal(userId)) > .forUpdate() > .fetchOne(); > > if (userRecord == null) { > throw new ObjectNotFoundException("User(userId = %d) not found", > userId); > } > else { > User user = new User(userRecord); > mutator.mutate(user); > userRecord.store(); > return user; > } > }); > > This is a step in the right direction. The caller of the method now has a > CompletionStage that can be chained, composed, etc. in a non-blocking > way. But store() and fetchOne() are still blocking calls, so the executor > thread that invokes the transactionResultAsync() lambda, is going to > block. > Yes, indeed. See the above rationale about transactions usually being thread-bound. One might accept this as "oh well". But if you can guarantee (through your connection pool implementation) that this thread-bound-ness is not a requirement, we can go on This is where I'm uncertain on how to proceed. Was it the designers' intent > that we would stop here? I'd like to continue with fetchAsync() but it > starts to get hairy: > > CompletionStage<CompletionStage<User>> userCompletionStageCompletionStage > = DSL.using(configuration) > .transactionResultAsync(transactionConfiguration -> > DSL.using(transactionConfiguration) > .selectFrom(USER) > .where(USER.USER_ID.equal(userId)) > .forUpdate() > .fetchAsync() > .thenApply(result -> { > if (result.isEmpty()) { > throw new ObjectNotFoundException("User(userId = > %d) not found", userId); > } > else { > UserRecord userRecord = result.get(0); > User user = new User(userRecord); > mutator.mutate(user); > userRecord.store(); > return user; > } > }) > ); > > That just doesn't seem right. > No, it doesn't. Do note that not everyone uses transactions. FetchAsync() can be used as a standalone, chainable asynchronous fetch, e.g. with auto commit = true, or with readonly transaction mode. The two APIs (transactionResultAsync() and fetchAsync()) are orthogonal, not really composable. > If it wasn't for the fact that the asynchronous transactional scope > "wraps" the we could compose the stages together and flatten the stages. > This is, in fact, the approach we could take if we wanted to squash that > last blocking call, store(): > > CompletionStage<CompletionStage<User>> userCompletionStageCompletionStage > = DSL.using(configuration) > .transactionResultAsync(transactionConfiguration -> { > CompletionStage<Result<UserRecord>> > selectCompletionStage = DSL.using(transactionConfiguration) > .selectFrom(USER) > .where(USER.USER_ID.equal(userId)) > .forUpdate() > .fetchAsync(); > > Function<Result<UserRecord>, CompletionStage<User>> > mutateAndPersistAsync = result -> { > if (result.isEmpty()) { > throw new ObjectNotFoundException("User(userId > = %d) not found", userId); > } > else { > return CompletableFuture.supplyAsync(() -> { > UserRecord userRecord = result.get(0); > User user = new User(userRecord); > mutator.mutate(user); > userRecord.store(); > return user; > }, configuration.executorProvider().provide()); > } > }; > > return > selectCompletionStage.thenCompose(mutateAndPersistAsync); > }); > > > But that still leaves us with CompletionStage<CompletionStage<User>>. Am > I missing something? Any and all feedback appreciated. > That doesn't look right :) But this discussion made me think about an alternative transaction API. What we would really need for cases like yours is an API that would allow for (pseudo code): ctx.beginTransactionAsync() .thenApply(... -> ...) .thenApply(... -> ...) .thenApply(... -> commit()); With this model, the BEGIN TRANSACTION and COMMIT commands would be just like any other "async" database interaction, instead of the current API, which assumes that an "async" transaction is atomically blocking. Does that make sense? -- You received this message because you are subscribed to the Google Groups "jOOQ User Group" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. For more options, visit https://groups.google.com/d/optout.
