mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbdc5d753e | ||
|
|
a7fddf1981 | ||
|
|
f610d03a1d | ||
|
|
852f199ffb | ||
|
|
564bc131cc | ||
|
|
0e6cd9db8c | ||
|
|
484ed5d859 | ||
|
|
56d33b1f1b | ||
|
|
e9b00cbbd1 | ||
|
|
230d7a36a7 | ||
|
|
10bf5dbbbd | ||
|
|
bffe741642 | ||
|
|
a8189567dd | ||
|
|
1545e68c3e | ||
|
|
4fd32b45d2 | ||
|
|
313fdda857 | ||
|
|
d42f72c0a2 | ||
|
|
6d53e89a80 | ||
|
|
45297a52f6 | ||
|
|
af20eef1df | ||
|
|
7c5ee5b555 | ||
|
|
a2337a096e | ||
|
|
7bf5fc2798 | ||
|
|
987798add9 | ||
|
|
327f6a3115 | ||
|
|
6295faa351 | ||
|
|
d9252a9c85 | ||
|
|
ddd864852d | ||
|
|
7966d8bc1b | ||
|
|
66e6e83e60 | ||
|
|
0e7f68e75a | ||
|
|
7b8e1713dd | ||
|
|
3ea9a5fa7f | ||
|
|
4fab5dfe4b | ||
|
|
9fac046f39 | ||
|
|
50bfd04987 | ||
|
|
d71ced32e8 | ||
|
|
6722cf50e9 | ||
|
|
13cebbcacb | ||
|
|
e70777bdb6 | ||
|
|
86cfd185f3 | ||
|
|
3970847443 | ||
|
|
b440c5ea27 | ||
|
|
1836ff18a5 | ||
|
|
efdee053cc | ||
|
|
a1ee75ffec | ||
|
|
da3dd8b215 | ||
|
|
6d30e6ff82 | ||
|
|
453a320210 | ||
|
|
b88e523ed9 | ||
|
|
61cc5e3459 | ||
|
|
23f23fa32b | ||
|
|
f65c409bfe | ||
|
|
ec49824430 | ||
|
|
819aaece4d | ||
|
|
c275953582 | ||
|
|
82e8494f42 | ||
|
|
abacda2c8f | ||
|
|
a7d1253dba | ||
|
|
a081d08621 | ||
|
|
8c37b53bad | ||
|
|
dadeea640d | ||
|
|
3f6d565800 | ||
|
|
1408696912 | ||
|
|
e0531a6315 | ||
|
|
0023f0ce13 | ||
|
|
90da31d2be | ||
|
|
032f4de857 | ||
|
|
59431814ce | ||
|
|
e7194055be | ||
|
|
806c7cd752 | ||
|
|
dfc7f92160 | ||
|
|
26351325a8 | ||
|
|
2d37f9d01e | ||
|
|
d3fd03ce5b | ||
|
|
a8e92791cd | ||
|
|
94e63cc30f | ||
|
|
3567263250 | ||
|
|
bc03f8d516 | ||
|
|
7035ff20eb | ||
|
|
f38479022a | ||
|
|
986cc905d8 | ||
|
|
8f3fe688cb | ||
|
|
eee26d9ed3 | ||
|
|
44bd60e586 | ||
|
|
20d7af6d25 | ||
|
|
e5aeda9d4e | ||
|
|
46bb1ca060 | ||
|
|
0652167101 | ||
|
|
6633293151 | ||
|
|
9e603f2848 | ||
|
|
fff345ffc8 | ||
|
|
dd1fe845aa | ||
|
|
80451c3c62 | ||
|
|
2d199737aa | ||
|
|
8f588095b5 | ||
|
|
71a1a07aec | ||
|
|
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 | ||
|
|
daa33ca5a5 | ||
|
|
ebb0305cf3 | ||
|
|
d03d36bba4 | ||
|
|
88408edd24 | ||
|
|
0aad35b840 | ||
|
|
e7979b285d | ||
|
|
91920b00dc | ||
|
|
bdfe016d2d | ||
|
|
b2670f9cc0 | ||
|
|
ebc90e0e82 | ||
|
|
c4230c780f | ||
|
|
794f933310 | ||
|
|
c8a7a263a9 | ||
|
|
aeef6d0ea2 | ||
|
|
a99aef8b00 | ||
|
|
6f17cc6327 | ||
|
|
903947b3cb | ||
|
|
797f75f7b7 | ||
|
|
af443ef2f2 | ||
|
|
ad4f027835 | ||
|
|
59fa175662 | ||
|
|
367eba8685 | ||
|
|
be542635a5 | ||
|
|
6e9d0a91e0 | ||
|
|
a99ef3ebf2 | ||
|
|
19afa845d1 | ||
|
|
239900cd3b | ||
|
|
73ac39971c | ||
|
|
3a876252ec | ||
|
|
231b52ba88 | ||
|
|
808019cf3d | ||
|
|
364520455f | ||
|
|
e92621a489 | ||
|
|
ccd1b7b8ec | ||
|
|
d144273307 | ||
|
|
41c5ebdcd7 | ||
|
|
9e711efecb | ||
|
|
ef7fd76f14 | ||
|
|
b7e3ddbedd | ||
|
|
9ac26da003 | ||
|
|
69c932816a | ||
|
|
a1facf527b | ||
|
|
ec42ebf66d | ||
|
|
5dede70801 | ||
|
|
b2f02ecc1f | ||
|
|
15fa7cd1c2 | ||
|
|
8dced38cd5 | ||
|
|
5711ba8949 | ||
|
|
65c4ae4099 | ||
|
|
4dcfbda481 | ||
|
|
5a9867b48e | ||
|
|
3b4e3eb952 | ||
|
|
3d3d92c450 | ||
|
|
1c0b68df6a | ||
|
|
3b588b2f44 | ||
|
|
2c7adf205f | ||
|
|
d74ddf064b | ||
|
|
162ede4f00 | ||
|
|
314a18f101 | ||
|
|
adb3057074 | ||
|
|
5ccf077a84 | ||
|
|
ba6eff7b5b | ||
|
|
b7b7014fc4 | ||
|
|
abdf249e00 | ||
|
|
34e267278e | ||
|
|
91c89746de | ||
|
|
3b49a3878d | ||
|
|
0f06480c22 | ||
|
|
e8d8d596dd | ||
|
|
160f042b1d | ||
|
|
e199a25ca8 | ||
|
|
0e24e78e9c | ||
|
|
fc8bc93742 | ||
|
|
cd18675dba | ||
|
|
67772b058e | ||
|
|
94185185db | ||
|
|
3e05278ab4 | ||
|
|
7853bf0960 | ||
|
|
15b3eb2bf3 | ||
|
|
f53c56490c | ||
|
|
559b39eedc | ||
|
|
0ebcba17f1 | ||
|
|
71edd33fa3 | ||
|
|
24b99d065a | ||
|
|
db7fbd5246 | ||
|
|
ca97338839 | ||
|
|
355144f16d | ||
|
|
7316fb8f1d | ||
|
|
475c30c2a2 | ||
|
|
5a11dcb2e3 | ||
|
|
66d29a7f0c | ||
|
|
e3af5cdeb4 | ||
|
|
c4bfa48b59 | ||
|
|
8473f9fd39 |
@@ -40,5 +40,5 @@ jobs:
|
||||
# - name: Maven single test
|
||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||
- name: Build with Maven
|
||||
run: mvn -T 8 clean test -Pdefault
|
||||
run: mvn -T 1C clean test -Pdefault
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: db2
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: H2Database
|
||||
run: mvn -T 8 clean package
|
||||
run: mvn -T 1C clean package
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mariadb 10.11
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mysql
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: oracle
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: postgres
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: sqlserver 2022
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
|
||||
@@ -13,6 +13,9 @@ ebean-profiling*.xml
|
||||
profiling/
|
||||
.DS_Store
|
||||
|
||||
# Local Redis integration test credentials
|
||||
ebean-redis/src/test/resources/redis-local.yml
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
|
||||
[](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
|
||||
[](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
|
||||
[](https://www.graalvm.org/)
|
||||
|
||||
##### Build with database platforms
|
||||
[](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
|
||||
@@ -80,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.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# 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) |
|
||||
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
|
||||
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.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,24 @@
|
||||
# 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
|
||||
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.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
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.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,203 @@
|
||||
# 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 |
|
||||
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
|
||||
|
||||
## 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 |
|
||||
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
|
||||
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
|
||||
|
||||
## 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)` |
|
||||
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
|
||||
|
||||
## 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
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.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,400 @@
|
||||
# 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
|
||||
|
||||
### Protecting hand-edited and non-versioned migrations across regeneration
|
||||
|
||||
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
|
||||
entity model**. It can therefore overwrite content you did not change in the
|
||||
entity beans, including:
|
||||
|
||||
- **hand-edited DDL** in a generated versioned `.sql` file, and
|
||||
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
|
||||
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
|
||||
|
||||
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
|
||||
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
|
||||
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
|
||||
regeneration. The trade-off: to pick up an upstream change to a built-in init
|
||||
script (e.g. the partition helper) you must **delete the file first**, then
|
||||
regenerate. Repeatable scripts are always regenerated.
|
||||
|
||||
To avoid losing manual work:
|
||||
|
||||
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
|
||||
cannot express — it is isolated and now protected from regeneration.
|
||||
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
|
||||
produces, review the diff after **every** regeneration and **restore** any
|
||||
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
|
||||
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
|
||||
re-check that the new migration is listed and filenames match after renaming a
|
||||
generated file.
|
||||
|
||||
> **Run the generator from the module directory.** The output path set via
|
||||
> `setPathToResources(...)` is resolved relative to the **working directory**.
|
||||
> Run `GenerateDbMigration` with the working directory set to the module that owns
|
||||
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
|
||||
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
|
||||
|
||||
---
|
||||
|
||||
## 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,116 @@
|
||||
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
|
||||
|
||||
## Purpose
|
||||
|
||||
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
|
||||
|
||||
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
|
||||
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
|
||||
for everything the built-in support does not handle.
|
||||
|
||||
This guide lists exactly which property types are handled built-in and which require
|
||||
`ebean-jackson-mapper`.
|
||||
|
||||
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
|
||||
> classpath, Ebean fails fast at startup:
|
||||
>
|
||||
> ```text
|
||||
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
|
||||
> Jackson ObjectMapper not present for <property>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|
||||
|---|:---:|:---:|
|
||||
| `String` | ✅ | |
|
||||
| `List<String>`, `List<Long>` | ✅ | |
|
||||
| `Set<String>`, `Set<Long>` | ✅ | |
|
||||
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
|
||||
| `Map<String, String>` | ✅ | |
|
||||
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
|
||||
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
|
||||
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
|
||||
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
|
||||
| POJOs, records, or any other type | | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Built-in support (no Jackson required)
|
||||
|
||||
The built-in path materialises JSON into the *natural* JSON value types
|
||||
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
|
||||
for the following declared property types:
|
||||
|
||||
- **`String`** — stored as raw JSON text.
|
||||
- **`List<String>`** and **`List<Long>`**.
|
||||
- **`Set<String>`** and **`Set<Long>`**.
|
||||
- **`Map<K, V>`** where:
|
||||
- the key `K` is `String` or an **enum**, and
|
||||
- the value `V` is `Object`, `String`, or a wildcard `?`.
|
||||
|
||||
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
|
||||
are all built-in.
|
||||
|
||||
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
|
||||
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
|
||||
|
||||
---
|
||||
|
||||
## Everything else → Jackson `ObjectMapper`
|
||||
|
||||
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
|
||||
requires `ebean-jackson-mapper`:
|
||||
|
||||
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
|
||||
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
|
||||
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
|
||||
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
|
||||
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
|
||||
- **POJOs, records, and any other custom type.**
|
||||
|
||||
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
|
||||
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
|
||||
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
|
||||
|
||||
---
|
||||
|
||||
## Adding `ebean-jackson-mapper`
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
|
||||
registers the mapper-based JSON support automatically.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
|
||||
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
|
||||
JSON keys.
|
||||
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
|
||||
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
|
||||
(with a JSON fallback on platforms without array support) and supports more element types
|
||||
than built-in `@DbJson` collections.
|
||||
- The reason typed value/element collections need a real mapper is that the built-in path
|
||||
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
|
||||
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
|
||||
type-aware mapper.
|
||||
|
||||
---
|
||||
|
||||
## Choosing
|
||||
|
||||
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
|
||||
object/string maps) to avoid pulling in Jackson.
|
||||
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
|
||||
typed-value maps.
|
||||
@@ -0,0 +1,262 @@
|
||||
# Guide: Ebean query metrics and naming
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains the metrics Ebean captures, how the metric **name** for a query
|
||||
is derived, and how you influence that name with `setLabel(..)` and **profile
|
||||
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
|
||||
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
|
||||
|
||||
Use this guide when you want to identify a query in metrics/telemetry, when a query
|
||||
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ebean records timing and counter metrics for the work it does. Every metric has a
|
||||
**name** whose leading segment identifies the kind of work:
|
||||
|
||||
| Prefix | What it measures | Example name |
|
||||
|---|---|---|
|
||||
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
|
||||
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
|
||||
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
|
||||
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
|
||||
| `orm.update.` | ORM update statement | `orm.update.<label>` |
|
||||
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
|
||||
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
|
||||
| `l2n.` | L2 cache region | `l2n.customer.hit` |
|
||||
|
||||
The rest of this guide focuses on **`orm.` query names**, which is where labels and
|
||||
profile locations apply.
|
||||
|
||||
---
|
||||
|
||||
## How an ORM query name is derived
|
||||
|
||||
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
|
||||
of three sources, in priority order:
|
||||
|
||||
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
|
||||
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
|
||||
3. **Neither** — the bean type plus the query type (e.g. `findList`).
|
||||
|
||||
| Root query source | Resulting name |
|
||||
|---|---|
|
||||
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
|
||||
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
|
||||
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
|
||||
|
||||
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
|
||||
could be used for any bean), so the bean type is prefixed. A profile location is already
|
||||
unique and type-independent, so it is used as-is.
|
||||
|
||||
### Step 1 - Label a query explicitly
|
||||
|
||||
```java
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setLabel("custMain")
|
||||
.findList();
|
||||
// metric name: orm.Customer.custMain
|
||||
```
|
||||
|
||||
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
|
||||
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
|
||||
used as-is, and an unlabelled DTO query uses just the DTO type:
|
||||
|
||||
```java
|
||||
DB.findDto(CustomerDto.class, sql)
|
||||
.setLabel("byEmail")
|
||||
.findList();
|
||||
// metric name: dto.CustomerDto.byEmail
|
||||
// profile location only -> dto.<location> (no type prefix)
|
||||
// unlabelled -> dto.CustomerDto
|
||||
```
|
||||
|
||||
### Step 2 - Use a profile location (preferred for finders / query beans)
|
||||
|
||||
A profile location identifies a query by its **call site** (`Class.method`) instead of a
|
||||
hand-written label.
|
||||
|
||||
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
|
||||
setup when using query beans / finders), Ebean assigns each query a profile location
|
||||
derived from its call site — no code is required:
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.eq(Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
|
||||
```
|
||||
|
||||
The enhancer derives the location from the calling code (the method that runs the query),
|
||||
and for many call sites it includes the **source line number** (e.g.
|
||||
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
|
||||
names automatically.
|
||||
|
||||
**Setting one explicitly.** You can also set a profile location yourself, which is useful
|
||||
without enhancement or to control the identity:
|
||||
|
||||
```java
|
||||
ProfileLocation LOC = ProfileLocation.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setProfileLocation(LOC)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<DeclaringClass>.<method>
|
||||
```
|
||||
|
||||
Factory choices:
|
||||
|
||||
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
|
||||
- `ProfileLocation.createWithLine()` — includes the source line number
|
||||
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
|
||||
**distinct** names.
|
||||
- `ProfileLocation.create("label")` — a named location (used for named transactions).
|
||||
|
||||
> Note: a location with no line number (`create()`, or a call site the enhancer emits
|
||||
> without a line) means two different queries in the same method share one name. The
|
||||
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
|
||||
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
|
||||
> same-method call sites in the name itself.
|
||||
|
||||
---
|
||||
|
||||
## Secondary (lazy / query) load naming
|
||||
|
||||
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
|
||||
**secondary** query. Its name **extends the parent query's full name** with the relative
|
||||
path and the load mode (`lazy` or `query`), joined with `.`:
|
||||
|
||||
```
|
||||
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
|
||||
```
|
||||
|
||||
So a secondary load is always an exact extension of its parent metric name, which makes
|
||||
the relationship obvious in dashboards.
|
||||
|
||||
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
|
||||
|
||||
Lazy loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.lazy
|
||||
orm.Customer.custMain.orders.lazy.details.lazy
|
||||
```
|
||||
|
||||
Secondary eager `fetchQuery()` loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.query
|
||||
orm.Customer.custMain.orders.query.details.query
|
||||
```
|
||||
|
||||
The same applies with a **profile-location** root (no explicit `setLabel`):
|
||||
```
|
||||
orm.CustomerFinder.byName
|
||||
orm.CustomerFinder.byName.contacts.lazy
|
||||
```
|
||||
|
||||
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
|
||||
type — it inherits the parent's name so it relates back to where the load originated.
|
||||
|
||||
---
|
||||
|
||||
## Inline SQL comment
|
||||
|
||||
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
|
||||
profile-location label) as an inline SQL comment, which is useful for matching slow
|
||||
queries in database logs back to application code:
|
||||
|
||||
```sql
|
||||
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
|
||||
```
|
||||
|
||||
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
|
||||
label. Secondary queries use their full extended name
|
||||
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
|
||||
commented.
|
||||
|
||||
Disable it via the builder:
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.includeLabelInSql(false)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collecting metrics at runtime
|
||||
|
||||
Read collected metrics through `Database.metaInfo()`:
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaQueryMetric;
|
||||
import io.ebean.meta.ServerMetrics;
|
||||
|
||||
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
|
||||
|
||||
for (MetaQueryMetric q : metrics.queryMetrics()) {
|
||||
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
|
||||
q.name(), // e.g. orm.Customer.custMain
|
||||
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
|
||||
q.count(), q.total(), q.mean());
|
||||
}
|
||||
```
|
||||
|
||||
Key API:
|
||||
|
||||
- `database.metaInfo()` → `MetaInfoManager`.
|
||||
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
|
||||
reset; `visitMetrics(visitor)` for streaming.
|
||||
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
|
||||
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
|
||||
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
|
||||
|
||||
---
|
||||
|
||||
## Mapping to avaje-metrics tags
|
||||
|
||||
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
|
||||
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
|
||||
form, with the bean type carried as a `type` tag:
|
||||
|
||||
```
|
||||
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
|
||||
```
|
||||
|
||||
Because the entity is available as the `type` tag, two different-entity queries that
|
||||
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
|
||||
Prometheus, StatsD) without needing the bean type in the name.
|
||||
|
||||
For the integration setup, see the avaje-metrics guide
|
||||
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
|
||||
|
||||
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
|
||||
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### A query shows up as `orm.<Bean>.findList` (no useful identity)
|
||||
|
||||
It has neither a label nor a profile location. Add `setLabel(..)` or a
|
||||
`ProfileLocation`, or apply a profile location on the finder / query bean.
|
||||
|
||||
### Two queries in one method share a metric name
|
||||
|
||||
This happens when the profile location for those call sites has no line number. With
|
||||
enhancement, many call sites already include a line number; for those that don't, use
|
||||
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
|
||||
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
|
||||
different entity types.
|
||||
|
||||
### A secondary (lazy / query) load isn't grouped under its parent
|
||||
|
||||
Secondary names extend the parent's full name. If the parent has no label or profile
|
||||
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
|
||||
that. Give the root query a label or profile location for a stable parent name.
|
||||
@@ -0,0 +1,242 @@
|
||||
# Guide: Ebean query plan capture
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable and configure **query plan capture** in Ebean — the
|
||||
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
|
||||
queries, so you can diagnose missing indexes and poor plans in production.
|
||||
|
||||
Use this guide when you want Ebean to record real query plans, when tuning the capture
|
||||
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Query plan capture is a **two-phase** mechanism:
|
||||
|
||||
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
|
||||
slower than a threshold, captures the actual **bind values** that were used. This is
|
||||
cheap: it just remembers the parameters of a slow execution.
|
||||
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
|
||||
against the database to obtain the execution plan, producing `MetaQueryPlan` results
|
||||
that are handed to a `QueryPlanListener`.
|
||||
|
||||
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
|
||||
happens periodically or on demand, against representative bind values, rather than on
|
||||
every slow query.
|
||||
|
||||
Two ways to trigger phase 2:
|
||||
|
||||
- **Automatic periodic capture** — a background timer collects plans on a schedule.
|
||||
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
|
||||
(this is what remote tooling such as ebean-insight uses).
|
||||
|
||||
Plan capable queries are:
|
||||
|
||||
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
|
||||
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
|
||||
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
|
||||
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
|
||||
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
|
||||
does not double-count in `queryPlanInit`.
|
||||
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
|
||||
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
|
||||
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
|
||||
|
||||
Specifically **excluded** are:
|
||||
|
||||
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
|
||||
|
||||
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
|
||||
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
|
||||
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
|
||||
never captures bind values and cannot be `EXPLAIN`'d.
|
||||
|
||||
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
|
||||
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
|
||||
> incur no extra cost beyond their existing timing metric.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Enable bind capture
|
||||
|
||||
Bind capture is the master switch; nothing is captured until it is on.
|
||||
|
||||
> **Security — bind values may contain PII.** Bind capture records the **actual
|
||||
> parameter values** used by slow query executions, and those values are stored
|
||||
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
|
||||
> plan). They can therefore contain personal or otherwise sensitive data. Capture
|
||||
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
|
||||
> that data exposure is acceptable, restrict who can read captured plans, and
|
||||
> prefer arming specific query hashes (Step 3) over a low global threshold so you
|
||||
> capture the minimum needed.
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true) // turn on bind capture
|
||||
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
|
||||
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
|
||||
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
|
||||
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
|
||||
|
||||
Equivalent `application.properties` (avaje-config / properties):
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Enable automatic periodic capture (optional)
|
||||
|
||||
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true)
|
||||
.queryPlanThresholdMicros(100_000)
|
||||
.queryPlanCapture(true) // turn on the periodic capture timer
|
||||
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
|
||||
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
|
||||
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
|
||||
.queryPlanListener(capture -> {
|
||||
for (var plan : capture.plans()) {
|
||||
System.out.println(plan.label() + "\n" + plan.plan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
|
||||
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
|
||||
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
|
||||
exceeded, bounding the database load. Default **10000** (10s).
|
||||
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
|
||||
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
|
||||
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
|
||||
|
||||
Properties form:
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
queryPlan.capture=true
|
||||
queryPlan.capturePeriodSecs=600
|
||||
queryPlan.captureMaxTimeMillis=10000
|
||||
queryPlan.captureMaxCount=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Capture on demand (foreground)
|
||||
|
||||
Instead of (or in addition to) the periodic timer, drive capture through
|
||||
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
|
||||
arms specific slow queries by their plan hash.
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaInfoManager;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
|
||||
MetaInfoManager meta = database.metaInfo();
|
||||
|
||||
// Phase 1: arm bind capture - either all plans or specific hashes
|
||||
QueryPlanInit init = new QueryPlanInit();
|
||||
init.setAll(true); // or init.add("<planHash>", 50_000);
|
||||
init.thresholdMicros(100_000);
|
||||
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
|
||||
|
||||
// ... let the application run so slow executions capture their bind values ...
|
||||
|
||||
// Phase 2: collect plans now (runs EXPLAIN)
|
||||
QueryPlanRequest request = new QueryPlanRequest();
|
||||
request.maxCount(10);
|
||||
request.maxTimeMillis(10_000);
|
||||
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
|
||||
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
|
||||
```
|
||||
|
||||
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
|
||||
arms a specific plan (a hash of `"all"` is treated as all).
|
||||
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
|
||||
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
|
||||
bound the work, mirroring the periodic settings.
|
||||
|
||||
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
|
||||
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
|
||||
`captureMicros()`, and `whenCaptured()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - EXPLAIN dialect
|
||||
|
||||
Ebean chooses the `EXPLAIN` statement per database platform:
|
||||
|
||||
| Platform | EXPLAIN used |
|
||||
|---|---|
|
||||
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
|
||||
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
|
||||
| Oracle | `EXPLAIN PLAN FOR <sql>` |
|
||||
| SQL Server | platform-specific logger |
|
||||
| H2 / MySQL / other | `explain <sql>` |
|
||||
|
||||
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
|
||||
.build();
|
||||
```
|
||||
|
||||
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
|
||||
> executes** the query to produce real timings. For non-idempotent or expensive queries,
|
||||
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
|
||||
|
||||
---
|
||||
|
||||
## Related setting: internal plan TTL
|
||||
|
||||
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
|
||||
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
|
||||
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
|
||||
the same builder.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No plans are captured
|
||||
|
||||
1. `queryPlanEnable(true)` must be set — it is the master switch.
|
||||
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
|
||||
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
|
||||
3. For periodic capture, also set `queryPlanCapture(true)`.
|
||||
4. Queries must actually run slower than the threshold to have their binds captured.
|
||||
|
||||
### Plans appear but nothing is reported anywhere
|
||||
|
||||
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
|
||||
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
|
||||
|
||||
### Capture adds noticeable database load
|
||||
|
||||
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
|
||||
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
|
||||
`queryPlanExplain` to a non-ANALYZE form.
|
||||
|
||||
### An unlabelled SqlQuery or update metric never offers plan capture
|
||||
|
||||
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
|
||||
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
|
||||
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
|
||||
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
|
||||
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
|
||||
capture and are intentionally excluded.
|
||||
@@ -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,91 @@
|
||||
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
|
||||
avaje-json-core APIs.
|
||||
|
||||
Use this when upgrading code that references:
|
||||
|
||||
- `com.fasterxml.jackson.core.JsonParser`
|
||||
- `com.fasterxml.jackson.core.JsonGenerator`
|
||||
- `com.fasterxml.jackson.core.JsonFactory`
|
||||
|
||||
The replacement types are:
|
||||
|
||||
- `io.avaje.json.JsonReader`
|
||||
- `io.avaje.json.JsonWriter`
|
||||
- `io.avaje.json.stream.JsonStream`
|
||||
|
||||
---
|
||||
|
||||
## Breaking API changes
|
||||
|
||||
| Previous API | New API |
|
||||
|---|---|
|
||||
| `JsonParser` | `JsonReader` |
|
||||
| `JsonGenerator` | `JsonWriter` |
|
||||
| `JsonFactory` | `JsonStream` |
|
||||
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
|
||||
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
|
||||
|
||||
---
|
||||
|
||||
## Typical migration rewrites
|
||||
|
||||
### Parser and generator signatures
|
||||
|
||||
```java
|
||||
// before
|
||||
void read(JsonParser parser)
|
||||
void write(JsonGenerator generator)
|
||||
|
||||
// after
|
||||
void read(JsonReader parser)
|
||||
void write(JsonWriter generator)
|
||||
```
|
||||
|
||||
### Database configuration
|
||||
|
||||
```java
|
||||
// before
|
||||
Database.builder().jsonFactory(factory)
|
||||
|
||||
// after
|
||||
Database.builder().jsonStream(stream)
|
||||
```
|
||||
|
||||
### JSON utility calls
|
||||
|
||||
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
|
||||
If your code was calling those APIs with Jackson core types, switch to avaje types.
|
||||
|
||||
---
|
||||
|
||||
## Dependency and module notes
|
||||
|
||||
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
|
||||
parsing/writing.
|
||||
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
|
||||
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
|
||||
integrations.
|
||||
|
||||
---
|
||||
|
||||
## Behavior notes to verify during upgrade
|
||||
|
||||
1. Parser token handling is now based on avaje `JsonReader.Token`.
|
||||
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
|
||||
be validated in your tests if you previously depended on Jackson token quirks.
|
||||
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
|
||||
keep ObjectMapper wiring enabled.
|
||||
|
||||
---
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
|
||||
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
|
||||
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
|
||||
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
|
||||
|
||||
@@ -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,573 @@
|
||||
# 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 |
|
||||
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
|
||||
| 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.
|
||||
|
||||
### Example - stream and map to another type
|
||||
|
||||
Choose based on result size and how you consume it:
|
||||
|
||||
- **`findList().stream()`** — executes the query, materialises the rows,
|
||||
**releases the connection**, then streams over an in-memory list. No open
|
||||
database resources and no try-with-resources needed. Prefer this for small or
|
||||
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
|
||||
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
|
||||
connection (and an implicit transaction) open for the **whole lifetime of the
|
||||
stream pipeline**. It must be closed with try-with-resources. Prefer it when
|
||||
the result may be large, when you want constant memory, or when you want to
|
||||
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
|
||||
|
||||
```java
|
||||
// small, bounded result fully collected -> findList().stream()
|
||||
List<PendingPlan> pending = new QCaptureRequest()
|
||||
.collectedAt.isNull()
|
||||
.orderBy().requestedAt.asc()
|
||||
.findList()
|
||||
.stream()
|
||||
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
|
||||
.toList();
|
||||
|
||||
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
|
||||
try (Stream<Customer> stream = new QCustomer()
|
||||
.status.equalTo(Status.NEW)
|
||||
.findStream()) {
|
||||
stream
|
||||
.map(...)
|
||||
.forEach(...);
|
||||
}
|
||||
```
|
||||
|
||||
For processing large results one bean at a time, `findEach()` is often the
|
||||
simplest choice because it closes the underlying resources automatically.
|
||||
|
||||
---
|
||||
|
||||
## 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();
|
||||
```
|
||||
|
||||
### Optional predicates - prefer conditional helpers over `if` blocks
|
||||
|
||||
When a filter is driven by a nullable/optional parameter, use the built-in
|
||||
conditional helpers instead of wrapping predicates in `if` blocks. The query
|
||||
stays fluent and reads top-to-bottom, and no predicate is added when the value
|
||||
is absent.
|
||||
|
||||
| Helper | Adds predicate when | Resulting SQL |
|
||||
|--------|---------------------|---------------|
|
||||
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
|
||||
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
|
||||
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
|
||||
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
|
||||
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
|
||||
|
||||
```java
|
||||
// Instead of building the query with if blocks:
|
||||
QCustomer q = new QCustomer();
|
||||
if (name != null && !name.isBlank()) {
|
||||
q.name.eq(name.trim());
|
||||
}
|
||||
if (status != null) {
|
||||
q.status.eq(status);
|
||||
}
|
||||
List<Customer> customers = q.findList();
|
||||
|
||||
// Prefer the conditional helpers:
|
||||
List<Customer> customers = new QCustomer()
|
||||
.name.eqIfNotBlank(name)
|
||||
.status.eqIfPresent(status)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Use `eqOrNull(v)` when a null column value should also match - for example an
|
||||
"any environment" row stored with `env_id is null` should surface under any env
|
||||
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
|
||||
|
||||
```java
|
||||
List<CaptureRequest> rows = new QCaptureRequest()
|
||||
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
|
||||
.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
|
||||
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
|
||||
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
|
||||
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
|
||||
null column"
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
+5
-7
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.2.1</version>
|
||||
<version>18.0.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -70,15 +70,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<optional>true</optional>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-core</artifactId>
|
||||
<version>${avaje-json-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope for JsonNode support -->
|
||||
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
|
||||
@@ -155,10 +155,4 @@ public abstract class BeanFinder<I,T> {
|
||||
return db().findNative(type, nativeSql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query using the ORM query language.
|
||||
*/
|
||||
protected Query<T> query(String ormQuery) {
|
||||
return db().createQuery(type, ormQuery);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,21 +741,6 @@ public final class DB {
|
||||
return getDefault().createUpdate(beanType, ormUpdate);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a named query.
|
||||
* <p>
|
||||
* For RawSql the named query is expected to be in ebean.xml.
|
||||
*
|
||||
* @param beanType The type of entity bean
|
||||
* @param namedQuery The name of the query
|
||||
* @param <T> The type of entity bean
|
||||
* @return The query
|
||||
*/
|
||||
public static <T> Query<T> createNamedQuery(Class<T> beanType, String namedQuery) {
|
||||
return getDefault().createNamedQuery(beanType, namedQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for a type of entity bean.
|
||||
* <p>
|
||||
@@ -775,43 +760,6 @@ public final class DB {
|
||||
return getDefault().createQuery(beanType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Ebean query language statement returning the query which can then
|
||||
* be modified (add expressions, change order by clause, change maxRows, change
|
||||
* fetch and select paths etc).
|
||||
* <p>
|
||||
* <h3>Example</h3>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // Find order additionally fetching the customer, details and details.product name.
|
||||
*
|
||||
* String eql = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
|
||||
*
|
||||
* Query<Order> query = DB.createQuery(Order.class, eql);
|
||||
* query.setParameter("orderId", 2);
|
||||
*
|
||||
* Order order = query.findOne();
|
||||
*
|
||||
* // This is the same as:
|
||||
*
|
||||
* Order order = DB.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .fetch("details")
|
||||
* .fetch("detail.product", "name")
|
||||
* .setId(2)
|
||||
* .findOne();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param beanType The type of bean to fetch
|
||||
* @param eql The Ebean query
|
||||
* @param <T> The type of the entity bean
|
||||
* @return The query with expressions defined as per the parsed query statement
|
||||
*/
|
||||
public static <T> Query<T> createQuery(Class<T> beanType, String eql) {
|
||||
return getDefault().createQuery(beanType, eql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a query for a type of entity bean.
|
||||
* <p>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -243,18 +247,6 @@ public interface Database {
|
||||
*/
|
||||
<T> UpdateQuery<T> update(Class<T> beanType);
|
||||
|
||||
/**
|
||||
* Create a named query.
|
||||
* <p>
|
||||
* For RawSql the named query is expected to be in ebean.xml.
|
||||
*
|
||||
* @param beanType The type of entity bean
|
||||
* @param namedQuery The name of the query
|
||||
* @param <T> The type of entity bean
|
||||
* @return The query
|
||||
*/
|
||||
<T> Query<T> createNamedQuery(Class<T> beanType, String namedQuery);
|
||||
|
||||
/**
|
||||
* Create a query for an entity bean and synonym for {@link #find(Class)}.
|
||||
*
|
||||
@@ -262,41 +254,6 @@ public interface Database {
|
||||
*/
|
||||
<T> Query<T> createQuery(Class<T> beanType);
|
||||
|
||||
/**
|
||||
* Parse the Ebean query language statement returning the query which can then
|
||||
* be modified (add expressions, change order by clause, change maxRows, change
|
||||
* fetch and select paths etc).
|
||||
* <p>
|
||||
* <h3>Example</h3>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // Find order additionally fetching the customer, details and details.product name.
|
||||
*
|
||||
* String ormQuery = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
|
||||
*
|
||||
* Query<Order> query = DB.createQuery(Order.class, ormQuery);
|
||||
* query.setParameter("orderId", 2);
|
||||
*
|
||||
* Order order = query.findOne();
|
||||
*
|
||||
* // This is the same as:
|
||||
*
|
||||
* Order order = DB.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .fetch("details")
|
||||
* .fetch("detail.product", "name")
|
||||
* .setId(2)
|
||||
* .findOne();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param beanType The type of bean to fetch
|
||||
* @param ormQuery The Ebean ORM query
|
||||
* @param <T> The type of the entity bean
|
||||
* @return The query with expressions defined as per the parsed query statement
|
||||
*/
|
||||
<T> Query<T> createQuery(Class<T> beanType, String ormQuery);
|
||||
|
||||
/**
|
||||
* Create a query for a type of entity bean.
|
||||
* <p>
|
||||
@@ -460,18 +417,6 @@ public interface Database {
|
||||
*/
|
||||
<T> DtoQuery<T> findDto(Class<T> dtoType, String sql);
|
||||
|
||||
/**
|
||||
* Create a named Query for DTO beans.
|
||||
* <p>
|
||||
* DTO beans are just normal bean like classes with public constructor(s) and setters.
|
||||
* They do not need to be registered with DB before use.
|
||||
*
|
||||
* @param dtoType The type of the DTO bean the rows will be mapped into.
|
||||
* @param namedQuery The name of the query
|
||||
* @param <T> The type of the DTO bean.
|
||||
*/
|
||||
<T> DtoQuery<T> createNamedDtoQuery(Class<T> dtoType, String namedQuery);
|
||||
|
||||
/**
|
||||
* Look to execute a native sql query that does not return beans but instead
|
||||
* returns SqlRow or direct access to ResultSet.
|
||||
@@ -1374,109 +1319,6 @@ public interface Database {
|
||||
*/
|
||||
ScriptRunner script();
|
||||
|
||||
/**
|
||||
* Return the Document store.
|
||||
*/
|
||||
DocumentStore docStore();
|
||||
|
||||
/**
|
||||
* Publish a single bean given its type and id returning the resulting live bean.
|
||||
* <p>
|
||||
* The values are published from the draft to the live bean.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param beanType the type of the entity bean
|
||||
* @param id the id of the entity bean
|
||||
* @param transaction the transaction the publish process should use (can be null)
|
||||
*/
|
||||
@Nullable
|
||||
<T> T publish(Class<T> beanType, Object id, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Publish a single bean given its type and id returning the resulting live bean.
|
||||
* This will use the current transaction or create one if required.
|
||||
* <p>
|
||||
* The values are published from the draft to the live bean.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param beanType the type of the entity bean
|
||||
* @param id the id of the entity bean
|
||||
*/
|
||||
@Nullable
|
||||
<T> T publish(Class<T> beanType, Object id);
|
||||
|
||||
/**
|
||||
* Publish the beans that match the query returning the resulting published beans.
|
||||
* <p>
|
||||
* The values are published from the draft beans to the live beans.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param query the query used to select the draft beans to publish
|
||||
* @param transaction the transaction the publish process should use (can be null)
|
||||
*/
|
||||
<T> List<T> publish(Query<T> query, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Publish the beans that match the query returning the resulting published beans.
|
||||
* This will use the current transaction or create one if required.
|
||||
* <p>
|
||||
* The values are published from the draft beans to the live beans.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param query the query used to select the draft beans to publish
|
||||
*/
|
||||
<T> List<T> publish(Query<T> query);
|
||||
|
||||
/**
|
||||
* Restore the draft bean back to the live state.
|
||||
* <p>
|
||||
* The values from the live beans are set back to the draft bean and the
|
||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param beanType the type of the entity bean
|
||||
* @param id the id of the entity bean to restore
|
||||
* @param transaction the transaction the restore process should use (can be null)
|
||||
*/
|
||||
@Nullable
|
||||
<T> T draftRestore(Class<T> beanType, Object id, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Restore the draft bean back to the live state.
|
||||
* <p>
|
||||
* The values from the live beans are set back to the draft bean and the
|
||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param beanType the type of the entity bean
|
||||
* @param id the id of the entity bean to restore
|
||||
*/
|
||||
@Nullable
|
||||
<T> T draftRestore(Class<T> beanType, Object id);
|
||||
|
||||
/**
|
||||
* Restore the draft beans matching the query back to the live state.
|
||||
* <p>
|
||||
* The values from the live beans are set back to the draft bean and the
|
||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param query the query used to select the draft beans to restore
|
||||
* @param transaction the transaction the restore process should use (can be null)
|
||||
*/
|
||||
<T> List<T> draftRestore(Query<T> query, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Restore the draft beans matching the query back to the live state.
|
||||
* <p>
|
||||
* The values from the live beans are set back to the draft bean and the
|
||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
||||
*
|
||||
* @param <T> the type of the entity bean
|
||||
* @param query the query used to select the draft beans to restore
|
||||
*/
|
||||
<T> List<T> draftRestore(Query<T> query);
|
||||
|
||||
/**
|
||||
* Returns the set of properties/paths that are unknown (do not map to known properties or paths).
|
||||
* <p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.annotation.*;
|
||||
import io.ebean.cache.ServerCachePlugin;
|
||||
import io.ebean.config.*;
|
||||
@@ -13,8 +13,6 @@ import io.ebean.event.*;
|
||||
import io.ebean.event.changelog.ChangeLogListener;
|
||||
import io.ebean.event.changelog.ChangeLogPrepare;
|
||||
import io.ebean.event.changelog.ChangeLogRegister;
|
||||
import io.ebean.event.readaudit.ReadAuditLogger;
|
||||
import io.ebean.event.readaudit.ReadAuditPrepare;
|
||||
import jakarta.persistence.EnumType;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
@@ -360,18 +358,18 @@ public interface DatabaseBuilder {
|
||||
DatabaseBuilder putServiceObject(Object configObject);
|
||||
|
||||
/**
|
||||
* Set the Jackson JsonFactory to use.
|
||||
* Set the JsonStream to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
|
||||
return setJsonFactory(jsonFactory);
|
||||
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
|
||||
return setJsonStream(jsonStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
|
||||
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
|
||||
*/
|
||||
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
|
||||
DatabaseBuilder setJsonStream(JsonStream jsonStream);
|
||||
|
||||
/**
|
||||
* Set the JSON format to use for DateTime types.
|
||||
@@ -691,51 +689,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setChangeLogAsync(boolean changeLogAsync);
|
||||
|
||||
/**
|
||||
* Set the ReadAuditLogger to use. If not set the default implementation is used
|
||||
* which logs the read events in JSON format to a standard named SLF4J logger
|
||||
* (which can be configured in say logback to log to a separate log file).
|
||||
*/
|
||||
default DatabaseBuilder readAuditLogger(ReadAuditLogger readAuditLogger) {
|
||||
return setReadAuditLogger(readAuditLogger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #readAuditLogger(ReadAuditLogger)}.
|
||||
*/
|
||||
@Deprecated
|
||||
DatabaseBuilder setReadAuditLogger(ReadAuditLogger readAuditLogger);
|
||||
|
||||
/**
|
||||
* Set the ReadAuditPrepare to use.
|
||||
* <p>
|
||||
* It is expected that an implementation is used that read user context information
|
||||
* (user id, user ip address etc) and sets it on the ReadEvent bean before it is sent
|
||||
* to the ReadAuditLogger.
|
||||
*/
|
||||
default DatabaseBuilder readAuditPrepare(ReadAuditPrepare readAuditPrepare) {
|
||||
return setReadAuditPrepare(readAuditPrepare);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #readAuditPrepare(ReadAuditPrepare)}.
|
||||
*/
|
||||
@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.
|
||||
@@ -1013,16 +966,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setAllQuotedIdentifiers(boolean allQuotedIdentifiers);
|
||||
|
||||
/**
|
||||
* Set to true if this Database is Document store only instance (has no JDBC DB).
|
||||
*/
|
||||
DatabaseBuilder setDocStoreOnly(boolean docStoreOnly);
|
||||
|
||||
/**
|
||||
* Set the configuration for the ElasticSearch integration.
|
||||
*/
|
||||
DatabaseBuilder setDocStoreConfig(DocStoreConfig docStoreConfig);
|
||||
|
||||
/**
|
||||
* Set the constraint naming convention used in DDL generation.
|
||||
*/
|
||||
@@ -2234,7 +2177,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 +2195,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
|
||||
@@ -2267,11 +2210,11 @@ public interface DatabaseBuilder {
|
||||
boolean isAutoLoadModuleInfo();
|
||||
|
||||
/**
|
||||
* Return the Jackson JsonFactory to use.
|
||||
* Return the JsonStream to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
JsonFactory getJsonFactory();
|
||||
JsonStream getJsonStream();
|
||||
|
||||
/**
|
||||
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
|
||||
@@ -2475,26 +2418,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isChangeLogAsync();
|
||||
|
||||
/**
|
||||
* Return the ReadAuditLogger to use.
|
||||
*/
|
||||
ReadAuditLogger getReadAuditLogger();
|
||||
|
||||
/**
|
||||
* Return the ReadAuditPrepare to use.
|
||||
*/
|
||||
ReadAuditPrepare getReadAuditPrepare();
|
||||
|
||||
/**
|
||||
* Return the tenancy catalog provider.
|
||||
*/
|
||||
TenantCatalogProvider getTenantCatalogProvider();
|
||||
|
||||
/**
|
||||
* Return the configuration for profiling.
|
||||
*/
|
||||
ProfilingConfig getProfilingConfig();
|
||||
|
||||
/**
|
||||
* Return the DB schema to use.
|
||||
*/
|
||||
@@ -2628,16 +2556,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isAllQuotedIdentifiers();
|
||||
|
||||
/**
|
||||
* Return true if this Database is a Document store only instance (has no JDBC DB).
|
||||
*/
|
||||
boolean isDocStoreOnly();
|
||||
|
||||
/**
|
||||
* Return the configuration for the ElasticSearch integration.
|
||||
*/
|
||||
DocStoreConfig getDocStoreConfig();
|
||||
|
||||
/**
|
||||
* Return the constraint naming convention used in DDL generation.
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Bean holding the details to update the document store.
|
||||
*/
|
||||
public final class DocStoreQueueEntry {
|
||||
|
||||
/**
|
||||
* Action to either update or delete a document from the index.
|
||||
*/
|
||||
public enum Action {
|
||||
|
||||
/**
|
||||
* Action is to update a document in the doc store.
|
||||
*/
|
||||
INDEX(1),
|
||||
|
||||
/**
|
||||
* Action is to delete a document from the doc store..
|
||||
*/
|
||||
DELETE(2),
|
||||
|
||||
/**
|
||||
* An update is required based on a change to a nested/embedded object at a given path.
|
||||
*/
|
||||
NESTED(3);
|
||||
|
||||
int value;
|
||||
|
||||
Action(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value associated with this action type.
|
||||
*/
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private final Action type;
|
||||
|
||||
private final String queueId;
|
||||
|
||||
private final String path;
|
||||
|
||||
private final Object beanId;
|
||||
|
||||
/**
|
||||
* Construct for an INDEX or DELETE action.
|
||||
*/
|
||||
public DocStoreQueueEntry(Action type, String queueId, Object beanId) {
|
||||
this(type, queueId, null, beanId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct for an NESTED/embedded path invalidation action.
|
||||
*/
|
||||
public DocStoreQueueEntry(Action type, String queueId, String path, Object beanId) {
|
||||
this.type = type;
|
||||
this.queueId = queueId;
|
||||
this.path = path;
|
||||
this.beanId = beanId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event type.
|
||||
*/
|
||||
public Action getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the associate queueId.
|
||||
*/
|
||||
public String getQueueId() {
|
||||
return queueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path if this is a nested update.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean id (which matches the document id).
|
||||
*/
|
||||
public Object getBeanId() {
|
||||
return beanId;
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.docstore.DocQueryContext;
|
||||
import io.ebean.docstore.RawDoc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* Document storage operations.
|
||||
*/
|
||||
@NullMarked
|
||||
public interface DocumentStore {
|
||||
|
||||
/**
|
||||
* Update the associated document store using the result of the query.
|
||||
* <p>
|
||||
* This will execute the query against the database creating a document for each
|
||||
* bean graph and sending this to the document store.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that the select and fetch paths of the query is set for you to match the
|
||||
* document structure needed based on <code>@DocStore</code> and <code>@DocStoreEmbedded</code>
|
||||
* so what this query requires is the predicates only.
|
||||
* </p>
|
||||
* <p>
|
||||
* This query will be executed using findEach so it is safe to use a query
|
||||
* that will fetch a lot of beans. The default bulkBatchSize is used.
|
||||
* </p>
|
||||
*
|
||||
* @param query The query that selects object to send to the document store.
|
||||
*/
|
||||
<T> void indexByQuery(Query<T> query);
|
||||
|
||||
/**
|
||||
* Update the associated document store index using the result of the query additionally specifying a
|
||||
* bulkBatchSize to use for sending the messages to ElasticSearch.
|
||||
*
|
||||
* @param query The query that selects object to send to the document store.
|
||||
* @param bulkBatchSize The batch size to use when bulk sending to the document store.
|
||||
*/
|
||||
<T> void indexByQuery(Query<T> query, int bulkBatchSize);
|
||||
|
||||
/**
|
||||
* Update the document store for all beans of this type.
|
||||
* <p>
|
||||
* This is the same as indexByQuery where the query has no predicates and so fetches all rows.
|
||||
* </p>
|
||||
*/
|
||||
void indexAll(Class<?> beanType);
|
||||
|
||||
/**
|
||||
* Return the bean by fetching it's content from the document store.
|
||||
* If the document is not found null is returned.
|
||||
* <p>
|
||||
* Typically this is called indirectly by findOne() on the query.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* Customer customer =
|
||||
* database.find(Customer.class)
|
||||
* .setUseDocStore(true)
|
||||
* .setId(42)
|
||||
* .findOne();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@Nullable
|
||||
<T> T find(DocQueryContext<T> request);
|
||||
|
||||
/**
|
||||
* Execute the find list query. This request is prepared to execute secondary queries.
|
||||
* <p>
|
||||
* Typically this is called indirectly by findList() on the query that has setUseDocStore(true).
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* List<Customer> newCustomers =
|
||||
* database.find(Customer.class)
|
||||
* .setUseDocStore(true)
|
||||
* .where().eq("status, Customer.Status.NEW)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
<T> List<T> findList(DocQueryContext<T> request);
|
||||
|
||||
/**
|
||||
* Execute the query against the document store returning the paged list.
|
||||
* <p>
|
||||
* The query should have <code>firstRow</code> or <code>maxRows</code> set prior to calling this method.
|
||||
* </p>
|
||||
* <p>
|
||||
* Typically this is called indirectly by findPagedList() on the query that has setUseDocStore(true).
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<Customer> newCustomers =
|
||||
* database.find(Customer.class)
|
||||
* .setUseDocStore(true)
|
||||
* .where().eq("status, Customer.Status.NEW)
|
||||
* .setMaxRows(50)
|
||||
* .findPagedList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
<T> PagedList<T> findPagedList(DocQueryContext<T> request);
|
||||
|
||||
/**
|
||||
* Execute the query against the document store with the expectation of a large set of results
|
||||
* that are processed in a scrolling resultSet fashion.
|
||||
* <p>
|
||||
* For example, with the ElasticSearch doc store this uses SCROLL.
|
||||
* </p>
|
||||
* <p>
|
||||
* Typically this is called indirectly by findEach() on the query that has setUseDocStore(true).
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* database.find(Order.class)
|
||||
* .setUseDocStore(true)
|
||||
* .where()... // perhaps add predicates
|
||||
* .findEach((Order order) -> {
|
||||
* // process the bean ...
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
<T> void findEach(DocQueryContext<T> query, Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query against the document store with the expectation of a large set of results
|
||||
* that are processed in a scrolling resultSet fashion.
|
||||
* <p>
|
||||
* Unlike findEach() this provides the opportunity to stop iterating through the large query.
|
||||
* </p>
|
||||
* <p>
|
||||
* For example, with the ElasticSearch doc store this uses SCROLL.
|
||||
* </p>
|
||||
* <p>
|
||||
* Typically this is called indirectly by findEachWhile() on the query that has setUseDocStore(true).
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* database.find(Order.class)
|
||||
* .setUseDocStore(true)
|
||||
* .where()... // perhaps add predicates
|
||||
* .findEachWhile(new Predicate<Order>() {
|
||||
* @Override
|
||||
* public void accept(Order bean) {
|
||||
* // process the bean
|
||||
*
|
||||
* // return true to continue, false to stop
|
||||
* // boolean shouldContinue = ...
|
||||
* return shouldContinue;
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
<T> void findEachWhile(DocQueryContext<T> query, Predicate<T> consumer);
|
||||
|
||||
/**
|
||||
* Find each processing raw documents.
|
||||
*
|
||||
* @param indexNameType The full index name and type
|
||||
* @param rawQuery The query to execute
|
||||
* @param consumer Consumer to process each document
|
||||
*/
|
||||
void findEach(String indexNameType, String rawQuery, Consumer<RawDoc> consumer);
|
||||
|
||||
/**
|
||||
* Find each processing raw documents stopping when the predicate returns false.
|
||||
*
|
||||
* @param indexNameType The full index name and type
|
||||
* @param rawQuery The query to execute
|
||||
* @param consumer Consumer to process each document until false is returned
|
||||
*/
|
||||
void findEachWhile(String indexNameType, String rawQuery, Predicate<RawDoc> consumer);
|
||||
|
||||
/**
|
||||
* Process the queue entries sending updates to the document store or queuing them for later processing.
|
||||
*/
|
||||
long process(List<DocStoreQueueEntry> queueEntries) throws IOException;
|
||||
|
||||
/**
|
||||
* Drop the index from the document store (similar to DDL drop table).
|
||||
* <pre>{@code
|
||||
*
|
||||
* DocumentStore documentStore = database.docStore();
|
||||
*
|
||||
* documentStore.dropIndex("product_copy");
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
void dropIndex(String indexName);
|
||||
|
||||
/**
|
||||
* Create an index given a mapping file as a resource in the classPath (similar to DDL create table).
|
||||
* <pre>{@code
|
||||
*
|
||||
* DocumentStore documentStore = database.docStore();
|
||||
*
|
||||
* // uses product_copy.mapping.json resource
|
||||
* // ... to define mappings for the index
|
||||
*
|
||||
* documentStore.createIndex("product_copy", null);
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param indexName the name of the new index
|
||||
* @param alias the alias of the index
|
||||
*/
|
||||
void createIndex(String indexName, String alias);
|
||||
|
||||
/**
|
||||
* Modify the settings on an index.
|
||||
* <p>
|
||||
* For example, this can be used be used to set elasticSearch refresh_interval
|
||||
* on an index before a bulk update.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // refresh_interval -1 ... disable refresh while bulk loading
|
||||
*
|
||||
* Map<String,Object> settings = new LinkedHashMap<>();
|
||||
* settings.put("refresh_interval", "-1");
|
||||
*
|
||||
* documentStore.indexSettings("product", settings);
|
||||
*
|
||||
* }</pre>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // refresh_interval 1s ... restore after bulk loading
|
||||
*
|
||||
* Map<String,Object> settings = new LinkedHashMap<>();
|
||||
* settings.put("refresh_interval", "1s");
|
||||
*
|
||||
* documentStore.indexSettings("product", settings);
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param indexName the name of the index to update settings on
|
||||
* @param settings the settings to set on the index
|
||||
*/
|
||||
void indexSettings(String indexName, Map<String, Object> settings);
|
||||
|
||||
/**
|
||||
* Copy the index to a new index.
|
||||
* <p>
|
||||
* This copy process does not use the database but instead will copy from the source index to a destination index.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* long copyCount = documentStore.copyIndex(Product.class, "product_copy");
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param beanType The bean type of the source index
|
||||
* @param newIndex The name of the index to copy to
|
||||
* @return the number of documents copied to the new index
|
||||
*/
|
||||
long copyIndex(Class<?> beanType, String newIndex);
|
||||
|
||||
/**
|
||||
* Copy entries from an index to a new index but limiting to documents that have been
|
||||
* modified since the sinceEpochMillis time.
|
||||
* <p>
|
||||
* To support this the document needs to have a <code>@WhenModified</code> property.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* long copyCount = documentStore.copyIndex(Product.class, "product_copy", sinceMillis);
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param beanType The bean type of the source index
|
||||
* @param newIndex The name of the index to copy to
|
||||
* @return the number of documents copied to the new index
|
||||
*/
|
||||
long copyIndex(Class<?> beanType, String newIndex, long sinceEpochMillis);
|
||||
|
||||
/**
|
||||
* Copy from a source index to a new index taking only the documents
|
||||
* matching the given query.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // predicates to select the source documents to copy
|
||||
* Query<Product> query = database.find(Product.class)
|
||||
* .where()
|
||||
* .ge("whenModified", new Timestamp(since))
|
||||
* .ge("name", "A")
|
||||
* .lt("name", "D")
|
||||
* .query();
|
||||
*
|
||||
* // copy from the source index to "product_copy" index
|
||||
* long copyCount = documentStore.copyIndex(query, "product_copy", 1000);
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param query The query to select the source documents to copy
|
||||
* @param newIndex The target index to copy the documents to
|
||||
* @param bulkBatchSize The ElasticSearch bulk batch size, if 0 uses the default.
|
||||
* @return The number of documents copied to the new index.
|
||||
*/
|
||||
long copyIndex(Query<?> query, String newIndex, int bulkBatchSize);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.search.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -625,31 +623,6 @@ public interface ExpressionFactory {
|
||||
*/
|
||||
Expression raw(String raw);
|
||||
|
||||
/**
|
||||
* Create a Text Match expression (currently doc store/Elastic only).
|
||||
*/
|
||||
Expression textMatch(String propertyName, String search, Match options);
|
||||
|
||||
/**
|
||||
* Create a Text Multi match expression (currently doc store/Elastic only).
|
||||
*/
|
||||
Expression textMultiMatch(String query, MultiMatch options);
|
||||
|
||||
/**
|
||||
* Create a text simple query expression (currently doc store/Elastic only).
|
||||
*/
|
||||
Expression textSimple(String search, TextSimple options);
|
||||
|
||||
/**
|
||||
* Create a text query string expression (currently doc store/Elastic only).
|
||||
*/
|
||||
Expression textQueryString(String search, TextQueryString options);
|
||||
|
||||
/**
|
||||
* Create a text common terms expression (currently doc store/Elastic only).
|
||||
*/
|
||||
Expression textCommonTerms(String search, TextCommonTerms options);
|
||||
|
||||
/**
|
||||
* And - join two expressions with a logical and.
|
||||
*/
|
||||
@@ -693,12 +666,4 @@ public interface ExpressionFactory {
|
||||
*/
|
||||
<T> Junction<T> junction(Junction.Type type, Query<T> query, ExpressionList<T> parent);
|
||||
|
||||
/**
|
||||
* Add the expressions to the given expression list.
|
||||
*
|
||||
* @param where The expression list to add the expressions to
|
||||
* @param expressions The expressions that are parsed
|
||||
* @param params Bind parameters to match ? or ?1 bind positions.
|
||||
*/
|
||||
<T> void where(ExpressionList<T> where, String expressions, Object[] params);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.ebean.search.*;
|
||||
|
||||
import jakarta.persistence.NonUniqueResultException;
|
||||
import java.sql.Connection;
|
||||
@@ -90,11 +89,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Query<T> asOf(Timestamp asOf);
|
||||
|
||||
/**
|
||||
* Execute the query against the draft set of tables.
|
||||
*/
|
||||
Query<T> asDraft();
|
||||
|
||||
/**
|
||||
* Convert the query to a DTO bean query.
|
||||
* <p>
|
||||
@@ -471,28 +465,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> filterMany(String manyProperty);
|
||||
|
||||
/**
|
||||
* @deprecated for removal - migrate to {@link #filterManyRaw(String, String, Object...)}.
|
||||
* <p>
|
||||
* Add filter expressions to the many property.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* DB.find(Customer.class)
|
||||
* .where()
|
||||
* .eq("name", "Rob")
|
||||
* .filterMany("orders", "status = ?", Status.NEW)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param manyProperty The many property
|
||||
* @param expressions Filter expressions with and, or and ? or ?1 type bind parameters
|
||||
* @param params Bind parameters used in the expressions
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
ExpressionList<T> filterMany(String manyProperty, String expressions, Object... params);
|
||||
|
||||
/**
|
||||
* Add filter expressions for the many path. The expressions can include SQL functions if
|
||||
* desired and the property names are translated to column names.
|
||||
@@ -544,19 +516,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Query<T> setDistinct(boolean distinct);
|
||||
|
||||
/**
|
||||
* Set the index(es) to search for a document store which uses partitions.
|
||||
* <p>
|
||||
* For example, when executing a query against ElasticSearch with daily indexes we can
|
||||
* explicitly specify the indexes to search against.
|
||||
* </p>
|
||||
*
|
||||
* @param indexName The index or indexes to search against
|
||||
* @return This query
|
||||
* @see Query#setDocIndexName(String)
|
||||
*/
|
||||
Query<T> setDocIndexName(String indexName);
|
||||
|
||||
/**
|
||||
* Set the first row to fetch.
|
||||
*
|
||||
@@ -640,14 +599,6 @@ public interface ExpressionList<T> {
|
||||
return setUseQueryCache(enabled ? CacheMode.ON : CacheMode.OFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if this query should execute against the doc store.
|
||||
* <p>
|
||||
* When setting this you may also consider disabling lazy loading.
|
||||
* </p>
|
||||
*/
|
||||
Query<T> setUseDocStore(boolean useDocsStore);
|
||||
|
||||
/**
|
||||
* Set true if you want to disable lazy loading.
|
||||
* <p>
|
||||
@@ -656,16 +607,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Query<T> setDisableLazyLoading(boolean disableLazyLoading);
|
||||
|
||||
/**
|
||||
* Disable read auditing for this query.
|
||||
* <p>
|
||||
* This is intended to be used when the query is not a user initiated query and instead
|
||||
* part of the internal processing in an application to load a cache or document store etc.
|
||||
* In these cases we don't want the query to be part of read auditing.
|
||||
* </p>
|
||||
*/
|
||||
Query<T> setDisableReadAuditing();
|
||||
|
||||
/**
|
||||
* Set a label on the query (to help identify query execution statistics).
|
||||
*/
|
||||
@@ -685,14 +626,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> where();
|
||||
|
||||
/**
|
||||
* Add the expressions to this expression list.
|
||||
*
|
||||
* @param expressions The expressions that are parsed and added to this expression list
|
||||
* @param params Bind parameters to match ? or ?1 bind positions.
|
||||
*/
|
||||
ExpressionList<T> where(String expressions, Object... params);
|
||||
|
||||
/**
|
||||
* Path exists - for the given path in a JSON document.
|
||||
* <pre>{@code
|
||||
@@ -1073,6 +1006,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 +1021,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 +1072,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.
|
||||
*/
|
||||
@@ -1587,47 +1568,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> rawOrEmpty(String raw, Collection<?> values);
|
||||
|
||||
/**
|
||||
* Add a match expression.
|
||||
*
|
||||
* @param propertyName The property name for the match
|
||||
* @param search The search value
|
||||
*/
|
||||
ExpressionList<T> match(String propertyName, String search);
|
||||
|
||||
/**
|
||||
* Add a match expression with options.
|
||||
*
|
||||
* @param propertyName The property name for the match
|
||||
* @param search The search value
|
||||
*/
|
||||
ExpressionList<T> match(String propertyName, String search, Match options);
|
||||
|
||||
/**
|
||||
* Add a multi-match expression.
|
||||
*/
|
||||
ExpressionList<T> multiMatch(String search, String... properties);
|
||||
|
||||
/**
|
||||
* Add a multi-match expression using options.
|
||||
*/
|
||||
ExpressionList<T> multiMatch(String search, MultiMatch options);
|
||||
|
||||
/**
|
||||
* Add a simple query string expression.
|
||||
*/
|
||||
ExpressionList<T> textSimple(String search, TextSimple options);
|
||||
|
||||
/**
|
||||
* Add a query string expression.
|
||||
*/
|
||||
ExpressionList<T> textQueryString(String search, TextQueryString options);
|
||||
|
||||
/**
|
||||
* Add common terms expression.
|
||||
*/
|
||||
ExpressionList<T> textCommonTerms(String search, TextCommonTerms options);
|
||||
|
||||
/**
|
||||
* And - join two expressions with a logical and.
|
||||
*/
|
||||
@@ -1770,42 +1710,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Junction<T> disjunction();
|
||||
|
||||
/**
|
||||
* Start a list of expressions that will be joined by MUST.
|
||||
* <p>
|
||||
* This automatically makes the query a useDocStore(true) query that
|
||||
* will execute against the document store (ElasticSearch etc).
|
||||
* </p>
|
||||
* <p>
|
||||
* This is logically similar to and().
|
||||
* </p>
|
||||
*/
|
||||
Junction<T> must();
|
||||
|
||||
/**
|
||||
* Start a list of expressions that will be joined by SHOULD.
|
||||
* <p>
|
||||
* This automatically makes the query a useDocStore(true) query that
|
||||
* will execute against the document store (ElasticSearch etc).
|
||||
* </p>
|
||||
* <p>
|
||||
* This is logically similar to or().
|
||||
* </p>
|
||||
*/
|
||||
Junction<T> should();
|
||||
|
||||
/**
|
||||
* Start a list of expressions that will be joined by MUST NOT.
|
||||
* <p>
|
||||
* This automatically makes the query a useDocStore(true) query that
|
||||
* will execute against the document store (ElasticSearch etc).
|
||||
* </p>
|
||||
* <p>
|
||||
* This is logically similar to not().
|
||||
* </p>
|
||||
*/
|
||||
Junction<T> mustNot();
|
||||
|
||||
/**
|
||||
* End a junction returning the parent expression list.
|
||||
* <p>
|
||||
|
||||
@@ -213,11 +213,4 @@ public class Finder<I, T> {
|
||||
return db().findNative(type, nativeSql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a query using the ORM query language.
|
||||
*/
|
||||
public Query<T> query(String ormQuery) {
|
||||
return db().createQuery(type, ormQuery);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -346,23 +346,6 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
*/
|
||||
ExpressionList<T> where();
|
||||
|
||||
/**
|
||||
* Add Full text search expressions for Document store queries.
|
||||
* <p>
|
||||
* This is currently ElasticSearch only and provides the full text
|
||||
* expressions such as Match and Multi-Match.
|
||||
* <p>
|
||||
* This automatically makes this query a "Doc Store" query and will execute
|
||||
* against the document store (ElasticSearch).
|
||||
* <p>
|
||||
* Expressions added here are added to the "query" section of an ElasticSearch
|
||||
* query rather than the "filter" section.
|
||||
* <p>
|
||||
* Expressions added to the where() are added to the "filter" section of an
|
||||
* ElasticSearch query.
|
||||
*/
|
||||
ExpressionList<T> text();
|
||||
|
||||
/**
|
||||
* This applies a filter on the 'many' property list rather than the root
|
||||
* level objects.
|
||||
@@ -457,11 +440,6 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
@Nullable
|
||||
LockType getForUpdateLockType();
|
||||
|
||||
/**
|
||||
* Returns the inherit type. This is normally the same as getBeanType() returns as long as no other type is set.
|
||||
*/
|
||||
Class<? extends T> getInheritType();
|
||||
|
||||
/**
|
||||
* Return the type of query being executed.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
@@ -54,11 +64,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF asOf(Timestamp asOf);
|
||||
|
||||
/**
|
||||
* Execute the query against the draft set of tables.
|
||||
*/
|
||||
SELF asDraft();
|
||||
|
||||
/**
|
||||
* Convert the query to a DTO bean query.
|
||||
* <p>
|
||||
@@ -115,6 +120,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.
|
||||
*/
|
||||
@@ -238,44 +248,7 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF setHint(String hint);
|
||||
|
||||
/**
|
||||
* Set the index(es) to search for a document store which uses partitions.
|
||||
* <p>
|
||||
* For example, when executing a query against ElasticSearch with daily indexes we can
|
||||
* explicitly specify the indexes to search against.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // explicitly specify the indexes to search
|
||||
* query.setDocIndexName("logstash-2016.11.5,logstash-2016.11.6")
|
||||
*
|
||||
* // search today's index
|
||||
* query.setDocIndexName("$today")
|
||||
*
|
||||
* // search the last 3 days
|
||||
* query.setDocIndexName("$last-3")
|
||||
*
|
||||
* }</pre>
|
||||
* <p>
|
||||
* If the indexName is specified with ${daily} e.g. "logstash-${daily}" ... then we can use
|
||||
* $today and $last-x as the search docIndexName like the examples below.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // search today's index
|
||||
* query.setDocIndexName("$today")
|
||||
*
|
||||
* // search the last 3 days
|
||||
* query.setDocIndexName("$last-3")
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param indexName The index or indexes to search against
|
||||
* @return This query
|
||||
*/
|
||||
SELF setDocIndexName(String indexName);
|
||||
|
||||
/**
|
||||
/**
|
||||
* Execute the query including soft deleted rows.
|
||||
* <p>
|
||||
* This means that Ebean will not add any predicates to the query for filtering out
|
||||
@@ -285,15 +258,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF setIncludeSoftDeletes();
|
||||
|
||||
/**
|
||||
* Disable read auditing for this query.
|
||||
* <p>
|
||||
* This is intended to be used when the query is not a user initiated query and instead
|
||||
* part of the internal processing in an application to load a cache or document store etc.
|
||||
* In these cases we don't want the query to be part of read auditing.
|
||||
*/
|
||||
SELF setDisableReadAuditing();
|
||||
|
||||
/**
|
||||
* Set true if you want to disable lazy loading.
|
||||
* <p>
|
||||
@@ -306,20 +270,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF setDistinct(boolean distinct);
|
||||
|
||||
/**
|
||||
* Restrict the query to only return subtypes of the given inherit type.
|
||||
* <pre>{@code
|
||||
*
|
||||
* List<Animal> animals =
|
||||
* new QAnimal()
|
||||
* .name.startsWith("Fluffy")
|
||||
* .setInheritType(Cat.class)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
SELF setInheritType(Class<? extends T> type);
|
||||
|
||||
/**
|
||||
* Set the first row to return for this query.
|
||||
*
|
||||
@@ -390,13 +340,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF setMapKey(String mapKey);
|
||||
|
||||
/**
|
||||
* Set to true if this query should execute against the doc store.
|
||||
* <p>
|
||||
* When setting this you may also consider disabling lazy loading.
|
||||
*/
|
||||
SELF setUseDocStore(boolean useDocStore);
|
||||
|
||||
/**
|
||||
* When set to true when you want the returned beans to be unmodifiable read only.
|
||||
* <p>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.annotation.DocStoreMode;
|
||||
import io.ebean.annotation.PersistBatch;
|
||||
import io.ebean.config.DatabaseConfig;
|
||||
import io.ebean.config.DocStoreConfig;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.sql.Connection;
|
||||
@@ -211,29 +209,6 @@ public interface Transaction extends AutoCloseable {
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* Set the behavior for document store updates on this transaction.
|
||||
* <p>
|
||||
* For example, set the mode to DocStoreEvent.IGNORE for this transaction and
|
||||
* then any changes via this transaction are not sent to the doc store. This
|
||||
* would be used when doing large bulk inserts into the database and we want
|
||||
* to control how that is sent to the document store.
|
||||
* </p>
|
||||
*/
|
||||
void setDocStoreMode(DocStoreMode mode);
|
||||
|
||||
/**
|
||||
* Set the batch size to use for sending messages to the document store.
|
||||
* <p>
|
||||
* You might set this if you know the changes in this transaction result in especially large or
|
||||
* especially small payloads and want to adjust the batch size to match.
|
||||
* </p>
|
||||
* <p>
|
||||
* Setting this overrides the default of {@link DocStoreConfig#getBulkBatchSize()}
|
||||
* </p>
|
||||
*/
|
||||
void setDocStoreBatchSize(int batchSize);
|
||||
|
||||
/**
|
||||
* Explicitly turn off or on the cascading nature of save() and delete(). This
|
||||
* gives the developer exact control over what beans are saved and deleted
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -60,7 +60,8 @@ public class ClassLoadConfig {
|
||||
}
|
||||
|
||||
public boolean isJacksonCorePresent() {
|
||||
return isPresent("com.fasterxml.jackson.core.JsonParser");
|
||||
// Legacy method name retained for compatibility; now checks avaje JSON core.
|
||||
return isPresent("io.avaje.json.JsonReader");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,4 +159,3 @@ public class ClassLoadConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import io.avaje.config.Config;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.*;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.annotation.PersistBatch;
|
||||
@@ -16,8 +16,6 @@ import io.ebean.event.*;
|
||||
import io.ebean.event.changelog.ChangeLogListener;
|
||||
import io.ebean.event.changelog.ChangeLogPrepare;
|
||||
import io.ebean.event.changelog.ChangeLogRegister;
|
||||
import io.ebean.event.readaudit.ReadAuditLogger;
|
||||
import io.ebean.event.readaudit.ReadAuditPrepare;
|
||||
import io.ebean.meta.MetricNamingMatch;
|
||||
import io.ebean.util.StringHelper;
|
||||
import jakarta.persistence.EnumType;
|
||||
@@ -31,38 +29,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 {
|
||||
|
||||
@@ -143,16 +118,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
private List<String> packages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Configuration for the ElasticSearch integration.
|
||||
*/
|
||||
private DocStoreConfig docStoreConfig = new DocStoreConfig();
|
||||
|
||||
/**
|
||||
* Set to true when the Database only uses Document store.
|
||||
*/
|
||||
private boolean docStoreOnly;
|
||||
|
||||
/**
|
||||
* This is used to populate @WhoCreated, @WhoModified and
|
||||
* support other audit features (who executed a query etc).
|
||||
@@ -430,8 +395,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
private ChangeLogListener changeLogListener;
|
||||
private ChangeLogRegister changeLogRegister;
|
||||
private boolean changeLogAsync = true;
|
||||
private ReadAuditLogger readAuditLogger;
|
||||
private ReadAuditPrepare readAuditPrepare;
|
||||
private EncryptKeyManager encryptKeyManager;
|
||||
private EncryptDeployManager encryptDeployManager;
|
||||
private Encryptor encryptor;
|
||||
@@ -443,7 +406,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
* The default PersistenceContextScope used if one is not explicitly set on a query.
|
||||
*/
|
||||
private PersistenceContextScope persistenceContextScope = PersistenceContextScope.TRANSACTION;
|
||||
private JsonFactory jsonFactory;
|
||||
private JsonStream jsonStream;
|
||||
private boolean localTimeWithNanos;
|
||||
private boolean durationWithNanos;
|
||||
private int maxCallStack = 5;
|
||||
@@ -539,8 +502,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
private SlowQueryListener slowQueryListener;
|
||||
|
||||
private ProfilingConfig profilingConfig = new ProfilingConfig();
|
||||
|
||||
/**
|
||||
* The mappingLocations for searching xml mapping.
|
||||
*/
|
||||
@@ -560,12 +521,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);
|
||||
}
|
||||
@@ -654,13 +617,13 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonFactory getJsonFactory() {
|
||||
return jsonFactory;
|
||||
public JsonStream getJsonStream() {
|
||||
return jsonStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setJsonFactory(JsonFactory jsonFactory) {
|
||||
this.jsonFactory = jsonFactory;
|
||||
public DatabaseConfig setJsonStream(JsonStream jsonStream) {
|
||||
this.jsonStream = jsonStream;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1006,39 +969,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadAuditLogger getReadAuditLogger() {
|
||||
return readAuditLogger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setReadAuditLogger(ReadAuditLogger readAuditLogger) {
|
||||
this.readAuditLogger = readAuditLogger;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadAuditPrepare getReadAuditPrepare() {
|
||||
return readAuditPrepare;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setReadAuditPrepare(ReadAuditPrepare readAuditPrepare) {
|
||||
this.readAuditPrepare = readAuditPrepare;
|
||||
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;
|
||||
@@ -1313,28 +1243,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDocStoreOnly() {
|
||||
return docStoreOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setDocStoreOnly(boolean docStoreOnly) {
|
||||
this.docStoreOnly = docStoreOnly;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocStoreConfig getDocStoreConfig() {
|
||||
return docStoreConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setDocStoreConfig(DocStoreConfig docStoreConfig) {
|
||||
this.docStoreConfig = docStoreConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DbConstraintNaming getConstraintNaming() {
|
||||
return platformConfig.getConstraintNaming();
|
||||
@@ -2118,13 +2026,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
readOnlyDataSourceConfig.loadSettings(p.properties, name + "-ro");
|
||||
}
|
||||
|
||||
/**
|
||||
* This is broken out to allow overridden behaviour.
|
||||
*/
|
||||
protected void loadDocStoreSettings(PropertiesWrapper p) {
|
||||
docStoreConfig.loadSettings(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is broken out to allow overridden behaviour.
|
||||
*/
|
||||
@@ -2137,7 +2038,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();
|
||||
@@ -2156,11 +2056,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
}
|
||||
loadDataSourceSettings(p);
|
||||
|
||||
if (docStoreConfig == null) {
|
||||
docStoreConfig = new DocStoreConfig();
|
||||
}
|
||||
loadDocStoreSettings(p);
|
||||
|
||||
defaultServer = p.getBoolean("defaultServer", defaultServer);
|
||||
shutdownHook = p.getBoolean("shutdownHook", shutdownHook);
|
||||
readOnlyDatabase = p.getBoolean("readOnlyDatabase", readOnlyDatabase);
|
||||
@@ -2179,7 +2074,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
queryPlanCaptureMaxTimeMillis = p.getLong("queryPlan.captureMaxTimeMillis", queryPlanCaptureMaxTimeMillis);
|
||||
queryPlanCaptureMaxCount = p.getInt("queryPlan.captureMaxCount", queryPlanCaptureMaxCount);
|
||||
queryPlanExplain = p.get("queryPlan.explain", queryPlanExplain);
|
||||
docStoreOnly = p.getBoolean("docStoreOnly", docStoreOnly);
|
||||
disableL2Cache = p.getBoolean("disableL2Cache", disableL2Cache);
|
||||
localOnlyL2Cache = p.getBoolean("localOnlyL2Cache", localOnlyL2Cache);
|
||||
enabledL2Regions = p.get("enabledL2Regions", enabledL2Regions);
|
||||
|
||||
@@ -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,320 +0,0 @@
|
||||
package io.ebean.config;
|
||||
|
||||
import io.ebean.Transaction;
|
||||
import io.ebean.annotation.DocStoreMode;
|
||||
|
||||
/**
|
||||
* Configuration for the Document store integration (e.g. ElasticSearch).
|
||||
*/
|
||||
public class DocStoreConfig {
|
||||
|
||||
/**
|
||||
* True when the Document store integration is active/on.
|
||||
*/
|
||||
protected boolean active;
|
||||
|
||||
/**
|
||||
* Set to true means Ebean will generate mapping files on startup.
|
||||
*/
|
||||
protected boolean generateMapping;
|
||||
|
||||
/**
|
||||
* When true the Document store should drop and re-create document indexes.
|
||||
*/
|
||||
protected boolean dropCreate;
|
||||
|
||||
/**
|
||||
* When true the Document store should create any document indexes that don't already exist.
|
||||
*/
|
||||
protected boolean create;
|
||||
|
||||
/**
|
||||
* The URL of the Document store. For example: http://localhost:9200.
|
||||
*/
|
||||
protected String url;
|
||||
|
||||
/**
|
||||
* Credential that be used for authentication to document store.
|
||||
*/
|
||||
protected String username;
|
||||
|
||||
/**
|
||||
* Password credential that be used for authentication to document store.
|
||||
*/
|
||||
protected String password;
|
||||
|
||||
/**
|
||||
* Set to true such that the client allows connections to invalid/self signed SSL certificates.
|
||||
*/
|
||||
protected boolean allowAllCertificates;
|
||||
|
||||
/**
|
||||
* The default mode used by indexes.
|
||||
*/
|
||||
protected DocStoreMode persist = DocStoreMode.UPDATE;
|
||||
|
||||
/**
|
||||
* The default batch size to use for the Bulk API calls.
|
||||
*/
|
||||
protected int bulkBatchSize = 1000;
|
||||
|
||||
/**
|
||||
* Resource path for the Document store mapping files.
|
||||
*/
|
||||
protected String mappingPath;
|
||||
|
||||
/**
|
||||
* Suffix used for mapping files.
|
||||
*/
|
||||
protected String mappingSuffix;
|
||||
|
||||
/**
|
||||
* Location of resources that mapping files are generated into.
|
||||
*/
|
||||
protected String pathToResources = "src/main/resources";
|
||||
|
||||
/**
|
||||
* Return true if the Document store (ElasticSearch) integration is active.
|
||||
*/
|
||||
public boolean isActive() {
|
||||
String systemValue = System.getProperty("ebean.docstore.active");
|
||||
if (systemValue != null) {
|
||||
return Boolean.parseBoolean(systemValue);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to make the Document store (ElasticSearch) integration active.
|
||||
*/
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the URL to the Document store.
|
||||
*/
|
||||
public String getUrl() {
|
||||
String systemValue = System.getProperty("ebean.docstore.url");
|
||||
if (systemValue != null) {
|
||||
return systemValue;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user credential for connecting to the document store.
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user credential for connecting to the document store.
|
||||
*/
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the password credential for connecting to the document store.
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the password credential for connecting to the document store.
|
||||
*/
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL to the Document store.
|
||||
* <p>
|
||||
* For a local ElasticSearch server this would be: http://localhost:9200
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if Ebean should generate mapping files on server startup.
|
||||
*/
|
||||
public boolean isGenerateMapping() {
|
||||
String systemValue = System.getProperty("ebean.docstore.generateMapping");
|
||||
if (systemValue != null) {
|
||||
return Boolean.parseBoolean(systemValue);
|
||||
}
|
||||
return generateMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if Ebean should generate mapping files on server startup.
|
||||
*/
|
||||
public void setGenerateMapping(boolean generateMapping) {
|
||||
this.generateMapping = generateMapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the document store should recreate mapped indexes.
|
||||
*/
|
||||
public boolean isDropCreate() {
|
||||
String systemValue = System.getProperty("ebean.docstore.dropCreate");
|
||||
if (systemValue != null) {
|
||||
return Boolean.parseBoolean(systemValue);
|
||||
}
|
||||
return dropCreate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if the document store should recreate mapped indexes.
|
||||
*/
|
||||
public void setDropCreate(boolean dropCreate) {
|
||||
this.dropCreate = dropCreate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create true if the document store should create mapped indexes that don't yet exist.
|
||||
* This is only used if dropCreate is false.
|
||||
*/
|
||||
public boolean isCreate() {
|
||||
String systemValue = System.getProperty("ebean.docstore.create");
|
||||
if (systemValue != null) {
|
||||
return Boolean.parseBoolean(systemValue);
|
||||
}
|
||||
return create;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if the document store should create mapped indexes that don't yet exist.
|
||||
* This is only used if dropCreate is false.
|
||||
*/
|
||||
public void setCreate(boolean create) {
|
||||
this.create = create;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the client allows connections to invalid/self signed SSL certificates.
|
||||
*/
|
||||
public boolean isAllowAllCertificates() {
|
||||
return allowAllCertificates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true such that the client allows connections to invalid/self signed SSL certificates.
|
||||
*/
|
||||
public void setAllowAllCertificates(boolean allowAllCertificates) {
|
||||
this.allowAllCertificates = allowAllCertificates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default batch size to use for calls to the Bulk API.
|
||||
*/
|
||||
public int getBulkBatchSize() {
|
||||
return bulkBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default batch size to use for calls to the Bulk API.
|
||||
* <p>
|
||||
* The batch size can be set on a transaction via {@link Transaction#setDocStoreBatchSize(int)}.
|
||||
* </p>
|
||||
*/
|
||||
public void setBulkBatchSize(int bulkBatchSize) {
|
||||
this.bulkBatchSize = bulkBatchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mapping path.
|
||||
*/
|
||||
public String getMappingPath() {
|
||||
return mappingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mapping path.
|
||||
*/
|
||||
public void setMappingPath(String mappingPath) {
|
||||
this.mappingPath = mappingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mapping suffix.
|
||||
*/
|
||||
public String getMappingSuffix() {
|
||||
return mappingSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mapping suffix.
|
||||
*/
|
||||
public void setMappingSuffix(String mappingSuffix) {
|
||||
this.mappingSuffix = mappingSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relative file system path to resources when generating mapping files.
|
||||
*/
|
||||
public String getPathToResources() {
|
||||
return pathToResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the relative file system path to resources when generating mapping files.
|
||||
*/
|
||||
public void setPathToResources(String pathToResources) {
|
||||
this.pathToResources = pathToResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default behavior for when Insert, Update and Delete events occur on beans that have an associated
|
||||
* Document store.
|
||||
*/
|
||||
public DocStoreMode getPersist() {
|
||||
return persist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default behavior for when Insert, Update and Delete events occur on beans that have an associated
|
||||
* Document store.
|
||||
* <ul>
|
||||
* <li>DocStoreEvent.UPDATE - build and send message to Bulk API</li>
|
||||
* <li>DocStoreEvent.QUEUE - add an entry with the index type and id only into a queue for later processing</li>
|
||||
* <li>DocStoreEvent.IGNORE - ignore. Most likely used when some scheduled batch job handles updating the index</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* You might choose to use QUEUE if that particular index data is updating very frequently or the cost of indexing
|
||||
* is expensive. Setting it to QUEUE can mean many changes can be batched together potentially coalescing multiple
|
||||
* updates for an index entry into a single update.
|
||||
* </p>
|
||||
* <p>
|
||||
* You might choose to use IGNORE when you have your own external process for updating the indexes. In this case
|
||||
* you don't want Ebean to do anything when the data changes.
|
||||
* </p>
|
||||
*/
|
||||
public void setPersist(DocStoreMode persist) {
|
||||
this.persist = persist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings specified in properties files.
|
||||
*/
|
||||
public void loadSettings(PropertiesWrapper properties) {
|
||||
|
||||
active = properties.getBoolean("docstore.active", active);
|
||||
url = properties.get("docstore.url", url);
|
||||
username = properties.get("docstore.username", url);
|
||||
password = properties.get("docstore.password", url);
|
||||
persist = properties.getEnum(DocStoreMode.class, "docstore.persist", persist);
|
||||
bulkBatchSize = properties.getInt("docstore.bulkBatchSize", bulkBatchSize);
|
||||
generateMapping = properties.getBoolean("docstore.generateMapping", generateMapping);
|
||||
dropCreate = properties.getBoolean("docstore.dropCreate", dropCreate);
|
||||
create = properties.getBoolean("docstore.create", create);
|
||||
allowAllCertificates = properties.getBoolean("docstore.allowAllCertificates", allowAllCertificates);
|
||||
mappingPath = properties.get("docstore.mappingPath", mappingPath);
|
||||
mappingSuffix = properties.get("docstore.mappingSuffix", mappingSuffix);
|
||||
pathToResources = properties.get("docstore.pathToResources", pathToResources);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,13 @@ public class DbPlatformTypeMapping {
|
||||
private static final DbPlatformType VECTOR_BIT = new DbPlatformType("bit", 64000, null);
|
||||
private static final DbPlatformType VECTOR_SPARSE = new DbPlatformType("sparsevec", 1000, null);
|
||||
|
||||
/**
|
||||
* Timestamp with max precision of 15, and fallback to plain timestamp without precision defined.
|
||||
*/
|
||||
private static final DbPlatformType TIMESTAMP =
|
||||
new DbPlatformType("timestamp", 0, 15,
|
||||
new DbPlatformType("timestamp", false));
|
||||
|
||||
private final Map<DbType, DbPlatformType> typeMap = new EnumMap<>(DbType.class);
|
||||
|
||||
/**
|
||||
@@ -87,7 +94,8 @@ public class DbPlatformTypeMapping {
|
||||
put(DbType.ARRAY);
|
||||
put(DbType.DATE);
|
||||
put(DbType.TIME);
|
||||
put(DbType.TIMESTAMP);
|
||||
put(DbType.TIMESTAMP, TIMESTAMP);
|
||||
|
||||
put(DbType.LONGVARBINARY);
|
||||
put(DbType.LONGVARCHAR);
|
||||
// most commonly real maps to db float
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.ebean.docstore;
|
||||
|
||||
/**
|
||||
* Document Mapping for a bean marker interface.
|
||||
*/
|
||||
public interface DocMapping {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.ebean.docstore;
|
||||
|
||||
/**
|
||||
* Document query request context marker interface.
|
||||
*/
|
||||
public interface DocQueryContext<T> {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.ebean.docstore;
|
||||
|
||||
/**
|
||||
* Document update context marker interface.
|
||||
*/
|
||||
public interface DocUpdateContext {
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
package io.ebean.docstore;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Raw document.
|
||||
*/
|
||||
public class RawDoc {
|
||||
|
||||
private Map<String, Object> source;
|
||||
private String id;
|
||||
private double score;
|
||||
private String index;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Construct the document with all the meta data.
|
||||
*/
|
||||
public RawDoc(Map<String, Object> source, String id, double score, String index, String type) {
|
||||
this.source = source;
|
||||
this.id = id;
|
||||
this.score = score;
|
||||
this.index = index;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct empty (typically for JSON marshalling).
|
||||
*/
|
||||
public RawDoc() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the source document as a Map.
|
||||
*/
|
||||
public Map<String, Object> getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Id value.
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the score.
|
||||
*/
|
||||
public double getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index name.
|
||||
*/
|
||||
public String getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index type.
|
||||
*/
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source document.
|
||||
*/
|
||||
public void setSource(Map<String, Object> source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the id value.
|
||||
*/
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the score.
|
||||
*/
|
||||
public void setScore(double score) {
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the index name.
|
||||
*/
|
||||
public void setIndex(String index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the index type.
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package io.ebean.event.readaudit;
|
||||
|
||||
/**
|
||||
* Log that the query was executed
|
||||
*/
|
||||
public interface ReadAuditLogger {
|
||||
|
||||
/**
|
||||
* Called when a new query plan is created.
|
||||
* <p>
|
||||
* The query plan has the full sql and logging the query plan separately means that each of
|
||||
* the bean and many read events can log the query plan key and not the full sql (reducing the
|
||||
* bulk size of the read audit logs).
|
||||
* </p>
|
||||
*/
|
||||
void queryPlan(ReadAuditQueryPlan queryPlan);
|
||||
|
||||
/**
|
||||
* Audit a find bean query that returned a bean.
|
||||
* <p>
|
||||
* Finds that did not return a bean are excluded.
|
||||
* </p>
|
||||
*/
|
||||
void auditBean(ReadEvent readBean);
|
||||
|
||||
/**
|
||||
* Audit a find many query that returned some beans.
|
||||
* <p>
|
||||
* Finds that did not return any beans are excluded.
|
||||
* </p>
|
||||
* <p>
|
||||
* For large queries executed via findEach() etc the ids are collected in batches
|
||||
* and logged. Hence the ids list has a maximum size of the batch size.
|
||||
* </p>
|
||||
*/
|
||||
void auditMany(ReadEvent readMany);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package io.ebean.event.readaudit;
|
||||
|
||||
/**
|
||||
* Set user context information into the read event prior to it being logged.
|
||||
*/
|
||||
public interface ReadAuditPrepare {
|
||||
|
||||
/**
|
||||
* Prepare the read event by setting any user context information into the read event such as the
|
||||
* application user id and ip address.
|
||||
* <p>
|
||||
* This method is called prior to the read event being sent to the ReadAuditLogger.
|
||||
* </p>
|
||||
* <p>
|
||||
* Note that for findFutureList() queries prepare() is called early in the foreground thread
|
||||
* prior to the query executing and at that point the ReadEvent bean only has the bean type
|
||||
* and no other details (which are populated later when the query is executed in the background
|
||||
* thread).
|
||||
* </p>
|
||||
*/
|
||||
void prepare(ReadEvent readEvent);
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package io.ebean.event.readaudit;
|
||||
|
||||
/**
|
||||
* A SQL query and associated keys.
|
||||
* <p>
|
||||
* This is logged as a separate event so that the
|
||||
* </p>
|
||||
*/
|
||||
public class ReadAuditQueryPlan {
|
||||
|
||||
String beanType;
|
||||
|
||||
String queryKey;
|
||||
|
||||
String sql;
|
||||
|
||||
/**
|
||||
* Construct given the beanType, queryKey and sql.
|
||||
*/
|
||||
public ReadAuditQueryPlan(String beanType, String queryKey, String sql) {
|
||||
this.beanType = beanType;
|
||||
this.queryKey = queryKey;
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct for JSON tools.
|
||||
*/
|
||||
public ReadAuditQueryPlan() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "beanType:" + beanType + " queryKey:" + queryKey + " sql:" + sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bean type.
|
||||
*/
|
||||
public String getBeanType() {
|
||||
return beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bean type.
|
||||
*/
|
||||
public void setBeanType(String beanType) {
|
||||
this.beanType = beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query key (relative to the bean type).
|
||||
*/
|
||||
public String getQueryKey() {
|
||||
return queryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query key.
|
||||
*/
|
||||
public void setQueryKey(String queryKey) {
|
||||
this.queryKey = queryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sql statement.
|
||||
*/
|
||||
public String getSql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sql statement.
|
||||
*/
|
||||
public void setSql(String sql) {
|
||||
this.sql = sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
ReadAuditQueryPlan that = (ReadAuditQueryPlan) o;
|
||||
if (!beanType.equals(that.beanType)) return false;
|
||||
if (!queryKey.equals(that.queryKey)) return false;
|
||||
return sql.equals(that.sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = beanType.hashCode();
|
||||
result = 92821 * result + queryKey.hashCode();
|
||||
result = 92821 * result + sql.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package io.ebean.event.readaudit;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Read event sent to the ReadEventLogger.
|
||||
* <p>
|
||||
* This is a flattened in that it contains either a read bean or list of beans. It is flattened
|
||||
* in this way to simplify logging and processing and simply means that it either contains an
|
||||
* id or a list of ids.
|
||||
* </p>
|
||||
*/
|
||||
public class ReadEvent {
|
||||
|
||||
/**
|
||||
* User defined 'source' such as the application name.
|
||||
*/
|
||||
protected String source;
|
||||
|
||||
/**
|
||||
* Application user id expected to be optionally populated by ChangeLogPrepare.
|
||||
*/
|
||||
protected String userId;
|
||||
|
||||
/**
|
||||
* Application user ip address expected to be optionally populated by ChangeLogPrepare.
|
||||
*/
|
||||
protected String userIpAddress;
|
||||
|
||||
/**
|
||||
* Arbitrary user context information expected to be optionally populated by ChangeLogPrepare.
|
||||
*/
|
||||
protected Map<String, String> userContext;
|
||||
|
||||
/**
|
||||
* The time the bean change was created.
|
||||
*/
|
||||
protected long eventTime;
|
||||
|
||||
/**
|
||||
* The type of the bean(s) read.
|
||||
*/
|
||||
protected String beanType;
|
||||
|
||||
/**
|
||||
* The query key (relative to the bean type).
|
||||
*/
|
||||
protected String queryKey;
|
||||
|
||||
/**
|
||||
* The bind log when the query was executed.
|
||||
*/
|
||||
protected String bindLog;
|
||||
|
||||
/**
|
||||
* The id of the bean read.
|
||||
*/
|
||||
protected Object id;
|
||||
|
||||
/**
|
||||
* The ids of the beans read.
|
||||
*/
|
||||
protected List<Object> ids;
|
||||
|
||||
/**
|
||||
* Common constructor for single bean and multi-bean read events.
|
||||
*/
|
||||
protected ReadEvent(String beanType, String queryKey, String bindLog) {
|
||||
this.beanType = beanType;
|
||||
this.queryKey = queryKey;
|
||||
this.bindLog = bindLog;
|
||||
this.eventTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct for a single bean read.
|
||||
*/
|
||||
public ReadEvent(String beanType, String queryKey, String bindLog, Object id) {
|
||||
this(beanType, queryKey, bindLog);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct for many beans read.
|
||||
*/
|
||||
public ReadEvent(String beanType, String queryKey, String bindLog, List<Object> ids) {
|
||||
this(beanType, queryKey, bindLog);
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct for many future list query.
|
||||
*/
|
||||
public ReadEvent(String beanType) {
|
||||
this.beanType = beanType;
|
||||
this.eventTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for JSON tools.
|
||||
*/
|
||||
public ReadEvent() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a code that identifies the source of the change (like the name of the application).
|
||||
*/
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source of the change (like the name of the application).
|
||||
*/
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the application user Id.
|
||||
*/
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application user Id.
|
||||
* <p>
|
||||
* This can be set by the ChangeLogListener in the prepare() method which is called
|
||||
* in the foreground thread.
|
||||
* </p>
|
||||
*/
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the application users ip address.
|
||||
*/
|
||||
public String getUserIpAddress() {
|
||||
return userIpAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the application users ip address.
|
||||
* <p>
|
||||
* This can be set by the ChangeLogListener in the prepare() method which is called
|
||||
* in the foreground thread.
|
||||
* </p>
|
||||
*/
|
||||
public void setUserIpAddress(String userIpAddress) {
|
||||
this.userIpAddress = userIpAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a user context value - anything you set yourself in ChangeLogListener prepare().
|
||||
*/
|
||||
public Map<String, String> getUserContext() {
|
||||
if (userContext == null) {
|
||||
userContext = new LinkedHashMap<>();
|
||||
}
|
||||
return userContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user context value (anything you like).
|
||||
* <p>
|
||||
* This can be set by the ChangeLogListener in the prepare() method which is called
|
||||
* in the foreground thread.
|
||||
* </p>
|
||||
*/
|
||||
public void setUserContext(Map<String, String> userContext) {
|
||||
this.userContext = userContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of bean read.
|
||||
*/
|
||||
public String getBeanType() {
|
||||
return beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of bean read.
|
||||
*/
|
||||
public void setBeanType(String beanType) {
|
||||
this.beanType = beanType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query key (relative to the bean type).
|
||||
*/
|
||||
public String getQueryKey() {
|
||||
return queryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query key (relative to the bean type).
|
||||
*/
|
||||
public void setQueryKey(String queryKey) {
|
||||
this.queryKey = queryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the bind log used when executing the query.
|
||||
*/
|
||||
public String getBindLog() {
|
||||
return bindLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bind log used when executing the query.
|
||||
*/
|
||||
public void setBindLog(String bindLog) {
|
||||
this.bindLog = bindLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the event date time.
|
||||
*/
|
||||
public long getEventTime() {
|
||||
return eventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the event date time.
|
||||
*/
|
||||
public void setEventTime(long eventTime) {
|
||||
this.eventTime = eventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the bean read.
|
||||
*/
|
||||
public Object getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the id of the bean read.
|
||||
*/
|
||||
public void setId(Object id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ids of the beans read.
|
||||
*/
|
||||
public List<Object> getIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ids of the beans read.
|
||||
*/
|
||||
public void setIds(List<Object> ids) {
|
||||
this.ids = ids;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Provides Auditing of read events including queries and L2 cache.
|
||||
* <p>
|
||||
* Provides a built support for supplied an audit of all the 'read events' for beans annotated
|
||||
* with <code>@ReadAudit</code>
|
||||
* </p>
|
||||
*/
|
||||
package io.ebean.event.readaudit;
|
||||
@@ -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,135 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Canonical "v2" mapping of Ebean's internal flat metric names (e.g.
|
||||
* {@code orm.Customer.findList}, {@code iud.User.save}, {@code txn.named.X},
|
||||
* {@code l2.<region>.<op>}) into a metric family name plus a tag string following
|
||||
* the label-tag convention.
|
||||
*
|
||||
* <p>This is the source-of-truth mapping for the v2 metrics JSON form
|
||||
* ({@link ServerMetricsAsJson#writeV2(Appendable)}). The tag string is a canonical,
|
||||
* sorted, comma separated list of {@code key:value} pairs, e.g.
|
||||
* {@code "kind:orm,label:Customer.findList,type:Customer"}.
|
||||
*
|
||||
* <table>
|
||||
* <caption>Ebean prefix → family name + tags</caption>
|
||||
* <tr><th>Ebean prefix</th><th>name</th><th>tags</th></tr>
|
||||
* <tr><td>{@code iud.X}</td><td>{@code ebean.dml}</td><td>{@code label=X}</td></tr>
|
||||
* <tr><td>{@code orm.X}</td><td>{@code ebean.query}</td><td>{@code kind=orm, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code dto.X}</td><td>{@code ebean.query}</td><td>{@code kind=dto, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code sql.X}</td><td>{@code ebean.query}</td><td>{@code kind=sql, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code txn.named.X} / {@code txn.X}</td><td>{@code ebean.txn}</td><td>{@code label=X}</td></tr>
|
||||
* <tr><td>{@code l2.<region>.<op>}</td><td>{@code ebean.l2}</td><td>{@code op=<op>, region=<region>}</td></tr>
|
||||
* <tr><td>(unrecognised)</td><td>{@code ebean.other}</td><td>{@code label=<original name>}</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>The {@code kind} tag is the query category (orm/dto/sql) while the {@code type}
|
||||
* tag is the queried bean/entity simple name. The {@code type} tag is omitted when
|
||||
* the bean type is unknown.
|
||||
*/
|
||||
final class MetricNamingV2 {
|
||||
|
||||
/** Result of a name mapping: family name plus canonical tag string. */
|
||||
static final class Mapped {
|
||||
private final String name;
|
||||
private final String tags;
|
||||
|
||||
Mapped(String name, String tags) {
|
||||
this.name = name;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
String tags() {
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
private MetricNamingV2() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an Ebean flat metric name (and optional bean type for query metrics) into
|
||||
* the canonical family name plus tag string.
|
||||
*/
|
||||
static Mapped map(String ebeanName, String beanType) {
|
||||
if (ebeanName == null || ebeanName.isEmpty()) {
|
||||
return new Mapped("ebean.other", "");
|
||||
}
|
||||
int firstDot = ebeanName.indexOf('.');
|
||||
if (firstDot <= 0) {
|
||||
return new Mapped("ebean.other", tags("label", ebeanName));
|
||||
}
|
||||
String prefix = ebeanName.substring(0, firstDot);
|
||||
String rest = ebeanName.substring(firstDot + 1);
|
||||
switch (prefix) {
|
||||
case "iud":
|
||||
return new Mapped("ebean.dml", tags("label", rest));
|
||||
case "orm":
|
||||
return query("orm", rest, beanType);
|
||||
case "dto":
|
||||
return query("dto", rest, beanType);
|
||||
case "sql":
|
||||
return query("sql", rest, beanType);
|
||||
case "txn":
|
||||
String txnLabel = rest.startsWith("named.") ? rest.substring("named.".length()) : rest;
|
||||
return new Mapped("ebean.txn", tags("label", txnLabel));
|
||||
case "l2":
|
||||
return l2(rest);
|
||||
default:
|
||||
return new Mapped("ebean.other", tags("label", ebeanName));
|
||||
}
|
||||
}
|
||||
|
||||
private static Mapped query(String kind, String label, String beanType) {
|
||||
if (beanType == null || beanType.isEmpty()) {
|
||||
return new Mapped("ebean.query", tags("kind", kind, "label", label));
|
||||
}
|
||||
return new Mapped("ebean.query", tags("kind", kind, "type", beanType, "label", label));
|
||||
}
|
||||
|
||||
private static Mapped l2(String rest) {
|
||||
int dot = rest.indexOf('.');
|
||||
if (dot <= 0) {
|
||||
return new Mapped("ebean.l2", tags("op", rest));
|
||||
}
|
||||
String region = rest.substring(0, dot);
|
||||
String op = rest.substring(dot + 1);
|
||||
return new Mapped("ebean.l2", tags("op", op, "region", region));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a canonical (sorted) {@code key:value,key2:value2} tag string from the given
|
||||
* key/value pairs, skipping null/empty values and sanitising the reserved
|
||||
* delimiter characters from values.
|
||||
*/
|
||||
private static String tags(String... keyValues) {
|
||||
List<String> pairs = new ArrayList<>(keyValues.length / 2);
|
||||
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
||||
String value = keyValues[i + 1];
|
||||
if (value != null && !value.isEmpty()) {
|
||||
pairs.add(keyValues[i] + ':' + sanitize(value));
|
||||
}
|
||||
}
|
||||
Collections.sort(pairs);
|
||||
return String.join(",", pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the reserved tag delimiter characters ({@code ,} and {@code :}) so they
|
||||
* cannot break the {@code key:value,key2:value2} encoding.
|
||||
*/
|
||||
private static String sanitize(String value) {
|
||||
if (value.indexOf(',') < 0 && value.indexOf(':') < 0) {
|
||||
return value;
|
||||
}
|
||||
return value.replace(',', '_').replace(':', '_');
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
private Comparator<MetaTimedMetric> sortBy = SortMetric.NAME;
|
||||
private int listCounter;
|
||||
private int objKeyCounter;
|
||||
private boolean v2;
|
||||
|
||||
MetricsAsJson(ServerMetrics metrics) {
|
||||
this.metrics = metrics;
|
||||
@@ -67,6 +68,13 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
collect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeV2(Appendable buffer) {
|
||||
this.v2 = true;
|
||||
this.writer = buffer;
|
||||
collect();
|
||||
}
|
||||
|
||||
private void collect() {
|
||||
try {
|
||||
start();
|
||||
@@ -151,12 +159,26 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
}
|
||||
|
||||
private void metricStart(MetaMetric metric) throws IOException {
|
||||
metricStart(metric, null);
|
||||
}
|
||||
|
||||
private void metricStart(MetaMetric metric, String beanType) throws IOException {
|
||||
if (listCounter++ > 0) {
|
||||
writer.append(',').append(newLine);
|
||||
}
|
||||
objStart();
|
||||
key("name");
|
||||
val(metric.name());
|
||||
if (v2) {
|
||||
MetricNamingV2.Mapped mapped = MetricNamingV2.map(metric.name(), beanType);
|
||||
key("name");
|
||||
val(mapped.name());
|
||||
if (!mapped.tags().isEmpty()) {
|
||||
key("tags");
|
||||
val(mapped.tags());
|
||||
}
|
||||
} else {
|
||||
key("name");
|
||||
val(metric.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void metricEnd() throws IOException {
|
||||
@@ -180,7 +202,8 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
}
|
||||
|
||||
private void logQuery(MetaQueryMetric metric) throws IOException {
|
||||
metricStart(metric);
|
||||
Class<?> beanType = metric.type();
|
||||
metricStart(metric, beanType == null ? null : beanType.getSimpleName());
|
||||
appendTiming(metric);
|
||||
if (withHash) {
|
||||
append("hash", metric.hash());
|
||||
|
||||
@@ -41,6 +41,17 @@ public interface ServerMetricsAsJson {
|
||||
*/
|
||||
void write(Appendable buffer);
|
||||
|
||||
/**
|
||||
* Collect and write metrics as "v2" JSON to the given buffer.
|
||||
* <p>
|
||||
* The v2 form uses the canonical label-tag convention: each metric is written with
|
||||
* a family {@code name} (e.g. {@code ebean.query}, {@code ebean.dml}) plus a
|
||||
* {@code tags} string of sorted {@code key:value} pairs (e.g.
|
||||
* {@code "kind:orm,label:Customer.findList,type:Customer"}) rather than the flat
|
||||
* prefixed name. Timing, hash, location and sql attributes are unchanged.
|
||||
*/
|
||||
void writeV2(Appendable buffer);
|
||||
|
||||
/**
|
||||
* Return the metrics in raw JSON.
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,6 @@ public interface MetricFactory extends BootstrapService {
|
||||
/**
|
||||
* Create a Timed metric.
|
||||
*/
|
||||
QueryPlanMetric createQueryPlanMetric(Class<?> type, String label, ProfileLocation profileLocation, String sql);
|
||||
QueryPlanMetric createQueryPlanMetric(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package io.ebean.plugin;
|
||||
|
||||
import io.ebean.FetchPath;
|
||||
import io.ebean.Query;
|
||||
import io.ebean.docstore.DocUpdateContext;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Doc store functions for a specific entity bean type.
|
||||
*
|
||||
* @param <T> The type of entity bean
|
||||
*/
|
||||
public interface BeanDocType<T> {
|
||||
|
||||
/**
|
||||
* Return the doc store index type for this bean type.
|
||||
*/
|
||||
String indexType();
|
||||
|
||||
/**
|
||||
* Return the doc store index name for this bean type.
|
||||
*/
|
||||
String indexName();
|
||||
|
||||
/**
|
||||
* Apply the appropriate fetch path to the query such that the query returns beans matching
|
||||
* the document store structure with the expected embedded properties.
|
||||
*/
|
||||
void applyPath(Query<T> spiQuery);
|
||||
|
||||
/**
|
||||
* Return the FetchPath for the embedded document.
|
||||
*/
|
||||
FetchPath embedded(String path);
|
||||
|
||||
/**
|
||||
* For embedded 'many' properties we need a FetchPath relative to the root which is used to
|
||||
* build and replace the embedded list.
|
||||
*/
|
||||
FetchPath embeddedManyRoot(String path);
|
||||
|
||||
/**
|
||||
* Return a 'raw' property mapped for the given property.
|
||||
* If none exists the given property is returned.
|
||||
*/
|
||||
String rawProperty(String property);
|
||||
|
||||
/**
|
||||
* Store the bean in the doc store index.
|
||||
* <p>
|
||||
* This somewhat assumes the bean is fetched with appropriate path properties
|
||||
* to match the expected document structure.
|
||||
*/
|
||||
void index(Object idValue, T bean, DocUpdateContext txn) throws IOException;
|
||||
|
||||
/**
|
||||
* Add a delete by Id to the doc store.
|
||||
*/
|
||||
void deleteById(Object idValue, DocUpdateContext txn) throws IOException;
|
||||
|
||||
/**
|
||||
* Add a embedded document update to the doc store.
|
||||
*
|
||||
* @param idValue the Id value of the bean holding the embedded document
|
||||
* @param embeddedProperty the embedded property
|
||||
* @param embeddedRawContent the content of the embedded document in JSON form
|
||||
* @param txn the doc store transaction to add the update to
|
||||
*/
|
||||
void updateEmbedded(Object idValue, String embeddedProperty, String embeddedRawContent, DocUpdateContext txn) throws IOException;
|
||||
|
||||
}
|
||||
@@ -2,15 +2,12 @@ package io.ebean.plugin;
|
||||
|
||||
import io.ebean.Query;
|
||||
import io.ebean.config.dbplatform.IdType;
|
||||
import io.ebean.docstore.DocMapping;
|
||||
import io.ebean.event.BeanFindController;
|
||||
import io.ebean.event.BeanPersistController;
|
||||
import io.ebean.event.BeanPersistListener;
|
||||
import io.ebean.event.BeanQueryAdapter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Information and methods on BeanDescriptors made available to plugins.
|
||||
@@ -145,73 +142,4 @@ public interface BeanType<T> {
|
||||
*/
|
||||
IdType idType();
|
||||
|
||||
/**
|
||||
* Return true if this bean type has doc store backing.
|
||||
*/
|
||||
boolean isDocStoreMapped();
|
||||
|
||||
/**
|
||||
* Return the DocumentMapping for this bean type.
|
||||
* <p>
|
||||
* This is the document structure and mapping options for how this bean type is mapped
|
||||
* for the document store.
|
||||
* </p>
|
||||
*/
|
||||
DocMapping docMapping();
|
||||
|
||||
/**
|
||||
* Return the doc store queueId for this bean type.
|
||||
*/
|
||||
String docStoreQueueId();
|
||||
|
||||
/**
|
||||
* Return the doc store support for this bean type.\
|
||||
*/
|
||||
BeanDocType<T> docStore();
|
||||
|
||||
/**
|
||||
* Add the discriminator value to the query if needed.
|
||||
*/
|
||||
void addInheritanceWhere(Query<?> query);
|
||||
|
||||
/**
|
||||
* Return the root bean type for an inheritance hierarchy.
|
||||
*/
|
||||
BeanType<?> root();
|
||||
|
||||
/**
|
||||
* Return true if this bean type has an inheritance hierarchy.
|
||||
*/
|
||||
boolean hasInheritance();
|
||||
|
||||
/**
|
||||
* Return true if this object is the root level object in its entity
|
||||
* inheritance.
|
||||
*/
|
||||
boolean isInheritanceRoot();
|
||||
|
||||
/**
|
||||
* Returns all direct children of this beantype
|
||||
*/
|
||||
List<BeanType<?>> inheritanceChildren();
|
||||
|
||||
/**
|
||||
* Returns the parent in inheritance hierarchy
|
||||
*/
|
||||
BeanType<?> inheritanceParent();
|
||||
|
||||
/**
|
||||
* Visit all children recursively
|
||||
*/
|
||||
void visitAllInheritanceChildren(Consumer<BeanType<?>> visitor);
|
||||
|
||||
/**
|
||||
* Return the discriminator column.
|
||||
*/
|
||||
String discColumn();
|
||||
|
||||
/**
|
||||
* Create a bean given the discriminator value.
|
||||
*/
|
||||
T createBeanUsingDisc(Object discValue);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,6 @@ public interface SpiServer extends Database {
|
||||
*/
|
||||
List<? extends BeanType<?>> beanTypes(String baseTableName);
|
||||
|
||||
/**
|
||||
* Return the bean type for a given doc store queueId.
|
||||
*/
|
||||
BeanType<?> beanTypeForQueueId(String queueId);
|
||||
|
||||
/**
|
||||
* Return a BeanLoader.
|
||||
*/
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Options for the text match and multi match expressions.
|
||||
*/
|
||||
public abstract class AbstractMatch {
|
||||
|
||||
protected boolean operatorAnd;
|
||||
|
||||
protected String analyzer;
|
||||
|
||||
protected double boost;
|
||||
|
||||
protected String minShouldMatch;
|
||||
|
||||
protected int maxExpansions;
|
||||
|
||||
protected String zeroTerms;
|
||||
|
||||
protected double cutoffFrequency;
|
||||
|
||||
protected String fuzziness;
|
||||
|
||||
protected int prefixLength;
|
||||
|
||||
protected String rewrite;
|
||||
|
||||
/**
|
||||
* Return true if using the AND operator otherwise using the OR operator.
|
||||
*/
|
||||
public boolean isOperatorAnd() {
|
||||
return operatorAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the boost.
|
||||
*/
|
||||
public double getBoost() {
|
||||
return boost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum should match.
|
||||
*/
|
||||
public String getMinShouldMatch() {
|
||||
return minShouldMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the zero terms option.
|
||||
*/
|
||||
public String getZeroTerms() {
|
||||
return zeroTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cutoff frequency.
|
||||
*/
|
||||
public double getCutoffFrequency() {
|
||||
return cutoffFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the max expansions.
|
||||
*/
|
||||
public int getMaxExpansions() {
|
||||
return maxExpansions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the analyzer.
|
||||
*/
|
||||
public String getAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fuzziness.
|
||||
*/
|
||||
public String getFuzziness() {
|
||||
return fuzziness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the prefix length.
|
||||
*/
|
||||
public int getPrefixLength() {
|
||||
return prefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rewrite option.
|
||||
*/
|
||||
public String getRewrite() {
|
||||
return rewrite;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Options for the text match expression.
|
||||
*/
|
||||
public class Match extends AbstractMatch {
|
||||
|
||||
protected boolean phrase;
|
||||
|
||||
protected boolean phrasePrefix;
|
||||
|
||||
public Match() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to be a "Phrase" type expression.
|
||||
*/
|
||||
public Match phrase() {
|
||||
phrase = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to be a "Phrase Prefix" type expression.
|
||||
*/
|
||||
public Match phrasePrefix() {
|
||||
phrasePrefix = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the AND operator (rather than OR).
|
||||
*/
|
||||
public Match opAnd() {
|
||||
operatorAnd = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the OR operator (rather than AND).
|
||||
*/
|
||||
public Match opOr() {
|
||||
operatorAnd = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zero terms.
|
||||
*/
|
||||
public Match zeroTerms(String zeroTerms) {
|
||||
this.zeroTerms = zeroTerms;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cutoff frequency.
|
||||
*/
|
||||
public Match cutoffFrequency(double cutoffFrequency) {
|
||||
this.cutoffFrequency = cutoffFrequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max expansions (for phrase prefix only).
|
||||
*/
|
||||
public Match maxExpansions(int maxExpansions) {
|
||||
this.maxExpansions = maxExpansions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Analyzer to use for this expression.
|
||||
*/
|
||||
public Match analyzer(String analyzer) {
|
||||
this.analyzer = analyzer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the boost.
|
||||
*/
|
||||
public Match boost(double boost) {
|
||||
this.boost = boost;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rewrite to use.
|
||||
*/
|
||||
public Match minShouldMatch(String minShouldMatch) {
|
||||
this.minShouldMatch = minShouldMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rewrite to use.
|
||||
*/
|
||||
public Match rewrite(String rewrite) {
|
||||
this.rewrite = rewrite;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a phrase query.
|
||||
*/
|
||||
public boolean isPhrase() {
|
||||
return phrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a phrase prefix query.
|
||||
*/
|
||||
public boolean isPhrasePrefix() {
|
||||
return phrasePrefix;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Options for the text match expression.
|
||||
*/
|
||||
public class MultiMatch extends AbstractMatch {
|
||||
|
||||
/**
|
||||
* The MultiMatch type.
|
||||
*/
|
||||
public enum Type {
|
||||
BEST_FIELDS,
|
||||
MOST_FIELDS,
|
||||
CROSS_FIELDS,
|
||||
PHRASE,
|
||||
PHRASE_PREFIX
|
||||
}
|
||||
|
||||
protected final String[] fields;
|
||||
|
||||
protected Type type = Type.BEST_FIELDS;
|
||||
|
||||
protected double tieBreaker;
|
||||
|
||||
/**
|
||||
* Create with the given fields.
|
||||
*/
|
||||
public static MultiMatch fields(String... fields) {
|
||||
return new MultiMatch(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct with a set of fields.
|
||||
*/
|
||||
public MultiMatch(String... fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the type of query.
|
||||
*/
|
||||
public MultiMatch type(Type type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tieBreaker to use.
|
||||
*/
|
||||
public MultiMatch tieBreaker(double tieBreaker) {
|
||||
this.tieBreaker = tieBreaker;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the AND operator (rather than OR).
|
||||
*/
|
||||
public MultiMatch opAnd() {
|
||||
operatorAnd = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the OR operator (rather than AND).
|
||||
*/
|
||||
public MultiMatch opOr() {
|
||||
operatorAnd = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match value.
|
||||
*/
|
||||
public MultiMatch minShouldMatch(String minShouldMatch) {
|
||||
this.minShouldMatch = minShouldMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the boost.
|
||||
*/
|
||||
public MultiMatch boost(double boost) {
|
||||
this.boost = boost;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the zero terms.
|
||||
*/
|
||||
public MultiMatch zeroTerms(String zeroTerms) {
|
||||
this.zeroTerms = zeroTerms;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cutoff frequency.
|
||||
*/
|
||||
public MultiMatch cutoffFrequency(double cutoffFrequency) {
|
||||
this.cutoffFrequency = cutoffFrequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max expansions (for phrase prefix only).
|
||||
*/
|
||||
public MultiMatch maxExpansions(int maxExpansions) {
|
||||
this.maxExpansions = maxExpansions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Analyzer to use for this expression.
|
||||
*/
|
||||
public MultiMatch analyzer(String analyzer) {
|
||||
this.analyzer = analyzer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rewrite to use.
|
||||
*/
|
||||
public MultiMatch rewrite(String rewrite) {
|
||||
this.rewrite = rewrite;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type.
|
||||
*/
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fields to search.
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tie breaker.
|
||||
*/
|
||||
public double getTieBreaker() {
|
||||
return tieBreaker;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Text common terms query.
|
||||
* <p>
|
||||
* This maps to an ElasticSearch "common terms query".
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* TextCommonTerms options = new TextCommonTerms()
|
||||
* .cutoffFrequency(0.001)
|
||||
* .minShouldMatch("50%")
|
||||
* .lowFreqOperatorAnd(true)
|
||||
* .highFreqOperatorAnd(true);
|
||||
*
|
||||
* List<Customer> customers = database.find(Customer.class)
|
||||
* .text()
|
||||
* .textCommonTerms("the brown", options)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // ElasticSearch expression
|
||||
*
|
||||
* "common": {
|
||||
* "body": {
|
||||
* "query": "the brown",
|
||||
* "cutoff_frequency": 0.001,
|
||||
* "low_freq_operator": "and",
|
||||
* "high_freq_operator": "and",
|
||||
* "minimum_should_match": "50%"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public class TextCommonTerms {
|
||||
|
||||
protected double cutoffFrequency;
|
||||
|
||||
protected boolean lowFreqOperatorAnd;
|
||||
protected boolean highFreqOperatorAnd;
|
||||
|
||||
protected String minShouldMatch;
|
||||
protected String minShouldMatchLowFreq;
|
||||
protected String minShouldMatchHighFreq;
|
||||
|
||||
/**
|
||||
* Set the cutoff frequency.
|
||||
*/
|
||||
public TextCommonTerms cutoffFrequency(double cutoffFrequency) {
|
||||
this.cutoffFrequency = cutoffFrequency;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if low frequency terms should use AND operator.
|
||||
*/
|
||||
public TextCommonTerms lowFreqOperatorAnd(boolean opAnd) {
|
||||
this.lowFreqOperatorAnd = opAnd;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true if high frequency terms should use AND operator.
|
||||
*/
|
||||
public TextCommonTerms highFreqOperatorAnd(boolean opAnd) {
|
||||
this.highFreqOperatorAnd = opAnd;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match.
|
||||
*/
|
||||
public TextCommonTerms minShouldMatch(String minShouldMatch) {
|
||||
this.minShouldMatch = minShouldMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match for low frequency terms.
|
||||
*/
|
||||
public TextCommonTerms minShouldMatchLowFreq(String minShouldMatchLowFreq) {
|
||||
this.minShouldMatchLowFreq = minShouldMatchLowFreq;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match for high frequency terms.
|
||||
*/
|
||||
public TextCommonTerms minShouldMatchHighFreq(String minShouldMatchHighFreq) {
|
||||
this.minShouldMatchHighFreq = minShouldMatchHighFreq;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if low freq should use the AND operator.
|
||||
*/
|
||||
public boolean isLowFreqOperatorAnd() {
|
||||
return lowFreqOperatorAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if high freq should use the AND operator.
|
||||
*/
|
||||
public boolean isHighFreqOperatorAnd() {
|
||||
return highFreqOperatorAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cutoff frequency.
|
||||
*/
|
||||
public double getCutoffFrequency() {
|
||||
return cutoffFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum to match.
|
||||
*/
|
||||
public String getMinShouldMatch() {
|
||||
return minShouldMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum to match for high frequency.
|
||||
*/
|
||||
public String getMinShouldMatchHighFreq() {
|
||||
return minShouldMatchHighFreq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum to match for low frequency.
|
||||
*/
|
||||
public String getMinShouldMatchLowFreq() {
|
||||
return minShouldMatchLowFreq;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Text query string options.
|
||||
* <p>
|
||||
* This maps to an ElasticSearch "query string query".
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* TextQueryString options = new TextQueryString()
|
||||
* .analyzeWildcard(true)
|
||||
* .fields("name")
|
||||
* .lenient(true)
|
||||
* .opAnd();
|
||||
*
|
||||
* List<Customer> customers = database.find(Customer.class)
|
||||
* .text()
|
||||
* .textSimple("quick brown", options)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // just use default options
|
||||
* TextQueryString options = new TextQueryString();
|
||||
*
|
||||
* List<Customer> customers = database.find(Customer.class)
|
||||
* .text()
|
||||
* .textSimple("quick brown", options)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public class TextQueryString {
|
||||
|
||||
public static final int DEFAULT_FUZZY_MAX_EXPANSIONS = 50;
|
||||
|
||||
protected final String[] fields;
|
||||
|
||||
/**
|
||||
* Only used when multiple fields set.
|
||||
*/
|
||||
protected boolean useDisMax = true;
|
||||
|
||||
/**
|
||||
* Only used when multiple fields set.
|
||||
*/
|
||||
protected double tieBreaker;
|
||||
|
||||
protected String defaultField;
|
||||
|
||||
protected boolean operatorAnd;
|
||||
|
||||
protected String analyzer;
|
||||
|
||||
protected boolean allowLeadingWildcard = true;
|
||||
|
||||
protected boolean lowercaseExpandedTerms = true;
|
||||
|
||||
protected int fuzzyMaxExpansions = DEFAULT_FUZZY_MAX_EXPANSIONS;
|
||||
|
||||
protected String fuzziness;
|
||||
|
||||
protected int fuzzyPrefixLength;
|
||||
|
||||
protected double phraseSlop;
|
||||
|
||||
protected double boost;
|
||||
|
||||
protected boolean analyzeWildcard;
|
||||
|
||||
protected boolean autoGeneratePhraseQueries;
|
||||
|
||||
protected String minShouldMatch;
|
||||
|
||||
protected boolean lenient;
|
||||
|
||||
protected String locale;
|
||||
|
||||
protected String timeZone;
|
||||
|
||||
protected String rewrite;
|
||||
|
||||
/**
|
||||
* Create with given fields.
|
||||
*/
|
||||
public static TextQueryString fields(String... fields) {
|
||||
return new TextQueryString(fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct with the fields to use.
|
||||
*/
|
||||
public TextQueryString(String... fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the AND operator (rather than OR).
|
||||
*/
|
||||
public TextQueryString opAnd() {
|
||||
this.operatorAnd = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the OR operator (rather than AND).
|
||||
*/
|
||||
public TextQueryString opOr() {
|
||||
this.operatorAnd = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locale.
|
||||
*/
|
||||
public TextQueryString locale(String locale) {
|
||||
this.locale = locale;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lenient mode.
|
||||
*/
|
||||
public TextQueryString lenient(boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match.
|
||||
*/
|
||||
public TextQueryString minShouldMatch(String minShouldMatch) {
|
||||
this.minShouldMatch = minShouldMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the analyzer.
|
||||
*/
|
||||
public TextQueryString analyzer(String analyzer) {
|
||||
this.analyzer = analyzer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set useDisMax option (when multiple fields only).
|
||||
*/
|
||||
public TextQueryString useDisMax(boolean useDisMax) {
|
||||
this.useDisMax = useDisMax;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set tieBreaker option (when multiple fields only).
|
||||
*/
|
||||
public TextQueryString tieBreaker(double tieBreaker) {
|
||||
this.tieBreaker = tieBreaker;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default field.
|
||||
*/
|
||||
public TextQueryString defaultField(String defaultField) {
|
||||
this.defaultField = defaultField;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set allow leading wildcard mode.
|
||||
*/
|
||||
public TextQueryString allowLeadingWildcard(boolean allowLeadingWildcard) {
|
||||
this.allowLeadingWildcard = allowLeadingWildcard;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lowercase expanded terms mode.
|
||||
*/
|
||||
public TextQueryString lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
|
||||
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fuzzy max expansions.
|
||||
*/
|
||||
public TextQueryString fuzzyMaxExpansions(int fuzzyMaxExpansions) {
|
||||
this.fuzzyMaxExpansions = fuzzyMaxExpansions;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fuzziness.
|
||||
*/
|
||||
public TextQueryString fuzziness(String fuzziness) {
|
||||
this.fuzziness = fuzziness;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fuzzy prefix length.
|
||||
*/
|
||||
public TextQueryString fuzzyPrefixLength(int fuzzyPrefixLength) {
|
||||
this.fuzzyPrefixLength = fuzzyPrefixLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the phrase slop.
|
||||
*/
|
||||
public TextQueryString phraseSlop(double phraseSlop) {
|
||||
this.phraseSlop = phraseSlop;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the boost.
|
||||
*/
|
||||
public TextQueryString boost(double boost) {
|
||||
this.boost = boost;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the analyze wildcard mode.
|
||||
*/
|
||||
public TextQueryString analyzeWildcard(boolean analyzeWildcard) {
|
||||
this.analyzeWildcard = analyzeWildcard;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the auto generate phrase queries mode.
|
||||
*/
|
||||
public TextQueryString autoGeneratePhraseQueries(boolean autoGeneratePhraseQueries) {
|
||||
this.autoGeneratePhraseQueries = autoGeneratePhraseQueries;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time zone.
|
||||
*/
|
||||
public TextQueryString timeZone(String timeZone) {
|
||||
this.timeZone = timeZone;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the rewrite option.
|
||||
*/
|
||||
public TextQueryString rewrite(String rewrite) {
|
||||
this.rewrite = rewrite;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the rewrite option.
|
||||
*/
|
||||
public String getRewrite() {
|
||||
return rewrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fields.
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if AND is the default operator.
|
||||
*/
|
||||
public boolean isOperatorAnd() {
|
||||
return operatorAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the analyzer.
|
||||
*/
|
||||
public String getAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locale.
|
||||
*/
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lenient mode.
|
||||
*/
|
||||
public boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum should match.
|
||||
*/
|
||||
public String getMinShouldMatch() {
|
||||
return minShouldMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the useDixMax mode.
|
||||
*/
|
||||
public boolean isUseDisMax() {
|
||||
return useDisMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tie breaker.
|
||||
*/
|
||||
public double getTieBreaker() {
|
||||
return tieBreaker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default field.
|
||||
*/
|
||||
public String getDefaultField() {
|
||||
return defaultField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the allow leading wildcard mode.
|
||||
*/
|
||||
public boolean isAllowLeadingWildcard() {
|
||||
return allowLeadingWildcard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the lowercase expanded terms mode.
|
||||
*/
|
||||
public boolean isLowercaseExpandedTerms() {
|
||||
return lowercaseExpandedTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fuzzy max expansions.
|
||||
*/
|
||||
public int getFuzzyMaxExpansions() {
|
||||
return fuzzyMaxExpansions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fuzziness.
|
||||
*/
|
||||
public String getFuzziness() {
|
||||
return fuzziness;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fuzzy prefix length.
|
||||
*/
|
||||
public int getFuzzyPrefixLength() {
|
||||
return fuzzyPrefixLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the phrase slop.
|
||||
*/
|
||||
public double getPhraseSlop() {
|
||||
return phraseSlop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the analyze wildcard mode.
|
||||
*/
|
||||
public boolean isAnalyzeWildcard() {
|
||||
return analyzeWildcard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the boost.
|
||||
*/
|
||||
public double getBoost() {
|
||||
return boost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the auto generate phase queries mode.
|
||||
*/
|
||||
public boolean isAutoGeneratePhraseQueries() {
|
||||
return autoGeneratePhraseQueries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the time zone.
|
||||
*/
|
||||
public String getTimeZone() {
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
package io.ebean.search;
|
||||
|
||||
/**
|
||||
* Simple text query options.
|
||||
* <p>
|
||||
* This maps to an ElasticSearch "simple text query".
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* TextSimple options = new TextSimple()
|
||||
* .analyzeWildcard(true)
|
||||
* .fields("name")
|
||||
* .lenient(true)
|
||||
* .opAnd();
|
||||
*
|
||||
* List<Customer> customers = database.find(Customer.class)
|
||||
* .text()
|
||||
* .textSimple("quick brown", options)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public class TextSimple {
|
||||
|
||||
protected String[] fields;
|
||||
|
||||
protected boolean operatorAnd;
|
||||
|
||||
protected String analyzer;
|
||||
|
||||
protected String flags;
|
||||
|
||||
protected boolean lowercaseExpandedTerms = true;
|
||||
|
||||
protected boolean analyzeWildcard;
|
||||
|
||||
protected String locale;
|
||||
|
||||
protected boolean lenient;
|
||||
|
||||
protected String minShouldMatch;
|
||||
|
||||
/**
|
||||
* Construct
|
||||
*/
|
||||
public TextSimple() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields.
|
||||
*/
|
||||
public TextSimple fields(String... fields) {
|
||||
this.fields = fields;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use AND as the default operator.
|
||||
*/
|
||||
public TextSimple opAnd() {
|
||||
this.operatorAnd = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use OR as the default operator.
|
||||
*/
|
||||
public TextSimple opOr() {
|
||||
this.operatorAnd = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the analyzer
|
||||
*/
|
||||
public TextSimple analyzer(String analyzer) {
|
||||
this.analyzer = analyzer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the flags.
|
||||
*/
|
||||
public TextSimple flags(String flags) {
|
||||
this.flags = flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the false to not use lowercase expanded terms.
|
||||
*/
|
||||
public TextSimple lowercaseExpandedTerms(boolean lowercaseExpandedTerms) {
|
||||
this.lowercaseExpandedTerms = lowercaseExpandedTerms;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to use analyze wildcard.
|
||||
*/
|
||||
public TextSimple analyzeWildcard(boolean analyzeWildcard) {
|
||||
this.analyzeWildcard = analyzeWildcard;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the locale.
|
||||
*/
|
||||
public TextSimple locale(String locale) {
|
||||
this.locale = locale;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the lenient mode.
|
||||
*/
|
||||
public TextSimple lenient(boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum should match.
|
||||
*/
|
||||
public TextSimple minShouldMatch(String minShouldMatch) {
|
||||
this.minShouldMatch = minShouldMatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lenient mode.
|
||||
*/
|
||||
public boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true to analyse wildcard.
|
||||
*/
|
||||
public boolean isAnalyzeWildcard() {
|
||||
return analyzeWildcard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lowercase expanded terms mode.
|
||||
*/
|
||||
public boolean isLowercaseExpandedTerms() {
|
||||
return lowercaseExpandedTerms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the default operator should be AND.
|
||||
*/
|
||||
public boolean isOperatorAnd() {
|
||||
return operatorAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the analyzer to use.
|
||||
*/
|
||||
public String getAnalyzer() {
|
||||
return analyzer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the fields.
|
||||
*/
|
||||
public String[] getFields() {
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the locale.
|
||||
*/
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the flags.
|
||||
*/
|
||||
public String getFlags() {
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum should match.
|
||||
*/
|
||||
public String getMinShouldMatch() {
|
||||
return minShouldMatch;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Provides text search expressions like Match, TextQueryString etc.
|
||||
*/
|
||||
package io.ebean.search;
|
||||
@@ -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);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user