mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbdc5d753e | ||
|
|
564bc131cc | ||
|
|
45297a52f6 | ||
|
|
af20eef1df | ||
|
|
327f6a3115 | ||
|
|
b88e523ed9 | ||
|
|
dadeea640d | ||
|
|
90da31d2be | ||
|
|
8f588095b5 | ||
|
|
71a1a07aec | ||
|
|
daa33ca5a5 | ||
|
|
59fa175662 | ||
|
|
9ac26da003 | ||
|
|
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 |
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -40,7 +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 1C clean install -Pdefault
|
||||
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
|
||||
run: cd tests/test-java16 && mvn test
|
||||
run: mvn -T 1C clean test -Pdefault
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11, 17, 21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
+1
-3
@@ -54,8 +54,7 @@ Ebean is an ORM library for Java and Kotlin focused on relational data access, t
|
||||
| `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()` | Flat DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
||||
| `mapTo(...).findList()` | Nested DTO graph projection reads | `new QCustomer().mapTo(CustomerDto.class).findList();` |
|
||||
| `asDto(...).findList()` | DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
||||
|
||||
### Entity mapping and lifecycle annotations
|
||||
|
||||
@@ -189,7 +188,6 @@ database.save(customer);
|
||||
| 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) |
|
||||
| Map nested entity graphs to DTO graphs | [mapping-entity-graphs-to-dtos.md](guides/mapping-entity-graphs-to-dtos.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) |
|
||||
|
||||
|
||||
@@ -1,990 +0,0 @@
|
||||
# Nested DTO Mapping — API Design
|
||||
|
||||
Design spike for the accepted requirements in [dto-mapping-requirements.md](./dto-mapping-requirements.md),
|
||||
covering issue #2540. This captures the concrete API shape, annotations, and open-question decisions made
|
||||
during design review — before implementation begins.
|
||||
|
||||
## Two source-vs-target mapping pipelines
|
||||
|
||||
Ebean now has (or will have) two distinct DTO pipelines. It's important callers can tell which one they're
|
||||
using:
|
||||
|
||||
1. **`asDto(Dto.class)`** (existing) — a `DtoQuery`, executed directly against a flat SQL `ResultSet`.
|
||||
One row -> one DTO, via constructor/setter matching. No nested ToOne/ToMany support, no entity graph
|
||||
involved.
|
||||
2. **`mapTo(Dto.class)`** (new) — runs the normal ORM entity query (joins/fetches as usual), producing an
|
||||
*unmodifiable entity graph*, then maps that Java object graph into a DTO graph. Supports nested
|
||||
ToOne/ToMany, identity-aware de-duplication, and derives its own fetch spec from the DTO shape.
|
||||
|
||||
## Proposed API
|
||||
|
||||
```java
|
||||
// Existing flat DtoQuery pipeline — unchanged
|
||||
new QUser().valid.eq(true)
|
||||
.select(firstName, lastName)
|
||||
.asDto(UserInfo.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
```java
|
||||
// NEW: nested DTO graph pipeline
|
||||
public class CustomerDto {
|
||||
Long id;
|
||||
String name;
|
||||
AddressDto billingAddress; // ToOne -> nested DTO, matched by property name "billingAddress"
|
||||
List<ContactDto> contacts; // ToMany -> nested DTO list, matched by property name "contacts"
|
||||
|
||||
@DtoPath("billingAddress.line1")
|
||||
String billingLine1; // renamed / flattened path
|
||||
}
|
||||
|
||||
public class ContactDto {
|
||||
Long id;
|
||||
String firstName;
|
||||
String lastName;
|
||||
|
||||
@DtoRef
|
||||
Long customerId; // id-only back-reference, avoids re-embedding CustomerDto (cycle)
|
||||
}
|
||||
|
||||
List<CustomerDto> dtos = new QCustomer()
|
||||
.status.eq(Status.ACTIVE)
|
||||
.mapTo(CustomerDto.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Computed values (e.g. `cityOrUnknown` derived from `coalesce(billingAddress.city, 'Unknown')`) are
|
||||
**not** modeled with a `@Formula2` annotation directly on the DTO - that was explored and rejected
|
||||
(see "Formula2-on-DTO scope" below). Instead they're modeled as a plain matching field on the DTO,
|
||||
sourced from an `@Entity @View` entity that itself carries the `@Formula2` - see "Computed/aggregate
|
||||
properties" below for the worked example.
|
||||
|
||||
`mapTo(CustomerDto.class)`:
|
||||
- Introspects `CustomerDto` (recursively, at codegen time) to derive the `select(...)`/`.fetch(...)` spec
|
||||
automatically from the DTO's declared shape.
|
||||
- Forces `setUnmodifiable(true)` under the hood — gives fail-fast + a cheap, non-mutable source graph
|
||||
(satisfies the fail-fast requirement without a separate flag).
|
||||
- Runs the query, then runs a mapper over the resulting entity graph, de-duplicating DTO instances by id
|
||||
for repeated nested references (identity-aware, mirrors the source entity graph's own de-duplication).
|
||||
|
||||
## Decisions made
|
||||
|
||||
### Fetch spec: auto-derived from DTO shape
|
||||
|
||||
The DTO's declared structure (fields, nested DTO types, `@DtoPath` overrides) is the single source of truth
|
||||
for what gets selected/fetched from the database. Callers do not need to separately maintain a `.fetch(...)`
|
||||
spec in parallel with the DTO — this directly addresses the original issue's pain point (DTO and query
|
||||
projection drifting out of sync).
|
||||
|
||||
### Entry point naming: `mapTo(Dto.class)`
|
||||
|
||||
Chosen over `asGraph(...)` / `into(...)` / overloading `findList(Class)`. Reads clearly as "map the
|
||||
resulting entity graph to this DTO type" and is unambiguous against the existing `asDto(...)` (flat,
|
||||
SQL-row-based) mechanism.
|
||||
|
||||
### Cycle handling: codegen-time DAG check + `@DtoRef` escape hatch
|
||||
|
||||
Because the fetch spec and mapper are both derived from the *static* DTO type graph (not live object
|
||||
traversal), cycle detection is a compile-time/codegen-time concern, not a runtime one. This is stronger
|
||||
than the common approach in the ecosystem:
|
||||
|
||||
- **MapStruct** does not auto-detect cycles. It offers an opt-in `@Context` "cycle guard" pattern (an
|
||||
identity map of already-mapped source -> target objects) that the developer must wire up manually to
|
||||
avoid infinite recursion mapping bidirectional object graphs.
|
||||
- **Blaze-Persistence / QueryDSL / JOOQ record mapping** avoid the problem architecturally: view/projection
|
||||
types are required to be a strict tree; a back-reference is modeled as an id or a much shallower type,
|
||||
never the same full view type again.
|
||||
|
||||
Ebean's approach: fail the build at annotation-processing time if a DTO's declared type graph is not a DAG,
|
||||
with a clear error message. Provide `@DtoRef` as an explicit escape hatch for intentional back-references
|
||||
(e.g. `Contact.customer`) — it maps only the id, not the full nested DTO, breaking the cycle by design
|
||||
rather than by runtime guard.
|
||||
|
||||
### `@DtoPath` / `@DtoRef`: parallels for readers coming from MapStruct or Blaze-Persistence
|
||||
|
||||
Neither annotation is a novel concept - both map onto things MapStruct and Blaze-Persistence users will
|
||||
already recognise, which is worth spelling out explicitly so it's easy to "grok fast":
|
||||
|
||||
- **`@DtoPath("billingAddress.line1")` is Ebean's equivalent of MapStruct's dot-path `source` flattening**
|
||||
— e.g. `@Mapping(target = "line1", source = "billingAddress.line1")`. MapStruct auto-generates a
|
||||
null-safe chain of getter calls for a dotted `source`; `@DtoPath` does exactly the same thing, just
|
||||
declared on the DTO field itself rather than on a mapper method parameter list. It is also close to
|
||||
Blaze-Persistence's `@Mapping("billingAddress.line1")` on an `@EntityView` attribute, which is a JPQL
|
||||
path expression evaluated the same way — Blaze's placement (directly on the target view property) is
|
||||
actually the closer analogue of the two, since Ebean's `@DtoPath` is likewise placed on the DTO field.
|
||||
The difference from Blaze: `@DtoPath` is restricted to plain getter-chain navigation (no arbitrary JPQL/
|
||||
SQL expression) - see "Formula2-on-DTO scope" below for the boundary and why full expression support is
|
||||
deliberately deferred.
|
||||
- **`@DtoRef` has no dedicated equivalent in either tool** - both MapStruct and Blaze would express the
|
||||
same "just the id" mapping as a plain dot-path to `.id` (`@Mapping(source = "customer.id")` / Blaze
|
||||
`@Mapping("customer.id")`), with no special marker for it. What `@DtoRef` adds beyond that shorthand is
|
||||
*intent*: it tells the codegen this property is a deliberate cycle-breaking reference, so (a) it adds
|
||||
just the association's own name (not a dotted `.id` path) to the generated fetch spec's root
|
||||
`select(...)` - reading the FK column directly with no join, and skipped entirely if that same
|
||||
association is already fully fetched by a `NESTED_ONE`/`NESTED_MANY` property elsewhere on the same DTO
|
||||
(see `DtoMapperWriter.fetchGroupChainCalls()`'s `case REF` branch) - and (b) it participates in the
|
||||
codegen-time DAG cycle check above as an explicit "this is fine, don't flag it" signal, rather than
|
||||
requiring a suppression escape hatch bolted on afterwards.
|
||||
|
||||
**Bug found and fixed while building the aggregation worked example below:** the original implementation
|
||||
excluded `REF` properties from the fetch spec *entirely*, on the assumption the id is "already available
|
||||
off an unfetched reference without triggering a fetch/lazy load". That assumption is only true when some
|
||||
*other* property on the same DTO happens to also fetch that association (as was always the case in the
|
||||
existing hand-built examples). Tested directly against a bare `@ManyToOne` with no other fetch of it:
|
||||
accessing `.getCustomer().getId()` in that case triggers a full lazy-reload of the owning row (extra SQL,
|
||||
not free) - and for an aggregation query it's worse, since the property being grouped by must be selected
|
||||
or the query can't group correctly at all. Fixed so `REF` always contributes its association name to the
|
||||
root `select(...)` (deduped against any existing `NESTED_ONE`/`NESTED_MANY` fetch of the same path).
|
||||
|
||||
**Bug found and fixed (validation phase, testing against `central-access`): primitive-typed field +
|
||||
nullable intermediate hop = unboxing `NullPointerException`.** A multi-hop `@DtoPath` (or `@DtoRef`,
|
||||
which is always 2-hop) null-guards each intermediate getter with a ternary, e.g.
|
||||
`(source.getOrganisation() == null ? null : source.getOrganisation().getId())`. That ternary's static
|
||||
type is always the boxed wrapper (`Long`), since one branch is the `null` literal - fine when the DTO
|
||||
field is itself a reference type (`Long organisationId`), but when the DTO field is a **primitive**
|
||||
(`long organisationId`), passing that boxed expression to the constructor auto-unboxes it, throwing an
|
||||
unhelpful `NullPointerException` at runtime whenever the relation really is `null`. This compiled clean
|
||||
and only failed at runtime with real (nullable) production data - exactly the kind of gap a hand-written
|
||||
mapper would defensively guard against (e.g. `cEbox.getOrganisation() == null ? 0 : ...getId()`) but
|
||||
generated code didn't.
|
||||
|
||||
Fixed in the generator: when a multi-hop `SCALAR`/`REF` property's DTO field type is primitive, the
|
||||
whole null-guarded chain is now wrapped in a small runtime helper (`io.ebean.DtoMapperSupport`) that
|
||||
resolves it safely:
|
||||
- **Default** (`@DtoPath` with no `failOnNull`, or any `@DtoRef`): silently defaults to the primitive's
|
||||
zero-equivalent value (`0`/`false`/etc.) - matches the old hand-written-mapper convention.
|
||||
- **`@DtoPath(failOnNull = true)`**: throws a clear `IllegalStateException` naming the offending property
|
||||
path instead, for callers who'd rather fail fast than silently mask a null they don't expect.
|
||||
|
||||
`@DtoRef` has no `failOnNull` attribute (it has no other attributes at all) - it always uses the
|
||||
default (silent zero) behaviour. See `PrimitiveNullPathDto`/`PrimitiveNullPathFailOnNullDto` /
|
||||
`TestPrimitiveNullPath` for regression coverage.
|
||||
|
||||
### Read-only entity memory overhead: `InterceptReadOnly`
|
||||
`setUnmodifiable(true)` isn't just a behavioural fail-fast flag - it also swaps the per-bean intercept
|
||||
implementation to `InterceptReadOnly`, which is deliberately minimal: just a `boolean[] loaded` (one flag
|
||||
per property) and a `boolean frozen`, plus the inherited owner reference and `fullyLoadedBean` flag. Compare
|
||||
to `InterceptReadWrite` (the mutable/updatable variant), which additionally carries a `ReentrantLock`, four
|
||||
transient collaborator references (`NodeUsageCollector`, `PersistenceContext`, `BeanLoader`,
|
||||
`PreGetterCallback`), a `byte[] flags` array (per-property loaded+changed+dirty+orig-value-set state),
|
||||
`Object[] origValues`, `Exception[] loadErrors`, `MutableValueInfo[]`/`MutableValueNext[]`, and several more
|
||||
scalar bookkeeping fields. None of that is needed for a bean that will only ever be read, so
|
||||
`setUnmodifiable(true)` graphs carry meaningfully less per-instance overhead than normal fetched entities -
|
||||
relevant here because `mapTo(Dto.class)` forces `setUnmodifiable(true)` on its underlying query, making the
|
||||
*source* graph for a DTO mapping cheaper than the equivalent normal (writable) entity graph would be.
|
||||
|
||||
### Ad-hoc computed/formula properties: model as `@Entity @View`/`@Sql`, not ad-hoc SQL-on-DTO
|
||||
|
||||
The "fully ad-hoc SQL-on-DTO" stretch goal above (closer to Blaze's arbitrary `@Mapping` expressions) doesn't
|
||||
need to be built as a bespoke DTO-annotation-processing feature. Ebean already supports modelling read-only,
|
||||
computed, or view-backed data as ordinary entities via `@Entity` + `@View` (backed by a SQL view, e.g. one
|
||||
with aggregates/computed columns) or `@Entity` + `@Sql` (backed by arbitrary `RawSql`, no base table). Given
|
||||
that, the Blaze-Persistence-style "an entity view attribute backed by an arbitrary SQL expression" need can
|
||||
usually be satisfied by:
|
||||
|
||||
1. Modelling the computed/derived shape as its own `@Entity @View` (or `@Sql`) "read entity" - the SQL
|
||||
expression/aggregation lives in the view definition, not in a new annotation-processed DTO mechanism.
|
||||
`@View`'s `name()` doesn't have to point at a genuinely separate database view - it can just point at
|
||||
an *existing* table (e.g. `@View(name = "contact")` on a second entity class reading the same table as
|
||||
`Contact`) purely to mark the entity as view-like/read-only, in which case Ebean's DDL generator emits
|
||||
**no new table or view at all** for it - it's just a second lens onto the same physical data.
|
||||
2. Mapping *that* entity into a plain DTO using the existing, already-implemented `@DtoMapping` machinery -
|
||||
no ad-hoc-SQL-on-DTO support required, since there's no computed expression left to resolve at the DTO
|
||||
layer at all; it's just another entity-to-DTO mapping.
|
||||
3. This read entity benefits from the same `setUnmodifiable(true)`/`InterceptReadOnly` memory efficiency
|
||||
above when used purely as `mapTo(...)` input, so there's no meaningful cost to preferring this over a
|
||||
hypothetical native ad-hoc-SQL-on-DTO feature.
|
||||
|
||||
This significantly narrows (and may eliminate) the case for a dedicated ad-hoc-SQL-on-DTO mechanism - it
|
||||
remains listed as an open stretch goal below primarily for the case where a computed value's SQL is genuinely
|
||||
one-off/DTO-specific and not worth promoting to a standalone `@View`/`@Sql` entity.
|
||||
|
||||
**Worked example** (`tests/test-dto-mapping`): `ContactSummary` is `@Entity @View(name = "contact")` (no new
|
||||
DDL - reads the same table as `Contact`) with `@Formula2("concat(firstName, ' ', lastName)")` computing
|
||||
`fullName`; `ContactSummaryDto` is a plain two-field DTO; `@DtoMapping(source = ContactSummary.class, target
|
||||
= ContactSummaryDto.class)` generates `ContactSummaryDtoMapper` exactly like any other entity→DTO pair - the
|
||||
formula property is just selected like any other field (`select("id,fullName")` in the generated
|
||||
`fetchGroup()`). See `TestContactSummaryDtoMapping`.
|
||||
|
||||
### Aggregate/group-by computed properties: `@Sum`/`@Aggregation` as `@Entity @View`, same pattern
|
||||
|
||||
Ebean's `@Sum` (shorthand for `@Aggregation("sum($1)")`) and `@Aggregation("count(...)"/"sum(...)"/"avg(...)"/
|
||||
"min(...)"/"max(...)")` are the group-by parallel to the formula pattern above - the same `@Entity @View`
|
||||
approach applies, just with an implicit `GROUP BY` instead of a per-row computed column. Ebean auto-derives
|
||||
the `GROUP BY` clause from whichever non-aggregate properties end up in the query's `select()`/`fetch()` - so
|
||||
a second `@Entity @View(name = <same base table>)` entity with one or more `@Sum`/`@Aggregation` properties
|
||||
plus a `@ManyToOne` grouping key becomes a per-parent rollup, with **no new table/view and no explicit
|
||||
`.groupBy()` call required**. This is Ebean's parallel to Blaze-Persistence entity view correlated aggregate
|
||||
mappings, e.g. `@Mapping("SIZE(contacts)")` / `@Mapping("SUM(contacts.engagementScore)")` on an `@EntityView`.
|
||||
|
||||
**Nuance found while building the worked example, and since fixed: `@DtoRef` originally didn't fit the
|
||||
grouping key.** `@DtoRef` was originally excluded from the generated `select()`/`fetch()` spec entirely, on
|
||||
the premise that the id is already available off an unfetched reference for an ordinary entity graph. That
|
||||
premise doesn't hold for an aggregation query: the `@ManyToOne` *is* the property being grouped by, so if
|
||||
it's never selected, the query has nothing to group by. It turned out the premise didn't fully hold for
|
||||
ordinary entity graphs either - see the `@DtoRef` bug writeup above. Fixed so `@DtoRef` now adds the
|
||||
association's own name to the root `select(...)` (reading the FK column directly, no join) - which both
|
||||
supplies the grouping key here and fixes the general-case gap.
|
||||
|
||||
**Worked example** (`tests/test-dto-mapping`): `ContactStats` is `@Entity @View(name = "contact")` (no new
|
||||
DDL - reads the same table as `Contact`/`ContactSummary`) with `@Aggregation("count(id)") contactCount` and
|
||||
`@Sum Integer engagementScore` (a new nullable field added to `Contact` purely to have something to sum),
|
||||
grouped by its `@ManyToOne customer`. `ContactStatsDto` is a flat 3-field DTO (`customerId`, `contactCount`,
|
||||
`engagementScore`), with `customerId` mapped via plain `@DtoRef`. The generated `ContactStatsDtoMapper`:
|
||||
|
||||
```java
|
||||
this.fetchGroup = FetchGroup.of(ContactStats.class)
|
||||
.select("customer,contactCount,engagementScore")
|
||||
.build();
|
||||
...
|
||||
// skip DtoMapContext, only ever a top-level mapping
|
||||
return new ContactStatsDto(
|
||||
(source.getCustomer() == null ? null : source.getCustomer().getId()),
|
||||
source.getContactCount(),
|
||||
source.getEngagementScore());
|
||||
```
|
||||
|
||||
confirmed (via `LoggedSql`) to produce `select t0.customer_id, count(t0.id), sum(t0.engagement_score) from
|
||||
contact t0 ... group by t0.customer_id` - **no join**, one row per customer, correctly summed and counted.
|
||||
See `TestContactStatsDtoMapping`.
|
||||
|
||||
### Formula2-on-DTO scope (v1): existing entity formulas only
|
||||
|
||||
`@Formula2` on a DTO property in v1 only pulls in a formula **already declared on the source entity** (or
|
||||
a reachable associated entity) — it does not support fully ad-hoc SQL declared directly on the DTO with no
|
||||
matching entity property. Fully ad-hoc SQL-on-DTO (closer to Blaze's arbitrary `@Mapping` expressions) is a
|
||||
separate, larger stretch goal to revisit once the core graph-mapping mechanism is proven.
|
||||
|
||||
**Attempted and rejected for v1.** A narrower version was implemented (`@Formula2(value)` resolved exactly
|
||||
like `@DtoPath` - a dot-path getter chain - plus a codegen-time validation that the resolved entity property
|
||||
is itself `@Formula2`/`@Formula`-annotated) but was rejected: for the common case (a DTO field with the same
|
||||
name as the entity's formula property) it generated **identical code to a plain unannotated field** - the
|
||||
only difference was the validation, which wasn't judged enough distinct value to justify a new annotation
|
||||
surface. Not implemented. The only way `@Formula2`-on-DTO would add real value is the full ad-hoc-SQL
|
||||
capability described above, which remains an open stretch goal.
|
||||
|
||||
### Mapper implementation strategy: codegen, not reflection (native-image constraint)
|
||||
|
||||
Native-image support is a core Ebean requirement, so the entity-graph -> DTO-graph mapper must not rely on
|
||||
runtime reflection or `MethodHandles`. This ruled out an initial reflection-based spike:
|
||||
|
||||
- Ebean's existing flat `DtoQuery` (`DtoMetaConstructor`) already uses `MethodHandles` via
|
||||
`Lookups.getLookup()`, but there is no `reflect-config.json` / native-image reachability metadata shipped
|
||||
for it anywhere in the repo. That existing approach is not a clean precedent to copy for a bigger,
|
||||
native-image-first feature.
|
||||
- Instead, the approach mirrors `querybean-generator`, which already generates real `.java` source for
|
||||
`Q*` query bean types (not reflection) — consistent with the wider avaje-ecosystem convention
|
||||
(avaje-inject / avaje-jsonb are explicitly reflection-free via compile-time codegen).
|
||||
|
||||
**Implementation sequencing:** hand-write the mapper in the exact shape the annotation processor will
|
||||
eventually generate (plain Java, direct getter/constructor/setter calls, zero reflection) for one concrete
|
||||
example first, to validate the mapping algorithm and API shape quickly without ever introducing throwaway
|
||||
reflective code. That hand-written mapper then becomes the target/acceptance-test shape for the
|
||||
`querybean-generator` annotation processor that automates producing it.
|
||||
|
||||
### Codegen target: Java first
|
||||
|
||||
The mapper generation (requirement r2) targets `querybean-generator` (the existing APT module that already
|
||||
generates `Q*` query beans, reusing its `PropertyMeta` / `ProcessingContext` machinery). Kotlin parity via
|
||||
`kotlin-querybean-generator` is deferred to a later phase — not blocking initial delivery.
|
||||
|
||||
### Mapper composition: one mapper per entity/DTO pair, generic `DtoMapper<SOURCE, TARGET>` interface
|
||||
|
||||
Rather than one large mapper inlining every nested DTO type, each entity/DTO pair gets its own small
|
||||
mapper class - mirroring MapStruct's per-type mapper generation. All mappers implement a shared generic
|
||||
interface (prototyped as `org.tests.dtomapping.DtoMapper<SOURCE, TARGET>` in the spike, expected to move to
|
||||
`io.ebean` as a public type once solidified):
|
||||
|
||||
```java
|
||||
public interface DtoMapper<SOURCE, TARGET> {
|
||||
TARGET map(SOURCE source);
|
||||
default List<TARGET> mapList(List<SOURCE> source) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
A parent mapper composes nested mappers via **constructor injection**, not a static singleton:
|
||||
|
||||
```java
|
||||
public final class CustomerDtoMapper implements DtoMapper<Customer, CustomerDto> {
|
||||
private final DtoMapper<Address, AddressDto> addressMapper;
|
||||
|
||||
public CustomerDtoMapper() {
|
||||
this(new AddressDtoMapper());
|
||||
}
|
||||
|
||||
public CustomerDtoMapper(DtoMapper<Address, AddressDto> addressMapper) {
|
||||
this.addressMapper = addressMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerDto map(Customer source) {
|
||||
if (source == null) return null;
|
||||
return new CustomerDto(source.getId(), source.getName(), addressMapper.map(source.getBillingAddress()));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rationale:
|
||||
- **Composability & reuse** - the same nested DTO type (e.g. `AddressDto`) used from multiple parent DTOs
|
||||
reuses one generated mapper class rather than duplicating inline mapping logic.
|
||||
- **Constructor injection over static state** - avoids a global mutable singleton; a no-arg constructor
|
||||
gives the common case (default nested mapper), while an overload accepting the nested mapper explicitly
|
||||
allows substitution (tests, customization) without touching global state.
|
||||
- **Codegen-friendly** - this shape generates naturally: one top-level mapper class per DTO type, each
|
||||
constructor-injecting the mappers for any nested DTO types it references.
|
||||
|
||||
## ToMany collections and identity de-duplication (dto-spike-tomany-identity)
|
||||
|
||||
Extending the spike (`ebean-test/src/test/java/org/tests/dtomapping/`) to a `Customer` with a
|
||||
`List<Contact> contacts` ToMany, where each `Contact` has a `customer` back-reference, surfaced
|
||||
two things worth recording.
|
||||
|
||||
### The `DtoMapper` interface threads a shared context
|
||||
|
||||
`DtoMapper<SOURCE, TARGET>` was extended so that mapping is always done against a `DtoMapContext`:
|
||||
|
||||
```java
|
||||
public interface DtoMapper<SOURCE, TARGET> {
|
||||
TARGET map(SOURCE source, DtoMapContext context);
|
||||
|
||||
default TARGET map(SOURCE source) {
|
||||
return map(source, new DtoMapContext());
|
||||
}
|
||||
|
||||
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) { ... }
|
||||
|
||||
default List<TARGET> mapList(List<SOURCE> source) {
|
||||
return mapList(source, new DtoMapContext());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`DtoMapContext` is an identity-keyed cache of already-mapped source -> target instances, created
|
||||
once per top-level `mapList(...)`/`map(...)` call and threaded through every nested `map(...)`
|
||||
call. This lets repeated references to the *same* source entity instance - which Ebean's own
|
||||
persistence context already de-duplicates within one query (`contact.getCustomer() == customer`
|
||||
for the enclosing `Customer`, confirmed by an existing test) - map to the *same* target DTO
|
||||
instance, rather than each producing an equal-but-distinct copy. This is what makes the mapped
|
||||
DTO output "graph shaped" rather than "tree of copies shaped", and is required for r1/r3.
|
||||
|
||||
### Bug found and fixed: the cache must be partitioned by target type, not just source identity
|
||||
|
||||
The first cut of `DtoMapContext` was a single `IdentityHashMap<Object, Object>` keyed only by the
|
||||
source instance. This breaks as soon as the *same* source instance legitimately needs to map to
|
||||
*two different target types* within one graph - which happens immediately with a back-reference:
|
||||
|
||||
- The top-level `CustomerDtoMapper` maps a `Customer` -> full `CustomerDto`.
|
||||
- The nested `ContactDtoMapper`, mapping `contact.getCustomer()` (the *same* `Customer` instance,
|
||||
by identity), maps it -> shallow `CustomerRefDto` (the `@DtoRef`-style escape hatch that avoids
|
||||
the `Customer -> Contact -> Customer` cycle).
|
||||
|
||||
With a single un-partitioned identity map, whichever mapper runs first "wins" the cache slot for
|
||||
that `Customer` instance, and the other mapper incorrectly receives the wrong-typed cached result
|
||||
(a `ClassCastException` at best, silently wrong data at worst). This was caught by a failing test
|
||||
during the spike and fixed by partitioning the cache per target type:
|
||||
|
||||
```java
|
||||
public final class DtoMapContext {
|
||||
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
|
||||
|
||||
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
|
||||
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
|
||||
// ... existing/create/put ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each generated mapper passes its own target DTO `Class` as the first argument, so `Customer ->
|
||||
CustomerDto` and `Customer -> CustomerRefDto` are cached independently even though the key
|
||||
(`Customer` instance) is identical. **This is an implementation detail the codegen must get
|
||||
right** - worth flagging explicitly when `dto-codegen-mapper` starts, since it's easy to
|
||||
regress if the generator is written from scratch without this test coverage in front of it.
|
||||
|
||||
### Codegen optimization: skip the `DtoMapContext` cache for types that are never nested elsewhere
|
||||
|
||||
`DtoMapContext.computeIfAbsent` only ever produces a cache *hit* when the exact same source
|
||||
instance is presented to `map()` more than once within one top-level call - which can only happen
|
||||
when the target type is reachable via more than one path in the graph, i.e. it's used as a
|
||||
`NESTED_ONE`/`NESTED_MANY` property by some *other* `@DtoMapping` pair (e.g. `CustomerRefDto`
|
||||
reached from many `Contact`s via a shared `Customer`, or `AddressDto` shared as `billingAddress`
|
||||
across customers). A type that's only ever a top-level `mapTo(...)`/`mapList(...)` entry point can
|
||||
never receive the same source instance twice within one call - Ebean's own query engine already
|
||||
de-duplicates root entity instances - so the cache lookup/insert there is pure overhead with a
|
||||
guaranteed-never-hit `IdentityHashMap`.
|
||||
|
||||
Since all `@DtoMapping` pairs are resolved together at codegen time (`DtoMappingReader.
|
||||
resolveAndValidate()`), it's straightforward to compute this: after cycle exclusion, walk every
|
||||
surviving `DtoBeanMeta`'s properties and mark any `nested()` target as `nestedElsewhere()`. The
|
||||
generated `map()` method then branches per mapper:
|
||||
|
||||
```java
|
||||
// CustomerDto - never nested by another mapper, only a mapTo()/mapList() entry point
|
||||
public CustomerDto map(Customer source, DtoMapContext context) {
|
||||
if (source == null) return null;
|
||||
// DtoMapContext for nested mappers only
|
||||
return new CustomerDto(source.getId(), source.getName(),
|
||||
billingAddressMapper.map(source.getBillingAddress(), context),
|
||||
contactsMapper.mapList(source.getContacts(), context));
|
||||
}
|
||||
|
||||
// AddressDto - nested under CustomerDto.billingAddress, so may be shared across customers
|
||||
public AddressDto map(Address source, DtoMapContext context) {
|
||||
if (source == null) return null;
|
||||
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
|
||||
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(
|
||||
s.getId(), s.getLine1(), s.getCity()));
|
||||
}
|
||||
|
||||
// ContactSummaryDto - flat, top-level only, no nested children at all
|
||||
public ContactSummaryDto map(ContactSummary source, DtoMapContext context) {
|
||||
if (source == null) return null;
|
||||
// skip DtoMapContext, only ever a top-level mapping
|
||||
return new ContactSummaryDto(source.getId(), source.getFullName());
|
||||
}
|
||||
```
|
||||
|
||||
Deliberately terse, single-line comments - just enough for a developer skimming generated code (e.g.
|
||||
per the earlier `@Formula2`-on-DTO worked example) to know at a glance *why* a given mapper does or
|
||||
doesn't use the cache, without spelling out the full reachability argument inline every time (that
|
||||
lives here in the design doc instead). Note `CustomerDto`'s own construction skips the cache even
|
||||
though it *has* nested children - `context` is still threaded down to `billingAddressMapper`/
|
||||
`contactsMapper` since those target types (`AddressDto`, `ContactDto`) *are* nested elsewhere and
|
||||
still need the identity cache for themselves - hence the distinct "for nested mappers only" wording
|
||||
from the "only ever a top-level mapping" case (`ContactSummaryDto`), which has no children to thread
|
||||
a context to at all.
|
||||
|
||||
### Fetching a ToOne back-reference used only for its FK/id needs the FK property fetched too
|
||||
|
||||
Confirmed (via a first-cut test failure) that if a ToMany's element type has a ToOne back to its
|
||||
parent (e.g. `Contact.customer`), that FK property must itself be included in the fetch
|
||||
(`.fetch("contacts", "id,firstName,lastName,customer")`) even when the mapper only reads the id
|
||||
off the reference. Omitting it throws `LazyInitialisationException: Property not loaded:
|
||||
customer` on the `getCustomer()` call itself (not merely on a property access on the returned
|
||||
reference) - i.e. the earlier "ToOne reference access alone doesn't lazy load" finding
|
||||
(dto-validate-fetch-pagination) only holds once the ToOne/FK property is itself part of the
|
||||
fetch/select spec. This reinforces r6 (auto-deriving the fetch spec from DTO shape): the codegen
|
||||
must include a ToOne property in the fetch spec whenever a DTO needing it (even just its id) is
|
||||
reachable through a ToMany, not just at the top level.
|
||||
|
||||
### Test coverage added
|
||||
|
||||
- `TestCustomerDtoGraphMapping` extended to cover `contacts` ToMany mapping and to assert that
|
||||
sibling `ContactDto`s under the same customer share the identical `CustomerRefDto` instance.
|
||||
- `TestContactDtoGraphMapping` (new) - standalone `ContactDtoMapper` test focused specifically on
|
||||
the identity de-dup guarantee and null-source handling.
|
||||
|
||||
## Codegen foundation: avaje-prisms adopted in querybean-generator (dto-codegen-mapper, step 1)
|
||||
|
||||
Before writing the DTO-mapper annotation-processing logic itself, adopted `avaje-prisms`
|
||||
(`io.avaje:avaje-prisms`) in `querybean-generator` as the mechanism for reading the new
|
||||
`@DtoPath`/`@DtoRef` annotations at APT time, replacing what would otherwise be more hand-rolled
|
||||
`AnnotationMirror` walking (the existing pattern in `FindDbName.java`/`ReadModuleInfo.java`,
|
||||
left as-is/unmigrated - only the *new* annotations use prisms).
|
||||
|
||||
This mirrors the proven pattern already used in two sibling projects in the same ecosystem -
|
||||
`avaje-inject`'s `inject-generator` and `avaje-jsonb`'s `jsonb-generator` - both declare
|
||||
`@GeneratePrism(SomeAnnotation.class)` once and get a generated `SomeAnnotationPrism` with
|
||||
`isPresent(element)` / `getInstanceOn(element)` / `getOptionalOn(element)` and typed accessors
|
||||
for every annotation member (correctly handling `Class`-valued members, avoiding the classic
|
||||
`MirroredTypeException` dance).
|
||||
|
||||
Key property preserved: `querybean-generator` has **zero runtime/compile dependencies today**
|
||||
(confirmed via `mvn dependency:list` returning "none"), matching annotations by FQN string
|
||||
constants (`Constants.java`) rather than importing the actual annotation classes - deliberately
|
||||
keeping the processor free of any dependency footprint for consumers. Adding `avaje-prisms` (to
|
||||
generate the prism wrapper) and `ebean-annotation` (to reference `@DtoPath`/`@DtoRef` as literal
|
||||
`Class` values in `@GeneratePrism(...)`) as `optional` dependencies preserves this: `mvn
|
||||
dependency:list -DincludeScope=runtime` confirms every one of these (plus their own transitive
|
||||
deps: `avaje-prism-core`, `avaje-spi-service`, `avaje-spi-core`) is marked `(optional)`, so none
|
||||
of it propagates to a project that depends on `querybean-generator` (whether as a normal
|
||||
dependency or via `annotationProcessorPaths`).
|
||||
|
||||
New annotations were added to the separate `ebean-annotation` repo (`io.ebean.annotation`
|
||||
package, alongside `@Formula2`), not this repo:
|
||||
|
||||
```java
|
||||
@DtoPath("billingAddress.line1")
|
||||
String billingLine1; // rename/flatten a DTO property from a nested source path
|
||||
|
||||
@DtoRef
|
||||
Integer customerId; // id-only back-reference, breaks what would otherwise be a graph cycle
|
||||
```
|
||||
|
||||
Both use `@Target({FIELD, METHOD})` and `RetentionPolicy.CLASS` - visible to the annotation
|
||||
processor (including across module boundaries, since `CLASS` retention survives in the compiled
|
||||
`.class` file) but absent from runtime reflection, consistent with DTOs remaining plain,
|
||||
framework-free types with no runtime footprint.
|
||||
|
||||
Wiring changes in `querybean-generator`:
|
||||
- `pom.xml`: added `avaje-prisms` (`optional`, plus `annotationProcessorPaths` entry) and
|
||||
`ebean-annotation` (`optional`) dependencies; removed the previous `-proc:none` compiler arg
|
||||
(which would have suppressed `avaje-prisms`' own processor from running to generate the prism
|
||||
source) - annotation processing is now scoped to exactly `avaje-prisms` via the explicit
|
||||
`annotationProcessorPaths` list, so no other processor is auto-discovered.
|
||||
- `module-info.java`: added `requires static io.avaje.prism;` and `requires static
|
||||
io.ebean.annotation;` (`static` = compile-time only, matching the `optional` Maven scope).
|
||||
- New `package-info.java` declaring `@GeneratePrism(DtoPath.class)` and
|
||||
`@GeneratePrism(DtoRef.class)`, generating `DtoPathPrism`/`DtoRefPrism` into
|
||||
`target/generated-sources/annotations`.
|
||||
|
||||
Verified: full `querybean-generator` build + existing test suite pass unchanged, and a downstream
|
||||
full rebuild (`ebean-test` with `-am`) - which exercises the existing Q-bean codegen - also
|
||||
passes with no regressions.
|
||||
|
||||
### Trigger mechanism: `@DtoMapping(source, target)` on a neutral package-info.java
|
||||
|
||||
Considered and rejected: putting a `source`/entity-referencing annotation directly on the DTO
|
||||
class itself (e.g. `@Dto(Customer.class)` on `CustomerDto`). Rejected because DTO types are
|
||||
often owned/generated elsewhere (e.g. from an OpenAPI spec) and must not be forced to reference
|
||||
an internal persistence/entity type - that would leak internal domain types into a
|
||||
public-facing/generated DTO module.
|
||||
|
||||
Instead, adopted the same pattern `avaje-jsonb` uses for external/foreign types it doesn't own
|
||||
(`@Json.Import`): a repeatable annotation declared on a *neutral* holder - a `package-info.java`
|
||||
- naming the `source` entity and `target` DTO as a pair:
|
||||
|
||||
```java
|
||||
@DtoMapping(source = Customer.class, target = CustomerDto.class)
|
||||
@DtoMapping(source = Contact.class, target = ContactDto.class)
|
||||
package org.example.dto;
|
||||
```
|
||||
|
||||
`@DtoMapping` (new, in `ebean-annotation`) is `@Target({PACKAGE, MODULE})`,
|
||||
`@Retention(SOURCE)` (pure codegen trigger, never needed at runtime - unlike `@DtoPath`/
|
||||
`@DtoRef` which need `CLASS` retention to remain visible to the DTO field itself),
|
||||
`@Repeatable(DtoMapping.List.class)` following Java's own repeatable-annotation idiom. Neither
|
||||
the entity nor the DTO needs any annotation of its own.
|
||||
|
||||
**Generated mapper package placement** - also modeled directly on `avaje-jsonb`'s handling of
|
||||
`@Json.Import` for external types (`AdapterName`/`ProcessingContext.isImported`): defaults to the
|
||||
target DTO's own package, *unless* the source or target type belongs to a different Java module
|
||||
than the one being processed, in which case the generated mapper is placed in a package derived
|
||||
from the processing module's own name instead - avoiding a JPMS "split package" violation that
|
||||
would occur from generating source into a package owned by another module. An explicit
|
||||
`mapperPackage` attribute is available to override this for edge cases. Same-module (or
|
||||
non-modular/unnamed-module) projects are unaffected and just get the mapper alongside the DTO.
|
||||
|
||||
### mapTo(Class) dispatch: Class-token API + generated compile-time-safe registry
|
||||
|
||||
The original API sketch above (`mapTo(CustomerDto.class)`) predates the native-image/no-reflection
|
||||
decision. Rather than switching to an instance-based API (`mapTo(new CustomerDtoMapper())`),
|
||||
decided to keep the `Class`-token shape and generate a compile-time-safe registry to resolve it -
|
||||
no reflection, no `Class.forName`, just literal `Class` comparisons generated at build time, e.g.:
|
||||
|
||||
```java
|
||||
<S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> targetType) {
|
||||
if (sourceType == Customer.class && targetType == CustomerDto.class) {
|
||||
return (DtoMapper<S, D>) new CustomerDtoMapper();
|
||||
}
|
||||
if (sourceType == Contact.class && targetType == ContactDto.class) {
|
||||
return (DtoMapper<S, D>) new ContactDtoMapper();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
Dispatch is keyed on the **(source, target) pair**, not target alone - this matches how
|
||||
`@DtoMapping(source, target)` pairs are declared, allows the same DTO type to be mapped from more
|
||||
than one source entity without ambiguity, and lets `query.mapTo(dtoType)` fail fast with a clear
|
||||
`PersistenceException` (rather than an incorrect match) when `query.getBeanType()` doesn't pair
|
||||
with the requested DTO.
|
||||
|
||||
This mirrors the existing, already-proven `EbeanEntityRegister`/`EntityClassRegister` mechanism
|
||||
(`SimpleModuleInfoWriter.java`) that `querybean-generator` already generates per module for entity
|
||||
classes - a `List<Class<?>>` built from literal `SomeEntity.class` references, registered via
|
||||
`META-INF/services` (`ServiceLoader`, itself native-image-friendly with no extra reflection
|
||||
config needed for simple no-arg-constructor implementations). The DTO mapper registry follows the
|
||||
same per-module aggregation + `META-INF/services` registration shape, giving `mapTo(Class)` a
|
||||
concrete generated implementation to dispatch through at runtime without reflection anywhere in
|
||||
the chain.
|
||||
|
||||
### mapTo(Dto.class) runtime wiring (implemented)
|
||||
|
||||
`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`/
|
||||
`findStream()`/`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
|
||||
On first use it resolves the generated `DtoMapper<S, D>` for the query's `(getBeanType(), dtoType)`
|
||||
pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all generated
|
||||
`DtoMapperRegister`s, analogous to `DtoBeanManager`), then:
|
||||
|
||||
- applies `mapper.fetchGroup()` to the query via `query.select(fetchGroup)` - the fetch/select spec
|
||||
is entirely derived from the DTO's declared shape, no manual `.select()`/`.fetch()` needed;
|
||||
- forces `query.setUnmodifiable(true)` - the resulting entity graph is read-only input to the
|
||||
mapper, and any DTO property whose source wasn't actually fetched fails fast with
|
||||
`LazyInitialisationException` rather than silently lazy loading or returning `null`;
|
||||
- executes the query and maps the result(s) via `mapper.map(...)`/`mapper.mapList(...)`.
|
||||
|
||||
An unregistered `(source, dtoType)` pair throws a `PersistenceException` with a suggested
|
||||
`@DtoMapping` fix, at first use (i.e. `findList()`/`findOne()`), not at `mapTo(dtoType)` call time.
|
||||
|
||||
`MappedQuery<D>.usingMaster(boolean)`, `.usingTransaction(Transaction)`, and `.usingConnection(Connection)`
|
||||
all delegate directly to the underlying entity query, mirroring `Query`/`QueryBuilder`. This lets a
|
||||
caller retry against the master data source after a read-replica failure by calling
|
||||
`usingMaster(true)` on the *same* `MappedQuery` instance and re-invoking a find method - there's no
|
||||
need to rebuild the query and call `.mapTo(...)` again.
|
||||
|
||||
`MappedQuery<D>.findStream()` mirrors `QueryBuilder#findStream()` - the underlying entity query is
|
||||
streamed (supporting very large result sets, potentially using multiple persistence contexts
|
||||
internally) and each entity is mapped to its target DTO lazily as the stream is consumed. One
|
||||
`DtoMapContext` is shared across the whole stream (not per-element), so identity de-duplication of
|
||||
nested DTOs (e.g. several `Contact`s sharing the same `Customer`) still holds even when the source
|
||||
entities are never materialized into one `List` at all. As with the entity-level `findStream()`,
|
||||
callers must consume it via try-with-resources to ensure the underlying resources are closed.
|
||||
|
||||
|
||||
## Still open / to revisit during implementation
|
||||
|
||||
- Whether `.fetch(...)` calls can still be layered on top of a `mapTo(Dto.class)` query for explicit
|
||||
overrides. Currently the mapper's `fetchGroup()` is the *only* source of the fetch spec - any
|
||||
`.select()`/`.fetch()` calls made before `.mapTo(...)` are overwritten by it.
|
||||
- Whether `@DtoPath`/`@DtoRef` need additional attributes beyond a bare path/marker (e.g. an explicit
|
||||
target type on `@DtoRef` for disambiguation) once real DTOs with more complex shapes are codegen'd.
|
||||
- Behavior when a DTO property has no matching entity property and no `@DtoPath`/`@Formula2` override
|
||||
(fail at codegen time, most likely, consistent with the "fail fast" philosophy).
|
||||
- `@Formula2`-on-DTO mapping to Blaze-Persistence/QueryDSL-style computed properties - not yet
|
||||
implemented (see requirements doc); a narrower validation-only variant was attempted and rejected
|
||||
as not distinct enough from `@DtoPath` (see "Formula2-on-DTO scope" above). The broader ad-hoc-SQL
|
||||
case likely doesn't need a dedicated DTO feature at all - see "Ad-hoc computed/formula properties"
|
||||
above for the `@Entity @View`/`@Sql` alternative.
|
||||
- **Fetch-path collision between a `NESTED_ONE`/`NESTED_MANY` property and a `@DtoPath` property -
|
||||
found and fixed**: `DtoMapperWriter.fetchGroupChainCalls()` builds one `.fetch(path, ...)`
|
||||
chain-call per distinct fetch path, but the underlying `OrmQueryDetail.fetch(...)` unconditionally
|
||||
**overwrites** (rather than merges) any existing entry for the same path key. If a DTO declared a
|
||||
`NESTED_ONE`/`NESTED_MANY` property AND a `@DtoPath` property whose fetch-path prefix is the *exact
|
||||
same* path (e.g. a nested `AddressDto billingAddress` alongside `@DtoPath("billingAddress.line1")`
|
||||
on the same DTO - both resolve to fetch path `"billingAddress"`), the generator would emit two
|
||||
`.fetch("billingAddress", ...)` calls and the second would silently discard the first's selected
|
||||
properties. Merging wasn't practical - the nested property's `.fetch(path, mapper.fetchGroup())`
|
||||
call passes another mapper's own pre-built, immutable, shared `FetchGroup`, so there's no clean way
|
||||
to splice an extra scalar property into it at the call site. Fixed instead with a **fail-fast
|
||||
compile-time error**: `DtoMapperWriter` now detects the collision and raises a clear
|
||||
`ctx.logError(...)` (annotation-processor `ERROR` diagnostic, fails the compile) naming the
|
||||
colliding property and fetch path, and suggesting the two ways out - move the property onto the
|
||||
nested DTO type instead, or pick a `@DtoPath` that reaches a different, non-colliding path (as
|
||||
`ContactDto.customerCity` already does deliberately, per its own comment, using a 3-segment path).
|
||||
Verified empirically by compiling a small reproduction with a colliding `@DtoPath` and confirming
|
||||
the expected error fires; a permanent regression test
|
||||
(`DtoMapperFetchPathCollisionTest` in `querybean-generator`) now runs this same repro directly
|
||||
through `javax.tools.JavaCompiler` with the `Processor` registered, asserting the compile fails with
|
||||
the expected diagnostic message.
|
||||
- **Compile-time verification of `select(...).asDto(...)` (r6, aspirational) - explored and closed as
|
||||
rejected**: raw SQL is an opaque `String` at compile time, and even the typed query-bean
|
||||
`.select(...)` form only type-checks against the *entity* - the match to the target DTO's constructor
|
||||
still happens at runtime via reflection (`DtoQueryPlanConstructor`), and the `.asDto(...)` call site
|
||||
can be arbitrarily distant from the `.select(...)` call, so there's no fixed AST shape an annotation
|
||||
processor could reliably verify (unlike QueryDSL, whose compile-time safety actually comes from typed
|
||||
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking a select-list
|
||||
against a DTO). `mapTo(Dto.class)` already closes the underlying gap in the tractable direction - it
|
||||
derives the select/fetch spec *from* the DTO's declared shape at APT time, so it is compile-time safe
|
||||
by construction. Recommend `mapTo()` whenever compile-time-checked DTO projection matters, and treat
|
||||
`asDto()`/`findDto()` as the flexible, runtime-checked escape hatch for raw/dynamic SQL. See
|
||||
`dto-mapping-requirements.md` requirement r6.
|
||||
- **Custom property conversion (`@DtoConvert`/`@DtoMixin`, r13/r14) - implemented**: motivated by a
|
||||
real hand-written mapper (`DriverMapper`, central-access) needing both a dependency-free scalar
|
||||
coercion (`short` -> `boolean`) and a dependency-backed conversion (AES decryption via an injected
|
||||
cipher). Final design (see `dto-mapping-requirements.md` section E), as built:
|
||||
- `@DtoConvert(value = ConverterType.class, method = "name")` on a DTO property (combinable with
|
||||
`@DtoPath`); the generator dispatches on whether the referenced method is `static` - static means a
|
||||
direct inlined static call (no registration, covers common reusable coercions), instance means
|
||||
dispatch via a new `DtoConverterManager.get(ConverterType.class).method(...)` call, with the
|
||||
resolved instance wired as a real constructor parameter/field on the generated mapper (same shape
|
||||
as existing nested-mapper constructor injection). Multiple properties on the same mapper sharing
|
||||
the same converter type are deduplicated to a single constructor parameter/field
|
||||
(`DtoBeanMeta.converterDeps()`).
|
||||
- `DtoConverterManager` (`ebean-api`, `io.ebean` package) is a small, narrowly-scoped static put/get
|
||||
bridge - the app registers an already-DI-constructed converter singleton (e.g. built by
|
||||
avaje-inject) *before* building the `Database`. This is a deliberate, narrow exception to the
|
||||
general no-static-mutable-state convention: `ServiceLoader`-discovered, no-arg-constructed
|
||||
generated code (`EbeanDtoMapperRegister`) has no other way to reach an already-DI-constructed
|
||||
singleton. `DtoConverterManager.get(type)` throws immediately if nothing was registered for that
|
||||
type, so a missing converter fails fast at Database-startup time (an eager field initializer on
|
||||
`EbeanDtoMapperRegister`, and equally on each mapper's own no-arg constructor, which resolves the
|
||||
same way via `DtoConverterManager.get(...)` for standalone/test construction), not lazily on first
|
||||
use - `DtoMapperRegister`'s `mapperFor(...)` signature and `DtoMapperManager` are otherwise
|
||||
completely unchanged, as originally planned.
|
||||
- Two alternatives were explored and rejected first: (a) a `DtoMapContext.service(Class)` lookup -
|
||||
wrong lifetime, `DtoMapContext` is a short-lived per-call identity-cache only; (b) a
|
||||
`ServiceLoader`-discovered `DtoConverterSource` SPI mirroring `DtoMapperRegister` itself - can't
|
||||
bridge to an *already* DI-constructed dependency without reconstructing/duplicating it.
|
||||
- `@DtoMixin(Target.class)` - a companion type overlaying `@DtoPath`/`@DtoConvert`/`@DtoRef`
|
||||
annotations onto a DTO that can't be annotated directly (e.g. OpenAPI-generated). Discovered via
|
||||
`roundEnv.getElementsAnnotatedWith(...)` (added to `Processor.getSupportedAnnotationTypes()`,
|
||||
since - unlike `@DtoPath`/`@DtoRef`/`@DtoConvert` - a mixin doesn't annotate an already-iterated
|
||||
field of a known `@DtoMapping` target, so it can't be found lazily). `DtoMappingReader` resolves
|
||||
each target property's annotations from the field itself first, falling back to a same-named
|
||||
method on the registered mixin (`DtoMappingReader.prismOn(...)`) - directly mirrors avaje-jsonb's
|
||||
proven `@Json.MixIn` mechanism.
|
||||
- Implemented in `ebean-annotation` (`DtoConvert`, `DtoMixin`), `ebean-api` (`DtoConverterManager`),
|
||||
and `querybean-generator` (`DtoConverterMeta`, `DtoBeanMeta.converterDeps()`,
|
||||
`DtoMappingReader`/`DtoMapperWriter`/`DtoMapperRegisterWriter` changes). Test coverage:
|
||||
`tests/test-dto-mapping` `TestDtoConvert` (static + instance dispatch, fail-fast unregistered-type
|
||||
check) and `TestDtoMixin` (mixin overlay, including instance-dispatch conversion resolved purely
|
||||
from mixin-declared annotations). The instance-dispatch converter is registered via a
|
||||
`DatabaseConfigProvider` (ServiceLoader hook run before the `Database` is built) rather than a test
|
||||
`@BeforeAll`, since `EbeanDtoMapperRegister`'s mapper fields (including any needing
|
||||
`DtoConverterManager`) are all constructed eagerly during `Database` startup, which can be
|
||||
triggered by whichever test class in the module happens to run first.
|
||||
|
||||
- **Fixed (validation phase, found via `central-access`): `@DtoPath` through a computed/derived
|
||||
getter now fails at compile time, with an explicit `requires()` escape hatch.** `@DtoPath`
|
||||
assumes every dotted segment names a real, fetchable Ebean bean property - so a path like
|
||||
`@DtoPath("currentMachine.organisationMachine.registrationPlate")`, where `getOrganisationMachine()`
|
||||
is a hand-written derived getter (not a real relation/column), used to **compile cleanly** (the
|
||||
codegen had no way to tell it apart from a real property from source alone) but **fail at
|
||||
runtime** with a `PersistenceException: No property found for [organisationMachine] in
|
||||
expression ...`, because the generated `FetchGroup` builder tried to `fetch`/`select` it as if it
|
||||
were a real Ebean property.
|
||||
- Two genuinely separate sub-problems: (1) *detecting* that a path segment isn't a real,
|
||||
fetchable property - solvable at compile time, since a real persistent property always has a
|
||||
backing field (Ebean requires one to enhance), checked via `javax.lang.model`
|
||||
(`ElementFilter.fieldsIn(...)` over the type + superclass chain, see `DtoMappingReader.hasField(...)`);
|
||||
versus (2) *knowing what the computed getter needs fetched* to execute safely - not solvable at
|
||||
compile time without full static/bytecode analysis of the getter's method body, out of scope.
|
||||
- Resolution: don't attempt to infer (2) automatically. When `DtoMappingReader` detects a `@DtoPath`
|
||||
segment with no backing field, it now fails fast at compile time (`ctx.logError(...)`) unless the
|
||||
developer explicitly declares the real entity paths that must be fetched via
|
||||
`@DtoPath(requires = {...})` (dot-notation, same convention as `@DtoPath`'s own `value()`) - e.g.
|
||||
`@DtoPath(value = "primaryContact.lastName", requires = "contacts")` where `getPrimaryContact()`
|
||||
picks the first entry out of the `contacts` collection. The real prefix before the computed
|
||||
segment (if any) is automatically combined with the declared `requires()` paths, so the developer
|
||||
doesn't need to redundantly repeat it. Declared paths are emitted as bare `.fetch(path)` calls in
|
||||
the generated `FetchGroup` (distinct from the `.fetch(path, "props")` shape used for ordinary
|
||||
scalar `@DtoPath` properties, since there's no specific target property list to narrow to here).
|
||||
- **The zero-extra-fetch case is also supported, via an explicit `requires = {}`** - e.g.
|
||||
`@DtoPath(value = "idBadge", requires = {})` where `getIdBadge()` derives purely from `id`
|
||||
(always fetched regardless). An explicit empty array confirms "nothing extra needed", distinct
|
||||
from omitting `requires()` entirely ("not yet considered", still a compile error) - `requires()`
|
||||
itself can't tell the two cases apart (both read back as an empty `List`), so `DtoMappingReader`
|
||||
checks the avaje-prism-generated `DtoPathPrism.values.requires()` instead, which returns `null`
|
||||
only when the member was left at its default (i.e. omitted from source). `DtoPropertyMeta`
|
||||
correspondingly carries `hasComputedSegment()` as its own boolean flag (set whenever a computed
|
||||
segment was detected at all), independent of whether `requiredFetchPaths()` happens to be empty -
|
||||
an earlier version conflated the two (inferring "has a computed segment" from "has a non-empty
|
||||
requiredFetchPaths list"), which broke exactly this explicit-empty case by falling through to the
|
||||
ordinary scalar `.select(...)` path and failing at runtime with `PersistenceException: Property
|
||||
not found - idBadge` (`idBadge` isn't a real Ebean property, so it can't be selected).
|
||||
- Implemented in `ebean-annotation` (`DtoPath.requires()`), and `querybean-generator`
|
||||
(`DtoMappingReader` computed-segment detection/validation, `DtoPropertyMeta.requiredFetchPaths()`/
|
||||
`hasComputedSegment()`, `DtoMapperWriter.fetchGroupChainCalls()` bare-fetch emission). Test
|
||||
coverage: `tests/test-dto-mapping` `ComputedPathDto`/`TestComputedPath` (happy path, `requires`
|
||||
correctly fetches the dependency and the mapped value is correct), `ComputedPathNoFetchDto`/
|
||||
`TestComputedPathNoFetch` (explicit `requires = {}`, genuinely nothing extra needed), and
|
||||
`querybean-generator`'s `DtoMapperComputedPathTest` (negative case - omitting `requires` on a
|
||||
computed segment is a compile-time `ERROR` diagnostic, verified via direct `javax.tools.JavaCompiler`
|
||||
compilation, mirroring `DtoMapperFetchPathCollisionTest`).
|
||||
- Known gap: the dedup between the computed segment's required fetch paths and existing
|
||||
`pathSelect`/`nestedAssocPaths` keys in `DtoMapperWriter` is a simplified exact-path-string check
|
||||
(skip emitting a duplicate `.fetch(path)`), not full collision detection like the existing
|
||||
NESTED_ONE/MANY vs `@DtoPath` check - a bare `fetch(path)` and an existing `fetch(path,
|
||||
"specific,props")` for the same path string are not merged/reconciled, just left as two separate
|
||||
calls if that edge case arises.
|
||||
|
||||
- **Fixed: a single-hop `@DtoPath` rename through a computed/derived getter whose return type is
|
||||
itself a registered nested DTO (`NESTED_ONE`/`NESTED_MANY`, not `SCALAR`) bypassed the
|
||||
computed-segment validation above entirely.** E.g. `@DtoPath("primaryContact")` where the DTO
|
||||
field's declared type is `ContactDto` (a type with its own `@DtoMapping(source = Contact.class,
|
||||
target = ContactDto.class)`) and `getPrimaryContact()` is a computed getter with no backing
|
||||
field on `Customer`. This resolves to a single-segment path, so `DtoMappingReader.resolveProperty()`
|
||||
took its `properties.size() == 1` nested-lookup shortcut and returned early - before the
|
||||
`computedFrom`/`requires()` validation block (added for the `SCALAR` case above) ever ran. The
|
||||
generated `FetchGroup` then emitted a broken `fetch("primaryContact",
|
||||
contactMapper.fetchGroup())` call (`"primaryContact"` isn't a real Ebean fetch path), failing at
|
||||
runtime rather than compile time - the exact class of bug the `SCALAR` fix was meant to close off
|
||||
entirely.
|
||||
- Resolution: restructured `resolveProperty()` so the computed-segment detection/validation block
|
||||
runs *before* the `properties.size() == 1` nested-lookup branch, so both `SCALAR` and
|
||||
`NESTED_ONE`/`NESTED_MANY` paths share the same detection/validation. `DtoPropertyMeta` gained a
|
||||
matching constructor overload for `NESTED_ONE`/`NESTED_MANY` carrying `computedSegment`/
|
||||
`requiredFetchPaths`. In `DtoMapperWriter.fetchGroupChainCalls()`, a `NESTED_ONE`/`NESTED_MANY`
|
||||
property with `hasComputedSegment()` true is routed into `extraFetchPaths` (the same bare
|
||||
`.fetch(path)` mechanism as the `SCALAR` case) instead of emitting `fetch(path,
|
||||
mapper.fetchGroup())` - since the nested mapper's own `FetchGroup` requirements can't be
|
||||
meaningfully attached under a path name that doesn't exist on the source entity.
|
||||
- Note the nested mapper's *own* fetch requirements (e.g. if `ContactDto` itself needed
|
||||
`customer.billingAddress`) are **not** automatically propagated up through a computed segment -
|
||||
only whatever the computed getter itself needs (via `requires()`) is fetched. The nested
|
||||
mapper's `map(...)` call still works via plain Java method invocation regardless (Ebean
|
||||
transparent lazy loading covers any gap), but relying on that silently reintroduces N+1 queries,
|
||||
so the nested DTO used through a computed segment should ideally be a "leaf" shape needing
|
||||
nothing beyond what `requires()` already declares.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()` restructuring,
|
||||
`DtoPropertyMeta`'s new constructor overload, `DtoMapperWriter.fetchGroupChainCalls()`). Test
|
||||
coverage: `tests/test-dto-mapping` `ContactLeafDto`/`ComputedNestedDto`/`TestComputedNestedPath`
|
||||
(happy path - generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken
|
||||
`fetch("primaryContact", ...)` call, and the mapped value is correct end-to-end), and
|
||||
`querybean-generator`'s `DtoMapperComputedPathTest#dtoPathThroughComputedGetter_targetingNestedDto_withoutRequires_expectCompileError`
|
||||
(negative case, mirroring the `SCALAR` one). The `NESTED_MANY` variant (a computed getter
|
||||
returning a `List` of a type with its own registered nested DTO mapping) shares the identical
|
||||
code path but had no dedicated regression test until later confirmed via `Customer
|
||||
.getRecentContacts()` / `ComputedNestedListDto` / `TestComputedNestedListPath` (coverage only,
|
||||
not a bug fix - passed cleanly first try, confirming the shared code path does work end-to-end
|
||||
for both `NESTED_ONE` and `NESTED_MANY`).
|
||||
|
||||
- **Fixed: `@DtoRef` never checked for a computed/derived association getter at all.** Unlike
|
||||
`@DtoPath`, `@DtoRef`'s association name (derived by stripping the `Id` suffix off the field
|
||||
name, e.g. `primaryContactId` -> `primaryContact`) was never checked against `hasField(...)` -
|
||||
so `@DtoRef` on a computed getter (e.g. `getPrimaryContact()` picking the first entry out of a
|
||||
`contacts` collection) compiled cleanly and generated a broken `FetchGroup.select("primaryContact")`
|
||||
call (`"primaryContact"` isn't a real Ebean property), failing at runtime rather than compile
|
||||
time - the same class of bug as the original `@DtoPath` fix, just entirely unaddressed for
|
||||
`@DtoRef`'s separate code path.
|
||||
- Resolution: `@DtoRef` gained its own `requires()` attribute (dot-notation, same convention and
|
||||
explicit-empty semantics as `@DtoPath#requires()`, using the same `DtoRefPrism.values.requires()
|
||||
== null` omitted-vs-explicit-empty technique). `DtoMappingReader`'s `@DtoRef` branch now checks
|
||||
`hasField(meta.source(), assocName)` and fails fast at compile time (`ctx.logError(...)`) when
|
||||
the association has no backing field and `requires()` wasn't specified. `DtoPropertyMeta`'s
|
||||
`REF` properties now carry `computedSegment`/`requiredFetchPaths` through the existing fields
|
||||
(no new constructor needed - the full constructor already had the right shape).
|
||||
`DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case now checks `hasComputedSegment()` and
|
||||
routes into `extraFetchPaths` (bare `.fetch(path)`) instead of `rootSelect.add(assoc)` when
|
||||
true - the value expression itself (`source.getPrimaryContact().getId()`, null-guarded) is
|
||||
unaffected, since it's plain Java method invocation regardless of whether the association name
|
||||
is a real Ebean property.
|
||||
- Implemented in `ebean-annotation` (`DtoRef.requires()`), and `querybean-generator`
|
||||
(`DtoMappingReader`'s `@DtoRef` branch, `DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case).
|
||||
Test coverage: `tests/test-dto-mapping` `ComputedRefDto`/`TestComputedRefPath` (happy path -
|
||||
generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken `select("primaryContact")`
|
||||
call, and the mapped id is correct end-to-end), and `querybean-generator`'s
|
||||
`DtoMapperComputedPathTest#dtoRefThroughComputedGetter_withoutRequires_expectCompileError`
|
||||
(negative case, mirroring the `@DtoPath` ones).
|
||||
|
||||
- **Fixed: `requires()` path values themselves were never validated against the source type's
|
||||
real property graph.** `@DtoPath(requires = {...})`/`@DtoRef(requires = {...})` values are
|
||||
handed straight through to `FetchGroup.fetch(...)` unmodified - a typo (e.g. `requires =
|
||||
"contactz"` for the real `contacts` property) compiled cleanly, since only the *computed
|
||||
segment itself* was checked against `hasField(...)`, not the developer-declared dependency
|
||||
paths meant to fix it. That silently reintroduced the exact runtime `PersistenceException` the
|
||||
whole `requires()` escape hatch exists to prevent, just one step removed and harder to spot.
|
||||
- Resolution: added `DtoMappingReader.validateRequiresPath(...)`, which walks each dot-notation
|
||||
segment of a declared `requires()` value from the source root (`meta.source()`), checking
|
||||
`hasField(...)` at every hop exactly like `@DtoPath#value()`'s own segments are checked, and
|
||||
unwrapping a `java.util.List`-typed intermediate hop to its element type (via
|
||||
`listElementType(TypeMirror)`) so a collection segment followed by a further hop resolves
|
||||
correctly - needed a new `getterReturnTypeMirror(...)` helper (returning the raw `TypeMirror`
|
||||
rather than converting straight to `TypeElement`, which can't distinguish a `List` from any
|
||||
other declared type) alongside the existing `getterReturnType(...)`. Called for every entry in
|
||||
`pathPrism.requires()`/`refPrism.requires()` right after they're read, for both the `@DtoPath`
|
||||
and `@DtoRef` branches. The already-validated real prefix (segments before the computed one in
|
||||
a `@DtoPath#value()`) is intentionally *not* re-validated, since it was already checked while
|
||||
walking `value()` itself.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.validateRequiresPath(...)`,
|
||||
`getterReturnTypeMirror(...)`, called from both the `@DtoPath` and `@DtoRef` branches). Test
|
||||
coverage: `querybean-generator`'s
|
||||
`DtoMapperComputedPathTest#dtoPathRequires_withTypoInPathValue_expectCompileError` (negative
|
||||
case - a typo'd `requires()` segment is a compile-time `ERROR` diagnostic); existing
|
||||
`tests/test-dto-mapping`/`central-access` suites (real multi-segment `requires()` values like
|
||||
`"currentMachine.organisationMachines"`) continue to pass unchanged, confirming the validation
|
||||
doesn't false-positive on legitimate paths.
|
||||
|
||||
- **Fixed: a bare, full `requires()` fetch and a sibling property's narrowed `@DtoPath` fetch of
|
||||
the exact same path silently conflicted, with the narrow one always (incorrectly) winning.**
|
||||
`DtoMapperWriter.fetchGroupChainCalls()`'s dedup logic used to skip emitting a computed
|
||||
segment's bare `fetch(path)` call whenever another property's `@DtoPath` already had a narrowed
|
||||
`fetch(path, "specific,props")` entry for that exact path string - on the assumption the two
|
||||
were interchangeable/redundant. They aren't: `FetchGroup`'s builder (`OrmQueryDetail.fetch(...)`)
|
||||
keys fetch calls by path in a plain `Map` and **replaces** rather than merges same-path entries,
|
||||
so whichever call format was emitted meant the *other* was silently discarded. Since the narrow
|
||||
entry was always emitted first and the bare one skipped whenever it existed, the narrow selection
|
||||
always won - meaning a computed getter's `requires()` declaration could be completely ignored
|
||||
whenever an unrelated sibling `@DtoPath` happened to narrow-select the exact same path, leaving
|
||||
whatever extra properties the computed getter actually touches unfetched (a silent lazy load, or
|
||||
a hard `LazyInitialisationException` outside a persistence context).
|
||||
- Resolution: reversed the priority - `fetchGroupChainCalls()` now skips a narrowed `pathSelect`
|
||||
entry when `extraFetchPaths` (the computed segment's `requires()`) declares the exact same
|
||||
path, letting the bare, full `fetch(path)` call win instead. This is always safe since a full
|
||||
fetch is a superset of any narrower property selection - the narrow entry's own properties are
|
||||
included within it regardless. The existing `nestedAssocPaths` priority (a `NESTED_ONE`/
|
||||
`NESTED_MANY` property's full `fetch(path, mapper.fetchGroup())` always wins over a bare
|
||||
`fetch(path)`) was correct already and left unchanged - a nested mapper's own `FetchGroup` is
|
||||
strictly richer than either form and must not be replaced by either.
|
||||
- Implemented in `querybean-generator` (`DtoMapperWriter.fetchGroupChainCalls()`). Test coverage:
|
||||
`tests/test-dto-mapping` `FetchCollisionDto`/`TestFetchCollisionPath`, plus a new computed
|
||||
getter `Customer.getBillingSummary()` (reads `billingAddress.getLine1()`, deliberately a
|
||||
different `Address` property to the `city` narrowly selected by a sibling `@DtoPath` on the
|
||||
same DTO) - confirmed to reproduce `LazyInitialisationException: Property not loaded: line1`
|
||||
when the fix is reverted, and pass cleanly (correct `line1`-derived value, generated
|
||||
`FetchGroup` is `.select("id").fetch("billingAddress")` with no narrowed variant at all) with
|
||||
it in place.
|
||||
|
||||
- **Fixed: two `@DtoMixin` companion types targeting the same DTO class silently conflicted, with
|
||||
the second-processed one winning.** `DtoMappingReader.collectMixins()` keyed a single
|
||||
`mixinsByTarget` map by the target DTO's FQN, and `Map.put(...)` unconditionally overwrote any
|
||||
existing entry - so if two mixin interfaces (e.g. a legitimate one plus an accidental duplicate,
|
||||
or two independently-added mixins that both happened to target the same generated/unowned DTO)
|
||||
both declared `@DtoMixin(SameDto.class)`, whichever was visited last by
|
||||
`roundEnv.getElementsAnnotatedWith(...)` silently won, and *all* of the other mixin's
|
||||
`@DtoPath`/`@DtoRef`/`@DtoConvert` overlays were discarded with no diagnostic at all.
|
||||
- Resolution: `collectMixins()` now checks for an existing registration before storing a new one
|
||||
and raises a compile `ERROR` naming both the target and the already-registered mixin's
|
||||
qualified name, rather than silently overwriting it.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.collectMixins()`). Test coverage: new
|
||||
negative compile-error test `DtoMapperComputedPathTest#duplicateDtoMixin_forSameTarget_expectCompileError`
|
||||
(two minimal `@DtoMixin(FooDto.class)` interfaces both declaring a `bar()` method, compiled
|
||||
together, asserting the `Duplicate @DtoMixin` diagnostic is raised); existing
|
||||
`tests/test-dto-mapping` `TestDtoMixin` (single, legitimate mixin usage) continues to pass
|
||||
unchanged.
|
||||
|
||||
- **Fixed: `@DtoRef` and `@DtoPath` both present on the same field silently conflicted, with
|
||||
`@DtoRef` always (invisibly) winning.** `resolveProperty()` checked `refPrism != null` first and
|
||||
returned immediately whenever present, so a field carrying both annotations at once - whether by
|
||||
copy/paste mistake, a half-finished rename from one style to the other, or simple confusion
|
||||
between the two escape hatches - had its `@DtoPath` completely ignored with no diagnostic at all.
|
||||
- Resolution: `resolveProperty()` now resolves both prisms upfront and raises a compile `ERROR`
|
||||
naming the field when both are present, rather than silently picking `@DtoRef` and discarding
|
||||
`@DtoPath`.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`). Test coverage: new
|
||||
negative compile-error test `DtoMapperComputedPathTest#dtoRefAndDtoPath_onSameField_expectCompileError`
|
||||
(a field carrying both `@DtoRef` and `@DtoPath("bar.id")` over a real, non-computed association,
|
||||
isolating the conflict diagnostic from the separate computed-getter `requires()` diagnostics).
|
||||
|
||||
- **Fixed: `@DtoConvert` on a `NESTED_ONE`/`NESTED_MANY` property was silently ignored.**
|
||||
`resolveProperty()` resolves the property's `DtoConverterMeta` unconditionally up front (before
|
||||
it's known whether the property will resolve to `SCALAR`/`REF`/`NESTED_ONE`/`NESTED_MANY`), but
|
||||
only the `SCALAR`/`REF` `DtoPropertyMeta` constructors actually accept/store a converter - the
|
||||
`NESTED_ONE`/`NESTED_MANY` constructor calls never took one, so a resolved converter was simply
|
||||
dropped on the floor with no diagnostic. A developer adding `@DtoConvert` to a nested-DTO field
|
||||
(e.g. hoping to post-process the nested mapper's result) would see it silently do nothing -
|
||||
`DtoMapperWriter.propertyValueExpression()`'s `NESTED_ONE`/`NESTED_MANY` cases call straight into
|
||||
`mapperFieldName(property) + ".map(...)"`/`".mapList(...)"` with no converter wrapping at all.
|
||||
- Resolution: added `rejectConverterOnNested(...)`, called at each of the four call sites that
|
||||
construct a `NESTED_ONE`/`NESTED_MANY` `DtoPropertyMeta` (the single-hop `@DtoPath`-rename
|
||||
branch's two cases, and the plain non-`@DtoPath` branch's two cases) - raises a compile `ERROR`
|
||||
naming the field whenever a converter was resolved for it, rather than silently discarding it.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`,
|
||||
`rejectConverterOnNested()`). Test coverage: new negative compile-error test
|
||||
`DtoMapperComputedPathTest#dtoConvertOnNestedOne_expectCompileError` (a `NESTED_ONE` field
|
||||
carrying `@DtoConvert` over a legitimately nested, separately-`@DtoMapping`-registered type);
|
||||
existing `tests/test-dto-mapping` suite (no nested property currently combines `@DtoConvert`
|
||||
with `NESTED_ONE`/`NESTED_MANY`) continues to pass unchanged, confirming no false positives on
|
||||
plain nested properties.
|
||||
|
||||
- **Fixed: `@DtoConvert(method = ...)` resolution ignored parameter arity/overloads.** The shared
|
||||
`findMethod(type, name)` helper (also used for the builder's `build()` lookup and `@DtoMixin`
|
||||
companion-method lookup) matches purely by simple name - the first `ExecutableElement` found -
|
||||
with no arity or parameter-type check at all. For `@DtoConvert` specifically this is a real risk:
|
||||
its documented contract is a method "taking the source property value and returning the
|
||||
converted DTO property value" (i.e. exactly one parameter), but a shared/reusable conversion
|
||||
utility class is a very plausible place to have multiple same-named overloads (e.g. `format
|
||||
(Instant)` and `format(LocalDate)`) - `findMethod` would silently bind to whichever one
|
||||
`ElementFilter.methodsIn` happened to return first, independent of which one the developer
|
||||
actually meant, generating either a confusing arity/type-mismatch compile error in the generated
|
||||
mapper or, if both overloads happened to be call-compatible, silently invoking the wrong one.
|
||||
- Resolution: added a dedicated `findConverterMethod(...)` (used only by `resolveConverter()`,
|
||||
leaving the shared `findMethod()` untouched for the builder/mixin call sites which have their
|
||||
own, different arity expectations) that filters same-named candidates down to those taking
|
||||
exactly one parameter. Zero matches raises a clear "not found ... taking exactly one
|
||||
parameter" error; more than one match (multiple 1-arg overloads sharing the name) raises an
|
||||
"ambiguous - N overloads take exactly one parameter" error, since `@DtoConvert` has no
|
||||
parameter-type-based way to disambiguate and the developer must rename one of the overloads.
|
||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveConverter()`,
|
||||
`findConverterMethod()`). Test coverage: new negative compile-error tests
|
||||
`DtoMapperComputedPathTest#dtoConvertMethod_withAmbiguousOverloads_expectCompileError` (two
|
||||
same-named 1-arg overloads) and `#dtoConvertMethod_withWrongArity_expectCompileError` (a
|
||||
same-named 0-arg method, no 1-arg candidate at all); existing `tests/test-dto-mapping`
|
||||
converter usage (a single, unambiguous 1-arg method per converter type) continues to resolve
|
||||
and pass unchanged.
|
||||
|
||||
## References
|
||||
|
||||
- Requirements: [dto-mapping-requirements.md](./dto-mapping-requirements.md)
|
||||
- Issue: https://github.com/ebean-orm/ebean/issues/2540
|
||||
- MapStruct cycle mapping: https://mapstruct.org/documentation/stable/reference/html/#mapping-object-cycles
|
||||
@@ -1,337 +0,0 @@
|
||||
# Nested DTO Mapping — Requirements
|
||||
|
||||
Design requirements distilled from [issue #2540 "Support nested DTO mapping"](https://github.com/ebean-orm/ebean/issues/2540),
|
||||
reviewed against comparable features in QueryDSL (`@QueryProjection`) and Blaze-Persistence (`@EntityView`).
|
||||
|
||||
## Context
|
||||
|
||||
Ebean already supports:
|
||||
|
||||
- Partial/flat DTO queries via `DB.findDto(...)` and `query.select(...).asDto(Dto.class)`.
|
||||
- `@Formula` / `@Formula2` — path-based, auto-joined computed SQL expressions, but only on managed entities.
|
||||
- `query.setUnmodifiable(true)` — builds a read-only, non-lazy-loading entity graph (`InterceptReadOnly`,
|
||||
see PR #2626). Accessing an unloaded property throws `LazyInitializationException`; mutating throws
|
||||
`UnmodifiableEntityException`.
|
||||
|
||||
Unlike Hibernate, Ebean does dirty-detection on the bean itself (no dynamic proxies), so there is very little
|
||||
extra cost to an entity-graph query versus a DTO query. This makes an **unmodifiable entity graph** a cheap,
|
||||
natural intermediate representation to map *from* when producing a DTO graph — we don't need Blaze/Hibernate's
|
||||
proxy-based `EntityView` mechanism to get the performance benefit they are chasing.
|
||||
|
||||
The goal is nested DTO graph support (DTOs containing ToOne/ToMany child DTOs), not just today's flat DTOs,
|
||||
while keeping DTOs as plain, framework-unattached classes.
|
||||
|
||||
## Accepted Requirements
|
||||
|
||||
### A. Nested DTO graphs
|
||||
|
||||
- **Support nested DTO graphs (ToOne/ToMany)**
|
||||
Allow mapping a query result into a DTO graph where DTO fields are themselves DTOs (ToOne) or
|
||||
`List`/`Set<Dto>` (ToMany), not just flat DTOs. Use the existing `setUnmodifiable(true)` entity graph as
|
||||
the intermediate, de-duplicated, identity-consistent source to map from.
|
||||
*Inspiration: Blaze `@EntityView` subviews/subview collections; Jimmer fetcher DTOs.*
|
||||
|
||||
- **Auto-generated entity → DTO graph mapper**
|
||||
Given an unmodifiable entity graph plus a target nested DTO type, generate (via annotation processing,
|
||||
reflection-free) a mapper that walks the graph and populates the DTO graph, matching properties by
|
||||
name/type with override annotations for renames, computed values, and collection element types.
|
||||
*Inspiration: Blaze `@EntityView` + subview mapping; conceptually similar to MapStruct but Ebean-generated
|
||||
and graph/identity aware.*
|
||||
|
||||
- **Identity-aware de-duplication in nested collections**
|
||||
When mapping nested collections referencing the same underlying entity instance multiple times, reuse the
|
||||
same DTO instance (mirrors Blaze/Jimmer identity semantics) rather than producing independent copies.
|
||||
*Inspiration: Blaze/Jimmer identity handling.*
|
||||
|
||||
### B. Formula-style DTO annotations
|
||||
|
||||
- **`@Formula2`-like annotations on DTO fields**
|
||||
Bring the existing `@Formula` / `@Formula2` concept (auto-joined, path-based computed SQL expressions) to
|
||||
DTO classes so a DTO field can request a computed/aggregated value with the join auto-derived, instead of
|
||||
only being available on managed entities.
|
||||
*Inspiration: User suggestion; Ebean `@Formula2`; Blaze `@Mapping` computed expressions.*
|
||||
*Status: a narrower version (pulling in an existing entity-level `@Formula2` by path) was implemented and
|
||||
then rejected - for the common same-name case it generated code identical to a plain unannotated field, so
|
||||
the annotation added no real value beyond a codegen-time validation. See `docs/dto-mapping-design.md`
|
||||
("Formula2-on-DTO scope" and "Ad-hoc computed/formula properties" sections). The broader goal - arbitrary
|
||||
ad-hoc computed SQL on a DTO field - is better served by modelling the computed value as its own
|
||||
`@Entity @View`/`@Sql` read entity and mapping *that* into a plain DTO, reusing the existing (already
|
||||
accepted) nested-DTO mapping machinery rather than a new DTO-level annotation.
|
||||
|
||||
- **Path-based property mapping annotation on DTO**
|
||||
Allow a DTO field or constructor param to be annotated with a source path expression (e.g. `parent.name`)
|
||||
so Ebean can auto-derive the select clause plus joins for nested/renamed properties, reducing manual
|
||||
constructor wiring for non-trivial mappings.
|
||||
*Inspiration: Blaze `@Mapping`; QueryDSL constructor expressions.*
|
||||
|
||||
### C. Compile-time safety
|
||||
|
||||
- **Compile-time verification of `select(...).asDto(...)` mapping** *(explored, rejected as impractical -
|
||||
`mapTo()` accepted as the alternative)*
|
||||
Today `select(props).asDto(Dto.class)` is only checked at runtime. Explored an annotation-processor
|
||||
based mechanism to verify at compile time that selected properties match the DTO constructor or setters,
|
||||
mirroring QueryDSL's `@QueryProjection` compile-time Q-type generation. Rejected as impractical: raw SQL
|
||||
is an opaque `String` at compile time, and even the typed query-bean `.select(...)` form only
|
||||
type-checks against the *entity* - the match to the target DTO still happens at runtime via reflection
|
||||
(`DtoQueryPlanConstructor`), and the `.asDto(...)` call site can be arbitrarily distant from the
|
||||
`.select(...)` call, so there's no fixed AST shape an annotation processor could reliably verify.
|
||||
QueryDSL's actual compile-time safety comes from a different mechanism entirely - typed
|
||||
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking an
|
||||
independently-built select-list against a DTO. `mapTo(Dto.class)` (see section A) already closes the
|
||||
underlying gap in the opposite, tractable direction: it derives the select/fetch spec *from* the DTO's
|
||||
declared shape at APT time, so it is compile-time safe by construction, with no separate select-list to
|
||||
drift out of sync. Recommendation: document `mapTo()` as the compile-time-safe answer for DTO
|
||||
projections, and treat `asDto()`/`findDto()` explicitly as the flexible, runtime-checked escape hatch
|
||||
for raw/dynamic SQL.
|
||||
*Inspiration: QueryDSL `@QueryProjection` compile-time Q-type generation.*
|
||||
|
||||
- **Fail-fast on unmapped or lazy property access**
|
||||
Ensure a clear, documented, minimal-ceremony way to fail fast if code touches a property not included in
|
||||
the query projection, instead of silently lazy loading or returning null. `query.setUnmodifiable(true)`
|
||||
already satisfies this (throws `LazyInitializationException`) — document/promote it as the answer, and
|
||||
evaluate whether a lighter-weight flag decoupled from full unmodifiable/read-only semantics is needed.
|
||||
*Inspiration: Original issue ask; already solved via `setUnmodifiable()` (PR #2626 `InterceptReadOnly`).*
|
||||
|
||||
### D. Fetch strategy and performance
|
||||
|
||||
- **Fetch strategy control for DTO graph relationships**
|
||||
Existing entity query fetch hints (join vs. select/subselect secondary query, `+query`/`+lazy`) should
|
||||
transparently carry over when the target of the query is a DTO graph rather than an entity graph.
|
||||
`query.mapTo(Dto.class)` applies the DTO-derived `FetchGroup` only when the query has no
|
||||
`select()`/`fetch()` already set - a manually tuned fetch spec always takes precedence and is never
|
||||
overridden, allowing manual query optimisation when needed (at the cost of falling back to the
|
||||
existing fail-fast-on-unmapped-property behaviour if the manual spec doesn't cover what the DTO needs).
|
||||
*Inspiration: Blaze FETCH/SELECT/SUBSELECT fetch strategies.*
|
||||
|
||||
- **Pagination support for DTO graph queries**
|
||||
Confirm existing pagination works unchanged when projecting into nested DTO graphs.
|
||||
*Inspiration: Blaze pagination and keyset pagination support.*
|
||||
|
||||
### E. Custom property conversion
|
||||
|
||||
- **Per-property custom scalar conversion (`@DtoConvert`)**
|
||||
Motivated by real hand-written mapper code (`DriverMapper`, central-access) doing per-property scalar
|
||||
coercion (`short` -> `boolean`) and dependency-backed conversion (AES decryption via an injected cipher).
|
||||
Introduce a `@DtoConvert(value = ConverterType.class, method = "name")` annotation (combinable with
|
||||
`@DtoPath` for source-getter override) on a DTO property. The generator dispatches based on whether the
|
||||
referenced method is `static`:
|
||||
- **Static method** -> a direct static call is inlined (`ConverterType.method(source.getX())`), zero
|
||||
ceremony, no registration - covers common, reusable, dependency-free scalar coercions (e.g.
|
||||
`short`/`boolean`, enum <-> `String`) that could apply across many unrelated entity/DTO pairs.
|
||||
- **Instance method** -> dispatched via `DtoConverterManager.get(ConverterType.class).method(source.getX())`
|
||||
and wired as a real constructor parameter/field on the generated mapper (same shape as existing
|
||||
nested-mapper constructor injection) - covers conversions needing a real dependency (e.g. a cipher).
|
||||
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge: the app registers an
|
||||
already-DI-constructed converter singleton (e.g. built by avaje-inject) *before* building the `Database`.
|
||||
This is a narrow, accepted exception to the general no-static-mutable-state convention - it exists solely
|
||||
to bridge an already-DI-constructed singleton into `ServiceLoader`-discovered, no-arg-constructed generated
|
||||
code, which cannot otherwise reach a DI container. `DtoConverterManager.get(type)` throws immediately if
|
||||
nothing was registered, so a missing converter fails fast at Database-startup time (an eager field
|
||||
initializer on the generated `EbeanDtoMapperRegister`), not lazily on first use.
|
||||
*Design exploration considered and rejected two alternatives first: (a) a `DtoMapContext.service(Class)`
|
||||
lookup - rejected because `DtoMapContext` is a short-lived per-call identity-cache only, wrong lifetime for
|
||||
a real singleton dependency; (b) a `ServiceLoader`-discovered `DtoConverterSource` SPI mirroring
|
||||
`DtoMapperRegister` itself - rejected because a `ServiceLoader`-instantiated (no-arg) source cannot bridge
|
||||
to an *already* DI-constructed dependency (e.g. a cipher needing config/secrets) without reconstructing it
|
||||
itself, duplicating/bypassing the app's own DI-managed instance.*
|
||||
*Inspiration: `DriverMapper` (central-access) hand-written pattern; MapStruct qualified converter methods.*
|
||||
*Status: implemented - `@DtoConvert` (ebean-annotation), `io.ebean.DtoConverterManager` (ebean-api), and
|
||||
querybean-generator codegen support (static/instance dispatch, constructor wiring deduplicated by converter
|
||||
type). Test coverage: `tests/test-dto-mapping` `TestDtoConvert`.*
|
||||
|
||||
- **Type-pair (package-level) custom scalar conversion**
|
||||
Motivated by real hand-written mapper code (`EboxMapper`, central-access): the same conversion repeats
|
||||
across many unrelated properties on one target - `DateUtils.toCalendar(...)` on ~9 fields,
|
||||
`parseEnum(EnumType.class, value)` on ~3 - under today's `@DtoConvert` every one of those properties must
|
||||
carry its own repeated annotation. MapStruct solves this by letting a conversion method be defined once
|
||||
(in the mapper or a `uses = {...}` helper) and auto-applying it to *every* property whose source/target
|
||||
types match that method's signature - no per-field wiring. Proposed: a package-level, repeatable
|
||||
`@DtoConverters({ConverterType.class, ...})` (sibling to `@DtoMapping` in `package-info.java`) - the
|
||||
generator indexes every public static/instance method on the referenced type(s) by `(paramType ->
|
||||
returnType)`, then for any property whose source getter type doesn't already match the target field type
|
||||
and which carries no explicit per-property `@DtoConvert`, looks up that type pair and wires it in
|
||||
automatically (same static-vs-instance/`DtoConverterManager` dispatch rules as `@DtoConvert` today). An
|
||||
explicit per-property `@DtoConvert` always overrides the type-level default. Deliberately no built-in
|
||||
conversions shipped by Ebean itself (no implicit `Enum.valueOf`/`.name()`) - the app still owns
|
||||
exception/null-handling semantics (e.g. `parseEnum`'s catch-and-null-on-bad-value), just declares it once
|
||||
instead of per-field.
|
||||
**Status: implemented.** `@DtoConverters(ConverterType.class, ...)` (a single non-repeatable annotation
|
||||
taking a `Class<?>[]`, `@Target({PACKAGE, MODULE})`) is registered once per package/module alongside
|
||||
`@DtoMapping`. The generator indexes every public, single-arg, non-void method on each referenced type by
|
||||
exact `(paramType -> returnType)`; any SCALAR property (plain or `@DtoPath`-renamed) with no explicit
|
||||
`@DtoConvert` and a source/target type mismatch is auto-wired to the matching method (a duplicate/ambiguous
|
||||
type pair across the registered types is a compile-time processor error). List-element-wise conversion and
|
||||
`@DtoRef` (FK-id) properties are out of scope. Test coverage:
|
||||
`tests/test-dto-mapping/.../TestDtoConverters.java` (`UuidConverters`/`UuidShortCodeConverter`,
|
||||
`ContactTypeConverterDto`) - covers same-name auto-dispatch, `@DtoPath`-renamed auto-dispatch, and explicit
|
||||
`@DtoConvert` overriding the registered default.
|
||||
*Inspiration: `EboxMapper` (central-access) hand-written pattern; MapStruct type-signature-matched
|
||||
conversion methods.*
|
||||
|
||||
- **`@DtoMixin` for DTOs that cannot be annotated directly**
|
||||
Some DTOs are generated (e.g. from an OpenAPI spec) and not editable/annotatable, so `@DtoPath`/
|
||||
`@DtoConvert`/`@DtoRef` cannot always be placed directly on the DTO. Introduce a `@DtoMixin(Target.class)`
|
||||
companion interface/type, discovered by scanning the compilation round and overlaying its per-property
|
||||
annotations onto the real target's properties by name-match - directly mirrors avaje-jsonb's
|
||||
`@Json.MixIn` mechanism (`KingfisherMixin`/`CrewMateMixIn` pattern), a proven prior-art solution to the
|
||||
exact same "can't annotate a generated/unowned type" problem.
|
||||
*Inspiration: avaje-jsonb `@Json.MixIn`.*
|
||||
*Status: implemented - `@DtoMixin` (ebean-annotation) and querybean-generator round-scanning/overlay
|
||||
support (matches mixin methods to target properties by name, applying whichever of `@DtoPath`/`@DtoRef`/
|
||||
`@DtoConvert` is present as if declared on the target field itself). Test coverage: `tests/test-dto-mapping`
|
||||
`TestDtoMixin`.*
|
||||
|
||||
### F. DI-friendly manual mapper usage
|
||||
|
||||
- **Public `DtoMapperManager` with `get(Class<T> mapperType)` for DI**
|
||||
Motivated by `DriverMapper`/`DriverService` (central-access): `DriverMapper` is a hand-written
|
||||
`@Component` constructor-injected into `DriverService`. Moved `DtoMapperManager` from internal
|
||||
(`io.ebeaninternal.server.dto`) to public `io.ebean` - unchanged `mapperFor(source, dto)`, plus a new
|
||||
`get(Class<T> mapperType)` keyed by the generated mapper's own concrete class (e.g.
|
||||
`manager.get(CustomerDtoMapper.class)`), for direct/concrete-typed DI injection. `DtoMapperRegister`
|
||||
gained a default `mapperOfType(Class<T>)` method (non-breaking); the generator emits the real if-chain
|
||||
body (mirrors `mapperFor`'s if-chain). `DtoMapperManager` has zero `Database` dependency (constructor
|
||||
only does `ServiceLoader.load(DtoMapperRegister.class)`), so it can be constructed standalone,
|
||||
independent of/before a `Database` - e.g. as an avaje-inject bean.
|
||||
*Inspiration: `DriverMapper`/`DriverService` (central-access).*
|
||||
*Status: implemented.*
|
||||
|
||||
- **`DtoMapperManager` sharing via `DatabaseBuilder.putServiceObject`**
|
||||
So `query.mapTo()` and application-injected mappers share the exact same `DtoMapperManager` instance
|
||||
(and hence the same underlying generated mapper singletons) rather than each independently constructing
|
||||
its own, `InternalConfiguration` checks `config.getServiceObject(DtoMapperManager.class)` first (mirrors
|
||||
the existing `AutoMigrationRunner`/`GeoTypeProvider` `putServiceObject`/`getServiceObject` pattern),
|
||||
falling back to constructing a default `new DtoMapperManager()` if none was supplied.
|
||||
*Inspiration: user proposal following `DriverMapper`/`DriverService` review.*
|
||||
*Status: implemented.*
|
||||
|
||||
*Rejected: generator-emitted `builder(source)` method* - `DriverMapper` exposes `builder(cDriver)`
|
||||
returning a partially-populated `DriverBuilder` so callers can add extra caller-supplied fields (e.g.
|
||||
fleets) before `build()`. Rejected as a generator feature - `Driver`/`DriverSummary` already use
|
||||
avaje-recordbuilder's `@RecordBuilder`, which generates `Target.builder(existingInstance)`
|
||||
(seed-from-instance). The same effect is already achievable with zero ebean changes:
|
||||
`mapper.map(source)` then `Builder.builder(mapped).extraField(x).build()`. Documented as a recipe
|
||||
instead (see "Recipe: adding extra caller-supplied fields after mapping" in
|
||||
`docs/guides/mapping-entity-graphs-to-dtos.md`).
|
||||
|
||||
### G. Large-target construction and shape variants
|
||||
|
||||
- **Builder-based target construction for large DTOs**
|
||||
Motivated by `UserService`/`User` (central-access): `User` is a 24-field OpenAPI-generated record with
|
||||
a `@RecordBuilder`-generated `UserBuilder`, hand-mapped via a long fluent builder chain rather than a
|
||||
positional constructor to stay readable/refactor-safe. The generator auto-detects a RecordBuilder-style
|
||||
builder on the target (static `Target.builder()` + fluent per-property setters + `build()`) and uses
|
||||
`Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)` whenever (a) a builder is
|
||||
detected and (b) the target has more than a threshold number of properties (default 5), falling back to
|
||||
the positional constructor otherwise. An explicit `@DtoMapping` attribute (`builder = AUTO | ALWAYS |
|
||||
NEVER`) overrides the heuristic in either direction. Applies regardless of whether the target class is
|
||||
hand-authored or foreign/generated (e.g. an OpenAPI record) - `@DtoMapping` is already declared
|
||||
externally via `package-info.java`, not on the target class, so this was already compatible with
|
||||
foreign target types.
|
||||
*Inspiration: `UserService`/`User` (central-access).*
|
||||
*Status: implemented.*
|
||||
|
||||
- **Named mapper variants excluding nested paths, sharing one generated class**
|
||||
Motivated by `UserService`/`User` (central-access): `CUser` -> `User` is mapped in two shapes - with
|
||||
nested `fleets` (`findUserByGid`) and without (`findAll`, bulk listing) - to avoid an unnecessary
|
||||
join/fetch on the common bulk-listing path. Keeps the existing "shape always derived from declaration,
|
||||
fetch spec always wins" philosophy (rejected relaxing that rule / rejected a runtime
|
||||
is-property-loaded auto-skip check as less deterministic). The same `(source, target)` pair can be
|
||||
declared more than once in `package-info.java` via a named variant, e.g.
|
||||
`@DtoMapping(source = CUser.class, target = User.class)` (base/full) plus
|
||||
`@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")`
|
||||
(variant). Both variants are generated into the **same** mapper class (one class per target, not one
|
||||
per variant) and share a single private `build(source, context, boolean includeXxx, ...)` method
|
||||
containing the common field population written once; each excluded nested path becomes a `boolean
|
||||
includeXxx` parameter of that shared method rather than a precomputed value, so `build()` still
|
||||
evaluates every property - included or excluded - inline, at its own declared field position (a
|
||||
`includeFleets ? fleetsMapper.mapList(...) : List.of()` ternary in place, not hoisted out as a
|
||||
pre-evaluated call argument). This preserves the DTO's declared property order as the true evaluation
|
||||
order regardless of which properties a variant happens to exclude. The base `map()` passes `true` for
|
||||
every flag; each named variant (exposed as a same-named accessor, e.g. `noFleets()`, returning a single
|
||||
shared/cached instance of its own small `DtoMapper<SOURCE, TARGET>`-implementing inner class - not
|
||||
reconstructed per call) passes `false` for the paths it excludes and omits that path from its own
|
||||
`fetchGroup`. Selected via a new `query.mapTo(Class<D> dtoType, DtoMapper<T, D> mapper)` overload
|
||||
taking an already-resolved mapper instance directly (e.g. `query.mapTo(User.class,
|
||||
userMapper.noFleets())`) - no string-based variant lookup, and no changes needed to
|
||||
`DtoMapperRegister`/`DtoMapperManager`.
|
||||
*Inspiration: `UserService`/`User` (central-access).*
|
||||
*Status: implemented.*
|
||||
|
||||
- **Setter-based (mutable JavaBean) target construction**
|
||||
Motivated by `EboxMapper` (central-access): its target types (`Ebox`, `MachineSummaryInfo`, from
|
||||
`nz.co.eroad.schema.eroadtypes`, JAXB/XSD-generated legacy SOAP shapes) are plain mutable JavaBeans - a
|
||||
public no-arg constructor plus a `void setXxx(...)` setter per property - neither a positional constructor
|
||||
match nor a RecordBuilder-style fluent builder (see section G above). The generator currently only
|
||||
recognizes those two construction strategies, so this common third shape (typical of JAXB/XSD-generated
|
||||
and many hand-written mutable POJOs) can't be targeted by `@DtoMapping` at all today. Proposed: detect a
|
||||
no-arg constructor plus a `void setXxx(propertyType)` setter per mapped property as a third construction
|
||||
strategy, generating `Target target = new Target(); target.setX(...); ...; return target;` (mirroring the
|
||||
existing `build = AUTO | ALWAYS | NEVER` override precedent from section G for explicit control over which
|
||||
strategy applies). Would also unblock the `mapToBuilder()`-style "populate ignored/derived properties after
|
||||
the generated mapping, before finishing construction" pattern for these targets (currently only available
|
||||
for builder-shaped targets) - relevant to `EboxMapper`'s `machineSummaryInfo` (a genuinely composite,
|
||||
multi-association derived value, out of reach of `@DtoConvert`/`@DtoPath` regardless of this gap, but a
|
||||
natural fit for the same "map base fields via codegen, then set the derived one by hand" pattern already
|
||||
used for `Fleet.assignedMachines`/`assignedDrivers`).
|
||||
*Inspiration: `EboxMapper` (central-access); JAXB/XSD-generated SOAP DTO shapes generally.*
|
||||
**Status: implemented.** `@DtoMapping(setter = AUTO | ALWAYS | NEVER)` mirrors `builder()`'s override
|
||||
precedent. Detection requires a public no-arg constructor plus a public `setXxx(...)` setter for every
|
||||
mapped property - either `void` or fluent-style (returning the target type itself, e.g. `public Target
|
||||
setXxx(...) { ...; return this; }`); the generated code always calls the setter as a bare statement and
|
||||
discards any return value, so either shape works identically. A builder, when selected, always takes
|
||||
priority over setter-based construction. Under the default `AUTO`, setter-based construction is only
|
||||
attempted when the target has no positional constructor matching the mapped properties (arity-based) and
|
||||
no builder was selected - existing positional-constructor and builder-shaped targets are entirely
|
||||
unaffected. `ALWAYS` requires the shape (codegen-time error otherwise); `NEVER` always uses a positional
|
||||
constructor. Generated shape: `Target target = new Target(); target.setX(...); ...; return target;` (a
|
||||
`computeIfAbsent(...)`-wrapped block-lambda variant when the target is nested elsewhere in the graph).
|
||||
Deliberately **no** `mapToBuilder(...)`-style post-construction accessor is generated for this strategy -
|
||||
the returned target is already the final, fully mutable instance (setters are required to be `public`), so
|
||||
a caller can already call e.g. `dto.setExternalRef(...)` directly on the mapped result, exactly the pattern
|
||||
`EboxMapper` already uses by hand; this is unlike the builder strategy, where the intermediate builder is
|
||||
otherwise unreachable after its one-shot `build()` call. Test coverage:
|
||||
`tests/test-dto-mapping/.../TestDtoSetterConstruction.java` (`ContactSetterDto`) - covers auto-detected
|
||||
setter-chain construction plus post-construction population of two `@DtoIgnore` properties (a plain scalar
|
||||
and a `List`) via their public setters; plus `ContactSetterFluentDto` - covers the fluent-setter-return-shape
|
||||
variant.
|
||||
|
||||
### H. Record entity sources
|
||||
|
||||
- **Record-style (bare/fluent) accessors on the source (entity) side**
|
||||
Ebean supports entity beans declared as Java `record`s (e.g. `public record CourseRecordEntity(@Id long id,
|
||||
String name, String notes) {}` - see `test-java16`), whose only accessor shape is the bare component name
|
||||
(`active()`, `name()`, `id()`) - never `getXxx()`/`isXxx()`. This bare-accessor convention isn't limited to
|
||||
an actual `record` type though - an ordinary class can just as easily expose bare/fluent-style accessors
|
||||
with no `get`/`is` prefix at all. The generator resolves the real accessor for each source type (the direct
|
||||
source, or an intermediate `@DtoPath`/`@DtoRef` association type) by checking which shape actually exists as
|
||||
a method, in order: (1) `isXxx()` returning `boolean` (JavaBean boolean convention), (2) `getXxx()` (JavaBean
|
||||
convention), (3) the bare `propertyName()` itself - falling back to a guessed `getXxx()` only if none of the
|
||||
three are found. Resolution is entirely name/existence-based (no dependency on whether the type is actually
|
||||
a `record`). The Ebean bean-property name used in generated `FetchGroup.select(...)`/`.fetch(...)` calls is
|
||||
tracked directly from the original property/segment name (not reverse-parsed from the resolved accessor's
|
||||
method name), so it's correct regardless of which of the three accessor shapes was used.
|
||||
*Inspiration: Ebean's own record-entity support (`test-java16`); user-reported gap during review.*
|
||||
*Status: implemented.*
|
||||
|
||||
## Rejected Requirements
|
||||
|
||||
These were considered and explicitly rejected as out of scope:
|
||||
|
||||
- **DTO as interface / dynamic proxy views** — Blaze `@EntityView` defines views as interfaces backed by
|
||||
runtime proxies. This conflicts with Ebean's preference for plain, framework-unattached DTO classes.
|
||||
- **Updatable or creatable entity views (persist through DTO)** — Blaze's `@UpdatableEntityView` /
|
||||
`@CreatableEntityView` cascade persist/update through the view. This would duplicate Ebean's existing
|
||||
entity persistence model and introduce a second, ambiguous dirty-checking/cascade model.
|
||||
- **New predicate/filter DSL for subview collections** — Blaze allows filter expressions directly in
|
||||
`@Mapping` (e.g. filtering a collection by an attribute value). Ebean already has typed query bean
|
||||
predicates and `.filterMany()` for filtering child collections in queries; no new embedded filter
|
||||
expression language is needed on the DTO itself.
|
||||
|
||||
## References
|
||||
|
||||
- Issue: https://github.com/ebean-orm/ebean/issues/2540
|
||||
- PR #2626: `InterceptReadOnly` / `InterceptReadWrite` split enabling the unmodifiable entity graph fast path
|
||||
- Ebean docs: https://ebean.io/docs/query/option#unmodifiable
|
||||
- QueryDSL: `@QueryProjection` (constructor-based, compile-time-checked projections)
|
||||
- Blaze-Persistence Entity Views: https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/
|
||||
@@ -14,8 +14,6 @@ Key guides (fetch and follow when performing the relevant task):
|
||||
- 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
|
||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.md
|
||||
- Derived / formula properties (`@Formula`, `@Formula2`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/derived-formula-properties.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
|
||||
|
||||
@@ -40,16 +40,13 @@ existing Maven project. Complete the steps in order.
|
||||
| [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 |
|
||||
| [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md) | Read-only computed properties: physical-SQL `@Formula` (with `${ta}` and hand-written joins) versus logical path-based `@Formula2` (auto-resolved joins); use in `select`/`where`/`orderBy`; default inclusion and the `@Transient` opt-out |
|
||||
|
||||
## 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 |
|
||||
| [Mapping entity graphs to DTOs (`mapTo`)](mapping-entity-graphs-to-dtos.md) | Map a nested entity graph query result to a nested DTO graph via `query.mapTo(Dto.class)`; `@DtoPath`/`@DtoRef` for renamed/flattened/id-only properties; identity-aware de-dup via `DtoMapContext`; computed/aggregate DTO values via `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation`; comparison with the flat `asDto()` pipeline |
|
||||
| [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 |
|
||||
| [Using `RawSql` with Ebean](using-rawsql-with-ebean.md) | Choose between `RawSqlBuilder.parse()`, `unparsed()`, and `withPlaceholders()`; the `${where}`/`${andWhere}`/`${having}`/`${andHaving}` placeholder reference for CTEs, window functions, and subqueries; column mapping; and using `RawSql` with query beans |
|
||||
|
||||
## Persisting & transactions
|
||||
|
||||
@@ -154,7 +151,6 @@ Key guides (fetch and follow these when performing the relevant task):
|
||||
- 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
|
||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.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
|
||||
@@ -184,7 +180,6 @@ Key guides (fetch and follow these when performing the relevant task):
|
||||
- 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
|
||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.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
|
||||
|
||||
@@ -101,7 +101,7 @@ Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.11</version>
|
||||
<version>42.7.8</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -1,39 +1,40 @@
|
||||
# Guide: `@DbJson` / `@DbJsonB` mapping support
|
||||
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
|
||||
|
||||
## Purpose
|
||||
|
||||
Ebean can map `@DbJson` and `@DbJsonB` properties in three ways:
|
||||
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 **`ebean-jackson-mapper`**.
|
||||
- **Avaje Jsonb**, provided by **`ebean-avajejsonb-mapper`** and its generated adapters.
|
||||
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
|
||||
for everything the built-in support does not handle.
|
||||
|
||||
The built-in mapper handles the common natural JSON types. Add one mapper module for
|
||||
typed collections, POJOs, records, and other custom types.
|
||||
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 no mapper module is on the classpath,
|
||||
> Ebean fails fast at startup:
|
||||
> 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 JSON mapper dependency for <property>
|
||||
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
|
||||
> Jackson ObjectMapper not present for <property>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Property type | Built-in (avaje-json-core) | Mapper module |
|
||||
|---|:---:|---|
|
||||
| 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, …) | | Jackson or Avaje Jsonb |
|
||||
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | Jackson or Avaje Jsonb |
|
||||
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | Jackson or Avaje Jsonb |
|
||||
| POJOs, records, or any other type | | Jackson or Avaje Jsonb |
|
||||
| `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 | | ✅ |
|
||||
|
||||
---
|
||||
|
||||
@@ -58,10 +59,10 @@ Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
|
||||
|
||||
---
|
||||
|
||||
## Jackson `ObjectMapper`
|
||||
## Everything else → Jackson `ObjectMapper`
|
||||
|
||||
`ebean-jackson-mapper` uses a Jackson `ObjectMapper` for the property types not handled
|
||||
built-in:
|
||||
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>`).
|
||||
@@ -76,7 +77,7 @@ built-in:
|
||||
|
||||
---
|
||||
|
||||
### Adding `ebean-jackson-mapper`
|
||||
## Adding `ebean-jackson-mapper`
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
@@ -91,79 +92,6 @@ registers the mapper-based JSON support automatically.
|
||||
|
||||
---
|
||||
|
||||
## Avaje Jsonb
|
||||
|
||||
`ebean-avajejsonb-mapper` uses Avaje Jsonb adapters. Annotate each JSON payload type with
|
||||
`@Json`, or use `@Json.Import`, and configure `avaje-jsonb-generator` as an annotation
|
||||
processor.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-avajejsonb-mapper</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-jsonb-generator</artifactId>
|
||||
<version>${avaje-jsonb.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```java
|
||||
import io.avaje.jsonb.Json;
|
||||
|
||||
@Json
|
||||
public class Address {
|
||||
public String line1;
|
||||
public String city;
|
||||
}
|
||||
```
|
||||
|
||||
Avaje Jsonb preserves a property's declared generic type, so `List<Address>` and other
|
||||
parameterised JSON values use the generated `Address` adapter.
|
||||
|
||||
### Avaje JsonNode
|
||||
|
||||
`@DbJson` and `@DbJsonB` properties declared as `io.avaje.json.node.JsonNode` are supported
|
||||
when the application includes `avaje-json-node`. Its Jsonb component supplies the JSON tree
|
||||
adapters; no application-generated adapter is needed for the node hierarchy.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-node</artifactId>
|
||||
<version>${avaje-jsonb.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
## Mapper module selection
|
||||
|
||||
Use exactly one mapper module in an application: `ebean-jackson-mapper` or
|
||||
`ebean-avajejsonb-mapper`. Ebean selects a single `ScalarJsonMapper` service provider, so
|
||||
having both modules on the runtime classpath is not supported.
|
||||
|
||||
The `ebean` composite dependency includes `ebean-jackson-mapper`. Applications using Avaje
|
||||
Jsonb should depend on the individual Ebean modules they need instead of that composite.
|
||||
|
||||
To migrate from Jackson to Avaje Jsonb, remove `ebean-jackson-mapper`, add
|
||||
`ebean-avajejsonb-mapper`, and generate adapters for each JSON payload type.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
|
||||
@@ -174,13 +102,15 @@ To migrate from Jackson to Avaje Jsonb, remove `ebean-jackson-mapper`, add
|
||||
(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`.
|
||||
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 an additional mapper module.
|
||||
- Add **`ebean-jackson-mapper`** or **`ebean-avajejsonb-mapper`** when you need rich POJO JSON
|
||||
columns or typed collections / typed-value maps.
|
||||
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.
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
# Guide: Derived / formula properties — `@Formula` and `@Formula2`
|
||||
|
||||
## Purpose
|
||||
|
||||
A *formula property* is a read-only entity property whose value is computed by a SQL
|
||||
expression at query time rather than stored in its own column. Ebean has two
|
||||
annotations for this:
|
||||
|
||||
- **`@Formula`** — you write the **physical SQL** for the `select` (and any `join`),
|
||||
using the `${ta}` placeholder for the base table alias. Maximum control; verbose.
|
||||
- **`@Formula2`** — you write a **logical expression** using dot-notation property
|
||||
paths (e.g. `parent.familyName`). Ebean translates the paths to the correct table
|
||||
aliases and **adds the required JOINs automatically**.
|
||||
|
||||
`@Formula2` is intended as the easier, path-based replacement for `@Formula`. Both
|
||||
produce read-only properties and behave the same way with respect to default
|
||||
inclusion (see [Default inclusion](#default-inclusion-and-transient)).
|
||||
|
||||
---
|
||||
|
||||
## Quick comparison
|
||||
|
||||
| | `@Formula` | `@Formula2` |
|
||||
|---|---|---|
|
||||
| Expression | Physical SQL columns + aliases | Logical property paths |
|
||||
| Table alias | `${ta}` placeholder you write | Resolved automatically |
|
||||
| Joins | You write the `join` clause | Added automatically from the paths |
|
||||
| Read only | ✅ | ✅ |
|
||||
| Included by default | ✅ (use `@Transient` to opt out) | ✅ (use `@Transient` to opt out) |
|
||||
| Usable in `select` / `where` / `orderBy` / `having` | ✅ | ✅ |
|
||||
| Creates a DB column (DDL) | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## `@Formula` — physical SQL
|
||||
|
||||
You supply the SQL `select` fragment, and an optional `join`. Use `${ta}` wherever you
|
||||
need the base table alias of the entity.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class ParentPerson {
|
||||
|
||||
// aggregation via a derived join; ${ta} is the base table alias
|
||||
@Formula(select = "coalesce(f2.child_count, 0)",
|
||||
join = "left join (select parent_id, count(*) as child_count"
|
||||
+ " from child group by parent_id) f2 on f2.parent_id = ${ta}.id")
|
||||
Integer childCount;
|
||||
|
||||
// coalesce across a joined table using an explicit join alias (j1)
|
||||
@Formula(select = "coalesce(${ta}.family_name, j1.family_name)",
|
||||
join = "join parent_person j1 on j1.id = ${ta}.parent_id")
|
||||
String effectiveFamilyName;
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The `join` string must start with `join` or `left join`.
|
||||
- You manage the join aliases (`j1`, `f2`, …) yourself and reference them in `select`.
|
||||
- `@Formula` is `@Repeatable` and supports a `platforms()` restriction.
|
||||
|
||||
---
|
||||
|
||||
## `@Formula2` — logical property paths
|
||||
|
||||
Write the expression using property paths. Ebean resolves each path to the right table
|
||||
alias and adds the joins it needs.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class ParentPerson {
|
||||
|
||||
@ManyToOne
|
||||
GrandParentPerson parent;
|
||||
|
||||
String familyName;
|
||||
|
||||
// Ebean automatically left joins 'parent' and resolves the aliases
|
||||
@Formula2("coalesce(familyName, parent.familyName)")
|
||||
String derivedFamilyName;
|
||||
}
|
||||
```
|
||||
|
||||
A query selecting `derivedFamilyName` produces (roughly):
|
||||
|
||||
```sql
|
||||
select t0.id, coalesce(t0.family_name, t1.family_name)
|
||||
from parent_person t0
|
||||
left join grand_parent_person t1 on t1.id = t0.parent_id
|
||||
```
|
||||
|
||||
Multi-level paths join through each step:
|
||||
|
||||
```java
|
||||
// joins parent and parent.parent automatically
|
||||
@Formula2("coalesce(familyName, parent.familyName, parent.parent.familyName)")
|
||||
String deepFamilyName;
|
||||
```
|
||||
|
||||
`@Formula2` works wherever a normal property does — the required joins are added
|
||||
automatically in each case:
|
||||
|
||||
```java
|
||||
// selected explicitly
|
||||
DB.find(ParentPerson.class).select("derivedFamilyName").findList();
|
||||
|
||||
// used in where (auto-joins even when not selected)
|
||||
DB.find(ParentPerson.class).where().eq("derivedFamilyName", "Smith").findList();
|
||||
|
||||
// used in order by
|
||||
DB.find(ParentPerson.class).orderBy("derivedFamilyName").findList();
|
||||
|
||||
// referenced via a path from another bean
|
||||
DB.find(ChildPerson.class).where().eq("parent.derivedFamilyName", "Smith").findList();
|
||||
```
|
||||
|
||||
It also resolves correctly inside nested `fetch()` joins, so a `@Formula2` on a fetched
|
||||
association is computed with its own joins relative to that association.
|
||||
|
||||
Notes:
|
||||
- The expression supports any SQL function whose arguments are logical property paths.
|
||||
- `@Formula2` supports a `platforms()` restriction.
|
||||
- No `${ta}` and no hand-written join — that is the point of `@Formula2`.
|
||||
|
||||
---
|
||||
|
||||
## Default inclusion and `@Transient`
|
||||
|
||||
Both annotations are **included in queries by default** (just like a normal mapped
|
||||
property). When no explicit `select()`/`fetch()` is given, the formula — and for
|
||||
`@Formula2` the joins it requires — are added to the query.
|
||||
|
||||
Add `@Transient` to make the formula **opt-in**: it is then **not** selected by default
|
||||
and must be requested explicitly via `select()` or `fetch()`. Do this when the formula
|
||||
(or the joins it needs) is relatively expensive.
|
||||
|
||||
```java
|
||||
// not selected by default; must be requested explicitly
|
||||
@Transient
|
||||
@Formula2("coalesce(familyName, parent.familyName)")
|
||||
String lazyDerivedFamilyName;
|
||||
```
|
||||
|
||||
```java
|
||||
DB.find(ParentPerson.class)
|
||||
.select("lazyDerivedFamilyName") // explicitly included, join auto-added
|
||||
.findList();
|
||||
```
|
||||
|
||||
This is the same `@Transient` opt-out mechanism used by `@Formula`.
|
||||
|
||||
---
|
||||
|
||||
## Which should I use?
|
||||
|
||||
- Prefer **`@Formula2`** for expressions over property paths (coalesce/case/functions
|
||||
across associations). It is shorter, refactor-friendly, and the joins stay correct as
|
||||
the model changes.
|
||||
- Use **`@Formula`** when you need raw SQL that does not map cleanly to property paths —
|
||||
for example a derived aggregate sub-select / dynamic view, or vendor-specific SQL.
|
||||
|
||||
For read models that exist only to carry computed values, also consider projecting to a
|
||||
DTO instead of mapping the formula onto the entity — see
|
||||
[writing-ebean-query-beans.md](writing-ebean-query-beans.md).
|
||||
@@ -376,12 +376,7 @@ public class Customer {
|
||||
**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 — **except** `@OneToOne(mappedBy=...)`,
|
||||
which defaults to `FetchType.EAGER` and adds a `left join` to every default
|
||||
select of the owning entity. Explicitly set `fetch = FetchType.LAZY` on
|
||||
`@OneToOne(mappedBy=...)` fields unless the association is needed on
|
||||
(almost) every load. See "`@OneToOne(mappedBy=...)` is EAGER by default" in
|
||||
the query-beans guide for details.
|
||||
- Relationships are lazy-loaded by default
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,785 +0,0 @@
|
||||
# Guide: Mapping entity graphs to DTOs — `query.mapTo(Dto.class)`
|
||||
|
||||
## Purpose
|
||||
|
||||
`query.mapTo(SomeDto.class)` maps an entity query result to a **nested DTO graph** —
|
||||
DTO fields can themselves be DTOs (`ToOne`) or `List<Dto>`/`Set<Dto>` (`ToMany`), not
|
||||
just flat scalar columns. Ebean generates the mapper (reflection-free), automatically
|
||||
derives the query's `select()`/`fetch()` spec from the target DTO's declared shape, and
|
||||
forces `setUnmodifiable(true)` so any property the mapper needs but wasn't fetched fails
|
||||
fast with `LazyInitialisationException` instead of silently lazy loading.
|
||||
|
||||
This is distinct from the existing flat `asDto(Dto.class)` — see
|
||||
[Quick comparison](#quick-comparison-mapto-vs-asdto-vs-plain-entity-query) below.
|
||||
|
||||
```java
|
||||
Optional<CustomerDto> dto = new QCustomer()
|
||||
.id.eq(customerId)
|
||||
.mapTo(CustomerDto.class)
|
||||
.findOneOrEmpty();
|
||||
```
|
||||
|
||||
```java
|
||||
List<CustomerDto> dtos = DB.find(Customer.class)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.mapTo(CustomerDto.class) // no .select()/.fetch() needed - derived from CustomerDto's shape
|
||||
.findList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick comparison: `mapTo()` vs `asDto()` vs plain entity query
|
||||
|
||||
| | `mapTo(Dto.class)` | `asDto(Dto.class)` | Plain entity query |
|
||||
|---|---|---|---|
|
||||
| Shape | Nested DTO **graph** (ToOne/ToMany) | Flat, single-row DTO | Entity graph |
|
||||
| Fetch spec | Auto-derived from the DTO's declared shape | Whatever `select()`/SQL you write | Whatever `select()`/`fetch()` you write |
|
||||
| Mismatch caught | At compile time (unregistered pair fails fast at first use; codegen fails fast on structural problems) | At runtime (reflection-based constructor/setter matching) | N/A (real entity properties) |
|
||||
| Identity/de-dup | Yes - repeated source instances map to the same DTO instance (`DtoMapContext`) | N/A (one row in, one DTO out) | Yes (entity/persistence-context identity) |
|
||||
| Backing pipeline | Executes the entity ORM query, `setUnmodifiable(true)`, maps the resulting graph | Executes SQL directly against a flat `ResultSet` | Executes the entity ORM query |
|
||||
| Best for | API/read-model responses that mirror a **nested** entity shape | Flat summary rows, reports, native/vendor SQL | Data you intend to mutate and save back |
|
||||
|
||||
See also [writing-ebean-query-beans.md](writing-ebean-query-beans.md) (Step 8/9) for
|
||||
`asDto()` and the general query-shape decision guide.
|
||||
|
||||
---
|
||||
|
||||
## Basic usage
|
||||
|
||||
### 1. Declare a plain DTO
|
||||
|
||||
DTOs are plain classes with **no framework attachment** — no annotations required for
|
||||
the common case (properties matched to the source entity by name):
|
||||
|
||||
```java
|
||||
public class CustomerDto {
|
||||
private final Long id;
|
||||
private final String name;
|
||||
private final AddressDto billingAddress; // nested ToOne
|
||||
private final List<ContactDto> contacts; // nested ToMany
|
||||
|
||||
public CustomerDto(Long id, String name, AddressDto billingAddress, List<ContactDto> contacts) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.billingAddress = billingAddress;
|
||||
this.contacts = contacts;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public AddressDto getBillingAddress() { return billingAddress; }
|
||||
public List<ContactDto> getContacts() { return contacts; }
|
||||
}
|
||||
```
|
||||
|
||||
A constructor whose parameters match (by name) a source entity/DTO property is used
|
||||
for mapping — same shape convention as the existing `DtoQuery`. Getters are used to
|
||||
read the source's properties — a bare/fluent accessor like `active()` is resolved
|
||||
automatically too, not just `getActive()`/`isActive()` (useful both for Ebean's own
|
||||
record entity beans and for ordinary classes that just expose bare-name accessors).
|
||||
|
||||
### 2. Register the (source, target) pair
|
||||
|
||||
Declare each entity → DTO pair with `@DtoMapping` on a `package-info.java` (a neutral
|
||||
holder — see [Why `package-info.java`?](#why-package-infojava)):
|
||||
|
||||
```java
|
||||
@DtoMapping(source = Customer.class, target = CustomerDto.class)
|
||||
@DtoMapping(source = Address.class, target = AddressDto.class)
|
||||
@DtoMapping(source = Contact.class, target = ContactDto.class)
|
||||
package org.example.dto;
|
||||
|
||||
import io.ebean.annotation.DtoMapping;
|
||||
```
|
||||
|
||||
This triggers `querybean-generator` (the existing annotation processor) to generate a
|
||||
`CustomerDtoMapper implements DtoMapper<Customer, CustomerDto>` for each pair — no new
|
||||
Maven/Gradle setup beyond what query beans already require.
|
||||
|
||||
### 3. Query with `mapTo(...)`
|
||||
|
||||
```java
|
||||
List<CustomerDto> dtos = DB.find(Customer.class)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.mapTo(CustomerDto.class)
|
||||
.findList();
|
||||
|
||||
CustomerDto one = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOne();
|
||||
|
||||
Optional<CustomerDto> maybe = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOneOrEmpty();
|
||||
```
|
||||
|
||||
`mapTo(...)` works the same from a query bean (`QCustomer`) or a plain `DB.find(...)`/
|
||||
`ExpressionList` query.
|
||||
|
||||
### Paging - `findPagedList()`
|
||||
|
||||
`findPagedList()` mirrors `Query#findPagedList()` — the underlying entity query is paged
|
||||
as normal and each page's result is mapped to the target DTO list:
|
||||
|
||||
```java
|
||||
PagedList<CustomerDto> paged = DB.find(Customer.class)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.orderBy().asc("name")
|
||||
.setFirstRow(0)
|
||||
.setMaxRows(50)
|
||||
.mapTo(CustomerDto.class)
|
||||
.findPagedList();
|
||||
|
||||
int totalRowCount = paged.getTotalCount(); // page metadata - unaffected by DTO mapping
|
||||
List<CustomerDto> page1 = paged.getList(); // mapped DTOs for this page
|
||||
```
|
||||
|
||||
Page metadata (`getTotalCount()`, `getTotalPageCount()`, `hasNext()`, `hasPrev()`,
|
||||
`loadCount()`, ...) reflects the underlying entity query directly; only `getList()`
|
||||
is mapped (once, cached) to the DTO type.
|
||||
|
||||
### An unregistered pair fails fast
|
||||
|
||||
If `(Customer.class, SomeDto.class)` was never declared via `@DtoMapping`, the first
|
||||
`mapTo(SomeDto.class)` call throws immediately:
|
||||
|
||||
```
|
||||
PersistenceException: No DtoMapper registered mapping Customer -> SomeDto
|
||||
- check @DtoMapping(source = Customer.class, target = SomeDto.class) is declared
|
||||
on a package-info.java processed by querybean-generator
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Auto-derived fetch spec
|
||||
|
||||
You never write `.select()`/`.fetch()` for a `mapTo(...)` query — the generated mapper
|
||||
exposes a `fetchGroup()` built directly from the DTO's declared shape, and `mapTo(...)`
|
||||
applies it automatically:
|
||||
|
||||
```java
|
||||
public CustomerDtoMapper() {
|
||||
this(new AddressDtoMapper(), new ContactDtoMapper());
|
||||
}
|
||||
|
||||
public CustomerDtoMapper(DtoMapper<Address, AddressDto> billingAddressMapper,
|
||||
DtoMapper<Contact, ContactDto> contactsMapper) {
|
||||
this.fetchGroup = FetchGroup.of(Customer.class)
|
||||
.select("id,name")
|
||||
.fetch("billingAddress", billingAddressMapper.fetchGroup())
|
||||
.fetch("contacts", contactsMapper.fetchGroup())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
Each nested DTO gets its own generated mapper (mirroring MapStruct's per-type mapper
|
||||
generation), wired together via constructor injection — mappers are stateless and
|
||||
substitutable, not static singletons. Mapper instances are constructed once, in
|
||||
dependency order, and reused — see [DtoMapperManager](#one-mapper-instance-per-pair)
|
||||
below.
|
||||
|
||||
---
|
||||
|
||||
## Nested collections and identity-aware de-duplication
|
||||
|
||||
When the same source entity instance is reachable via more than one path in the graph
|
||||
(e.g. two `Contact`s sharing the same `Customer`, or the same `Address` referenced from
|
||||
two paths), the mapper reuses the **same** target DTO instance rather than creating
|
||||
duplicate-but-equal copies — mirroring the identity semantics the entity graph already
|
||||
has:
|
||||
|
||||
```java
|
||||
List<CustomerDto> dtos = DB.find(Customer.class).mapTo(CustomerDto.class).findList();
|
||||
|
||||
CustomerDto customer = dtos.get(0);
|
||||
// both contacts share the exact same customer.billingAddress AddressDto instance
|
||||
assertThat(customer.getContacts().get(0).getCustomer())
|
||||
.isSameAs(customer.getContacts().get(1).getCustomer());
|
||||
```
|
||||
|
||||
This is done via a `DtoMapContext` threaded through every nested `map(...)` call within
|
||||
one top-level `mapList(...)`/`findList()` invocation. The generated code only pays for
|
||||
this when it can actually matter — a DTO that's never nested under another DTO skips
|
||||
`DtoMapContext` entirely (there's nothing else in scope to de-duplicate against):
|
||||
|
||||
```java
|
||||
// AddressDto is nested under CustomerDto (reachable via multiple contacts) - dedup needed
|
||||
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
|
||||
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(...));
|
||||
|
||||
// ContactSummaryDto is only ever mapped as a top-level query result - no dedup possible
|
||||
// skip DtoMapContext, only ever a top-level mapping
|
||||
return new ContactSummaryDto(source.getId(), source.getFullName());
|
||||
|
||||
// CustomerDto has nested mappers (billingAddress, contacts) but is never itself nested
|
||||
// DtoMapContext for nested mappers only
|
||||
return new CustomerDto(source.getId(), source.getName(), ...);
|
||||
```
|
||||
|
||||
The generated comment tells you at a glance which of the three cases applies — useful
|
||||
when debugging why a `DtoMapContext` is (or isn't) in the generated code for a
|
||||
particular mapper.
|
||||
|
||||
---
|
||||
|
||||
## Using generated mappers directly (outside `query.mapTo()`)
|
||||
|
||||
Every generated `XxxDtoMapper` is a plain public class — you don't need `ServiceLoader`,
|
||||
a registry, or a `Database` just to construct or call one directly (though
|
||||
`DtoMapperManager`, below, is available if you want a shared, DI-friendly lookup). It
|
||||
always has a public no-arg constructor (delegating to defaults for any nested mappers/
|
||||
`@DtoConvert` converters) plus an explicit constructor taking those dependencies directly,
|
||||
and implements `DtoMapper<SOURCE, TARGET>`'s `map(...)`/`mapList(...)`:
|
||||
|
||||
```java
|
||||
CustomerDtoMapper mapper = new CustomerDtoMapper();
|
||||
CustomerDto dto = mapper.map(customer); // any Customer you already have on hand
|
||||
List<CustomerDto> dtos = mapper.mapList(customers);
|
||||
```
|
||||
|
||||
This works on **any** entity graph, not just one that just came out of a `mapTo(...)`
|
||||
query — e.g. entities you loaded with a plain `.fetch(...)` query, entities you just
|
||||
`.save()`d, or entities built by hand in a test. The only requirement is that whatever the
|
||||
mapper reads (via plain getters) is actually populated — there's no lazy-loading fallback.
|
||||
|
||||
### Testing the mapping in isolation
|
||||
|
||||
Because mappers are plain, constructor-injected classes, you can unit test the mapping
|
||||
logic itself — independent of `query.mapTo()`, the DTO-pair registry, and (for
|
||||
`@DtoConvert` instance-dispatch converters) `DtoConverterManager` — by passing a test
|
||||
double straight into the explicit constructor:
|
||||
|
||||
```java
|
||||
SecretCipher upperCasingTestCipher = String::toUpperCase;
|
||||
ContactConversionDto dto = new ContactConversionDtoMapper(upperCasingTestCipher).map(contact);
|
||||
|
||||
assertThat(dto.getSecretCode()).isEqualTo("SHH");
|
||||
```
|
||||
|
||||
No `DtoConverterManager.put(...)` registration needed for this kind of test — the real
|
||||
production wiring (`DtoConverterManager.get(SecretCipher.class)`) only happens in the
|
||||
generated no-arg constructor, which the explicit-constructor call above bypasses entirely.
|
||||
See `TestCustomerDtoGraphMapping` (mapper called directly against a manually queried
|
||||
graph) and `TestMapperManualUsage` (mapper called directly against hand-built/just-saved
|
||||
entities, plus the converter test-double case above) in `tests/test-dto-mapping`.
|
||||
|
||||
### `DtoMapperManager` — resolving a generated mapper for dependency injection
|
||||
|
||||
`new CustomerDtoMapper()` is enough for a single mapper, but if your application wants a
|
||||
single shared instance of *every* generated mapper (mirroring how `query.mapTo()` resolves
|
||||
them internally) - e.g. to wire one up for constructor injection into a service, replacing
|
||||
a hand-written mapper class - use `io.ebean.DtoMapperManager`:
|
||||
|
||||
```java
|
||||
DtoMapperManager manager = new DtoMapperManager(); // ServiceLoader discovery only - no Database needed
|
||||
CustomerDtoMapper mapper = manager.get(CustomerDtoMapper.class);
|
||||
```
|
||||
|
||||
`DtoMapperManager` has no dependency on `Database` at all - its constructor only does
|
||||
`ServiceLoader.load(DtoMapperRegister.class)` - so it can be constructed independently,
|
||||
before (or entirely without) a `Database`, e.g. as a bean in an avaje-inject (or any DI
|
||||
framework's) dependency graph:
|
||||
|
||||
```java
|
||||
@Factory
|
||||
class DtoMapperFactory {
|
||||
|
||||
@Bean
|
||||
DtoMapperManager dtoMapperManager() {
|
||||
return new DtoMapperManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CustomerDtoMapper customerDtoMapper(DtoMapperManager manager) {
|
||||
return manager.get(CustomerDtoMapper.class);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you also want `query.mapTo(...)` to use that *exact same* manager instance (so there's
|
||||
only ever one instance of each generated mapper, whichever path resolves it), register it
|
||||
via `DatabaseBuilder.putServiceObject` before building the `Database` - this is the same
|
||||
`putServiceObject`/`getServiceObject` mechanism already used for things like
|
||||
`AutoMigrationRunner`:
|
||||
|
||||
```java
|
||||
DtoMapperManager sharedManager = new DtoMapperManager();
|
||||
|
||||
Database db = Database.builder()
|
||||
.putServiceObject(DtoMapperManager.class, sharedManager)
|
||||
.build();
|
||||
|
||||
// query.mapTo(...) against `db` now resolves mappers via `sharedManager`
|
||||
```
|
||||
|
||||
If nothing is registered via `putServiceObject`, the `Database` builds its own default
|
||||
`DtoMapperManager` instance instead - registering one is entirely optional. A standalone
|
||||
`DtoMapperManager()` construction bypasses the `DatabaseConfigProvider` hook (that hook is
|
||||
specifically about `Database` startup ordering), so if any of your mappers need a
|
||||
`@DtoConvert` instance-dispatch converter, register it via `DtoConverterManager.put(...)`
|
||||
yourself first, exactly as you would before building a `Database`. See
|
||||
`TestDtoMapperManager` and `TestDtoMapperManagerSharing` in `tests/test-dto-mapping`.
|
||||
|
||||
### Recipe: adding extra caller-supplied fields after mapping
|
||||
|
||||
Sometimes a target DTO needs a field that isn't sourced from the entity graph at all - e.g.
|
||||
populated from a separate query or business rule, only when a caller-supplied flag is set.
|
||||
Rather than the generator supporting partial/builder-based mapping directly, if your DTO is
|
||||
a record with a "seed from instance" builder (e.g. via `avaje-recordbuilder`'s
|
||||
`@RecordBuilder`, which generates `Target.builder(existingInstance)`), just map the
|
||||
graph-sourced fields as usual and layer the extra field on afterwards:
|
||||
|
||||
```java
|
||||
Driver base = mapper.map(cDriver);
|
||||
Driver full = DriverBuilder.builder(base).fleets(fleets).build();
|
||||
```
|
||||
|
||||
No generator changes needed - the mapped instance is simply the seed for the builder.
|
||||
|
||||
---
|
||||
|
||||
## Large targets: builder-based construction and named variants
|
||||
|
||||
Two features aimed at large, builder-shaped target DTOs (typically OpenAPI-generated records
|
||||
with a generated builder), where a positional constructor call is unwieldy and a single query
|
||||
needs to populate the target in more than one shape.
|
||||
|
||||
### Builder-based construction (`builder = AUTO | ALWAYS | NEVER`)
|
||||
|
||||
If the target has a static no-arg `Target.builder()` factory returning a type with a fluent
|
||||
(returns-itself) setter per property plus a `build()` method - the shape
|
||||
`avaje-recordbuilder`'s `@RecordBuilder` generates - the generated mapper can construct the
|
||||
target via `Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)`:
|
||||
|
||||
```java
|
||||
public record User(Long id, String name, String email, /* ... 21 more fields */) {
|
||||
|
||||
public static UserBuilder builder() {
|
||||
return UserBuilder.builder();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@DtoMapping(source = CUser.class, target = User.class)
|
||||
package org.example.dto;
|
||||
```
|
||||
|
||||
By default (`builder = AUTO`), the generator auto-detects a matching builder and uses it only
|
||||
once the target has more than 5 properties, falling back to a positional constructor for
|
||||
smaller DTOs. Override explicitly either direction:
|
||||
|
||||
```java
|
||||
@DtoMapping(source = CUser.class, target = User.class, builder = DtoMapping.Builder.ALWAYS)
|
||||
```
|
||||
|
||||
`builder = ALWAYS` is a codegen-time error if no matching builder shape is found; `builder =
|
||||
NEVER` always uses a positional constructor even if a builder is detected. This applies
|
||||
regardless of whether the target is hand-authored or foreign/generated - `@DtoMapping` is
|
||||
already declared externally via `package-info.java`, so no annotation on the target itself is
|
||||
needed either way.
|
||||
|
||||
### Named variants excluding nested paths (`name=`, `exclude=`)
|
||||
|
||||
The same `(source, target)` pair can be registered more than once - one base mapping (leaving
|
||||
`name()` empty) plus any number of named variants, each excluding one or more nested
|
||||
ToOne/ToMany properties:
|
||||
|
||||
```java
|
||||
@DtoMapping(source = CUser.class, target = User.class)
|
||||
@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")
|
||||
package org.example.dto;
|
||||
```
|
||||
|
||||
Both variants are generated into the **same** mapper class (one class per target, not one per
|
||||
variant) - the generated `noFleets()` accessor returns a single shared/cached `DtoMapper<CUser,
|
||||
User>` view (not reconstructed per call), omitting `fleets` from both its mapped output (`null`
|
||||
for a ToOne, `List.of()` for a ToMany) and its own `fetchGroup()`. Each excluded property is still
|
||||
evaluated inline at its own declared field position internally (guarded by a boolean flag) - a
|
||||
variant's exclusions never change the evaluation order of the DTO's other properties. Select it
|
||||
with the `query.mapTo(Class, DtoMapper)` overload, which takes an already-resolved mapper instance
|
||||
directly - no string-based lookup:
|
||||
|
||||
```java
|
||||
UserMapper userMapper = new UserMapper();
|
||||
|
||||
// full shape, with fleets fetched/mapped
|
||||
List<User> withFleets = DB.find(CUser.class)
|
||||
.mapTo(User.class, userMapper) // or plain .mapTo(User.class)
|
||||
.findList();
|
||||
|
||||
// bulk listing shape - fleets excluded from both the fetch spec and the output
|
||||
List<User> noFleets = DB.find(CUser.class)
|
||||
.mapTo(User.class, userMapper.noFleets())
|
||||
.findList();
|
||||
```
|
||||
|
||||
Only nested ToOne/ToMany properties can be excluded - a scalar or `@DtoRef` property can't be,
|
||||
since there's no type-safe "absent" value for an arbitrary scalar type. Named variants are
|
||||
scoped to independent, top-level query results only - unlike the base mapping, they don't
|
||||
participate in `DtoMapContext` identity de-duplication when nested elsewhere in a graph, since a
|
||||
variant is never intended to be nested inside another DTO's mapping.
|
||||
|
||||
---
|
||||
|
||||
## `@DtoPath` — renamed or flattened properties
|
||||
|
||||
By default a DTO property is matched to the source entity property (or nested DTO
|
||||
mapper) of the **same name**. `@DtoPath` overrides that, allowing a DTO property to be
|
||||
renamed and/or flattened from a nested path using dot-notation:
|
||||
|
||||
```java
|
||||
public class ContactDto {
|
||||
private final long id;
|
||||
private final String firstName;
|
||||
private final String lastName;
|
||||
|
||||
@DtoPath("customer.billingAddress.city")
|
||||
private final String customerCity; // flattened, 2 hops through customer
|
||||
|
||||
// constructor / getters ...
|
||||
}
|
||||
```
|
||||
|
||||
The generated mapper reads the path with a null-guard at each hop and adds the
|
||||
necessary joins to the fetch spec automatically:
|
||||
|
||||
```java
|
||||
(s.getCustomer() == null ? null
|
||||
: (s.getCustomer().getBillingAddress() == null ? null
|
||||
: s.getCustomer().getBillingAddress().getCity()))
|
||||
```
|
||||
|
||||
`@DtoPath` is purely a compile-time/codegen-time hint — the DTO class itself carries no
|
||||
runtime dependency on the annotation.
|
||||
|
||||
### Fetch-path collisions are a compile-time error
|
||||
|
||||
A `@DtoPath` whose fetch path is identical to a nested `ToOne`/`ToMany` property's own
|
||||
fetch path on the *same* DTO (e.g. a nested `customer` field alongside
|
||||
`@DtoPath("customer.name")` — both resolve to fetch path `"customer"`) fails the build
|
||||
with a clear error, rather than silently discarding one side's fetched properties:
|
||||
|
||||
```
|
||||
error: @DtoPath property 'customerName' on FooDto resolves to fetch path 'customer',
|
||||
which collides with the nested mapping already using that same fetch path - Ebean's
|
||||
fetch spec can only carry one set of properties per path, so one silently discards
|
||||
the other. Move 'customerName' onto the nested DTO type instead, or choose a
|
||||
@DtoPath that reaches into a different, non-colliding path.
|
||||
```
|
||||
|
||||
Fix it either way it suggests: move the property onto the nested DTO type, or choose a
|
||||
`@DtoPath` that reaches a different path (as `customerCity` above does deliberately,
|
||||
using a 3-segment path through `customer.billingAddress` rather than colliding with a
|
||||
plain `customer` nested field).
|
||||
|
||||
---
|
||||
|
||||
## `@DtoRef` — id-only back-references (breaking cycles)
|
||||
|
||||
The DTO graph derived from a set of DTO types must form a DAG — codegen fails if it
|
||||
doesn't. `@DtoRef` is the explicit escape hatch for an intentional back-reference, e.g.
|
||||
a `Contact` DTO referencing its parent `Customer` by id only, rather than re-embedding
|
||||
a full `CustomerDto` (which would recreate the `Customer → Contact → Customer` cycle):
|
||||
|
||||
```java
|
||||
public class ContactDto {
|
||||
private final long id;
|
||||
|
||||
@DtoRef
|
||||
private final Long customerId; // id-only, no nested CustomerDto re-embedded
|
||||
|
||||
// constructor / getters ...
|
||||
}
|
||||
```
|
||||
|
||||
The generated fetch spec adds the association to the **root** `select(...)` rather
|
||||
than a nested `.fetch(...)` — this reads the foreign-key column directly off the base
|
||||
table (no SQL join):
|
||||
|
||||
```java
|
||||
this.fetchGroup = FetchGroup.of(ContactStats.class)
|
||||
.select("customer,contactCount,engagementScore") // "customer" -> FK column, no join
|
||||
.build();
|
||||
```
|
||||
|
||||
```java
|
||||
(source.getCustomer() == null ? null : source.getCustomer().getId())
|
||||
```
|
||||
|
||||
If the same association is *also* independently nested-fetched elsewhere on the DTO
|
||||
(e.g. `ContactDto` has both a nested `customer` field **and** `@DtoRef Long
|
||||
customerId`), the generator recognizes the association is already covered and doesn't
|
||||
add a redundant/duplicate select — no join is added twice.
|
||||
|
||||
---
|
||||
|
||||
## `@DtoConvert` — custom property conversion
|
||||
|
||||
Some properties need more than a plain getter copy — a scalar coercion (`short` to
|
||||
`boolean`), an enum-to-`String` mapping, or a conversion needing a real dependency (e.g.
|
||||
decrypting a value with a cipher). `@DtoConvert(value = ConverterType.class, method =
|
||||
"name")` covers both, combinable with `@DtoPath` when the source value also needs a
|
||||
path/rename override:
|
||||
|
||||
```java
|
||||
public class ContactDto {
|
||||
@DtoPath("status")
|
||||
@DtoConvert(value = ContactConversions.class, method = "toActive")
|
||||
private final boolean active; // Contact.status (Short) -> boolean
|
||||
|
||||
@DtoConvert(value = SecretCipher.class, method = "decode")
|
||||
private final String secretCode; // decrypted via a registered SecretCipher
|
||||
|
||||
// constructor / getters ...
|
||||
}
|
||||
```
|
||||
|
||||
The generator resolves the referenced method at codegen time and dispatches one of two
|
||||
ways, purely based on whether it's `static`:
|
||||
|
||||
- **Static method** — inlined as a direct static call
|
||||
(`ContactConversions.toActive(source.getStatus())`). No registration needed at all —
|
||||
use this for common, reusable, dependency-free coercions.
|
||||
- **Instance method** — the generated mapper resolves one shared instance via
|
||||
`DtoConverterManager.get(SecretCipher.class)`, wired as a constructor
|
||||
parameter/field (the same shape as nested-mapper constructor injection), then calls
|
||||
`secretCipher.decode(source.getSecretCode())`. Use this when the conversion needs a
|
||||
real dependency.
|
||||
|
||||
### Registering an instance-dispatch converter
|
||||
|
||||
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge — register
|
||||
an already-constructed converter instance (e.g. built by your DI container) **before**
|
||||
building the `Database`:
|
||||
|
||||
```java
|
||||
AES256Cipher cipher = ...; // already DI-constructed
|
||||
DtoConverterManager.put(SecretCipher.class, cipher::decrypt); // or a small adapter class
|
||||
|
||||
Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
|
||||
```
|
||||
|
||||
If nothing is registered for a required type, `DtoConverterManager.get(...)` throws a
|
||||
`PersistenceException` immediately — this happens as an eager field initializer on the
|
||||
generated `EbeanDtoMapperRegister`, so a missing registration fails fast at `Database`
|
||||
build time, not lazily on first `mapTo(...)` call.
|
||||
|
||||
> **Testing tip:** since `EbeanDtoMapperRegister`'s mapper fields are all constructed
|
||||
> together when the `Database` starts, register converters via a `DatabaseConfigProvider`
|
||||
> (a `ServiceLoader` hook that runs before the `Database` is built) rather than a test
|
||||
> `@BeforeAll`, so registration always happens before *any* test triggers startup —
|
||||
> regardless of which test class runs first.
|
||||
|
||||
## `@DtoMixin` — overlaying annotations onto a DTO you can't edit
|
||||
|
||||
Some DTOs are generated elsewhere (e.g. from an OpenAPI spec, regenerated on every
|
||||
build) and can't be annotated directly. `@DtoMixin(Target.class)` overlays
|
||||
`@DtoPath`/`@DtoRef`/`@DtoConvert` from a separate companion type instead — directly
|
||||
mirroring avaje-jsonb's `@Json.MixIn` mechanism. Declare a companion interface (or
|
||||
class) whose method names match the target DTO's property names:
|
||||
|
||||
```java
|
||||
// ContactMixinDto itself carries no Ebean annotations at all
|
||||
public class ContactMixinDto {
|
||||
public ContactMixinDto(long id, String firstName, boolean active, String secretCode) { ... }
|
||||
// getters ...
|
||||
}
|
||||
|
||||
@DtoMixin(ContactMixinDto.class)
|
||||
interface ContactMixinDtoMixin {
|
||||
|
||||
@DtoPath("status")
|
||||
@DtoConvert(value = ContactConversions.class, method = "toActive")
|
||||
boolean active();
|
||||
|
||||
@DtoConvert(value = SecretCipher.class, method = "decode")
|
||||
String secretCode();
|
||||
}
|
||||
```
|
||||
|
||||
The processor matches each mixin method to the target's property by name and applies
|
||||
whichever annotations are present as if they were declared on the target field itself.
|
||||
The mixin type is never instantiated and carries no runtime footprint — it's purely a
|
||||
compile-time/codegen-time hint.
|
||||
|
||||
---
|
||||
|
||||
## Computed / aggregate properties via `@Entity @View`
|
||||
|
||||
There's no dedicated "formula on DTO" annotation (a narrower `@Formula2`-on-DTO
|
||||
variant was explored and rejected — see
|
||||
[dto-mapping-design.md](../dto-mapping-design.md) for the reasoning). Instead, model
|
||||
the computed value as its own read-only entity using `@View`, then map that entity to a
|
||||
plain DTO with the same `@DtoMapping` machinery described above. `@View(name = "...")`
|
||||
here just points a second entity at an **existing** table — it does not create a new
|
||||
database view or table.
|
||||
|
||||
### Worked example — computed column (`@Formula2`)
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@View(name = "contact") // reads the existing 'contact' table, no new DDL
|
||||
public class ContactSummary {
|
||||
@Id
|
||||
private Long id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
|
||||
@Formula2("concat(firstName, ' ', lastName)")
|
||||
private String fullName;
|
||||
|
||||
// getters ...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class ContactSummaryDto {
|
||||
private final Long id;
|
||||
private final String fullName;
|
||||
// constructor / getters ...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@DtoMapping(source = ContactSummary.class, target = ContactSummaryDto.class)
|
||||
```
|
||||
|
||||
```java
|
||||
List<ContactSummaryDto> summaries = DB.find(ContactSummary.class)
|
||||
.mapTo(ContactSummaryDto.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Worked example — group-by aggregation (`@Sum`/`@Aggregation`)
|
||||
|
||||
The same `@View`-on-base-table pattern applies to Ebean's `@Sum`/`@Aggregation`
|
||||
group-by formulas — the Blaze-Persistence parallel is an `@EntityView` with
|
||||
`@Mapping("SIZE(...)")`/`@Mapping("SUM(...)")` correlated mappings:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@View(name = "contact")
|
||||
public class ContactStats {
|
||||
@Id
|
||||
private Long id; // required so @Aggregation("count(id)") has something to
|
||||
// count; deliberately never selected/mapped - selecting it
|
||||
// would defeat the aggregation (one row per contact
|
||||
// instead of one row per customer)
|
||||
@ManyToOne
|
||||
private Customer customer;
|
||||
|
||||
@Aggregation("count(id)")
|
||||
private Long contactCount;
|
||||
|
||||
@Sum
|
||||
private Integer engagementScore;
|
||||
|
||||
// getters ...
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class ContactStatsDto {
|
||||
@DtoRef
|
||||
private final Long customerId; // also the implicit GROUP BY key
|
||||
private final Long contactCount;
|
||||
private final Integer engagementScore;
|
||||
// constructor / getters ...
|
||||
}
|
||||
```
|
||||
|
||||
Because `customerId` uses `@DtoRef`, the generated fetch spec is
|
||||
`select("customer,contactCount,engagementScore")` with **no join** — the query groups
|
||||
by the FK column directly:
|
||||
|
||||
```sql
|
||||
select t0.customer_id, count(t0.id), sum(t0.engagement_score)
|
||||
from contact t0
|
||||
group by t0.customer_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance notes
|
||||
|
||||
### Fail-fast, no accidental lazy loading
|
||||
|
||||
`mapTo(...)` forces `query.setUnmodifiable(true)` under the hood. If the mapper ever
|
||||
needs a property that wasn't fetched, it throws `LazyInitialisationException`
|
||||
immediately rather than silently issuing an extra query per row or returning `null`.
|
||||
`InterceptReadOnly` (the unmodifiable-graph bean state) is also cheap — a `boolean[]
|
||||
loaded` flag array plus a `frozen` flag, not a full second copy of bean state.
|
||||
|
||||
### One mapper instance per pair
|
||||
|
||||
Generated mappers are constructed once (in dependency order — a mapper with nested
|
||||
mappers takes them as constructor params) and reused across every `mapTo(...)` call for
|
||||
that pair, resolved and cached by `DtoMapperManager` keyed on `(sourceType, dtoType)`.
|
||||
|
||||
### `DtoMapContext` overhead only where it earns its keep
|
||||
|
||||
As shown above, the generator only involves `DtoMapContext` for mappers that can
|
||||
actually be reached via more than one path in some graph (dedup) or that have nested
|
||||
mappers of their own (need to thread the context down); a DTO that's only ever a
|
||||
top-level query result skips it entirely.
|
||||
|
||||
### Fetch strategy and pagination carry over unchanged
|
||||
|
||||
Existing fetch-strategy control (`+query`/`+lazy`, `fetchQuery()`) and pagination
|
||||
(including keyset pagination and `findPagedList()`) work the same whether the query
|
||||
target is an entity graph or a `mapTo(...)` DTO graph — no special-casing needed.
|
||||
|
||||
---
|
||||
|
||||
## Which should I use?
|
||||
|
||||
- **`mapTo(Dto.class)`** — the target is a **nested** shape (has its own ToOne/ToMany
|
||||
DTO fields) that should mirror part of the entity graph; you want the fetch spec
|
||||
derived automatically and verified to match the DTO's declared shape.
|
||||
- **`asDto(Dto.class)`** / `DB.findDto(...)` — the target is a **flat** row (report,
|
||||
summary, native/vendor SQL); you're comfortable with runtime-checked column-to-bean
|
||||
matching, or the SQL doesn't map cleanly to entity property paths at all.
|
||||
- **Plain entity query** — the caller needs a real, persistable, mutable entity — not a
|
||||
read-only projection.
|
||||
|
||||
---
|
||||
|
||||
## Reference
|
||||
|
||||
### Why `package-info.java`?
|
||||
|
||||
`@DtoMapping` is declared on a package (`ElementType.PACKAGE`), not the DTO or the
|
||||
entity, because:
|
||||
- the DTO type is often owned/generated elsewhere (e.g. from an OpenAPI spec) and
|
||||
shouldn't need to be annotated with an internal persistence/entity type;
|
||||
- one entity may be the source for several different DTOs (e.g. a summary vs. a detail
|
||||
view), and the same entity/DTO pair may need registering from multiple consuming
|
||||
modules.
|
||||
|
||||
### Annotations at a glance
|
||||
|
||||
| Annotation | Target | Purpose |
|
||||
|---|---|---|
|
||||
| `@DtoMapping(source=, target=)` | `package-info.java` | Registers an entity → DTO pair, triggers mapper generation |
|
||||
| `@DtoMapping(..., builder=)` | `package-info.java` | `AUTO` (default, threshold-based) / `ALWAYS` / `NEVER` - builder-chain vs positional constructor |
|
||||
| `@DtoMapping(..., name=, exclude=)` | `package-info.java` | Registers a named variant sharing the base mapping's generated class, excluding nested paths |
|
||||
| `@DtoPath("a.b.c")` | DTO field/getter | Renamed and/or flattened multi-hop property mapping |
|
||||
| `@DtoRef` | DTO field/getter | Id-only back-reference; breaks a cycle; root-selects the FK (no join) |
|
||||
| `@DtoConvert(value=, method=)` | DTO field/getter | Custom scalar conversion - static (no registration) or instance (via `DtoConverterManager`) dispatch |
|
||||
| `@DtoMixin(Target.class)` | Companion interface/class | Overlays `@DtoPath`/`@DtoRef`/`@DtoConvert` onto a DTO that can't be annotated directly |
|
||||
|
||||
### Parallels with other tools
|
||||
|
||||
If you're coming from another mapping library, here's the rough correspondence:
|
||||
|
||||
| Ebean | MapStruct | Blaze-Persistence |
|
||||
|---|---|---|
|
||||
| Generated `DtoMapper` per (source, DTO) pair | Generated `@Mapper` implementation | `@EntityView` (interface + runtime proxy) |
|
||||
| `@DtoPath("a.b.c")` | `@Mapping(target = "x", source = "a.b.c")` | `@Mapping("a.b.c")` |
|
||||
| `@DtoRef` | `@Context`/manual cycle-breaking (no dedicated annotation) | Sub-view referencing an id-only projection |
|
||||
| `@DtoConvert(value=, method=)` | `@Mapping(qualifiedByName = "...")` / custom mapper methods | Custom converter/`@Mapping` expression |
|
||||
| `@DtoMixin(Target.class)` | N/A (annotate the `@Mapper` interface's abstract methods instead) | N/A |
|
||||
| `DtoMapContext` identity de-dup | Not built in (opt-in `@MappingTarget`/manual caching) | Built in (entity-view identity) |
|
||||
| `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation` for computed DTO values | N/A (MapStruct doesn't touch SQL) | `@Mapping("SIZE(...)")` / `@Mapping("SUM(...)")` correlated mappings |
|
||||
|
||||
See [dto-mapping-design.md](../dto-mapping-design.md) for the full design rationale and
|
||||
[dto-mapping-requirements.md](../dto-mapping-requirements.md) for the accepted/rejected
|
||||
requirements this feature was scoped against (issue
|
||||
[#2540](https://github.com/ebean-orm/ebean/issues/2540)).
|
||||
@@ -1,464 +0,0 @@
|
||||
# Guide: Using `RawSql` with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
`RawSql` lets you back an Ebean bean with a **hand-written SQL query** instead of
|
||||
Ebean generating the SQL from the entity mapping. Ebean still handles object
|
||||
mapping (result set columns → bean properties), lazy loading of associated beans,
|
||||
and - depending on how the `RawSql` is built - dynamic `WHERE`/`HAVING` predicates
|
||||
added through the normal query API.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- run vendor-specific SQL, complex aggregation, or reporting queries that don't
|
||||
map cleanly to an ORM query
|
||||
- reuse a hand-tuned query but still want typed/dynamic predicates, paging, or
|
||||
`ORDER BY` added by the caller
|
||||
- back a query bean (`Q*`) or DTO-like bean with SQL containing a CTE, window
|
||||
function, or subquery in the `FROM` clause
|
||||
|
||||
Prefer ordinary query bean queries first - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md), Step 9,
|
||||
for the full decision order (query bean → `asDto()` → DTO query → raw SQL).
|
||||
This guide covers raw SQL once you've decided it's the right tool.
|
||||
|
||||
---
|
||||
|
||||
## The bean behind a `RawSql` query
|
||||
|
||||
A bean queried with `RawSql` is not necessarily backed by a physical table. Annotate
|
||||
it `@Entity @Sql` to tell Ebean it is mapped via `RawSql` rather than table DDL:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Sql
|
||||
public class OrderAggregate {
|
||||
|
||||
@OneToOne
|
||||
Order order;
|
||||
|
||||
Double totalAmount;
|
||||
Long totalItems;
|
||||
|
||||
// getters/setters
|
||||
}
|
||||
```
|
||||
|
||||
`@Sql` beans still get a generated query bean (`QOrderAggregate`) if the
|
||||
querybean-generator annotation processor is configured - see
|
||||
[Using `RawSql` with query beans](#using-rawsql-with-query-beans) below.
|
||||
|
||||
You can also query an ordinary table-backed `@Entity` with `RawSql` - the column
|
||||
mapping just needs to line up with that entity's properties.
|
||||
|
||||
---
|
||||
|
||||
## Building a `RawSql` - three factory methods
|
||||
|
||||
`RawSqlBuilder` has three ways to construct a `RawSql`, depending on how much of
|
||||
the SQL Ebean needs to understand:
|
||||
|
||||
| Method | SELECT columns parsed? | Dynamic WHERE/HAVING/ORDER BY? | Use for |
|
||||
|--------|------------------------|------------------------|---------|
|
||||
| `RawSqlBuilder.parse(sql)` | Yes | Yes | Ordinary `SELECT ... FROM ... WHERE ...` statements |
|
||||
| `RawSqlBuilder.unparsed(sql)` | No | No | Fixed SQL that never needs additional predicates |
|
||||
| `RawSqlBuilder.withPlaceholders(sql)` | No (explicit `columnMapping()` required) | Yes, via `${where}` / `${andWhere}` / `${having}` / `${andHaving}` / `${orderBy}` / `${andOrderBy}` | CTEs, window functions, subqueries - SQL that keyword-based parsing can't handle |
|
||||
|
||||
### `parse(sql)` - the common case
|
||||
|
||||
`parse(sql)` scans the SQL text for the `select` / `from` / `where` / `group by`
|
||||
/ `having` / `order by` keywords to work out the SELECT column list (so it can
|
||||
validate your column mappings) and the injection points for dynamic `WHERE`/
|
||||
`HAVING` expressions.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse(
|
||||
"select c.id, c.name, c.status from customer c")
|
||||
.columnMapping("c.id", "id")
|
||||
.columnMapping("c.name", "name")
|
||||
.columnMapping("c.status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().eq("status", Customer.Status.ACTIVE)
|
||||
.orderBy("name")
|
||||
.findList();
|
||||
```
|
||||
|
||||
Because the SQL is parsed, mistakes in `columnMapping()` (unknown column, wrong
|
||||
order for `unparsed`-style mappings) are caught early. **This fails on SQL the
|
||||
keyword parser can't make sense of** - a `WITH` CTE, a window function, a
|
||||
subquery in `FROM`, etc. - because the keyword positions found don't correspond
|
||||
to the outer query's real structure. Use `withPlaceholders(sql)` for that SQL
|
||||
instead (see below).
|
||||
|
||||
### `unparsed(sql)` - fixed queries
|
||||
|
||||
`unparsed(sql)` skips all parsing. The SQL is used exactly as written, and **no
|
||||
further `WHERE`/`HAVING`/`ORDER BY` can be added** by the caller - useful for a
|
||||
completely fixed reporting query with no caller-supplied filtering.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.unparsed(
|
||||
"select id, name, status from customer where status = 'ACTIVE'")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Column mappings for `unparsed(sql)` must be supplied **in the same order** as
|
||||
the columns appear in the SQL, since there's no parsing to match them by name.
|
||||
|
||||
### `withPlaceholders(sql)` - complex SQL (CTEs, window functions, subqueries)
|
||||
|
||||
`withPlaceholders(sql)` avoids keyword scanning entirely. You mark exactly where
|
||||
a dynamic `WHERE`/`HAVING`/`ORDER BY` expression should be injected using
|
||||
placeholder tokens, and column mappings are always explicit (as with `unparsed`).
|
||||
|
||||
#### Placeholder reference
|
||||
|
||||
| Placeholder | Meaning | Use when |
|
||||
|-------------|---------|----------|
|
||||
| `${where}` | Insert a new `WHERE <expr>` clause here | No static `WHERE` clause exists yet at this point in the SQL |
|
||||
| `${andWhere}` | Insert `AND <expr>` here | A static `WHERE ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${having}` | Insert a new `HAVING <expr>` clause here | No static `HAVING` clause exists yet at this point in the SQL |
|
||||
| `${andHaving}` | Insert `AND <expr>` here | A static `HAVING ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${orderBy}` | Insert a new `ORDER BY <expr>` clause here | No static `ORDER BY` clause exists yet at this point in the SQL, and callers may supply `.orderBy(...)` |
|
||||
| `${andOrderBy}` | Insert `, <expr>` here | A static `ORDER BY ...` clause already exists in the SQL and you want callers to be able to append extra sort columns to it |
|
||||
|
||||
Rules:
|
||||
|
||||
- At least one placeholder is required - `withPlaceholders(sql)` throws
|
||||
`IllegalArgumentException` if none of the six tokens are present.
|
||||
- Use only the placeholders you need. Omit `${where}`/`${andWhere}` entirely if
|
||||
the query never needs a dynamic `WHERE` (e.g. only a dynamic `HAVING` on an
|
||||
aggregate). Omit `${having}`/`${andHaving}` if there's no dynamic `HAVING`.
|
||||
Omit `${orderBy}`/`${andOrderBy}` if the ordering is always fixed.
|
||||
- Explicit `columnMapping()` is required for every returned column - there is no
|
||||
column-list parsing to infer names from.
|
||||
- **A caller-supplied `.orderBy(...)`/`.order(...)` is only applied if the SQL
|
||||
contains an `${orderBy}` or `${andOrderBy}` placeholder.** Without one of
|
||||
those placeholders there is no defined injection point for dynamic ordering,
|
||||
so any `.orderBy(...)` call on the query is safely ignored rather than risk
|
||||
producing invalid SQL - even if the template has a static trailing
|
||||
`ORDER BY ...` of its own. If you need callers to be able to influence
|
||||
ordering, add `${orderBy}` (no existing static order by) or `${andOrderBy}`
|
||||
(append after an existing static order by).
|
||||
- Any other static SQL that follows a `${where}`/`${having}` placeholder (e.g.
|
||||
a trailing `GROUP BY`) is preserved and correctly positioned **after**
|
||||
whatever dynamic expression gets injected at that placeholder.
|
||||
|
||||
#### Example - CTE with `${where}`
|
||||
|
||||
```java
|
||||
String sql = """
|
||||
with order_totals as (
|
||||
select o.id as order_id, sum(d.qty * d.unit_price) as total_amount
|
||||
from o_order o
|
||||
join o_order_detail d on d.order_id = o.id
|
||||
group by o.id
|
||||
)
|
||||
select order_id, total_amount
|
||||
from order_totals
|
||||
${where}
|
||||
order by order_id
|
||||
""";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
`total_amount` is a genuine column of the `order_totals` CTE here, so it's valid
|
||||
to filter on it in the outer `WHERE` - this only works because the aggregate is
|
||||
computed inside the CTE rather than as a same-level `SELECT` alias.
|
||||
|
||||
#### Example - static `WHERE` already present, append with `${andWhere}`
|
||||
|
||||
```java
|
||||
String sql = "... from order_totals where total_amount > 0 ${andWhere} order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > 0 and total_amount > ? order by order_id
|
||||
DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
#### Example - `${having}` only, filtering on an aggregate directly
|
||||
|
||||
No `WHERE` placeholder is needed if you only ever filter on the aggregate value:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.having().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The dynamic `HAVING` clause is injected before the static trailing `ORDER BY`,
|
||||
even though `${having}` is the only placeholder present. Because there's no
|
||||
`${orderBy}`/`${andOrderBy}` placeholder here, a caller-supplied `.orderBy(...)`
|
||||
would be ignored - the ordering stays fixed as `order by order_id`.
|
||||
|
||||
#### Example - both `${where}` and `${having}`
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" ${where}" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("order.id", 0)
|
||||
.having().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Both the dynamic `WHERE` and dynamic `HAVING` are injected at their respective
|
||||
placeholders, and the trailing `order by order_id` is preserved after the
|
||||
`HAVING` clause.
|
||||
|
||||
#### Example - `${orderBy}`, fully dynamic ordering
|
||||
|
||||
Use `${orderBy}` when there's no static default ordering and you want the
|
||||
caller's `.orderBy(...)` to control it entirely:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"with order_totals as (" +
|
||||
" select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
")" +
|
||||
" select order_id, total_amount from order_totals" +
|
||||
" ${where}" +
|
||||
" ${orderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > ? order by total_amount desc
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("totalAmount desc")
|
||||
.findList();
|
||||
```
|
||||
|
||||
If the caller doesn't call `.orderBy(...)`, nothing is injected at `${orderBy}`
|
||||
and no `ORDER BY` clause is emitted at all.
|
||||
|
||||
#### Example - `${andOrderBy}`, appending to a static default ordering
|
||||
|
||||
Use `${andOrderBy}` when there's a sensible static default ordering but you
|
||||
want callers to be able to add extra tie-breaker sort columns:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"... from order_totals" +
|
||||
" ${where}" +
|
||||
" order by total_amount desc ${andOrderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... order by total_amount desc , order_id
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("order.id")
|
||||
.findList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using `fetchQuery()` to build out more of the graph
|
||||
|
||||
A `RawSql` query can be the **root query** and still use `fetchQuery(path)` the
|
||||
same way an ordinary ORM query does - Ebean runs the raw SQL for the root rows,
|
||||
then runs additional secondary ORM queries to populate the requested paths. This
|
||||
lets you hand-write only the part of the query that needs raw SQL (e.g. an
|
||||
aggregate/CTE) and let the ORM build out the rest of the object graph normally.
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql) // root query - runs the CTE/aggregate SQL
|
||||
.fetchQuery("order") // secondary query - loads the full Order
|
||||
.fetchQuery("order.details") // secondary query - loads Order.details
|
||||
.where().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
This executes **three** queries: the raw SQL root query, then one secondary
|
||||
query per `fetchQuery(path)` call.
|
||||
|
||||
**Important**: if the raw SQL's column mapping only populates part of an
|
||||
association (e.g. only `order.id`, as in the examples above), that association
|
||||
is a *partial reference*. To load a nested to-many under it (e.g.
|
||||
`order.details`), you must add an explicit `fetchQuery(...)` (or `fetch(...)`)
|
||||
for the **intermediate path** (`order`) as well as the nested path
|
||||
(`order.details`) - `fetchQuery("order.details")` alone will leave `details` as
|
||||
a deferred/lazy collection, because Ebean doesn't otherwise have a fetch node
|
||||
for `order` to hang the secondary query off. If the raw SQL already selects the
|
||||
full set of columns for an association directly (no partial reference), this
|
||||
extra step isn't needed.
|
||||
|
||||
This is the same `fetchQuery()` mechanism used for ordinary query bean queries -
|
||||
see [Use `fetchQuery()` for to-many paths](writing-ebean-query-beans.md#step-7---use-fetchquery-for-to-many-paths-and-fetchgroup-for-reusable-query-shapes)
|
||||
for background on why to-many paths are loaded via secondary queries rather than
|
||||
a single joined query.
|
||||
|
||||
---
|
||||
|
||||
## Column mapping
|
||||
|
||||
Every `RawSqlBuilder` (except a bare `unparsed(sql)` with implicit positional
|
||||
mapping) uses `columnMapping(dbColumn, propertyName)` to map SQL result columns
|
||||
to bean properties:
|
||||
|
||||
```java
|
||||
.columnMapping("order_id", "order.id") // maps to the "order" association's "id" property
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
```
|
||||
|
||||
- Dotted property paths (e.g. `"order.id"`) map a column into a nested/associated
|
||||
bean property.
|
||||
- `columnMappingIgnore(dbColumn)` marks a selected column as intentionally unmapped
|
||||
(present in the SQL but not needed on the bean).
|
||||
- `tableAliasMapping(tableAlias, path)` bulk-renames every mapping using a given
|
||||
SQL table alias to be prefixed with a bean property path - handy when a `parse()`
|
||||
query selects many columns from a joined table (e.g. alias `c` → path `customer`)
|
||||
and you don't want to repeat the prefix in every `columnMapping()` call.
|
||||
|
||||
---
|
||||
|
||||
## Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()`-equivalent
|
||||
expressions (as bean properties) over hand-written SQL. Every generated query
|
||||
bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
This also works with `withPlaceholders(sql)` and an `@Sql` query bean:
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = new QOrderAggregate()
|
||||
.setRawSql(rawSql) // built with withPlaceholders() as shown above
|
||||
.totalAmount.gt(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The typed property expression (`.totalAmount.gt(100)`) is translated to a bound
|
||||
predicate and injected at the `${where}`/`${having}` placeholder position, exactly
|
||||
as `.where().gt("totalAmount", 100)` would be on the plain `Query<T>` API.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - reaching for raw SQL before trying a query bean
|
||||
|
||||
Complex-looking joins are often just ordinary association traversal in a query
|
||||
bean. Don't use raw SQL just because a query touches several tables - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md).
|
||||
|
||||
### Anti-pattern 2 - using `parse(sql)` on a CTE or window-function query
|
||||
|
||||
`parse(sql)` will throw a parsing exception (or silently mis-locate the WHERE
|
||||
injection point) on SQL it can't understand structurally. If your SQL starts
|
||||
with `WITH ...` or has a subquery in `FROM`, use `withPlaceholders(sql)` instead.
|
||||
|
||||
### Anti-pattern 3 - filtering on a same-level SELECT alias
|
||||
|
||||
You cannot add a dynamic `WHERE` predicate on a `SELECT`-clause alias in the
|
||||
same query level (e.g. `select sum(x) as total ... ${where}` - `total` isn't a
|
||||
real column yet at the `WHERE` stage of that query level). Either:
|
||||
|
||||
- move the aggregation into a CTE and filter on the CTE's output column in the
|
||||
outer query (`WHERE` case), or
|
||||
- use `${having}`/`${andHaving}` to filter on the aggregate at the `HAVING` stage
|
||||
of the same query level, where the aggregate expression is valid.
|
||||
|
||||
### Anti-pattern 4 - forgetting `columnMapping()` with `unparsed()`/`withPlaceholders()`
|
||||
|
||||
Both `unparsed(sql)` and `withPlaceholders(sql)` require **every** returned
|
||||
column to be explicitly mapped (or explicitly ignored via
|
||||
`columnMappingIgnore(...)`) - there's no column-list parsing to infer them.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `RuntimeException: Error parsing sql, can not find ... keyword` | `parse(sql)` used on SQL with a CTE, window function, or subquery in `FROM` | Use `RawSqlBuilder.withPlaceholders(sql)` instead |
|
||||
| `IllegalArgumentException: withPlaceholders() requires at least one of ${where}, ${andWhere}, ${having}, ${andHaving}, ${orderBy}, ${andOrderBy}...` | None of the six placeholder tokens were found in the SQL | Add the appropriate placeholder token at the injection point |
|
||||
| Dynamic `WHERE`/`HAVING` predicate silently has no effect, or query throws | Used `unparsed(sql)` and then tried to add a predicate | `unparsed(sql)` queries cannot be modified - switch to `parse(sql)` or `withPlaceholders(sql)` |
|
||||
| Generated SQL is invalid / clauses appear in the wrong order | Predicates added via `.where()`/`.having()` don't match the placeholders actually present in the SQL | Make sure `${where}`/`${having}` (or the `and` variants) exist at the point you expect predicates to be injected |
|
||||
| `.orderBy(...)`/`.order(...)` on the query silently has no effect | The SQL has no `${orderBy}`/`${andOrderBy}` placeholder | This is by design - without one of those placeholders there's no defined injection point, so the ordering is ignored rather than corrupting the SQL. Add `${orderBy}` or `${andOrderBy}` if you need caller-controlled ordering |
|
||||
| `Unknown column` / unmapped property error | Missing `columnMapping()` for a selected column | Add a `columnMapping(...)` or `columnMappingIgnore(...)` for every SQL column |
|
||||
| `fetchQuery("a.b")` collection stays deferred/lazy | `a` is a partial reference from the raw SQL column mapping (e.g. only `a.id` mapped), and there's no fetch node for `a` itself | Add `fetchQuery("a")` (or `fetch("a")`) alongside `fetchQuery("a.b")` |
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Write Ebean queries with query beans](writing-ebean-query-beans.md)
|
||||
- [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
@@ -431,48 +431,6 @@ List<Customer> customers = new QCustomer()
|
||||
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.
|
||||
|
||||
### `@OneToOne(mappedBy=...)` is EAGER by default — mark it LAZY
|
||||
|
||||
The non-owning side of a `@OneToOne` (the side with `mappedBy`) defaults to
|
||||
`FetchType.EAGER` per JPA, same as `@ManyToOne`. Unlike a `@ManyToOne`
|
||||
reference (which is FK-only until `.fetch()`'d), Ebean's default select for an
|
||||
EAGER `@OneToOne(mappedBy=...)` still adds a `left join` to the target table
|
||||
on **every** query for the owning entity — even a plain `findById()` — because
|
||||
there is no local FK column to use as a lazy reference; the only way to know
|
||||
the associated row exists is to join to it.
|
||||
|
||||
If that association is rarely needed (e.g. a rarely-read child/detail table),
|
||||
this join executes on every load of the parent, including in hot-path list
|
||||
queries, and can dominate query cost as more such associations accumulate.
|
||||
|
||||
**Always set `fetch = FetchType.LAZY` on `@OneToOne(mappedBy=...)`
|
||||
associations unless the association is genuinely needed on (almost) every
|
||||
load:**
|
||||
|
||||
```java
|
||||
@OneToOne(mappedBy = "device", fetch = FetchType.LAZY)
|
||||
private SensorBoard sensorBoard;
|
||||
```
|
||||
|
||||
This correctly excludes the join from Ebean's default select clause (verified
|
||||
for FK-based, non-shared-primary-key `@OneToOne` relationships — the common
|
||||
case). Callers that do need the association can still `.fetch("sensorBoard")`
|
||||
explicitly on the query bean.
|
||||
|
||||
**Caveat:** the exclusion is driven by Ebean's default-select-clause
|
||||
mechanism. It is bypassed if the query has already been switched into an
|
||||
"all properties" mode by something other than the deploy-time
|
||||
`FetchType.LAZY`/`EAGER` metadata (for example, an active AutoTune profile
|
||||
that supplies its own tuned property set). Confirm the join is actually gone
|
||||
by checking generated SQL (`LoggedSql` in tests, or query logging) after
|
||||
making this change — don't assume it's excluded from the annotation alone.
|
||||
|
||||
### Agent rule
|
||||
|
||||
Default new `@OneToOne(mappedBy=...)` fields to `fetch = FetchType.LAZY`
|
||||
unless there's a clear reason the association is needed on every load. This
|
||||
is a one-line, low-risk change that avoids an always-on join.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use DTO projection when the caller does not need entity beans
|
||||
@@ -504,11 +462,6 @@ List<CustomerSummary> summaries = new QCustomer()
|
||||
- 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
|
||||
|
||||
`asDto(...)` maps a **flat**, single-row result. If the target DTO itself needs nested
|
||||
DTO fields (ToOne/ToMany) mirroring part of the entity graph, use
|
||||
`mapTo(Dto.class)` instead — see
|
||||
[Mapping entity graphs to DTOs](mapping-entity-graphs-to-dtos.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
|
||||
@@ -530,30 +483,6 @@ Prefer the following order:
|
||||
Do **not** jump to raw SQL just because the query joins multiple tables. Query
|
||||
beans already handle ordinary relationship traversal well.
|
||||
|
||||
### Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()` expressions over
|
||||
hand-written SQL. Every generated query bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
For the full guide to building `RawSql` - including `unparsed()`,
|
||||
`withPlaceholders()` for CTEs/window functions, the `${where}` / `${andWhere}`
|
||||
/ `${having}` / `${andHaving}` placeholder reference, and column mapping - see
|
||||
[Using `RawSql` with Ebean](using-rawsql-with-ebean.md).
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
@@ -641,5 +570,4 @@ When asked to add or modify an Ebean query:
|
||||
- [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)
|
||||
- [Using `RawSql` with Ebean](using-rawsql-with-ebean.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
|
||||
+1
-48
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.5.0</version>
|
||||
<version>18.0.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -103,53 +103,6 @@
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<release>11</release>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>compile-21</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
<compileSourceRoots>
|
||||
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
|
||||
</compileSourceRoots>
|
||||
<multiReleaseOutput>true</multiReleaseOutput>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<addMavenDescriptor>false</addMavenDescriptor>
|
||||
<manifestEntries>
|
||||
<Multi-Release>true</Multi-Release>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
<!-- <manifest>-->
|
||||
<!-- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>-->
|
||||
<!-- </manifest>-->
|
||||
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,10 +457,6 @@ public final class DB {
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)} but with given transaction.
|
||||
* <p>
|
||||
* For control over query cache use and whether to skip the check when the bean's unique
|
||||
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
|
||||
* via {@link #getDefault()} instead.
|
||||
*/
|
||||
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return getDefault().checkUniqueness(bean, transaction);
|
||||
@@ -745,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>
|
||||
@@ -779,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>
|
||||
|
||||
@@ -247,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)}.
|
||||
*
|
||||
@@ -266,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>
|
||||
@@ -464,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.
|
||||
@@ -1078,21 +1019,12 @@ public interface Database {
|
||||
* @param bean The entity bean to check uniqueness on
|
||||
* @return a set of Properties if constraint validation was detected or empty list.
|
||||
*/
|
||||
default Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null, false, true);
|
||||
}
|
||||
Set<Property> checkUniqueness(Object bean);
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
||||
*/
|
||||
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||
return checkUniqueness(bean, transaction, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
|
||||
Set<Property> checkUniqueness(Object bean, Transaction transaction);
|
||||
|
||||
/**
|
||||
* Marks the entity bean as dirty.
|
||||
@@ -1387,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>
|
||||
|
||||
@@ -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;
|
||||
@@ -61,11 +59,6 @@ public interface DatabaseBuilder {
|
||||
|
||||
/**
|
||||
* Build and return the Database instance.
|
||||
* <p>
|
||||
* When {@link #setRegister(boolean)} is set to true (the default), and a database
|
||||
* with the same name is already registered, this throws an {@link IllegalStateException}.
|
||||
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
|
||||
* Database instance is not intended to be registered/looked up by name.
|
||||
*/
|
||||
Database build();
|
||||
|
||||
@@ -696,38 +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 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.
|
||||
@@ -888,13 +849,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper);
|
||||
|
||||
/**
|
||||
* Enable tenant-partitioned caches. When enabled each tenant gets its own cache namespace,
|
||||
* improving cache-hit ratio by preventing cross-tenant key collisions.
|
||||
* Use {@link SpiCacheManager#clearTenant(Object)} when a tenant is deactivated.
|
||||
*/
|
||||
DatabaseBuilder tenantPartitionedCache(boolean tenantPartitionedCache);
|
||||
|
||||
/**
|
||||
* Set the L2 cache default max size.
|
||||
*/
|
||||
@@ -994,7 +948,7 @@ public interface DatabaseBuilder {
|
||||
* <p>
|
||||
* Use this to override the default known aggregation functions.
|
||||
*/
|
||||
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
|
||||
/**
|
||||
* Set to true if all DB column and table names should use quoted identifiers.
|
||||
@@ -1012,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.
|
||||
*/
|
||||
@@ -2474,16 +2418,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isChangeLogAsync();
|
||||
|
||||
/**
|
||||
* Return the ReadAuditLogger to use.
|
||||
*/
|
||||
ReadAuditLogger getReadAuditLogger();
|
||||
|
||||
/**
|
||||
* Return the ReadAuditPrepare to use.
|
||||
*/
|
||||
ReadAuditPrepare getReadAuditPrepare();
|
||||
|
||||
/**
|
||||
* Return the tenancy catalog provider.
|
||||
*/
|
||||
@@ -2575,11 +2509,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isAutoPersistUpdates();
|
||||
|
||||
/**
|
||||
* Return true if caches are partitioned by tenant.
|
||||
*/
|
||||
boolean isTenantPartitionedCache();
|
||||
|
||||
/**
|
||||
* Return the L2 cache default max size.
|
||||
*/
|
||||
@@ -2627,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.
|
||||
*/
|
||||
|
||||
@@ -71,28 +71,18 @@ public final class DatabaseFactory {
|
||||
lock.lock();
|
||||
try {
|
||||
var config = builder.settings();
|
||||
var name = config.getName();
|
||||
if (name == null) {
|
||||
if (config.getName() == null) {
|
||||
throw new PersistenceException("The name is null (it is required)");
|
||||
}
|
||||
if (config.isRegister()) {
|
||||
// We're explicitly creating a database to be registered, so avoid
|
||||
// triggering DbContext static initialisation to auto-create a default one.
|
||||
DbPrimary.setSkip(true);
|
||||
if (DbContext.getInstance().contains(name)) {
|
||||
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
|
||||
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
|
||||
+ " if this Database instance is not intended to be registered/looked up by name.");
|
||||
}
|
||||
}
|
||||
Database server = createInternal(config);
|
||||
if (config.isRegister()) {
|
||||
if (config.isDefaultServer()) {
|
||||
if (defaultServerName != null && !defaultServerName.equals(name)) {
|
||||
throw new IllegalStateException("Registering [" + name + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
if (defaultServerName != null && !defaultServerName.equals(config.getName())) {
|
||||
throw new IllegalStateException("Registering [" + config.getName() + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
}
|
||||
defaultServerName = name;
|
||||
defaultServerName = config.getName();
|
||||
}
|
||||
DbPrimary.setSkip(true);
|
||||
DbContext.getInstance().register(server, config.isDefaultServer());
|
||||
}
|
||||
return server;
|
||||
@@ -121,24 +111,6 @@ public final class DatabaseFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration of this Database.
|
||||
* <p>
|
||||
* This is invoked when a Database is shutdown so that its registered name
|
||||
* becomes available again for a subsequently created Database with the same name.
|
||||
*/
|
||||
public static void unregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
DbContext.getInstance().deregister(server);
|
||||
if (server.name().equals(defaultServerName)) {
|
||||
defaultServerName = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown gracefully all Database instances cleaning up any resources as required.
|
||||
* <p>
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
|
||||
import io.ebean.datasource.DataSourceConfigurationException;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -76,10 +75,6 @@ final class DbContext {
|
||||
return defaultDatabase;
|
||||
}
|
||||
|
||||
boolean contains(String name) {
|
||||
return concMap.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the database by name.
|
||||
*/
|
||||
@@ -120,27 +115,6 @@ final class DbContext {
|
||||
registerWithName(server.name(), server, isDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the registration for this Database (typically on shutdown) so that
|
||||
* its name becomes available again for a subsequently created Database.
|
||||
* <p>
|
||||
* Only removes the registration if it currently maps to this exact instance
|
||||
* (avoids removing a different Database subsequently registered with the same name).
|
||||
*/
|
||||
void deregister(Database server) {
|
||||
lock.lock();
|
||||
try {
|
||||
String name = server.name();
|
||||
concMap.remove(name, server);
|
||||
syncMap.remove(name, server);
|
||||
if (defaultDatabase == server) {
|
||||
defaultDatabase = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void registerWithName(String name, Database server, boolean isDefault) {
|
||||
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,65 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Static bridge registering custom {@code @DtoConvert} converter instances so generated DTO
|
||||
* mappers can reach them.
|
||||
* <p>
|
||||
* Generated mappers (see {@code query.mapTo(SomeDto.class)}) are wired via {@code ServiceLoader}
|
||||
* as plain, no-arg-constructed, compile-time singletons (mirroring how entity/query-bean
|
||||
* registration already works) - they have no way to reach a dependency-injection container, or
|
||||
* any particular {@code Database} instance, at construction time. When a
|
||||
* {@code @DtoConvert(value = ConverterType.class, method = "...")} property's converter is an
|
||||
* <b>instance</b> method (as opposed to a {@code static} one, which is called directly with no
|
||||
* registration needed at all), the generated mapper resolves it via {@link #get(Class)} - so the
|
||||
* application must register an instance here, typically one already built by its own DI
|
||||
* container, <b>before</b> building the {@code Database}:
|
||||
* <pre>{@code
|
||||
* AES256Cipher cipher = ...; // already DI-constructed
|
||||
* DtoConverterManager.put(DriverConversions.class, new DriverConversionsImpl(cipher));
|
||||
*
|
||||
* Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
|
||||
* }</pre>
|
||||
* <p>
|
||||
* This is a deliberate, narrowly-scoped exception to preferring dependency injection over static
|
||||
* mutable state - it exists solely to bridge an already-DI-constructed singleton into
|
||||
* {@code ServiceLoader}-discovered, no-arg-constructed generated code, which cannot otherwise
|
||||
* reach a DI container or a specific {@code Database} instance. {@link #get(Class)} throws
|
||||
* immediately if nothing was registered for the given type, so a missing/late registration fails
|
||||
* fast at {@code Database} build time (a generated mapper's eager field initializer) rather than
|
||||
* lazily on first use.
|
||||
*/
|
||||
public final class DtoConverterManager {
|
||||
|
||||
private static final Map<Class<?>, Object> converters = new ConcurrentHashMap<>();
|
||||
|
||||
private DtoConverterManager() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a converter instance for the given type - must be called before the
|
||||
* {@code Database} using it is built.
|
||||
*/
|
||||
public static <T> void put(Class<T> type, T instance) {
|
||||
converters.put(type, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered converter instance for the given type.
|
||||
*
|
||||
* @throws PersistenceException if no instance was registered for {@code type}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(Class<T> type) {
|
||||
T instance = (T) converters.get(type);
|
||||
if (instance == null) {
|
||||
throw new PersistenceException("No " + type.getName() + " registered - call "
|
||||
+ "DtoConverterManager.put(" + type.getSimpleName() + ".class, ...) before starting the Database");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Identity-keyed cache of already-mapped source -> target instances, shared across one
|
||||
* top-level {@link DtoMapper#mapList(java.util.List)} call (or an explicitly shared context).
|
||||
* <p>
|
||||
* Keyed by source object <b>identity</b> (an {@link IdentityHashMap}, not {@code equals()}/
|
||||
* {@code hashCode()}) because the source is an Ebean entity graph, where repeated references to
|
||||
* the same row within one query already resolve to the same Java object instance.
|
||||
* <p>
|
||||
* The identity map is partitioned <b>per target DTO type</b>. This matters because the same
|
||||
* source instance can legitimately need to be mapped to more than one target type within a
|
||||
* single graph - e.g. a top-level {@code CustomerDtoMapper} maps a {@code Customer} to a full
|
||||
* {@code CustomerDto}, while a nested {@code ContactDtoMapper} maps the very same {@code Customer}
|
||||
* instance (accessed via {@code contact.getCustomer()}) to a shallow {@code CustomerRefDto} to
|
||||
* avoid a cycle. A single un-partitioned {@code IdentityHashMap<Object,Object>} would have the
|
||||
* two mappers collide on the same source key and incorrectly hand back the other mapper's
|
||||
* (wrong-typed) cached result. Partitioning by target type keeps each mapper's cache isolated
|
||||
* while still sharing one context/instance per top-level mapping call.
|
||||
* <p>
|
||||
* Not thread-safe - a context is expected to be created per top-level mapping call and not
|
||||
* shared across threads.
|
||||
*/
|
||||
public final class DtoMapContext {
|
||||
|
||||
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Return the already-mapped target for the given source instance if present, otherwise map it
|
||||
* via {@code mappingFunction}, register it, and return it.
|
||||
*
|
||||
* @param targetType the DTO type being produced - used to partition the identity cache so that
|
||||
* mapping the same source to different target types never collides.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
|
||||
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
|
||||
T existing = (T) mapped.get(source);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
T created = mappingFunction.apply(source);
|
||||
mapped.put(source, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper interface implemented by generated (or hand-written) entity -> DTO graph mappers.
|
||||
* <p>
|
||||
* Used with nested entity-to-DTO graph mapping (see {@code query.mapTo(SomeDto.class)}) as
|
||||
* distinct from the existing flat, single-row {@link DtoQuery} pipeline. Each entity/DTO type
|
||||
* pair gets its own small, composable mapper implementation (mirroring MapStruct's per-type
|
||||
* mapper generation) rather than one large mapper inlining every nested type. Nested mappers are
|
||||
* wired together via constructor injection, not static singletons - this keeps mappers stateless,
|
||||
* substitutable (e.g. for tests) and avoids global mutable state.
|
||||
* <p>
|
||||
* A {@link DtoMapContext} is threaded through every nested {@code map(...)} call within one
|
||||
* top-level {@link #mapList(List)} invocation, so that repeated references to the same source
|
||||
* entity instance (e.g. several {@code Contact}s sharing the same {@code Customer}) map to the
|
||||
* <b>same</b> target DTO instance rather than creating duplicate-but-equal copies. This mirrors
|
||||
* the identity semantics Ebean's own entity graph already has, and is what makes the resulting
|
||||
* DTO graph "graph shaped" rather than "tree of copies shaped".
|
||||
* <p>
|
||||
* Implementations contain no reflection or {@code MethodHandles} - only direct getter calls and
|
||||
* constructor invocation - so generated mappers are safe under GraalVM native-image with zero
|
||||
* additional reachability metadata.
|
||||
*
|
||||
* @param <SOURCE> the source entity (or embeddable) type
|
||||
* @param <TARGET> the target DTO type
|
||||
*/
|
||||
public interface DtoMapper<SOURCE, TARGET> {
|
||||
|
||||
/**
|
||||
* Return the {@link FetchGroup} of exactly the source properties (and nested paths) needed to
|
||||
* populate the target DTO graph - the select()/fetch() spec is derived from the DTO's declared
|
||||
* shape rather than maintained separately by hand. Used by {@code query.mapTo(TARGET.class)}
|
||||
* to automatically apply the correct fetch spec before the query is executed.
|
||||
*/
|
||||
FetchGroup<SOURCE> fetchGroup();
|
||||
|
||||
/**
|
||||
* Map a single source instance to its target DTO, reusing/registering the mapping in the
|
||||
* given context so that repeated references to the same source instance de-duplicate to the
|
||||
* same target instance. Must return {@code null} when given {@code null}.
|
||||
*/
|
||||
TARGET map(SOURCE source, DtoMapContext context);
|
||||
|
||||
/**
|
||||
* Map a single source instance using a fresh, one-off context. Convenience for mapping a
|
||||
* single object in isolation (no de-duplication opportunity since there's nothing else in
|
||||
* scope to de-duplicate against).
|
||||
*/
|
||||
default TARGET map(SOURCE source) {
|
||||
return map(source, new DtoMapContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a list of source instances to a list of target DTOs sharing the given context,
|
||||
* preserving order.
|
||||
*/
|
||||
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) {
|
||||
List<TARGET> result = new ArrayList<>(source.size());
|
||||
for (SOURCE s : source) {
|
||||
result.add(map(s, context));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a list of source instances to a list of target DTOs using a fresh context shared across
|
||||
* the whole list - this is the usual top-level entry point, e.g. mapping the result of a
|
||||
* {@code query.findList()} call.
|
||||
*/
|
||||
default List<TARGET> mapList(List<SOURCE> source) {
|
||||
return mapList(source, new DtoMapContext());
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.ebean.config.DtoMapperRegister;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Loads all generated {@link DtoMapperRegister} implementations (via {@code ServiceLoader},
|
||||
* mirroring how {@code EntityClassRegister} is discovered) once, and resolves the {@link
|
||||
* DtoMapper} for a given (source, dto) pair, or by the generated mapper's own concrete type, on
|
||||
* request.
|
||||
* <p>
|
||||
* Has no dependency on {@link Database} - it can be constructed independently, before (or
|
||||
* without) a {@code Database} existing at all, e.g. as a DI-managed singleton constructed
|
||||
* alongside the rest of an application's dependency graph. If you want the exact same instance
|
||||
* (and hence the exact same underlying mapper instances) shared between {@code query.mapTo(...)}
|
||||
* and your own application code, construct it yourself and register it via {@code
|
||||
* DatabaseBuilder.putServiceObject(DtoMapperManager.class, myManager)} before building the {@code
|
||||
* Database} - it is then used instead of a Database-internal default instance.
|
||||
* <p>
|
||||
* Resolved mappers are cached so that repeated lookups only ever pay the cost of iterating the
|
||||
* generated registers and constructing the mapper (and its nested mapper/{@code FetchGroup}
|
||||
* graph) once - after that, every lookup is a single hash-map hit regardless of how many entity/
|
||||
* DTO pairs are registered.
|
||||
*/
|
||||
public final class DtoMapperManager {
|
||||
|
||||
private final List<DtoMapperRegister> registers;
|
||||
private final ConcurrentHashMap<MapperKey, DtoMapper<?, ?>> pairCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Class<?>, Object> typeCache = new ConcurrentHashMap<>();
|
||||
|
||||
public DtoMapperManager() {
|
||||
this.registers = load();
|
||||
}
|
||||
|
||||
private static List<DtoMapperRegister> load() {
|
||||
List<DtoMapperRegister> result = new ArrayList<>();
|
||||
for (DtoMapperRegister register : ServiceLoader.load(DtoMapperRegister.class)) {
|
||||
result.add(register);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link DtoMapper} for the given (source, dto) pair.
|
||||
*
|
||||
* @throws PersistenceException if no generated mapper is registered for that pair.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> dtoType) {
|
||||
return (DtoMapper<S, D>) pairCache.computeIfAbsent(new MapperKey(sourceType, dtoType), this::resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the generated mapper instance of the given concrete mapper type - e.g. {@code
|
||||
* manager.get(CustomerDtoMapper.class)} - typically used to resolve a mapper instance for
|
||||
* dependency injection into application code (e.g. an avaje-inject {@code @Factory} bean
|
||||
* method).
|
||||
*
|
||||
* @throws PersistenceException if no generated mapper of that type is registered.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(Class<T> mapperType) {
|
||||
return (T) typeCache.computeIfAbsent(mapperType, this::resolveByType);
|
||||
}
|
||||
|
||||
private DtoMapper<?, ?> resolve(MapperKey key) {
|
||||
for (DtoMapperRegister register : registers) {
|
||||
DtoMapper<?, ?> mapper = register.mapperFor(key.sourceType, key.dtoType);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
throw new PersistenceException("No DtoMapper registered mapping " + key.sourceType + " -> " + key.dtoType
|
||||
+ " - check @DtoMapping(source = " + key.sourceType.getSimpleName() + ".class, target = "
|
||||
+ key.dtoType.getSimpleName() + ".class) is declared on a package-info.java processed by querybean-generator");
|
||||
}
|
||||
|
||||
private Object resolveByType(Class<?> mapperType) {
|
||||
for (DtoMapperRegister register : registers) {
|
||||
Object mapper = register.mapperOfType(mapperType);
|
||||
if (mapper != null) {
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
throw new PersistenceException("No DtoMapper of type " + mapperType.getName() + " registered"
|
||||
+ " - check a @DtoMapping(...) pair generating " + mapperType.getSimpleName()
|
||||
+ " is declared on a package-info.java processed by querybean-generator");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache key pairing the source entity type and target DTO type.
|
||||
*/
|
||||
private static final class MapperKey {
|
||||
|
||||
private final Class<?> sourceType;
|
||||
private final Class<?> dtoType;
|
||||
|
||||
MapperKey(Class<?> sourceType, Class<?> dtoType) {
|
||||
this.sourceType = sourceType;
|
||||
this.dtoType = dtoType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof MapperKey)) {
|
||||
return false;
|
||||
}
|
||||
MapperKey other = (MapperKey) o;
|
||||
return sourceType == other.sourceType && dtoType == other.dtoType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 31 * sourceType.hashCode() + dtoType.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Runtime helpers used by generated {@link DtoMapper} implementations to safely resolve a
|
||||
* primitive-typed DTO field whose value is derived from a multi-hop {@code @DtoPath} that
|
||||
* traverses a nullable intermediate relation.
|
||||
* <p>
|
||||
* A {@code null}-guarded getter-chain (e.g. {@code source.getOrganisation() == null ? null :
|
||||
* source.getOrganisation().getId()}) always types as the boxed wrapper (since one branch is the
|
||||
* {@code null} literal). When the DTO's target field is a primitive (e.g. {@code long
|
||||
* organisationId}), passing that boxed expression to the constructor auto-unboxes it - which
|
||||
* throws a raw, unhelpful {@link NullPointerException} if the relation really is {@code null}.
|
||||
* <p>
|
||||
* These methods give the generated mapper a choice, controlled by {@code @DtoPath#failOnNull()}:
|
||||
* default to the primitive's zero-equivalent value ({@code orZero} methods, the default), or
|
||||
* throw a clear, descriptive exception naming the offending property path ({@code require}
|
||||
* methods, opted into via {@code failOnNull = true}).
|
||||
*
|
||||
* @see io.ebean.annotation.DtoPath
|
||||
*/
|
||||
public final class DtoMapperSupport {
|
||||
|
||||
private DtoMapperSupport() {
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static long orZero(Long value) {
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static int orZero(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static short orZero(Short value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static byte orZero(Byte value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0.0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static double orZero(Double value) {
|
||||
return value == null ? 0.0 : value;
|
||||
}
|
||||
|
||||
/** Return {@code 0.0f} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static float orZero(Float value) {
|
||||
return value == null ? 0.0f : value;
|
||||
}
|
||||
|
||||
/** Return {@code false} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static boolean orZero(Boolean value) {
|
||||
return value != null && value;
|
||||
}
|
||||
|
||||
/** Return {@code '\u0000'} if {@code value} is {@code null}, otherwise its unboxed value. */
|
||||
public static char orZero(Character value) {
|
||||
return value == null ? '\u0000' : value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static long require(Long value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static int require(Integer value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static short require(Short value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static byte require(Byte value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static double require(Double value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static float require(Float value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static boolean require(Boolean value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
||||
public static char require(Character value, String path) {
|
||||
if (value == null) {
|
||||
throw failure(path);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static IllegalStateException failure(String path) {
|
||||
return new IllegalStateException(
|
||||
"@DtoPath(\"" + path + "\") resolved to null via a nullable intermediate relation, but the"
|
||||
+ " target DTO field is primitive and failOnNull=true - either handle the null case in"
|
||||
+ " source data, use a boxed wrapper type for the DTO field, or remove failOnNull to"
|
||||
+ " default to the primitive's zero-equivalent value instead.");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Query for performing native SQL queries that return DTO Bean's.
|
||||
@@ -36,7 +43,12 @@ import java.util.List;
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
public interface DtoQuery<T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query returning a list.
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time.
|
||||
@@ -47,6 +59,56 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
*/
|
||||
QueryIterator<T> findIterate();
|
||||
|
||||
/**
|
||||
* Execute the query returning a Stream.
|
||||
* <p>
|
||||
* Note that the Stream holds resources related to the underlying
|
||||
* resultSet and potentially connection and MUST be closed. We should use
|
||||
* the Stream in a <em>try with resource block</em>.
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time.
|
||||
* <p>
|
||||
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||
* </p>
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query iterating the results and batching them for the consumer.
|
||||
* <p>
|
||||
* This runs like findEach streaming results from the database but just collects the results
|
||||
* into batches to pass to the consumer.
|
||||
*
|
||||
* @param batch The number of dto beans to collect before given them to the consumer
|
||||
* @param consumer The consumer to process the batch of DTO beans
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query iterating a row at a time with the ability to stop consuming part way through.
|
||||
* <p>
|
||||
* Returning false after processing a row stops the iteration through the query results.
|
||||
* </p>
|
||||
* <p>
|
||||
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||
* </p>
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean.
|
||||
*/
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional bean.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Bind all the parameters using index positions.
|
||||
* <p>
|
||||
@@ -153,41 +215,35 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
||||
DtoQuery<T> setBufferFetchSizeHint(int bufferFetchSizeHint);
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
* <p>
|
||||
* This is only supported for a DtoQuery that is derived from an ORM query via
|
||||
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
|
||||
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
|
||||
* there is no query structure available from which to derive a matching row count query -
|
||||
* a PersistenceException is thrown in that case.
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<OrderDto> pagedList =
|
||||
* DB.find(Order.class)
|
||||
* .where().eq("status", Order.Status.NEW)
|
||||
* .orderBy().asc("id")
|
||||
* .setFirstRow(50)
|
||||
* .setMaxRows(20)
|
||||
* .asDto(OrderDto.class)
|
||||
* .findPagedList();
|
||||
*
|
||||
* // fetch the total row count in the background
|
||||
* pagedList.loadCount();
|
||||
*
|
||||
* List<OrderDto> orders = pagedList.getList();
|
||||
* int totalRowCount = pagedList.getTotalCount();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @return The PagedList
|
||||
* Use the explicit transaction to execute the query.
|
||||
*/
|
||||
@Override
|
||||
PagedList<T> findPagedList();
|
||||
DtoQuery<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
DtoQuery<T> usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default DtoQuery<T> usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -10,7 +9,6 @@ import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* List of Expressions that make up a where or having clause.
|
||||
@@ -91,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>
|
||||
@@ -104,29 +97,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
<D> DtoQuery<D> asDto(Class<D> dtoClass);
|
||||
|
||||
/**
|
||||
* Map the query result to a nested DTO graph, automatically deriving the select()/fetch() spec
|
||||
* from the target DTO's declared shape and forcing {@code setUnmodifiable(true)}.
|
||||
* <p>
|
||||
* Distinct from {@link #asDto(Class)} (the flat, single-row SQL pipeline) - this supports
|
||||
* nested ToOne/ToMany DTO graphs, mapped from the normal ORM entity query result.
|
||||
*
|
||||
* @throws jakarta.persistence.PersistenceException if no generated {@link DtoMapper} is
|
||||
* registered for this (entity, dto) pair.
|
||||
*/
|
||||
<D> MappedQuery<D> mapTo(Class<D> dtoType);
|
||||
|
||||
/**
|
||||
* Map the query result to a nested DTO graph using an already-resolved {@link DtoMapper}
|
||||
* instance, rather than looking one up by (entity, dtoType) - e.g. to select a named variant
|
||||
* mapper (see {@code @DtoMapping(name = "...", exclude = "...")}), such as
|
||||
* {@code query.mapTo(User.class, userMapper.noFleets())}.
|
||||
*
|
||||
* @param dtoType the DTO type mapped to (must match {@code mapper}'s target type)
|
||||
* @param mapper the mapper instance to use, e.g. a named variant accessor on a generated mapper
|
||||
*/
|
||||
<D> MappedQuery<D> mapTo(Class<D> dtoType, DtoMapper<T, D> mapper);
|
||||
|
||||
/**
|
||||
* Return the underlying query as an UpdateQuery.
|
||||
* <p>
|
||||
@@ -229,22 +199,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
* </p>
|
||||
*
|
||||
* @return the number of rows that were permanently deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
|
||||
/**
|
||||
* Execute as a update query.
|
||||
*
|
||||
@@ -422,26 +376,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow()
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return query().findOneOrThrow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing the exception produced by the
|
||||
* given supplier if there is no matching bean.
|
||||
*
|
||||
* @see Query#findOneOrThrow(Supplier)
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return query().findOneOrThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute find row count query in a background thread.
|
||||
* <p>
|
||||
@@ -531,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.
|
||||
@@ -604,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.
|
||||
*
|
||||
@@ -700,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>
|
||||
@@ -716,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).
|
||||
*/
|
||||
@@ -745,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
|
||||
@@ -1695,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.
|
||||
*/
|
||||
@@ -1878,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>
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Common find operations shared by the query types that can execute and return
|
||||
* results - {@link SqlQuery}, {@link DtoQuery}, {@link MappedQuery} and {@link QueryBuilder}.
|
||||
*
|
||||
* @param <SELF> The query type (used for method chaining)
|
||||
* @param <T> The type of the result
|
||||
*/
|
||||
@NullMarked
|
||||
public interface FindableQuery<SELF extends FindableQuery<SELF, T>, T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query returning the list of results.
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result, or {@code null} if there is no matching row.
|
||||
* <p>
|
||||
* If more than 1 row is found for this query then a PersistenceException is thrown.
|
||||
*/
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional result.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result or throwing a
|
||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query returning a single result or throwing the exception produced
|
||||
* by the given supplier if there is no matching row.
|
||||
*/
|
||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
SELF usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
SELF usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SELF usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SELF usingMaster(boolean useMaster);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Query that maps an entity graph query result to a nested DTO graph, produced by
|
||||
* {@code query.mapTo(SomeDto.class)}.
|
||||
* <p>
|
||||
* Distinct from the existing flat, single-row {@link DtoQuery} pipeline (see {@link
|
||||
* QueryBuilder#asDto(Class)}) - this executes the underlying entity ORM query (with the
|
||||
* select()/fetch() spec automatically derived from the target DTO's declared shape, see
|
||||
* {@link DtoMapper#fetchGroup()}), forces {@code setUnmodifiable(true)}, and then maps the
|
||||
* resulting (unmodifiable) entity graph into a DTO graph via the generated {@link DtoMapper},
|
||||
* supporting nested ToOne/ToMany and identity-aware de-duplication.
|
||||
*
|
||||
* @param <D> the target DTO type
|
||||
*/
|
||||
@NullMarked
|
||||
public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
|
||||
|
||||
/**
|
||||
* Execute the query returning the mapped DTO list.
|
||||
*/
|
||||
@Override
|
||||
List<D> findList();
|
||||
|
||||
/**
|
||||
* Execute the query returning a paged list of mapped DTOs.
|
||||
* <p>
|
||||
* Mirrors {@code Query#findPagedList()} - the underlying entity graph query is paged (via
|
||||
* {@code setFirstRow(int)}/{@code setMaxRows(int)}) and executed as normal, then each page's
|
||||
* result is mapped to the target DTO graph. Row-count/page-index metadata
|
||||
* ({@link PagedList#getTotalCount()}, {@link PagedList#hasNext()}, etc.) reflects the
|
||||
* underlying entity query and is unaffected by the DTO mapping.
|
||||
*/
|
||||
@Override
|
||||
PagedList<D> findPagedList();
|
||||
|
||||
/**
|
||||
* Execute the query returning the result as a Stream of mapped DTOs.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findStream()} - the underlying entity graph query is streamed
|
||||
* (supporting very large queries iterating any number of results, potentially using multiple
|
||||
* persistence contexts internally) and each entity is mapped to its target DTO lazily as the
|
||||
* stream is consumed, sharing one {@link DtoMapContext} across the whole stream so that
|
||||
* repeated references to the same source entity still de-duplicate to the same DTO instance.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // use try with resources to ensure Stream is closed
|
||||
*
|
||||
* try (Stream<CustomerDto> stream = query.mapTo(CustomerDto.class).findStream()) {
|
||||
* stream
|
||||
* .map(...)
|
||||
* .collect(...);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@Override
|
||||
Stream<D> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query processing the mapped DTOs one at a time.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEach(Consumer)} - the underlying entity graph query is
|
||||
* streamed one entity at a time and each entity is mapped to its target DTO lazily as it is
|
||||
* consumed, sharing one {@link DtoMapContext} across the whole callback so that repeated
|
||||
* references to the same source entity still de-duplicate to the same DTO instance.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the mapped DTOs are
|
||||
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
|
||||
*
|
||||
* @param consumer the consumer used to process the mapped DTOs.
|
||||
*/
|
||||
@Override
|
||||
void findEach(Consumer<D> consumer);
|
||||
/**
|
||||
* Execute findEach streaming query batching the mapped DTOs for consuming.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEach(int, Consumer)} - typically used when we want to do
|
||||
* further processing on the mapped DTOs in batch form, for example 100 at a time. Each batch
|
||||
* shares one {@link DtoMapContext} with the rest of the query so that repeated references to
|
||||
* the same source entity still de-duplicate to the same DTO instance.
|
||||
*
|
||||
* @param batch The number of mapped DTOs processed in the batch
|
||||
* @param consumer Process the batch of mapped DTOs
|
||||
*/
|
||||
@Override
|
||||
void findEach(int batch, Consumer<List<D>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to process the resulting mapped DTOs one at a time,
|
||||
* with the ability to stop processing part way through.
|
||||
* <p>
|
||||
* Mirrors {@link QueryBuilder#findEachWhile(Predicate)} - returning {@code false} after
|
||||
* processing a DTO stops the iteration through the query results. Sharing one
|
||||
* {@link DtoMapContext} across the whole callback so that repeated references to the same
|
||||
* source entity still de-duplicate to the same DTO instance.
|
||||
*
|
||||
* @param consumer the consumer used to process the mapped DTOs, returning {@code false} to
|
||||
* stop processing.
|
||||
*/
|
||||
@Override
|
||||
void findEachWhile(Predicate<D> consumer);
|
||||
|
||||
}
|
||||
@@ -151,7 +151,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* @param <T> the type of Entity bean this query will fetch.
|
||||
*/
|
||||
@NullMarked
|
||||
public interface Query<T> extends QueryBuilder<Query<T>, T> {
|
||||
public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
|
||||
/**
|
||||
* The lock type (strength) to use with query FOR UPDATE row locking.
|
||||
@@ -346,23 +346,6 @@ public interface Query<T> extends 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 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.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,8 @@ package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -10,6 +11,8 @@ import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Build and execute an ORM query.
|
||||
@@ -17,7 +20,7 @@ import java.util.function.Consumer;
|
||||
* @param <SELF> The type of the builder
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T>, StreamableQuery<SELF, T> {
|
||||
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T> {
|
||||
|
||||
/**
|
||||
* Set root table alias.
|
||||
@@ -61,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>
|
||||
@@ -74,32 +72,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
<D> DtoQuery<D> asDto(Class<D> dtoClass);
|
||||
|
||||
/**
|
||||
* Map the query result to a nested DTO graph, automatically deriving the select()/fetch() spec
|
||||
* from the target DTO's declared shape and forcing {@code setUnmodifiable(true)}.
|
||||
* <p>
|
||||
* Distinct from {@link #asDto(Class)} (the flat, single-row SQL pipeline) - this supports
|
||||
* nested ToOne/ToMany DTO graphs, mapped from the normal ORM entity query result.
|
||||
*
|
||||
* @throws jakarta.persistence.PersistenceException if no generated {@link DtoMapper} is
|
||||
* registered for this (entity, dto) pair.
|
||||
*/
|
||||
<D> MappedQuery<D> mapTo(Class<D> dtoType);
|
||||
|
||||
/**
|
||||
* Map the query result to a nested DTO graph using an already-resolved {@link DtoMapper}
|
||||
* instance, rather than looking one up by (entity, dtoType). Bypasses {@link DtoMapperManager}
|
||||
* entirely, so it's the way to select a named variant mapper (see {@code @DtoMapping(name =
|
||||
* "...", exclude = "...")}) - e.g. {@code query.mapTo(User.class, userMapper.noFleets())}.
|
||||
* <p>
|
||||
* Also forces {@code setUnmodifiable(true)} and derives the select()/fetch() spec from
|
||||
* {@code mapper.fetchGroup()}, same as {@link #mapTo(Class)}.
|
||||
*
|
||||
* @param dtoType the DTO type mapped to (must match {@code mapper}'s target type)
|
||||
* @param mapper the mapper instance to use, e.g. a named variant accessor on a generated mapper
|
||||
*/
|
||||
<D> MappedQuery<D> mapTo(Class<D> dtoType, DtoMapper<T, D> mapper);
|
||||
|
||||
/**
|
||||
* Convert the query to a UpdateQuery.
|
||||
* <p>
|
||||
@@ -143,16 +115,48 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF copy();
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
SELF usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Execute the query using the given database.
|
||||
*/
|
||||
SELF usingDatabase(Database database);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.config.DatabaseConfig#setReadOnlyDataSource(DataSource)} then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SELF usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SELF usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Set the base table to use for this query.
|
||||
* <p>
|
||||
@@ -244,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
|
||||
@@ -291,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>
|
||||
@@ -312,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.
|
||||
*
|
||||
@@ -396,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>
|
||||
@@ -598,21 +535,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
*
|
||||
* @return the number of beans/rows that were permanently deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
|
||||
/**
|
||||
* Execute the query returning true if a row is found.
|
||||
* <p>
|
||||
@@ -644,24 +566,88 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
boolean exists();
|
||||
|
||||
/**
|
||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
||||
* if there is no matching bean.
|
||||
* Execute the query returning either a single bean or null (if no matching
|
||||
* bean is found).
|
||||
* <p>
|
||||
* If more than 1 row is found for this query then a PersistenceException is
|
||||
* thrown.
|
||||
* <p>
|
||||
* This is useful when your predicates dictate that your query should only
|
||||
* return 0 or 1 results.
|
||||
* <p>
|
||||
* This is a convenience alternative to:
|
||||
* <pre>{@code
|
||||
* query.findOneOrEmpty()
|
||||
* .orElseThrow(() -> new EntityNotFoundException(...));
|
||||
*
|
||||
* // assuming the sku of products is unique...
|
||||
* Product product =
|
||||
* new QProduct()
|
||||
* .sku.equalTo("aa113")
|
||||
* .findOne();
|
||||
* ...
|
||||
* }</pre>
|
||||
* <p>
|
||||
* The exception message is a best effort - it uses the id when this is effectively a
|
||||
* find-by-id query, or the single equality predicate when the query is filtered by what
|
||||
* looks like a natural/unique key, otherwise a generic "not found" message.
|
||||
* It is also useful with finding objects by their id when you want to specify
|
||||
* further join information to optimise the query.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // Fetch order 42 and additionally fetch join its order details...
|
||||
* Order order =
|
||||
* new QOrder()
|
||||
* .fetch("details") // eagerly load the order details
|
||||
* .id.equalTo(42)
|
||||
* .findOne();
|
||||
*
|
||||
* // the order details were eagerly loaded
|
||||
* List<OrderDetail> details = order.getDetails();
|
||||
* ...
|
||||
* }</pre>
|
||||
*/
|
||||
@Override
|
||||
default T findOneOrThrow() {
|
||||
return findOneOrEmpty().orElseThrow(() ->
|
||||
new EntityNotFoundException(getBeanType().getSimpleName() + " not found"));
|
||||
}
|
||||
@Nullable
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional bean.
|
||||
*/
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Execute the query returning the list of objects.
|
||||
* <p>
|
||||
* This query will execute against the EbeanServer that was used to create it.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* List<Customer> customers =
|
||||
* new QCustomer()
|
||||
* .name.ilike("rob%")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @see Query#findList()
|
||||
*/
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
* Execute the query returning the result as a Stream.
|
||||
* <p>
|
||||
* Note that this can support very large queries iterating
|
||||
* any number of results. To do so internally it can use
|
||||
* multiple persistence contexts.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // use try with resources to ensure Stream is closed
|
||||
*
|
||||
* try (Stream<Customer> stream = query.findStream()) {
|
||||
* stream
|
||||
* .map(...)
|
||||
* .collect(...);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Execute the query returning the set of objects.
|
||||
@@ -806,6 +792,84 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
<A> Set<A> findSingleAttributeSet();
|
||||
|
||||
/**
|
||||
* Execute the query processing the beans one at a time.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the
|
||||
* beans are consumed one at a time and do not need to be held in memory
|
||||
* (unlike #findList #findSet etc)
|
||||
* <p>
|
||||
* Note that internally Ebean can inform the JDBC driver that it is expecting larger
|
||||
* resultSet and specifically for MySQL this hint is required to stop it's JDBC driver
|
||||
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
|
||||
* generally preferable.
|
||||
* <p>
|
||||
* Compared with #findEachWhile this will always process all the beans where as
|
||||
* #findEachWhile provides a way to stop processing the query result early before
|
||||
* all the beans have been read.
|
||||
* <p>
|
||||
* This method is functionally equivalent to findIterate() but instead of using an
|
||||
* iterator uses the Consumer interface which is better suited to use with closures.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* new QCustomer()
|
||||
* .status.equalTo(Status.NEW)
|
||||
* .orderBy().id.asc()
|
||||
* .findEach((Customer customer) -> {
|
||||
*
|
||||
* // do something with customer
|
||||
* System.out.println("-- visit " + customer);
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param consumer the consumer used to process the queried beans.
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute findEach streaming query batching the results for consuming.
|
||||
* <p>
|
||||
* This query execution will stream the results and is suited to consuming
|
||||
* large numbers of results from the database.
|
||||
* <p>
|
||||
* Typically, we use this batch consumer when we want to do further processing on
|
||||
* the beans and want to do that processing in batch form, for example - 100 at
|
||||
* a time.
|
||||
*
|
||||
* @param batch The number of beans processed in the batch
|
||||
* @param consumer Process the batch of beans
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to a visitor to process the resulting
|
||||
* beans one at a time.
|
||||
* <p>
|
||||
* This method is functionally equivalent to findIterate() but instead of using an
|
||||
* iterator uses the Predicate interface which is better suited to use with closures.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* new QCustomer()
|
||||
* .status.equalTo(Status.NEW)
|
||||
* .orderBy().id.asc()
|
||||
* .findEachWhile((Customer customer) -> {
|
||||
*
|
||||
* // do something with customer
|
||||
* System.out.println("-- visit " + customer);
|
||||
*
|
||||
* // return true to continue processing or false to stop
|
||||
* return (customer.getId() < 40);
|
||||
* });
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @param consumer the consumer used to process the queried beans.
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
/**
|
||||
* Return versions of a @History entity bean.
|
||||
* <p>
|
||||
@@ -871,4 +935,33 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
<K> FutureMap<K,T> findFutureMap();
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
* <p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* PagedList<Order> pagedList =
|
||||
* new QOrder()
|
||||
* .setFirstRow(50)
|
||||
* .setMaxRows(20)
|
||||
* .findPagedList();
|
||||
*
|
||||
* // fetch the total row count in the background
|
||||
* pagedList.loadRowCount();
|
||||
*
|
||||
* List<Order> orders = pagedList.getList();
|
||||
* int totalRowCount = pagedList.getTotalRowCount();
|
||||
*
|
||||
* }</pre>
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -41,49 +41,6 @@ public interface RawSqlBuilder {
|
||||
return XBootstrapService.rawSql().unparsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RawSqlBuilder for SQL containing {@code ${where}}, {@code ${having}} and/or
|
||||
* {@code ${orderBy}} placeholder(s). Unlike {@link #parse(String)} this does NOT attempt to parse
|
||||
* the SELECT columns, so it supports complex SQL such as CTEs, subqueries, and window functions.
|
||||
* <p>
|
||||
* Explicit column mappings must be provided (as with {@link #unparsed(String)}), but
|
||||
* WHERE, HAVING and ORDER BY expressions can be added dynamically via the query API - provided
|
||||
* the corresponding placeholder is present in the SQL. If a query calls {@code .orderBy(...)}
|
||||
* on a template with no {@code ${orderBy}}/{@code ${andOrderBy}} placeholder, that ordering is
|
||||
* ignored (there is no injection point for it) rather than producing invalid SQL.
|
||||
* </p>
|
||||
* <p>
|
||||
* Available placeholders:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code ${where}} / {@code ${andWhere}} - inject "where <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${having}} / {@code ${andHaving}} - inject "having <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${orderBy}} / {@code ${andOrderBy}} - inject "order by <expr>" / ", <expr>"</li>
|
||||
* </ul>
|
||||
* <h3>Example:</h3>
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = """
|
||||
* with agg as (
|
||||
* select company_id, sum(amount) as total
|
||||
* from orders
|
||||
* ${where}
|
||||
* group by company_id
|
||||
* )
|
||||
* select company_id, total from agg ${orderBy}
|
||||
* """;
|
||||
*
|
||||
* RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
* .columnMapping("company_id", "companyId")
|
||||
* .columnMapping("total", "total")
|
||||
* .create();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
static RawSqlBuilder withPlaceholders(String sql) {
|
||||
return XBootstrapService.rawSql().withPlaceholders(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RawSqlBuilder parsing the sql.
|
||||
* <p>
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.ebean;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.Serializable;
|
||||
import java.sql.Connection;
|
||||
import java.util.Collection;
|
||||
@@ -40,7 +41,44 @@ import java.util.function.Predicate;
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow> {
|
||||
public interface SqlQuery extends Serializable, CancelableQuery {
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
SqlQuery usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
SqlQuery usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Ensure that the master DataSource is used if there is a read only data source
|
||||
* being used (that is using a read replica database potentially with replication lag).
|
||||
* <p>
|
||||
* When the database is configured with a read-only DataSource via
|
||||
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)}then
|
||||
* by default when a query is run without an active transaction, it uses the read-only data
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
default SqlQuery usingMaster() {
|
||||
return usingMaster(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
* data source can be used if defined.
|
||||
*
|
||||
* @see #usingMaster()
|
||||
*/
|
||||
SqlQuery usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query returning a list.
|
||||
*/
|
||||
List<SqlRow> findList();
|
||||
|
||||
/**
|
||||
* Execute the SqlQuery iterating a row at a time.
|
||||
@@ -61,6 +99,16 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*/
|
||||
void findEachWhile(Predicate<SqlRow> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning a single row or null.
|
||||
* <p>
|
||||
* If this query finds 2 or more rows then it will throw a
|
||||
* PersistenceException.
|
||||
* </p>
|
||||
*/
|
||||
@Nullable
|
||||
SqlRow findOne();
|
||||
|
||||
/**
|
||||
* Execute the query reading each row from ResultSet using the RowConsumer.
|
||||
* <p>
|
||||
@@ -91,6 +139,11 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*/
|
||||
void findEachRow(RowConsumer consumer);
|
||||
|
||||
/**
|
||||
* Execute the query returning an optional row.
|
||||
*/
|
||||
Optional<SqlRow> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Set one of more positioned parameters.
|
||||
* <p>
|
||||
@@ -312,7 +365,7 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*
|
||||
* @param <T> The type of the scalar values
|
||||
*/
|
||||
interface TypeQuery<T> extends FindableQuery<TypeQuery<T>, T> {
|
||||
interface TypeQuery<T> {
|
||||
|
||||
/**
|
||||
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
|
||||
@@ -320,38 +373,27 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
|
||||
*
|
||||
* @see SqlQuery#usingMaster(boolean)
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* Execute the query using the given transaction.
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
@Override
|
||||
TypeQuery<T> usingConnection(Connection connection);
|
||||
|
||||
/**
|
||||
* Return the single value.
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
T findOne();
|
||||
|
||||
/**
|
||||
* Return the single value that is optional.
|
||||
*/
|
||||
@Override
|
||||
Optional<T> findOneOrEmpty();
|
||||
|
||||
/**
|
||||
* Return the list of values.
|
||||
*/
|
||||
@Override
|
||||
List<T> findList();
|
||||
|
||||
/**
|
||||
|
||||
@@ -158,15 +158,6 @@ public interface SqlUpdate {
|
||||
*/
|
||||
int executeNow();
|
||||
|
||||
/**
|
||||
* Set an explicit transaction to use to execute this statement.
|
||||
* <p>
|
||||
* When not set, {@link #execute()} and {@link #executeNow()} use whatever transaction
|
||||
* is currently active on the thread (or auto-commit if none is active) - consistent
|
||||
* with {@link Database#execute(SqlUpdate, Transaction)}.
|
||||
*/
|
||||
SqlUpdate usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute when addBatch() has been used to batch multiple bind executions.
|
||||
*
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A {@link FindableQuery} that additionally supports streaming results and paging.
|
||||
*
|
||||
* @param <SELF> The query type (used for method chaining)
|
||||
* @param <T> The type of the result
|
||||
*/
|
||||
@NullMarked
|
||||
public interface StreamableQuery<SELF extends StreamableQuery<SELF, T>, T> extends FindableQuery<SELF, T> {
|
||||
|
||||
/**
|
||||
* Execute the query returning the result as a Stream.
|
||||
* <p>
|
||||
* Note that this can support very large queries iterating any number of results.
|
||||
* To do so internally it can use multiple persistence contexts.
|
||||
* <p>
|
||||
* Note that the Stream holds resources related to the underlying resultSet and
|
||||
* potentially connection and MUST be closed. We should use the Stream in a
|
||||
* <em>try with resource block</em>.
|
||||
* <pre>{@code
|
||||
*
|
||||
* // use try with resources to ensure Stream is closed
|
||||
*
|
||||
* try (Stream<T> stream = query.findStream()) {
|
||||
* stream
|
||||
* .map(...)
|
||||
* .collect(...);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
Stream<T> findStream();
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
* The benefit of using this over findList() is that it provides functionality to get the
|
||||
* total row count etc.
|
||||
* <p>
|
||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
||||
* PersistenceException is thrown.
|
||||
*
|
||||
* @return The PagedList
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
/**
|
||||
* Execute the query processing the results one at a time.
|
||||
* <p>
|
||||
* This method is appropriate to process very large query results as the results are
|
||||
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
|
||||
* <p>
|
||||
* Note that internally Ebean can inform the JDBC driver that it is expecting a larger
|
||||
* resultSet and specifically for MySQL this hint is required to stop its JDBC driver
|
||||
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
|
||||
* generally preferable.
|
||||
* <p>
|
||||
* Compared with {@link #findEachWhile(Predicate)} this will always process all the results
|
||||
* whereas findEachWhile() provides a way to stop processing the query result early before
|
||||
* all the results have been read.
|
||||
*
|
||||
* @param consumer the consumer used to process the queried results.
|
||||
*/
|
||||
void findEach(Consumer<T> consumer);
|
||||
|
||||
/**
|
||||
* Execute findEach streaming query batching the results for consuming.
|
||||
* <p>
|
||||
* This query execution will stream the results and is suited to consuming
|
||||
* large numbers of results from the database.
|
||||
* <p>
|
||||
* Typically, we use this batch consumer when we want to do further processing on
|
||||
* the results and want to do that processing in batch form, for example - 100 at
|
||||
* a time.
|
||||
*
|
||||
* @param batch The number of results processed in the batch
|
||||
* @param consumer Process the batch of results
|
||||
*/
|
||||
void findEach(int batch, Consumer<List<T>> consumer);
|
||||
|
||||
/**
|
||||
* Execute the query using callbacks to process the resulting results one at a time,
|
||||
* with the ability to stop processing part way through.
|
||||
* <p>
|
||||
* Returning {@code false} after processing a result stops the iteration through the
|
||||
* query results.
|
||||
*
|
||||
* @param consumer the consumer used to process the queried results, returning
|
||||
* {@code false} to stop processing.
|
||||
*/
|
||||
void findEachWhile(Predicate<T> consumer);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -260,27 +235,6 @@ public interface Transaction extends AutoCloseable {
|
||||
*/
|
||||
void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties);
|
||||
|
||||
/**
|
||||
* Set to false to disable auto-generation of {@code @WhenCreated}, {@code @WhenModified},
|
||||
* {@code @WhoCreated} and {@code @WhoModified} values for this transaction.
|
||||
* <p>
|
||||
* When disabled, Ebean will only set a generated property value if the property currently
|
||||
* has a null value (for inserts) or is a {@code @Version} property. Any value already set
|
||||
* on the bean is preserved.
|
||||
* <p>
|
||||
* This is useful in backup and restore scenarios where you need to retain the original
|
||||
* audit timestamps and user values rather than have them overwritten.
|
||||
* <pre>{@code
|
||||
* try (Transaction txn = DB.beginTransaction()) {
|
||||
* txn.setGeneratedPropertiesEnabled(false);
|
||||
* bean.setWhenCreated(originalTimestamp);
|
||||
* DB.save(bean);
|
||||
* txn.commit();
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
void setGeneratedPropertiesEnabled(boolean enable);
|
||||
|
||||
/**
|
||||
* Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries.
|
||||
* <p>
|
||||
|
||||
@@ -84,15 +84,6 @@ public interface Update<T> {
|
||||
*/
|
||||
int execute();
|
||||
|
||||
/**
|
||||
* Set an explicit transaction to use to execute this statement.
|
||||
* <p>
|
||||
* When not set, {@link #execute()} uses whatever transaction is currently active on
|
||||
* the thread (or auto-commit if none is active) - consistent with
|
||||
* {@link Database#execute(Update, Transaction)}.
|
||||
*/
|
||||
Update<T> usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Set an ordered bind parameter.
|
||||
* <p>
|
||||
|
||||
@@ -175,7 +175,7 @@ public final class InterceptReadOnly extends InterceptBase {
|
||||
|
||||
@Override
|
||||
public boolean isUpdate() {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -151,16 +151,14 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
|
||||
}
|
||||
}
|
||||
|
||||
public BeanCollectionAdd collectionAdd() {
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void refresh(ModifyListenMode modifyListenMode, BeanList<E> newList) {
|
||||
setModifyListening(modifyListenMode);
|
||||
this.list = newList.actualList();
|
||||
/**
|
||||
* Set the actual underlying list.
|
||||
* <p>
|
||||
* This is primarily for the deferred fetching function.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setActualList(List<?> list) {
|
||||
this.list = (List<E>) list;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.ebean.common;
|
||||
|
||||
import io.ebean.bean.*;
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.BeanCollectionLoader;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.ToStringBuilder;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -14,12 +17,12 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
|
||||
/**
|
||||
* The underlying map implementation.
|
||||
*/
|
||||
private LinkedHashMap<K, E> map;
|
||||
private Map<K, E> map;
|
||||
|
||||
/**
|
||||
* Create with a given Map.
|
||||
*/
|
||||
public BeanMap(LinkedHashMap<K, E> map) {
|
||||
public BeanMap(Map<K, E> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@@ -162,23 +165,18 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
|
||||
}
|
||||
}
|
||||
|
||||
public LinkedHashMap<K, E> collectionAdd() {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the actual underlying map. Used for performing lazy fetch.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void refresh(ModifyListenMode modifyListenMode, BeanMap<?, ?> newMap) {
|
||||
setModifyListening(modifyListenMode);
|
||||
this.map = (LinkedHashMap<K, E>) newMap.actualMap();
|
||||
public void setActualMap(Map<?, ?> map) {
|
||||
this.map = (Map<K, E>) map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying map.
|
||||
*/
|
||||
public LinkedHashMap<K, E> actualMap() {
|
||||
public Map<K, E> actualMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
|
||||
/**
|
||||
* The underlying Set implementation.
|
||||
*/
|
||||
private LinkedHashSet<E> set;
|
||||
private Set<E> set;
|
||||
|
||||
/**
|
||||
* Create with a specific Set implementation.
|
||||
*/
|
||||
public BeanSet(LinkedHashSet<E> set) {
|
||||
public BeanSet(Set<E> set) {
|
||||
this.set = set;
|
||||
}
|
||||
|
||||
@@ -146,22 +146,18 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
|
||||
}
|
||||
}
|
||||
|
||||
public BeanCollectionAdd collectionAdd() {
|
||||
if (set == null) {
|
||||
set = new LinkedHashSet<>();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void refresh(ModifyListenMode modifyListenMode, BeanSet<E> newSet) {
|
||||
setModifyListening(modifyListenMode);
|
||||
this.set = newSet.actualSet();
|
||||
/**
|
||||
* Set the underlying set (used for lazy fetch).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setActualSet(Set<?> set) {
|
||||
this.set = (Set<E>) set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying set.
|
||||
*/
|
||||
public LinkedHashSet<E> actualSet() {
|
||||
public Set<E> actualSet() {
|
||||
return set;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -120,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).
|
||||
@@ -407,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;
|
||||
@@ -432,8 +418,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
private int backgroundExecutorShutdownSecs = 30;
|
||||
private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper();
|
||||
|
||||
private boolean tenantPartitionedCache;
|
||||
|
||||
// defaults for the L2 bean caching
|
||||
|
||||
private int cacheMaxSize = 10000;
|
||||
@@ -985,28 +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 String getDbSchema() {
|
||||
return dbSchema;
|
||||
@@ -1177,17 +1139,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
return cacheMaxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTenantPartitionedCache() {
|
||||
return tenantPartitionedCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig tenantPartitionedCache(boolean tenantPartitionedCache) {
|
||||
this.tenantPartitionedCache = tenantPartitionedCache;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setCacheMaxSize(int cacheMaxSize) {
|
||||
this.cacheMaxSize = cacheMaxSize;
|
||||
@@ -1292,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();
|
||||
@@ -2097,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.
|
||||
*/
|
||||
@@ -2134,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);
|
||||
@@ -2157,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);
|
||||
@@ -2241,15 +2157,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders);
|
||||
ddlHeader = p.get("ddl.header", ddlHeader);
|
||||
|
||||
tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache);
|
||||
|
||||
cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize);
|
||||
cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime);
|
||||
cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive);
|
||||
queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize);
|
||||
queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime);
|
||||
queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive);
|
||||
|
||||
// read tenant-configuration from config:
|
||||
// tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION
|
||||
String mode = p.get("tenant.mode");
|
||||
|
||||
@@ -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,31 +0,0 @@
|
||||
package io.ebean.config;
|
||||
|
||||
import io.ebean.DtoMapper;
|
||||
|
||||
/**
|
||||
* Loads and returns the {@link DtoMapper} to use for a given DTO type, generated per-module by
|
||||
* the querybean-generator annotation processor for each {@code io.ebean.annotation.DtoMapping}
|
||||
* registered pair.
|
||||
* <p>
|
||||
* Implementations resolve purely via literal {@code Class} comparisons (no reflection,
|
||||
* {@code Class.forName}, or {@code MethodHandles}) - safe under GraalVM native-image with zero
|
||||
* additional reachability metadata - mirroring {@link EntityClassRegister}.
|
||||
*/
|
||||
public interface DtoMapperRegister {
|
||||
|
||||
/**
|
||||
* Return the mapper for the given DTO type, or {@code null} if this register has no mapper for
|
||||
* that type.
|
||||
*/
|
||||
<SOURCE,TARGET> DtoMapper<SOURCE, TARGET> mapperFor(Class<SOURCE> sourceType, Class<TARGET> targetType);
|
||||
|
||||
/**
|
||||
* Return the mapper instance of the given concrete generated mapper type, or {@code null} if
|
||||
* this register has no mapper of that type.
|
||||
* <p>
|
||||
* An alternative to {@link #mapperFor(Class, Class)} for looking up a mapper by its own class
|
||||
* (e.g. {@code CustomerDtoMapper.class}) rather than by its (source, target) pair - typically
|
||||
* used to resolve a mapper instance for dependency injection into application code.
|
||||
*/
|
||||
<T> T mapperOfType(Class<T> mapperType);
|
||||
}
|
||||
@@ -162,18 +162,6 @@ public class DatabasePlatform {
|
||||
protected boolean selectCountWithAlias;
|
||||
protected boolean selectCountWithColumnAlias;
|
||||
|
||||
/**
|
||||
* Set true for platforms where {@code exists(...)} can only be used as a predicate
|
||||
* and not as a directly selectable scalar boolean expression (e.g. SQL Server, Oracle).
|
||||
*/
|
||||
protected boolean existsWithCaseWhen;
|
||||
|
||||
/**
|
||||
* Clause appended after the {@code case when exists(...) then 1 else 0 end} exists query
|
||||
* for platforms that require a FROM clause on every select (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
protected String existsFromClause = "";
|
||||
|
||||
/**
|
||||
* If set then use the FORWARD ONLY hint when creating ResultSets for
|
||||
* findIterate() and findVisit().
|
||||
@@ -672,21 +660,6 @@ public class DatabasePlatform {
|
||||
return selectCountWithColumnAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if a scalar boolean {@code exists(...)} expression is not supported
|
||||
* as a select expression and needs to be wrapped as {@code case when exists(...) then 1 else 0 end}.
|
||||
*/
|
||||
public boolean existsWithCaseWhen() {
|
||||
return existsWithCaseWhen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the clause to append after the exists case-when wrapping (e.g. {@code from dual} on Oracle).
|
||||
*/
|
||||
public String existsFromClause() {
|
||||
return existsFromClause;
|
||||
}
|
||||
|
||||
|
||||
public String completeSql(String sql, Query<?> query) {
|
||||
if (query.isForUpdate()) {
|
||||
|
||||
@@ -11,12 +11,10 @@ import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -25,36 +23,18 @@ import static java.lang.System.Logger.Level.ERROR;
|
||||
|
||||
/**
|
||||
* Database sequence based IdGenerator.
|
||||
* <p>
|
||||
* Maintains a separate buffer of pre-fetched id values per tenant when the supplied
|
||||
* DataSource implements {@link TenantConnectionSource}. For the common single-tenant
|
||||
* case a single buffer is used (keyed by {@link #SINGLE}).
|
||||
*/
|
||||
public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
|
||||
protected static final System.Logger log = AppLog.getLogger("io.ebean.SEQ");
|
||||
|
||||
/**
|
||||
* Buffer key used when there is no current tenant (single-tenant or no tenant in scope).
|
||||
*/
|
||||
private static final Object SINGLE = new Object();
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
protected final String seqName;
|
||||
protected final DataSource dataSource;
|
||||
protected final BackgroundExecutor backgroundExecutor;
|
||||
protected final NavigableSet<Long> idList = new TreeSet<>();
|
||||
protected final int allocationSize;
|
||||
private final TenantConnectionSource tenantSource;
|
||||
private final TenantBuffer single = new TenantBuffer();
|
||||
private final ConcurrentMap<Object, TenantBuffer> buffers = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Per-tenant pre-fetched id buffer with its own lock and background-loading flag.
|
||||
*/
|
||||
private static final class TenantBuffer {
|
||||
final ReentrantLock lock = new ReentrantLock();
|
||||
final Deque<Long> idList = new ArrayDeque<>();
|
||||
final AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
|
||||
}
|
||||
protected AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Construct given a dataSource and sql to return the next sequence value.
|
||||
@@ -64,7 +44,6 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
this.dataSource = ds;
|
||||
this.seqName = seqName;
|
||||
this.allocationSize = allocationSize;
|
||||
this.tenantSource = (ds instanceof TenantConnectionSource) ? (TenantConnectionSource) ds : null;
|
||||
}
|
||||
|
||||
public abstract String getSql(int batchSize);
|
||||
@@ -85,24 +64,6 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Object currentTenantKey() {
|
||||
if (tenantSource != null) {
|
||||
Object tenantId = tenantSource.currentTenantId();
|
||||
if (tenantId != null) {
|
||||
return tenantId;
|
||||
}
|
||||
}
|
||||
return SINGLE;
|
||||
}
|
||||
|
||||
private TenantBuffer buffer(Object tenantKey) {
|
||||
if (tenantKey == SINGLE) {
|
||||
// common single-tenant path - avoid the concurrent map lookup
|
||||
return single;
|
||||
}
|
||||
return buffers.computeIfAbsent(tenantKey, k -> new TenantBuffer());
|
||||
}
|
||||
|
||||
/**
|
||||
* If allocateSize is large load some sequences in a background thread.
|
||||
* <p>
|
||||
@@ -117,22 +78,23 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
|
||||
/**
|
||||
* Return the next Id.
|
||||
* <p>
|
||||
* If a Transaction has been passed in use the Connection from it.
|
||||
* </p>
|
||||
*/
|
||||
@Override
|
||||
public Object nextId(Transaction t) {
|
||||
Object tenantKey = currentTenantKey();
|
||||
TenantBuffer buffer = buffer(tenantKey);
|
||||
buffer.lock.lock();
|
||||
lock.lock();
|
||||
try {
|
||||
int size = buffer.idList.size();
|
||||
int size = idList.size();
|
||||
if (size > 0) {
|
||||
maybeLoadMoreInBackground(size);
|
||||
} else {
|
||||
loadMore(tenantKey, buffer, allocationSize);
|
||||
loadMore(allocationSize);
|
||||
}
|
||||
return buffer.idList.poll();
|
||||
return idList.pollFirst();
|
||||
} finally {
|
||||
buffer.lock.unlock();
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,36 +106,29 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMore(Object tenantKey, TenantBuffer buffer, int requestSize) {
|
||||
List<Long> newIds = getMoreIds(tenantKey, requestSize);
|
||||
buffer.lock.lock();
|
||||
private void loadMore(int requestSize) {
|
||||
List<Long> newIds = getMoreIds(requestSize);
|
||||
lock.lock();
|
||||
try {
|
||||
buffer.idList.addAll(newIds);
|
||||
idList.addAll(newIds);
|
||||
} finally {
|
||||
buffer.lock.unlock();
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load another batch of Id's using a background thread.
|
||||
* <p>
|
||||
* The tenant is captured here (submit time) as the current tenant is not in scope
|
||||
* on the background executor thread.
|
||||
*/
|
||||
protected void loadInBackground(final int requestSize) {
|
||||
final Object tenantKey = currentTenantKey();
|
||||
final TenantBuffer buffer = buffer(tenantKey);
|
||||
if (!buffer.currentlyBackgroundLoading.compareAndSet(false, true)) {
|
||||
if (currentlyBackgroundLoading.get()) {
|
||||
// skip as already background loading
|
||||
log.log(DEBUG, "... skip background sequence load (another load in progress)");
|
||||
return;
|
||||
}
|
||||
currentlyBackgroundLoading.set(true);
|
||||
backgroundExecutor.execute(() -> {
|
||||
try {
|
||||
loadMore(tenantKey, buffer, requestSize);
|
||||
} finally {
|
||||
buffer.currentlyBackgroundLoading.set(false);
|
||||
}
|
||||
loadMore(requestSize);
|
||||
currentlyBackgroundLoading.set(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,7 +140,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
/**
|
||||
* Get more Id's by executing a query and reading the Id's returned.
|
||||
*/
|
||||
protected List<Long> getMoreIds(Object tenantKey, int requestSize) {
|
||||
protected List<Long> getMoreIds(int requestSize) {
|
||||
|
||||
String sql = getSql(requestSize);
|
||||
|
||||
@@ -193,7 +148,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
PreparedStatement statement = null;
|
||||
ResultSet resultSet = null;
|
||||
try {
|
||||
connection = connectionFor(tenantKey);
|
||||
connection = dataSource.getConnection();
|
||||
|
||||
statement = connection.prepareStatement(sql);
|
||||
resultSet = statement.executeQuery();
|
||||
@@ -219,17 +174,6 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a connection for the given tenant. For multi-tenant this is routed to the
|
||||
* tenant database/schema/catalog; otherwise the plain DataSource connection is used.
|
||||
*/
|
||||
private Connection connectionFor(Object tenantKey) throws SQLException {
|
||||
if (tenantSource != null && tenantKey != SINGLE) {
|
||||
return tenantSource.connectionForTenant(tenantKey);
|
||||
}
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the JDBC resources.
|
||||
*/
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package io.ebean.config.dbplatform;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Optionally implemented by the DataSource passed to a {@link SequenceIdGenerator}
|
||||
* to make sequence id allocation multi-tenant aware.
|
||||
* <p>
|
||||
* When the DataSource implements this interface the sequence generator maintains
|
||||
* a separate id buffer per tenant and obtains connections that are routed to the
|
||||
* correct tenant database (TenantMode.DB) or schema/catalog (TenantMode.SCHEMA / CATALOG).
|
||||
* <p>
|
||||
* The {@link #connectionForTenant(Object)} method takes an explicit tenantId so that
|
||||
* background pre-fetch (which runs on a separate thread without the current tenant
|
||||
* in scope) can fetch sequence values for the tenant captured at submit time.
|
||||
*/
|
||||
public interface TenantConnectionSource {
|
||||
|
||||
/**
|
||||
* Return the current tenant id, or null when there is no current tenant scope.
|
||||
*/
|
||||
Object currentTenantId();
|
||||
|
||||
/**
|
||||
* Return a connection routed to the given tenant (its database, schema or catalog).
|
||||
*/
|
||||
Connection connectionForTenant(Object tenantId) throws SQLException;
|
||||
}
|
||||
@@ -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,7 +1,6 @@
|
||||
package io.ebean.event;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.DatabaseFactory;
|
||||
import io.ebean.EbeanVersion;
|
||||
import io.ebean.service.SpiContainer;
|
||||
|
||||
@@ -189,7 +188,6 @@ public final class ShutdownManager {
|
||||
*/
|
||||
public static void unregisterDatabase(Database server) {
|
||||
databases.remove(server);
|
||||
DatabaseFactory.unregister(server);
|
||||
}
|
||||
|
||||
private static class ShutdownHook extends Thread {
|
||||
|
||||
@@ -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;
|
||||
@@ -5,18 +5,13 @@ package io.ebean.meta;
|
||||
*/
|
||||
public abstract class AbstractMetricVisitor implements MetricVisitor {
|
||||
|
||||
private final Mode mode;
|
||||
private final boolean reset;
|
||||
private final boolean collectTransactionMetrics;
|
||||
private final boolean collectQueryMetrics;
|
||||
private final boolean collectL2Metrics;
|
||||
|
||||
public AbstractMetricVisitor(boolean reset, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
|
||||
this(reset ? Mode.RESET : Mode.CUMULATIVE,
|
||||
collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
|
||||
}
|
||||
|
||||
public AbstractMetricVisitor(Mode mode, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
|
||||
this.mode = mode;
|
||||
this.reset = reset;
|
||||
this.collectTransactionMetrics = collectTransactionMetrics;
|
||||
this.collectQueryMetrics = collectQueryMetrics;
|
||||
this.collectL2Metrics = collectL2Metrics;
|
||||
@@ -24,12 +19,7 @@ public abstract class AbstractMetricVisitor implements MetricVisitor {
|
||||
|
||||
@Override
|
||||
public boolean reset() {
|
||||
return mode == Mode.RESET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode mode() {
|
||||
return mode;
|
||||
return reset;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,3 +47,4 @@ public abstract class AbstractMetricVisitor implements MetricVisitor {
|
||||
// do nothing by default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,16 +30,7 @@ public class BasicMetricVisitor extends AbstractMetricVisitor implements ServerM
|
||||
* Construct specifying reset and what to collect.
|
||||
*/
|
||||
public BasicMetricVisitor(String name, Function<String,String> naming, boolean reset, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
|
||||
this(name, naming, reset ? Mode.RESET : Mode.CUMULATIVE,
|
||||
collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct specifying the collection mode and what to collect.
|
||||
*/
|
||||
public BasicMetricVisitor(String name, Function<String,String> naming, Mode mode,
|
||||
boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
|
||||
super(mode, collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
|
||||
super(reset, collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
|
||||
this.name = name;
|
||||
this.naming = naming;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,6 @@ import java.time.Instant;
|
||||
*/
|
||||
public interface MetaQueryPlan {
|
||||
|
||||
/**
|
||||
* Return the name of the database for the query.
|
||||
*/
|
||||
String dbName();
|
||||
|
||||
/**
|
||||
* Return the bean type for the query.
|
||||
*/
|
||||
@@ -49,11 +44,6 @@ public interface MetaQueryPlan {
|
||||
*/
|
||||
String plan();
|
||||
|
||||
/**
|
||||
* The tenant ID of the plan.
|
||||
*/
|
||||
Object tenantId();
|
||||
|
||||
/**
|
||||
* Return the query execution time associated with the bind values capture.
|
||||
*/
|
||||
|
||||
@@ -7,12 +7,6 @@ import java.util.function.Function;
|
||||
*/
|
||||
public interface MetricVisitor {
|
||||
|
||||
enum Mode {
|
||||
RESET,
|
||||
CUMULATIVE,
|
||||
DELTA
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the naming convention that should be applied to the reported metric names.
|
||||
*/
|
||||
@@ -23,13 +17,6 @@ public interface MetricVisitor {
|
||||
*/
|
||||
boolean reset();
|
||||
|
||||
/**
|
||||
* Return the metric collection mode.
|
||||
*/
|
||||
default Mode mode() {
|
||||
return reset() ? Mode.RESET : Mode.CUMULATIVE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if we should visit the transaction metrics.
|
||||
*/
|
||||
|
||||
@@ -37,16 +37,6 @@ public interface TimedMetric {
|
||||
*/
|
||||
TimedMetricStats collect(boolean reset);
|
||||
|
||||
/**
|
||||
* Collect a snapshot using the given collection mode.
|
||||
*
|
||||
* <p>Implementations that do not support delta collection use cumulative
|
||||
* collection for {@link MetricVisitor.Mode#DELTA}.</p>
|
||||
*/
|
||||
default TimedMetricStats collect(MetricVisitor.Mode mode) {
|
||||
return collect(mode == MetricVisitor.Mode.RESET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit non empty metrics.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user