Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/manual/working/javaGuide/main/sql/JavaEbean.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,41 @@ If your class is an action, you can annotate your action method with `@play.db.e

@[annotation](code/javaguide/ebean/JavaEbeanTest.java)

The transaction gets committed as soon as the action returns its result, or rolled back if the action throws an exception. As Ebean binds the transaction to the current thread, it only covers the database operations that the action runs on its own thread, before it returns:

- Database operations that run on another thread, e.g. in a `CompletableFuture.supplyAsync`, don't run in the transaction, even if they start before the action returns. Ebean then runs each of them in a transaction of its own, so they aren't atomic together anymore.
- The same applies to the action method itself, if another action that runs between `@Transactional` and the action method calls it asynchronously, e.g. only once an asynchronous check completed.
- If the action throws an exception, the transaction gets rolled back. If it returns a `CompletionStage` that fails instead, even one that failed already, the transaction gets committed.

To run asynchronous database operations in a transaction, begin it on the thread that runs them, and run all database operations of the transaction synchronously in there, e.g. with `DB.executeCall` on an execution context meant for blocking database calls:

@[async](code/javaguide/ebean/TaskService.java)

Don't return a `CompletionStage` from the callable that runs in the transaction, as the transaction would get committed before that completes as well. The `DatabaseExecutionContext` is a [`CustomExecutionContext`](https://www.playframework.com/documentation/latest/JavaAsync#Using-CustomExecutionContext-and-ClassLoaderExecution) for a dispatcher that keeps the blocking database calls off Play's default thread pool:

@[execution-context](code/javaguide/ebean/DatabaseExecutionContext.java)

Configure the dispatcher in `application.conf`. As each of its threads blocks on a database connection, give it as many threads as the connection pool has connections:

```
# Number of database connections
fixedConnectionPool = 9

play.db.prototype.hikaricp {
minimumIdle = ${fixedConnectionPool}
maximumPoolSize = ${fixedConnectionPool}
}

# Worker threads matched to the connection pool
database.dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = ${fixedConnectionPool}
}
}
```

Or if you want a more traditional approach you can begin, commit and rollback transactions explicitly:

@[traditional](code/javaguide/ebean/JavaEbeanTest.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

// #execution-context
// ###replace: package services;
package javaguide.ebean;

import javax.inject.Inject;
import org.apache.pekko.actor.ActorSystem;
import play.libs.concurrent.CustomExecutionContext;

public class DatabaseExecutionContext extends CustomExecutionContext {

@Inject
public DatabaseExecutionContext(ActorSystem actorSystem) {
// The dispatcher configured as "database.dispatcher" in application.conf
super(actorSystem, "database.dispatcher");
}
}
// #execution-context
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.*;
import play.Application;
import play.db.ebean.Transactional;
Expand All @@ -27,6 +28,7 @@ protected Application provideApplication() {
Map<String, String> config = new HashMap<>();
config.putAll(inMemoryDatabase());
config.put("ebean.default", "javaguide.ebean.Task");
config.put("database.dispatcher.executor", "thread-pool-executor");
return fakeApplication(config);
}

Expand Down Expand Up @@ -149,6 +151,21 @@ public void traditional() {
assertThat(Task.find.byId(34L).isDone(), is(true));
}

@Test
public void asyncTransaction() throws Exception {
createTask();

Task task =
app.injector()
.instanceOf(TaskService.class)
.markDone(34L)
.toCompletableFuture()
.get(10, TimeUnit.SECONDS);

assertThat(task.isDone(), is(true));
assertThat(Task.find.byId(34L).isDone(), is(true));
}

@Test
public void startupDataLoader() {
// Evolutions get applied automatically in test mode
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

// #async
// ###replace: package services;
package javaguide.ebean;

import static java.util.concurrent.CompletableFuture.supplyAsync;

import io.ebean.DB;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;

// ###insert: import models.Task;

public class TaskService {

private final DatabaseExecutionContext executionContext;

@Inject
public TaskService(DatabaseExecutionContext executionContext) {
this.executionContext = executionContext;
}

public CompletionStage<Task> markDone(long id) {
return supplyAsync(
() ->
DB.executeCall(
() -> {
// All database operations of the transaction run in here, synchronously
Task task = Task.find.byId(id);
task.setDone(true);

task.save();
return task;
}),
executionContext);
}
}
// #async
12 changes: 11 additions & 1 deletion play-ebean/src/main/java/play/db/ebean/Transactional.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
import java.lang.annotation.Target;
import play.mvc.With;

/** Wraps the annotated action in an Ebean transaction. */
/**
* Wraps the annotated action in an Ebean transaction.
*
* <p>The transaction gets committed as soon as the action returns its result, or rolled back if the
* action throws an exception. As Ebean binds the transaction to the current thread, it only covers
* the database operations that the action runs on its own thread, before it returns. Database
* operations that run on another thread, e.g. in {@code CompletableFuture.supplyAsync}, don't run
* in this transaction, even if they start before the action returns, and a {@code CompletionStage}
* that fails doesn't roll it back. To run asynchronous database operations in a transaction, begin
* it on the thread that runs them, e.g. with {@code DB.executeCall}.
*/
@With(TransactionalAction.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
Expand Down