Thanks Lukas. That helps. Curious though, if the JDBC driver doesn't 
provide an async I/O API, what is the gain from adding async methods to the 
jOOQ API? Clients can always wrap jOOQ usage in CompletableFuture.
supplyAsync(() -> ...) themselves and that would at least not perpetuate 
the lie.

On Thursday, May 26, 2016 at 10:22:54 AM UTC-7, Lukas Eder wrote:
>
> 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] <javascript:>>:
>
>> 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.

Reply via email to