From 16f778537c62423d2b4857431dda8a028863f66a Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 8 Oct 2026 12:06:08 +0200 Subject: [PATCH 1/3] Fix enhancing single model classes and packages without classes The sbt plugin passed the configured models as they are to Ebean's enhancer, which treats every entry as a package directory. So single classes (e.g. "models.Task", as shown in the docs) and packages without compiled classes (e.g. not existing yet, or provided by a dependency) made it throw. Up to the sbt 2 cross build, the plugin swallowed that error, which also skipped all following entries, so entities silently stayed unenhanced and the application failed at runtime. Since then, compilation fails. Now single classes get enhanced via their package and packages without compiled classes get skipped. --- .../main/scala/play/ebean/sbt/PlayEbean.scala | 18 +++++++++- .../app/models/Task.java | 29 ++++++++++++++++ .../enhancement-model-entries/build.sbt | 17 ++++++++++ .../conf/application.conf | 3 ++ .../project/plugins.sbt | 5 +++ .../sbt-ebean/enhancement-model-entries/test | 2 ++ .../tests/models/TestTask.java | 34 +++++++++++++++++++ 7 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/app/models/Task.java create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/build.sbt create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/conf/application.conf create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/project/plugins.sbt create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/test create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/tests/models/TestTask.java diff --git a/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala b/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala index c1a6c2dd..27f397b8 100644 --- a/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala +++ b/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala @@ -94,6 +94,18 @@ object PlayEbean extends AutoPlugin { // This is replacement of old Stamp `Exists` representation private final val notPresent = "absent" + /** + * Turns the configured models, which can be packages (`models.*`) or single classes (`models.Task`), into the + * packages the enhancer processes. Packages without compiled classes (e.g. not existing yet, or provided by a + * dependency) are skipped, because the enhancer fails for them. + */ + private def packagesToEnhance(models: Seq[String], classes: File): Seq[String] = + models + .map(_.trim) + .map(model => if (model.endsWith(".*")) model.dropRight(2) else model.take(math.max(model.lastIndexOf('.'), 0))) + .distinct + .filter(pkg => new File(classes, pkg.replace('.', '/')).isDirectory) + def ebeanEnhance: Def.Initialize[Task[CompileResult]] = Def.task { @@ -120,7 +132,11 @@ object PlayEbean extends AutoPlugin { val transformer = new Transformer(classLoader, agentArgsString) val fileTransform = new OfflineFileTransform(transformer, classLoader, classes.getAbsolutePath) - fileTransform.process(models.mkString(",")) + val packages = packagesToEnhance(models, classes) + // An empty list would make the enhancer process all classes + if (packages.nonEmpty) { + fileTransform.process(packages.mkString(",")) + } } finally { Thread.currentThread.setContextClassLoader(originalContextClassLoader) diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/app/models/Task.java b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/app/models/Task.java new file mode 100644 index 00000000..0af270d9 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/app/models/Task.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package models; + +import java.util.*; +import jakarta.persistence.*; + +import io.ebean.*; +import play.data.format.*; +import play.data.validation.*; + +@Entity +public class Task extends Model { + + public static final Finder find = new Finder<>(Task.class); + + @Id + public Long id; + + @Constraints.Required + public String name; + + public boolean done; + + @Formats.DateTime(pattern = "dd/MM/yyyy") + public Date dueDate = new Date(); +} diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/build.sbt b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/build.sbt new file mode 100644 index 00000000..c7ee09d6 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/build.sbt @@ -0,0 +1,17 @@ +// Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + +lazy val root = project + .in(file(".")) + .enablePlugins(PlayJava, PlayEbean) + +scalaVersion := sys.props("scala.version") + +resolvers ++= Seq(Resolver.sonatypeCentralSnapshots, Resolver.ApacheMavenSnapshotsRepo) + +Test / sourceDirectory := baseDirectory.value / "tests" + +Test / scalaSource := baseDirectory.value / "tests" + +Test / javaSource := baseDirectory.value / "tests" + +libraryDependencies += "com.h2database" % "h2" % "2.5.252" diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/conf/application.conf b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/conf/application.conf new file mode 100644 index 00000000..62509a28 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/conf/application.conf @@ -0,0 +1,3 @@ +# A package without any compiled classes (listed first, so it must not stop the enhancement of the +# following entries) and a single model class instead of a package +ebean.default = [ "missing.*", "models.Task" ] diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/project/plugins.sbt b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/project/plugins.sbt new file mode 100644 index 00000000..d65f662c --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/project/plugins.sbt @@ -0,0 +1,5 @@ +// Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + +resolvers += Resolver.sonatypeCentralSnapshots + +addSbtPlugin("org.playframework" % "sbt-play-ebean" % sys.props("project.version")) diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/test b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/test new file mode 100644 index 00000000..0f99eb3c --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/test @@ -0,0 +1,2 @@ +> test +$ exists conf/evolutions/default/1.sql \ No newline at end of file diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/tests/models/TestTask.java b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/tests/models/TestTask.java new file mode 100644 index 00000000..06dee9f3 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-model-entries/tests/models/TestTask.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package models; + +import org.junit.*; + +import static org.junit.Assert.*; + +import play.test.*; +import play.Application; + +import static play.test.Helpers.*; + +public class TestTask extends WithApplication { + + protected Application provideApplication() { + return fakeApplication(inMemoryDatabase()); + } + + @Test + public void saveAndFind() { + Task task = new Task(); + task.id = 10L; + task.name = "Hello"; + task.done = false; + task.save(); + + Task saved = Task.find.byId(10L); + assertEquals("Hello", saved.name); + } + +} From 6f52393b4c68bf9c9beece684ac861bf7433a180 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 8 Oct 2026 12:09:43 +0200 Subject: [PATCH 2/3] Fix outdated docs code samples and run the docs tests in CI The ServerConfigStartup and transaction samples still used Ebean APIs that have been removed in the meantime (ServerConfig and the Ebean class), so they did not compile anymore. That went unnoticed, because CI only validated the docs, but never compiled or ran the samples. Their models did not even get enhanced, as the docs build did not enhance its test sources. Also point out that a ServerConfigStartup has to be covered by an "ebean." config line, since Ebean does not scan the classpath for it when used with Play Ebean. --- .github/workflows/build-test.yml | 2 +- docs/build.sbt | 3 ++- .../working/javaGuide/main/sql/JavaEbean.md | 6 ++++-- .../code/javaguide/ebean/JavaEbeanTest.java | 18 ++++++++++-------- .../javaguide/ebean/MyServerConfigStartup.java | 7 ++++--- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ade87e7f..37e5cf2d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -30,7 +30,7 @@ jobs: java: 25, 21, 17 # Symbolic lanes resolve to exact versions in project/Dependencies.scala. scala: 2.13.x, 3.3.x, 3.9.x, 3.next - cmd: cd docs && sbt -Dscala.version=$MATRIX_SCALA validateCode + cmd: cd docs && sbt -Dscala.version=$MATRIX_SCALA "validateCode; test" tests: name: Tests diff --git a/docs/build.sbt b/docs/build.sbt index d74409d6..cfd1a931 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -61,8 +61,9 @@ lazy val docs = project ) .settings(PlayEbean.unscopedSettings) .settings( + // The code samples are test sources, so enhance their models there inConfig(Test)( - Seq( + PlayEbean.scopedSettings ++ Seq( playEbeanModels := Seq("javaguide.ebean.*") ) ) diff --git a/docs/manual/working/javaGuide/main/sql/JavaEbean.md b/docs/manual/working/javaGuide/main/sql/JavaEbean.md index 8ecb2284..96da08e4 100644 --- a/docs/manual/working/javaGuide/main/sql/JavaEbean.md +++ b/docs/manual/working/javaGuide/main/sql/JavaEbean.md @@ -37,12 +37,14 @@ In this example, we have access to two Ebean servers - each using its own databa Each `ebean.` config line (as above) can map *any* classes that Ebean may be interested in registering (eg. `@Entity`/`Model` classes, `@Embeddable`s, custom `ScalarType`s and `CompoundType`s, `BeanPersistController`s, `BeanPersistListener`s, `BeanFinder`s, `ServerConfigStartup`s, etc). These can be individually listed separated by commas, and/or you can use the wildcard `.*`. For example, `models.*` registers with Ebean all classes within the models package that Ebean can make use of. -To customise the underlying Ebean Server configuration, you can either add a [`conf/application.yaml`](https://ebean.io/docs/intro/configuration/) file, or create an instance of the `ServerConfigStartup` interface to programmatically manipulate the Ebean `ServerConfig` before the server is initialised. +To customise the underlying Ebean Server configuration, you can either add a [`conf/application.yaml`](https://ebean.io/docs/intro/configuration/) file, or implement the `ServerConfigStartup` interface to programmatically manipulate the Ebean `DatabaseBuilder` before the server is initialised. As an example, the fairly common problem of reducing the sequence batch size in order to minimise sequence gaps, could be solved quite simply with a class like this: @[content](code/javaguide/ebean/MyServerConfigStartup.java) +Make sure that such a class is covered by an `ebean.` config line, for example by putting it into the `models` package. Play Ebean registers the configured classes with Ebean explicitly, which disables Ebean's own classpath scanning, so Ebean silently ignores any `ServerConfigStartup` that is not covered. + Note that Ebean will also make use of a `conf/orm.xml` file (if present), to configure ``. #### Using a read replica @@ -108,7 +110,7 @@ By default Ebean will use transactions. However these transactions will be creat @[transaction](code/javaguide/ebean/JavaEbeanTest.java) -So, if you want to do more than one action in the same transaction you can use TxRunnable and TxCallable: +So, if you want to do more than one action in the same transaction you can use `DB.execute` (or `DB.executeCall` to return a value): @[txrunnable](code/javaguide/ebean/JavaEbeanTest.java) diff --git a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java index 2ac60307..5aa31174 100644 --- a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java +++ b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/JavaEbeanTest.java @@ -8,7 +8,8 @@ import static org.junit.Assert.*; import static play.test.Helpers.*; -import io.ebean.Ebean; +import io.ebean.DB; +import io.ebean.Transaction; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -91,11 +92,11 @@ public void txRunnable() { // ###insert: ... - Ebean.execute( + DB.execute( () -> { // code running in "REQUIRED" transactional scope // ... as "REQUIRED" is the default TxType - System.out.println(Ebean.currentTransaction()); + System.out.println(DB.currentTransaction()); Task task = Task.find.byId(34L); task.setDone(true); @@ -131,16 +132,17 @@ public void traditional() { createTask(); // #traditional - Ebean.beginTransaction(); - try { + // ###insert: import io.ebean.*; + + // ###insert: ... + + try (Transaction transaction = DB.beginTransaction()) { Task task = Task.find.byId(34L); task.setDone(true); task.save(); - Ebean.commitTransaction(); - } finally { - Ebean.endTransaction(); + transaction.commit(); } // #traditional diff --git a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/MyServerConfigStartup.java b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/MyServerConfigStartup.java index 6c82a80c..4933165f 100644 --- a/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/MyServerConfigStartup.java +++ b/docs/manual/working/javaGuide/main/sql/code/javaguide/ebean/MyServerConfigStartup.java @@ -6,12 +6,13 @@ // ###replace: package models; package javaguide.ebean; -import io.ebean.config.ServerConfig; +import io.ebean.DatabaseBuilder; import io.ebean.event.ServerConfigStartup; public class MyServerConfigStartup implements ServerConfigStartup { - public void onStart(ServerConfig serverConfig) { - serverConfig.setDatabaseSequenceBatchSize(1); + @Override + public void onStart(DatabaseBuilder config) { + config.databaseSequenceBatchSize(1); } } // #content From 7cc06bdd888eee91d50735f2675956936fd66e48 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Thu, 8 Oct 2026 12:39:00 +0200 Subject: [PATCH 3/3] Keep enhancing plain package names as packages playEbeanModels can also contain package names without a ".*" suffix, e.g. Seq("models"), which Ebean's enhancer handles just like "models.*". However, the previous commit took them for single classes and enhanced their parent package instead, which for "models" is the whole classes directory. Now only entries that are no package with compiled classes are treated as single classes. --- .../main/scala/play/ebean/sbt/PlayEbean.scala | 19 ++++++++---- .../app/models/Task.java | 29 +++++++++++++++++++ .../app/other/Other.java | 14 +++++++++ .../enhancement-plain-package/build.sbt | 20 +++++++++++++ .../project/plugins.sbt | 5 ++++ .../sbt-ebean/enhancement-plain-package/test | 1 + .../tests/models/EnhancementTest.java | 20 +++++++++++++ 7 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/models/Task.java create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/other/Other.java create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/build.sbt create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/project/plugins.sbt create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/test create mode 100644 sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/tests/models/EnhancementTest.java diff --git a/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala b/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala index 27f397b8..5eae7851 100644 --- a/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala +++ b/sbt-play-ebean/src/main/scala/play/ebean/sbt/PlayEbean.scala @@ -95,16 +95,23 @@ object PlayEbean extends AutoPlugin { private final val notPresent = "absent" /** - * Turns the configured models, which can be packages (`models.*`) or single classes (`models.Task`), into the - * packages the enhancer processes. Packages without compiled classes (e.g. not existing yet, or provided by a - * dependency) are skipped, because the enhancer fails for them. + * Turns the configured models into the packages the enhancer processes: packages (`models.*` or `models`) as they + * are, and single classes (`models.Task`) via their package. Entries without compiled classes (e.g. packages not + * existing yet, or provided by a dependency) are skipped, because the enhancer fails for them. */ - private def packagesToEnhance(models: Seq[String], classes: File): Seq[String] = + private def packagesToEnhance(models: Seq[String], classes: File): Seq[String] = { + def isPackage(name: String) = new File(classes, name.replace('.', '/')).isDirectory models .map(_.trim) - .map(model => if (model.endsWith(".*")) model.dropRight(2) else model.take(math.max(model.lastIndexOf('.'), 0))) + .flatMap { model => + // Like the enhancer, ignore a trailing ".*" or ".**" + val pkg = model.stripSuffix("*").stripSuffix("*").stripSuffix(".") + if (isPackage(pkg)) Some(pkg) + else if (pkg == model && model.contains('.')) Some(model.substring(0, model.lastIndexOf('.'))).filter(isPackage) + else None + } .distinct - .filter(pkg => new File(classes, pkg.replace('.', '/')).isDirectory) + } def ebeanEnhance: Def.Initialize[Task[CompileResult]] = Def.task { diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/models/Task.java b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/models/Task.java new file mode 100644 index 00000000..0af270d9 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/models/Task.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package models; + +import java.util.*; +import jakarta.persistence.*; + +import io.ebean.*; +import play.data.format.*; +import play.data.validation.*; + +@Entity +public class Task extends Model { + + public static final Finder find = new Finder<>(Task.class); + + @Id + public Long id; + + @Constraints.Required + public String name; + + public boolean done; + + @Formats.DateTime(pattern = "dd/MM/yyyy") + public Date dueDate = new Date(); +} diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/other/Other.java b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/other/Other.java new file mode 100644 index 00000000..468bf1b5 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/app/other/Other.java @@ -0,0 +1,14 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package other; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** An entity outside of the configured package, so it must not get enhanced. */ +@Entity +public class Other { + @Id public Long id; +} diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/build.sbt b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/build.sbt new file mode 100644 index 00000000..904fd58c --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/build.sbt @@ -0,0 +1,20 @@ +// Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + +lazy val root = project + .in(file(".")) + .enablePlugins(PlayJava, PlayEbean) + +scalaVersion := sys.props("scala.version") + +resolvers ++= Seq(Resolver.sonatypeCentralSnapshots, Resolver.ApacheMavenSnapshotsRepo) + +Test / sourceDirectory := baseDirectory.value / "tests" + +Test / scalaSource := baseDirectory.value / "tests" + +Test / javaSource := baseDirectory.value / "tests" + +libraryDependencies += "com.h2database" % "h2" % "2.5.252" + +// A plain package name (without ".*"), which must not be taken for a class +Compile / playEbeanModels := Seq("models") diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/project/plugins.sbt b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/project/plugins.sbt new file mode 100644 index 00000000..d65f662c --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/project/plugins.sbt @@ -0,0 +1,5 @@ +// Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + +resolvers += Resolver.sonatypeCentralSnapshots + +addSbtPlugin("org.playframework" % "sbt-play-ebean" % sys.props("project.version")) diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/test b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/test new file mode 100644 index 00000000..dfffb838 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/test @@ -0,0 +1 @@ +> test diff --git a/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/tests/models/EnhancementTest.java b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/tests/models/EnhancementTest.java new file mode 100644 index 00000000..7c752550 --- /dev/null +++ b/sbt-play-ebean/src/sbt-test/sbt-ebean/enhancement-plain-package/tests/models/EnhancementTest.java @@ -0,0 +1,20 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package models; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import io.ebean.bean.EntityBean; +import org.junit.Test; + +public class EnhancementTest { + + @Test + public void enhancesOnlyTheConfiguredPackage() { + assertTrue(EntityBean.class.isAssignableFrom(models.Task.class)); + assertFalse(EntityBean.class.isAssignableFrom(other.Other.class)); + } +}