mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8e92791cd | ||
|
|
94e63cc30f | ||
|
|
3567263250 | ||
|
|
bc03f8d516 | ||
|
|
7035ff20eb | ||
|
|
f38479022a | ||
|
|
986cc905d8 | ||
|
|
8f3fe688cb | ||
|
|
eee26d9ed3 | ||
|
|
44bd60e586 | ||
|
|
20d7af6d25 | ||
|
|
e5aeda9d4e | ||
|
|
46bb1ca060 | ||
|
|
0652167101 | ||
|
|
6633293151 | ||
|
|
9e603f2848 | ||
|
|
fff345ffc8 | ||
|
|
dd1fe845aa | ||
|
|
80451c3c62 | ||
|
|
2d199737aa | ||
|
|
63a1e9907d | ||
|
|
67db089646 | ||
|
|
26478924a7 | ||
|
|
3cb22fb0cc | ||
|
|
4dde7064ba | ||
|
|
681221c0ff | ||
|
|
d6ce093ac5 | ||
|
|
515b8e5958 | ||
|
|
86b4af1321 | ||
|
|
eb73318f53 | ||
|
|
96a3ab9d79 | ||
|
|
9954ad1ea7 | ||
|
|
2928247ba7 | ||
|
|
3066bb4f74 | ||
|
|
a96144cdaa | ||
|
|
50e20c3aa0 | ||
|
|
f07ca68eb3 | ||
|
|
0ec7ddb82d | ||
|
|
8074a24d03 | ||
|
|
7e4a2db9aa | ||
|
|
292dcf4b0e | ||
|
|
f44bf59cb6 | ||
|
|
7c3741e258 | ||
|
|
afadaa7208 | ||
|
|
46b7510e6c | ||
|
|
bcb0b049ff | ||
|
|
42bfaaf2bf | ||
|
|
2ca39f1419 | ||
|
|
de2914e550 | ||
|
|
6eda7044a0 | ||
|
|
e4adf25022 | ||
|
|
ad5483100e | ||
|
|
6d9ed670ef | ||
|
|
9e2d821475 | ||
|
|
80ce7a41a2 | ||
|
|
6458bb0f1c | ||
|
|
7ba550cc0d | ||
|
|
fff8dfb8b1 | ||
|
|
d0af7b1788 | ||
|
|
ebb0305cf3 | ||
|
|
d03d36bba4 | ||
|
|
88408edd24 | ||
|
|
0aad35b840 | ||
|
|
e7979b285d | ||
|
|
91920b00dc | ||
|
|
bdfe016d2d | ||
|
|
b2670f9cc0 | ||
|
|
ebc90e0e82 | ||
|
|
c4230c780f | ||
|
|
794f933310 | ||
|
|
c8a7a263a9 | ||
|
|
aeef6d0ea2 | ||
|
|
a99aef8b00 | ||
|
|
6f17cc6327 | ||
|
|
903947b3cb | ||
|
|
797f75f7b7 | ||
|
|
af443ef2f2 | ||
|
|
ad4f027835 |
@@ -81,6 +81,18 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
|
||||
## Documentation
|
||||
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
|
||||
|
||||
## Guides
|
||||
Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
|
||||
|
||||
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
|
||||
|
||||
Available guides:
|
||||
- [Maven POM setup](docs/guides/add-ebean-postgres-maven-pom.md)
|
||||
- [Database configuration](docs/guides/add-ebean-postgres-database-config.md)
|
||||
- [Test container setup](docs/guides/add-ebean-postgres-test-container.md)
|
||||
- [DB migration generation](docs/guides/add-ebean-db-migration-generation.md)
|
||||
- [Lombok with Ebean entity beans](docs/guides/lombok-with-ebean-entity-beans.md)
|
||||
|
||||
## Maven central
|
||||
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
# Ebean ORM Library Definition
|
||||
|
||||
Ebean is an ORM library for Java and Kotlin focused on relational data access, type-safe query construction, and production-friendly SQL behavior.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: Ebean ORM
|
||||
- **Package**: `io.ebean`
|
||||
- **Primary Maven Group**: `io.ebean`
|
||||
- **Category**: ORM / Data Access
|
||||
- **Repository**: https://github.com/ebean-orm/ebean
|
||||
- **Issues**: https://github.com/ebean-orm/ebean/issues
|
||||
- **Discussions**: https://github.com/ebean-orm/ebean/discussions
|
||||
- **Website**: https://ebean.io/
|
||||
- **Documentation**: https://ebean.io/docs/
|
||||
- **License**: Apache 2.0
|
||||
|
||||
## Version & Requirements
|
||||
|
||||
- **Repository Version (this checkout)**: `16.5.0` (from repository `pom.xml`)
|
||||
- **Minimum Java Version**: 11+
|
||||
- **Languages**: Java, Kotlin
|
||||
- **Build Tooling in this docs set**: Maven-focused examples
|
||||
|
||||
## Core Artifacts
|
||||
|
||||
| Artifact | Purpose |
|
||||
|------|------|
|
||||
| `io.ebean:ebean` | Core ORM runtime and API |
|
||||
| `io.ebean:ebean-postgres` | PostgreSQL platform bundle used in setup guides |
|
||||
| `io.ebean:ebean-test` | Test support, including Docker-backed database testing |
|
||||
| `io.ebean:querybean-generator` | Generates `Q*` type-safe query beans |
|
||||
| `io.ebean:ebean-maven-plugin` | Bytecode enhancement for entities at build time |
|
||||
| `io.ebean:ebean-migration` | Runtime migration runner (often transitive via platform artifact) |
|
||||
|
||||
## Core APIs & Annotations
|
||||
|
||||
### Database and transaction APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `DB.getDefault()` | Access default `Database` | `Database db = DB.getDefault();` |
|
||||
| `DB.byName("...")` | Access named `Database` | `Database reporting = DB.byName("reporting");` |
|
||||
| `database.find(...)` | Query entities | `Customer c = database.find(Customer.class, id);` |
|
||||
| `database.insert/save/update/delete` | Persist entity changes | `database.save(customer);` |
|
||||
| `database.beginTransaction()` | Manual transaction boundary | `try (Transaction txn = database.beginTransaction()) { ... }` |
|
||||
| `Database.builder()` | Programmatic `Database` setup | `Database.builder().loadFromProperties().build();` |
|
||||
|
||||
### Query APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `Q*` query beans | Type-safe query construction | `new QCustomer().status.equalTo(ACTIVE).findList();` |
|
||||
| `exists()` | Efficient existence checks | `new QCustomer().email.equalTo(email).exists();` |
|
||||
| `findOne()` | Unique/single-row retrieval | `new QCustomer().id.equalTo(id).findOne();` |
|
||||
| `findList()` | List retrieval | `new QCustomer().findList();` |
|
||||
| `asDto(...).findList()` | DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
||||
|
||||
### Entity mapping and lifecycle annotations
|
||||
|
||||
| Annotation | Purpose |
|
||||
|------|------|
|
||||
| `@Entity` | Marks class as persistent entity |
|
||||
| `@Id` | Primary key mapping |
|
||||
| `@Version` | Optimistic locking |
|
||||
| `@WhenCreated` | Creation timestamp management |
|
||||
| `@WhenModified` | Modification timestamp management |
|
||||
| `@Transactional` | Declarative transaction boundary |
|
||||
|
||||
## Capabilities
|
||||
|
||||
### ✅ Included
|
||||
|
||||
- Relational ORM with automatic dirty checking and lazy loading (via enhancement)
|
||||
- Multiple query abstraction levels (ORM query, DTO query, SQL/JDBC)
|
||||
- Type-safe query beans (`Q*`) with IDE autocomplete
|
||||
- Built-in migration generation and migration running support
|
||||
- Transaction APIs for implicit, declarative, and explicit transaction control
|
||||
- Support for test-time Docker database workflows
|
||||
- Query tuning and caching features for performance-sensitive workloads
|
||||
|
||||
### ❌ Not in scope
|
||||
|
||||
- HTTP routing, REST controllers, or web server runtime
|
||||
- Dependency injection container functionality
|
||||
- JSON serialization framework responsibilities
|
||||
- Front-end/UI rendering concerns
|
||||
|
||||
Ebean is intentionally focused on persistence and data access. Pair it with a web framework and DI library as needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### ✅ Strong fit
|
||||
|
||||
- SQL-backed business applications with rich domain models
|
||||
- Services that need both ORM productivity and SQL-level control
|
||||
- Projects requiring type-safe query authoring via generated query beans
|
||||
- Teams that want migration generation integrated with entity model changes
|
||||
- Integration test suites that need real database behavior (not only in-memory mocks)
|
||||
|
||||
### ⚠️ Consider alternatives if
|
||||
|
||||
- You need a full web framework (routing/controllers) rather than a persistence layer
|
||||
- Your project does not use relational databases as a core storage model
|
||||
- You want a single library to cover persistence, DI, and HTTP all at once
|
||||
|
||||
## Quick Start (Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<ebean.version><!-- use latest stable from Maven Central --></ebean.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-maven-plugin</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```java
|
||||
import io.ebean.DB;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
Database database = DB.getDefault(); // or injected
|
||||
|
||||
Customer customer = database.find(Customer.class, 42);
|
||||
customer.setName("Updated");
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
## Common Tasks & Guides
|
||||
|
||||
| Task | Guide |
|
||||
|------|------|
|
||||
| Add Ebean to an existing Maven project | [add-ebean-postgres-maven-pom.md](guides/add-ebean-postgres-maven-pom.md) |
|
||||
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
|
||||
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
|
||||
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
|
||||
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
|
||||
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
|
||||
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
|
||||
| Persist changes and manage transactions | [persisting-and-transactions-with-ebean.md](guides/persisting-and-transactions-with-ebean.md) |
|
||||
| Build test entities quickly | [testing-with-testentitybuilder.md](guides/testing-with-testentitybuilder.md) |
|
||||
|
||||
**Guides index**: [guides/README.md](guides/README.md)
|
||||
|
||||
## Related Ecosystem Docs
|
||||
|
||||
- [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)
|
||||
- [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)
|
||||
- [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
### For Claude, GPT, and web-based agents
|
||||
|
||||
Use this file as the top-level reference when answering Ebean questions.
|
||||
|
||||
1. Check this file first for scope and capability fit.
|
||||
2. Route implementation tasks to the relevant guide in **Common Tasks & Guides**.
|
||||
3. Treat Ebean as the persistence layer only; avoid implying it provides HTTP/DI features.
|
||||
4. Prefer type-safe query bean examples when showing query code.
|
||||
5. For setup and migration changes, follow the Maven-focused guide steps exactly.
|
||||
|
||||
### For IDE-based agents (Copilot, Cursor, etc.)
|
||||
|
||||
If `docs/LIBRARY.md` is not in context automatically:
|
||||
|
||||
1. Read `README.md` for docs entry points.
|
||||
2. Open `docs/guides/README.md` for task-specific guides.
|
||||
3. Follow linked guide files directly for concrete implementation steps.
|
||||
|
||||
---
|
||||
|
||||
## Notes for Maintainers
|
||||
|
||||
### When to update this file
|
||||
|
||||
- New release that changes requirements or key APIs
|
||||
- New guide added to `docs/guides/`
|
||||
- Capability/scope changes that affect "Included" or "Not in scope"
|
||||
- Significant migration or setup workflow changes
|
||||
|
||||
### Maintenance checklist
|
||||
|
||||
- [ ] Keep requirements and version references accurate
|
||||
- [ ] Keep Common Tasks table aligned with `docs/guides/README.md`
|
||||
- [ ] Keep artifact names/snippets aligned with setup guides
|
||||
- [ ] Keep AI instructions aligned with current docs structure
|
||||
|
||||
### Link from repository README
|
||||
|
||||
In `README.md`, include:
|
||||
|
||||
```markdown
|
||||
## Documentation
|
||||
|
||||
- [Ebean docs](https://ebean.io/docs/)
|
||||
- [Library reference](docs/LIBRARY.md)
|
||||
- [Step-by-step guides](docs/guides/README.md)
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# AGENTS.md — Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io).
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
Step-by-step guides for common Ebean tasks (Maven setup, database config, query writing, transactions, test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
|
||||
|
||||
Key guides (fetch and follow when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
|
||||
Agents: Before performing any Ebean-related task, fetch and follow the relevant guide above.
|
||||
@@ -0,0 +1,197 @@
|
||||
# Guides
|
||||
|
||||
See also: [AGENTS.md](AGENTS.md) — a minimal template for AI agent onboarding and automation in Ebean ORM projects.
|
||||
|
||||
Step-by-step guides written as instructions for AI agents and developers.
|
||||
|
||||
For a high-level capability reference (scope, core APIs, and AI guidance), see
|
||||
[../LIBRARY.md](../LIBRARY.md).
|
||||
|
||||
## Adding Ebean ORM with PostgreSQL to an existing Maven project
|
||||
|
||||
A three-part guide covering everything needed to wire Ebean + PostgreSQL into an
|
||||
existing Maven project. Complete the steps in order.
|
||||
|
||||
| Step | Guide | Description |
|
||||
|------|-------|-------------|
|
||||
| 1 | [Maven POM setup](add-ebean-postgres-maven-pom.md) | Add Ebean dependencies, the enhancement plugin, and the querybean-generator annotation processor to `pom.xml` |
|
||||
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
|
||||
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
|
||||
|
||||
## Migration & upgrades
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
|
||||
|
||||
## Observability
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
|
||||
|
||||
## Entity beans
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
|
||||
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
|
||||
|
||||
## Querying
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
|
||||
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
|
||||
|
||||
## Persisting & transactions
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Persisting and transactions with Ebean](persisting-and-transactions-with-ebean.md) | Step-by-step guidance for AI agents to choose `insert` / `save` / `update` / `delete`; inspect cascades; select the right transaction boundary; and use batch or bulk update for large write sets |
|
||||
|
||||
## Testing
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
|
||||
|
||||
## Database migrations
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [DB migration generation](add-ebean-db-migration-generation.md) | Add `GenerateDbMigration.java` to generate schema diff migrations offline; configure the migration runner; understand `.sql` and `.model.xml` output files; workflow for pending drops |
|
||||
|
||||
## Connection Pooling & DataSource Configuration
|
||||
|
||||
The [ebean-datasource](https://github.com/ebean-orm/ebean-datasource) project provides
|
||||
comprehensive guides on connection pool configuration and best practices. These are particularly
|
||||
useful for production deployments, especially in Kubernetes or AWS environments:
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md) | Step-by-step guide for basic, read-only, Kubernetes, and AWS Lambda datasource configurations |
|
||||
| [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md) | Setting up dual DataSources with Aurora read and write endpoints for Ebean integration |
|
||||
| [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md) | Understanding connection heartbeat strategies and why `Connection.isValid()` is the recommended approach |
|
||||
|
||||
---
|
||||
|
||||
## Helping AI agents find these guides
|
||||
|
||||
### Agent Skills (recommended)
|
||||
|
||||
The **ebean-orm skill** packages these guides into a structured format that AI agents
|
||||
load on-demand. See the [ebean-orm/skills](https://github.com/ebean-orm/skills) repo
|
||||
for installation instructions. Quick start:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:ebean-orm/skills.git ~/.agents/ebean-skills
|
||||
mkdir -p ~/.agents/skills
|
||||
ln -sf ~/.agents/ebean-skills/ebean-orm ~/.agents/skills/ebean-orm
|
||||
```
|
||||
|
||||
This works with [pi](https://github.com/mariozechner/pi-coding-agent),
|
||||
[Claude Code](https://docs.anthropic.com/en/docs/claude-code), and any harness
|
||||
that supports the [Agent Skills standard](https://agentskills.io/specification).
|
||||
|
||||
### Other approaches
|
||||
|
||||
AI coding agents can only follow these guides if they know they exist. Below are
|
||||
copy-paste snippets for the most common AI tooling configurations. Add whichever
|
||||
applies to your project.
|
||||
|
||||
### llms.txt (fastest bootstrap for any agent)
|
||||
|
||||
`https://ebean.io/llms.txt` provides a concise Ebean capability reference and links
|
||||
to all guides in a format optimised for LLMs. Agents that check `llms.txt` before
|
||||
starting work will find everything they need without requiring project-level
|
||||
configuration. This is worth knowing if you are an AI agent reading this file.
|
||||
|
||||
|
||||
### Project `README.md` (universal — works with all AI tools)
|
||||
|
||||
Add a section near the top of your project README:
|
||||
|
||||
```markdown
|
||||
## AI Agent Instructions
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
Ebean tasks (Maven setup, database configuration, query writing, transactions,
|
||||
test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
|
||||
|
||||
AI agents should fetch and follow the relevant guide before performing Ebean-related
|
||||
tasks.
|
||||
```
|
||||
|
||||
### Why README is important
|
||||
|
||||
Many AI agents (Copilot, Claude, Cursor, Aider) read the project `README.md`
|
||||
automatically as part of understanding the codebase. A visible link in README is the
|
||||
most universally effective way to ensure any agent working on your project will
|
||||
discover the guides without requiring tool-specific configuration files.
|
||||
|
||||
------
|
||||
|
||||
### GitHub Copilot — `.github/copilot-instructions.md`
|
||||
|
||||
```markdown
|
||||
## Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
|
||||
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
```
|
||||
|
||||
### Claude Code — `CLAUDE.md`
|
||||
|
||||
Same content as above — Claude Code reads `CLAUDE.md` at the project root.
|
||||
|
||||
### AGENTS.md — OpenAI Codex / GitHub Copilot coding agent
|
||||
|
||||
Place an `AGENTS.md` at your repo root:
|
||||
|
||||
```markdown
|
||||
## Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
|
||||
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Entity bean creation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/entity-bean-creation.md
|
||||
```
|
||||
|
||||
### Cursor — `.cursor/rules/ebean.mdc`
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Ebean ORM task guidance
|
||||
globs: ["**/*.java", "**/pom.xml"]
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
## Ebean ORM
|
||||
|
||||
This project uses Ebean ORM. Before performing any Ebean-related task, fetch and
|
||||
follow the relevant step-by-step guide from:
|
||||
https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
```
|
||||
@@ -0,0 +1,366 @@
|
||||
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for adding Ebean DB migration generation
|
||||
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
|
||||
performing a diff of the current entity model against the previously recorded model state,
|
||||
producing platform-specific DDL SQL scripts.
|
||||
|
||||
These instructions are designed for AI agents and developers to follow precisely.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An existing Maven project with Ebean ORM configured (entity beans present)
|
||||
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
|
||||
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Verify migration dependencies
|
||||
|
||||
### Generation tooling (`ebean-ddl-generator`)
|
||||
|
||||
`ebean-test` (already present as a test dependency) transitively includes
|
||||
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
|
||||
is required for generation.
|
||||
|
||||
### Runtime migration runner (`ebean-migration`)
|
||||
|
||||
`ebean-migration` is the library that runs migrations on application startup.
|
||||
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
|
||||
equivalent platform dependency). Verify it is on the classpath by running:
|
||||
|
||||
```bash
|
||||
mvn dependency:tree | grep ebean-migration
|
||||
```
|
||||
|
||||
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Create `GenerateDbMigration.java`
|
||||
|
||||
Create the following class in `src/test/java/main/`. This `main` method is run manually
|
||||
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
|
||||
|
||||
```java
|
||||
package main;
|
||||
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.dbmigration.DbMigration;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Generate the next database migration based on a diff of the entity model.
|
||||
* Run this main method after making entity bean changes to produce the migration SQL.
|
||||
*/
|
||||
public class GenerateDbMigration {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
DbMigration migration = DbMigration.create();
|
||||
migration.setPlatform(Platform.POSTGRES);
|
||||
|
||||
migration.setVersion("1.1"); // set to the next migration version
|
||||
migration.setName("add-customer"); // short description of the change
|
||||
|
||||
migration.generateMigration();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Version naming convention
|
||||
|
||||
Ebean supports two common version formats — choose one and apply it consistently:
|
||||
|
||||
| Format | Example | Notes |
|
||||
|--------|---------|-------|
|
||||
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
|
||||
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
|
||||
|
||||
The version controls execution order — Ebean runs migrations in ascending version order.
|
||||
|
||||
### Name convention
|
||||
|
||||
The `name` should be a short, lowercase, hyphenated description of the change:
|
||||
- `add-customer-email`
|
||||
- `rename-machine-type`
|
||||
- `drop-unused-columns`
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Configure the output path (if needed)
|
||||
|
||||
By default, migration files are written to `src/main/resources/dbmigration/` relative
|
||||
to the **current working directory** when `generateMigration()` is called. This is
|
||||
usually the module root, which is correct for single-module projects.
|
||||
|
||||
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
|
||||
resources directory is at a different relative path, specify it explicitly:
|
||||
|
||||
```java
|
||||
// Relative path from the working directory (project root) to the module's resources
|
||||
migration.setPathToResources("my-module/src/main/resources");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Run `GenerateDbMigration` to produce the first migration
|
||||
|
||||
Run the `main` method via the IDE or Maven:
|
||||
|
||||
```bash
|
||||
# Run via Maven exec plugin (or use IDE run configuration)
|
||||
mvn test-compile exec:java \
|
||||
-Dexec.mainClass="main.GenerateDbMigration" \
|
||||
-Dexec.classpathScope="test" \
|
||||
-pl <your-module>
|
||||
```
|
||||
|
||||
Ebean migration generation runs in **offline mode** — no database connection is required.
|
||||
|
||||
### Expected output files
|
||||
|
||||
After running, two files are created per migration in `src/main/resources/dbmigration/`:
|
||||
|
||||
```
|
||||
src/main/resources/dbmigration/
|
||||
1.1__add-customer.sql ← DDL SQL to apply (commit this)
|
||||
model/
|
||||
1.1__add-customer.model.xml ← logical model diff XML (commit this)
|
||||
```
|
||||
|
||||
Both files must be committed to source control. The `.model.xml` file records the
|
||||
logical state of the diff and is used by subsequent migration generations to determine
|
||||
what has changed.
|
||||
|
||||
If **no entity beans have changed** since the last migration, the command outputs:
|
||||
```
|
||||
DbMigration - no changes detected - no migration written
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Enable the migration runner
|
||||
|
||||
Configure Ebean to run pending migrations automatically on application startup.
|
||||
|
||||
### Preferred approach — programmatic via `DatabaseBuilder`
|
||||
|
||||
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
|
||||
the `Database` bean. This is the preferred approach as it is explicit, co-located with
|
||||
the database configuration, and does not rely on external property files.
|
||||
|
||||
In the `@Factory` class that builds the `Database` bean (see the database configuration
|
||||
guide), add `.runMigration(true)` to the builder chain:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(ConfigWrapper config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.getDatabaseUrl())
|
||||
.username(config.getDatabaseUser())
|
||||
.password(config.getDatabasePassword())
|
||||
// ... other datasource settings ...
|
||||
;
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.runMigration(true) // run pending migrations on startup
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If migrations should only run in certain environments (e.g., not in production, or
|
||||
only when a config flag is set), make it conditional:
|
||||
|
||||
```java
|
||||
.runMigration(config.isRunMigrations()) // driven by config value
|
||||
```
|
||||
|
||||
### Alternative — via application properties
|
||||
|
||||
If programmatic configuration is not available or not preferred, set the property
|
||||
in `src/main/resources/application.properties`:
|
||||
|
||||
```properties
|
||||
ebean.migration.run=true
|
||||
```
|
||||
|
||||
Or in `src/main/resources/application.yaml`:
|
||||
```yaml
|
||||
ebean:
|
||||
migration:
|
||||
run: true
|
||||
```
|
||||
|
||||
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
|
||||
name in the property key:
|
||||
|
||||
```properties
|
||||
ebean.mydb.migration.run=true
|
||||
```
|
||||
|
||||
### What the runner does at startup
|
||||
|
||||
When migration running is enabled, Ebean will on each application start:
|
||||
1. Look at the migrations in `src/main/resources/dbmigration/`
|
||||
2. Compare against the `db_migration` table (created automatically on first run)
|
||||
3. Apply any migrations that have not yet been executed, in version order
|
||||
4. Record each successfully applied migration in `db_migration`
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Commit the migration files
|
||||
|
||||
Add both generated files to source control:
|
||||
|
||||
```bash
|
||||
git add src/main/resources/dbmigration/1.1__add-customer.sql
|
||||
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
|
||||
git commit -m "Add db migration 1.1: add-customer"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Ongoing workflow — generating subsequent migrations
|
||||
|
||||
For each future set of entity bean changes:
|
||||
|
||||
1. Make changes to the entity bean classes
|
||||
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
|
||||
```java
|
||||
migration.setVersion("1.2");
|
||||
migration.setName("add-address-table");
|
||||
```
|
||||
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
|
||||
4. Review the generated `.sql` to confirm it reflects the intended changes
|
||||
5. Commit both files
|
||||
|
||||
---
|
||||
|
||||
## Understanding the output files
|
||||
|
||||
### Apply SQL (`.sql`)
|
||||
|
||||
The apply SQL file contains the DDL that will be executed against the database:
|
||||
|
||||
```sql
|
||||
-- apply changes
|
||||
alter table customer add column email varchar(255);
|
||||
```
|
||||
|
||||
### Model XML (`.model.xml`)
|
||||
|
||||
The model XML records the logical diff in a database-agnostic format. Ebean uses
|
||||
this file on the next generation run to determine what has already been captured.
|
||||
It is not executed against the database.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
|
||||
<changeSet type="apply">
|
||||
<addColumn tableName="customer">
|
||||
<column name="email" type="varchar(255)"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</migration>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optional configurations
|
||||
|
||||
### Multiple database platforms
|
||||
|
||||
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
|
||||
instead of `setPlatform()`:
|
||||
|
||||
```java
|
||||
migration.addPlatform(Platform.POSTGRES);
|
||||
migration.addPlatform(Platform.SQLSERVER17);
|
||||
migration.addPlatform(Platform.MYSQL);
|
||||
```
|
||||
|
||||
Each platform gets its own subdirectory under `dbmigration/`.
|
||||
|
||||
### Include index
|
||||
|
||||
When enabled the migration generation also generates a file that contains
|
||||
all the migrations and their associated hashes. This is a performance
|
||||
optimisation (that will become the default) and means that the migration
|
||||
runner just needs to read the one resource and has the pre-computed hash
|
||||
values (so does not need to read each migration resource and compute the
|
||||
hash for each of those at runtime).
|
||||
|
||||
```java
|
||||
migration.setIncludeIndex(true);
|
||||
```
|
||||
|
||||
### Strict mode
|
||||
|
||||
Strict mode (on by default) errors if there are any pending drops not yet applied.
|
||||
Set to `false` to allow generation to proceed regardless:
|
||||
|
||||
```java
|
||||
migration.setStrictMode(false);
|
||||
```
|
||||
|
||||
### Applying pending drops
|
||||
|
||||
Destructive changes (drop column, drop table) are **not** included in the apply
|
||||
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
|
||||
the application to be deployed without immediately dropping columns (important for
|
||||
rolling deployments).
|
||||
|
||||
The migration runner logs a message when pending drops exist:
|
||||
```
|
||||
INFO DbMigration - Pending un-applied drops in versions [1.1]
|
||||
```
|
||||
|
||||
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
|
||||
contains the pending drops:
|
||||
|
||||
```java
|
||||
migration.setVersion("1.3");
|
||||
migration.setName("drop-pending-from-1.1");
|
||||
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
|
||||
migration.generateMigration();
|
||||
```
|
||||
|
||||
### Custom dbSchema
|
||||
|
||||
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
|
||||
`application.properties`), no additional configuration is needed in
|
||||
`GenerateDbMigration` — Ebean picks up the schema from the application config
|
||||
automatically when running in offline mode.
|
||||
|
||||
```properties
|
||||
# application.properties
|
||||
ebean.dbSchema=myschema
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
|
||||
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
|
||||
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
|
||||
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
|
||||
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
|
||||
@@ -0,0 +1,158 @@
|
||||
# Guide: Add Ebean OpenTelemetry tracing
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
|
||||
most importantly, how to order startup so Ebean sees the intended global
|
||||
OpenTelemetry instance.
|
||||
|
||||
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
|
||||
or fixing `GlobalOpenTelemetry` double-registration errors.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
|
||||
spans as children of the current active OpenTelemetry span. It does not create
|
||||
top-level request, job, or Lambda invocation spans by itself.
|
||||
|
||||
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
|
||||
`Database` is configured. For that reason, the application must build and register
|
||||
the OpenTelemetry SDK before any Ebean `Database` beans are created.
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
- Register the global OpenTelemetry instance once.
|
||||
- Register it before building Ebean databases.
|
||||
- Model that ordering as a real DI dependency.
|
||||
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
|
||||
multiple places.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Add the dependency
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
|
||||
No manual Ebean plugin registration is normally required.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Build OpenTelemetry before Ebean databases
|
||||
|
||||
Create one application-owned OpenTelemetry bean. For example, when using
|
||||
`avaje-metrics-otel`:
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.metrics.otel.MetricsOpenTelemetry;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Factory
|
||||
class OpenTelemetryConfig {
|
||||
|
||||
@Bean
|
||||
OpenTelemetry openTelemetry(Configuration config) {
|
||||
return MetricsOpenTelemetry.builder()
|
||||
.endpoint(config.get("otel.endpoint"))
|
||||
.serviceName(config.get("otel.serviceName", "orders"))
|
||||
.deploymentEnvironmentName(config.get("app.env", "local"))
|
||||
.meterInterval(Duration.ofSeconds(30))
|
||||
.traceInterval(Duration.ofSeconds(30))
|
||||
.buildAndRegisterGlobal();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you build the SDK directly, use the same principle: create the SDK once and
|
||||
register that instance globally before any Ebean databases are built.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Make database beans depend on OpenTelemetry
|
||||
|
||||
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
|
||||
is intentionally present to make startup order deterministic: OpenTelemetry is
|
||||
created and registered before Ebean configures the database and profile handler.
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
|
||||
@Bean
|
||||
Database database(OpenTelemetry openTelemetry, Configuration config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.get("db.url"))
|
||||
.username(config.get("db.username"))
|
||||
.password(config.get("db.password"));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
|
||||
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
|
||||
initialized first.
|
||||
|
||||
Do not invert the dependency by making OpenTelemetry depend on the Ebean
|
||||
`Database`. That creates a startup cycle and can still initialize Ebean before the
|
||||
global OpenTelemetry instance is ready.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Create a parent span at the application boundary
|
||||
|
||||
Ebean transaction spans are child spans. They are only created when a recording
|
||||
OpenTelemetry span is active on the current thread.
|
||||
|
||||
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
|
||||
root span around the top-level request/job boundary. Ebean will then attach
|
||||
transaction spans beneath that current span.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `GlobalOpenTelemetry.set has already been called`
|
||||
|
||||
This usually means more than one component is trying to register a global SDK, or
|
||||
some startup path touched the global before the application registered its SDK.
|
||||
|
||||
Fixes:
|
||||
|
||||
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
|
||||
call in the application.
|
||||
2. Build that OpenTelemetry bean before Ebean `Database` beans.
|
||||
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
|
||||
|
||||
### No Ebean spans appear
|
||||
|
||||
Check:
|
||||
|
||||
1. `ebean-opentelemetry` is on the runtime classpath.
|
||||
2. OpenTelemetry is registered before Ebean databases are built.
|
||||
3. There is a current recording parent span when Ebean transactions run.
|
||||
4. Sampling is not dropping the parent trace.
|
||||
@@ -0,0 +1,295 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Database Configuration
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for configuring an Ebean `Database` bean
|
||||
using **Avaje Inject** (`@Factory` / `@Bean`), backed by a PostgreSQL datasource built
|
||||
with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 3 of 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
|
||||
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
|
||||
- **Step 2 complete**: Test container setup is working and `mvn verify` passes
|
||||
(see `add-ebean-postgres-test-container.md`)
|
||||
- **Avaje Inject** is on the classpath (e.g. `io.avaje:avaje-inject`)
|
||||
- A configuration source is available at runtime (e.g. `avaje-config` reading
|
||||
`application.yml` or environment variables)
|
||||
- The following configuration keys are resolvable at runtime (adapt names to your project):
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `db_url` | JDBC URL for the master/write connection |
|
||||
| `db_user` | Database username |
|
||||
| `db_pass` | Database password |
|
||||
| `db_master_min_connections` | Minimum pool size (default: 1) |
|
||||
| `db_master_initial_connections` | Initial pool size at startup — set high to pre-warm on pod start (see K8s note below) |
|
||||
| `db_master_max_connections` | Maximum pool size (default: 200) |
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Locate or create the `@Factory` class
|
||||
|
||||
Look for an existing Avaje Inject `@Factory`-annotated class in the project
|
||||
(often named `AppConfig`, `DatabaseConfig`, or similar). If one exists, add the new
|
||||
`@Bean` method to it. If none exists, create one:
|
||||
|
||||
```java
|
||||
package com.example.configuration;
|
||||
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
// beans will be added in the steps below
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add the `Database` bean method (minimal — master datasource only)
|
||||
|
||||
Add the following `@Bean` method to the `@Factory` class. This creates an Ebean
|
||||
`Database` backed by a single master (read-write) PostgreSQL datasource.
|
||||
|
||||
```java
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
|
||||
@Bean
|
||||
Database database() {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(/* resolve from config, e.g.: */ Config.get("db_url"))
|
||||
.username(Config.get("db_user"))
|
||||
.password(Config.get("db_pass"))
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema") // set to your target schema
|
||||
.applicationName("my-app") // visible in pg_stat_activity
|
||||
.minConnections(Config.getInt("db_master_min_connections", 1))
|
||||
.initialConnections(Config.getInt("db_master_initial_connections", 10))
|
||||
.maxConnections(Config.getInt("db_master_max_connections", 200));
|
||||
|
||||
return Database.builder()
|
||||
.name("db") // logical name for this Database instance
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Field guidance
|
||||
|
||||
| Field | Notes |
|
||||
|-------|-------|
|
||||
| `url` | Full JDBC URL, e.g. `jdbc:postgresql://host:5432/dbname` |
|
||||
| `schema` | The Postgres schema Ebean should use (omit if using `public`) |
|
||||
| `applicationName` | Shown in `pg_stat_activity.application_name`; helps with DB-side diagnostics |
|
||||
| `name("db")` | Logical Ebean database name; relevant if multiple Database instances exist |
|
||||
| `minConnections` | Connections kept open at all times; pool will not shrink below this |
|
||||
| `initialConnections` | Connections opened at startup; see K8s warm-up note below |
|
||||
| `maxConnections` | Hard upper limit on concurrent connections |
|
||||
|
||||
### Connection pool sizing for Kubernetes (and similar orchestrated environments)
|
||||
|
||||
When a pod starts in Kubernetes it will receive live traffic as soon as it passes
|
||||
readiness checks — often before the connection pool has had a chance to grow to handle
|
||||
the load. This can cause latency spikes on the first wave of requests while the pool
|
||||
expands one connection at a time.
|
||||
|
||||
Use `initialConnections` to **pre-warm the pool at startup** so it is already sized
|
||||
for peak load when the pod goes live:
|
||||
|
||||
```
|
||||
minConnections: 2 ← floor; pool will shrink back here when idle
|
||||
initialConnections: 20 ← opened at pod start, before first request arrives
|
||||
maxConnections: 50 ← hard ceiling
|
||||
```
|
||||
|
||||
The lifecycle is:
|
||||
1. **Pod starts** — pool opens `initialConnections` connections immediately.
|
||||
2. **Pod receives traffic** — pool is already at capacity; no growth latency.
|
||||
3. **Traffic drops** — idle connections are closed; pool trims back toward `minConnections`.
|
||||
4. **Next traffic spike** — pool grows again up to `maxConnections` on demand.
|
||||
|
||||
Set `initialConnections` to a value high enough that the pool does not need to grow
|
||||
during the first minute of live traffic. A common starting point is 50–75% of
|
||||
`maxConnections`.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Inject configuration via a constructor or config helper (recommended)
|
||||
|
||||
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
|
||||
Avaje `Configuration` bean if one is available. This makes the factory testable and
|
||||
keeps the wiring explicit. For example:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(Configuration config) {
|
||||
String url = config.get("db_url");
|
||||
String user = config.get("db_user");
|
||||
String pass = config.get("db_pass");
|
||||
int min = config.getInt("db_master_min_connections", 1);
|
||||
int init = config.getInt("db_master_initial_connections", 10);
|
||||
int max = config.getInt("db_master_max_connections", 200);
|
||||
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(url)
|
||||
.username(user)
|
||||
.password(pass)
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema")
|
||||
.applicationName("my-app")
|
||||
.minConnections(min)
|
||||
.initialConnections(init)
|
||||
.maxConnections(max);
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If the project has a dedicated config-wrapper class (a `@Component` that reads config
|
||||
keys), accept it as a parameter instead of `Configuration`.
|
||||
|
||||
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
|
||||
> into the DI context. If you encounter "No dependency provided for
|
||||
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
|
||||
> (as shown in Step 2).
|
||||
|
||||
---
|
||||
|
||||
## Step 4 (Optional) — Add a read-only datasource
|
||||
|
||||
For production services that have a separate read-replica, add a second
|
||||
`DataSourceBuilder` for read-only queries and wire it via
|
||||
`readOnlyDataSourceBuilder(...)`. The read-only datasource:
|
||||
|
||||
- Uses `readOnly(true)` and `autoCommit(true)` (Ebean routes read queries there automatically)
|
||||
- Typically has a higher max connection count than the master
|
||||
- Benefits from a prepared-statement cache (`pstmtCacheSize`)
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(Configuration config) {
|
||||
String masterUrl = config.get("db_url");
|
||||
String readOnlyUrl = config.get("db_url_readonly");
|
||||
String user = config.get("db_user");
|
||||
String pass = config.get("db_pass");
|
||||
|
||||
var masterDataSource = buildDataSource(user, pass)
|
||||
.url(masterUrl)
|
||||
.minConnections(config.getInt("db_master_min_connections", 1))
|
||||
.initialConnections(config.getInt("db_master_initial_connections", 10))
|
||||
.maxConnections(config.getInt("db_master_max_connections", 50));
|
||||
|
||||
var readOnlyDataSource = buildDataSource(user, pass)
|
||||
.url(readOnlyUrl)
|
||||
.readOnly(true)
|
||||
.autoCommit(true)
|
||||
.pstmtCacheSize(250) // cache up to 250 prepared statements per connection
|
||||
.maxInactiveTimeSecs(600) // close idle connections after 10 minutes
|
||||
.minConnections(config.getInt("db_readonly_min_connections", 2))
|
||||
.initialConnections(config.getInt("db_readonly_initial_connections", 10))
|
||||
.maxConnections(config.getInt("db_readonly_max_connections", 200));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(masterDataSource)
|
||||
.readOnlyDataSourceBuilder(readOnlyDataSource)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static DataSourceBuilder buildDataSource(String user, String pass) {
|
||||
return DataSourceBuilder.create()
|
||||
.username(user)
|
||||
.password(pass)
|
||||
.driver("org.postgresql.Driver")
|
||||
.schema("myschema")
|
||||
.applicationName("my-app")
|
||||
.addProperty("prepareThreshold", "2"); // PostgreSQL: server-side prepared statements
|
||||
}
|
||||
```
|
||||
|
||||
### Additional configuration keys for the read-only datasource
|
||||
|
||||
| Key | Description | Default |
|
||||
|-----|-------------|---------|
|
||||
| `db_url_readonly` | JDBC URL for the read replica | — |
|
||||
| `db_master_initial_connections` | Initial master pool size at startup | 10 |
|
||||
| `db_readonly_min_connections` | Minimum pool size | 2 |
|
||||
| `db_readonly_initial_connections` | Initial pool size at startup | same as min |
|
||||
| `db_readonly_max_connections` | Maximum pool size | 20 |
|
||||
|
||||
---
|
||||
|
||||
## Step 5 (Optional) — Enable the migration runner
|
||||
|
||||
If the project uses Ebean's built-in DB migration runner to apply SQL migrations on
|
||||
startup, enable it on the `DatabaseBuilder`:
|
||||
|
||||
```java
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.runMigration(true) // run pending migrations on startup
|
||||
.build();
|
||||
```
|
||||
|
||||
This is equivalent to setting `ebean.migration.run=true` in `application.properties`
|
||||
but is preferred because it keeps all database configuration in one place. To make it
|
||||
conditional (e.g. only in non-production environments):
|
||||
|
||||
```java
|
||||
.runMigration(config.getBoolean("db.runMigrations", false))
|
||||
```
|
||||
|
||||
See the DB migration generation guide (`add-ebean-db-migration-generation.md`) for
|
||||
full details on generating and managing migration files.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
For advanced connection pool configuration, production deployment patterns, and connection
|
||||
validation best practices, see the [ebean-datasource guides](https://github.com/ebean-orm/ebean-datasource/tree/master/docs/guides/):
|
||||
|
||||
- **[Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)** — Covers read-only pools (`readOnly(true)` + `autoCommit(true)`), Kubernetes deployment strategies using `initialConnections`, and AWS Lambda optimization
|
||||
- **[AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)** — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
|
||||
- **[Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)** — Why `Connection.isValid()` is the recommended default and when (rarely) explicit `heartbeatSql` is needed
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. Start the application (or run `mvn test -pl <your-module>`).
|
||||
2. Look for log output similar to:
|
||||
|
||||
```
|
||||
INFO o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
|
||||
INFO io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
|
||||
```
|
||||
|
||||
3. If you see `DataSourcePool` and `DatabasePlatform` log lines, Ebean is connected and
|
||||
the database bean is wired correctly.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|-------------|-----|
|
||||
| `ClassNotFoundException: org.postgresql.Driver` | PostgreSQL JDBC driver missing | Add `org.postgresql:postgresql` dependency (see Step 1 guide) |
|
||||
| `Cannot connect to database` at startup | DB unreachable but `skipDataSourceCheck` is `false` | Set `.skipDataSourceCheck(true)` |
|
||||
| Ebean enhancement warnings in logs | `ebean-maven-plugin` not configured | Complete Step 1 guide |
|
||||
| `NullPointerException` reading config key | Config key not defined | Add the key to `application.yml` or environment |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
The test container setup (Step 2) should already be complete and passing
|
||||
before this step. See `add-ebean-postgres-test-container.md`.
|
||||
@@ -0,0 +1,294 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 1: POM Setup
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for modifying an existing Maven `pom.xml`
|
||||
to add Ebean ORM with PostgreSQL support. Follow every step in order. This is Step 1 of 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An existing Maven project (`pom.xml` already exists)
|
||||
- Java 11 or higher
|
||||
- The project does **not** yet include any Ebean dependencies
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Gather requirements from the user
|
||||
|
||||
Before modifying any files, ask the user the following questions to determine
|
||||
the correct setup path. Record the answers — they affect dependency choices
|
||||
in this step and the approach used in Steps 2 and 3.
|
||||
|
||||
### Mandatory gate (do not skip)
|
||||
|
||||
- Do **not** continue to Step 1+ until the DI path is explicitly recorded.
|
||||
- Do **not** infer the **None** path by default. Use **None** only when the user explicitly confirms no DI framework.
|
||||
- If the user asks for a partial action (for example, "do only step 3"), keep the previously selected DI path; do not switch paths implicitly.
|
||||
|
||||
### DI path precedence (when user has not answered yet)
|
||||
|
||||
Use this precedence order:
|
||||
|
||||
1. Existing project context (highest priority): if dependencies/config already show Avaje Inject or Spring, select that path.
|
||||
2. Explicit user answer in this guide's questions.
|
||||
3. Recommended default only when context is genuinely unknown: Avaje Inject.
|
||||
|
||||
If context remains ambiguous, ask one multiple-choice clarification question and wait for the answer before editing files.
|
||||
|
||||
### Question 1: Dependency injection framework
|
||||
|
||||
> "Does this project use (or will it use) a DI framework? If so, which one?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Avaje Inject** | Add `avaje-inject` + `avaje-inject-test` dependencies; use `@TestScope @Factory` for test container (Step 2); use `@Factory`/`@Bean` for production database (Step 3) |
|
||||
| **Spring** | Use Spring `@TestConfiguration` for test container (Step 2); use Spring `@Configuration`/`@Bean` for production database (Step 3) |
|
||||
| **None** | Use declarative `application-test.yaml` for test container (Step 2); use programmatic `Database.builder()` directly in application code (Step 3) |
|
||||
|
||||
### Question 2: PostGIS
|
||||
|
||||
> "Do you need PostGIS spatial extensions (geometry types, spatial queries)?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Use `PostgisContainer` in test setup (Step 2); may need `net.postgis:postgis-jdbc` dependency |
|
||||
| **No** | Use `PostgresContainer` in test setup (Step 2) |
|
||||
|
||||
### Question 3: Read replica
|
||||
|
||||
> "Does your production environment use a separate read-replica (read-only) database?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Configure a read-only `DataSourceBuilder` in production database config (Step 3) |
|
||||
| **No** | Single datasource only (Step 3) |
|
||||
|
||||
### Defaults
|
||||
|
||||
If the user is unsure or setting up a new project, recommend:
|
||||
- **Avaje Inject** (lightweight, fast compile-time DI)
|
||||
- **No PostGIS** (can be added later)
|
||||
- **No read replica** (can be added later)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Define the Ebean version property
|
||||
|
||||
Open the module's `pom.xml` (the one that will use Ebean directly, i.e. the module
|
||||
containing the database configuration and entity classes).
|
||||
|
||||
Inside the `<properties>` block, add the `ebean.version` property if it does not
|
||||
already exist:
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
|
||||
<ebean.version>17.5.0</ebean.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
> If the project has a parent POM that already defines `ebean.version`, skip this step.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add the PostgreSQL JDBC driver dependency
|
||||
|
||||
Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.8</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Check [Maven Central](https://central.sonatype.com/artifact/org.postgresql/postgresql)
|
||||
> for the latest version. If the parent POM manages the PostgreSQL version, omit the
|
||||
> `<version>` tag.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add the Ebean PostgreSQL platform dependency
|
||||
|
||||
Inside the `<dependencies>` block, add the Ebean Postgres platform dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
This single artifact pulls in the Ebean core, the datasource connection pool
|
||||
(`ebean-datasource`), and all Postgres-specific support.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Add the ebean-test dependency (test scope)
|
||||
|
||||
`ebean-test` configures Ebean for tests and enables automatic Docker container management
|
||||
for Postgres test instances:
|
||||
|
||||
```xml
|
||||
<!-- test dependencies -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
|
||||
avoiding the need to declare those dependencies separately.
|
||||
|
||||
---
|
||||
|
||||
## Step 4b — Add DI framework dependencies (if applicable)
|
||||
|
||||
If the user chose **Avaje Inject** in Step 0, add the following dependencies and
|
||||
annotation processor. Skip this step if the user chose Spring or no DI.
|
||||
|
||||
### Dependencies
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>12.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>12.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Check [Maven Central](https://central.sonatype.com/artifact/io.avaje/avaje-inject)
|
||||
> for the latest version.
|
||||
|
||||
### Annotation processor
|
||||
|
||||
The `avaje-inject-generator` must be added to the `annotationProcessorPaths` in
|
||||
`maven-compiler-plugin` (added in Step 6 below). When adding both processors,
|
||||
the final `<annotationProcessorPaths>` block should include both:
|
||||
|
||||
```xml
|
||||
<annotationProcessorPaths>
|
||||
<path> <!-- generate ebean query beans -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
<path> <!-- generate avaje-inject DI code -->
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>12.5</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Add the ebean-maven-plugin (bytecode enhancement)
|
||||
|
||||
Ebean requires bytecode enhancement to provide dirty-checking and lazy-loading.
|
||||
The `ebean-maven-plugin` performs this enhancement at build time.
|
||||
|
||||
Inside the `<build><plugins>` block, add:
|
||||
|
||||
```xml
|
||||
<plugin> <!-- perform ebean enhancement -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-maven-plugin</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6 — Add the querybean-generator annotation processor
|
||||
|
||||
The `querybean-generator` annotation processor generates type-safe query bean classes
|
||||
at compile time. It must be registered as an `annotationProcessorPath` inside
|
||||
`maven-compiler-plugin`.
|
||||
|
||||
### Case A — No existing `maven-compiler-plugin` configuration
|
||||
|
||||
Add the full plugin entry to `<build><plugins>`:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.15.0</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path> <!-- generate ebean query beans -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### Case B — `maven-compiler-plugin` already exists with `<annotationProcessorPaths>`
|
||||
|
||||
Locate the existing `<annotationProcessorPaths>` block inside the existing
|
||||
`maven-compiler-plugin` entry and add the new `<path>` inside it. Do **not** add a
|
||||
second `<configuration>` block or a second `<annotationProcessorPaths>` block.
|
||||
|
||||
Example — if the existing block already has a path for, say, `avaje-nima-generator`:
|
||||
|
||||
```xml
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-nima-generator</artifactId>
|
||||
<version>${avaje-nima.version}</version>
|
||||
</path>
|
||||
<!-- ADD the new path here, inside the existing block -->
|
||||
<path>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Run the following to confirm the POM is valid and both main and test sources compile:
|
||||
|
||||
```bash
|
||||
mvn test-compile
|
||||
```
|
||||
|
||||
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
|
||||
Using `test-compile` rather than `compile` ensures test dependencies and test
|
||||
source files are also verified.
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Proceed to **Step 2: Test container setup**
|
||||
(`add-ebean-postgres-test-container.md`) to wire an injectable test `Database`
|
||||
backed by `ebean-test` containers. Verify with `mvn verify` before continuing
|
||||
to production database configuration.
|
||||
@@ -0,0 +1,445 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project - Step 2: Test Container Setup
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
|
||||
container for tests, exposing an `io.ebean.Database` instance for use in test
|
||||
classes. This is Step 2 of 3.
|
||||
|
||||
Complete this step before configuring the production database in Step 3. Getting
|
||||
the test container working first gives you a fast feedback loop - you can verify
|
||||
entity changes compile, enhance, and persist correctly with `mvn verify` before
|
||||
wiring up production datasource configuration.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`,
|
||||
`querybean-generator`, and **`ebean-test`** as a test-scoped dependency
|
||||
(see `add-ebean-postgres-maven-pom.md`)
|
||||
- **Step 0 answers recorded**: DI framework choice and PostGIS requirement
|
||||
- **Docker** is installed and running on the developer machine
|
||||
|
||||
---
|
||||
|
||||
## Overview: Choosing your approach
|
||||
|
||||
The approach depends on the DI framework choice made in Step 0:
|
||||
|
||||
| DI framework | Approach | How |
|
||||
|--------------|----------|-----|
|
||||
| **Avaje Inject** | Programmatic | `@TestScope @Factory` class with injectable `Database` bean |
|
||||
| **Spring** | Programmatic | `@TestConfiguration` class with `@Bean` methods |
|
||||
| **None** | Declarative | `application-test.yaml` + plain JUnit test |
|
||||
|
||||
Follow the path that matches your choice below.
|
||||
|
||||
---
|
||||
|
||||
## Path A — Programmatic with Avaje Inject (recommended)
|
||||
|
||||
This approach uses `@TestScope @Factory` to expose the container and `Database`
|
||||
as injectable beans. It offers more control (image mirrors, custom config) and
|
||||
makes `Database` directly injectable into test classes.
|
||||
|
||||
### A.1 — Verify Avaje Inject test dependencies
|
||||
|
||||
Confirm the following are present in `pom.xml` (in addition to `ebean-test`):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
And the `avaje-inject-generator` annotation processor in `maven-compiler-plugin`:
|
||||
|
||||
```xml
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</path>
|
||||
```
|
||||
|
||||
### A.2 — Create a `@TestScope @Factory` class
|
||||
|
||||
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`):
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.inject.test.TestScope;
|
||||
import io.ebean.Database;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
// bean methods added below
|
||||
}
|
||||
```
|
||||
|
||||
### A.3 — Add a container bean and a Database bean
|
||||
|
||||
#### Plain PostgreSQL
|
||||
|
||||
```java
|
||||
import io.ebean.test.containers.PostgresContainer;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### PostGIS (PostgreSQL + PostGIS extension)
|
||||
|
||||
Use `PostgisContainer` instead. The default image is
|
||||
`ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`,
|
||||
and `postgis` are installed automatically.
|
||||
|
||||
```java
|
||||
import io.ebean.test.containers.PostgisContainer;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgisContainer postgres() {
|
||||
return PostgisContainer.builder("17")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgisContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Key differences
|
||||
|
||||
| | PostgresContainer | PostgisContainer |
|
||||
|---|---|---|
|
||||
| Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` |
|
||||
| Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` |
|
||||
| Default port | 6432 | 6432 |
|
||||
| Optional LW mode | — | `.useLW(true)` (see Optional section) |
|
||||
|
||||
### A.4 — Write a test
|
||||
|
||||
Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`:
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.avaje.inject.test.InjectTest;
|
||||
import io.ebean.Database;
|
||||
import jakarta.inject.Inject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@InjectTest
|
||||
class DatabaseTest {
|
||||
|
||||
@Inject
|
||||
Database database;
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### A.5 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
INFO Container ut_postgres running with port:6432 ...
|
||||
INFO connectivity confirmed for ut_postgres
|
||||
INFO DataSourcePool [my_app] autoCommit[false] ...
|
||||
INFO DatabasePlatform name:my_app platform:postgres
|
||||
INFO Executing db-create-all.sql - ...
|
||||
```
|
||||
|
||||
**Important:** Verify this step passes with `mvn verify` before proceeding to
|
||||
Step 3 (production database configuration).
|
||||
|
||||
---
|
||||
|
||||
## Path B — Programmatic with Spring
|
||||
|
||||
Use Spring’s `@TestConfiguration` to provide the container and `Database` beans.
|
||||
|
||||
### B.1 — Create a `@TestConfiguration` class
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.containers.PostgresContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@TestConfiguration
|
||||
class TestDatabaseConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For PostGIS, use `PostgisContainer` instead (same pattern as Path A).
|
||||
|
||||
### B.2 — Write a test
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
class DatabaseTest {
|
||||
|
||||
@Autowired
|
||||
Database database;
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### B.3 — Verify
|
||||
|
||||
Run `mvn verify` and confirm the same log output as Path A.
|
||||
|
||||
---
|
||||
|
||||
## Path C — Declarative (no DI framework)
|
||||
|
||||
This is the simplest approach but offers less control. `ebean-test` reads a
|
||||
YAML config file and automatically manages the Docker container and `Database`
|
||||
instance. Use this when the project has no DI framework.
|
||||
|
||||
### C.1 — Create `application-test.yaml`
|
||||
|
||||
Create `src/test/resources/application-test.yaml`:
|
||||
|
||||
```yaml
|
||||
ebean:
|
||||
test:
|
||||
platform: postgres
|
||||
ddlMode: dropCreate
|
||||
dbName: my_app
|
||||
```
|
||||
|
||||
For PostGIS, use `platform: postgis` instead.
|
||||
|
||||
### C.2 — Write a test
|
||||
|
||||
Use `DB.getDefault()` to obtain the `Database` instance:
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DatabaseTest {
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
Database database = DB.getDefault();
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### C.3 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
INFO Container ut_postgres running with port:6432 ...
|
||||
INFO connectivity confirmed for ut_postgres
|
||||
INFO DataSourcePool [my_app] autoCommit[false] ...
|
||||
INFO DatabasePlatform name:my_app platform:postgres
|
||||
```
|
||||
|
||||
**Important:** Verify this passes before proceeding to Step 3.
|
||||
|
||||
Skip to [Optional configurations](#optional-configurations) or proceed to Step 3.
|
||||
|
||||
---
|
||||
|
||||
## Optional configurations
|
||||
|
||||
### Image mirror (for CI / private registry)
|
||||
|
||||
If CI builds pull images from a private registry (e.g., AWS ECR) instead of Docker Hub
|
||||
or GitHub Container Registry, specify a mirror. The mirror is **only used in CI** -
|
||||
it is ignored on local developer machines (where Docker Hub / GHCR is used directly).
|
||||
|
||||
```java
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("16")
|
||||
.dbName("my_app")
|
||||
.mirror("123456789.dkr.ecr.ap-southeast-2.amazonaws.com/mirrored")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, set the mirror globally via a system property or
|
||||
`ebean.test.containers.mirror` in a properties file, avoiding code changes per project.
|
||||
|
||||
### Read-only datasource (for tests using read-replica simulation)
|
||||
|
||||
Call `.autoReadOnlyDataSource(true)` on the `DatabaseBuilder` to automatically
|
||||
create a second read-only datasource pointing at the same container:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.autoReadOnlyDataSource(true) // test read-only queries against same container
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Dump metrics on shutdown
|
||||
|
||||
Useful for performance analysis during test runs:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.dumpMetricsOnShutdown(true)
|
||||
.dumpMetricsOptions("loc,sql,hash")
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### PostGIS: LW mode (HexWKB)
|
||||
|
||||
For PostGIS with DriverWrapperLW (HexWKB binary geometry encoding), set `.useLW(true)`.
|
||||
This switches the JDBC URL prefix to `jdbc:postgresql_lwgis://` and requires the
|
||||
`net.postgis:postgis-jdbc` dependency on the test classpath:
|
||||
|
||||
```xml
|
||||
<!-- add to pom.xml test dependencies when using useLW(true) -->
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
<version>2024.1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```java
|
||||
@Bean
|
||||
PostgisContainer postgres() {
|
||||
return PostgisContainer.builder("16")
|
||||
.dbName("my_app")
|
||||
.useLW(true) // use HexWKB + DriverWrapperLW
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: LW mode is not required for most PostGIS use cases. Only enable it if
|
||||
> your entities use binary geometry types (e.g., `net.postgis.jdbc.geometry.Geometry`)
|
||||
> that require the `DriverWrapperLW` driver.
|
||||
|
||||
---
|
||||
|
||||
## Keeping the container running (local development)
|
||||
|
||||
By default, `ebean-test` stops the Docker container when tests finish. To keep it
|
||||
running between test runs (much faster for local development), create a marker file:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
|
||||
```
|
||||
|
||||
On CI servers, omit this file so containers are cleaned up after each build.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
|
||||
with auto-populated random values. See `testing-with-testentitybuilder.md`.
|
||||
- **Proceed to Step 3** — production database configuration
|
||||
(`add-ebean-postgres-database-config.md`). Verify this step passes with
|
||||
`mvn verify` before continuing.
|
||||
@@ -0,0 +1,797 @@
|
||||
# Entity Bean Creation Guide for AI Agents
|
||||
|
||||
**Target Audience:** AI systems (Claude, Copilot, ChatGPT, etc.)
|
||||
**Purpose:** Learn how to generate clean, idiomatic Ebean entity beans
|
||||
**Key Insight:** Ebean entity fields must be non-public (no public fields). Accessors don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed
|
||||
**Language:** Java
|
||||
**Framework:** Ebean ORM
|
||||
|
||||
---
|
||||
|
||||
## Quick Rules
|
||||
|
||||
Before writing entity code, remember:
|
||||
|
||||
| Requirement | Needed? | Notes |
|
||||
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------|
|
||||
| `@Entity` annotation | ✅ **YES** | Marks class as persistent entity |
|
||||
| `@Id` annotation | ✅ **YES** | Marks primary key field |
|
||||
| Getters/setters (or other accessors) | ✅ **YES** | Needed for application code to access fields. Naming can be JavaBeans, fluent, or custom — no specific convention required. |
|
||||
| Default constructor | ❌ **NO** | Not required. Ebean can instantiate without it. |
|
||||
| equals/hashCode | ❌ **NO** | Ebean auto-enhances these at compile time. |
|
||||
| toString() | ❌ **NO** | Ebean auto-enhances this. Don't implement with getters. |
|
||||
| `@Version` | ⚠️ **OPTIONAL** | Use for optimistic locking. Highly recommended. |
|
||||
| `@WhenCreated` | ⚠️ **OPTIONAL** | Auto-timestamp creation time. Highly recommended. Use for audit trail. |
|
||||
| `@WhenModified` | ⚠️ **OPTIONAL** | Auto-timestamp modification time. Highly recommended. Use for audit trail. |
|
||||
|
||||
**Critical:**
|
||||
- Prefer primitive `long` for `@Id` and `@Version`, NOT `Long` object.
|
||||
- Fields should be non-public: **private**, **protected**, or package-private.
|
||||
- If you add accessors, they do NOT need to follow Java bean conventions.
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions: The D* (Domain) Prefix Pattern
|
||||
|
||||
Entity beans represent internal domain/persistence model details. It's a common best practice in Ebean projects to use the **D* prefix** (D for Domain) for entity class names.
|
||||
|
||||
**Why use D* prefix?**
|
||||
|
||||
1. **Avoid name clashes with DTOs** - Your public API may have `Customer` (DTO), but your entity is `DCustomer` (Domain). They're clearly different.
|
||||
2. **Signal intent clearly** - The D prefix immediately tells developers "this is an internal domain class, not part of the public API"
|
||||
3. **Clarify conversions** - When converting `DCustomer` → `Customer` (DTO), the direction is obvious
|
||||
4. **Separate concerns** - API classes in one package (no prefix), domain classes in another (with D prefix)
|
||||
|
||||
**Example naming pattern:**
|
||||
- Entity: `DCustomer`, `DOrder`, `DProduct`, `DInvoice`
|
||||
- DTO: `Customer`, `Order`, `Product`, `Invoice`
|
||||
- Converter: `DCustomerMapper.toDTO(DCustomer)` → `Customer`
|
||||
|
||||
**Where to place entities:**
|
||||
- Entities: `com.example.domain.entity` (or `persistence`)
|
||||
- DTOs: `com.example.api.model` or `com.example.dto`
|
||||
|
||||
**When to use D* prefix:**
|
||||
- ✅ **DO** use for entity beans (internal domain model)
|
||||
- ✅ **DO** use when you have parallel DTO classes with similar names
|
||||
- ❌ **DON'T** use for DTOs or public API classes
|
||||
- ❌ **DON'T** use if you have no DTOs and entities are your public API
|
||||
|
||||
Example with and without prefix:
|
||||
|
||||
```java
|
||||
// With D* prefix (recommended - allows both entity and DTO to exist)
|
||||
@Entity
|
||||
public class DCustomer {
|
||||
@Id private long id;
|
||||
private String name;
|
||||
// ... entity-specific fields and methods
|
||||
}
|
||||
|
||||
// Public API DTO (no D prefix)
|
||||
public record Customer(long id, String name) {
|
||||
// ... conversion method
|
||||
}
|
||||
|
||||
// Conversion
|
||||
public static Customer toDTO(DCustomer entity) {
|
||||
return new Customer(entity.getId(), entity.getName());
|
||||
}
|
||||
```
|
||||
|
||||
This naming convention is optional but highly recommended for projects with separate domain and API layers.
|
||||
|
||||
---
|
||||
|
||||
## Minimal Entity (No Boilerplate)
|
||||
|
||||
This is a complete, valid Ebean entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- ✅ `@Entity` marks it as persistent
|
||||
- ✅ `@Id private long id` is the primary key
|
||||
- ✅ Private fields (Ebean does NOT support public fields without expert flags enabled)
|
||||
- ✅ Accessors can follow any naming convention, and can be omitted when field access is preferred
|
||||
- ✅ No default constructor needed
|
||||
- ✅ No equals/hashCode needed (Ebean enhances these)
|
||||
|
||||
**What Ebean does at compile time:**
|
||||
- Enhances equals/hashCode based on @Id
|
||||
- Adds field change tracking
|
||||
- Enables lazy loading
|
||||
- Enhances toString()
|
||||
|
||||
**Result:** Your entity is now fully functional with zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Basic Entity
|
||||
|
||||
**Use this when:** You need a simple persistent object.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Product {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private BigDecimal price;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(BigDecimal price) {
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- Primary key: `id` (private field, accessed via getter)
|
||||
- Three properties: `name`, `description`, `price` (private fields, accessed via getters/setters)
|
||||
- Automatic equals/hashCode based on id
|
||||
- Full ORM functionality
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Entity with Audit Trail
|
||||
|
||||
**Use this when:** You need to track who/when created/modified data.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
@WhenModified
|
||||
private Instant modifiedAt;
|
||||
|
||||
private String orderNumber;
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getModifiedAt() {
|
||||
return modifiedAt;
|
||||
}
|
||||
|
||||
public String getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(String orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- `version`: Optimistic locking (prevents concurrent update conflicts)
|
||||
- `createdAt`: Automatically set when inserted (Ebean manages this)
|
||||
- `modifiedAt`: Automatically updated on every modification (Ebean manages this)
|
||||
|
||||
**Example usage:**
|
||||
```java
|
||||
// Create
|
||||
Order order = new Order();
|
||||
order.setOrderNumber("ORD-001");
|
||||
order.setTotalAmount(new BigDecimal("99.99"));
|
||||
database.save(order); // createdAt is automatically set by Ebean
|
||||
|
||||
// Modify
|
||||
order.setTotalAmount(new BigDecimal("109.99"));
|
||||
database.update(order); // version incremented, modifiedAt updated automatically
|
||||
|
||||
// Check when modified
|
||||
System.out.println(order.getModifiedAt()); // Current timestamp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Entity with Constructor
|
||||
|
||||
**Use this when:** Domain logic requires initialization or validation.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Invoice {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
private String invoiceNumber;
|
||||
private String customerName;
|
||||
private BigDecimal amount;
|
||||
|
||||
public Invoice(String invoiceNumber, String customerName, BigDecimal amount) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
this.customerName = customerName;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getInvoiceNumber() {
|
||||
return invoiceNumber;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to add a constructor:**
|
||||
- ✅ Required fields must be set during creation
|
||||
- ✅ Validation needs to happen on initialization
|
||||
- ✅ Domain logic needs setup
|
||||
|
||||
**When NOT to add:**
|
||||
- ❌ If users will just set fields afterwards anyway
|
||||
- ❌ If there are many optional fields
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Entity with Relationships
|
||||
|
||||
**Use this when:** You need associations to other entities.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // Use List, not Set
|
||||
|
||||
@ManyToOne
|
||||
private Address billingAddress;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public List<Order> getOrders() {
|
||||
return orders;
|
||||
}
|
||||
|
||||
public Address getBillingAddress() {
|
||||
return billingAddress;
|
||||
}
|
||||
|
||||
public void setBillingAddress(Address billingAddress) {
|
||||
this.billingAddress = billingAddress;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs)
|
||||
- `mappedBy` means Order.customer is the owner
|
||||
- Relationships are lazy-loaded by default
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Do (Anti-Patterns)
|
||||
|
||||
### ❌ Anti-Pattern 1: Public Fields
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id public long id; // ❌ Public field - not supported
|
||||
public String name; // ❌ Public field - not supported
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Private field with getter
|
||||
private String name; // ✅ Private field with accessors
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean does NOT support public fields. Fields must be private and accessed via getters/setters or other accessor methods. Public fields bypass Ebean's tracking mechanisms and will cause data consistency issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 2: Use Long Object Instead of Primitive
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private Long id; // ❌ Object type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Primitive type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Performance, nullability semantics, Ebean optimization.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 3: Implement equals/hashCode
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) { // ❌ Unnecessary
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Customer customer = (Customer) o;
|
||||
return id == customer.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { // ❌ Unnecessary
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
// Ebean enhances equals/hashCode automatically
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's enhancement is optimized for ORM operations. Your implementation might conflict with Ebean's tracking.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 4: Use Set for Collections
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private Set<Order> orders; // ❌ Set calls equals/hashCode before IDs assigned
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // ✅ List doesn't require equals/hashCode on unsaved beans
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Set calls equals/hashCode immediately. New beans don't have IDs yet, causing issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 5: toString() with Getters
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public String toString() { // ❌ Uses getters
|
||||
return "Customer{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** In a debugger, toString() is called automatically. Getters can trigger lazy loading, changing debug behavior.
|
||||
|
||||
**DO:** Either don't implement toString(), or access fields directly:
|
||||
```java
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Customer{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 6: @Column(name=...) for Naming Convention
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Column(name = "first_name") // ❌ Unnecessary
|
||||
private String firstName;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
private String firstName; // ✅ Ebean uses naming convention: first_name
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
|
||||
|
||||
---
|
||||
|
||||
## What Ebean Enhancement Provides
|
||||
|
||||
At compile time, Ebean enhances your entity classes:
|
||||
|
||||
1. **equals/hashCode** - Based on @Id, optimal for ORM
|
||||
2. **Field change tracking** - Knows which fields were modified
|
||||
3. **Lazy loading** - Collections and relationships load on demand
|
||||
4. **Persistence context** - Manages identity and state
|
||||
5. **toString()** - Auto-implemented (don't override with getters)
|
||||
|
||||
**Result:** Your entity bean is minimal, but fully featured.
|
||||
|
||||
---
|
||||
|
||||
## Field Types
|
||||
|
||||
**Recommended for ID/Version:**
|
||||
- `long` (primitive) ✅ Use this
|
||||
- `int` (primitive) ✅ Use this
|
||||
- `UUID` ✅ Use this
|
||||
|
||||
**Not recommended:**
|
||||
- `Long` object ⚠️ Avoid (use primitive long)
|
||||
- `Integer` object ⚠️ Avoid (use primitive int)
|
||||
|
||||
**For other fields:**
|
||||
- Use standard Java types: `String`, `BigDecimal`, `Instant`, `LocalDate`, etc.
|
||||
- Use primitives where nullable semantics don't apply: `int`, `long`, `boolean`
|
||||
- Use objects where null has meaning: `String`, `BigDecimal`, `LocalDate`
|
||||
|
||||
---
|
||||
|
||||
## Example: Building an Entity Step by Step
|
||||
|
||||
Start minimal, add what you need:
|
||||
|
||||
**Step 1: Minimal**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add audit trail**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add author relationship**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Add constructor if needed**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public BlogPost(String title, String content, Author author) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
Each step adds only what's necessary for the entity structure. Accessors are always included so application code can use the entity.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating and saving
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Alice");
|
||||
database.save(customer); // id auto-generated
|
||||
```
|
||||
|
||||
### Finding
|
||||
```java
|
||||
Customer found = database.find(Customer.class, 1);
|
||||
System.out.println(found.getName());
|
||||
```
|
||||
|
||||
### Updating
|
||||
```java
|
||||
found.setName("Bob");
|
||||
database.update(found); // version auto-incremented
|
||||
```
|
||||
|
||||
### Collections (relationships)
|
||||
```java
|
||||
Customer customer = database.find(Customer.class, 1);
|
||||
List<Order> orders = customer.getOrders(); // Lazy loads automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary for AI Agents
|
||||
|
||||
When generating Ebean entity beans:
|
||||
|
||||
✅ **DO:**
|
||||
- Use primitive `long` for @Id and @Version
|
||||
- Keep entities minimal (just fields + @Entity + @Id)
|
||||
- Use @Version for concurrency control
|
||||
- Use @WhenCreated/@WhenModified for audit trail
|
||||
- Use List for collections, not Set
|
||||
- Add constructors only if domain logic requires it
|
||||
- Add getters/setters for all fields that application code needs to read or write
|
||||
|
||||
❌ **DON'T:**
|
||||
- Use Long object for @Id/@Version
|
||||
- Implement equals/hashCode
|
||||
- Implement toString() with getters
|
||||
- Use Set for @OneToMany/@ManyToMany
|
||||
- Add unnecessary @Column annotations
|
||||
- Add default constructors "just in case"
|
||||
|
||||
**Result:** Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Entity Bean Best Practices: `/docs/best-practice/`
|
||||
- JPA Mapping Reference: `/docs/mapping/jpa/`
|
||||
- Ebean Extensions: `/docs/mapping/extensions/`
|
||||
- First Entity Guide: `/docs/intro/first-entity/`
|
||||
@@ -0,0 +1,136 @@
|
||||
# Immutable bean cache for read-only references
|
||||
|
||||
This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
|
||||
references (for example `Label` references reused across many entities).
|
||||
|
||||
Use this when you want:
|
||||
|
||||
- fewer lazy-load SQL calls for assoc-one references via caching
|
||||
- reusable fetch-group-based loading for cache misses
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Build an immutable cache (typical via builder)
|
||||
|
||||
```java
|
||||
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
|
||||
.loading(database, fetchGroup)
|
||||
.maxSize(10_000)
|
||||
.maxIdleSeconds(300)
|
||||
.maxSecondsToLive(6_000)
|
||||
.build();
|
||||
```
|
||||
|
||||
`loading(...)` uses the query shape:
|
||||
|
||||
- `select(fetchGroup)`
|
||||
- `setUnmodifiable(true)`
|
||||
- `where().idIn(ids)`
|
||||
- `findMap()`
|
||||
|
||||
Alternative domain example:
|
||||
|
||||
```java
|
||||
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
.select("name,version")
|
||||
.fetch("billingAddress", "line1,city")
|
||||
.fetch("shippingAddress", "line1,city")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
.loading(database, customerGroup)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Attach cache to the root query
|
||||
|
||||
```java
|
||||
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
|
||||
.setId(id)
|
||||
.setUnmodifiable(true)
|
||||
.using(labelCache)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
`using(...)` is on the root query. Ebean will use this cache for matching
|
||||
bean types when resolving references.
|
||||
|
||||
---
|
||||
|
||||
## Use loading helper for simple memoization
|
||||
|
||||
If you don't need policy controls, use the shorthand helper:
|
||||
|
||||
```java
|
||||
ImmutableBeanCache<Label> labelCache =
|
||||
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
|
||||
```
|
||||
|
||||
With `ebean-core` on the classpath, builder policy settings are backed by core
|
||||
cache implementation (including periodic trim / eviction).
|
||||
|
||||
---
|
||||
|
||||
## Unmodifiable vs mutable query behavior
|
||||
|
||||
### Unmodifiable query path
|
||||
|
||||
`setUnmodifiable(true)` disables lazy loading. If you need association content
|
||||
in cached beans make sure that is included in the fetch group.
|
||||
|
||||
```java
|
||||
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
```
|
||||
|
||||
### Mutable query path
|
||||
|
||||
On a mutable query (no `setUnmodifiable(true)`), references populated from the
|
||||
immutable cache are still mutable beans in that object graph. Additional
|
||||
unloaded properties can still lazy load as normal.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
1. cache serves already-loaded reference properties (for example `version`)
|
||||
2. later access to unloaded properties (for example `labelTexts`) triggers
|
||||
normal lazy loading
|
||||
|
||||
---
|
||||
|
||||
## Understand secondary query behavior (`+query`, `+lazy`)
|
||||
|
||||
When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
|
||||
the immutable caches configured on the root query are propagated to those
|
||||
secondary queries.
|
||||
|
||||
That means assoc-one references resolved in secondary query paths can still hit
|
||||
the immutable cache.
|
||||
|
||||
---
|
||||
|
||||
## Operational note (TTL / max size)
|
||||
|
||||
Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
|
||||
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
|
||||
loader-based memoization.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
1. Hit / partial hit / miss behavior for `getAll(ids)`
|
||||
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
|
||||
3. Mutable path: additional unloaded properties can still lazy load
|
||||
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
|
||||
5. If needed associations are in fetch group, assert no extra SQL for those
|
||||
accesses
|
||||
@@ -0,0 +1,206 @@
|
||||
# Guide: Using Lombok with Ebean Entity Beans
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains which Lombok annotations are safe and recommended for Ebean
|
||||
entity beans, which ones to avoid, and why. It is written as prescriptive instructions
|
||||
for AI agents and developers.
|
||||
|
||||
---
|
||||
|
||||
## The Core Rule
|
||||
|
||||
> **Do NOT use `@Data` on Ebean entity beans.**
|
||||
|
||||
Use `@Getter` + `@Setter` instead, with the optional `@Accessors(chain = true)` for
|
||||
a fluent setter style.
|
||||
|
||||
---
|
||||
|
||||
## Why `@Data` is Incompatible with Ebean
|
||||
|
||||
`@Data` is a convenience annotation that is equivalent to applying `@Getter`,
|
||||
`@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode` together.
|
||||
Three of those are problematic for Ebean entity beans:
|
||||
|
||||
### 1. `@EqualsAndHashCode` (included in `@Data`) — breaks entity identity
|
||||
|
||||
`@Data` generates `hashCode()` and `equals()` based on all non-static, non-transient
|
||||
fields. Ebean entity beans have identity semantics — two references to the same database
|
||||
row should be considered equal based on their `@Id` value, not field-by-field comparison.
|
||||
|
||||
Problems caused:
|
||||
- Inconsistent `hashCode` before and after persist (the `@Id` field is `0` on a new
|
||||
entity, then changes after insert — violating the `hashCode` contract for collections)
|
||||
- Entities placed in a `Set` or `HashMap` before saving will be unfindable after saving
|
||||
- Ebean's internal identity map and dirty checking can be confused
|
||||
|
||||
### 2. `@ToString` (included in `@Data`) — triggers unexpected lazy loading
|
||||
|
||||
`@Data` generates a `toString()` that accesses **all** fields, including
|
||||
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
|
||||
outside of a transaction triggers a `LazyInitialisationException` or fires an unexpected
|
||||
SQL query, which can:
|
||||
- Cause subtle bugs in logging statements
|
||||
- Trigger N+1 queries in test output or debug logging
|
||||
- Fail with an exception if no active transaction exists
|
||||
|
||||
### 3. `@RequiredArgsConstructor` (included in `@Data`) — unnecessary for Ebean
|
||||
|
||||
Ebean does not require a default constructor — it can construct entity instances without
|
||||
one. `@RequiredArgsConstructor` therefore adds nothing useful to entity beans.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Annotation Set
|
||||
|
||||
Use exactly these three Lombok annotations on every Ebean entity bean:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain = true)
|
||||
@Table(name = "my_table")
|
||||
public class MyEntity {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
| Annotation | Purpose |
|
||||
|---|---|
|
||||
| `@Getter` | Generates `getFoo()` / `isFoo()` accessor methods |
|
||||
| `@Setter` | Generates `setFoo(value)` mutator methods; Ebean enhancement intercepts these for dirty tracking |
|
||||
| `@Accessors(chain = true)` | Makes setters return `this`, enabling fluent/builder-style property setting |
|
||||
|
||||
---
|
||||
|
||||
## `@Accessors(chain = true)` — Fluent Setter Style
|
||||
|
||||
With `chain = true`, setters return `this` instead of `void`, allowing method chaining:
|
||||
|
||||
```java
|
||||
// without chain = true (void setters)
|
||||
CMachine machine = new CMachine();
|
||||
machine.setMake("Toyota");
|
||||
machine.setModel("Hilux");
|
||||
machine.setStatus("active");
|
||||
|
||||
// with @Accessors(chain = true)
|
||||
CMachine machine = new CMachine()
|
||||
.setMake("Toyota")
|
||||
.setModel("Hilux")
|
||||
.setStatus("active");
|
||||
```
|
||||
|
||||
This is particularly useful when building test data:
|
||||
|
||||
```java
|
||||
CMachine machine = new CMachine()
|
||||
.setGid(UUID.randomUUID())
|
||||
.setMachineType("HV")
|
||||
.setStatus("active")
|
||||
.setMake("Komatsu")
|
||||
.setModel("PC200");
|
||||
|
||||
database.save(machine);
|
||||
```
|
||||
|
||||
Ebean's bytecode enhancement is fully compatible with chained setters — the
|
||||
enhancement intercepts each `setFoo()` call to record which fields have been modified
|
||||
(dirty checking), regardless of whether the setter returns `void` or `this`.
|
||||
|
||||
---
|
||||
|
||||
## `@Accessors(fluent = true)` — also compatible
|
||||
|
||||
`@Accessors(fluent = true)` removes the `get`/`set`/`is` prefix, generating `name()`
|
||||
(getter) and `name(value)` (setter) instead of `getName()` and `setName(value)`.
|
||||
|
||||
Ebean does **not** require JavaBeans naming conventions — it can work with any accessor
|
||||
method style, including fluent accessors with no prefix. `@Accessors(fluent = true)` is
|
||||
therefore compatible with Ebean.
|
||||
|
||||
`@Accessors(chain = true)` is the more common choice in practice (it keeps the familiar
|
||||
`get`/`set` prefix while adding method chaining), but `fluent = true` is a valid
|
||||
alternative if that style is preferred consistently across the codebase.
|
||||
|
||||
---
|
||||
|
||||
## Full Entity Bean Example
|
||||
|
||||
```java
|
||||
package com.example.repository.data;
|
||||
|
||||
import io.ebean.annotation.WhenCreated;
|
||||
import io.ebean.annotation.WhenModified;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain = true)
|
||||
@Table(name = "machine")
|
||||
public class CMachine {
|
||||
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private UUID gid;
|
||||
|
||||
@Column(nullable = false, length = 10)
|
||||
private String machineType;
|
||||
|
||||
@Column(length = 200)
|
||||
private String make;
|
||||
|
||||
@Column(length = 200)
|
||||
private String model;
|
||||
|
||||
@WhenCreated
|
||||
private Instant created;
|
||||
|
||||
@WhenModified
|
||||
private Instant lastModified;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary: Lombok Annotations and Ebean Compatibility
|
||||
|
||||
| Lombok Annotation | Compatible? | Notes |
|
||||
|---|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `@Getter` | ✅ Safe | Use on every entity bean |
|
||||
| `@Setter` | ✅ Safe | Use on every entity bean; enhancement intercepts these |
|
||||
| `@Accessors(chain = true)` | ✅ Safe | Recommended for fluent construction style |
|
||||
| `@ToString` | ❌ Avoid | Ebean does a better job and handles recursion |
|
||||
| `@EqualsAndHashCode` | ❌ Avoid | Breaks entity identity and `@Id`-based equality |
|
||||
| `@Data` | ❌ Avoid | Includes `@EqualsAndHashCode` and `@ToString` — both problematic |
|
||||
| `@Value` | ❌ Avoid | Makes fields final — incompatible with Ebean's field-level bytecode enhancement |
|
||||
| `@Accessors(fluent = true)` | ✅ Safe | Removes `get`/`set` prefix — Ebean does not require JavaBeans naming conventions and works with any accessor style |
|
||||
| `@Builder` | ⚠️ Careful | Usable on non-entity helper/factory classes; on entity beans it requires a no-arg constructor alongside it and offers no advantage over `@Accessors(chain = true)` |
|
||||
|
||||
---
|
||||
|
||||
## Relationship with Ebean Bytecode Enhancement
|
||||
|
||||
Ebean's bytecode enhancement (applied by `ebean-maven-plugin` at build time) modifies
|
||||
the `setXxx()` methods of entity beans to:
|
||||
1. Mark the field as dirty (changed) so only modified fields are included in UPDATE statements
|
||||
2. Support lazy loading of associations when a getter is called on an unloaded field
|
||||
|
||||
For this to work correctly, Ebean needs:
|
||||
- Accessor methods for each persistent field (any naming style is fine — `getFoo()`, `foo()`, or no accessors at all; Ebean can also access fields directly)
|
||||
- No override of `hashCode()` / `equals()` that would interfere with the identity map — which means **no `@Data` or `@EqualsAndHashCode`**
|
||||
@@ -0,0 +1,242 @@
|
||||
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide shows how to migrate legacy programmatic database creation code from:
|
||||
|
||||
- `new DatabaseConfig()`
|
||||
- `DatabaseFactory.create(...)`
|
||||
- old `setXxx(...)` builder-style configuration methods
|
||||
|
||||
…to the preferred builder-based style using:
|
||||
|
||||
- `Database.builder()`
|
||||
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
|
||||
- `DatabaseBuilder.build()`
|
||||
|
||||
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
|
||||
|
||||
---
|
||||
|
||||
## Preferred pattern
|
||||
|
||||
Prefer code shaped like this:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.dataSourceBuilder(dataSource)
|
||||
.register(true)
|
||||
.defaultDatabase(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
The important points are:
|
||||
|
||||
1. Start with `Database.builder()`
|
||||
2. Configure via `DatabaseBuilder`
|
||||
3. Finish with `.build()`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
|
||||
- If existing code only uses standard builder methods, this change is usually mechanical.
|
||||
- If existing code later reads configuration back, use `config.settings()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
Database database = config.build();
|
||||
```
|
||||
|
||||
### Short form
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Replace `DatabaseFactory.create("name")`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
Database database = DatabaseFactory.create("other");
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("other")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
|
||||
|
||||
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
|
||||
Use those names when migrating older setup code.
|
||||
|
||||
| Legacy call | Preferred call |
|
||||
|---|---|
|
||||
| `setName("db")` | `name("db")` |
|
||||
| `setRegister(false)` | `register(false)` |
|
||||
| `setDefaultServer(false)` | `defaultDatabase(false)` |
|
||||
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
|
||||
| `setDbSchema("app")` | `dbSchema("app")` |
|
||||
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
|
||||
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
|
||||
| `setRunMigration(true)` | `runMigration(true)` |
|
||||
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
|
||||
| `setPersistBatch(batch)` | `persistBatch(batch)` |
|
||||
|
||||
### Full example
|
||||
|
||||
#### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.setRegister(false);
|
||||
config.setDefaultServer(false);
|
||||
config.setDataSourceConfig(dataSource);
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.register(false)
|
||||
.defaultDatabase(false)
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify semantics after migration
|
||||
|
||||
The migration should preserve behavior, but verify these points:
|
||||
|
||||
- `register(true)` is still the default
|
||||
- `defaultDatabase(true)` is still the default
|
||||
- call `loadFromProperties()` if the old code loaded configuration from properties
|
||||
- for named databases, set the name before loading properties
|
||||
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
|
||||
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
|
||||
|
||||
---
|
||||
|
||||
## Manual-review cases
|
||||
|
||||
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
|
||||
|
||||
### `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
|
||||
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
|
||||
|
||||
### `DatabaseFactory.initialiseContainer(...)`
|
||||
|
||||
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
|
||||
|
||||
### `DatabaseFactory.shutdown()`
|
||||
|
||||
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
|
||||
|
||||
### Variables or method signatures typed as `DatabaseConfig`
|
||||
|
||||
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
|
||||
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
|
||||
|
||||
### Code that needs read access to builder settings
|
||||
|
||||
Use:
|
||||
|
||||
```java
|
||||
DatabaseBuilder builder = Database.builder();
|
||||
DatabaseBuilder.Settings settings = builder.settings();
|
||||
```
|
||||
|
||||
rather than relying on the concrete `DatabaseConfig` type only to read getters.
|
||||
|
||||
---
|
||||
|
||||
## Automation notes for AI agents and bulk refactors
|
||||
|
||||
This migration is a good candidate for semi-automated upgrading.
|
||||
|
||||
### Safe mechanical rewrites
|
||||
|
||||
These are usually safe to rewrite automatically:
|
||||
|
||||
- `new DatabaseConfig()` → `Database.builder()`
|
||||
- `DatabaseFactory.create(builder)` → `builder.build()`
|
||||
- `DatabaseFactory.create("name")` → `Database.builder().name("name").loadFromProperties().build()`
|
||||
- legacy `setXxx(...)` calls → preferred fluent builder methods
|
||||
|
||||
### Flag for manual review
|
||||
|
||||
Automatically flag, but do not blindly rewrite:
|
||||
|
||||
- `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
- `DatabaseFactory.initialiseContainer(...)`
|
||||
- `DatabaseFactory.shutdown()`
|
||||
- parameters, fields, or return types declared as `DatabaseConfig`
|
||||
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
|
||||
|
||||
---
|
||||
|
||||
## Related guides
|
||||
|
||||
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
|
||||
- [Guide index](README.md) — full list of Ebean setup and migration guides
|
||||
@@ -0,0 +1,447 @@
|
||||
# Guide: Persist Changes and Manage Transactions with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to save,
|
||||
update, delete, and batch changes with Ebean while choosing the correct
|
||||
transaction boundary.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- create a new entity row
|
||||
- update one or more existing rows
|
||||
- delete rows safely
|
||||
- decide between implicit transactions, `@Transactional`, and explicit
|
||||
transactions
|
||||
- batch or bulk-write many rows efficiently
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Choose the correct persistence operation first
|
||||
2. Use implicit transactions for a single isolated write
|
||||
3. Use `@Transactional` for multi-step application workflows
|
||||
4. Use explicit transactions only when you need explicit control
|
||||
5. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Entity beans and database configuration already exist
|
||||
- You know which `Database` is being used (`DB.getDefault()` or a named database)
|
||||
|
||||
If the project is not yet configured, first follow:
|
||||
|
||||
- [`add-ebean-postgres-database-config.md`](add-ebean-postgres-database-config.md)
|
||||
- [`entity-bean-creation.md`](entity-bean-creation.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Choose the correct persistence operation before editing code
|
||||
|
||||
Do not start with `database.save(...)` by habit. First decide what kind of change the
|
||||
caller is making.
|
||||
|
||||
| Need | Preferred API | Use when |
|
||||
|------|---------------|----------|
|
||||
| Insert a bean that is definitely new | `database.insert(bean)` | New-create flow, seed data, fixture setup |
|
||||
| Save a bean that may be new or existing | `database.save(bean)` | Common default when bean state determines insert vs update |
|
||||
| Update a bean that is definitely existing | `database.update(bean)` | Existing row should be updated only |
|
||||
| Delete one bean | `database.delete(bean)` | Remove a loaded entity bean |
|
||||
| Update many rows without loading beans | `database.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
|
||||
| Delete many rows without loading beans | bulk update/delete API or `database.sqlUpdate(...)` | Set-based deletion |
|
||||
|
||||
### Agent rule
|
||||
|
||||
Choose the operation that matches intent:
|
||||
|
||||
- known new row -> `insert`
|
||||
- known existing row -> `update`
|
||||
- uncertain/new-or-existing -> `save`
|
||||
- many rows -> bulk update/delete, not a loop of individual saves
|
||||
|
||||
### Style note
|
||||
|
||||
Use a `Database` instance for all persistence operations: `database.save(bean)`,
|
||||
`database.insert(bean)`, `database.update(bean)`, `database.delete(bean)`.
|
||||
Inject the `Database` bean or obtain it via `DB.getDefault()`. Avoid using the
|
||||
static `DB.*` convenience methods.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Persist single-bean changes with the correct API
|
||||
|
||||
### Example - insert a known new bean
|
||||
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Rob");
|
||||
customer.setEmail("rob@example.com");
|
||||
|
||||
database.insert(customer);
|
||||
```
|
||||
|
||||
### Example - update an existing bean
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.ACTIVE);
|
||||
|
||||
database.update(customer);
|
||||
```
|
||||
|
||||
### When to prefer `insert()` over `save()`
|
||||
|
||||
Use `insert()` when the code is creating a brand new row and should fail if the
|
||||
operation does not behave like an insert.
|
||||
|
||||
### When to prefer `update()` over `save()`
|
||||
|
||||
Use `update()` when the bean is definitely existing and the method should not
|
||||
silently behave like an insert.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Check cascade mappings before assuming related beans will persist or delete
|
||||
|
||||
Ebean follows cascade rules defined on mapping annotations such as
|
||||
`@OneToMany`, `@OneToOne`, `@ManyToOne`, and `@ManyToMany`.
|
||||
|
||||
The default is **no cascade**.
|
||||
|
||||
### Example
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
|
||||
@ManyToOne
|
||||
private Customer customer; // no cascade by default
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL)
|
||||
private List<OrderDetail> details; // save + delete cascade
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
database.save(order);
|
||||
```
|
||||
|
||||
With the mapping above:
|
||||
|
||||
- `details` are cascaded
|
||||
- `customer` is **not** cascaded
|
||||
|
||||
### Agent rules for cascades
|
||||
|
||||
1. Inspect the mapping before writing save/delete logic
|
||||
2. Do not assume `@ManyToOne` cascades
|
||||
3. Avoid adding cascade to shared parent references unless ownership is truly
|
||||
intended
|
||||
4. If a relationship should not cascade, save/delete related beans explicitly
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Let Ebean use an implicit transaction for a single isolated write
|
||||
|
||||
If the method performs one isolated persistence operation, Ebean can manage the
|
||||
transaction implicitly.
|
||||
|
||||
### Good fit for implicit transaction
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.INACTIVE);
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
### Good fit
|
||||
|
||||
- one save
|
||||
- one update
|
||||
- one delete
|
||||
- small helper method with a single write
|
||||
|
||||
### Poor fit
|
||||
|
||||
- multiple writes that must commit or roll back together
|
||||
- query + save + save workflow
|
||||
- any method where later failure must roll back earlier writes
|
||||
|
||||
### Important
|
||||
|
||||
Queries also use implicit transactions when needed. You generally do **not**
|
||||
need to wrap ordinary read queries in an explicit transaction "just in case".
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Use `@Transactional` for multi-step service workflows
|
||||
|
||||
When multiple Ebean operations belong to one unit of work, use
|
||||
`@Transactional`.
|
||||
|
||||
### Example - service method
|
||||
|
||||
```java
|
||||
import io.ebean.annotation.Transactional;
|
||||
|
||||
@Transactional
|
||||
public void shipOrder(long orderId) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.setStatus(Order.Status.SHIPPED);
|
||||
database.save(order);
|
||||
|
||||
Shipment shipment = new Shipment(order, Instant.now());
|
||||
database.insert(shipment);
|
||||
}
|
||||
```
|
||||
|
||||
All database work inside the method runs in one transaction and commits only if
|
||||
the method completes successfully.
|
||||
|
||||
### Use `Transaction.current()` only when needed
|
||||
|
||||
If the method needs access to the current transaction itself:
|
||||
|
||||
```java
|
||||
Transaction txn = Transaction.current();
|
||||
```
|
||||
|
||||
Do this only for transaction-specific behavior such as comments, savepoints, or
|
||||
other advanced control. Do not fetch the current transaction if the method does
|
||||
not need it.
|
||||
|
||||
### Agent rules for `@Transactional`
|
||||
|
||||
1. Put it on application/service workflow methods, not everywhere by default
|
||||
2. Keep the transaction focused on database work
|
||||
3. Avoid remote HTTP calls, message publishing, or long-running CPU work inside
|
||||
the transaction if those can be moved outside
|
||||
|
||||
### Named database note
|
||||
|
||||
If the method uses a non-default database, obtain that `Database` instance via
|
||||
`DB.byName("...")` and consistently use that database for both queries and
|
||||
writes.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `beginTransaction()` when you need explicit control
|
||||
|
||||
Use an explicit transaction when you need manual `commit()`, batching, explicit
|
||||
flush, savepoints, or other low-level transaction control.
|
||||
|
||||
### Example - explicit transaction with try-with-resources
|
||||
|
||||
```java
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.cancel();
|
||||
database.save(order);
|
||||
|
||||
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
|
||||
database.insert(auditLog);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
If `commit()` is not reached, closing the transaction rolls it back.
|
||||
|
||||
### Useful explicit controls
|
||||
|
||||
- `txn.commit()` - commit current work
|
||||
- `txn.setRollbackOnly()` - force rollback-only behavior
|
||||
- `txn.flush()` - push batched statements to the database now
|
||||
|
||||
### Agent rule
|
||||
|
||||
Prefer `@Transactional` unless explicit transaction control is actually needed.
|
||||
Do not use `beginTransaction()` only because it feels "safer".
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `createTransaction()` only for non-thread-local transaction handling
|
||||
|
||||
`createTransaction()` creates a transaction that is **not** placed into the
|
||||
thread-local scope. This is a specialized tool.
|
||||
|
||||
Use it when:
|
||||
|
||||
- the transaction will be passed explicitly
|
||||
- you need more than one transaction in the same thread
|
||||
- you are coordinating work across threads or lower-level APIs
|
||||
|
||||
### Example - explicit transaction passed to query and save
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.createTransaction()) {
|
||||
|
||||
Customer customer = new QCustomer(txn)
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
|
||||
customer.setInactive(true);
|
||||
database.save(customer, txn);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are not deliberately bypassing thread-local transaction scope, do **not**
|
||||
use `createTransaction()`. Most service code should use `@Transactional` or
|
||||
`beginTransaction()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use bulk update/delete or JDBC batch for many-row writes
|
||||
|
||||
Loops of `database.save(...)` are often the wrong tool for large write sets.
|
||||
|
||||
### Prefer bulk update for set-based changes
|
||||
|
||||
If the update can be expressed as "change all rows matching this predicate",
|
||||
perform one bulk update instead of loading and saving each bean.
|
||||
|
||||
### Example - bulk update with query beans
|
||||
|
||||
```java
|
||||
var cust = QCustomer.alias();
|
||||
|
||||
int rows = new QCustomer()
|
||||
.status.equalTo(Customer.Status.NEW)
|
||||
.asUpdate()
|
||||
.set(cust.status, Customer.Status.ACTIVE)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Example - bulk update with `database.update(...)`
|
||||
|
||||
```java
|
||||
int rows = database.update(Customer.class)
|
||||
.set("status", Customer.Status.ACTIVE)
|
||||
.where()
|
||||
.eq("status", Customer.Status.NEW)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Prefer JDBC batch for many individual inserts/updates
|
||||
|
||||
If each row has different values and must still go through per-bean persistence,
|
||||
use batching.
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
txn.setBatchMode(true);
|
||||
txn.setBatchSize(100);
|
||||
txn.setGetGeneratedKeys(false);
|
||||
|
||||
for (Customer customer : customersToInsert) {
|
||||
database.insert(customer, txn);
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative - annotation-driven batching
|
||||
|
||||
```java
|
||||
@Transactional(batchSize = 50)
|
||||
public void importCustomers(List<Customer> customers) {
|
||||
for (Customer customer : customers) {
|
||||
database.insert(customer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch caveats
|
||||
|
||||
- Executing a query inside a batched transaction can flush the batch
|
||||
- Mixing bean persistence and `SqlUpdate` can also flush the batch
|
||||
- Accessing generated/unloaded properties on batched beans can flush the batch
|
||||
|
||||
If the workflow depends on delayed flushing, review the batch-flush rules before
|
||||
adding more queries inside the same transaction.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Saving many rows one by one without batch or bulk update
|
||||
|
||||
If you are changing hundreds or thousands of rows, first ask whether it should
|
||||
be a bulk update or a batched transaction.
|
||||
|
||||
### Anti-pattern 2 - Assuming child beans cascade automatically
|
||||
|
||||
Cascade is not automatic. Inspect the mapping first.
|
||||
|
||||
### Anti-pattern 3 - Wrapping external calls inside the database transaction
|
||||
|
||||
Do not keep transactions open while waiting on HTTP calls, queues, or other
|
||||
slow external systems unless the design genuinely requires it.
|
||||
|
||||
### Anti-pattern 4 - Using `createTransaction()` for ordinary service code
|
||||
|
||||
Most service code should not bypass thread-local transaction handling.
|
||||
|
||||
### Anti-pattern 5 - Using `save()` when you really need `insert()` or `update()`
|
||||
|
||||
If operation intent matters, choose the more specific API.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| Child beans were not saved or deleted | Missing cascade mapping | Inspect annotations and add explicit save/delete or the correct cascade |
|
||||
| Earlier writes committed even though later work failed | The whole workflow was not inside one transaction | Wrap the unit of work in `@Transactional` or an explicit transaction |
|
||||
| `OptimisticLockException` on update/delete | Concurrent modification or stale version | Re-fetch, merge, or handle concurrency explicitly |
|
||||
| Batch writes flush earlier than expected | Query, mixed SQL, or property access triggered flush | Review batch flush rules and transaction flow |
|
||||
| Explicit transaction example does not affect the expected database | Mixed default DB and named DB usage | Use the same `Database` instance consistently for query and write |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add persistence logic:
|
||||
|
||||
1. Choose `insert`, `save`, `update`, `delete`, or bulk update based on intent
|
||||
2. Inspect cascade mappings before assuming related beans will persist/delete
|
||||
3. Use implicit transactions for one isolated write
|
||||
4. Use `@Transactional` for multi-step units of work
|
||||
5. Use `beginTransaction()` only when explicit transaction control is needed
|
||||
6. Use `createTransaction()` only for explicit, non-thread-local handling
|
||||
7. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Testing with TestEntityBuilder](testing-with-testentitybuilder.md)
|
||||
- [Ebean persist docs](https://ebean.io/docs/persist)
|
||||
- [Ebean transaction docs](https://ebean.io/docs/transactions)
|
||||
@@ -0,0 +1,817 @@
|
||||
# Guide: Testing with TestEntityBuilder
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
|
||||
|
||||
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
|
||||
|
||||
- **Integration tests** that need representative data without caring about specific values
|
||||
- **Persistence layer tests** that verify save/update/delete operations work correctly
|
||||
- **Query and filter tests** where you need multiple entities with varied data
|
||||
- **Rapid test setup** that reduces test code verbosity and improves readability
|
||||
|
||||
---
|
||||
|
||||
## Setup & Dependencies
|
||||
|
||||
### Add ebean-test to Your Project
|
||||
|
||||
The `TestEntityBuilder` class is provided by the `ebean-test` module.
|
||||
|
||||
**Maven:**
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle:**
|
||||
```gradle
|
||||
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
|
||||
```
|
||||
|
||||
Use a version that matches your Ebean runtime (`ebean.version` /
|
||||
`ebeanVersion`), or replace with an explicit fixed version if your build does
|
||||
not centralize dependency versions.
|
||||
|
||||
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
|
||||
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
|
||||
> runtime and test versions are not supported.
|
||||
|
||||
### Import the Class
|
||||
|
||||
```java
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Create a Builder Instance
|
||||
|
||||
`TestEntityBuilder` uses a builder pattern for configuration:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
|
||||
```
|
||||
|
||||
The `Database` parameter specifies which Ebean database instance to use for entity type
|
||||
lookups and persistence operations. Pass the injected `Database` bean (see
|
||||
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
|
||||
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
|
||||
use the injected `database` bean for **all** persistence operations in your tests
|
||||
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
|
||||
|
||||
### Build an Entity (In-Memory)
|
||||
|
||||
The `build()` method creates an instance with populated fields **without persisting to the database:**
|
||||
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
|
||||
// Fields are populated:
|
||||
// - id: unset (typically 0 for primitive long, null for boxed Long)
|
||||
// - name: random UUID-based string
|
||||
// - price: random BigDecimal
|
||||
// - inStock: true
|
||||
// - createdAt: current instant
|
||||
// - etc.
|
||||
|
||||
// Not persisted yet (`@Id` is still unset until the entity is persisted).
|
||||
```
|
||||
|
||||
### Build and Save (Persist to Database)
|
||||
|
||||
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
|
||||
|
||||
```java
|
||||
Product product = builder.save(Product.class);
|
||||
|
||||
// Entity is now in the database:
|
||||
assert database.find(Product.class, product.getId()) != null;
|
||||
```
|
||||
|
||||
### Save Multiple Entities
|
||||
|
||||
The `saveAll()` method persists multiple pre-built entities in a single call:
|
||||
|
||||
```java
|
||||
Product p1 = builder.build(Product.class);
|
||||
Product p2 = builder.build(Product.class);
|
||||
builder.saveAll(p1, p2);
|
||||
|
||||
// Both are now in the database with assigned IDs:
|
||||
assert p1.getId() != null;
|
||||
assert p2.getId() != null;
|
||||
```
|
||||
|
||||
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
|
||||
`Database` reference in tests that already hold a `TestEntityBuilder`.
|
||||
|
||||
### Access the Underlying Database
|
||||
|
||||
The `database()` method returns the `Database` instance used internally by the builder.
|
||||
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
|
||||
still need to perform `find()`, `delete()`, or other database operations:
|
||||
|
||||
```java
|
||||
Product saved = builder.save(Product.class);
|
||||
|
||||
// Use builder.database() instead of injecting a separate Database bean:
|
||||
Product found = builder.database().find(Product.class, saved.getId());
|
||||
assert found != null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using with Dependency Injection
|
||||
|
||||
Most applications using Ebean also use a DI framework. The recommended pattern is to
|
||||
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
|
||||
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
|
||||
|
||||
### Avaje Inject — `@TestScope @Factory`
|
||||
|
||||
Add a `@Bean` method to your test-scoped `@Factory` class:
|
||||
|
||||
```java
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.ContainerDatabase;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.inject.test.TestScope;
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes using `@InjectTest`:
|
||||
|
||||
```java
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject Database database;
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both patterns produce a single shared `TestEntityBuilder` instance, wired
|
||||
from the managed `Database` bean — no `@BeforeEach` required.
|
||||
|
||||
### Spring Boot — `@TestConfiguration`
|
||||
|
||||
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
// use @Primary if your main application context also wires a Database bean
|
||||
// or conditionally wire the main Database bean to exclude it from tests
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired Database database;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type-Specific Value Generation
|
||||
|
||||
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
|
||||
|
||||
| Type | Generated Value | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
|
||||
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
|
||||
| `Integer` / `int` | Random in `[1, 1_000)` | |
|
||||
| `Long` / `long` | Random in `[1, 100_000)` | |
|
||||
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
|
||||
| `Double` / `double` | Random in `[1, 100)` | |
|
||||
| `Float` / `float` | Random in `[1, 100)` | |
|
||||
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
|
||||
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
|
||||
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
|
||||
| `LocalDate` | Today's date | Via `LocalDate.now()` |
|
||||
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
|
||||
| `Instant` | Current instant | Via `Instant.now()` |
|
||||
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
|
||||
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
|
||||
| `Enum` | First constant | Override in custom generator if needed |
|
||||
| Other types | `null` | Set these fields manually in tests |
|
||||
|
||||
### String Length Constraints
|
||||
|
||||
`TestEntityBuilder` respects column length constraints defined in the entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Column(length = 50)
|
||||
private String username;
|
||||
}
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getUsername().length() <= 50; // ✅ Constraint respected
|
||||
```
|
||||
|
||||
### BigDecimal Precision and Scale
|
||||
|
||||
For `BigDecimal` fields, the builder respects the database column precision and scale:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class LineItem {
|
||||
@Column(precision = 10, scale = 2) // max 99_999_999.99
|
||||
private BigDecimal amount;
|
||||
}
|
||||
|
||||
LineItem item = builder.build(LineItem.class);
|
||||
assert item.getAmount().scale() == 2;
|
||||
```
|
||||
|
||||
### Short Fields Used as Boolean Flags
|
||||
|
||||
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
|
||||
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
|
||||
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
|
||||
code checks `entity.getActive() == 1` specifically, override the field after building:
|
||||
|
||||
```java
|
||||
Organisation org = builder.build(Organisation.class)
|
||||
.setActive((short) 1); // explicit override — random short won't do
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Cascade-Persist Relationships: Recursively Built
|
||||
|
||||
Relationships marked with `cascade = PERSIST` are recursively populated:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Customer customer;
|
||||
}
|
||||
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Both order and customer are built:
|
||||
assert order != null;
|
||||
assert order.getCustomer() != null;
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
|
||||
// When saved, cascade handles both:
|
||||
Order saved = builder.save(Order.class);
|
||||
assert saved.getId() != null;
|
||||
assert saved.getCustomer().getId() != null; // parent also saved
|
||||
```
|
||||
|
||||
### Non-Cascade Relationships: Left Null
|
||||
|
||||
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
|
||||
Create and save the related entity first (the builder works well here), then assign it manually
|
||||
before saving the parent:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@ManyToOne
|
||||
private Author author; // No cascade = left null by builder
|
||||
}
|
||||
|
||||
BlogPost post = builder.build(BlogPost.class);
|
||||
assert post.getAuthor() == null;
|
||||
|
||||
// Use the builder to create the related entity, then set it manually:
|
||||
Author author = builder.save(Author.class);
|
||||
post.setAuthor(author);
|
||||
database.save(post);
|
||||
```
|
||||
|
||||
### Collection Relationships: Left Empty
|
||||
|
||||
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
|
||||
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
|
||||
`.add()` or `.addAll()` directly is safe:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Author {
|
||||
@OneToMany(mappedBy = "author")
|
||||
private List<BlogPost> posts; // Left empty
|
||||
}
|
||||
|
||||
Author author = builder.build(Author.class);
|
||||
assert author.getPosts().isEmpty();
|
||||
|
||||
// Populate if needed for testing:
|
||||
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
|
||||
```
|
||||
|
||||
### Cycle Detection: Prevents Infinite Recursion
|
||||
|
||||
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Person {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Organization org;
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Organization {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Person founder;
|
||||
}
|
||||
|
||||
Person person = builder.build(Person.class);
|
||||
// One reference will be null to break the cycle:
|
||||
// either person.org or person.org.founder is null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Value Generators
|
||||
|
||||
### Why Customize?
|
||||
|
||||
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
|
||||
|
||||
- Email addresses with your company domain
|
||||
- Realistic phone numbers
|
||||
- Product SKUs following a pattern
|
||||
- Addresses in specific regions
|
||||
- Monetary amounts within realistic ranges
|
||||
|
||||
### Creating a Custom Generator
|
||||
|
||||
Subclass `RandomValueGenerator` and override individual `random*()` methods:
|
||||
|
||||
```java
|
||||
class CompanyTestDataGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
if (propName != null && propName.toLowerCase().contains("email")) {
|
||||
// Use company domain instead of generic @domain.com
|
||||
String localPart = UUID.randomUUID().toString().substring(0, 8);
|
||||
String email = localPart + "@mycompany.com";
|
||||
if (maxLength > 0 && email.length() > maxLength) {
|
||||
return email.substring(0, maxLength);
|
||||
}
|
||||
return email;
|
||||
}
|
||||
return super.randomString(propName, maxLength);
|
||||
}
|
||||
|
||||
// Override other methods as needed:
|
||||
@Override
|
||||
protected Object randomEnum(Class<?> type) {
|
||||
if (type == OrderStatus.class) {
|
||||
// Bias towards common statuses for realistic test data
|
||||
return ThreadLocalRandom.current().nextDouble() < 0.8
|
||||
? OrderStatus.PENDING
|
||||
: OrderStatus.COMPLETED;
|
||||
}
|
||||
return super.randomEnum(type);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using a Custom Generator
|
||||
|
||||
Pass the custom generator when building:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getEmail().endsWith("@mycompany.com");
|
||||
```
|
||||
|
||||
In a DI context, register this as the bean:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Money Type
|
||||
|
||||
```java
|
||||
public class MoneyValueGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Generate prices in a realistic range: $5.00 to $999.99
|
||||
BigDecimal price = BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
|
||||
);
|
||||
return price.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use for Integration Tests, Not Unit Tests
|
||||
|
||||
✅ **Good:** Integration test with database
|
||||
```java
|
||||
@Test
|
||||
void whenSaving_thenCanRetrieve() {
|
||||
Product product = builder.save(Product.class);
|
||||
Product found = database.find(Product.class, product.getId());
|
||||
assertThat(found).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Poor:** Validation test requiring specific values
|
||||
```java
|
||||
@Test
|
||||
void whenNameIsBlank_thenThrowException() {
|
||||
Product product = builder.build(Product.class); // name is random!
|
||||
product.setName(""); // have to override anyway
|
||||
// ... test proceeds
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Override Values for Specific Test Scenarios
|
||||
|
||||
When test requirements demand specific field values, manually override after building:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenStockIsLow_thenShowWarning() {
|
||||
Product product = builder.build(Product.class);
|
||||
product.setQuantity(2); // Specific value for this test
|
||||
|
||||
boolean shouldWarn = product.shouldShowLowStockWarning();
|
||||
assertThat(shouldWarn).isTrue();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Fixture Factories for Common Patterns
|
||||
|
||||
For shared domain-specific setup, encapsulate build patterns in an instance helper class
|
||||
rather than a static factory. In a DI context, this class can be registered as a bean
|
||||
alongside `TestEntityBuilder`:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
return new OrderTestFactory(builder, database);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderTestFactory {
|
||||
|
||||
private final TestEntityBuilder builder;
|
||||
private final Database database;
|
||||
|
||||
public OrderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
this.builder = builder;
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Order savePendingOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
public Order saveShippedOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
order.setShippedAt(Instant.now());
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in tests:
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired OrderTestFactory orderFactory;
|
||||
|
||||
@Test
|
||||
void whenOrderPending_thenCanUpdate() {
|
||||
Order order = orderFactory.savePendingOrder();
|
||||
// ... test logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build Multiple Distinct Instances
|
||||
|
||||
Each call to `build()` or `save()` produces a new instance with fresh random values:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenFetchingMultipleOrders_thenAllUnique() {
|
||||
Order order1 = builder.save(Order.class);
|
||||
Order order2 = builder.save(Order.class);
|
||||
Order order3 = builder.save(Order.class);
|
||||
|
||||
assertThat(order1.getId()).isNotEqualTo(order2.getId());
|
||||
assertThat(order2.getId()).isNotEqualTo(order3.getId());
|
||||
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Integration Test with Spring Boot
|
||||
|
||||
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
|
||||
the repository under test:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootTest
|
||||
class OrderRepositoryTest {
|
||||
|
||||
@Autowired OrderRepository orderRepository;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
|
||||
assertThat(pending).hasSize(2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Integration Test with Avaje Inject
|
||||
|
||||
```java
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Recursive Relationship Building
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenBuildingOrderWithCustomer_thenBothPopulated() {
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
|
||||
assertThat(order.getCustomer()).isNotNull();
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
assertThat(order.getCustomer().getName()).isNotNull();
|
||||
|
||||
// Saving cascades to customer:
|
||||
Order saved = builder.save(Order.class);
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
assertThat(saved.getCustomer().getId()).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Custom Generator for Domain Values
|
||||
|
||||
```java
|
||||
// Custom generator for your domain
|
||||
class ECommerceTestDataGenerator extends RandomValueGenerator {
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Product prices typically range $10-$500
|
||||
return BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
|
||||
).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingCustomGenerator() {
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new ECommerceTestDataGenerator())
|
||||
.build();
|
||||
|
||||
Product product = builder.build(Product.class);
|
||||
assertThat(product.getPrice())
|
||||
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No BeanDescriptor found for [Class] — is it an @Entity?"
|
||||
|
||||
**Cause:** The class you're trying to build is not registered as an Ebean entity.
|
||||
|
||||
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Fields are unset even though I expected them to be populated
|
||||
|
||||
**Cause:** `TestEntityBuilder` does **not** populate:
|
||||
- `@Id` fields (identity/primary key; left unset until persist)
|
||||
- `@Version` fields (optimistic locking; left unset until persist)
|
||||
- `@Transient` fields
|
||||
- `@OneToMany` collections
|
||||
- Non-cascade `@ManyToOne` relationships
|
||||
|
||||
**Solution:** Set only the fields your test scenario cares about, then persist.
|
||||
`@Id` and `@Version` are usually database-managed and should typically be left
|
||||
unset before save:
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
product.setName("specific-name"); // test-specific override
|
||||
database.save(product); // database assigns @Id/@Version
|
||||
```
|
||||
|
||||
### Building recursive relationships causes StackOverflowError
|
||||
|
||||
**Cause:** Two or more entities mutually reference each other without cycle detection.
|
||||
|
||||
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
|
||||
```java
|
||||
Person person = builder.build(Person.class);
|
||||
person.getOrganization().setFounder(null); // Break cycle
|
||||
```
|
||||
|
||||
### Values generated are "too random" for my test
|
||||
|
||||
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
|
||||
|
||||
**Solution:** Create a custom generator that produces deterministic values:
|
||||
```java
|
||||
class DeterministicTestDataGenerator extends RandomValueGenerator {
|
||||
private int counter = 0;
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
return "test_" + (counter++);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`TestEntityBuilder` accelerates test development by:
|
||||
|
||||
1. **Reducing boilerplate** — No need to manually set every field
|
||||
2. **Improving readability** — Tests focus on what matters, not setup
|
||||
3. **Enabling variety** — Each build produces distinct random values
|
||||
4. **Respecting constraints** — Column lengths and decimal scales are enforced
|
||||
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
# Guide: Write Ebean Queries with Query Beans
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to write
|
||||
application queries using Ebean query beans.
|
||||
|
||||
Use this guide when the project already has Ebean configured and you need to:
|
||||
|
||||
- add a repository/service query
|
||||
- replace string-based ORM queries with type-safe query beans
|
||||
- tune what data is fetched to avoid over-fetching or N+1 issues
|
||||
- return DTO projections for list screens or API responses
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Prefer query beans first
|
||||
2. Prefer entity queries for domain logic
|
||||
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
|
||||
4. Prefer DTO projection for summary/read-model use cases
|
||||
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Query bean generation is configured (for Maven this usually means
|
||||
`querybean-generator` is registered as an annotation processor)
|
||||
- Entity beans already exist
|
||||
- A compile/build has run successfully since the last entity model change
|
||||
|
||||
If query beans are not yet configured, first follow:
|
||||
[`add-ebean-postgres-maven-pom.md`](add-ebean-postgres-maven-pom.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Verify the generated `Q*` query bean exists
|
||||
|
||||
For each entity bean, Ebean generates a query bean with the same name prefixed
|
||||
with `Q`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `Customer` -> `QCustomer`
|
||||
- `Order` -> `QOrder`
|
||||
- `Contact` -> `QContact`
|
||||
|
||||
Import the generated type from the query bean package:
|
||||
|
||||
```java
|
||||
import org.example.domain.query.QCustomer;
|
||||
```
|
||||
|
||||
If the `Q*` type does not exist or the IDE cannot resolve it:
|
||||
|
||||
1. Confirm the entity compiled successfully
|
||||
2. Run a normal project compile/build
|
||||
3. If the entity was renamed or moved, run a full rebuild rather than relying on
|
||||
incremental compilation
|
||||
|
||||
### Important caveat - entity rename
|
||||
|
||||
After refactoring an entity name, old generated query beans can remain on disk
|
||||
until the next full build. If both old and new `Q*` types appear to exist, do a
|
||||
clean rebuild before editing application queries.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Choose the terminal query method before writing predicates
|
||||
|
||||
Decide what the caller actually needs. This determines the terminal method and
|
||||
often the right query shape.
|
||||
|
||||
| Need | Preferred method | Notes |
|
||||
|------|------------------|-------|
|
||||
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
|
||||
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
|
||||
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
|
||||
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
|
||||
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
|
||||
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
|
||||
|
||||
### Example - existence check
|
||||
|
||||
```java
|
||||
boolean alreadyUsed = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.exists();
|
||||
```
|
||||
|
||||
### Example - unique lookup
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
Do **not** use `findOne()` for predicates that can match multiple rows.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Build predicates by traversing properties and associations
|
||||
|
||||
With query beans, write predicates directly against properties. When you
|
||||
traverse an association, Ebean adds the necessary joins automatically.
|
||||
|
||||
### Example - root property predicates
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - association traversal
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo("Auckland")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - collection predicate
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.contacts.isEmpty()
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
When adding a new query:
|
||||
|
||||
1. Start from the root entity that the caller wants back
|
||||
2. Add predicates with query bean properties
|
||||
3. Traverse relationships instead of writing manual join SQL
|
||||
4. Keep property references type-safe; avoid string property names unless the API
|
||||
specifically requires them
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Add ordering, limits, and pagination deliberately
|
||||
|
||||
Do not leave list queries unordered unless the call site truly does not care.
|
||||
For UI lists, APIs, and background jobs, explicit ordering is usually better.
|
||||
|
||||
### Example - ordered list with limit
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.setMaxRows(50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - offset/limit pagination
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - paged list with total count
|
||||
|
||||
```java
|
||||
PagedList<Customer> page = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findPagedList();
|
||||
|
||||
page.loadRowCount();
|
||||
List<Customer> customers = page.getList();
|
||||
int totalRowCount = page.getTotalRowCount();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
- Use `findList()` when the caller only needs rows
|
||||
- Use `findPagedList()` when the caller also needs page metadata or total counts
|
||||
- Pair pagination with a stable `orderBy()` so page boundaries stay predictable
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Control fetched data with `select()` and `fetch()`
|
||||
|
||||
By default, entity queries can load more of the object graph than the caller
|
||||
actually needs. Use `select()` and `fetch()` to control the root and association
|
||||
properties that are loaded.
|
||||
|
||||
### Root properties with `select()`
|
||||
|
||||
Use `select()` to define which properties should be fetched on the root entity.
|
||||
|
||||
### Associated bean properties with `fetch()`
|
||||
|
||||
Use `fetch()` to define what should be fetched on associated paths.
|
||||
|
||||
### Example - partial entity query
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `select(...)` tunes the root `Customer` properties
|
||||
- `contacts.fetch(...)` tunes the associated `Contact` properties
|
||||
- the query still returns `Customer` entity beans
|
||||
|
||||
### Agent rules for partial entity queries
|
||||
|
||||
1. Only use `select()`/`fetch()` when you know what the caller will read next
|
||||
2. Do not treat partially loaded entities like fully populated API DTOs
|
||||
3. If the caller only needs summary fields, prefer a DTO projection instead
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
|
||||
|
||||
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
|
||||
read-only graph.
|
||||
|
||||
This means:
|
||||
|
||||
- setters cannot mutate returned beans
|
||||
- associated collections are unmodifiable
|
||||
- lazy loading is disabled
|
||||
- accessing an unloaded property throws `LazyInitialisationException`
|
||||
- the query uses `PersistenceContextScope.QUERY`
|
||||
|
||||
### Example - read-only entity graph
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.setUnmodifiable(true)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### When to prefer `setUnmodifiable(true)`
|
||||
|
||||
Use it when the result is meant to be read-only, such as:
|
||||
|
||||
- service/query methods returning entity graphs for display or serialization
|
||||
- query results you want the application to treat as immutable
|
||||
- cached query results or other shared read models backed by entity graphs
|
||||
- partial entity graphs where you want accidental lazy loading to fail fast
|
||||
|
||||
### When **not** to use it
|
||||
|
||||
Do **not** use `setUnmodifiable(true)` when the caller will:
|
||||
|
||||
- modify the beans and save them later
|
||||
- rely on lazy loading of associations or unloaded scalar properties
|
||||
- treat the result as a working persistence model rather than a read-only view
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
|
||||
should be the default recommendation. If the caller needs a mutable model or a
|
||||
serialized summary shape, choose mutable entities or DTO projection instead.
|
||||
|
||||
If you need cached assoc-one references for unmodifiable graphs, see
|
||||
[Immutable bean cache for read-only references](immutable-bean-cache.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
|
||||
|
||||
Ebean applies important SQL rules when translating ORM queries:
|
||||
|
||||
1. It does not generate SQL cartesian products
|
||||
2. It honors `maxRows` in SQL
|
||||
|
||||
This means to-many paths often need special handling.
|
||||
|
||||
### Use `fetchQuery()` when:
|
||||
|
||||
- the query includes a `OneToMany` or `ManyToMany` path
|
||||
- the query includes `setMaxRows(...)`
|
||||
- the query loads multiple to-many paths
|
||||
- you want the query shape to make the secondary-query behavior explicit
|
||||
|
||||
### Example - explicit secondary queries for to-many paths
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
List<Order> orders = new QOrder()
|
||||
.customer.fetch(CUST.name)
|
||||
.lines.fetchQuery()
|
||||
.shipments.fetchQuery()
|
||||
.status.equalTo(Order.Status.NEW)
|
||||
.setMaxRows(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Use `FetchGroup` when:
|
||||
|
||||
- the same fetch shape is reused in multiple places
|
||||
- you want to separate predicate logic from fetch-shape tuning
|
||||
- you want an immutable, static query-shape definition
|
||||
|
||||
### Example - reusable fetch group
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
private static final FetchGroup<Customer> CUSTOMER_SUMMARY =
|
||||
QCustomer.forFetchGroup()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.billingAddress.fetch()
|
||||
.buildFetchGroup();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUSTOMER_SUMMARY)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If the caller needs multiple to-many paths or a paged query, be suspicious of a
|
||||
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use DTO projection when the caller does not need entity beans
|
||||
|
||||
For list screens, API summaries, exports, or read-model views, the caller often
|
||||
does **not** need managed entity beans. In those cases, project directly to a
|
||||
DTO using `asDto(...)`.
|
||||
|
||||
### Example - DTO projection with query beans
|
||||
|
||||
```java
|
||||
import static org.example.domain.query.QCustomer.Alias.id;
|
||||
import static org.example.domain.query.QCustomer.Alias.name;
|
||||
|
||||
public record CustomerSummary(long id, String name) {}
|
||||
|
||||
List<CustomerSummary> summaries = new QCustomer()
|
||||
.select(id, name)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.asDto(CustomerSummary.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Prefer DTO projection when:
|
||||
|
||||
- the caller will serialize the result directly
|
||||
- only a subset of fields is needed
|
||||
- the result is not going to be updated and saved back as an entity
|
||||
- the query contains formulas or aggregation intended for a read model
|
||||
|
||||
---
|
||||
|
||||
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
|
||||
|
||||
Prefer the following order:
|
||||
|
||||
1. Query bean query
|
||||
2. Query bean query + `asDto(...)`
|
||||
3. `database.findDto(...)` or DTO query
|
||||
4. Native SQL / `SqlQuery` / `RawSql`
|
||||
|
||||
### Typical reasons to use raw SQL
|
||||
|
||||
- vendor-specific SQL that query beans do not express well
|
||||
- advanced aggregation or database functions
|
||||
- hand-tuned reporting queries
|
||||
- stored procedures or raw JDBC workflows
|
||||
|
||||
Do **not** jump to raw SQL just because the query joins multiple tables. Query
|
||||
beans already handle ordinary relationship traversal well.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Using raw SQL first
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = database.findNative(Customer.class,
|
||||
"select c.* from customer c join address a on a.id = c.billing_address_id where a.city = ?")
|
||||
.setParameter(1, city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
**Prefer:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo(city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Anti-pattern 2 - Using `findOne()` on a non-unique predicate
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
**Why:** Many rows can match; this is not a unique lookup.
|
||||
|
||||
### Anti-pattern 3 - Returning partially loaded entities as API models
|
||||
|
||||
If the caller only needs summary fields, return a DTO instead of partially
|
||||
loaded entities that might later trigger more loading or confuse serializers.
|
||||
|
||||
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
|
||||
|
||||
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
|
||||
so accidental setter calls, collection mutation, and lazy loading fail fast.
|
||||
|
||||
### Anti-pattern 5 - Fetching every relationship "just in case"
|
||||
|
||||
Do not eagerly fetch large object graphs unless the immediate caller will use
|
||||
them. Query tuning is part of the job.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `Cannot resolve symbol QCustomer` | Query bean generation not configured or build not run | Check the annotation processor and run a build |
|
||||
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
|
||||
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
|
||||
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
|
||||
| Setter calls or collection mutation fail on query results | `setUnmodifiable(true)` returned a read-only graph | Remove `setUnmodifiable(true)` or treat the result as read-only |
|
||||
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
|
||||
| Ebean executes secondary queries for a to-many path | ORM rules avoided cartesian product or honored `maxRows` | This is expected; use `fetchQuery()` explicitly when appropriate |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add or modify an Ebean query:
|
||||
|
||||
1. Verify the relevant `Q*` type exists
|
||||
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
|
||||
3. Add predicates with query bean properties and association traversal
|
||||
4. Add explicit ordering and pagination if relevant
|
||||
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
|
||||
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
|
||||
7. Prefer DTO projection for read models and serialized responses
|
||||
8. Only use raw SQL if the ORM query is genuinely the wrong tool
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
@@ -0,0 +1,333 @@
|
||||
# Immutable Bean Cache — notes on multi-level / remote caching
|
||||
|
||||
These notes capture design thoughts for a possible future multi-level immutable bean cache,
|
||||
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
|
||||
in addition to an in-JVM cache.
|
||||
|
||||
## Current important constraint
|
||||
|
||||
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
|
||||
|
||||
That means:
|
||||
|
||||
- `getIfPresent(id)` is on the **row read hot path**
|
||||
- it must remain **cheap and local**
|
||||
- it should **not** perform network I/O
|
||||
- it should **not** deserialize remote payloads
|
||||
- it should **not** trigger loading or record misses
|
||||
|
||||
## Strong recommendation
|
||||
|
||||
For any multi-level cache design:
|
||||
|
||||
- **L1 cache** = in-JVM cache of already materialized immutable beans
|
||||
- **L2 cache** = remote/shared cache of serialized immutable snapshots
|
||||
- **Loader** = Ebean query using the configured fetch group
|
||||
|
||||
With that split:
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
|
||||
|
||||
This preserves the `AssocOneHelp` fast path.
|
||||
|
||||
---
|
||||
|
||||
## Snapshot mindset
|
||||
|
||||
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
|
||||
|
||||
A cached value is specific to:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant (if multi-tenant)
|
||||
- fetch-group / cache identity
|
||||
- serializer/schema version
|
||||
|
||||
This matters because a `Customer` cached with:
|
||||
|
||||
- `select("name,version")`
|
||||
|
||||
is not equivalent to a `Customer` cached with:
|
||||
|
||||
- `select("name,version").fetch("billingAddress", "line1,city")`
|
||||
|
||||
## Key design recommendation
|
||||
|
||||
Remote keys should include at least:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant id (if applicable)
|
||||
- cache/fetch-group identity
|
||||
- optionally serializer/schema version
|
||||
|
||||
Example shape:
|
||||
|
||||
- `immutable:Customer:basic:42`
|
||||
- `immutable:Customer:withAddresses:42`
|
||||
|
||||
---
|
||||
|
||||
## Recommended multi-level flow
|
||||
|
||||
### L1
|
||||
|
||||
Store actual read-only `EntityBean` instances.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- support `getIfPresent(id)`
|
||||
- avoid repeated deserialize cost
|
||||
- avoid network calls on row read path
|
||||
|
||||
### L2
|
||||
|
||||
Store serialized immutable snapshots.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- batch lookup only
|
||||
- support cross-JVM sharing
|
||||
- feed L1 with materialized immutable beans
|
||||
|
||||
### Loader
|
||||
|
||||
Use the existing query/fetch-group-based loader for misses.
|
||||
|
||||
### Suggested `getAll(ids)` flow
|
||||
|
||||
1. Check L1
|
||||
2. Batch remaining ids to L2
|
||||
3. Deserialize L2 hits into read-only beans
|
||||
4. Put those beans into L1
|
||||
5. Batch remaining misses to DB loader
|
||||
6. Freeze / ensure read-only beans
|
||||
7. Write through to L2
|
||||
8. Put into L1
|
||||
9. Negative-cache true misses if desired
|
||||
|
||||
---
|
||||
|
||||
## Invalidation is more important than serialization
|
||||
|
||||
Things to think about:
|
||||
|
||||
- update/delete invalidation across JVMs
|
||||
- local L1 invalidation when L2 entry is removed
|
||||
- ordering relative to DB commit
|
||||
- multiple cache instances for the same bean type but different fetch groups
|
||||
- tenant-scoped invalidation
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- keep current immutable-cache invalidation semantics
|
||||
- add a remote invalidation/event mechanism for L2-backed caches
|
||||
- each JVM should evict affected L1 entries when notified
|
||||
|
||||
Examples:
|
||||
|
||||
- Redis: pub/sub or streams
|
||||
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
|
||||
|
||||
---
|
||||
|
||||
## Serialization format considerations
|
||||
|
||||
## JSON
|
||||
|
||||
### Pros
|
||||
|
||||
- human readable / debuggable
|
||||
- easier rolling upgrades
|
||||
- field-name based, so generally more tolerant of schema evolution
|
||||
- good fit for Redis strings or Postgres JSONB
|
||||
- easier operational debugging
|
||||
|
||||
### Cons
|
||||
|
||||
- larger payloads
|
||||
- more CPU to serialize/deserialize
|
||||
- nested graphs / enums / dates / inheritance need disciplined handling
|
||||
|
||||
## Kryo / generic binary serialization
|
||||
|
||||
### Pros
|
||||
|
||||
- smaller payloads
|
||||
- often faster than JSON
|
||||
- can preserve object graphs efficiently
|
||||
|
||||
### Cons
|
||||
|
||||
- more fragile across versions and rolling deploys
|
||||
- class registration / compatibility pain
|
||||
- harder to inspect/debug
|
||||
- tighter coupling to JVM/class layout
|
||||
- riskier for long-lived shared cache entries
|
||||
|
||||
## Recommendation
|
||||
|
||||
For a first remote/shared implementation:
|
||||
|
||||
- prefer **JSON** or another self-describing structured format
|
||||
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
|
||||
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
|
||||
|
||||
---
|
||||
|
||||
## What to serialize
|
||||
|
||||
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
|
||||
|
||||
A cleaner model is:
|
||||
|
||||
- serialize a **snapshot representation**
|
||||
- deserialize into a fresh entity bean
|
||||
- mark loaded properties appropriately
|
||||
- freeze / ensure read-only state
|
||||
- store the resulting materialized bean in L1
|
||||
|
||||
This gives more control over:
|
||||
|
||||
- loaded-property semantics
|
||||
- read-only state
|
||||
- subtype handling
|
||||
- schema/version evolution
|
||||
|
||||
## Practical recommendation
|
||||
|
||||
Remote cache entries should represent exactly the configured fetch-group snapshot.
|
||||
|
||||
That means:
|
||||
|
||||
- cache what the fetch group loaded
|
||||
- include nested associations loaded by that fetch group
|
||||
- treat it as a self-contained immutable snapshot
|
||||
|
||||
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
|
||||
|
||||
---
|
||||
|
||||
## Redis vs Postgres cache table
|
||||
|
||||
## Redis
|
||||
|
||||
### Good for
|
||||
|
||||
- low latency
|
||||
- batch lookup via MGET / pipelining
|
||||
- TTL/eviction support
|
||||
- natural shared-cache use case
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- extra infrastructure
|
||||
- memory cost
|
||||
- invalidation/event coordination still required
|
||||
|
||||
## Postgres cache table (including unlogged-style approach)
|
||||
|
||||
### Good for
|
||||
|
||||
- simpler ops if Postgres is already present
|
||||
- easy batch lookup with `IN (...)`
|
||||
- fewer moving parts than introducing Redis
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- slower than Redis for hot shared-cache usage
|
||||
- adds pressure to Postgres
|
||||
- TTL/cleanup becomes application responsibility
|
||||
- still network/database I/O, so should remain off the `getIfPresent()` hot path
|
||||
|
||||
## Recommendation
|
||||
|
||||
- if the goal is a serious shared L2 cache, Redis is the more natural fit
|
||||
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
|
||||
|
||||
---
|
||||
|
||||
## Versioning / evolution
|
||||
|
||||
Whatever serializer is used, include versioning information.
|
||||
|
||||
Useful dimensions:
|
||||
|
||||
- serializer/schema version
|
||||
- cache implementation version
|
||||
- fetch-group/cache identity version
|
||||
|
||||
This helps when:
|
||||
|
||||
- fields are added/removed
|
||||
- graph shape changes
|
||||
- fetch-group definitions evolve
|
||||
|
||||
---
|
||||
|
||||
## Compression
|
||||
|
||||
If remote snapshots become large:
|
||||
|
||||
- compress only above a size threshold
|
||||
- avoid compressing tiny payloads
|
||||
|
||||
This is especially relevant for JSON in Redis or Postgres L2.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
A multi-level cache should expose at least:
|
||||
|
||||
- L1 hit rate
|
||||
- L2 hit rate
|
||||
- DB loader rate
|
||||
- deserialize failures
|
||||
- invalidation counts
|
||||
- average payload size
|
||||
- cold-start amplification
|
||||
|
||||
Without this, it will be hard to judge whether the remote cache is helping.
|
||||
|
||||
---
|
||||
|
||||
## Overall recommended architecture
|
||||
|
||||
### Recommended model
|
||||
|
||||
- **L1**: actual read-only `EntityBean` instances
|
||||
- **L2**: serialized immutable snapshots
|
||||
- **Loader**: fetch-group-based DB query
|
||||
|
||||
### Method responsibilities
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
|
||||
|
||||
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
|
||||
|
||||
---
|
||||
|
||||
## Bottom line
|
||||
|
||||
If/when multi-level immutable caching is explored, the main points to preserve are:
|
||||
|
||||
1. keep `getIfPresent()` local-only
|
||||
2. do remote work only in batched `getAll()`
|
||||
3. key by type + id + tenant + fetch-group/cache identity
|
||||
4. treat remote values as immutable snapshots
|
||||
5. prefer JSON/self-describing format first
|
||||
6. be cautious with generic binary serializers like Kryo
|
||||
|
||||
---
|
||||
|
||||
## Possible follow-up
|
||||
|
||||
If this becomes active design work later, consider promoting these notes into one of:
|
||||
|
||||
- a dedicated design note under `docs/notes/`
|
||||
- a GitHub issue / discussion for design iteration
|
||||
- a lightweight ADR if this becomes a committed architectural direction
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
|
||||
@@ -23,11 +23,18 @@ import java.util.concurrent.Callable;
|
||||
/**
|
||||
* Provides the API for fetching and saving beans to a particular database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
|
||||
* They can also be automatically constructed on demand using configuration information in
|
||||
* the application.properties file. The underlying implementation is provided by
|
||||
* {@link DatabaseFactory}.
|
||||
*
|
||||
* <h5>Registration with the DB singleton</h5>
|
||||
* <p>
|
||||
* When a Database instance is created it can be registered with the DB
|
||||
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
|
||||
* singleton is essentially a map of Database's that have been registered
|
||||
* When a Database instance is created it can be registered with the {@link DB}
|
||||
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
|
||||
* singleton is essentially a map of {@link Database}'s that have been registered
|
||||
* with it.
|
||||
* <p>
|
||||
* The Database can then be retrieved later via {@link DB#byName(String)}.
|
||||
@@ -35,16 +42,10 @@ import java.util.concurrent.Callable;
|
||||
* <h5>The 'default' Database</h5>
|
||||
* <p>
|
||||
* One Database can be designated as the 'default' or 'primary' Database
|
||||
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
|
||||
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
|
||||
* such as {@link DB#find(Class)} etc are actually just a convenient way to
|
||||
* call methods on the 'default/primary' Database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are constructed by the DatabaseFactory. They can be created
|
||||
* programmatically via {@link DatabaseFactory#create(DatabaseBuilder)} or they
|
||||
* can be automatically constructed on demand using configuration information in
|
||||
* the application.properties file.
|
||||
*
|
||||
* <h5>Example: Get a Database</h5>
|
||||
* <pre>{@code
|
||||
@@ -80,6 +81,7 @@ import java.util.concurrent.Callable;
|
||||
* method. Example: a single thread requires more than one transaction.
|
||||
*
|
||||
* @see DB
|
||||
* @see DatabaseBuilder
|
||||
* @see DatabaseFactory
|
||||
* @see DatabaseConfig
|
||||
*/
|
||||
@@ -94,11 +96,13 @@ public interface Database {
|
||||
* // from application.properties / application.yaml
|
||||
*
|
||||
* Database db = Database.builder()
|
||||
* .name("db")
|
||||
* .loadFromProperties()
|
||||
* .build();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
static DatabaseBuilder builder() {
|
||||
return new DatabaseConfig();
|
||||
}
|
||||
|
||||
@@ -723,19 +723,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
|
||||
|
||||
/**
|
||||
* Set the configuration for profiling.
|
||||
*/
|
||||
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
|
||||
return setProfilingConfig(profilingConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
|
||||
*/
|
||||
@Deprecated
|
||||
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
|
||||
|
||||
/**
|
||||
* Set the suffix appended to the base table to derive the view that contains the union
|
||||
* of the base table and the history table in order to support asOf queries.
|
||||
@@ -995,7 +982,7 @@ public interface DatabaseBuilder {
|
||||
* <p>
|
||||
* Use this to override the default known aggregation functions.
|
||||
*/
|
||||
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
|
||||
/**
|
||||
* Set to true if all DB column and table names should use quoted identifiers.
|
||||
@@ -2234,7 +2221,7 @@ public interface DatabaseBuilder {
|
||||
*
|
||||
* @param includeLabelInSql When true include a SQL inline comment in generated SELECT queries.
|
||||
*/
|
||||
DatabaseConfig includeLabelInSql(boolean includeLabelInSql);
|
||||
DatabaseBuilder includeLabelInSql(boolean includeLabelInSql);
|
||||
|
||||
/**
|
||||
* Set the naming convention to apply to metrics names.
|
||||
@@ -2252,7 +2239,7 @@ public interface DatabaseBuilder {
|
||||
/**
|
||||
* Sets the length check mode.
|
||||
*/
|
||||
DatabaseConfig lengthCheck(LengthCheck lengthCheck);
|
||||
DatabaseBuilder lengthCheck(LengthCheck lengthCheck);
|
||||
|
||||
/**
|
||||
* Provides read access (getters) for the DatabaseBuilder configuration
|
||||
@@ -2490,11 +2477,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
TenantCatalogProvider getTenantCatalogProvider();
|
||||
|
||||
/**
|
||||
* Return the configuration for profiling.
|
||||
*/
|
||||
ProfilingConfig getProfilingConfig();
|
||||
|
||||
/**
|
||||
* Return the DB schema to use.
|
||||
*/
|
||||
|
||||
@@ -8,18 +8,18 @@ import jakarta.persistence.PersistenceException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Creates Database instances.
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* <p>
|
||||
* This uses either DatabaseConfig or properties in the application.properties file to
|
||||
* configure and create a Database instance.
|
||||
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
|
||||
* This factory remains for legacy creation entry points plus container lifecycle methods.
|
||||
* <p>
|
||||
* The Database instance can either be registered with the DB singleton or
|
||||
* not. The DB singleton effectively holds a map of Database by a name.
|
||||
* If the Database is registered with the DB singleton you can retrieve it
|
||||
* The Database instance can either be registered with the {@link DB} singleton or
|
||||
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
|
||||
* If the Database is registered with the {@link DB} singleton you can retrieve it
|
||||
* later via {@link DB#byName(String)}.
|
||||
* <p>
|
||||
* One Database can be nominated as the 'default/primary' Database. Many
|
||||
* methods on the DB singleton such as {@link DB#find(Class)} are just a
|
||||
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
|
||||
* convenient way of using the 'default/primary' Database.
|
||||
*/
|
||||
public final class DatabaseFactory {
|
||||
@@ -36,7 +36,8 @@ public final class DatabaseFactory {
|
||||
* Initialise the container with clustering configuration.
|
||||
* <p>
|
||||
* Call this prior to creating any Database instances or alternatively set the
|
||||
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
|
||||
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
|
||||
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
|
||||
*/
|
||||
public static void initialiseContainer(ContainerConfig containerConfig) {
|
||||
lock.lock();
|
||||
@@ -48,8 +49,11 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using properties to configure the database.
|
||||
* Create using configuration loaded from properties for the given database name.
|
||||
*
|
||||
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Database create(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -60,18 +64,9 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig object to configure the database.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
* config.setName("db");
|
||||
* config.loadProperties();
|
||||
*
|
||||
* Database database = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
* @deprecated migrate to {@link DatabaseBuilder#build()}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public static Database create(DatabaseBuilder builder) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -97,7 +92,8 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
|
||||
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
|
||||
* context class loader.
|
||||
*/
|
||||
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
|
||||
lock.lock();
|
||||
|
||||
@@ -92,6 +92,7 @@ final class DbContext {
|
||||
/**
|
||||
* Read, create and put of Databases.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private Database getWithCreate(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -1073,6 +1073,14 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> like(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>likeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Like - property like value where the value contains the
|
||||
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
|
||||
@@ -1080,17 +1088,41 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> ilike(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>ilikeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Starts With - property like value%.
|
||||
*/
|
||||
ExpressionList<T> startsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>startsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Starts With - property like value%. Typically uses a
|
||||
* lower() function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> istartsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>istartsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Ends With - property like %value.
|
||||
*/
|
||||
@@ -1107,12 +1139,28 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> contains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>containsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Contains - property like %value%. Typically uses a lower()
|
||||
* function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> icontains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>icontainsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* In expression using pairs of value objects.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Query-scoped immutable bean cache.
|
||||
*
|
||||
* <p>Typical use is to attach an immutable cache to a query and let Ebean use it when
|
||||
* resolving assoc-one references.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
* .select("name,version")
|
||||
* .fetch("billingAddress", "line1,city")
|
||||
* .fetch("shippingAddress", "line1,city")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
* .loading(database, customerGroup)
|
||||
* .build();
|
||||
*
|
||||
* Order order = database.find(Order.class)
|
||||
* .setId(id)
|
||||
* .setUnmodifiable(true)
|
||||
* .using(customerCache)
|
||||
* .findOne();
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> The bean type.
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableBeanCache<T> {
|
||||
|
||||
/**
|
||||
* Return the bean type this cache provides values for.
|
||||
*/
|
||||
Class<T> type();
|
||||
|
||||
/**
|
||||
* Return immutable cached beans by id (loading and populating misses as needed).
|
||||
*/
|
||||
Map<Object, T> getAll(Set<Object> ids);
|
||||
|
||||
/**
|
||||
* Return a cached bean for the given id if it is already present.
|
||||
* <p>
|
||||
* This does not trigger loading or record a miss.
|
||||
*/
|
||||
default @Nullable T getIfPresent(Object id) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.service.SpiImmutableCacheFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* Utility factory methods for {@link ImmutableBeanCache}.
|
||||
*
|
||||
* <p>Use {@link #builder(Class)} when you want explicit cache policy controls (for example
|
||||
* max size or TTL). Use {@link #loading(Class, Database, FetchGroup)} as a shorthand for
|
||||
* query-loader-backed memoization.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public final class ImmutableBeanCaches {
|
||||
|
||||
private ImmutableBeanCaches() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for immutable bean caches.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, FetchGroup.of(MyRef.class, "version"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableCacheBuilder<T> builder(Class<T> type) {
|
||||
SpiImmutableCacheFactory factory = XBootstrapService.immutableCacheFactory();
|
||||
if (factory != null) {
|
||||
return factory.builder(type);
|
||||
}
|
||||
return new LoadingBuilder<>(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a loader-backed immutable bean cache that memoizes both hits and misses.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(MyRef.class, ids ->
|
||||
* database.find(MyRef.class)
|
||||
* .setUnmodifiable(true)
|
||||
* .where().idIn(ids)
|
||||
* .findMap()
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @param type The bean type.
|
||||
* @param loader Batch loader for unresolved ids.
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
return builder(type).loader(loader).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a query-loader-backed immutable bean cache.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(
|
||||
* MyRef.class,
|
||||
* database,
|
||||
* FetchGroup.of(MyRef.class, "version")
|
||||
* );
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
return builder(type).loading(db, fetchGroup).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a batch loader backed by an unmodifiable query using the given fetch group.
|
||||
*/
|
||||
public static <T> Function<Set<Object>, Map<Object, T>> queryLoader(Database db, Class<T> type, FetchGroup<T> fetchGroup) {
|
||||
return new QueryLoader<>(type, db, fetchGroup);
|
||||
}
|
||||
|
||||
private static final class QueryLoader<T> implements Function<Set<Object>, Map<Object, T>> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Database db;
|
||||
private final FetchGroup<T> fetchGroup;
|
||||
|
||||
QueryLoader(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
this.type = requireNonNull(type);
|
||||
this.db = requireNonNull(db);
|
||||
this.fetchGroup = requireNonNull(fetchGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> apply(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return db.find(type)
|
||||
.select(fetchGroup)
|
||||
.setUnmodifiable(true)
|
||||
.where().idIn(ids)
|
||||
.findMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingBuilder<T> implements ImmutableCacheBuilder<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private Function<Set<Object>, Map<Object, T>> loader;
|
||||
private int maxSize;
|
||||
private int maxIdleSeconds;
|
||||
private int maxSecondsToLive;
|
||||
|
||||
private LoadingBuilder(Class<T> type) {
|
||||
this.type = requireNonNull(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.loader = requireNonNull(loader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup) {
|
||||
this.loader = new QueryLoader<>(type, db, fetchGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds) {
|
||||
this.maxIdleSeconds = maxIdleSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive) {
|
||||
this.maxSecondsToLive = maxSecondsToLive;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableBeanCache<T> build() {
|
||||
if (loader == null) {
|
||||
throw new IllegalStateException("No loader defined. Call loader(...) or loading(...) before build().");
|
||||
}
|
||||
if (maxSize > 0 || maxIdleSeconds > 0 || maxSecondsToLive > 0) {
|
||||
throw new IllegalStateException("Cache policy options require SpiImmutableCacheFactory (ebean-core).");
|
||||
}
|
||||
return new LoadingCache<>(type, loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingCache<T> implements ImmutableBeanCache<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Function<Set<Object>, Map<Object, T>> loader;
|
||||
private final ConcurrentHashMap<Object, T> cache = new ConcurrentHashMap<>();
|
||||
private final Set<Object> misses = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private LoadingCache(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.type = requireNonNull(type);
|
||||
this.loader = requireNonNull(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T getIfPresent(Object id) {
|
||||
return cache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> getAll(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Set<Object> loadIds = null;
|
||||
for (Object id : ids) {
|
||||
if (!cache.containsKey(id) && !misses.contains(id)) {
|
||||
if (loadIds == null) {
|
||||
loadIds = new LinkedHashSet<>();
|
||||
}
|
||||
loadIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadIds != null && !loadIds.isEmpty()) {
|
||||
Map<Object, T> loaded = loader.apply(loadIds);
|
||||
if (loaded == null) {
|
||||
loaded = Collections.emptyMap();
|
||||
}
|
||||
for (Map.Entry<Object, T> entry : loaded.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
cache.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
for (Object id : loadIds) {
|
||||
if (!cache.containsKey(id)) {
|
||||
misses.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Object, T> result = new LinkedHashMap<>();
|
||||
for (Object id : ids) {
|
||||
T bean = cache.get(id);
|
||||
if (bean != null) {
|
||||
result.put(id, bean);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Builder for creating {@link ImmutableBeanCache} instances.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .fetch("names", "locale,text")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableCacheBuilder<T> {
|
||||
|
||||
/**
|
||||
* Set the batch loader used for unresolved ids.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader);
|
||||
|
||||
/**
|
||||
* Configure a query-based loader using the given database and fetch group.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup);
|
||||
|
||||
/**
|
||||
* Configure max cache size (0 means unbounded).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSize(int maxSize);
|
||||
|
||||
/**
|
||||
* Configure max idle time in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds);
|
||||
|
||||
/**
|
||||
* Configure max time-to-live in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive);
|
||||
|
||||
/**
|
||||
* Build the immutable bean cache.
|
||||
*/
|
||||
ImmutableBeanCache<T> build();
|
||||
}
|
||||
@@ -44,6 +44,16 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF alsoIf(BooleanSupplier predicate, Consumer<SELF> apply);
|
||||
|
||||
/**
|
||||
* Apply changes to the query when the supplied value is non-null.
|
||||
* <p>
|
||||
* Typically, the changes are extra predicates etc.
|
||||
*
|
||||
* @param value The value which when non-null the changes are applied
|
||||
* @param apply The changes to apply to the query
|
||||
*/
|
||||
SELF alsoIfPresent(@Nullable Object value, Consumer<SELF> apply);
|
||||
|
||||
/**
|
||||
* Perform an 'As of' query using history tables to return the object graph
|
||||
* as of a time in the past.
|
||||
@@ -115,6 +125,11 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute this query using immutable bean cache values for matching bean types.
|
||||
*/
|
||||
SELF using(ImmutableBeanCache<?> beanCache);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
|
||||
@@ -367,6 +367,14 @@ public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
*/
|
||||
interface TypeQuery<T> {
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see SqlQuery#usingMaster(boolean)
|
||||
*/
|
||||
TypeQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
|
||||
@@ -205,4 +205,20 @@ public interface UpdateQuery<T> {
|
||||
*/
|
||||
int update();
|
||||
|
||||
/**
|
||||
* Return the timeout used to execute this statement.
|
||||
*/
|
||||
int getTimeout();
|
||||
|
||||
/**
|
||||
* Set a timeout on this query.
|
||||
* <p>
|
||||
* This will typically result in a call to setQueryTimeout() on a
|
||||
* preparedStatement. If the timeout occurs an exception will be thrown - this
|
||||
* will be a SQLException wrapped up in a PersistenceException.
|
||||
* </p>
|
||||
*
|
||||
* @param secs the query timeout limit in seconds. Zero means there is no limit.
|
||||
*/
|
||||
UpdateQuery<T> setTimeout(int secs);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public final class XBootstrapService {
|
||||
private static final SpiRawSqlService rawSqlService;
|
||||
private static final SpiProfileLocationFactory profileLocationFactory;
|
||||
private static final SpiFetchGroupService fetchGroupService;
|
||||
private static final SpiImmutableCacheFactory immutableCacheFactory;
|
||||
private static final MetricFactory metricFactory;
|
||||
private static final SpiJsonService jsonService;
|
||||
static {
|
||||
@@ -21,6 +22,7 @@ public final class XBootstrapService {
|
||||
SpiRawSqlService _raw = null;
|
||||
SpiProfileLocationFactory _profile = null;
|
||||
SpiFetchGroupService _fetch = null;
|
||||
SpiImmutableCacheFactory _immutable = null;
|
||||
MetricFactory _metric = null;
|
||||
SpiJsonService _json = null;
|
||||
for (BootstrapService extension : ServiceLoader.load(BootstrapService.class)) {
|
||||
@@ -32,6 +34,8 @@ public final class XBootstrapService {
|
||||
_profile = (SpiProfileLocationFactory)extension;
|
||||
} else if (extension instanceof SpiFetchGroupService) {
|
||||
_fetch = (SpiFetchGroupService)extension;
|
||||
} else if (extension instanceof SpiImmutableCacheFactory) {
|
||||
_immutable = (SpiImmutableCacheFactory) extension;
|
||||
} else if (extension instanceof MetricFactory) {
|
||||
_metric = (MetricFactory)extension;
|
||||
} else if (extension instanceof SpiJsonService) {
|
||||
@@ -42,6 +46,7 @@ public final class XBootstrapService {
|
||||
rawSqlService = _raw;
|
||||
profileLocationFactory = _profile;
|
||||
fetchGroupService = _fetch;
|
||||
immutableCacheFactory = _immutable;
|
||||
metricFactory = _metric;
|
||||
jsonService = _json;
|
||||
}
|
||||
@@ -72,6 +77,10 @@ public final class XBootstrapService {
|
||||
return profileLocationFactory;
|
||||
}
|
||||
|
||||
static SpiImmutableCacheFactory immutableCacheFactory() {
|
||||
return immutableCacheFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the FetchGroup with the given select clause.
|
||||
*/
|
||||
|
||||
@@ -31,38 +31,15 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The configuration used for creating a Database.
|
||||
* <p>
|
||||
* Used to programmatically construct an Database and optionally register it
|
||||
* with the DB singleton.
|
||||
* <p>
|
||||
* If you just use DB thout this programmatic configuration Ebean will read
|
||||
* the application.properties file and take the configuration from there. This usually
|
||||
* includes searching the class path and automatically registering any entity
|
||||
* classes and listeners etc.
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
*
|
||||
* // read the ebean.properties and load
|
||||
* // those settings into this DatabaseConfig object
|
||||
* config.loadFromProperties();
|
||||
*
|
||||
* // explicitly register the entity beans to avoid classpath scanning
|
||||
* config.addClass(Customer.class);
|
||||
* config.addClass(User.class);
|
||||
*
|
||||
* Database db = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
* Deprecated migrate to {@link Database#builder()} rather than constructing {@code DatabaseConfig} directly.
|
||||
*
|
||||
* <p>
|
||||
* Note that DatabaseConfigProvider provides a standard Java ServiceLoader mechanism that can
|
||||
* be used to apply configuration to the DatabaseConfig.
|
||||
* Note that {@link DatabaseConfigProvider} provides a standard Java ServiceLoader mechanism that can
|
||||
* be used to apply configuration to the {@link DatabaseBuilder}.
|
||||
*
|
||||
* @author emcgreal
|
||||
* @author rbygrave
|
||||
* @see DatabaseFactory
|
||||
* @see Database#builder()
|
||||
*/
|
||||
public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
|
||||
@@ -539,8 +516,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
private SlowQueryListener slowQueryListener;
|
||||
|
||||
private ProfilingConfig profilingConfig = new ProfilingConfig();
|
||||
|
||||
/**
|
||||
* The mappingLocations for searching xml mapping.
|
||||
*/
|
||||
@@ -560,12 +535,14 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
private Function<String, String> metricNaming = MetricNamingMatch.INSTANCE;
|
||||
|
||||
/**
|
||||
* Construct a Database Configuration for programmatically creating an Database.
|
||||
* @deprecated migrate to {@link Database#builder()} and configure the returned {@link DatabaseBuilder}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public DatabaseConfig() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public Database build() {
|
||||
return DatabaseFactory.create(this);
|
||||
}
|
||||
@@ -1028,17 +1005,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfilingConfig getProfilingConfig() {
|
||||
return profilingConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setProfilingConfig(ProfilingConfig profilingConfig) {
|
||||
this.profilingConfig = profilingConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDbSchema() {
|
||||
return dbSchema;
|
||||
@@ -2137,7 +2103,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
protected void loadSettings(PropertiesWrapper p) {
|
||||
dbSchema = p.get("dbSchema", dbSchema);
|
||||
profilingConfig.loadSettings(p, name);
|
||||
platformConfig.loadSettings(p);
|
||||
if (platformConfig.isAllQuotedIdentifiers()) {
|
||||
adjustNamingConventionForAllQuoted();
|
||||
|
||||
@@ -3,14 +3,14 @@ package io.ebean.config;
|
||||
import io.ebean.DatabaseBuilder;
|
||||
|
||||
/**
|
||||
* Provides a ServiceLoader based mechanism to configure a DatabaseConfig.
|
||||
* Provides a ServiceLoader based mechanism to configure a {@link DatabaseBuilder}.
|
||||
* <p>
|
||||
* Provide an implementation and register it via the standard Java ServiceLoader mechanism
|
||||
* via a file at <code>META-INF/services/io.ebean.config.DatabaseConfigProvider</code>.
|
||||
* </p>
|
||||
* <p>
|
||||
* If you are using a DI container like Spring or Guice you are unlikely to use this but instead use a
|
||||
* spring specific configuration. When we are not using a DI container we may use this mechanism to
|
||||
* spring specific configuration. When we are not using a DI container we may use this mechanism to
|
||||
* explicitly register the entity beans and avoid classpath scanning.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
@@ -18,7 +18,7 @@ import io.ebean.DatabaseBuilder;
|
||||
* public class EbeanConfigProvider implements DatabaseConfigProvider {
|
||||
*
|
||||
* @Override
|
||||
* public void apply(DatabaseConfig config) {
|
||||
* public void apply(DatabaseBuilder config) {
|
||||
*
|
||||
* // register the entity bean classes explicitly
|
||||
* config.addClass(Customer.class);
|
||||
@@ -32,10 +32,9 @@ import io.ebean.DatabaseBuilder;
|
||||
public interface DatabaseConfigProvider {
|
||||
|
||||
/**
|
||||
* Apply the configuration to the DatabaseConfig.
|
||||
* Apply the configuration to the {@link DatabaseBuilder}.
|
||||
* <p>
|
||||
* Typically we explicitly register entity bean classes and thus avoid classpath scanning.
|
||||
* </p>
|
||||
*/
|
||||
void apply(DatabaseBuilder config);
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package io.ebean.config;
|
||||
|
||||
/**
|
||||
* Configuration for transaction profiling.
|
||||
*/
|
||||
public class ProfilingConfig {
|
||||
|
||||
/**
|
||||
* When true transaction profiling is enabled.
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* Set true for verbose mode.
|
||||
*/
|
||||
private boolean verbose;
|
||||
|
||||
/**
|
||||
* The minimum transaction execution time to be included in profiling.
|
||||
*/
|
||||
private long minimumMicros;
|
||||
|
||||
/**
|
||||
* A specific set of profileIds to include in profiling.
|
||||
*/
|
||||
private int[] includeProfileIds = {};
|
||||
|
||||
/**
|
||||
* The number of profiles to write per file.
|
||||
*/
|
||||
private long profilesPerFile = 1000;
|
||||
|
||||
private String directory = "profiling";
|
||||
|
||||
/**
|
||||
* Return true if transaction profiling is enabled.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to enable transaction profiling.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if verbose mode is used.
|
||||
*/
|
||||
public boolean isVerbose() {
|
||||
return verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to use verbose mode.
|
||||
*/
|
||||
public void setVerbose(boolean verbose) {
|
||||
this.verbose = verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum transaction execution to be included in profiling.
|
||||
*/
|
||||
public long getMinimumMicros() {
|
||||
return minimumMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum transaction execution to be included in profiling.
|
||||
*/
|
||||
public void setMinimumMicros(long minimumMicros) {
|
||||
this.minimumMicros = minimumMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the specific set of profileIds to include in profiling.
|
||||
* When not set all transactions with profileIds are included.
|
||||
*/
|
||||
public int[] getIncludeProfileIds() {
|
||||
return includeProfileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific set of profileIds to include in profiling.
|
||||
* When not set all transactions with profileIds are included.
|
||||
*/
|
||||
public void setIncludeProfileIds(int[] includeProfileIds) {
|
||||
this.includeProfileIds = includeProfileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of profiles to write to a single file.
|
||||
*/
|
||||
public long getProfilesPerFile() {
|
||||
return profilesPerFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of profiles to write to a single file.
|
||||
*/
|
||||
public void setProfilesPerFile(long profilesPerFile) {
|
||||
this.profilesPerFile = profilesPerFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the directory profiling files are put into.
|
||||
*/
|
||||
public String getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the directory profiling files are put into.
|
||||
*/
|
||||
public void setDirectory(String directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load setting from properties.
|
||||
*/
|
||||
public void loadSettings(PropertiesWrapper p, String name) {
|
||||
|
||||
enabled = p.getBoolean("profiling", enabled);
|
||||
verbose = p.getBoolean("profiling.verbose", verbose);
|
||||
|
||||
directory = p.get("profiling.directory", directory);
|
||||
profilesPerFile = p.getLong("profiling.profilesPerFile", profilesPerFile);
|
||||
minimumMicros = p.getLong("profiling.minimumMicros", minimumMicros);
|
||||
|
||||
String includeIds = p.get("profiling.includeProfileIds");
|
||||
if (includeIds != null) {
|
||||
includeProfileIds = parseIds(includeIds);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] parseIds(String includeIds) {
|
||||
|
||||
String[] ids = includeIds.split(",");
|
||||
int[] vals = new int[ids.length];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
vals[i] = Integer.parseInt(ids[i]);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,23 @@ public interface MetaInfoManager {
|
||||
/**
|
||||
* Return the metrics for the database instance.
|
||||
* <p>
|
||||
* This will reset the metrics (reset counters back to zero etc) and
|
||||
* will only return the non-empty metrics.
|
||||
* This is equivalent to {@link #collectMetrics(boolean)} with reset set to true.
|
||||
* It will reset the metrics (reset counters back to zero etc) and will only return
|
||||
* the non-empty metrics.
|
||||
* </p>
|
||||
*/
|
||||
ServerMetrics collectMetrics();
|
||||
|
||||
/**
|
||||
* Return the metrics for the database instance using the given reset behavior.
|
||||
* <p>
|
||||
* When reset is false, count and total values remain cumulative between collections.
|
||||
* </p>
|
||||
*/
|
||||
default ServerMetrics collectMetrics(boolean reset) {
|
||||
return collectMetrics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the metrics resetting and collecting/reporting as desired.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.ebean.service;
|
||||
|
||||
import io.ebean.ImmutableCacheBuilder;
|
||||
|
||||
/**
|
||||
* Factory for creating immutable cache builders.
|
||||
*/
|
||||
public interface SpiImmutableCacheFactory extends BootstrapService {
|
||||
|
||||
/**
|
||||
* Return a new builder for the given immutable bean type.
|
||||
*/
|
||||
<T> ImmutableCacheBuilder<T> builder(Class<T> type);
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class ImmutableBeanCachesTest {
|
||||
|
||||
@Test
|
||||
void loading_cache_memoizes_hits_and_misses() {
|
||||
|
||||
AtomicInteger loadCount = new AtomicInteger();
|
||||
AtomicReference<Set<Object>> lastLoadIds = new AtomicReference<>();
|
||||
|
||||
ImmutableBeanCache<String> cache = ImmutableBeanCaches.loading(String.class, ids -> {
|
||||
loadCount.incrementAndGet();
|
||||
lastLoadIds.set(Set.copyOf(ids));
|
||||
Map<Object, String> map = new LinkedHashMap<>();
|
||||
for (Object id : ids) {
|
||||
if ("A".equals(id) || "C".equals(id)) {
|
||||
map.put(id, "val-" + id);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
Map<Object, String> first = cache.getAll(Set.of("A", "B", "C"));
|
||||
assertThat(first).containsEntry("A", "val-A").containsEntry("C", "val-C");
|
||||
assertThat(first).doesNotContainKey("B");
|
||||
assertThat(loadCount.get()).isEqualTo(1);
|
||||
assertThat(lastLoadIds.get()).containsExactlyInAnyOrder("A", "B", "C");
|
||||
|
||||
Map<Object, String> second = cache.getAll(Set.of("A", "B", "C"));
|
||||
assertThat(second).isEqualTo(first);
|
||||
assertThat(loadCount.get()).isEqualTo(1);
|
||||
|
||||
Map<Object, String> third = cache.getAll(Set.of("B", "D"));
|
||||
assertThat(third).isEmpty();
|
||||
assertThat(loadCount.get()).isEqualTo(2);
|
||||
assertThat(lastLoadIds.get()).containsExactly("D");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loading_withDatabaseAndFetchGroup_usesQueryLoaderFindMap() {
|
||||
|
||||
AtomicReference<Set<Object>> capturedIds = new AtomicReference<>();
|
||||
AtomicBoolean unmodifiable = new AtomicBoolean();
|
||||
|
||||
Map<Object, String> loaded = new LinkedHashMap<>();
|
||||
loaded.put("A", "val-A");
|
||||
loaded.put("B", "val-B");
|
||||
|
||||
Object[] exprProxyRef = new Object[1];
|
||||
|
||||
Object expressionListProxy = Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{ExpressionList.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("idIn".equals(name)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Object> values = Set.copyOf((java.util.Collection<Object>) args[0]);
|
||||
capturedIds.set(values);
|
||||
return proxy;
|
||||
}
|
||||
if ("findMap".equals(name)) {
|
||||
return loaded;
|
||||
}
|
||||
if ("hashCode".equals(name)) return System.identityHashCode(proxy);
|
||||
if ("equals".equals(name)) return proxy == args[0];
|
||||
if ("toString".equals(name)) return "expressionListProxy";
|
||||
throw new UnsupportedOperationException(name);
|
||||
});
|
||||
exprProxyRef[0] = expressionListProxy;
|
||||
|
||||
Object queryProxy = Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{Query.class},
|
||||
(proxy, method, args) -> {
|
||||
String name = method.getName();
|
||||
if ("select".equals(name) || "setUnmodifiable".equals(name)) {
|
||||
if ("setUnmodifiable".equals(name)) {
|
||||
unmodifiable.set((Boolean) args[0]);
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
if ("where".equals(name)) {
|
||||
return exprProxyRef[0];
|
||||
}
|
||||
if ("hashCode".equals(name)) return System.identityHashCode(proxy);
|
||||
if ("equals".equals(name)) return proxy == args[0];
|
||||
if ("toString".equals(name)) return "queryProxy";
|
||||
throw new UnsupportedOperationException(name);
|
||||
});
|
||||
|
||||
Database db = (Database) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{Database.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("find".equals(method.getName())) {
|
||||
assertThat(args[0]).isEqualTo(String.class);
|
||||
return queryProxy;
|
||||
}
|
||||
if ("hashCode".equals(method.getName())) return System.identityHashCode(proxy);
|
||||
if ("equals".equals(method.getName())) return proxy == args[0];
|
||||
if ("toString".equals(method.getName())) return "dbProxy";
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
FetchGroup<String> fetchGroup = (FetchGroup<String>) Proxy.newProxyInstance(
|
||||
getClass().getClassLoader(),
|
||||
new Class<?>[]{FetchGroup.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("hashCode".equals(method.getName())) return System.identityHashCode(proxy);
|
||||
if ("equals".equals(method.getName())) return proxy == args[0];
|
||||
if ("toString".equals(method.getName())) return "fetchGroupProxy";
|
||||
throw new UnsupportedOperationException(method.getName());
|
||||
});
|
||||
|
||||
ImmutableBeanCache<String> cache = ImmutableBeanCaches.loading(String.class, db, fetchGroup);
|
||||
Map<Object, String> result = cache.getAll(Set.of("A", "B"));
|
||||
|
||||
assertThat(result).isEqualTo(loaded);
|
||||
assertThat(unmodifiable).isTrue();
|
||||
assertThat(capturedIds.get()).containsExactlyInAnyOrder("A", "B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void builder_loader_buildsCache() {
|
||||
|
||||
AtomicInteger loadCount = new AtomicInteger();
|
||||
|
||||
ImmutableBeanCache<String> cache = ImmutableBeanCaches.builder(String.class)
|
||||
.loader(ids -> {
|
||||
loadCount.incrementAndGet();
|
||||
Map<Object, String> map = new LinkedHashMap<>();
|
||||
for (Object id : ids) {
|
||||
if ("A".equals(id)) {
|
||||
map.put(id, "val-A");
|
||||
}
|
||||
}
|
||||
return map;
|
||||
})
|
||||
.build();
|
||||
|
||||
assertThat(cache.getAll(Set.of("A", "B"))).containsEntry("A", "val-A").doesNotContainKey("B");
|
||||
assertThat(cache.getAll(Set.of("A", "B"))).containsEntry("A", "val-A").doesNotContainKey("B");
|
||||
assertThat(loadCount.get()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void builder_withPolicy_requiresFactoryWhenUnavailable() {
|
||||
|
||||
if (XBootstrapService.immutableCacheFactory() != null) {
|
||||
ImmutableBeanCache<String> cache = ImmutableBeanCaches.builder(String.class)
|
||||
.maxSize(10)
|
||||
.loader(ids -> Collections.emptyMap())
|
||||
.build();
|
||||
assertThat(cache.getAll(Set.of("A"))).isEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
assertThatThrownBy(() -> ImmutableBeanCaches.builder(String.class)
|
||||
.maxSize(10)
|
||||
.loader(ids -> Collections.emptyMap())
|
||||
.build()
|
||||
).isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("SpiImmutableCacheFactory");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MetaInfoManagerTest {
|
||||
|
||||
@Test
|
||||
void collectMetricsBooleanDefaultsToCollectMetrics() {
|
||||
ServerMetrics metrics = new BasicMetricVisitor();
|
||||
MetaInfoManager manager = new MetaInfoManager() {
|
||||
@Override
|
||||
public ServerMetrics collectMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitMetrics(MetricVisitor visitor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicMetricVisitor visitBasic() {
|
||||
return new BasicMetricVisitor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAllMetrics() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaQueryPlan> queryPlanInit(QueryPlanInit initRequest) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaQueryPlan> queryPlanCollectNow(QueryPlanRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(manager.collectMetrics(false)).isSameAs(metrics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-bench</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>ebean-bench</name>
|
||||
<description>JMH benchmarks for ebean internals</description>
|
||||
|
||||
<properties>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
<jmh.version>1.37</jmh.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<finalName>benchmarks</finalName>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,110 @@
|
||||
package io.ebean.bench;
|
||||
|
||||
import io.ebean.FetchGroup;
|
||||
import io.ebean.service.SpiFetchGroupQuery;
|
||||
import io.ebeaninternal.api.SpiExpressionList;
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
|
||||
import io.ebeaninternal.server.querydefn.SpiFetchGroup;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
@Warmup(iterations = 5, time = 1)
|
||||
@Measurement(iterations = 10, time = 1)
|
||||
@Fork(2)
|
||||
@State(Scope.Thread)
|
||||
public class FetchGroupSelectApplyBenchmark {
|
||||
|
||||
private FetchGroup<DummyBean> fetchGroup;
|
||||
private SpiFetchGroup<DummyBean> spiFetchGroup;
|
||||
private SpiFetchGroupQuery<DummyBean> reusableQueryNoFilter;
|
||||
private OrmQueryDetail existingDetailNoFilter;
|
||||
private OrmQueryDetail existingDetailWithFilter;
|
||||
private SpiExpressionList<?> postApplyFilter;
|
||||
|
||||
@Setup(Level.Trial)
|
||||
public void setup() {
|
||||
fetchGroup = FetchGroup.of(DummyBean.class)
|
||||
.select("id,name,status,version")
|
||||
.fetch("billingAddress", "line1,city,country")
|
||||
.fetch("shippingAddress", "line1,city,country")
|
||||
.fetch("contacts", "firstName,lastName,email")
|
||||
.fetch("contacts.phoneNumbers", "number,type")
|
||||
.build();
|
||||
spiFetchGroup = (SpiFetchGroup<DummyBean>) fetchGroup;
|
||||
|
||||
reusableQueryNoFilter = FetchGroup.queryFor(DummyBean.class);
|
||||
|
||||
existingDetailNoFilter = new OrmQueryDetail();
|
||||
|
||||
existingDetailWithFilter = new OrmQueryDetail();
|
||||
existingDetailWithFilter.fetch("contacts", "firstName,lastName", null);
|
||||
existingDetailWithFilter.getChunk("contacts", false).setFilterMany(dummyFilterMany());
|
||||
postApplyFilter = dummyFilterMany();
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public SpiFetchGroupQuery<DummyBean> selectOnReusableQuery_noFilter() {
|
||||
reusableQueryNoFilter.select(fetchGroup);
|
||||
return reusableQueryNoFilter;
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public OrmQueryDetail applyToExistingDetail_noFilter() {
|
||||
return spiFetchGroup.detail(existingDetailNoFilter);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public OrmQueryDetail applyToExistingDetail_withFilter() {
|
||||
return spiFetchGroup.detail(existingDetailWithFilter);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public OrmQueryDetail applyToNewEmptyDetail_thenAddFilterMany() {
|
||||
OrmQueryDetail detail = spiFetchGroup.detail(new OrmQueryDetail());
|
||||
detail.getChunk("contacts", true).setFilterMany(postApplyFilter);
|
||||
return detail;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final class DummyBean {
|
||||
}
|
||||
|
||||
private static SpiExpressionList<?> dummyFilterMany() {
|
||||
return (SpiExpressionList<?>) Proxy.newProxyInstance(
|
||||
FetchGroupSelectApplyBenchmark.class.getClassLoader(),
|
||||
new Class<?>[]{SpiExpressionList.class},
|
||||
(proxy, method, args) -> {
|
||||
Class<?> returnType = method.getReturnType();
|
||||
if (returnType == boolean.class) {
|
||||
return false;
|
||||
}
|
||||
if (returnType == int.class) {
|
||||
return 0;
|
||||
}
|
||||
if (returnType == long.class) {
|
||||
return 0L;
|
||||
}
|
||||
if (returnType == float.class) {
|
||||
return 0f;
|
||||
}
|
||||
if (returnType == double.class) {
|
||||
return 0d;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package io.ebean.bench;
|
||||
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryDetail;
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Fork;
|
||||
import org.openjdk.jmh.annotations.Measurement;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.annotations.Warmup;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
@OutputTimeUnit(TimeUnit.MILLISECONDS)
|
||||
@Warmup(iterations = 5, time = 1)
|
||||
@Measurement(iterations = 10, time = 1)
|
||||
@Fork(2)
|
||||
@State(Scope.Thread)
|
||||
public class OrmQueryDetailCopyBenchmark {
|
||||
|
||||
private OrmQueryDetail source;
|
||||
private OrmQueryDetail existing;
|
||||
private StringBuilder hashBuilder;
|
||||
|
||||
@Setup
|
||||
public void setup() {
|
||||
source = new OrmQueryDetail();
|
||||
source.select("id,name,status,version");
|
||||
source.fetch("billingAddress", "line1,city,country", null);
|
||||
source.fetch("shippingAddress", "line1,city,country", null);
|
||||
source.fetch("contacts", "firstName,lastName,email", null);
|
||||
source.fetch("contacts.phoneNumbers", "number,type", null);
|
||||
|
||||
existing = new OrmQueryDetail();
|
||||
existing.fetch("contacts", "firstName,lastName", null);
|
||||
hashBuilder = new StringBuilder(256);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public OrmQueryDetail copyNull() {
|
||||
return source.copy(null);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public OrmQueryDetail copyExisting() {
|
||||
return source.copy(existing);
|
||||
}
|
||||
|
||||
@Benchmark
|
||||
public int queryPlanHash() {
|
||||
hashBuilder.setLength(0);
|
||||
source.queryPlanHash(hashBuilder);
|
||||
return hashBuilder.length();
|
||||
}
|
||||
}
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean bom</name>
|
||||
@@ -89,25 +89,25 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -125,13 +125,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<name>ebean-core-json</name>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core</artifactId>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -165,21 +165,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.4.0</version>
|
||||
<version>16.7.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package io.ebeaninternal.api;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
import io.ebean.bean.ObjectGraphNode;
|
||||
import io.ebean.bean.PersistenceContext;
|
||||
import io.ebeaninternal.server.core.OrmQueryRequest;
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
|
||||
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
|
||||
|
||||
@@ -51,6 +55,17 @@ public interface LoadContext {
|
||||
*/
|
||||
void register(String path, BeanPropertyAssocMany<?> many, BeanCollection<?> bc);
|
||||
|
||||
/**
|
||||
* Register a bean as a candidate for immutable cache population.
|
||||
*/
|
||||
void registerForImmutable(EntityBeanIntercept ebi);
|
||||
|
||||
/**
|
||||
* Return an immutable cached bean for the given descriptor and id if already present.
|
||||
*/
|
||||
@Nullable
|
||||
EntityBean immutableBeanHit(BeanDescriptor<?> descriptor, Object id);
|
||||
|
||||
/**
|
||||
* Use soft-references for streaming queries, so unreachable entries can be garbage collected.
|
||||
*/
|
||||
@@ -60,4 +75,9 @@ public interface LoadContext {
|
||||
* Return true to include a many as a secondary query for unmodified.
|
||||
*/
|
||||
boolean includeSecondary(BeanPropertyAssocMany<?> many);
|
||||
|
||||
/**
|
||||
* Populate buffered entity references from immutable bean caches.
|
||||
*/
|
||||
void populateFromImmutableCache();
|
||||
}
|
||||
|
||||
@@ -156,6 +156,12 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
|
||||
*/
|
||||
void remoteTransactionEvent(RemoteTransactionEvent event);
|
||||
|
||||
/**
|
||||
* Register immutable bean cache with this server for transactional invalidation.
|
||||
* Typically called when the cache is created.
|
||||
*/
|
||||
void registerImmutableCache(ImmutableBeanCache<?> beanCache);
|
||||
|
||||
/**
|
||||
* Compile a query.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebeaninternal.api;
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebeaninternal.server.transaction.ProfileStream;
|
||||
import io.ebeaninternal.server.transaction.TransactionProfile;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Handle the logging or processing of transaction profiling information that is collected.
|
||||
@@ -10,22 +11,14 @@ import io.ebeaninternal.server.transaction.TransactionProfile;
|
||||
public interface SpiProfileHandler {
|
||||
|
||||
/**
|
||||
* Process the collected transaction profiling information.
|
||||
* Create a profiling stream for this transaction, or return null to not profile this transaction.
|
||||
* <p>
|
||||
* Note that profileId and totalMicros are part of the profilingData but passed separately as the handler
|
||||
* may filter what it processed based on this information (ignore short transactions, only process specific
|
||||
* profileId transactions etc).
|
||||
* The location is null for implicit read-only transactions (queries without an explicit transaction).
|
||||
* Handlers should return null when they choose not to profile a given transaction.
|
||||
* </p>
|
||||
*
|
||||
* @param transactionProfile The transaction profile that has just been collected
|
||||
* @param location The profile location, or null for implicit transactions
|
||||
* @param label The transaction label
|
||||
*/
|
||||
void collectTransactionProfile(TransactionProfile transactionProfile);
|
||||
|
||||
/**
|
||||
* Create a profiling stream if we are profiling this transaction.
|
||||
* Return null if we are not profiling this transaction.
|
||||
*
|
||||
* @param location The profile location
|
||||
*/
|
||||
ProfileStream createProfileStream(ProfileLocation location);
|
||||
@Nullable ProfileStream createProfileStream(@Nullable ProfileLocation location, @Nullable String label);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.CacheMode;
|
||||
import io.ebean.CountDistinctOrder;
|
||||
import io.ebean.ExpressionList;
|
||||
import io.ebean.ImmutableBeanCache;
|
||||
import io.ebean.OrderBy;
|
||||
import io.ebean.PersistenceContextScope;
|
||||
import io.ebean.ProfileLocation;
|
||||
@@ -27,6 +28,7 @@ import io.ebeaninternal.server.rawsql.SpiRawSql;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -759,6 +761,16 @@ public interface SpiQuery<T> extends Query<T>, SpiQueryFetch, TxnProfileEventCod
|
||||
*/
|
||||
boolean isBeanCacheGet();
|
||||
|
||||
/**
|
||||
* Register immutable bean cache to use for this query execution.
|
||||
*/
|
||||
void putImmutableBeanCache(ImmutableBeanCache<?> beanCache);
|
||||
|
||||
/**
|
||||
* Return immutable bean caches configured for this query by bean type.
|
||||
*/
|
||||
Map<Class<?>, ImmutableBeanCache<?>> immutableBeanCaches();
|
||||
|
||||
/**
|
||||
* Return true if the query should PUT against the bean cache.
|
||||
*/
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.ebeaninternal.server.cache;
|
||||
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
|
||||
final class CacheChangeImmutableClear implements CacheChange {
|
||||
|
||||
private final BeanDescriptor<?> descriptor;
|
||||
|
||||
CacheChangeImmutableClear(BeanDescriptor<?> descriptor) {
|
||||
this.descriptor = descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
descriptor.clearImmutableCaches();
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.ebeaninternal.server.cache;
|
||||
|
||||
import io.ebeaninternal.server.deploy.BeanDescriptor;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
final class CacheChangeImmutableRemove implements CacheChange {
|
||||
|
||||
private final BeanDescriptor<?> descriptor;
|
||||
private final Collection<Object> ids;
|
||||
|
||||
CacheChangeImmutableRemove(BeanDescriptor<?> descriptor, Collection<Object> ids) {
|
||||
this.descriptor = descriptor;
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {
|
||||
descriptor.removeImmutableCacheByIds(ids);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,32 @@ public final class CacheChangeSet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear immutable caches for this bean type.
|
||||
*/
|
||||
public void addImmutableClear(BeanDescriptor<?> descriptor) {
|
||||
entries.add(new CacheChangeImmutableClear(descriptor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single id from immutable caches for this bean type.
|
||||
*/
|
||||
public <T> void addImmutableRemove(BeanDescriptor<T> desc, Object id) {
|
||||
if (id != null) {
|
||||
addImmutableRemoveMany(desc, Collections.singleton(id));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove many ids from immutable caches for this bean type.
|
||||
*/
|
||||
public <T> void addImmutableRemoveMany(BeanDescriptor<T> desc, Collection<Object> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
entries.add(new CacheChangeImmutableRemove(desc, ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a bean entry.
|
||||
*/
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package io.ebeaninternal.server.cache;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.BackgroundExecutor;
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.FetchGroup;
|
||||
import io.ebean.ImmutableBeanCache;
|
||||
import io.ebean.ImmutableBeanCaches;
|
||||
import io.ebean.ImmutableCacheBuilder;
|
||||
import io.ebean.cache.ServerCache;
|
||||
import io.ebean.cache.ServerCacheConfig;
|
||||
import io.ebean.cache.ServerCacheOptions;
|
||||
import io.ebean.cache.ServerCacheType;
|
||||
import io.ebean.service.SpiImmutableCacheFactory;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* Core implementation of immutable cache builder factory.
|
||||
*/
|
||||
public final class DImmutableCacheFactory implements SpiImmutableCacheFactory {
|
||||
|
||||
private static final AtomicLong COUNTER = new AtomicLong();
|
||||
|
||||
@Override
|
||||
public <T> ImmutableCacheBuilder<T> builder(Class<T> type) {
|
||||
return new Builder<>(type);
|
||||
}
|
||||
|
||||
private static final class Builder<T> implements ImmutableCacheBuilder<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private Function<Set<Object>, Map<Object, T>> loader;
|
||||
private int maxSize;
|
||||
private int maxIdleSeconds;
|
||||
private int maxSecondsToLive;
|
||||
private Database database;
|
||||
|
||||
private Builder(Class<T> type) {
|
||||
this.type = requireNonNull(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.loader = requireNonNull(loader);
|
||||
this.database = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup) {
|
||||
this.database = requireNonNull(db);
|
||||
this.loader = ImmutableBeanCaches.queryLoader(db, type, fetchGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds) {
|
||||
this.maxIdleSeconds = maxIdleSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive) {
|
||||
this.maxSecondsToLive = maxSecondsToLive;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableBeanCache<T> build() {
|
||||
if (loader == null) {
|
||||
throw new IllegalStateException("No loader defined. Call loader(...) or loading(...) before build().");
|
||||
}
|
||||
ServerCacheOptions options = new ServerCacheOptions();
|
||||
options.setMaxSize(maxSize);
|
||||
options.setMaxIdleSecs(maxIdleSeconds);
|
||||
options.setMaxSecsToLive(maxSecondsToLive);
|
||||
|
||||
String cacheKey = "immutable." + type.getName() + "." + COUNTER.incrementAndGet();
|
||||
String shortName = "im." + type.getSimpleName() + "." + COUNTER.get();
|
||||
ServerCacheConfig config = new ServerCacheConfig(ServerCacheType.BEAN, cacheKey, shortName, options, null, null);
|
||||
DefaultServerCache serverCache = new DefaultServerCache(new DefaultServerCacheConfig(config));
|
||||
BackgroundExecutor executor = executor();
|
||||
if (executor != null) {
|
||||
serverCache.periodicTrim(executor);
|
||||
}
|
||||
ServerLoadingCache<T> immutableCache = new ServerLoadingCache<>(type, loader, serverCache);
|
||||
if (database instanceof SpiEbeanServer) {
|
||||
((SpiEbeanServer) database).registerImmutableCache(immutableCache);
|
||||
}
|
||||
return immutableCache;
|
||||
}
|
||||
|
||||
private BackgroundExecutor executor() {
|
||||
try {
|
||||
return database != null ? database.backgroundExecutor() : DB.backgroundExecutor();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ServerLoadingCache<T> implements ImmutableBeanCache<T>, ImmutableCacheInvalidator {
|
||||
|
||||
private static final Object MISS = new Object();
|
||||
|
||||
private final Class<T> type;
|
||||
private final Function<Set<Object>, Map<Object, T>> loader;
|
||||
private final ServerCache cache;
|
||||
|
||||
private ServerLoadingCache(Class<T> type, Function<Set<Object>, Map<Object, T>> loader, ServerCache cache) {
|
||||
this.type = type;
|
||||
this.loader = loader;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public @Nullable T getIfPresent(Object id) {
|
||||
Object value = cache.get(id);
|
||||
return value == null || value == MISS ? null : (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Object, T> getAll(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<Object, T> result = new LinkedHashMap<>();
|
||||
Set<Object> loadIds = null;
|
||||
for (Object id : ids) {
|
||||
Object value = cache.get(id);
|
||||
if (value == null) {
|
||||
if (loadIds == null) {
|
||||
loadIds = new LinkedHashSet<>();
|
||||
}
|
||||
loadIds.add(id);
|
||||
} else if (value != MISS) {
|
||||
result.put(id, (T) value);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadIds != null && !loadIds.isEmpty()) {
|
||||
Map<Object, T> loaded = loader.apply(loadIds);
|
||||
if (loaded == null) {
|
||||
loaded = Collections.emptyMap();
|
||||
}
|
||||
for (Map.Entry<Object, T> entry : loaded.entrySet()) {
|
||||
T value = entry.getValue();
|
||||
if (value != null) {
|
||||
cache.put(entry.getKey(), value);
|
||||
result.put(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
for (Object id : loadIds) {
|
||||
if (!loaded.containsKey(id) || loaded.get(id) == null) {
|
||||
cache.put(id, MISS);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAll(Collection<Object> ids) {
|
||||
cache.removeAll(new HashSet<>(ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.ebeaninternal.server.cache;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface ImmutableCacheInvalidator {
|
||||
|
||||
void clear();
|
||||
|
||||
void removeAll(Collection<Object> ids);
|
||||
}
|
||||
@@ -35,14 +35,17 @@ final class DefaultMetaInfoManager implements MetaInfoManager {
|
||||
|
||||
@Override
|
||||
public ServerMetrics collectMetrics() {
|
||||
return visitBasic();
|
||||
return collectMetrics(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerMetrics collectMetrics(boolean reset) {
|
||||
return collectBasic(reset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicMetricVisitor visitBasic() {
|
||||
BasicMetricVisitor basic = new BasicMetricVisitor(server.name(), naming);
|
||||
visitMetrics(basic);
|
||||
return basic;
|
||||
return collectBasic(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +53,12 @@ final class DefaultMetaInfoManager implements MetaInfoManager {
|
||||
server.visitMetrics(new ResetVisitor());
|
||||
}
|
||||
|
||||
private BasicMetricVisitor collectBasic(boolean reset) {
|
||||
BasicMetricVisitor basic = new BasicMetricVisitor(server.name(), naming, reset, true, true, true);
|
||||
visitMetrics(basic);
|
||||
return basic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor that resets the statistics but doesn't collect them.
|
||||
*/
|
||||
|
||||
@@ -205,6 +205,14 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
return disableL2Cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerImmutableCache(ImmutableBeanCache<?> beanCache) {
|
||||
BeanDescriptor<?> descriptor = descriptorManager.descriptor(beanCache.type());
|
||||
if (descriptor != null) {
|
||||
descriptor.registerImmutableCache(beanCache);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiLogManager log() {
|
||||
return logManager;
|
||||
|
||||
@@ -401,16 +401,11 @@ public final class InternalConfiguration {
|
||||
}
|
||||
|
||||
private SpiProfileHandler profileHandler() {
|
||||
|
||||
ProfilingConfig profilingConfig = config.getProfilingConfig();
|
||||
if (!profilingConfig.isEnabled()) {
|
||||
return new NoopProfileHandler();
|
||||
}
|
||||
SpiProfileHandler handler = service(SpiProfileHandler.class);
|
||||
if (handler == null) {
|
||||
handler = new DefaultProfileHandler(profilingConfig);
|
||||
if (handler != null) {
|
||||
return plugin(handler);
|
||||
}
|
||||
return plugin(handler);
|
||||
return new NoopProfileHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -122,6 +122,12 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
|
||||
}
|
||||
}
|
||||
|
||||
public void populateFromImmutableCache() {
|
||||
if (loadContext != null) {
|
||||
loadContext.populateFromImmutableCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For use with QueryIterator and secondary queries this returns the minimum
|
||||
* batch size that should be loaded before executing the secondary queries.
|
||||
|
||||
@@ -52,6 +52,30 @@ abstract class AssocOneHelp {
|
||||
return val;
|
||||
}
|
||||
|
||||
final Object contextGetOrImmutableHit(DbReadContext ctx, BeanDescriptor<?> desc, Object id) {
|
||||
PersistenceContext pc = ctx.persistenceContext();
|
||||
Object existing = desc.contextGet(pc, id);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
return ctx.immutableBeanHit(desc, id);
|
||||
}
|
||||
|
||||
final Object createRegisterRef(DbReadContext ctx, BeanDescriptor<?> desc, Object id) {
|
||||
PersistenceContext pc = ctx.persistenceContext();
|
||||
EntityBean ref = (EntityBean) desc.contextRef(pc, id, ctx.unmodifiable(), ctx.isDisableLazyLoading());
|
||||
registerReference(ctx, ref);
|
||||
return ref;
|
||||
}
|
||||
|
||||
protected void registerReference(DbReadContext ctx, EntityBean ref) {
|
||||
if (!ctx.unmodifiable() && !ctx.isDisableLazyLoading()) {
|
||||
ctx.register(path, ref._ebean_getIntercept());
|
||||
} else {
|
||||
ctx.registerForImmutable(ref._ebean_getIntercept());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and return the bean.
|
||||
*/
|
||||
@@ -61,16 +85,8 @@ abstract class AssocOneHelp {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
PersistenceContext pc = ctx.persistenceContext();
|
||||
Object existing = target.contextGet(pc, id);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
Object ref = target.contextRef(pc, id, ctx.unmodifiable(), ctx.isDisableLazyLoading());
|
||||
if (!ctx.unmodifiable() && !ctx.isDisableLazyLoading()) {
|
||||
ctx.register(path, ((EntityBean) ref)._ebean_getIntercept());
|
||||
}
|
||||
return ref;
|
||||
Object existing = contextGetOrImmutableHit(ctx, target, id);
|
||||
return existing != null ? existing : createRegisterRef(ctx, target, id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-11
@@ -1,7 +1,6 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.PersistenceContext;
|
||||
import io.ebeaninternal.server.query.SqlJoinType;
|
||||
import io.ebeaninternal.server.query.SqlTreeJoin;
|
||||
|
||||
@@ -46,18 +45,17 @@ final class AssocOneHelpRefInherit extends AssocOneHelp {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
// check transaction context to see if it already exists
|
||||
PersistenceContext pc = ctx.persistenceContext();
|
||||
Object existing = desc.contextGet(pc, id);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
// for inheritance hierarchy create the correct type for this row...
|
||||
Object ref = desc.contextRef(pc, id, ctx.unmodifiable(), ctx.isDisableLazyLoading());
|
||||
Object existing = contextGetOrImmutableHit(ctx, desc, id);
|
||||
return existing != null ? existing : createRegisterRef(ctx, desc, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void registerReference(DbReadContext ctx, EntityBean ref) {
|
||||
if (!ctx.unmodifiable() && !ctx.isDisableLazyLoading()) {
|
||||
ctx.registerBeanInherit(property, ((EntityBean) ref)._ebean_getIntercept());
|
||||
ctx.registerBeanInherit(property, ref._ebean_getIntercept());
|
||||
} else {
|
||||
ctx.registerForImmutable(ref._ebean_getIntercept());
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
void appendFrom(DbSqlContext ctx, SqlJoinType joinType) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebeaninternal.server.deploy;
|
||||
import io.ebean.*;
|
||||
import io.ebean.annotation.DocStoreMode;
|
||||
import io.ebean.bean.*;
|
||||
import io.ebean.ImmutableBeanCache;
|
||||
import io.ebean.cache.QueryCacheEntry;
|
||||
import io.ebean.DatabaseBuilder;
|
||||
import io.ebean.config.EncryptKey;
|
||||
@@ -468,10 +469,6 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
return entityType;
|
||||
}
|
||||
|
||||
private String[] properties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public BeanProperty propertyByIndex(int pos) {
|
||||
return propertiesIndex[pos];
|
||||
}
|
||||
@@ -715,14 +712,17 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
public void merge(EntityBean bean, EntityBean existing) {
|
||||
EntityBeanIntercept fromEbi = bean._ebean_getIntercept();
|
||||
EntityBeanIntercept toEbi = existing._ebean_getIntercept();
|
||||
int propertyLength = toEbi.propertyLength();
|
||||
String[] names = properties();
|
||||
int propertyLength = Math.min(toEbi.propertyLength(), propertiesIndex.length);
|
||||
for (int i = 0; i < propertyLength; i++) {
|
||||
if (fromEbi.isLoadedProperty(i)) {
|
||||
BeanProperty property = beanProperty(names[i]);
|
||||
BeanProperty property = propertiesIndex[i];
|
||||
if (property == null) {
|
||||
property = beanProperty(fromEbi.property(i));
|
||||
}
|
||||
if (!toEbi.isLoadedProperty(i)) {
|
||||
Object val = property.getValue(bean);
|
||||
property.setValue(existing, val);
|
||||
toEbi.setLoadedProperty(i);
|
||||
} else if (property.isMany()) {
|
||||
property.merge(bean, existing);
|
||||
}
|
||||
@@ -1189,6 +1189,22 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
|
||||
cacheHelp.queryCacheClear();
|
||||
}
|
||||
|
||||
public void registerImmutableCache(ImmutableBeanCache<?> beanCache) {
|
||||
cacheHelp.registerImmutableCache(beanCache);
|
||||
}
|
||||
|
||||
public boolean hasImmutableCaches() {
|
||||
return cacheHelp.hasImmutableCaches();
|
||||
}
|
||||
|
||||
public void clearImmutableCaches() {
|
||||
cacheHelp.clearImmutableCaches();
|
||||
}
|
||||
|
||||
public void removeImmutableCacheByIds(Collection<Object> ids) {
|
||||
cacheHelp.removeImmutableCacheByIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a query result from the query cache.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import io.avaje.applog.AppLog;
|
||||
import io.ebean.ImmutableBeanCache;
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
@@ -17,6 +18,7 @@ import io.ebeaninternal.server.transaction.DefaultPersistenceContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
|
||||
@@ -52,6 +54,7 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
private final boolean noCaching;
|
||||
private final SpiCacheControl cacheControl;
|
||||
private final SpiCacheRegion cacheRegion;
|
||||
private final Set<ImmutableCacheInvalidator> immutableCaches = ConcurrentHashMap.newKeySet();
|
||||
/**
|
||||
* Set to true if all persist changes need to notify the cache.
|
||||
*/
|
||||
@@ -131,6 +134,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Return true if the persist request needs to notify the cache.
|
||||
*/
|
||||
boolean isCacheNotify(PersistRequest.Type type) {
|
||||
if (hasImmutableCaches()) {
|
||||
return true;
|
||||
}
|
||||
return cacheRegion.isEnabled()
|
||||
&& (cacheNotifyOnAll || cacheNotifyOnDelete && (type == PersistRequest.Type.DELETE || type == PersistRequest.Type.DELETE_PERMANENT));
|
||||
}
|
||||
@@ -759,6 +765,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Add appropriate cache changes to support delete by id.
|
||||
*/
|
||||
void persistDeleteIds(Collection<Object> ids, CacheChangeSet changeSet) {
|
||||
if (hasImmutableCaches()) {
|
||||
changeSet.addImmutableRemoveMany(desc, ids);
|
||||
}
|
||||
if (invalidateQueryCache) {
|
||||
changeSet.addInvalidate(desc);
|
||||
} else {
|
||||
@@ -774,6 +783,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Add appropriate cache changes to support delete bean.
|
||||
*/
|
||||
void persistDelete(Object id, PersistRequestBean<T> deleteRequest, CacheChangeSet changeSet) {
|
||||
if (hasImmutableCaches()) {
|
||||
changeSet.addImmutableRemove(desc, id);
|
||||
}
|
||||
if (invalidateQueryCache) {
|
||||
changeSet.addInvalidate(desc);
|
||||
} else {
|
||||
@@ -808,6 +820,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Add appropriate changes to support update.
|
||||
*/
|
||||
void persistUpdate(Object id, PersistRequestBean<T> updateRequest, CacheChangeSet changeSet) {
|
||||
if (hasImmutableCaches()) {
|
||||
changeSet.addImmutableRemove(desc, id);
|
||||
}
|
||||
if (invalidateQueryCache) {
|
||||
changeSet.addInvalidate(desc);
|
||||
|
||||
@@ -836,6 +851,9 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
* Invalidate parts of cache due to SqlUpdate or external modification etc.
|
||||
*/
|
||||
void persistTableIUD(TableIUD tableIUD, CacheChangeSet changeSet) {
|
||||
if (hasImmutableCaches() && tableIUD.isUpdateOrDelete()) {
|
||||
changeSet.addImmutableClear(desc);
|
||||
}
|
||||
if (invalidateQueryCache) {
|
||||
changeSet.addInvalidate(desc);
|
||||
return;
|
||||
@@ -895,4 +913,29 @@ final class BeanDescriptorCacheHelp<T> {
|
||||
}
|
||||
}
|
||||
|
||||
void registerImmutableCache(ImmutableBeanCache<?> beanCache) {
|
||||
if (beanCache instanceof ImmutableCacheInvalidator) {
|
||||
immutableCaches.add((ImmutableCacheInvalidator) beanCache);
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasImmutableCaches() {
|
||||
return !immutableCaches.isEmpty();
|
||||
}
|
||||
|
||||
void clearImmutableCaches() {
|
||||
for (ImmutableCacheInvalidator immutableCache : immutableCaches) {
|
||||
immutableCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void removeImmutableCacheByIds(Collection<Object> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (ImmutableCacheInvalidator immutableCache : immutableCaches) {
|
||||
immutableCache.removeAll(ids);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -438,6 +438,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
|
||||
if (list != null) {
|
||||
for (BeanDescriptor<?> desc : list) {
|
||||
desc.clearQueryCache();
|
||||
desc.clearImmutableCaches();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-7
@@ -10,7 +10,7 @@ import io.ebean.core.type.DataReader;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
import io.ebean.text.TextException;
|
||||
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
|
||||
import io.ebeaninternal.server.util.Checksum;
|
||||
import io.ebeaninternal.server.util.JsonContentHash;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.sql.SQLException;
|
||||
@@ -141,7 +141,10 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold checksum of json source content to use for dirty detection.
|
||||
* Hold canonical hash of json content to use for dirty detection.
|
||||
* <p>
|
||||
* Uses an order-independent hash so that databases which reorder JSON object
|
||||
* keys (e.g. PostgreSQL JSONB) do not cause false dirty detection.
|
||||
* <p>
|
||||
* Does not support rebuilding 'oldValue' as no original json content.
|
||||
*/
|
||||
@@ -152,7 +155,7 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
|
||||
ChecksumMutableValue(ScalarType<?> parent, String json) {
|
||||
this.parent = parent;
|
||||
this.checksum = Checksum.checksum(json);
|
||||
this.checksum = JsonContentHash.hash(json);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,13 +168,13 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
|
||||
@Override
|
||||
public MutableValueNext nextDirty(String json) {
|
||||
final long nextChecksum = Checksum.checksum(json);
|
||||
final long nextChecksum = JsonContentHash.hash(json);
|
||||
return nextChecksum == checksum ? null : new NextPair(json, new ChecksumMutableValue(parent, nextChecksum));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEqualToObject(Object obj) {
|
||||
return Checksum.checksum(parent.format(obj)) == checksum;
|
||||
return JsonContentHash.hash(parent.format(obj)) == checksum;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -182,6 +185,10 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
|
||||
/**
|
||||
* Hold json source content. This supports rebuilding the 'oldValue'.
|
||||
* <p>
|
||||
* Uses fast string equality as primary check, with an order-independent
|
||||
* canonical hash as fallback to handle databases that reorder JSON object
|
||||
* keys (e.g. PostgreSQL JSONB).
|
||||
*/
|
||||
private static final class SourceMutableValue implements MutableValueInfo, MutableValueNext {
|
||||
|
||||
@@ -195,12 +202,15 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
|
||||
@Override
|
||||
public MutableValueNext nextDirty(String json) {
|
||||
return Objects.equals(originalJson, json) ? null : new SourceMutableValue(parent, json);
|
||||
if (jsonContentEqual(originalJson, json)) {
|
||||
return null;
|
||||
}
|
||||
return new SourceMutableValue(parent, json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEqualToObject(Object obj) {
|
||||
return Objects.equals(originalJson, parent.format(obj));
|
||||
return jsonContentEqual(originalJson, parent.format(obj));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,4 +229,13 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two JSON strings for content equality, ignoring key ordering.
|
||||
* Uses fast string equality first, falls back to order-independent hash comparison.
|
||||
*/
|
||||
private static boolean jsonContentEqual(String json1, String json2) {
|
||||
return Objects.equals(json1, json2)
|
||||
|| JsonContentHash.hash(json1) == JsonContentHash.hash(json2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.ebeaninternal.server.deploy;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.EntityBeanIntercept;
|
||||
@@ -59,6 +61,17 @@ public interface DbReadContext {
|
||||
*/
|
||||
void register(BeanPropertyAssocMany<?> many, BeanCollection<?> bc);
|
||||
|
||||
/**
|
||||
* Register a bean as a candidate for immutable cache population.
|
||||
*/
|
||||
void registerForImmutable(EntityBeanIntercept ebi);
|
||||
|
||||
/**
|
||||
* Return an immutable cached bean for the given descriptor and id if already present.
|
||||
*/
|
||||
@Nullable
|
||||
EntityBean immutableBeanHit(BeanDescriptor<?> descriptor, Object id);
|
||||
|
||||
/**
|
||||
* Set back the bean that has just been loaded with its id.
|
||||
*/
|
||||
|
||||
+48
-8
@@ -11,6 +11,8 @@ import io.ebeaninternal.server.type.TypeManager;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
|
||||
@@ -35,7 +37,7 @@ public final class DeployCreateProperties {
|
||||
* Create the appropriate properties for a bean.
|
||||
*/
|
||||
public void createProperties(DeployBeanDescriptor<?> desc) {
|
||||
createProperties(desc, desc.getBeanType(), 0);
|
||||
createProperties(desc, desc.getBeanType(), 0, new HashMap<>());
|
||||
desc.sortProperties();
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ public final class DeployCreateProperties {
|
||||
* properties the bean properties from Class. Some of these properties may not map to database
|
||||
* columns.
|
||||
*/
|
||||
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level) {
|
||||
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
|
||||
if (beanType.equals(Model.class)) {
|
||||
// ignore all fields on model (_$dbName)
|
||||
return;
|
||||
@@ -74,7 +76,7 @@ public final class DeployCreateProperties {
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
Field field = fields[i];
|
||||
if (!ignoreField(field)) {
|
||||
DeployBeanProperty prop = createProp(desc, field, beanType);
|
||||
DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap);
|
||||
if (prop != null) {
|
||||
// set a order that gives priority to inherited properties
|
||||
// push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down
|
||||
@@ -95,7 +97,7 @@ public final class DeployCreateProperties {
|
||||
if (!superClass.equals(Object.class)) {
|
||||
// recursively add any properties in the inheritance hierarchy
|
||||
// up to the Object.class level...
|
||||
createProperties(desc, superClass, level + 1);
|
||||
createProperties(desc, superClass, level + 1, mapGenerics(beanType));
|
||||
}
|
||||
} catch (PersistenceException ex) {
|
||||
throw ex;
|
||||
@@ -116,8 +118,10 @@ public final class DeployCreateProperties {
|
||||
return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType);
|
||||
}
|
||||
|
||||
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field) {
|
||||
Class<?> propertyType = field.getType();
|
||||
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
|
||||
Class<?> propertyType = field.getGenericType() instanceof TypeVariable<?>
|
||||
? genericTypeMap.get(field.getGenericType())
|
||||
: field.getType();
|
||||
if (isSpecialScalarType(field)) {
|
||||
return new DeployBeanProperty(desc, propertyType, field.getGenericType());
|
||||
}
|
||||
@@ -172,8 +176,8 @@ public final class DeployCreateProperties {
|
||||
return AnnotationUtil.has(field, Transient.class);
|
||||
}
|
||||
|
||||
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType) {
|
||||
DeployBeanProperty prop = createProp(desc, field);
|
||||
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
|
||||
DeployBeanProperty prop = createProp(desc, field, genericTypeMap);
|
||||
if (prop == null) {
|
||||
// transient annotation on unsupported type
|
||||
return null;
|
||||
@@ -224,4 +228,40 @@ public final class DeployCreateProperties {
|
||||
// if targetType is null, then must be set in annotations
|
||||
return null;
|
||||
}
|
||||
|
||||
private Map<TypeVariable<?>, Class<?>> mapGenerics(Class<?> clazz) {
|
||||
Type genericSuperclass = clazz.getGenericSuperclass();
|
||||
if (!(genericSuperclass instanceof ParameterizedType)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
ParameterizedType parameterized = (ParameterizedType) genericSuperclass;
|
||||
TypeVariable<?>[] typeVars = ((Class<?>) parameterized.getRawType()).getTypeParameters();
|
||||
Type[] actualTypes = parameterized.getActualTypeArguments();
|
||||
|
||||
Map<TypeVariable<?>, Class<?>> typeMap = new HashMap<>();
|
||||
for (int i = 0; i < typeVars.length; i++) {
|
||||
Type actual = actualTypes[i];
|
||||
Class<?> resolvedClass = resolveToClass(actual);
|
||||
if (resolvedClass != null) {
|
||||
typeMap.put(typeVars[i], resolvedClass);
|
||||
} else {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return typeMap;
|
||||
}
|
||||
|
||||
private static Class<?> resolveToClass(Type type) {
|
||||
if (type instanceof Class<?>) {
|
||||
return (Class<?>) type;
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
ParameterizedType pType = (ParameterizedType) type;
|
||||
Type raw = pType.getRawType();
|
||||
if (raw instanceof Class<?>) {
|
||||
return (Class<?>) raw;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -814,6 +814,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.contains(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.contains(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> endsWith(String propertyName, String value) {
|
||||
return add(expr.endsWith(propertyName, value));
|
||||
@@ -864,6 +869,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.icontains(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.icontains(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> idIn(Object... idValues) {
|
||||
return add(expr.idIn(idValues));
|
||||
@@ -894,6 +904,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.ilike(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.ilike(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> inPairs(Pairs pairs) {
|
||||
return add(expr.inPairs(pairs));
|
||||
@@ -1027,6 +1042,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.istartsWith(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.istartsWith(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> le(String propertyName, Query<?> subQuery) {
|
||||
return add(expr.le(propertyName, subQuery));
|
||||
@@ -1052,6 +1072,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.like(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.like(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> lt(String propertyName, Query<?> subQuery) {
|
||||
return add(expr.lt(propertyName, subQuery));
|
||||
@@ -1144,6 +1169,11 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
|
||||
return add(expr.startsWith(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : add(expr.startsWith(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> match(String propertyName, String search) {
|
||||
return match(propertyName, search, null);
|
||||
|
||||
@@ -272,6 +272,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.contains(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.contains(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> endsWith(String propertyName, String value) {
|
||||
return exprList.endsWith(propertyName, value);
|
||||
@@ -660,6 +665,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.icontains(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.icontains(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> idEq(Object value) {
|
||||
return exprList.idEq(value);
|
||||
@@ -700,6 +710,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.ilike(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.ilike(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> inPairs(Pairs pairs) {
|
||||
return exprList.inPairs(pairs);
|
||||
@@ -830,6 +845,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.istartsWith(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.istartsWith(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> le(String propertyName, Query<?> subQuery) {
|
||||
return exprList.le(propertyName, subQuery);
|
||||
@@ -845,6 +865,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.like(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.like(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> lt(String propertyName, Query<?> subQuery) {
|
||||
return exprList.lt(propertyName, subQuery);
|
||||
@@ -1015,6 +1040,11 @@ final class JunctionExpression<T> implements SpiJunction<T>, SpiExpression, Expr
|
||||
return exprList.startsWith(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value) {
|
||||
return value == null ? this : exprList.startsWith(propertyName, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionList<T> where() {
|
||||
return exprList.where();
|
||||
|
||||
@@ -79,6 +79,21 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext
|
||||
currentBuffer.add(ebi);
|
||||
}
|
||||
|
||||
BeanDescriptor<?> descriptor() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
Set<EntityBeanIntercept> bufferedBeans() {
|
||||
if (bufferList != null) {
|
||||
Set<EntityBeanIntercept> beans = new HashSet<>();
|
||||
for (LoadBuffer loadBuffer : bufferList) {
|
||||
beans.addAll(loadBuffer.batch);
|
||||
}
|
||||
return beans;
|
||||
}
|
||||
return new HashSet<>(currentBuffer.batch);
|
||||
}
|
||||
|
||||
private LoadBuffer createBuffer(int size) {
|
||||
LoadBuffer buffer = new LoadBuffer(this, size);
|
||||
if (bufferList != null) {
|
||||
@@ -227,7 +242,7 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext
|
||||
// re-add to the batch and lazy load from DB skipping l2 cache
|
||||
if (loadingStarted.get()) {
|
||||
if (CoreLog.markedAsDeleted.isLoggable(DEBUG)) {
|
||||
CoreLog.markedAsDeleted.log(DEBUG, "Adding " + ebi + "to batch " + this + "after loadingStarted(2) ", new RuntimeException("Adding to batch after load(2"));
|
||||
CoreLog.markedAsDeleted.log(DEBUG, "Adding " + ebi + "to batch " + this + "after loadingStarted(2) ", new RuntimeException("Adding to batch after load(2)"));
|
||||
}
|
||||
}
|
||||
batch.add(ebi);
|
||||
@@ -239,9 +254,36 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ensure, that every bean in the batch is in the persistence context.
|
||||
// this may happen, when bean was previously deleted, but the result is not yet committed.
|
||||
List<Object> reincarnatedIds = ensureBatchInContext(ebi);
|
||||
try {
|
||||
context.desc.ebeanServer().loadBean(new LoadBeanRequest(this, ebi, context.hitCache));
|
||||
batch.clear();
|
||||
} finally {
|
||||
if (reincarnatedIds != null) {
|
||||
for (Object id : reincarnatedIds) {
|
||||
context.desc.contextClear(persistenceContext, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.desc.ebeanServer().loadBean(new LoadBeanRequest(this, ebi, context.hitCache));
|
||||
batch.clear();
|
||||
private List<Object> ensureBatchInContext(EntityBeanIntercept ebi) {
|
||||
List<Object> reincarnatedIds = null;
|
||||
for (EntityBeanIntercept batchEbi : batch) {
|
||||
Object id = context.desc.getId(batchEbi.owner());
|
||||
if (id != null && context.desc.contextPutIfAbsent(persistenceContext, id, batchEbi.owner()) == null) {
|
||||
if (reincarnatedIds == null) {
|
||||
reincarnatedIds = new ArrayList<>();
|
||||
}
|
||||
reincarnatedIds.add(id);
|
||||
if (CoreLog.markedAsDeleted.isLoggable(DEBUG)) {
|
||||
CoreLog.markedAsDeleted.log(DEBUG, "Temporary adding " + ebi + "to persistence context", new RuntimeException("Temporary adding bean to persistence context"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reincarnatedIds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.ebeaninternal.server.loadcontext;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.CacheMode;
|
||||
import io.ebean.ImmutableBeanCache;
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebean.bean.*;
|
||||
import io.ebeaninternal.api.*;
|
||||
@@ -20,7 +23,19 @@ public final class DLoadContext implements LoadContext {
|
||||
|
||||
private final SpiEbeanServer ebeanServer;
|
||||
private final BeanDescriptor<?> rootDescriptor;
|
||||
/**
|
||||
* Path based contexts used for +query secondary query execution.
|
||||
*/
|
||||
private final Map<String, DLoadBeanContext> beanMap = new HashMap<>();
|
||||
/**
|
||||
* Type based contexts used for assoc-one lazy loading registration.
|
||||
*/
|
||||
private final Map<String, DLoadBeanContext> lazyBeanMap = new HashMap<>();
|
||||
/**
|
||||
* Type based sets used when lazy loading is disabled but immutable cache population should still occur.
|
||||
*/
|
||||
private final Map<String, Set<EntityBeanIntercept>> immutableRefMap = new HashMap<>();
|
||||
private final Map<Class<?>, ImmutableBeanCache<?>> immutableCaches;
|
||||
private final Map<String, DLoadManyContext> manyMap = new HashMap<>();
|
||||
private final DLoadBeanContext rootBeanContext;
|
||||
private final boolean asDraft;
|
||||
@@ -69,6 +84,7 @@ public final class DLoadContext implements LoadContext {
|
||||
this.planLabel = null;
|
||||
this.profileLocation = null;
|
||||
this.profilingListener = null;
|
||||
this.immutableCaches = Collections.emptyMap();
|
||||
this.rootBeanContext = new DLoadBeanContext(this, rootDescriptor, null, null);
|
||||
this.secondaryProperties = null;
|
||||
}
|
||||
@@ -97,6 +113,7 @@ public final class DLoadContext implements LoadContext {
|
||||
this.profilingListener = query.profilingListener();
|
||||
this.planLabel = query.planLabel();
|
||||
this.profileLocation = query.profileLocation();
|
||||
this.immutableCaches = query.immutableBeanCaches();
|
||||
this.secondaryProperties = query.isUnmodifiable() ? new HashSet<>() : null;
|
||||
|
||||
ObjectGraphNode parentNode = query.parentNode();
|
||||
@@ -246,12 +263,22 @@ public final class DLoadContext implements LoadContext {
|
||||
|
||||
@Override
|
||||
public void register(String path, EntityBeanIntercept ebi) {
|
||||
beanContext(path).register(ebi);
|
||||
DLoadBeanContext context = pathBeanContext(path);
|
||||
if (context != null) {
|
||||
context.register(ebi);
|
||||
} else {
|
||||
lazyBeanContext(descriptor(ebi)).register(ebi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(String path, EntityBeanIntercept ebi, BeanPropertyAssocOne<?> property) {
|
||||
beanContextWithInherit(path, property).register(ebi);
|
||||
DLoadBeanContext context = pathBeanContext(path);
|
||||
if (context != null) {
|
||||
context.register(ebi);
|
||||
} else {
|
||||
lazyBeanContext(property.targetDescriptor()).register(ebi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -259,6 +286,36 @@ public final class DLoadContext implements LoadContext {
|
||||
manyContext(path, many).register(bc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerForImmutable(EntityBeanIntercept ebi) {
|
||||
if (immutableCaches.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BeanDescriptor<?> descriptor = descriptor(ebi);
|
||||
if (immutableCaches.containsKey(descriptor.type())) {
|
||||
immutableRefMap.computeIfAbsent(descriptor.fullName(), key -> new LinkedHashSet<>()).add(ebi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable EntityBean immutableBeanHit(BeanDescriptor<?> descriptor, Object id) {
|
||||
if (!unmodifiable || immutableCaches.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
ImmutableBeanCache<?> cache = immutableCaches.get(descriptor.type());
|
||||
if (cache == null) {
|
||||
return null;
|
||||
}
|
||||
Object bean = cache.getIfPresent(id);
|
||||
if (bean instanceof EntityBean && descriptor.type().isInstance(bean)) {
|
||||
EntityBean entityBean = (EntityBean) bean;
|
||||
if (entityBean._ebean_getIntercept() instanceof InterceptReadOnly) {
|
||||
return entityBean;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int batchSize(OrmQueryProperties props) {
|
||||
if (props == null) {
|
||||
return defaultBatchSize;
|
||||
@@ -267,16 +324,60 @@ public final class DLoadContext implements LoadContext {
|
||||
return batchSize == 0 ? defaultBatchSize : batchSize;
|
||||
}
|
||||
|
||||
DLoadBeanContext beanContext(String path) {
|
||||
private DLoadBeanContext pathBeanContext(String path) {
|
||||
if (path == null) {
|
||||
return rootBeanContext;
|
||||
}
|
||||
return beanMap.computeIfAbsent(path, p -> createBeanContext(p, null));
|
||||
return beanMap.get(path);
|
||||
}
|
||||
|
||||
DLoadBeanContext beanContextWithInherit(String path, BeanPropertyAssocOne<?> property) {
|
||||
String key = path + ":" + property.targetDescriptor().name();
|
||||
return beanMap.computeIfAbsent(key, p -> createBeanContext(property, path));
|
||||
private DLoadBeanContext lazyBeanContext(BeanDescriptor<?> descriptor) {
|
||||
return lazyBeanMap.computeIfAbsent(descriptor.fullName(), p -> new DLoadBeanContext(this, descriptor, descriptor.name(), null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void populateFromImmutableCache() {
|
||||
if (immutableCaches.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<Class<?>, ImmutableBeanCache<?>> entry : immutableCaches.entrySet()) {
|
||||
BeanDescriptor<?> descriptor = rootDescriptor.descriptor(entry.getKey());
|
||||
if (descriptor == null) {
|
||||
continue;
|
||||
}
|
||||
DLoadBeanContext context = lazyBeanMap.get(descriptor.fullName());
|
||||
Map<Object, EntityBeanIntercept> interceptById = new LinkedHashMap<>();
|
||||
if (context != null) {
|
||||
for (EntityBeanIntercept ebi : context.bufferedBeans()) {
|
||||
Object id = descriptor.id(ebi.owner());
|
||||
if (id != null) {
|
||||
interceptById.put(id, ebi);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<EntityBeanIntercept> immutableRefs = immutableRefMap.get(descriptor.fullName());
|
||||
if (immutableRefs != null) {
|
||||
for (EntityBeanIntercept ebi : immutableRefs) {
|
||||
Object id = descriptor.id(ebi.owner());
|
||||
if (id != null) {
|
||||
interceptById.putIfAbsent(id, ebi);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!interceptById.isEmpty()) {
|
||||
Map<Object, ?> hits = entry.getValue().getAll(interceptById.keySet());
|
||||
for (Map.Entry<Object, ?> hit : hits.entrySet()) {
|
||||
EntityBeanIntercept ebi = interceptById.get(hit.getKey());
|
||||
Object bean = hit.getValue();
|
||||
if (ebi != null && bean instanceof EntityBean) {
|
||||
EntityBean cachedBean = (EntityBean) bean;
|
||||
descriptor.merge(cachedBean, ebi.owner());
|
||||
ebi.setLoadedFromCache(true);
|
||||
ebi.setLoadedLazy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerSecondaryNode(boolean many, OrmQueryProperties props) {
|
||||
@@ -306,8 +407,8 @@ public final class DLoadContext implements LoadContext {
|
||||
return new DLoadBeanContext(this, p.targetDescriptor(), path, queryProps);
|
||||
}
|
||||
|
||||
private DLoadBeanContext createBeanContext(BeanPropertyAssoc<?> property, String path) {
|
||||
return new DLoadBeanContext(this, property.targetDescriptor(), path, null);
|
||||
private BeanDescriptor<?> descriptor(EntityBeanIntercept ebi) {
|
||||
return rootDescriptor.descriptor(ebi.owner().getClass());
|
||||
}
|
||||
|
||||
private BeanProperty beanProperty(BeanDescriptor<?> desc, String path) {
|
||||
@@ -333,6 +434,11 @@ public final class DLoadContext implements LoadContext {
|
||||
if (disableReadAudit) {
|
||||
query.setDisableReadAuditing();
|
||||
}
|
||||
if (!immutableCaches.isEmpty()) {
|
||||
for (ImmutableBeanCache<?> beanCache : immutableCaches.values()) {
|
||||
query.putImmutableBeanCache(beanCache);
|
||||
}
|
||||
}
|
||||
if (profilingListener != null) {
|
||||
query.setProfilingListener(profilingListener);
|
||||
}
|
||||
|
||||
@@ -116,9 +116,12 @@ public final class BatchedPstmt implements SpiProfileTransactionEvent {
|
||||
getGeneratedKeys();
|
||||
}
|
||||
postExecute();
|
||||
addTimingMetrics();
|
||||
list.clear();
|
||||
transaction.profileEvent(this);
|
||||
try {
|
||||
addTimingMetrics();
|
||||
transaction.profileEvent(this);
|
||||
} finally {
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void addTimingMetrics() {
|
||||
|
||||
@@ -19,9 +19,7 @@ final class DTimeMetricStats implements TimedMetricStats {
|
||||
this.collected = collected;
|
||||
this.count = count;
|
||||
this.total = total;
|
||||
// collection is racy so sanitize the max value if it has not been set
|
||||
// this most likely would happen when count = 1 so max = mean
|
||||
this.max = max != Long.MIN_VALUE ? max : (count < 1 ? 0 : Math.round(total / count));
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,7 +17,7 @@ final class DTimedMetric implements TimedMetric {
|
||||
private final String name;
|
||||
private final LongAdder count = new LongAdder();
|
||||
private final LongAdder total = new LongAdder();
|
||||
private final LongAccumulator max = new LongAccumulator(Math::max, Long.MIN_VALUE);
|
||||
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
|
||||
private boolean collected;
|
||||
private String reportName;
|
||||
|
||||
@@ -84,11 +84,8 @@ final class DTimedMetric implements TimedMetric {
|
||||
*/
|
||||
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
|
||||
try {
|
||||
if (reset) {
|
||||
return new DTimeMetricStats(name, collected, countSum, total.sumThenReset(), max.getThenReset());
|
||||
} else {
|
||||
return new DTimeMetricStats(name, collected, countSum, total.sum(), max.get());
|
||||
}
|
||||
final long totalSum = reset ? total.sumThenReset() : total.sum();
|
||||
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
|
||||
} finally {
|
||||
collected = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.CancelableQuery;
|
||||
import io.ebean.QueryIterator;
|
||||
import io.ebean.Version;
|
||||
@@ -191,11 +193,11 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
|
||||
this.autoTuneProfiling = profilingListener != null;
|
||||
// set the generated sql back to the query
|
||||
// so its available to the user...
|
||||
query.setGeneratedSql(queryPlan.sql());
|
||||
SqlTreePlan sqlTree = queryPlan.sqlTree();
|
||||
this.rootNode = sqlTree.rootNode();
|
||||
this.manyProperty = sqlTree.manyProperty();
|
||||
this.sql = queryPlan.sql();
|
||||
query.setGeneratedSql(sql);
|
||||
this.rawSql = queryPlan.isRawSql();
|
||||
this.logWhereSql = queryPlan.logWhereSql();
|
||||
this.desc = request.descriptor();
|
||||
@@ -567,7 +569,7 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
QueryIterator<T> readIterate(int bufferSize, OrmQueryRequest<T> request) {
|
||||
@@ -601,6 +603,16 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
|
||||
request.loadContext().register(path(many.name()), many, bc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerForImmutable(EntityBeanIntercept ebi) {
|
||||
request.loadContext().registerForImmutable(ebi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable EntityBean immutableBeanHit(BeanDescriptor<?> descriptor, Object id) {
|
||||
return request.loadContext().immutableBeanHit(descriptor, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean includeSecondary(BeanPropertyAssocMany<?> many) {
|
||||
return request.loadContext().includeSecondary(many);
|
||||
|
||||
@@ -354,6 +354,7 @@ public final class CQueryEngine {
|
||||
cquery.auditFindMany();
|
||||
}
|
||||
request.executeSecondaryQueries(false);
|
||||
request.populateFromImmutableCache();
|
||||
if (request.isQueryCachePut()) {
|
||||
request.addDependentTables(cquery.dependentTables());
|
||||
}
|
||||
@@ -390,6 +391,7 @@ public final class CQueryEngine {
|
||||
cquery.auditFind(bean);
|
||||
}
|
||||
request.executeSecondaryQueries(false);
|
||||
request.populateFromImmutableCache();
|
||||
request.unmodifiableFreeze(bean);
|
||||
return (T) bean;
|
||||
} catch (SQLException e) {
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ final class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent, Ca
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
Set<String> dependentTables() {
|
||||
|
||||
@@ -137,7 +137,7 @@ final class CQueryRowCount implements SpiProfileTransactionEvent, CancelableQuer
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
Set<String> dependentTables() {
|
||||
|
||||
@@ -115,7 +115,7 @@ final class CQueryUpdate implements SpiProfileTransactionEvent, CancelableQuery
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,8 +15,11 @@ final class DFetchGroup<T> implements SpiFetchGroup<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrmQueryDetail detail(OrmQueryDetail existing) {
|
||||
return detail.copy(existing);
|
||||
public OrmQueryDetail detail(OrmQueryDetail priorState) {
|
||||
if (priorState == null) {
|
||||
return detail.copy(null);
|
||||
}
|
||||
return detail.copyInto(priorState, priorState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -219,11 +219,21 @@ final class DefaultFetchGroupQuery<T> implements SpiFetchGroupQuery<T>, SpiQuery
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> alsoIfPresent(@Nullable Object value, Consumer<Query<T>> apply) {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> usingTransaction(Transaction transaction) {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> using(ImmutableBeanCache<?> beanCache) {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> usingConnection(Connection connection) {
|
||||
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
|
||||
|
||||
@@ -25,7 +25,7 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode {
|
||||
private final STreePropertyAssoc assocBeanProperty;
|
||||
private final SpiQuery.TemporalMode temporalMode;
|
||||
private final String prefix;
|
||||
private final boolean manyJoin;
|
||||
private boolean manyJoin;
|
||||
private final boolean pathContainsMany;
|
||||
private List<SqlTreeNode> children;
|
||||
|
||||
@@ -96,6 +96,9 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode {
|
||||
if (children == null) {
|
||||
children = new ArrayList<>();
|
||||
}
|
||||
if (child.hasMany()) {
|
||||
manyJoin = true;
|
||||
}
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
private String nativeSql;
|
||||
private boolean orderById;
|
||||
private ProfileLocation profileLocation;
|
||||
private Map<Class<?>, ImmutableBeanCache<?>> immutableBeanCaches;
|
||||
|
||||
public DefaultOrmQuery(BeanDescriptor<T> desc, SpiEbeanServer server, ExpressionFactory expressionFactory) {
|
||||
this.beanDescriptor = desc;
|
||||
@@ -307,6 +308,14 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> alsoIfPresent(@Nullable Object value, Consumer<Query<T>> consumer) {
|
||||
if (value != null) {
|
||||
consumer.accept(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addSoftDeletePredicate(String softDeletePredicate) {
|
||||
if (softDeletePredicates == null) {
|
||||
@@ -768,6 +777,9 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
copy.useBeanCache = useBeanCache;
|
||||
copy.useQueryCache = useQueryCache;
|
||||
copy.unmodifiable = unmodifiable;
|
||||
if (immutableBeanCaches != null) {
|
||||
copy.immutableBeanCaches = new LinkedHashMap<>(immutableBeanCaches);
|
||||
}
|
||||
if (detail != null) {
|
||||
copy.detail = detail.copy(null);
|
||||
}
|
||||
@@ -1462,6 +1474,12 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<T> using(ImmutableBeanCache<?> beanCache) {
|
||||
putImmutableBeanCache(beanCache);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Query<T> usingConnection(Connection connection) {
|
||||
this.transaction = new ExternalJdbcTransaction(connection);
|
||||
@@ -1480,6 +1498,19 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putImmutableBeanCache(ImmutableBeanCache<?> beanCache) {
|
||||
if (immutableBeanCaches == null) {
|
||||
immutableBeanCaches = new LinkedHashMap<>();
|
||||
}
|
||||
immutableBeanCaches.put(beanCache.type(), beanCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Class<?>, ImmutableBeanCache<?>> immutableBeanCaches() {
|
||||
return immutableBeanCaches == null ? Collections.emptyMap() : Collections.unmodifiableMap(immutableBeanCaches);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUseMaster() {
|
||||
return useMaster;
|
||||
|
||||
+12
@@ -269,6 +269,12 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingMaster(boolean useMaster) {
|
||||
DefaultRelationalQuery.this.usingMaster(useMaster);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingTransaction(Transaction transaction) {
|
||||
transaction(transaction);
|
||||
@@ -308,6 +314,12 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingMaster(boolean useMaster) {
|
||||
DefaultRelationalQuery.this.usingMaster(useMaster);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeQuery<T> usingTransaction(Transaction transaction) {
|
||||
transaction(transaction);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user