mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
165
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56894400f4 | ||
|
|
32931d6db0 | ||
|
|
73c9ff8d80 | ||
|
|
bdbe743f56 | ||
|
|
ed2026b858 | ||
|
|
8d21f91f62 | ||
|
|
6d1d58b8c2 | ||
|
|
a943ee9225 | ||
|
|
94f252f423 | ||
|
|
275f8ad9f9 | ||
|
|
9ac3bd71f6 | ||
|
|
bb46c88db1 | ||
|
|
28e6108315 | ||
|
|
c20d1cdf9b | ||
|
|
007409d21c | ||
|
|
8b61069b3f | ||
|
|
a82dcb2509 | ||
|
|
08bb170cb2 | ||
|
|
a2f954a60e | ||
|
|
1d654e350b | ||
|
|
6dd1763e54 | ||
|
|
b32f3bcad6 | ||
|
|
e525d4e159 | ||
|
|
4e639cb88a | ||
|
|
fc78b2af80 | ||
|
|
29647ebe1a | ||
|
|
38146fdce5 | ||
|
|
864aff6c3b | ||
|
|
a161a42742 | ||
|
|
24da374aa8 | ||
|
|
398848378d | ||
|
|
688eee532c | ||
|
|
88d7b5e85a | ||
|
|
985e3b12b1 | ||
|
|
942bab5efb | ||
|
|
c114ebaec5 | ||
|
|
9acc555e8e | ||
|
|
73a646e7dc | ||
|
|
bdda502d55 | ||
|
|
6d447a6c1a | ||
|
|
16872fdb30 | ||
|
|
ec9303762e | ||
|
|
a59c70054b | ||
|
|
25bce83afc | ||
|
|
e372579b15 | ||
|
|
f470782481 | ||
|
|
d7b9f6688a | ||
|
|
66bcfd107d | ||
|
|
3d2cc2d4af | ||
|
|
c73330e9de | ||
|
|
a317d669f0 | ||
|
|
291966cea9 | ||
|
|
ad05ed051b | ||
|
|
01b8c3dbcb | ||
|
|
443b68a3b0 | ||
|
|
98f0d42b7e | ||
|
|
959951203d | ||
|
|
c47fbd411b | ||
|
|
4b75820fd6 | ||
|
|
97bd0e1bb8 | ||
|
|
0f5c7e4390 | ||
|
|
d7d040d031 | ||
|
|
6ec7c61594 | ||
|
|
0126470391 | ||
|
|
d5f32690ea | ||
|
|
d7a3417fe4 | ||
|
|
141f98f5d7 | ||
|
|
fd74fed34f | ||
|
|
274411b8fa | ||
|
|
691f153d89 | ||
|
|
dd1acf58a0 | ||
|
|
7bf8f7fbce | ||
|
|
490c70a7b7 | ||
|
|
ca19a78c23 | ||
|
|
66d599faa2 | ||
|
|
e3dad5bd44 | ||
|
|
f50d56faee | ||
|
|
a7fddf1981 | ||
|
|
f610d03a1d | ||
|
|
852f199ffb | ||
|
|
0e6cd9db8c | ||
|
|
484ed5d859 | ||
|
|
56d33b1f1b | ||
|
|
e9b00cbbd1 | ||
|
|
230d7a36a7 | ||
|
|
10bf5dbbbd | ||
|
|
bffe741642 | ||
|
|
a8189567dd | ||
|
|
1545e68c3e | ||
|
|
4fd32b45d2 | ||
|
|
313fdda857 | ||
|
|
d42f72c0a2 | ||
|
|
6d53e89a80 | ||
|
|
7c5ee5b555 | ||
|
|
a2337a096e | ||
|
|
7bf5fc2798 | ||
|
|
987798add9 | ||
|
|
6295faa351 | ||
|
|
d9252a9c85 | ||
|
|
ddd864852d | ||
|
|
7966d8bc1b | ||
|
|
66e6e83e60 | ||
|
|
0e7f68e75a | ||
|
|
7b8e1713dd | ||
|
|
3ea9a5fa7f | ||
|
|
4fab5dfe4b | ||
|
|
9fac046f39 | ||
|
|
50bfd04987 | ||
|
|
d71ced32e8 | ||
|
|
6722cf50e9 | ||
|
|
13cebbcacb | ||
|
|
e70777bdb6 | ||
|
|
86cfd185f3 | ||
|
|
3970847443 | ||
|
|
b440c5ea27 | ||
|
|
1836ff18a5 | ||
|
|
efdee053cc | ||
|
|
a1ee75ffec | ||
|
|
da3dd8b215 | ||
|
|
6d30e6ff82 | ||
|
|
453a320210 | ||
|
|
61cc5e3459 | ||
|
|
23f23fa32b | ||
|
|
f65c409bfe | ||
|
|
ec49824430 | ||
|
|
819aaece4d | ||
|
|
c275953582 | ||
|
|
82e8494f42 | ||
|
|
abacda2c8f | ||
|
|
a7d1253dba | ||
|
|
a081d08621 | ||
|
|
8c37b53bad | ||
|
|
3f6d565800 | ||
|
|
1408696912 | ||
|
|
e0531a6315 | ||
|
|
0023f0ce13 | ||
|
|
032f4de857 | ||
|
|
59431814ce | ||
|
|
e7194055be | ||
|
|
806c7cd752 | ||
|
|
dfc7f92160 | ||
|
|
26351325a8 | ||
|
|
2d37f9d01e | ||
|
|
d3fd03ce5b | ||
|
|
a8e92791cd | ||
|
|
94e63cc30f | ||
|
|
3567263250 | ||
|
|
bc03f8d516 | ||
|
|
7035ff20eb | ||
|
|
f38479022a | ||
|
|
986cc905d8 | ||
|
|
8f3fe688cb | ||
|
|
eee26d9ed3 | ||
|
|
44bd60e586 | ||
|
|
20d7af6d25 | ||
|
|
e5aeda9d4e | ||
|
|
46bb1ca060 | ||
|
|
0652167101 | ||
|
|
6633293151 | ||
|
|
9e603f2848 | ||
|
|
fff345ffc8 | ||
|
|
dd1fe845aa | ||
|
|
80451c3c62 | ||
|
|
2d199737aa | ||
|
|
1c0a811c01 |
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -40,5 +40,7 @@ jobs:
|
||||
# - name: Maven single test
|
||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||
- name: Build with Maven
|
||||
run: mvn -T 8 clean test -Pdefault
|
||||
run: mvn -T 1C clean install -Pdefault
|
||||
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
|
||||
run: cd tests/test-java16 && mvn test
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: db2
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: H2Database
|
||||
run: mvn -T 8 clean package
|
||||
run: mvn -T 1C clean package
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mariadb 10.11
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11, 17, 21]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mysql
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: oracle
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: postgres
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: sqlserver 2022
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [11]
|
||||
java_version: [21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
|
||||
@@ -13,6 +13,9 @@ ebean-profiling*.xml
|
||||
profiling/
|
||||
.DS_Store
|
||||
|
||||
# Local Redis integration test credentials
|
||||
ebean-redis/src/test/resources/redis-local.yml
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
@@ -183,6 +183,8 @@ database.save(customer);
|
||||
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
|
||||
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
|
||||
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
|
||||
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
|
||||
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
|
||||
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
|
||||
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
|
||||
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
|
||||
|
||||
@@ -11,8 +11,13 @@ Step-by-step guides for common Ebean tasks (Maven setup, database config, query
|
||||
Key guides (fetch and follow when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- 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
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
|
||||
@@ -18,12 +18,29 @@ existing Maven project. Complete the steps in order.
|
||||
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
|
||||
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
|
||||
|
||||
## Migration & upgrades
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
|
||||
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
|
||||
|
||||
## Observability
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
|
||||
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
|
||||
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
|
||||
|
||||
## Entity beans
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
|
||||
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
|
||||
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
|
||||
| [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
|
||||
|
||||
@@ -31,6 +48,7 @@ existing Maven project. Complete the steps in order.
|
||||
|-------|-------------|
|
||||
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
|
||||
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
|
||||
| [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
|
||||
|
||||
@@ -133,8 +151,12 @@ tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
|
||||
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
@@ -158,6 +180,7 @@ tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
|
||||
@@ -250,6 +250,40 @@ For each future set of entity bean changes:
|
||||
4. Review the generated `.sql` to confirm it reflects the intended changes
|
||||
5. Commit both files
|
||||
|
||||
### Protecting hand-edited and non-versioned migrations across regeneration
|
||||
|
||||
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
|
||||
entity model**. It can therefore overwrite content you did not change in the
|
||||
entity beans, including:
|
||||
|
||||
- **hand-edited DDL** in a generated versioned `.sql` file, and
|
||||
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
|
||||
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
|
||||
|
||||
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
|
||||
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
|
||||
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
|
||||
regeneration. The trade-off: to pick up an upstream change to a built-in init
|
||||
script (e.g. the partition helper) you must **delete the file first**, then
|
||||
regenerate. Repeatable scripts are always regenerated.
|
||||
|
||||
To avoid losing manual work:
|
||||
|
||||
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
|
||||
cannot express — it is isolated and now protected from regeneration.
|
||||
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
|
||||
produces, review the diff after **every** regeneration and **restore** any
|
||||
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
|
||||
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
|
||||
re-check that the new migration is listed and filenames match after renaming a
|
||||
generated file.
|
||||
|
||||
> **Run the generator from the module directory.** The output path set via
|
||||
> `setPathToResources(...)` is resolved relative to the **working directory**.
|
||||
> Run `GenerateDbMigration` with the working directory set to the module that owns
|
||||
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
|
||||
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
|
||||
|
||||
---
|
||||
|
||||
## Understanding the output files
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
# Guide: Add Ebean OpenTelemetry tracing
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
|
||||
most importantly, how to order startup so Ebean sees the intended global
|
||||
OpenTelemetry instance.
|
||||
|
||||
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
|
||||
or fixing `GlobalOpenTelemetry` double-registration errors.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
|
||||
spans as children of the current active OpenTelemetry span. It does not create
|
||||
top-level request, job, or Lambda invocation spans by itself.
|
||||
|
||||
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
|
||||
`Database` is configured. For that reason, the application must build and register
|
||||
the OpenTelemetry SDK before any Ebean `Database` beans are created.
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
- Register the global OpenTelemetry instance once.
|
||||
- Register it before building Ebean databases.
|
||||
- Model that ordering as a real DI dependency.
|
||||
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
|
||||
multiple places.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Add the dependency
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
|
||||
No manual Ebean plugin registration is normally required.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Build OpenTelemetry before Ebean databases
|
||||
|
||||
Create one application-owned OpenTelemetry bean. For example, when using
|
||||
`avaje-metrics-otel`:
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.metrics.otel.MetricsOpenTelemetry;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Factory
|
||||
class OpenTelemetryConfig {
|
||||
|
||||
@Bean
|
||||
OpenTelemetry openTelemetry(Configuration config) {
|
||||
return MetricsOpenTelemetry.builder()
|
||||
.endpoint(config.get("otel.endpoint"))
|
||||
.serviceName(config.get("otel.serviceName", "orders"))
|
||||
.deploymentEnvironmentName(config.get("app.env", "local"))
|
||||
.meterInterval(Duration.ofSeconds(30))
|
||||
.traceInterval(Duration.ofSeconds(30))
|
||||
.buildAndRegisterGlobal();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you build the SDK directly, use the same principle: create the SDK once and
|
||||
register that instance globally before any Ebean databases are built.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Make database beans depend on OpenTelemetry
|
||||
|
||||
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
|
||||
is intentionally present to make startup order deterministic: OpenTelemetry is
|
||||
created and registered before Ebean configures the database and profile handler.
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
|
||||
@Bean
|
||||
Database database(OpenTelemetry openTelemetry, Configuration config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.get("db.url"))
|
||||
.username(config.get("db.username"))
|
||||
.password(config.get("db.password"));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
|
||||
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
|
||||
initialized first.
|
||||
|
||||
Do not invert the dependency by making OpenTelemetry depend on the Ebean
|
||||
`Database`. That creates a startup cycle and can still initialize Ebean before the
|
||||
global OpenTelemetry instance is ready.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Create a parent span at the application boundary
|
||||
|
||||
Ebean transaction spans are child spans. They are only created when a recording
|
||||
OpenTelemetry span is active on the current thread.
|
||||
|
||||
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
|
||||
root span around the top-level request/job boundary. Ebean will then attach
|
||||
transaction spans beneath that current span.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `GlobalOpenTelemetry.set has already been called`
|
||||
|
||||
This usually means more than one component is trying to register a global SDK, or
|
||||
some startup path touched the global before the application registered its SDK.
|
||||
|
||||
Fixes:
|
||||
|
||||
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
|
||||
call in the application.
|
||||
2. Build that OpenTelemetry bean before Ebean `Database` beans.
|
||||
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
|
||||
|
||||
### No Ebean spans appear
|
||||
|
||||
Check:
|
||||
|
||||
1. `ebean-opentelemetry` is on the runtime classpath.
|
||||
2. OpenTelemetry is registered before Ebean databases are built.
|
||||
3. There is a current recording parent span when Ebean transactions run.
|
||||
4. Sampling is not dropping the parent trace.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 2: Database Configuration
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Database Configuration
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -148,7 +148,6 @@ Database database(Configuration config) {
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.skipDataSourceCheck(true)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
@@ -156,6 +155,11 @@ Database database(Configuration config) {
|
||||
If the project has a dedicated config-wrapper class (a `@Component` that reads config
|
||||
keys), accept it as a parameter instead of `Configuration`.
|
||||
|
||||
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
|
||||
> into the DI context. If you encounter "No dependency provided for
|
||||
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
|
||||
> (as shown in Step 2).
|
||||
|
||||
---
|
||||
|
||||
## Step 4 (Optional) — Add a read-only datasource
|
||||
|
||||
@@ -85,7 +85,7 @@ already exist:
|
||||
```xml
|
||||
<properties>
|
||||
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
|
||||
<ebean.version>17.2.0</ebean.version>
|
||||
<ebean.version>17.5.0</ebean.version>
|
||||
</properties>
|
||||
```
|
||||
|
||||
@@ -101,7 +101,7 @@ Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.8</version>
|
||||
<version>42.7.11</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -141,8 +141,17 @@ for Postgres test instances:
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
|
||||
avoiding the need to declare those dependencies separately.
|
||||
|
||||
---
|
||||
|
||||
## Step 4b — Add DI framework dependencies (if applicable)
|
||||
@@ -156,12 +165,12 @@ annotation processor. Skip this step if the user chose Spring or no DI.
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>11.5</version>
|
||||
<version>12.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>11.5</version>
|
||||
<version>12.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
@@ -185,7 +194,7 @@ the final `<annotationProcessorPaths>` block should include both:
|
||||
<path> <!-- generate avaje-inject DI code -->
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>11.5</version>
|
||||
<version>12.5</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
@@ -433,3 +433,13 @@ mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
|
||||
```
|
||||
|
||||
On CI servers, omit this file so containers are cleaned up after each build.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
|
||||
with auto-populated random values. See `testing-with-testentitybuilder.md`.
|
||||
- **Proceed to Step 3** — production database configuration
|
||||
(`add-ebean-postgres-database-config.md`). Verify this step passes with
|
||||
`mvn verify` before continuing.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
|
||||
|
||||
## Purpose
|
||||
|
||||
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
|
||||
|
||||
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
|
||||
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
|
||||
for everything the built-in support does not handle.
|
||||
|
||||
This guide lists exactly which property types are handled built-in and which require
|
||||
`ebean-jackson-mapper`.
|
||||
|
||||
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
|
||||
> classpath, Ebean fails fast at startup:
|
||||
>
|
||||
> ```text
|
||||
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
|
||||
> Jackson ObjectMapper not present for <property>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|
||||
|---|:---:|:---:|
|
||||
| `String` | ✅ | |
|
||||
| `List<String>`, `List<Long>` | ✅ | |
|
||||
| `Set<String>`, `Set<Long>` | ✅ | |
|
||||
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
|
||||
| `Map<String, String>` | ✅ | |
|
||||
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
|
||||
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
|
||||
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
|
||||
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
|
||||
| POJOs, records, or any other type | | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Built-in support (no Jackson required)
|
||||
|
||||
The built-in path materialises JSON into the *natural* JSON value types
|
||||
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
|
||||
for the following declared property types:
|
||||
|
||||
- **`String`** — stored as raw JSON text.
|
||||
- **`List<String>`** and **`List<Long>`**.
|
||||
- **`Set<String>`** and **`Set<Long>`**.
|
||||
- **`Map<K, V>`** where:
|
||||
- the key `K` is `String` or an **enum**, and
|
||||
- the value `V` is `Object`, `String`, or a wildcard `?`.
|
||||
|
||||
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
|
||||
are all built-in.
|
||||
|
||||
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
|
||||
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
|
||||
|
||||
---
|
||||
|
||||
## Everything else → Jackson `ObjectMapper`
|
||||
|
||||
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
|
||||
requires `ebean-jackson-mapper`:
|
||||
|
||||
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
|
||||
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
|
||||
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
|
||||
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
|
||||
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
|
||||
- **POJOs, records, and any other custom type.**
|
||||
|
||||
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
|
||||
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
|
||||
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
|
||||
|
||||
---
|
||||
|
||||
## Adding `ebean-jackson-mapper`
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
|
||||
registers the mapper-based JSON support automatically.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
|
||||
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
|
||||
JSON keys.
|
||||
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
|
||||
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
|
||||
(with a JSON fallback on platforms without array support) and supports more element types
|
||||
than built-in `@DbJson` collections.
|
||||
- The reason typed value/element collections need a real mapper is that the built-in path
|
||||
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
|
||||
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
|
||||
type-aware mapper.
|
||||
|
||||
---
|
||||
|
||||
## Choosing
|
||||
|
||||
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
|
||||
object/string maps) to avoid pulling in Jackson.
|
||||
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
|
||||
typed-value maps.
|
||||
@@ -0,0 +1,164 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,262 @@
|
||||
# Guide: Ebean query metrics and naming
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains the metrics Ebean captures, how the metric **name** for a query
|
||||
is derived, and how you influence that name with `setLabel(..)` and **profile
|
||||
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
|
||||
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
|
||||
|
||||
Use this guide when you want to identify a query in metrics/telemetry, when a query
|
||||
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ebean records timing and counter metrics for the work it does. Every metric has a
|
||||
**name** whose leading segment identifies the kind of work:
|
||||
|
||||
| Prefix | What it measures | Example name |
|
||||
|---|---|---|
|
||||
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
|
||||
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
|
||||
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
|
||||
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
|
||||
| `orm.update.` | ORM update statement | `orm.update.<label>` |
|
||||
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
|
||||
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
|
||||
| `l2n.` | L2 cache region | `l2n.customer.hit` |
|
||||
|
||||
The rest of this guide focuses on **`orm.` query names**, which is where labels and
|
||||
profile locations apply.
|
||||
|
||||
---
|
||||
|
||||
## How an ORM query name is derived
|
||||
|
||||
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
|
||||
of three sources, in priority order:
|
||||
|
||||
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
|
||||
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
|
||||
3. **Neither** — the bean type plus the query type (e.g. `findList`).
|
||||
|
||||
| Root query source | Resulting name |
|
||||
|---|---|
|
||||
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
|
||||
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
|
||||
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
|
||||
|
||||
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
|
||||
could be used for any bean), so the bean type is prefixed. A profile location is already
|
||||
unique and type-independent, so it is used as-is.
|
||||
|
||||
### Step 1 - Label a query explicitly
|
||||
|
||||
```java
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setLabel("custMain")
|
||||
.findList();
|
||||
// metric name: orm.Customer.custMain
|
||||
```
|
||||
|
||||
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
|
||||
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
|
||||
used as-is, and an unlabelled DTO query uses just the DTO type:
|
||||
|
||||
```java
|
||||
DB.findDto(CustomerDto.class, sql)
|
||||
.setLabel("byEmail")
|
||||
.findList();
|
||||
// metric name: dto.CustomerDto.byEmail
|
||||
// profile location only -> dto.<location> (no type prefix)
|
||||
// unlabelled -> dto.CustomerDto
|
||||
```
|
||||
|
||||
### Step 2 - Use a profile location (preferred for finders / query beans)
|
||||
|
||||
A profile location identifies a query by its **call site** (`Class.method`) instead of a
|
||||
hand-written label.
|
||||
|
||||
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
|
||||
setup when using query beans / finders), Ebean assigns each query a profile location
|
||||
derived from its call site — no code is required:
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.eq(Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
|
||||
```
|
||||
|
||||
The enhancer derives the location from the calling code (the method that runs the query),
|
||||
and for many call sites it includes the **source line number** (e.g.
|
||||
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
|
||||
names automatically.
|
||||
|
||||
**Setting one explicitly.** You can also set a profile location yourself, which is useful
|
||||
without enhancement or to control the identity:
|
||||
|
||||
```java
|
||||
ProfileLocation LOC = ProfileLocation.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setProfileLocation(LOC)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<DeclaringClass>.<method>
|
||||
```
|
||||
|
||||
Factory choices:
|
||||
|
||||
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
|
||||
- `ProfileLocation.createWithLine()` — includes the source line number
|
||||
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
|
||||
**distinct** names.
|
||||
- `ProfileLocation.create("label")` — a named location (used for named transactions).
|
||||
|
||||
> Note: a location with no line number (`create()`, or a call site the enhancer emits
|
||||
> without a line) means two different queries in the same method share one name. The
|
||||
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
|
||||
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
|
||||
> same-method call sites in the name itself.
|
||||
|
||||
---
|
||||
|
||||
## Secondary (lazy / query) load naming
|
||||
|
||||
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
|
||||
**secondary** query. Its name **extends the parent query's full name** with the relative
|
||||
path and the load mode (`lazy` or `query`), joined with `.`:
|
||||
|
||||
```
|
||||
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
|
||||
```
|
||||
|
||||
So a secondary load is always an exact extension of its parent metric name, which makes
|
||||
the relationship obvious in dashboards.
|
||||
|
||||
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
|
||||
|
||||
Lazy loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.lazy
|
||||
orm.Customer.custMain.orders.lazy.details.lazy
|
||||
```
|
||||
|
||||
Secondary eager `fetchQuery()` loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.query
|
||||
orm.Customer.custMain.orders.query.details.query
|
||||
```
|
||||
|
||||
The same applies with a **profile-location** root (no explicit `setLabel`):
|
||||
```
|
||||
orm.CustomerFinder.byName
|
||||
orm.CustomerFinder.byName.contacts.lazy
|
||||
```
|
||||
|
||||
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
|
||||
type — it inherits the parent's name so it relates back to where the load originated.
|
||||
|
||||
---
|
||||
|
||||
## Inline SQL comment
|
||||
|
||||
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
|
||||
profile-location label) as an inline SQL comment, which is useful for matching slow
|
||||
queries in database logs back to application code:
|
||||
|
||||
```sql
|
||||
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
|
||||
```
|
||||
|
||||
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
|
||||
label. Secondary queries use their full extended name
|
||||
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
|
||||
commented.
|
||||
|
||||
Disable it via the builder:
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.includeLabelInSql(false)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collecting metrics at runtime
|
||||
|
||||
Read collected metrics through `Database.metaInfo()`:
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaQueryMetric;
|
||||
import io.ebean.meta.ServerMetrics;
|
||||
|
||||
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
|
||||
|
||||
for (MetaQueryMetric q : metrics.queryMetrics()) {
|
||||
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
|
||||
q.name(), // e.g. orm.Customer.custMain
|
||||
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
|
||||
q.count(), q.total(), q.mean());
|
||||
}
|
||||
```
|
||||
|
||||
Key API:
|
||||
|
||||
- `database.metaInfo()` → `MetaInfoManager`.
|
||||
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
|
||||
reset; `visitMetrics(visitor)` for streaming.
|
||||
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
|
||||
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
|
||||
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
|
||||
|
||||
---
|
||||
|
||||
## Mapping to avaje-metrics tags
|
||||
|
||||
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
|
||||
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
|
||||
form, with the bean type carried as a `type` tag:
|
||||
|
||||
```
|
||||
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
|
||||
```
|
||||
|
||||
Because the entity is available as the `type` tag, two different-entity queries that
|
||||
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
|
||||
Prometheus, StatsD) without needing the bean type in the name.
|
||||
|
||||
For the integration setup, see the avaje-metrics guide
|
||||
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
|
||||
|
||||
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
|
||||
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### A query shows up as `orm.<Bean>.findList` (no useful identity)
|
||||
|
||||
It has neither a label nor a profile location. Add `setLabel(..)` or a
|
||||
`ProfileLocation`, or apply a profile location on the finder / query bean.
|
||||
|
||||
### Two queries in one method share a metric name
|
||||
|
||||
This happens when the profile location for those call sites has no line number. With
|
||||
enhancement, many call sites already include a line number; for those that don't, use
|
||||
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
|
||||
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
|
||||
different entity types.
|
||||
|
||||
### A secondary (lazy / query) load isn't grouped under its parent
|
||||
|
||||
Secondary names extend the parent's full name. If the parent has no label or profile
|
||||
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
|
||||
that. Give the root query a label or profile location for a stable parent name.
|
||||
@@ -0,0 +1,242 @@
|
||||
# Guide: Ebean query plan capture
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable and configure **query plan capture** in Ebean — the
|
||||
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
|
||||
queries, so you can diagnose missing indexes and poor plans in production.
|
||||
|
||||
Use this guide when you want Ebean to record real query plans, when tuning the capture
|
||||
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Query plan capture is a **two-phase** mechanism:
|
||||
|
||||
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
|
||||
slower than a threshold, captures the actual **bind values** that were used. This is
|
||||
cheap: it just remembers the parameters of a slow execution.
|
||||
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
|
||||
against the database to obtain the execution plan, producing `MetaQueryPlan` results
|
||||
that are handed to a `QueryPlanListener`.
|
||||
|
||||
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
|
||||
happens periodically or on demand, against representative bind values, rather than on
|
||||
every slow query.
|
||||
|
||||
Two ways to trigger phase 2:
|
||||
|
||||
- **Automatic periodic capture** — a background timer collects plans on a schedule.
|
||||
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
|
||||
(this is what remote tooling such as ebean-insight uses).
|
||||
|
||||
Plan capable queries are:
|
||||
|
||||
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
|
||||
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
|
||||
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
|
||||
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
|
||||
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
|
||||
does not double-count in `queryPlanInit`.
|
||||
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
|
||||
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
|
||||
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
|
||||
|
||||
Specifically **excluded** are:
|
||||
|
||||
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
|
||||
|
||||
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
|
||||
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
|
||||
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
|
||||
never captures bind values and cannot be `EXPLAIN`'d.
|
||||
|
||||
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
|
||||
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
|
||||
> incur no extra cost beyond their existing timing metric.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Enable bind capture
|
||||
|
||||
Bind capture is the master switch; nothing is captured until it is on.
|
||||
|
||||
> **Security — bind values may contain PII.** Bind capture records the **actual
|
||||
> parameter values** used by slow query executions, and those values are stored
|
||||
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
|
||||
> plan). They can therefore contain personal or otherwise sensitive data. Capture
|
||||
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
|
||||
> that data exposure is acceptable, restrict who can read captured plans, and
|
||||
> prefer arming specific query hashes (Step 3) over a low global threshold so you
|
||||
> capture the minimum needed.
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true) // turn on bind capture
|
||||
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
|
||||
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
|
||||
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
|
||||
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
|
||||
|
||||
Equivalent `application.properties` (avaje-config / properties):
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Enable automatic periodic capture (optional)
|
||||
|
||||
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true)
|
||||
.queryPlanThresholdMicros(100_000)
|
||||
.queryPlanCapture(true) // turn on the periodic capture timer
|
||||
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
|
||||
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
|
||||
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
|
||||
.queryPlanListener(capture -> {
|
||||
for (var plan : capture.plans()) {
|
||||
System.out.println(plan.label() + "\n" + plan.plan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
|
||||
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
|
||||
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
|
||||
exceeded, bounding the database load. Default **10000** (10s).
|
||||
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
|
||||
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
|
||||
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
|
||||
|
||||
Properties form:
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
queryPlan.capture=true
|
||||
queryPlan.capturePeriodSecs=600
|
||||
queryPlan.captureMaxTimeMillis=10000
|
||||
queryPlan.captureMaxCount=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Capture on demand (foreground)
|
||||
|
||||
Instead of (or in addition to) the periodic timer, drive capture through
|
||||
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
|
||||
arms specific slow queries by their plan hash.
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaInfoManager;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
|
||||
MetaInfoManager meta = database.metaInfo();
|
||||
|
||||
// Phase 1: arm bind capture - either all plans or specific hashes
|
||||
QueryPlanInit init = new QueryPlanInit();
|
||||
init.setAll(true); // or init.add("<planHash>", 50_000);
|
||||
init.thresholdMicros(100_000);
|
||||
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
|
||||
|
||||
// ... let the application run so slow executions capture their bind values ...
|
||||
|
||||
// Phase 2: collect plans now (runs EXPLAIN)
|
||||
QueryPlanRequest request = new QueryPlanRequest();
|
||||
request.maxCount(10);
|
||||
request.maxTimeMillis(10_000);
|
||||
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
|
||||
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
|
||||
```
|
||||
|
||||
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
|
||||
arms a specific plan (a hash of `"all"` is treated as all).
|
||||
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
|
||||
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
|
||||
bound the work, mirroring the periodic settings.
|
||||
|
||||
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
|
||||
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
|
||||
`captureMicros()`, and `whenCaptured()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - EXPLAIN dialect
|
||||
|
||||
Ebean chooses the `EXPLAIN` statement per database platform:
|
||||
|
||||
| Platform | EXPLAIN used |
|
||||
|---|---|
|
||||
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
|
||||
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
|
||||
| Oracle | `EXPLAIN PLAN FOR <sql>` |
|
||||
| SQL Server | platform-specific logger |
|
||||
| H2 / MySQL / other | `explain <sql>` |
|
||||
|
||||
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
|
||||
.build();
|
||||
```
|
||||
|
||||
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
|
||||
> executes** the query to produce real timings. For non-idempotent or expensive queries,
|
||||
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
|
||||
|
||||
---
|
||||
|
||||
## Related setting: internal plan TTL
|
||||
|
||||
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
|
||||
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
|
||||
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
|
||||
the same builder.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No plans are captured
|
||||
|
||||
1. `queryPlanEnable(true)` must be set — it is the master switch.
|
||||
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
|
||||
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
|
||||
3. For periodic capture, also set `queryPlanCapture(true)`.
|
||||
4. Queries must actually run slower than the threshold to have their binds captured.
|
||||
|
||||
### Plans appear but nothing is reported anywhere
|
||||
|
||||
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
|
||||
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
|
||||
|
||||
### Capture adds noticeable database load
|
||||
|
||||
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
|
||||
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
|
||||
`queryPlanExplain` to a non-ANALYZE form.
|
||||
|
||||
### An unlabelled SqlQuery or update metric never offers plan capture
|
||||
|
||||
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
|
||||
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
|
||||
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
|
||||
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
|
||||
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
|
||||
capture and are intentionally excluded.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
|
||||
avaje-json-core APIs.
|
||||
|
||||
Use this when upgrading code that references:
|
||||
|
||||
- `com.fasterxml.jackson.core.JsonParser`
|
||||
- `com.fasterxml.jackson.core.JsonGenerator`
|
||||
- `com.fasterxml.jackson.core.JsonFactory`
|
||||
|
||||
The replacement types are:
|
||||
|
||||
- `io.avaje.json.JsonReader`
|
||||
- `io.avaje.json.JsonWriter`
|
||||
- `io.avaje.json.stream.JsonStream`
|
||||
|
||||
---
|
||||
|
||||
## Breaking API changes
|
||||
|
||||
| Previous API | New API |
|
||||
|---|---|
|
||||
| `JsonParser` | `JsonReader` |
|
||||
| `JsonGenerator` | `JsonWriter` |
|
||||
| `JsonFactory` | `JsonStream` |
|
||||
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
|
||||
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
|
||||
|
||||
---
|
||||
|
||||
## Typical migration rewrites
|
||||
|
||||
### Parser and generator signatures
|
||||
|
||||
```java
|
||||
// before
|
||||
void read(JsonParser parser)
|
||||
void write(JsonGenerator generator)
|
||||
|
||||
// after
|
||||
void read(JsonReader parser)
|
||||
void write(JsonWriter generator)
|
||||
```
|
||||
|
||||
### Database configuration
|
||||
|
||||
```java
|
||||
// before
|
||||
Database.builder().jsonFactory(factory)
|
||||
|
||||
// after
|
||||
Database.builder().jsonStream(stream)
|
||||
```
|
||||
|
||||
### JSON utility calls
|
||||
|
||||
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
|
||||
If your code was calling those APIs with Jackson core types, switch to avaje types.
|
||||
|
||||
---
|
||||
|
||||
## Dependency and module notes
|
||||
|
||||
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
|
||||
parsing/writing.
|
||||
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
|
||||
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
|
||||
integrations.
|
||||
|
||||
---
|
||||
|
||||
## Behavior notes to verify during upgrade
|
||||
|
||||
1. Parser token handling is now based on avaje `JsonReader.Token`.
|
||||
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
|
||||
be validated in your tests if you previously depended on Jackson token quirks.
|
||||
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
|
||||
keep ObjectMapper wiring enabled.
|
||||
|
||||
---
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
|
||||
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
|
||||
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
|
||||
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide shows how to migrate legacy programmatic database creation code from:
|
||||
|
||||
- `new DatabaseConfig()`
|
||||
- `DatabaseFactory.create(...)`
|
||||
- old `setXxx(...)` builder-style configuration methods
|
||||
|
||||
…to the preferred builder-based style using:
|
||||
|
||||
- `Database.builder()`
|
||||
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
|
||||
- `DatabaseBuilder.build()`
|
||||
|
||||
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
|
||||
|
||||
---
|
||||
|
||||
## Preferred pattern
|
||||
|
||||
Prefer code shaped like this:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.dataSourceBuilder(dataSource)
|
||||
.register(true)
|
||||
.defaultDatabase(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
The important points are:
|
||||
|
||||
1. Start with `Database.builder()`
|
||||
2. Configure via `DatabaseBuilder`
|
||||
3. Finish with `.build()`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
|
||||
- If existing code only uses standard builder methods, this change is usually mechanical.
|
||||
- If existing code later reads configuration back, use `config.settings()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
Database database = config.build();
|
||||
```
|
||||
|
||||
### Short form
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Replace `DatabaseFactory.create("name")`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
Database database = DatabaseFactory.create("other");
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("other")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
|
||||
|
||||
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
|
||||
Use those names when migrating older setup code.
|
||||
|
||||
| Legacy call | Preferred call |
|
||||
|---|---|
|
||||
| `setName("db")` | `name("db")` |
|
||||
| `setRegister(false)` | `register(false)` |
|
||||
| `setDefaultServer(false)` | `defaultDatabase(false)` |
|
||||
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
|
||||
| `setDbSchema("app")` | `dbSchema("app")` |
|
||||
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
|
||||
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
|
||||
| `setRunMigration(true)` | `runMigration(true)` |
|
||||
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
|
||||
| `setPersistBatch(batch)` | `persistBatch(batch)` |
|
||||
|
||||
### Full example
|
||||
|
||||
#### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.setRegister(false);
|
||||
config.setDefaultServer(false);
|
||||
config.setDataSourceConfig(dataSource);
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.register(false)
|
||||
.defaultDatabase(false)
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify semantics after migration
|
||||
|
||||
The migration should preserve behavior, but verify these points:
|
||||
|
||||
- `register(true)` is still the default
|
||||
- `defaultDatabase(true)` is still the default
|
||||
- call `loadFromProperties()` if the old code loaded configuration from properties
|
||||
- for named databases, set the name before loading properties
|
||||
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
|
||||
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
|
||||
|
||||
---
|
||||
|
||||
## Manual-review cases
|
||||
|
||||
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
|
||||
|
||||
### `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
|
||||
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
|
||||
|
||||
### `DatabaseFactory.initialiseContainer(...)`
|
||||
|
||||
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
|
||||
|
||||
### `DatabaseFactory.shutdown()`
|
||||
|
||||
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
|
||||
|
||||
### Variables or method signatures typed as `DatabaseConfig`
|
||||
|
||||
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
|
||||
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
|
||||
|
||||
### Code that needs read access to builder settings
|
||||
|
||||
Use:
|
||||
|
||||
```java
|
||||
DatabaseBuilder builder = Database.builder();
|
||||
DatabaseBuilder.Settings settings = builder.settings();
|
||||
```
|
||||
|
||||
rather than relying on the concrete `DatabaseConfig` type only to read getters.
|
||||
|
||||
---
|
||||
|
||||
## Automation notes for AI agents and bulk refactors
|
||||
|
||||
This migration is a good candidate for semi-automated upgrading.
|
||||
|
||||
### Safe mechanical rewrites
|
||||
|
||||
These are usually safe to rewrite automatically:
|
||||
|
||||
- `new DatabaseConfig()` → `Database.builder()`
|
||||
- `DatabaseFactory.create(builder)` → `builder.build()`
|
||||
- `DatabaseFactory.create("name")` → `Database.builder().name("name").loadFromProperties().build()`
|
||||
- legacy `setXxx(...)` calls → preferred fluent builder methods
|
||||
|
||||
### Flag for manual review
|
||||
|
||||
Automatically flag, but do not blindly rewrite:
|
||||
|
||||
- `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
- `DatabaseFactory.initialiseContainer(...)`
|
||||
- `DatabaseFactory.shutdown()`
|
||||
- parameters, fields, or return types declared as `DatabaseConfig`
|
||||
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
|
||||
|
||||
---
|
||||
|
||||
## Related guides
|
||||
|
||||
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
|
||||
- [Guide index](README.md) — full list of Ebean setup and migration guides
|
||||
@@ -0,0 +1,464 @@
|
||||
# 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/)
|
||||
@@ -77,6 +77,7 @@ often the right query shape.
|
||||
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
|
||||
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
|
||||
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
|
||||
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
|
||||
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
|
||||
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
|
||||
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
|
||||
@@ -99,6 +100,43 @@ Customer customer = new QCustomer()
|
||||
|
||||
Do **not** use `findOne()` for predicates that can match multiple rows.
|
||||
|
||||
### Example - stream and map to another type
|
||||
|
||||
Choose based on result size and how you consume it:
|
||||
|
||||
- **`findList().stream()`** — executes the query, materialises the rows,
|
||||
**releases the connection**, then streams over an in-memory list. No open
|
||||
database resources and no try-with-resources needed. Prefer this for small or
|
||||
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
|
||||
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
|
||||
connection (and an implicit transaction) open for the **whole lifetime of the
|
||||
stream pipeline**. It must be closed with try-with-resources. Prefer it when
|
||||
the result may be large, when you want constant memory, or when you want to
|
||||
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
|
||||
|
||||
```java
|
||||
// small, bounded result fully collected -> findList().stream()
|
||||
List<PendingPlan> pending = new QCaptureRequest()
|
||||
.collectedAt.isNull()
|
||||
.orderBy().requestedAt.asc()
|
||||
.findList()
|
||||
.stream()
|
||||
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
|
||||
.toList();
|
||||
|
||||
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
|
||||
try (Stream<Customer> stream = new QCustomer()
|
||||
.status.equalTo(Status.NEW)
|
||||
.findStream()) {
|
||||
stream
|
||||
.map(...)
|
||||
.forEach(...);
|
||||
}
|
||||
```
|
||||
|
||||
For processing large results one bean at a time, `findEach()` is often the
|
||||
simplest choice because it closes the underlying resources automatically.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Build predicates by traversing properties and associations
|
||||
@@ -131,6 +169,49 @@ List<Customer> customers = new QCustomer()
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Optional predicates - prefer conditional helpers over `if` blocks
|
||||
|
||||
When a filter is driven by a nullable/optional parameter, use the built-in
|
||||
conditional helpers instead of wrapping predicates in `if` blocks. The query
|
||||
stays fluent and reads top-to-bottom, and no predicate is added when the value
|
||||
is absent.
|
||||
|
||||
| Helper | Adds predicate when | Resulting SQL |
|
||||
|--------|---------------------|---------------|
|
||||
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
|
||||
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
|
||||
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
|
||||
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
|
||||
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
|
||||
|
||||
```java
|
||||
// Instead of building the query with if blocks:
|
||||
QCustomer q = new QCustomer();
|
||||
if (name != null && !name.isBlank()) {
|
||||
q.name.eq(name.trim());
|
||||
}
|
||||
if (status != null) {
|
||||
q.status.eq(status);
|
||||
}
|
||||
List<Customer> customers = q.findList();
|
||||
|
||||
// Prefer the conditional helpers:
|
||||
List<Customer> customers = new QCustomer()
|
||||
.name.eqIfNotBlank(name)
|
||||
.status.eqIfPresent(status)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Use `eqOrNull(v)` when a null column value should also match - for example an
|
||||
"any environment" row stored with `env_id is null` should surface under any env
|
||||
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
|
||||
|
||||
```java
|
||||
List<CaptureRequest> rows = new QCaptureRequest()
|
||||
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
When adding a new query:
|
||||
@@ -140,6 +221,10 @@ When adding a new query:
|
||||
3. Traverse relationships instead of writing manual join SQL
|
||||
4. Keep property references type-safe; avoid string property names unless the API
|
||||
specifically requires them
|
||||
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
|
||||
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
|
||||
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
|
||||
null column"
|
||||
|
||||
---
|
||||
|
||||
@@ -398,6 +483,30 @@ 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
|
||||
@@ -485,4 +594,5 @@ 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/)
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
# Immutable Bean Cache — notes on multi-level / remote caching
|
||||
|
||||
These notes capture design thoughts for a possible future multi-level immutable bean cache,
|
||||
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
|
||||
in addition to an in-JVM cache.
|
||||
|
||||
## Current important constraint
|
||||
|
||||
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
|
||||
|
||||
That means:
|
||||
|
||||
- `getIfPresent(id)` is on the **row read hot path**
|
||||
- it must remain **cheap and local**
|
||||
- it should **not** perform network I/O
|
||||
- it should **not** deserialize remote payloads
|
||||
- it should **not** trigger loading or record misses
|
||||
|
||||
## Strong recommendation
|
||||
|
||||
For any multi-level cache design:
|
||||
|
||||
- **L1 cache** = in-JVM cache of already materialized immutable beans
|
||||
- **L2 cache** = remote/shared cache of serialized immutable snapshots
|
||||
- **Loader** = Ebean query using the configured fetch group
|
||||
|
||||
With that split:
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
|
||||
|
||||
This preserves the `AssocOneHelp` fast path.
|
||||
|
||||
---
|
||||
|
||||
## Snapshot mindset
|
||||
|
||||
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
|
||||
|
||||
A cached value is specific to:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant (if multi-tenant)
|
||||
- fetch-group / cache identity
|
||||
- serializer/schema version
|
||||
|
||||
This matters because a `Customer` cached with:
|
||||
|
||||
- `select("name,version")`
|
||||
|
||||
is not equivalent to a `Customer` cached with:
|
||||
|
||||
- `select("name,version").fetch("billingAddress", "line1,city")`
|
||||
|
||||
## Key design recommendation
|
||||
|
||||
Remote keys should include at least:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant id (if applicable)
|
||||
- cache/fetch-group identity
|
||||
- optionally serializer/schema version
|
||||
|
||||
Example shape:
|
||||
|
||||
- `immutable:Customer:basic:42`
|
||||
- `immutable:Customer:withAddresses:42`
|
||||
|
||||
---
|
||||
|
||||
## Recommended multi-level flow
|
||||
|
||||
### L1
|
||||
|
||||
Store actual read-only `EntityBean` instances.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- support `getIfPresent(id)`
|
||||
- avoid repeated deserialize cost
|
||||
- avoid network calls on row read path
|
||||
|
||||
### L2
|
||||
|
||||
Store serialized immutable snapshots.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- batch lookup only
|
||||
- support cross-JVM sharing
|
||||
- feed L1 with materialized immutable beans
|
||||
|
||||
### Loader
|
||||
|
||||
Use the existing query/fetch-group-based loader for misses.
|
||||
|
||||
### Suggested `getAll(ids)` flow
|
||||
|
||||
1. Check L1
|
||||
2. Batch remaining ids to L2
|
||||
3. Deserialize L2 hits into read-only beans
|
||||
4. Put those beans into L1
|
||||
5. Batch remaining misses to DB loader
|
||||
6. Freeze / ensure read-only beans
|
||||
7. Write through to L2
|
||||
8. Put into L1
|
||||
9. Negative-cache true misses if desired
|
||||
|
||||
---
|
||||
|
||||
## Invalidation is more important than serialization
|
||||
|
||||
Things to think about:
|
||||
|
||||
- update/delete invalidation across JVMs
|
||||
- local L1 invalidation when L2 entry is removed
|
||||
- ordering relative to DB commit
|
||||
- multiple cache instances for the same bean type but different fetch groups
|
||||
- tenant-scoped invalidation
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- keep current immutable-cache invalidation semantics
|
||||
- add a remote invalidation/event mechanism for L2-backed caches
|
||||
- each JVM should evict affected L1 entries when notified
|
||||
|
||||
Examples:
|
||||
|
||||
- Redis: pub/sub or streams
|
||||
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
|
||||
|
||||
---
|
||||
|
||||
## Serialization format considerations
|
||||
|
||||
## JSON
|
||||
|
||||
### Pros
|
||||
|
||||
- human readable / debuggable
|
||||
- easier rolling upgrades
|
||||
- field-name based, so generally more tolerant of schema evolution
|
||||
- good fit for Redis strings or Postgres JSONB
|
||||
- easier operational debugging
|
||||
|
||||
### Cons
|
||||
|
||||
- larger payloads
|
||||
- more CPU to serialize/deserialize
|
||||
- nested graphs / enums / dates / inheritance need disciplined handling
|
||||
|
||||
## Kryo / generic binary serialization
|
||||
|
||||
### Pros
|
||||
|
||||
- smaller payloads
|
||||
- often faster than JSON
|
||||
- can preserve object graphs efficiently
|
||||
|
||||
### Cons
|
||||
|
||||
- more fragile across versions and rolling deploys
|
||||
- class registration / compatibility pain
|
||||
- harder to inspect/debug
|
||||
- tighter coupling to JVM/class layout
|
||||
- riskier for long-lived shared cache entries
|
||||
|
||||
## Recommendation
|
||||
|
||||
For a first remote/shared implementation:
|
||||
|
||||
- prefer **JSON** or another self-describing structured format
|
||||
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
|
||||
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
|
||||
|
||||
---
|
||||
|
||||
## What to serialize
|
||||
|
||||
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
|
||||
|
||||
A cleaner model is:
|
||||
|
||||
- serialize a **snapshot representation**
|
||||
- deserialize into a fresh entity bean
|
||||
- mark loaded properties appropriately
|
||||
- freeze / ensure read-only state
|
||||
- store the resulting materialized bean in L1
|
||||
|
||||
This gives more control over:
|
||||
|
||||
- loaded-property semantics
|
||||
- read-only state
|
||||
- subtype handling
|
||||
- schema/version evolution
|
||||
|
||||
## Practical recommendation
|
||||
|
||||
Remote cache entries should represent exactly the configured fetch-group snapshot.
|
||||
|
||||
That means:
|
||||
|
||||
- cache what the fetch group loaded
|
||||
- include nested associations loaded by that fetch group
|
||||
- treat it as a self-contained immutable snapshot
|
||||
|
||||
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
|
||||
|
||||
---
|
||||
|
||||
## Redis vs Postgres cache table
|
||||
|
||||
## Redis
|
||||
|
||||
### Good for
|
||||
|
||||
- low latency
|
||||
- batch lookup via MGET / pipelining
|
||||
- TTL/eviction support
|
||||
- natural shared-cache use case
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- extra infrastructure
|
||||
- memory cost
|
||||
- invalidation/event coordination still required
|
||||
|
||||
## Postgres cache table (including unlogged-style approach)
|
||||
|
||||
### Good for
|
||||
|
||||
- simpler ops if Postgres is already present
|
||||
- easy batch lookup with `IN (...)`
|
||||
- fewer moving parts than introducing Redis
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- slower than Redis for hot shared-cache usage
|
||||
- adds pressure to Postgres
|
||||
- TTL/cleanup becomes application responsibility
|
||||
- still network/database I/O, so should remain off the `getIfPresent()` hot path
|
||||
|
||||
## Recommendation
|
||||
|
||||
- if the goal is a serious shared L2 cache, Redis is the more natural fit
|
||||
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
|
||||
|
||||
---
|
||||
|
||||
## Versioning / evolution
|
||||
|
||||
Whatever serializer is used, include versioning information.
|
||||
|
||||
Useful dimensions:
|
||||
|
||||
- serializer/schema version
|
||||
- cache implementation version
|
||||
- fetch-group/cache identity version
|
||||
|
||||
This helps when:
|
||||
|
||||
- fields are added/removed
|
||||
- graph shape changes
|
||||
- fetch-group definitions evolve
|
||||
|
||||
---
|
||||
|
||||
## Compression
|
||||
|
||||
If remote snapshots become large:
|
||||
|
||||
- compress only above a size threshold
|
||||
- avoid compressing tiny payloads
|
||||
|
||||
This is especially relevant for JSON in Redis or Postgres L2.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
A multi-level cache should expose at least:
|
||||
|
||||
- L1 hit rate
|
||||
- L2 hit rate
|
||||
- DB loader rate
|
||||
- deserialize failures
|
||||
- invalidation counts
|
||||
- average payload size
|
||||
- cold-start amplification
|
||||
|
||||
Without this, it will be hard to judge whether the remote cache is helping.
|
||||
|
||||
---
|
||||
|
||||
## Overall recommended architecture
|
||||
|
||||
### Recommended model
|
||||
|
||||
- **L1**: actual read-only `EntityBean` instances
|
||||
- **L2**: serialized immutable snapshots
|
||||
- **Loader**: fetch-group-based DB query
|
||||
|
||||
### Method responsibilities
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
|
||||
|
||||
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
|
||||
|
||||
---
|
||||
|
||||
## Bottom line
|
||||
|
||||
If/when multi-level immutable caching is explored, the main points to preserve are:
|
||||
|
||||
1. keep `getIfPresent()` local-only
|
||||
2. do remote work only in batched `getAll()`
|
||||
3. key by type + id + tenant + fetch-group/cache identity
|
||||
4. treat remote values as immutable snapshots
|
||||
5. prefer JSON/self-describing format first
|
||||
6. be cautious with generic binary serializers like Kryo
|
||||
|
||||
---
|
||||
|
||||
## Possible follow-up
|
||||
|
||||
If this becomes active design work later, consider promoting these notes into one of:
|
||||
|
||||
- a dedicated design note under `docs/notes/`
|
||||
- a GitHub issue / discussion for design iteration
|
||||
- a lightweight ADR if this becomes a committed architectural direction
|
||||
+52
-7
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -70,15 +70,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<optional>true</optional>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-core</artifactId>
|
||||
<version>${avaje-json-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope for JsonNode support -->
|
||||
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
@@ -105,6 +103,53 @@
|
||||
</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>
|
||||
|
||||
@@ -457,6 +457,10 @@ 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);
|
||||
|
||||
@@ -23,11 +23,18 @@ import java.util.concurrent.Callable;
|
||||
/**
|
||||
* Provides the API for fetching and saving beans to a particular database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
|
||||
* They can also be automatically constructed on demand using configuration information in
|
||||
* the application.properties file. The underlying implementation is provided by
|
||||
* {@link DatabaseFactory}.
|
||||
*
|
||||
* <h5>Registration with the DB singleton</h5>
|
||||
* <p>
|
||||
* When a Database instance is created it can be registered with the DB
|
||||
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
|
||||
* singleton is essentially a map of Database's that have been registered
|
||||
* When a Database instance is created it can be registered with the {@link DB}
|
||||
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
|
||||
* singleton is essentially a map of {@link Database}'s that have been registered
|
||||
* with it.
|
||||
* <p>
|
||||
* The Database can then be retrieved later via {@link DB#byName(String)}.
|
||||
@@ -35,16 +42,10 @@ import java.util.concurrent.Callable;
|
||||
* <h5>The 'default' Database</h5>
|
||||
* <p>
|
||||
* One Database can be designated as the 'default' or 'primary' Database
|
||||
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
|
||||
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
|
||||
* such as {@link DB#find(Class)} etc are actually just a convenient way to
|
||||
* call methods on the 'default/primary' Database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are constructed by the DatabaseFactory. They can be created
|
||||
* programmatically via {@link DatabaseFactory#create(DatabaseBuilder)} or they
|
||||
* can be automatically constructed on demand using configuration information in
|
||||
* the application.properties file.
|
||||
*
|
||||
* <h5>Example: Get a Database</h5>
|
||||
* <pre>{@code
|
||||
@@ -80,6 +81,7 @@ import java.util.concurrent.Callable;
|
||||
* method. Example: a single thread requires more than one transaction.
|
||||
*
|
||||
* @see DB
|
||||
* @see DatabaseBuilder
|
||||
* @see DatabaseFactory
|
||||
* @see DatabaseConfig
|
||||
*/
|
||||
@@ -94,11 +96,13 @@ public interface Database {
|
||||
* // from application.properties / application.yaml
|
||||
*
|
||||
* Database db = Database.builder()
|
||||
* .name("db")
|
||||
* .loadFromProperties()
|
||||
* .build();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
static DatabaseBuilder builder() {
|
||||
return new DatabaseConfig();
|
||||
}
|
||||
@@ -1074,12 +1078,21 @@ 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.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean);
|
||||
default Set<Property> checkUniqueness(Object bean) {
|
||||
return checkUniqueness(bean, null, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
||||
*/
|
||||
Set<Property> checkUniqueness(Object bean, Transaction 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);
|
||||
|
||||
/**
|
||||
* Marks the entity bean as dirty.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.annotation.*;
|
||||
import io.ebean.cache.ServerCachePlugin;
|
||||
import io.ebean.config.*;
|
||||
@@ -61,6 +61,11 @@ 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();
|
||||
|
||||
@@ -360,18 +365,18 @@ public interface DatabaseBuilder {
|
||||
DatabaseBuilder putServiceObject(Object configObject);
|
||||
|
||||
/**
|
||||
* Set the Jackson JsonFactory to use.
|
||||
* Set the JsonStream to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
|
||||
return setJsonFactory(jsonFactory);
|
||||
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
|
||||
return setJsonStream(jsonStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
|
||||
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
|
||||
*/
|
||||
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
|
||||
DatabaseBuilder setJsonStream(JsonStream jsonStream);
|
||||
|
||||
/**
|
||||
* Set the JSON format to use for DateTime types.
|
||||
@@ -723,19 +728,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
|
||||
|
||||
/**
|
||||
* Set the configuration for profiling.
|
||||
*/
|
||||
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
|
||||
return setProfilingConfig(profilingConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
|
||||
*/
|
||||
@Deprecated
|
||||
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
|
||||
|
||||
/**
|
||||
* Set the suffix appended to the base table to derive the view that contains the union
|
||||
* of the base table and the history table in order to support asOf queries.
|
||||
@@ -896,6 +888,13 @@ 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.
|
||||
*/
|
||||
@@ -995,7 +994,7 @@ public interface DatabaseBuilder {
|
||||
* <p>
|
||||
* Use this to override the default known aggregation functions.
|
||||
*/
|
||||
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
|
||||
/**
|
||||
* Set to true if all DB column and table names should use quoted identifiers.
|
||||
@@ -2234,7 +2233,7 @@ public interface DatabaseBuilder {
|
||||
*
|
||||
* @param includeLabelInSql When true include a SQL inline comment in generated SELECT queries.
|
||||
*/
|
||||
DatabaseConfig includeLabelInSql(boolean includeLabelInSql);
|
||||
DatabaseBuilder includeLabelInSql(boolean includeLabelInSql);
|
||||
|
||||
/**
|
||||
* Set the naming convention to apply to metrics names.
|
||||
@@ -2252,7 +2251,7 @@ public interface DatabaseBuilder {
|
||||
/**
|
||||
* Sets the length check mode.
|
||||
*/
|
||||
DatabaseConfig lengthCheck(LengthCheck lengthCheck);
|
||||
DatabaseBuilder lengthCheck(LengthCheck lengthCheck);
|
||||
|
||||
/**
|
||||
* Provides read access (getters) for the DatabaseBuilder configuration
|
||||
@@ -2267,11 +2266,11 @@ public interface DatabaseBuilder {
|
||||
boolean isAutoLoadModuleInfo();
|
||||
|
||||
/**
|
||||
* Return the Jackson JsonFactory to use.
|
||||
* Return the JsonStream to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
JsonFactory getJsonFactory();
|
||||
JsonStream getJsonStream();
|
||||
|
||||
/**
|
||||
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
|
||||
@@ -2490,11 +2489,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
TenantCatalogProvider getTenantCatalogProvider();
|
||||
|
||||
/**
|
||||
* Return the configuration for profiling.
|
||||
*/
|
||||
ProfilingConfig getProfilingConfig();
|
||||
|
||||
/**
|
||||
* Return the DB schema to use.
|
||||
*/
|
||||
@@ -2581,6 +2575,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isAutoPersistUpdates();
|
||||
|
||||
/**
|
||||
* Return true if caches are partitioned by tenant.
|
||||
*/
|
||||
boolean isTenantPartitionedCache();
|
||||
|
||||
/**
|
||||
* Return the L2 cache default max size.
|
||||
*/
|
||||
|
||||
@@ -8,18 +8,18 @@ import jakarta.persistence.PersistenceException;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Creates Database instances.
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* <p>
|
||||
* This uses either DatabaseConfig or properties in the application.properties file to
|
||||
* configure and create a Database instance.
|
||||
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
|
||||
* This factory remains for legacy creation entry points plus container lifecycle methods.
|
||||
* <p>
|
||||
* The Database instance can either be registered with the DB singleton or
|
||||
* not. The DB singleton effectively holds a map of Database by a name.
|
||||
* If the Database is registered with the DB singleton you can retrieve it
|
||||
* The Database instance can either be registered with the {@link DB} singleton or
|
||||
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
|
||||
* If the Database is registered with the {@link DB} singleton you can retrieve it
|
||||
* later via {@link DB#byName(String)}.
|
||||
* <p>
|
||||
* One Database can be nominated as the 'default/primary' Database. Many
|
||||
* methods on the DB singleton such as {@link DB#find(Class)} are just a
|
||||
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
|
||||
* convenient way of using the 'default/primary' Database.
|
||||
*/
|
||||
public final class DatabaseFactory {
|
||||
@@ -36,7 +36,8 @@ public final class DatabaseFactory {
|
||||
* Initialise the container with clustering configuration.
|
||||
* <p>
|
||||
* Call this prior to creating any Database instances or alternatively set the
|
||||
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
|
||||
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
|
||||
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
|
||||
*/
|
||||
public static void initialiseContainer(ContainerConfig containerConfig) {
|
||||
lock.lock();
|
||||
@@ -48,8 +49,11 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using properties to configure the database.
|
||||
* Create using configuration loaded from properties for the given database name.
|
||||
*
|
||||
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Database create(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -60,34 +64,35 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig object to configure the database.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
* config.setName("db");
|
||||
* config.loadProperties();
|
||||
*
|
||||
* Database database = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
* @deprecated migrate to {@link DatabaseBuilder#build()}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public static Database create(DatabaseBuilder builder) {
|
||||
lock.lock();
|
||||
try {
|
||||
var config = builder.settings();
|
||||
if (config.getName() == null) {
|
||||
var name = config.getName();
|
||||
if (name == 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(config.getName())) {
|
||||
throw new IllegalStateException("Registering [" + config.getName() + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
if (defaultServerName != null && !defaultServerName.equals(name)) {
|
||||
throw new IllegalStateException("Registering [" + name + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
}
|
||||
defaultServerName = config.getName();
|
||||
defaultServerName = name;
|
||||
}
|
||||
DbPrimary.setSkip(true);
|
||||
DbContext.getInstance().register(server, config.isDefaultServer());
|
||||
}
|
||||
return server;
|
||||
@@ -97,7 +102,8 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
|
||||
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
|
||||
* context class loader.
|
||||
*/
|
||||
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
|
||||
lock.lock();
|
||||
@@ -115,6 +121,24 @@ 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,6 +4,7 @@ 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;
|
||||
@@ -75,6 +76,10 @@ final class DbContext {
|
||||
return defaultDatabase;
|
||||
}
|
||||
|
||||
boolean contains(String name) {
|
||||
return concMap.containsKey(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the database by name.
|
||||
*/
|
||||
@@ -92,6 +97,7 @@ final class DbContext {
|
||||
/**
|
||||
* Read, create and put of Databases.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private Database getWithCreate(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -114,6 +120,27 @@ 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 {
|
||||
|
||||
@@ -246,4 +246,41 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
*/
|
||||
DtoQuery<T> usingMaster(boolean useMaster);
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
PagedList<T> findPagedList();
|
||||
|
||||
}
|
||||
|
||||
@@ -205,6 +205,22 @@ 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.
|
||||
*
|
||||
|
||||
@@ -607,6 +607,21 @@ 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>
|
||||
|
||||
@@ -41,6 +41,49 @@ 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>
|
||||
|
||||
@@ -260,6 +260,27 @@ 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>
|
||||
|
||||
@@ -205,4 +205,20 @@ public interface UpdateQuery<T> {
|
||||
*/
|
||||
int update();
|
||||
|
||||
/**
|
||||
* Return the timeout used to execute this statement.
|
||||
*/
|
||||
int getTimeout();
|
||||
|
||||
/**
|
||||
* Set a timeout on this query.
|
||||
* <p>
|
||||
* This will typically result in a call to setQueryTimeout() on a
|
||||
* preparedStatement. If the timeout occurs an exception will be thrown - this
|
||||
* will be a SQLException wrapped up in a PersistenceException.
|
||||
* </p>
|
||||
*
|
||||
* @param secs the query timeout limit in seconds. Zero means there is no limit.
|
||||
*/
|
||||
UpdateQuery<T> setTimeout(int secs);
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public final class InterceptReadOnly extends InterceptBase {
|
||||
|
||||
@Override
|
||||
public boolean isUpdate() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -151,14 +151,16 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package io.ebean.common;
|
||||
|
||||
import io.ebean.bean.BeanCollection;
|
||||
import io.ebean.bean.BeanCollectionLoader;
|
||||
import io.ebean.bean.EntityBean;
|
||||
import io.ebean.bean.ToStringBuilder;
|
||||
import io.ebean.bean.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -17,12 +14,12 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
|
||||
/**
|
||||
* The underlying map implementation.
|
||||
*/
|
||||
private Map<K, E> map;
|
||||
private LinkedHashMap<K, E> map;
|
||||
|
||||
/**
|
||||
* Create with a given Map.
|
||||
*/
|
||||
public BeanMap(Map<K, E> map) {
|
||||
public BeanMap(LinkedHashMap<K, E> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@@ -165,18 +162,23 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the actual underlying map. Used for performing lazy fetch.
|
||||
*/
|
||||
public LinkedHashMap<K, E> collectionAdd() {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setActualMap(Map<?, ?> map) {
|
||||
this.map = (Map<K, E>) map;
|
||||
public void refresh(ModifyListenMode modifyListenMode, BeanMap<?, ?> newMap) {
|
||||
setModifyListening(modifyListenMode);
|
||||
this.map = (LinkedHashMap<K, E>) newMap.actualMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying map.
|
||||
*/
|
||||
public Map<K, E> actualMap() {
|
||||
public LinkedHashMap<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 Set<E> set;
|
||||
private LinkedHashSet<E> set;
|
||||
|
||||
/**
|
||||
* Create with a specific Set implementation.
|
||||
*/
|
||||
public BeanSet(Set<E> set) {
|
||||
public BeanSet(LinkedHashSet<E> set) {
|
||||
this.set = set;
|
||||
}
|
||||
|
||||
@@ -146,18 +146,22 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the underlying set (used for lazy fetch).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setActualSet(Set<?> set) {
|
||||
this.set = (Set<E>) set;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying set.
|
||||
*/
|
||||
public Set<E> actualSet() {
|
||||
public LinkedHashSet<E> actualSet() {
|
||||
return set;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ public class ClassLoadConfig {
|
||||
}
|
||||
|
||||
public boolean isJacksonCorePresent() {
|
||||
return isPresent("com.fasterxml.jackson.core.JsonParser");
|
||||
// Legacy method name retained for compatibility; now checks avaje JSON core.
|
||||
return isPresent("io.avaje.json.JsonReader");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,4 +159,3 @@ public class ClassLoadConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import io.avaje.config.Config;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.*;
|
||||
import io.ebean.annotation.MutationDetection;
|
||||
import io.ebean.annotation.PersistBatch;
|
||||
@@ -31,38 +31,15 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The configuration used for creating a Database.
|
||||
* <p>
|
||||
* Used to programmatically construct an Database and optionally register it
|
||||
* with the DB singleton.
|
||||
* <p>
|
||||
* If you just use DB thout this programmatic configuration Ebean will read
|
||||
* the application.properties file and take the configuration from there. This usually
|
||||
* includes searching the class path and automatically registering any entity
|
||||
* classes and listeners etc.
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
*
|
||||
* // read the ebean.properties and load
|
||||
* // those settings into this DatabaseConfig object
|
||||
* config.loadFromProperties();
|
||||
*
|
||||
* // explicitly register the entity beans to avoid classpath scanning
|
||||
* config.addClass(Customer.class);
|
||||
* config.addClass(User.class);
|
||||
*
|
||||
* Database db = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
* Deprecated migrate to {@link Database#builder()} rather than constructing {@code DatabaseConfig} directly.
|
||||
*
|
||||
* <p>
|
||||
* Note that DatabaseConfigProvider provides a standard Java ServiceLoader mechanism that can
|
||||
* be used to apply configuration to the DatabaseConfig.
|
||||
* Note that {@link DatabaseConfigProvider} provides a standard Java ServiceLoader mechanism that can
|
||||
* be used to apply configuration to the {@link DatabaseBuilder}.
|
||||
*
|
||||
* @author emcgreal
|
||||
* @author rbygrave
|
||||
* @see DatabaseFactory
|
||||
* @see Database#builder()
|
||||
*/
|
||||
public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
|
||||
@@ -443,7 +420,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
* The default PersistenceContextScope used if one is not explicitly set on a query.
|
||||
*/
|
||||
private PersistenceContextScope persistenceContextScope = PersistenceContextScope.TRANSACTION;
|
||||
private JsonFactory jsonFactory;
|
||||
private JsonStream jsonStream;
|
||||
private boolean localTimeWithNanos;
|
||||
private boolean durationWithNanos;
|
||||
private int maxCallStack = 5;
|
||||
@@ -455,6 +432,8 @@ 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;
|
||||
@@ -539,8 +518,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
private SlowQueryListener slowQueryListener;
|
||||
|
||||
private ProfilingConfig profilingConfig = new ProfilingConfig();
|
||||
|
||||
/**
|
||||
* The mappingLocations for searching xml mapping.
|
||||
*/
|
||||
@@ -560,12 +537,14 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
private Function<String, String> metricNaming = MetricNamingMatch.INSTANCE;
|
||||
|
||||
/**
|
||||
* Construct a Database Configuration for programmatically creating an Database.
|
||||
* @deprecated migrate to {@link Database#builder()} and configure the returned {@link DatabaseBuilder}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public DatabaseConfig() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
public Database build() {
|
||||
return DatabaseFactory.create(this);
|
||||
}
|
||||
@@ -654,13 +633,13 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonFactory getJsonFactory() {
|
||||
return jsonFactory;
|
||||
public JsonStream getJsonStream() {
|
||||
return jsonStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setJsonFactory(JsonFactory jsonFactory) {
|
||||
this.jsonFactory = jsonFactory;
|
||||
public DatabaseConfig setJsonStream(JsonStream jsonStream) {
|
||||
this.jsonStream = jsonStream;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1028,17 +1007,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfilingConfig getProfilingConfig() {
|
||||
return profilingConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DatabaseConfig setProfilingConfig(ProfilingConfig profilingConfig) {
|
||||
this.profilingConfig = profilingConfig;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDbSchema() {
|
||||
return dbSchema;
|
||||
@@ -1209,6 +1177,17 @@ 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;
|
||||
@@ -2137,7 +2116,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
|
||||
*/
|
||||
protected void loadSettings(PropertiesWrapper p) {
|
||||
dbSchema = p.get("dbSchema", dbSchema);
|
||||
profilingConfig.loadSettings(p, name);
|
||||
platformConfig.loadSettings(p);
|
||||
if (platformConfig.isAllQuotedIdentifiers()) {
|
||||
adjustNamingConventionForAllQuoted();
|
||||
@@ -2263,6 +2241,15 @@ 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");
|
||||
|
||||
@@ -3,14 +3,14 @@ package io.ebean.config;
|
||||
import io.ebean.DatabaseBuilder;
|
||||
|
||||
/**
|
||||
* Provides a ServiceLoader based mechanism to configure a DatabaseConfig.
|
||||
* Provides a ServiceLoader based mechanism to configure a {@link DatabaseBuilder}.
|
||||
* <p>
|
||||
* Provide an implementation and register it via the standard Java ServiceLoader mechanism
|
||||
* via a file at <code>META-INF/services/io.ebean.config.DatabaseConfigProvider</code>.
|
||||
* </p>
|
||||
* <p>
|
||||
* If you are using a DI container like Spring or Guice you are unlikely to use this but instead use a
|
||||
* spring specific configuration. When we are not using a DI container we may use this mechanism to
|
||||
* spring specific configuration. When we are not using a DI container we may use this mechanism to
|
||||
* explicitly register the entity beans and avoid classpath scanning.
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
@@ -18,7 +18,7 @@ import io.ebean.DatabaseBuilder;
|
||||
* public class EbeanConfigProvider implements DatabaseConfigProvider {
|
||||
*
|
||||
* @Override
|
||||
* public void apply(DatabaseConfig config) {
|
||||
* public void apply(DatabaseBuilder config) {
|
||||
*
|
||||
* // register the entity bean classes explicitly
|
||||
* config.addClass(Customer.class);
|
||||
@@ -32,10 +32,9 @@ import io.ebean.DatabaseBuilder;
|
||||
public interface DatabaseConfigProvider {
|
||||
|
||||
/**
|
||||
* Apply the configuration to the DatabaseConfig.
|
||||
* Apply the configuration to the {@link DatabaseBuilder}.
|
||||
* <p>
|
||||
* Typically we explicitly register entity bean classes and thus avoid classpath scanning.
|
||||
* </p>
|
||||
*/
|
||||
void apply(DatabaseBuilder config);
|
||||
}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
package io.ebean.config;
|
||||
|
||||
/**
|
||||
* Configuration for transaction profiling.
|
||||
*/
|
||||
public class ProfilingConfig {
|
||||
|
||||
/**
|
||||
* When true transaction profiling is enabled.
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* Set true for verbose mode.
|
||||
*/
|
||||
private boolean verbose;
|
||||
|
||||
/**
|
||||
* The minimum transaction execution time to be included in profiling.
|
||||
*/
|
||||
private long minimumMicros;
|
||||
|
||||
/**
|
||||
* A specific set of profileIds to include in profiling.
|
||||
*/
|
||||
private int[] includeProfileIds = {};
|
||||
|
||||
/**
|
||||
* The number of profiles to write per file.
|
||||
*/
|
||||
private long profilesPerFile = 1000;
|
||||
|
||||
private String directory = "profiling";
|
||||
|
||||
/**
|
||||
* Return true if transaction profiling is enabled.
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to enable transaction profiling.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if verbose mode is used.
|
||||
*/
|
||||
public boolean isVerbose() {
|
||||
return verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true to use verbose mode.
|
||||
*/
|
||||
public void setVerbose(boolean verbose) {
|
||||
this.verbose = verbose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minimum transaction execution to be included in profiling.
|
||||
*/
|
||||
public long getMinimumMicros() {
|
||||
return minimumMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum transaction execution to be included in profiling.
|
||||
*/
|
||||
public void setMinimumMicros(long minimumMicros) {
|
||||
this.minimumMicros = minimumMicros;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the specific set of profileIds to include in profiling.
|
||||
* When not set all transactions with profileIds are included.
|
||||
*/
|
||||
public int[] getIncludeProfileIds() {
|
||||
return includeProfileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific set of profileIds to include in profiling.
|
||||
* When not set all transactions with profileIds are included.
|
||||
*/
|
||||
public void setIncludeProfileIds(int[] includeProfileIds) {
|
||||
this.includeProfileIds = includeProfileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of profiles to write to a single file.
|
||||
*/
|
||||
public long getProfilesPerFile() {
|
||||
return profilesPerFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of profiles to write to a single file.
|
||||
*/
|
||||
public void setProfilesPerFile(long profilesPerFile) {
|
||||
this.profilesPerFile = profilesPerFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the directory profiling files are put into.
|
||||
*/
|
||||
public String getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the directory profiling files are put into.
|
||||
*/
|
||||
public void setDirectory(String directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load setting from properties.
|
||||
*/
|
||||
public void loadSettings(PropertiesWrapper p, String name) {
|
||||
|
||||
enabled = p.getBoolean("profiling", enabled);
|
||||
verbose = p.getBoolean("profiling.verbose", verbose);
|
||||
|
||||
directory = p.get("profiling.directory", directory);
|
||||
profilesPerFile = p.getLong("profiling.profilesPerFile", profilesPerFile);
|
||||
minimumMicros = p.getLong("profiling.minimumMicros", minimumMicros);
|
||||
|
||||
String includeIds = p.get("profiling.includeProfileIds");
|
||||
if (includeIds != null) {
|
||||
includeProfileIds = parseIds(includeIds);
|
||||
}
|
||||
}
|
||||
|
||||
private int[] parseIds(String includeIds) {
|
||||
|
||||
String[] ids = includeIds.split(",");
|
||||
int[] vals = new int[ids.length];
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
vals[i] = Integer.parseInt(ids[i]);
|
||||
}
|
||||
return vals;
|
||||
}
|
||||
}
|
||||
@@ -162,6 +162,18 @@ 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().
|
||||
@@ -660,6 +672,21 @@ 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,10 +11,12 @@ 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.NavigableSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -23,18 +25,36 @@ 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");
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
/**
|
||||
* Buffer key used when there is no current tenant (single-tenant or no tenant in scope).
|
||||
*/
|
||||
private static final Object SINGLE = new Object();
|
||||
|
||||
protected final String seqName;
|
||||
protected final DataSource dataSource;
|
||||
protected final BackgroundExecutor backgroundExecutor;
|
||||
protected final NavigableSet<Long> idList = new TreeSet<>();
|
||||
protected final int allocationSize;
|
||||
protected AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct given a dataSource and sql to return the next sequence value.
|
||||
@@ -44,6 +64,7 @@ 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);
|
||||
@@ -64,6 +85,24 @@ 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>
|
||||
@@ -78,23 +117,22 @@ 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) {
|
||||
lock.lock();
|
||||
Object tenantKey = currentTenantKey();
|
||||
TenantBuffer buffer = buffer(tenantKey);
|
||||
buffer.lock.lock();
|
||||
try {
|
||||
int size = idList.size();
|
||||
int size = buffer.idList.size();
|
||||
if (size > 0) {
|
||||
maybeLoadMoreInBackground(size);
|
||||
} else {
|
||||
loadMore(allocationSize);
|
||||
loadMore(tenantKey, buffer, allocationSize);
|
||||
}
|
||||
return idList.pollFirst();
|
||||
return buffer.idList.poll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
buffer.lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,29 +144,36 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMore(int requestSize) {
|
||||
List<Long> newIds = getMoreIds(requestSize);
|
||||
lock.lock();
|
||||
private void loadMore(Object tenantKey, TenantBuffer buffer, int requestSize) {
|
||||
List<Long> newIds = getMoreIds(tenantKey, requestSize);
|
||||
buffer.lock.lock();
|
||||
try {
|
||||
idList.addAll(newIds);
|
||||
buffer.idList.addAll(newIds);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
buffer.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) {
|
||||
if (currentlyBackgroundLoading.get()) {
|
||||
final Object tenantKey = currentTenantKey();
|
||||
final TenantBuffer buffer = buffer(tenantKey);
|
||||
if (!buffer.currentlyBackgroundLoading.compareAndSet(false, true)) {
|
||||
// skip as already background loading
|
||||
log.log(DEBUG, "... skip background sequence load (another load in progress)");
|
||||
return;
|
||||
}
|
||||
currentlyBackgroundLoading.set(true);
|
||||
backgroundExecutor.execute(() -> {
|
||||
loadMore(requestSize);
|
||||
currentlyBackgroundLoading.set(false);
|
||||
try {
|
||||
loadMore(tenantKey, buffer, requestSize);
|
||||
} finally {
|
||||
buffer.currentlyBackgroundLoading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,7 +185,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(int requestSize) {
|
||||
protected List<Long> getMoreIds(Object tenantKey, int requestSize) {
|
||||
|
||||
String sql = getSql(requestSize);
|
||||
|
||||
@@ -148,7 +193,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
|
||||
PreparedStatement statement = null;
|
||||
ResultSet resultSet = null;
|
||||
try {
|
||||
connection = dataSource.getConnection();
|
||||
connection = connectionFor(tenantKey);
|
||||
|
||||
statement = connection.prepareStatement(sql);
|
||||
resultSet = statement.executeQuery();
|
||||
@@ -174,6 +219,17 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
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,6 +1,7 @@
|
||||
package io.ebean.event;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.DatabaseFactory;
|
||||
import io.ebean.EbeanVersion;
|
||||
import io.ebean.service.SpiContainer;
|
||||
|
||||
@@ -188,6 +189,7 @@ public final class ShutdownManager {
|
||||
*/
|
||||
public static void unregisterDatabase(Database server) {
|
||||
databases.remove(server);
|
||||
DatabaseFactory.unregister(server);
|
||||
}
|
||||
|
||||
private static class ShutdownHook extends Thread {
|
||||
|
||||
@@ -10,12 +10,23 @@ public interface MetaInfoManager {
|
||||
/**
|
||||
* Return the metrics for the database instance.
|
||||
* <p>
|
||||
* This will reset the metrics (reset counters back to zero etc) and
|
||||
* will only return the non-empty metrics.
|
||||
* This is equivalent to {@link #collectMetrics(boolean)} with reset set to true.
|
||||
* It will reset the metrics (reset counters back to zero etc) and will only return
|
||||
* the non-empty metrics.
|
||||
* </p>
|
||||
*/
|
||||
ServerMetrics collectMetrics();
|
||||
|
||||
/**
|
||||
* Return the metrics for the database instance using the given reset behavior.
|
||||
* <p>
|
||||
* When reset is false, count and total values remain cumulative between collections.
|
||||
* </p>
|
||||
*/
|
||||
default ServerMetrics collectMetrics(boolean reset) {
|
||||
return collectMetrics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit the metrics resetting and collecting/reporting as desired.
|
||||
*/
|
||||
|
||||
@@ -44,6 +44,11 @@ public interface MetaQueryPlan {
|
||||
*/
|
||||
String plan();
|
||||
|
||||
/**
|
||||
* The tenant ID of the plan.
|
||||
*/
|
||||
Object tenantId();
|
||||
|
||||
/**
|
||||
* Return the query execution time associated with the bind values capture.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Canonical "v2" mapping of Ebean's internal flat metric names (e.g.
|
||||
* {@code orm.Customer.findList}, {@code iud.User.save}, {@code txn.named.X},
|
||||
* {@code l2.<region>.<op>}) into a metric family name plus a tag string following
|
||||
* the label-tag convention.
|
||||
*
|
||||
* <p>This is the source-of-truth mapping for the v2 metrics JSON form
|
||||
* ({@link ServerMetricsAsJson#writeV2(Appendable)}). The tag string is a canonical,
|
||||
* sorted, comma separated list of {@code key:value} pairs, e.g.
|
||||
* {@code "kind:orm,label:Customer.findList,type:Customer"}.
|
||||
*
|
||||
* <table>
|
||||
* <caption>Ebean prefix → family name + tags</caption>
|
||||
* <tr><th>Ebean prefix</th><th>name</th><th>tags</th></tr>
|
||||
* <tr><td>{@code iud.X}</td><td>{@code ebean.dml}</td><td>{@code label=X}</td></tr>
|
||||
* <tr><td>{@code orm.X}</td><td>{@code ebean.query}</td><td>{@code kind=orm, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code dto.X}</td><td>{@code ebean.query}</td><td>{@code kind=dto, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code sql.X}</td><td>{@code ebean.query}</td><td>{@code kind=sql, type=<bean>, label=X}</td></tr>
|
||||
* <tr><td>{@code txn.named.X} / {@code txn.X}</td><td>{@code ebean.txn}</td><td>{@code label=X}</td></tr>
|
||||
* <tr><td>{@code l2.<region>.<op>}</td><td>{@code ebean.l2}</td><td>{@code op=<op>, region=<region>}</td></tr>
|
||||
* <tr><td>(unrecognised)</td><td>{@code ebean.other}</td><td>{@code label=<original name>}</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>The {@code kind} tag is the query category (orm/dto/sql) while the {@code type}
|
||||
* tag is the queried bean/entity simple name. The {@code type} tag is omitted when
|
||||
* the bean type is unknown.
|
||||
*/
|
||||
final class MetricNamingV2 {
|
||||
|
||||
/** Result of a name mapping: family name plus canonical tag string. */
|
||||
static final class Mapped {
|
||||
private final String name;
|
||||
private final String tags;
|
||||
|
||||
Mapped(String name, String tags) {
|
||||
this.name = name;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
String tags() {
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
private MetricNamingV2() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an Ebean flat metric name (and optional bean type for query metrics) into
|
||||
* the canonical family name plus tag string.
|
||||
*/
|
||||
static Mapped map(String ebeanName, String beanType) {
|
||||
if (ebeanName == null || ebeanName.isEmpty()) {
|
||||
return new Mapped("ebean.other", "");
|
||||
}
|
||||
int firstDot = ebeanName.indexOf('.');
|
||||
if (firstDot <= 0) {
|
||||
return new Mapped("ebean.other", tags("label", ebeanName));
|
||||
}
|
||||
String prefix = ebeanName.substring(0, firstDot);
|
||||
String rest = ebeanName.substring(firstDot + 1);
|
||||
switch (prefix) {
|
||||
case "iud":
|
||||
return new Mapped("ebean.dml", tags("label", rest));
|
||||
case "orm":
|
||||
return query("orm", rest, beanType);
|
||||
case "dto":
|
||||
return query("dto", rest, beanType);
|
||||
case "sql":
|
||||
return query("sql", rest, beanType);
|
||||
case "txn":
|
||||
String txnLabel = rest.startsWith("named.") ? rest.substring("named.".length()) : rest;
|
||||
return new Mapped("ebean.txn", tags("label", txnLabel));
|
||||
case "l2":
|
||||
return l2(rest);
|
||||
default:
|
||||
return new Mapped("ebean.other", tags("label", ebeanName));
|
||||
}
|
||||
}
|
||||
|
||||
private static Mapped query(String kind, String label, String beanType) {
|
||||
if (beanType == null || beanType.isEmpty()) {
|
||||
return new Mapped("ebean.query", tags("kind", kind, "label", label));
|
||||
}
|
||||
return new Mapped("ebean.query", tags("kind", kind, "type", beanType, "label", label));
|
||||
}
|
||||
|
||||
private static Mapped l2(String rest) {
|
||||
int dot = rest.indexOf('.');
|
||||
if (dot <= 0) {
|
||||
return new Mapped("ebean.l2", tags("op", rest));
|
||||
}
|
||||
String region = rest.substring(0, dot);
|
||||
String op = rest.substring(dot + 1);
|
||||
return new Mapped("ebean.l2", tags("op", op, "region", region));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a canonical (sorted) {@code key:value,key2:value2} tag string from the given
|
||||
* key/value pairs, skipping null/empty values and sanitising the reserved
|
||||
* delimiter characters from values.
|
||||
*/
|
||||
private static String tags(String... keyValues) {
|
||||
List<String> pairs = new ArrayList<>(keyValues.length / 2);
|
||||
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
||||
String value = keyValues[i + 1];
|
||||
if (value != null && !value.isEmpty()) {
|
||||
pairs.add(keyValues[i] + ':' + sanitize(value));
|
||||
}
|
||||
}
|
||||
Collections.sort(pairs);
|
||||
return String.join(",", pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the reserved tag delimiter characters ({@code ,} and {@code :}) so they
|
||||
* cannot break the {@code key:value,key2:value2} encoding.
|
||||
*/
|
||||
private static String sanitize(String value) {
|
||||
if (value.indexOf(',') < 0 && value.indexOf(':') < 0) {
|
||||
return value;
|
||||
}
|
||||
return value.replace(',', '_').replace(':', '_');
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
private Comparator<MetaTimedMetric> sortBy = SortMetric.NAME;
|
||||
private int listCounter;
|
||||
private int objKeyCounter;
|
||||
private boolean v2;
|
||||
|
||||
MetricsAsJson(ServerMetrics metrics) {
|
||||
this.metrics = metrics;
|
||||
@@ -67,6 +68,13 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
collect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeV2(Appendable buffer) {
|
||||
this.v2 = true;
|
||||
this.writer = buffer;
|
||||
collect();
|
||||
}
|
||||
|
||||
private void collect() {
|
||||
try {
|
||||
start();
|
||||
@@ -151,12 +159,26 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
}
|
||||
|
||||
private void metricStart(MetaMetric metric) throws IOException {
|
||||
metricStart(metric, null);
|
||||
}
|
||||
|
||||
private void metricStart(MetaMetric metric, String beanType) throws IOException {
|
||||
if (listCounter++ > 0) {
|
||||
writer.append(',').append(newLine);
|
||||
}
|
||||
objStart();
|
||||
key("name");
|
||||
val(metric.name());
|
||||
if (v2) {
|
||||
MetricNamingV2.Mapped mapped = MetricNamingV2.map(metric.name(), beanType);
|
||||
key("name");
|
||||
val(mapped.name());
|
||||
if (!mapped.tags().isEmpty()) {
|
||||
key("tags");
|
||||
val(mapped.tags());
|
||||
}
|
||||
} else {
|
||||
key("name");
|
||||
val(metric.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void metricEnd() throws IOException {
|
||||
@@ -180,7 +202,8 @@ final class MetricsAsJson implements ServerMetricsAsJson {
|
||||
}
|
||||
|
||||
private void logQuery(MetaQueryMetric metric) throws IOException {
|
||||
metricStart(metric);
|
||||
Class<?> beanType = metric.type();
|
||||
metricStart(metric, beanType == null ? null : beanType.getSimpleName());
|
||||
appendTiming(metric);
|
||||
if (withHash) {
|
||||
append("hash", metric.hash());
|
||||
|
||||
@@ -41,6 +41,17 @@ public interface ServerMetricsAsJson {
|
||||
*/
|
||||
void write(Appendable buffer);
|
||||
|
||||
/**
|
||||
* Collect and write metrics as "v2" JSON to the given buffer.
|
||||
* <p>
|
||||
* The v2 form uses the canonical label-tag convention: each metric is written with
|
||||
* a family {@code name} (e.g. {@code ebean.query}, {@code ebean.dml}) plus a
|
||||
* {@code tags} string of sorted {@code key:value} pairs (e.g.
|
||||
* {@code "kind:orm,label:Customer.findList,type:Customer"}) rather than the flat
|
||||
* prefixed name. Timing, hash, location and sql attributes are unchanged.
|
||||
*/
|
||||
void writeV2(Appendable buffer);
|
||||
|
||||
/**
|
||||
* Return the metrics in raw JSON.
|
||||
*/
|
||||
|
||||
@@ -34,6 +34,6 @@ public interface MetricFactory extends BootstrapService {
|
||||
/**
|
||||
* Create a Timed metric.
|
||||
*/
|
||||
QueryPlanMetric createQueryPlanMetric(Class<?> type, String label, ProfileLocation profileLocation, String sql);
|
||||
QueryPlanMetric createQueryPlanMetric(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.ebean.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonReader.Token;
|
||||
import io.avaje.json.JsonWriter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
@@ -32,12 +32,12 @@ public interface SpiJsonService extends BootstrapService {
|
||||
/**
|
||||
* Write the nested Map/List as json to the jsonGenerator.
|
||||
*/
|
||||
void write(Object object, JsonGenerator jsonGenerator) throws IOException;
|
||||
void write(Object object, JsonWriter jsonGenerator) throws IOException;
|
||||
|
||||
/**
|
||||
* Write the collection as json array to the jsonGenerator.
|
||||
*/
|
||||
void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException;
|
||||
void writeCollection(Collection<Object> collection, JsonWriter jsonGenerator) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map additionally specifying if the returned map should
|
||||
@@ -61,17 +61,17 @@ public interface SpiJsonService extends BootstrapService {
|
||||
Map<String, Object> parseObject(Reader reader) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser.
|
||||
* Parse the json and return as a Map taking a JsonReader.
|
||||
*/
|
||||
Map<String, Object> parseObject(JsonParser parser) throws IOException;
|
||||
Map<String, Object> parseObject(JsonReader parser) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser and a starting token.
|
||||
* Parse the json and return as a Map taking a JsonReader and a starting token.
|
||||
* <p>
|
||||
* Used when the first token is checked to see if the value is null prior to calling this.
|
||||
* </p>
|
||||
*/
|
||||
Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException;
|
||||
Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a modify aware List.
|
||||
@@ -89,14 +89,14 @@ public interface SpiJsonService extends BootstrapService {
|
||||
List<Object> parseList(Reader reader) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List taking a JsonParser.
|
||||
* Parse the json and return as a List taking a JsonReader.
|
||||
*/
|
||||
List<Object> parseList(JsonParser parser) throws IOException;
|
||||
List<Object> parseList(JsonReader parser) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json returning as a List taking into account the current token.
|
||||
*/
|
||||
<T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException;
|
||||
<T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
@@ -111,7 +111,7 @@ public interface SpiJsonService extends BootstrapService {
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
*/
|
||||
Object parse(JsonParser parser) throws IOException;
|
||||
Object parse(JsonReader parser) throws IOException;
|
||||
|
||||
/**
|
||||
* Parse the json returning a Set that might be modify aware.
|
||||
@@ -121,5 +121,5 @@ public interface SpiJsonService extends BootstrapService {
|
||||
/**
|
||||
* Parse the json returning as a Set taking into account the current token.
|
||||
*/
|
||||
<T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException;
|
||||
<T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,13 @@ public interface SpiRawSqlService extends BootstrapService {
|
||||
*/
|
||||
RawSqlBuilder unparsed(String sql);
|
||||
|
||||
/**
|
||||
* SQL with ${where}/${having} placeholder(s) but no SELECT column parsing.
|
||||
* Supports complex SQL (CTEs, window functions) where keyword parsing would fail.
|
||||
* Explicit column mapping is required (as with unparsed).
|
||||
*/
|
||||
RawSqlBuilder withPlaceholders(String sql);
|
||||
|
||||
/**
|
||||
* Create based on a JDBC ResultSet.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package io.ebean.text.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonReader.Token;
|
||||
import io.ebean.XBootstrapService;
|
||||
import io.ebean.service.SpiJsonService;
|
||||
|
||||
@@ -38,14 +37,14 @@ public class EJson {
|
||||
/**
|
||||
* Write the nested Map/List as json to the jsonGenerator.
|
||||
*/
|
||||
public static void write(Object object, JsonGenerator jsonGenerator) throws IOException {
|
||||
public static void write(Object object, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
|
||||
plugin.write(object, jsonGenerator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the collection as json array to the jsonGenerator.
|
||||
*/
|
||||
public static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
|
||||
public static void writeCollection(Collection<Object> collection, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
|
||||
plugin.writeCollection(collection, jsonGenerator);
|
||||
}
|
||||
|
||||
@@ -79,19 +78,19 @@ public class EJson {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser.
|
||||
* Parse the json and return as a Map taking a JsonReader.
|
||||
*/
|
||||
public static Map<String, Object> parseObject(JsonParser parser) throws IOException {
|
||||
public static Map<String, Object> parseObject(JsonReader parser) throws IOException {
|
||||
return plugin.parseObject(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser and a starting token.
|
||||
* Parse the json and return as a Map taking a JsonReader and a starting token.
|
||||
* <p>
|
||||
* Used when the first token is checked to see if the value is null prior to calling this.
|
||||
* </p>
|
||||
*/
|
||||
public static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
|
||||
public static Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
|
||||
return plugin.parseObject(parser, token);
|
||||
}
|
||||
|
||||
@@ -117,16 +116,16 @@ public class EJson {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List taking a JsonParser.
|
||||
* Parse the json and return as a List taking a JsonReader.
|
||||
*/
|
||||
public static List<Object> parseList(JsonParser parser) throws IOException {
|
||||
public static List<Object> parseList(JsonReader parser) throws IOException {
|
||||
return plugin.parseList(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json returning as a List taking into account the current token.
|
||||
*/
|
||||
public static <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
|
||||
public static <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
|
||||
return plugin.parseList(parser, currentToken);
|
||||
}
|
||||
|
||||
@@ -147,7 +146,7 @@ public class EJson {
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
*/
|
||||
public static Object parse(JsonParser parser) throws IOException {
|
||||
public static Object parse(JsonReader parser) throws IOException {
|
||||
return plugin.parse(parser);
|
||||
}
|
||||
|
||||
@@ -161,7 +160,7 @@ public class EJson {
|
||||
/**
|
||||
* Parse the json returning as a Set taking into account the current token.
|
||||
*/
|
||||
public static <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
|
||||
public static <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
|
||||
return plugin.parseSet(parser, currentToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean.text.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.ebean.bean.PersistenceContext;
|
||||
|
||||
/**
|
||||
@@ -25,9 +25,9 @@ public interface JsonBeanReader<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new reader taking the context from the existing one but using a new JsonParser.
|
||||
* Create a new reader taking the context from the existing one but using a new JsonReader.
|
||||
*/
|
||||
JsonBeanReader<T> forJson(JsonParser moreJson);
|
||||
JsonBeanReader<T> forJson(JsonReader moreJson);
|
||||
|
||||
/**
|
||||
* Add a bean explicitly to the persistence context.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.ebean.text.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.ebean.FetchPath;
|
||||
import io.ebean.plugin.BeanType;
|
||||
|
||||
@@ -49,14 +48,14 @@ public interface JsonContext {
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> T toBean(Class<T> cls, JsonParser parser) throws JsonIOException;
|
||||
<T> T toBean(Class<T> cls, JsonReader parser) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Convert json parser input into a Bean of a specific type additionally using JsonReadOptions..
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
|
||||
<T> T toBean(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Read json parser input into a given Bean. <br>
|
||||
@@ -65,19 +64,19 @@ public interface JsonContext {
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> void toBean(T target, JsonParser parser) throws JsonIOException;
|
||||
<T> void toBean(T target, JsonReader parser) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Read json parser input into a given Bean additionally using JsonReadOptions.<br>
|
||||
* See {@link #toBean(Class, JsonParser)} for details modified.
|
||||
* See {@link #toBean(Class, JsonReader)} for details modified.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException;
|
||||
<T> void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Read json reader input into a given Bean.<br>
|
||||
* See {@link #toBean(Class, JsonParser)} for details
|
||||
* See {@link #toBean(Class, JsonReader)} for details
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
@@ -85,7 +84,7 @@ public interface JsonContext {
|
||||
|
||||
/**
|
||||
* Read json reader input into a given Bean additionally using JsonReadOptions.<br>
|
||||
* See {@link #toBean(Class, JsonParser)} for details modified.
|
||||
* See {@link #toBean(Class, JsonReader)} for details modified.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
@@ -93,7 +92,7 @@ public interface JsonContext {
|
||||
|
||||
/**
|
||||
* Read json string input into a given Bean.<br>
|
||||
* See {@link #toBean(Class, JsonParser)} for details
|
||||
* See {@link #toBean(Class, JsonReader)} for details
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
@@ -101,7 +100,7 @@ public interface JsonContext {
|
||||
|
||||
/**
|
||||
* Read json string input into a given Bean additionally using JsonReadOptions.<br>
|
||||
* See {@link #toBean(Class, JsonParser)} for details
|
||||
* See {@link #toBean(Class, JsonReader)} for details
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
@@ -113,7 +112,7 @@ public interface JsonContext {
|
||||
* Note that JsonOption provides an option for setting a persistence context and also enabling further lazy loading. Further lazy
|
||||
* loading requires a persistence context so if that is set on then a persistence context is created if there is not one set.
|
||||
*/
|
||||
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
|
||||
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Create and return a new bean reading for the bean type given the JSON options and source.
|
||||
@@ -122,7 +121,7 @@ public interface JsonContext {
|
||||
* further lazy loading. Further lazy loading requires a persistence context so if that is set
|
||||
* on then a persistence context is created if there is not one set.
|
||||
*/
|
||||
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException;
|
||||
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Convert json string input into a list of beans of a specific type.
|
||||
@@ -157,14 +156,14 @@ public interface JsonContext {
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> List<T> toList(Class<T> cls, JsonParser json) throws JsonIOException;
|
||||
<T> List<T> toList(Class<T> cls, JsonReader json) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Convert json parser input into a list of beans of a specific type additionally using JsonReadOptions.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
<T> List<T> toList(Class<T> cls, JsonParser json, JsonReadOptions options) throws JsonIOException;
|
||||
<T> List<T> toList(Class<T> cls, JsonReader json, JsonReadOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Use the genericType to determine if this should be converted into a List or
|
||||
@@ -188,7 +187,7 @@ public interface JsonContext {
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException;
|
||||
Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Return the bean or collection as JSON string.
|
||||
@@ -212,11 +211,11 @@ public interface JsonContext {
|
||||
void toJson(Object value, Writer writer) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Write the bean or collection to the JsonGenerator.
|
||||
* Write the bean or collection to the JsonWriter.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
void toJson(Object value, JsonGenerator generator) throws JsonIOException;
|
||||
void toJson(Object value, io.avaje.json.JsonWriter generator) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Return the bean or collection as JSON string using FetchPath.
|
||||
@@ -231,15 +230,15 @@ public interface JsonContext {
|
||||
void toJson(Object value, Writer writer, FetchPath fetchPath) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Write the bean or collection to the JsonGenerator using the FetchPath.
|
||||
* Write the bean or collection to the JsonWriter using the FetchPath.
|
||||
*/
|
||||
void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException;
|
||||
void toJson(Object value, io.avaje.json.JsonWriter generator, FetchPath fetchPath) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Deprecated in favour of using PathProperties by itself.
|
||||
* Write json to the JsonGenerator using the JsonWriteOptions.
|
||||
* Write json to the JsonWriter using the JsonWriteOptions.
|
||||
*/
|
||||
void toJson(Object value, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException;
|
||||
void toJson(Object value, io.avaje.json.JsonWriter generator, JsonWriteOptions options) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Deprecated in favour of using PathProperties by itself.
|
||||
@@ -264,27 +263,27 @@ public interface JsonContext {
|
||||
boolean isSupportedType(Type genericType);
|
||||
|
||||
/**
|
||||
* Create and return a new JsonGenerator for the given writer.
|
||||
* Create and return a new JsonWriter for the given writer.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
JsonGenerator createGenerator(Writer writer) throws JsonIOException;
|
||||
io.avaje.json.JsonWriter createGenerator(Writer writer) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Create and return a new JsonParser for the given reader.
|
||||
* Create and return a new JsonReader for the given reader.
|
||||
*
|
||||
* @throws JsonIOException When IOException occurs
|
||||
*/
|
||||
JsonParser createParser(Reader reader) throws JsonIOException;
|
||||
JsonReader createParser(Reader reader) throws JsonIOException;
|
||||
|
||||
/**
|
||||
* Write a scalar types known to Ebean to Jackson.
|
||||
* Write scalar types known to Ebean to JsonWriter.
|
||||
* <p>
|
||||
* Ebean has built in support for java8 and Joda types as well as the other
|
||||
* standard JDK types like URI, URL, UUID etc. This is a fast simple way to
|
||||
* write any of those types to Jackson.
|
||||
* write any of those types.
|
||||
* </p>
|
||||
*/
|
||||
void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException;
|
||||
void writeScalar(io.avaje.json.JsonWriter generator, Object scalarValue) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
package io.ebean.text.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Wraps an underlying JsonGenerator taking into account null suppression and exposing isIncludeEmpty() etc.
|
||||
* Wraps an underlying JsonWriter taking into account null suppression and exposing isIncludeEmpty() etc.
|
||||
*/
|
||||
public interface JsonWriter {
|
||||
|
||||
/**
|
||||
* Return the Jackson core JsonGenerator.
|
||||
* Return the underlying JsonWriter.
|
||||
*/
|
||||
JsonGenerator gen();
|
||||
io.avaje.json.JsonWriter gen();
|
||||
|
||||
/**
|
||||
* Return true if null values should be included in JSON output.
|
||||
|
||||
@@ -8,6 +8,7 @@ module io.ebean.api {
|
||||
|
||||
requires transitive java.sql;
|
||||
requires transitive io.avaje.config;
|
||||
requires transitive io.avaje.json;
|
||||
requires transitive org.jspecify;
|
||||
requires transitive jakarta.persistence.api;
|
||||
requires transitive io.ebean.annotation;
|
||||
@@ -16,7 +17,6 @@ module io.ebean.api {
|
||||
|
||||
requires static org.slf4j;
|
||||
requires static io.ebean.types;
|
||||
requires static com.fasterxml.jackson.core;
|
||||
requires static com.fasterxml.jackson.databind;
|
||||
|
||||
exports io.ebean;
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
package io.ebean.common;
|
||||
|
||||
import io.ebean.bean.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Map capable of lazy loading and modification aware.
|
||||
*/
|
||||
public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements SequencedMap<K, E> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* The underlying map implementation.
|
||||
*/
|
||||
private LinkedHashMap<K, E> map;
|
||||
|
||||
/**
|
||||
* Create with a given Map.
|
||||
*/
|
||||
public BeanMap(LinkedHashMap<K, E> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using a underlying LinkedHashMap.
|
||||
*/
|
||||
public BeanMap() {
|
||||
this(new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public BeanMap(BeanCollectionLoader ebeanServer, EntityBean ownerBean, String propertyName) {
|
||||
super(ebeanServer, ownerBean, propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<K, E> freeze() {
|
||||
return map == null ? null : Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toString(ToStringBuilder builder) {
|
||||
if (map == null || map.isEmpty()) {
|
||||
builder.addRaw("{}");
|
||||
} else {
|
||||
builder.addRaw("{");
|
||||
for (Entry<K, E> entry : map.entrySet()) {
|
||||
builder.add(String.valueOf(entry.getKey()), entry.getValue());
|
||||
}
|
||||
builder.addRaw("}");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(EntityBean ownerBean, String propertyName) {
|
||||
this.ownerBean = ownerBean;
|
||||
this.propertyName = propertyName;
|
||||
this.map = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkipSave() {
|
||||
return map == null || (map.isEmpty() && !holdsModifications());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void loadFrom(BeanCollection<?> other) {
|
||||
BeanMap<K, E> otherMap = (BeanMap<K, E>) other;
|
||||
internalPutNull();
|
||||
map.putAll(otherMap.actualMap());
|
||||
}
|
||||
|
||||
public void internalPutNull() {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void internalPut(Object key, Object bean) {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
if (key != null) {
|
||||
map.put((K) key, (E) bean);
|
||||
}
|
||||
}
|
||||
|
||||
public void internalPutWithCheck(Object key, Object bean) {
|
||||
if (map == null || key == null || !map.containsKey(key)) {
|
||||
internalPut(key, bean);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void internalAddWithCheck(Object bean) {
|
||||
throw new RuntimeException("Not allowed for map");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void internalAdd(Object bean) {
|
||||
throw new RuntimeException("Not allowed for map");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the underlying map has been populated. Returns false if it
|
||||
* has a deferred fetch pending.
|
||||
*/
|
||||
@Override
|
||||
public boolean isPopulated() {
|
||||
return map != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a reference (lazy loading) bean collection. This is
|
||||
* the same as !isPopulated();
|
||||
*/
|
||||
@Override
|
||||
public boolean isReference() {
|
||||
return map == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEmptyLazyLoad() {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void initClear() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (map == null) {
|
||||
if (!disableLazyLoad && modifyListening) {
|
||||
lazyLoadCollection(true);
|
||||
} else {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (map == null) {
|
||||
if (disableLazyLoad) {
|
||||
map = new LinkedHashMap<>();
|
||||
} else {
|
||||
lazyLoadCollection(false);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public LinkedHashMap<K, E> collectionAdd() {
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void refresh(ModifyListenMode modifyListenMode, BeanMap<?, ?> newMap) {
|
||||
setModifyListening(modifyListenMode);
|
||||
this.map = (LinkedHashMap<K, E>) newMap.actualMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying map.
|
||||
*/
|
||||
public LinkedHashMap<K, E> actualMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collection of beans (map values).
|
||||
*/
|
||||
@Override
|
||||
public Collection<E> actualDetails() {
|
||||
return map.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the map entrySet.
|
||||
*/
|
||||
@Override
|
||||
public Collection<?> actualEntries() {
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (map == null) {
|
||||
return "BeanMap<deferred>";
|
||||
} else {
|
||||
return map.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal if object is a Map and equal in a Map sense.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
init();
|
||||
return map.equals(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
init();
|
||||
return map.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
initClear();
|
||||
if (modifyListening) {
|
||||
// add all beans to the removal list
|
||||
for (E bean : map.values()) {
|
||||
modifyRemoval(bean);
|
||||
}
|
||||
}
|
||||
map.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
init();
|
||||
return map.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object value) {
|
||||
init();
|
||||
return map.containsValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<K, E>> entrySet() {
|
||||
init();
|
||||
return modifyListening ? new ModifyEntrySet<>(this, map.entrySet()) : map.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E get(Object key) {
|
||||
init();
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
init();
|
||||
return map.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<K> keySet() {
|
||||
init();
|
||||
return modifyListening ? new ModifyKeySet<>(this, map.keySet()) : map.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E put(K key, E value) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E oldBean = map.put(key, value);
|
||||
if (value != oldBean) {
|
||||
// register the add of the new and the removal of the old
|
||||
modifyAddition(value);
|
||||
modifyRemoval(oldBean);
|
||||
}
|
||||
return oldBean;
|
||||
} else {
|
||||
return map.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends K, ? extends E> puts) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
for (Entry<? extends K, ? extends E> entry : puts.entrySet()) {
|
||||
Object oldBean = map.put(entry.getKey(), entry.getValue());
|
||||
if (entry.getValue() != oldBean) {
|
||||
modifyAddition(entry.getValue());
|
||||
modifyRemoval(oldBean);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
map.putAll(puts);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBean(E bean) {
|
||||
throw new UnsupportedOperationException("Method not allowed on Map. Please use List instead.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeBean(E bean) {
|
||||
throw new UnsupportedOperationException("Method not allowed on Map. Please use List instead.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public E remove(Object key) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E o = map.remove(key);
|
||||
modifyRemoval(o);
|
||||
return o;
|
||||
}
|
||||
return map.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
init();
|
||||
return map.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<E> values() {
|
||||
init();
|
||||
return modifyListening ? new ModifyCollection<>(this, map.values()) : map.values();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------//
|
||||
// SequencedMap (Java 21+)
|
||||
// -----------------------------------------------------//
|
||||
|
||||
@Override
|
||||
public SequencedMap<K, E> reversed() {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
throw new UnsupportedOperationException("Not supported on modify listening map");
|
||||
}
|
||||
return map.reversed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<K, E> firstEntry() {
|
||||
init();
|
||||
return map.firstEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<K, E> lastEntry() {
|
||||
init();
|
||||
return map.lastEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<K, E> pollFirstEntry() {
|
||||
init();
|
||||
Entry<K, E> entry = map.pollFirstEntry();
|
||||
if (modifyListening && entry != null) {
|
||||
modifyRemoval(entry.getValue());
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<K, E> pollLastEntry() {
|
||||
init();
|
||||
Entry<K, E> entry = map.pollLastEntry();
|
||||
if (modifyListening && entry != null) {
|
||||
modifyRemoval(entry.getValue());
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E putFirst(K key, E value) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E oldBean = map.putFirst(key, value);
|
||||
if (value != oldBean) {
|
||||
// register the add of the new and the removal of the old
|
||||
modifyAddition(value);
|
||||
modifyRemoval(oldBean);
|
||||
}
|
||||
return oldBean;
|
||||
} else {
|
||||
return map.putFirst(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public E putLast(K key, E value) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E oldBean = map.putLast(key, value);
|
||||
if (value != oldBean) {
|
||||
// register the add of the new and the removal of the old
|
||||
modifyAddition(value);
|
||||
modifyRemoval(oldBean);
|
||||
}
|
||||
return oldBean;
|
||||
} else {
|
||||
return map.putLast(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SequencedSet<K> sequencedKeySet() {
|
||||
init();
|
||||
return map.sequencedKeySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SequencedCollection<E> sequencedValues() {
|
||||
init();
|
||||
return map.sequencedValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SequencedSet<Entry<K, E>> sequencedEntrySet() {
|
||||
init();
|
||||
return map.sequencedEntrySet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
package io.ebean.common;
|
||||
|
||||
import io.ebean.bean.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Set capable of lazy loading and modification aware.
|
||||
*/
|
||||
public final class BeanSet<E> extends AbstractBeanCollection<E> implements SequencedSet<E>, BeanCollectionAdd {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* The underlying Set implementation.
|
||||
*/
|
||||
private LinkedHashSet<E> set;
|
||||
|
||||
/**
|
||||
* Create with a specific Set implementation.
|
||||
*/
|
||||
public BeanSet(LinkedHashSet<E> set) {
|
||||
this.set = set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using an underlying LinkedHashSet.
|
||||
*/
|
||||
public BeanSet() {
|
||||
this(new LinkedHashSet<>());
|
||||
}
|
||||
|
||||
public BeanSet(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) {
|
||||
super(loader, ownerBean, propertyName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<E> freeze() {
|
||||
return set == null ? null : Collections.unmodifiableSet(set);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toString(ToStringBuilder builder) {
|
||||
builder.addCollection(set);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(EntityBean ownerBean, String propertyName) {
|
||||
this.ownerBean = ownerBean;
|
||||
this.propertyName = propertyName;
|
||||
this.set = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSkipSave() {
|
||||
return set == null || (set.isEmpty() && !holdsModifications());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void addEntityBean(EntityBean bean) {
|
||||
set.add((E) bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void loadFrom(BeanCollection<?> other) {
|
||||
if (set == null) {
|
||||
set = new LinkedHashSet<>();
|
||||
}
|
||||
set.addAll((Collection<? extends E>) other.actualDetails());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void internalAddWithCheck(Object bean) {
|
||||
// set add() already de-dups so just add it
|
||||
internalAdd(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void internalAdd(Object bean) {
|
||||
if (set == null) {
|
||||
set = new LinkedHashSet<>();
|
||||
}
|
||||
if (bean != null) {
|
||||
set.add((E) bean);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the underlying set has its data.
|
||||
*/
|
||||
@Override
|
||||
public boolean isPopulated() {
|
||||
return set != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a reference (lazy loading) bean collection. This is
|
||||
* the same as !isPopulated();
|
||||
*/
|
||||
@Override
|
||||
public boolean isReference() {
|
||||
return set == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkEmptyLazyLoad() {
|
||||
if (set == null) {
|
||||
set = new LinkedHashSet<>();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void initClear() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (set == null) {
|
||||
if (!disableLazyLoad && modifyListening) {
|
||||
lazyLoadCollection(false);
|
||||
} else {
|
||||
set = new LinkedHashSet<>();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void init() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (set == null) {
|
||||
if (disableLazyLoad) {
|
||||
set = new LinkedHashSet<>();
|
||||
} else {
|
||||
lazyLoadCollection(false);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual underlying set.
|
||||
*/
|
||||
public LinkedHashSet<E> actualSet() {
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<E> actualDetails() {
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<?> actualEntries() {
|
||||
return set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (set == null) {
|
||||
return "BeanSet<deferred>";
|
||||
} else {
|
||||
return set.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equal if obj is a Set and equal in a Set sense.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
init();
|
||||
return set.equals(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
init();
|
||||
return set.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addBean(E bean) {
|
||||
add(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeBean(E bean) {
|
||||
if (set.remove(bean)) {
|
||||
getModifyHolder().modifyRemoval(bean);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------//
|
||||
// proxy method for map
|
||||
// -----------------------------------------------------//
|
||||
|
||||
@Override
|
||||
public boolean add(E bean) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
if (set.add(bean)) {
|
||||
modifyAddition(bean);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return set.add(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends E> beans) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
boolean changed = false;
|
||||
for (E bean : beans) {
|
||||
if (set.add(bean)) {
|
||||
// register the addition of the bean
|
||||
modifyAddition(bean);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
return set.addAll(beans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
initClear();
|
||||
if (modifyListening) {
|
||||
for (E bean : set) {
|
||||
modifyRemoval(bean);
|
||||
}
|
||||
}
|
||||
set.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object bean) {
|
||||
init();
|
||||
return set.contains(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> beans) {
|
||||
init();
|
||||
return set.containsAll(beans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
init();
|
||||
return set.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<E> iterator() {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
return new ModifyIterator<>(this, set.iterator());
|
||||
}
|
||||
return set.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object bean) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
if (set.remove(bean)) {
|
||||
modifyRemoval(bean);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return set.remove(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> beans) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
boolean changed = false;
|
||||
for (Object bean : beans) {
|
||||
if (set.remove(bean)) {
|
||||
modifyRemoval(bean);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
return set.removeAll(beans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> beans) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
boolean changed = false;
|
||||
Iterator<?> it = set.iterator();
|
||||
while (it.hasNext()) {
|
||||
Object bean = it.next();
|
||||
if (!beans.contains(bean)) {
|
||||
// not retaining this bean so add it to the removal list
|
||||
it.remove();
|
||||
modifyRemoval(bean);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
return set.retainAll(beans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
init();
|
||||
return set.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] toArray() {
|
||||
init();
|
||||
return set.toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T[] toArray(T[] array) {
|
||||
init();
|
||||
//noinspection SuspiciousToArrayCall
|
||||
return set.toArray(array);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------//
|
||||
// SequencedSet (Java 21+)
|
||||
// -----------------------------------------------------//
|
||||
|
||||
@Override
|
||||
public SequencedSet<E> reversed() {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
throw new UnsupportedOperationException("Not supported on modify listening set");
|
||||
}
|
||||
return set.reversed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFirst(E bean) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
modifyAddition(bean);
|
||||
}
|
||||
set.addFirst(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLast(E bean) {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
modifyAddition(bean);
|
||||
}
|
||||
set.addLast(bean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E getFirst() {
|
||||
init();
|
||||
return set.getFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E getLast() {
|
||||
init();
|
||||
return set.getLast();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E removeFirst() {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E bean = set.removeFirst();
|
||||
modifyRemoval(bean);
|
||||
return bean;
|
||||
}
|
||||
return set.removeFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public E removeLast() {
|
||||
init();
|
||||
if (modifyListening) {
|
||||
E bean = set.removeLast();
|
||||
modifyRemoval(bean);
|
||||
return bean;
|
||||
}
|
||||
return set.removeLast();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -136,13 +136,13 @@ class ToStringBuilderTest {
|
||||
@Test
|
||||
void beanSet_null_empty() {
|
||||
assertThat(toStringFor(new BeanSet<String>(null))).isEqualTo("[]");
|
||||
assertThat(toStringFor(new BeanSet<String>(Collections.emptySet()))).isEqualTo("[]");
|
||||
assertThat(toStringFor(new BeanSet<String>(new LinkedHashSet<>()))).isEqualTo("[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanMap_null_empty() {
|
||||
assertThat(toStringFor(new BeanMap<String, String>(null))).isEqualTo("{}");
|
||||
assertThat(toStringFor(new BeanMap<String, String>(Collections.emptyMap()))).isEqualTo("{}");
|
||||
assertThat(toStringFor(new BeanMap<String, String>(new LinkedHashMap<>()))).isEqualTo("{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,7 +159,7 @@ class ToStringBuilderTest {
|
||||
|
||||
@Test
|
||||
void beanMap_some() {
|
||||
Map<String, Recurse> under = new LinkedHashMap<>();
|
||||
var under = new LinkedHashMap<String, Recurse>();
|
||||
under.put("a", new Recurse(1, "a"));
|
||||
under.put("b", new Recurse(2, "b"));
|
||||
BeanMap<String, Recurse> list = new BeanMap<>(under);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MetaInfoManagerTest {
|
||||
|
||||
@Test
|
||||
void collectMetricsBooleanDefaultsToCollectMetrics() {
|
||||
ServerMetrics metrics = new BasicMetricVisitor();
|
||||
MetaInfoManager manager = new MetaInfoManager() {
|
||||
@Override
|
||||
public ServerMetrics collectMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitMetrics(MetricVisitor visitor) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BasicMetricVisitor visitBasic() {
|
||||
return new BasicMetricVisitor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetAllMetrics() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaQueryPlan> queryPlanInit(QueryPlanInit initRequest) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaQueryPlan> queryPlanCollectNow(QueryPlanRequest request) {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
|
||||
assertThat(manager.collectMetrics(false)).isSameAs(metrics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MetricNamingV2Test {
|
||||
|
||||
private MetricNamingV2.Mapped map(String name, String beanType) {
|
||||
return MetricNamingV2.map(name, beanType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void orm_withBeanType() {
|
||||
MetricNamingV2.Mapped m = map("orm.Customer.findList", "Customer");
|
||||
assertThat(m.name()).isEqualTo("ebean.query");
|
||||
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList,type:Customer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orm_withoutBeanType() {
|
||||
MetricNamingV2.Mapped m = map("orm.Customer.findList", null);
|
||||
assertThat(m.name()).isEqualTo("ebean.query");
|
||||
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList");
|
||||
}
|
||||
|
||||
@Test
|
||||
void dto_andSql() {
|
||||
assertThat(map("dto.CustomerDto.findRecent", "CustomerDto").tags())
|
||||
.isEqualTo("kind:dto,label:CustomerDto.findRecent,type:CustomerDto");
|
||||
assertThat(map("sql.query.fooBar", "Customer").tags())
|
||||
.isEqualTo("kind:sql,label:query.fooBar,type:Customer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void iud() {
|
||||
MetricNamingV2.Mapped m = map("iud.User.save", null);
|
||||
assertThat(m.name()).isEqualTo("ebean.dml");
|
||||
assertThat(m.tags()).isEqualTo("label:User.save");
|
||||
}
|
||||
|
||||
@Test
|
||||
void txn_named_and_plain() {
|
||||
assertThat(map("txn.named.ProcessJob", null).name()).isEqualTo("ebean.txn");
|
||||
assertThat(map("txn.named.ProcessJob", null).tags()).isEqualTo("label:ProcessJob");
|
||||
assertThat(map("txn.main", null).tags()).isEqualTo("label:main");
|
||||
}
|
||||
|
||||
@Test
|
||||
void l2_regionAndOp() {
|
||||
MetricNamingV2.Mapped m = map("l2.customer.hit", null);
|
||||
assertThat(m.name()).isEqualTo("ebean.l2");
|
||||
assertThat(m.tags()).isEqualTo("op:hit,region:customer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void l2_opOnly() {
|
||||
assertThat(map("l2.hit", null).tags()).isEqualTo("op:hit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrecognisedPrefix_isOther() {
|
||||
MetricNamingV2.Mapped m = map("l2n.Customer.hit", null);
|
||||
assertThat(m.name()).isEqualTo("ebean.other");
|
||||
assertThat(m.tags()).isEqualTo("label:l2n.Customer.hit");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noDot_isOther() {
|
||||
assertThat(map("jvm", null).name()).isEqualTo("ebean.other");
|
||||
assertThat(map("jvm", null).tags()).isEqualTo("label:jvm");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullOrEmpty() {
|
||||
assertThat(map(null, null).name()).isEqualTo("ebean.other");
|
||||
assertThat(map(null, null).tags()).isEmpty();
|
||||
assertThat(map("", null).tags()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sanitisesReservedChars() {
|
||||
MetricNamingV2.Mapped m = map("orm.Customer.weird", "Cust:om,er");
|
||||
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.weird,type:Cust_om_er");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagsAreSortedByKey() {
|
||||
// kind < label < type alphabetically regardless of build order
|
||||
assertThat(map("orm.X.find", "Bean").tags())
|
||||
.isEqualTo("kind:orm,label:X.find,type:Bean");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package io.ebean.meta;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MetricsAsJsonV2Test {
|
||||
|
||||
@Test
|
||||
void writeV2_usesFamilyNamesAndTags() {
|
||||
ServerMetrics metrics = new FakeServerMetrics();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
new MetricsAsJson(metrics).writeV2(sb);
|
||||
String json = sb.toString();
|
||||
|
||||
assertThat(json).contains("\"db\":\"db1\"");
|
||||
// query metric -> ebean.query with kind/type/label tags
|
||||
assertThat(json).contains("\"name\":\"ebean.query\"");
|
||||
assertThat(json).contains("\"tags\":\"kind:orm,label:Customer.findList,type:Customer\"");
|
||||
// timed iud metric -> ebean.dml
|
||||
assertThat(json).contains("\"name\":\"ebean.dml\"");
|
||||
assertThat(json).contains("\"tags\":\"label:User.save\"");
|
||||
// count metric (l2n not specially mapped) -> ebean.other
|
||||
assertThat(json).contains("\"name\":\"ebean.other\"");
|
||||
assertThat(json).contains("\"tags\":\"label:l2n.Customer.hit\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void write_v1_unchanged_usesFlatNames() {
|
||||
ServerMetrics metrics = new FakeServerMetrics();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
new MetricsAsJson(metrics).write(sb);
|
||||
String json = sb.toString();
|
||||
|
||||
assertThat(json).contains("\"name\":\"orm.Customer.findList\"");
|
||||
assertThat(json).contains("\"name\":\"iud.User.save\"");
|
||||
assertThat(json).doesNotContain("\"tags\"");
|
||||
}
|
||||
|
||||
static final class FakeServerMetrics implements ServerMetrics {
|
||||
@Override
|
||||
public String name() {
|
||||
return "db1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerMetricsAsJson asJson() {
|
||||
return new MetricsAsJson(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetricData> asData() {
|
||||
return new java.util.ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaTimedMetric> timedMetrics() {
|
||||
return new java.util.ArrayList<>(List.of(new FakeTimed("iud.User.save")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaQueryMetric> queryMetrics() {
|
||||
return new java.util.ArrayList<>(List.of(new FakeQuery("orm.Customer.findList", Customer.class)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MetaCountMetric> countMetrics() {
|
||||
return new java.util.ArrayList<>(List.of(new FakeCount("l2n.Customer.hit")));
|
||||
}
|
||||
}
|
||||
|
||||
static class Customer {
|
||||
}
|
||||
|
||||
static class FakeTimed implements MetaTimedMetric {
|
||||
private final String name;
|
||||
|
||||
FakeTimed(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String location() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long total() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long max() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mean() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initialCollection() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static final class FakeQuery extends FakeTimed implements MetaQueryMetric {
|
||||
private final Class<?> type;
|
||||
|
||||
FakeQuery(String name, Class<?> type) {
|
||||
super(name);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sql() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash() {
|
||||
return "h1";
|
||||
}
|
||||
}
|
||||
|
||||
static final class FakeCount implements MetaCountMetric {
|
||||
private final String name;
|
||||
|
||||
FakeCount(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-bench</artifactId>
|
||||
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean bom</name>
|
||||
@@ -89,25 +89,25 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -125,13 +125,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<name>ebean-core-json</name>
|
||||
@@ -16,15 +16,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<optional>true</optional>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-core</artifactId>
|
||||
<version>${avaje-json-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -1,190 +1,170 @@
|
||||
package io.ebeaninternal.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonReader.Token;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.avaje.json.mapper.JsonMapper;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.service.SpiJsonService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Utility that converts between JSON content and simple java Maps/Lists.
|
||||
* <p>
|
||||
* Backed by avaje {@link JsonMapper} using {@link EbeanJsonAdapter} which
|
||||
* preserves Ebean's modify-aware collection and number semantics.
|
||||
*/
|
||||
public final class DJsonService implements SpiJsonService {
|
||||
|
||||
/**
|
||||
* Write the nested Map/List as json.
|
||||
*/
|
||||
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
|
||||
private static final JsonMapper MAPPER = JsonMapper.builder().jsonStream(JSON_STREAM).build();
|
||||
|
||||
private static final JsonMapper.Type<Object> PLAIN = MAPPER.type(EbeanJsonAdapter.PLAIN);
|
||||
private static final JsonMapper.Type<Object> MODIFY_AWARE = MAPPER.type(EbeanJsonAdapter.MODIFY_AWARE);
|
||||
|
||||
private static JsonMapper.Type<Object> type(boolean modifyAware) {
|
||||
return modifyAware ? MODIFY_AWARE : PLAIN;
|
||||
}
|
||||
|
||||
private static boolean blank(String content) {
|
||||
return content == null || content.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String readAll(Reader reader) throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
char[] buffer = new char[2048];
|
||||
int len;
|
||||
while ((len = reader.read(buffer)) != -1) {
|
||||
builder.append(buffer, 0, len);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String write(Object object) throws IOException {
|
||||
return EJsonWriter.write(object);
|
||||
StringWriter writer = new StringWriter();
|
||||
write(object, writer);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the nested Map/List as json to the writer.
|
||||
*/
|
||||
@Override
|
||||
public void write(Object object, Writer writer) throws IOException {
|
||||
EJsonWriter.write(object, writer);
|
||||
JsonWriter jsonWriter = JSON_STREAM.writer(writer);
|
||||
jsonWriter.serializeNulls(true);
|
||||
PLAIN.toJson(object, jsonWriter);
|
||||
jsonWriter.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the nested Map/List as json to the jsonGenerator.
|
||||
*/
|
||||
@Override
|
||||
public void write(Object object, JsonGenerator jsonGenerator) throws IOException {
|
||||
EJsonWriter.write(object, jsonGenerator);
|
||||
public void write(Object object, JsonWriter jsonWriter) throws IOException {
|
||||
PLAIN.toJson(object, jsonWriter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the collection as json array to the jsonGenerator.
|
||||
*/
|
||||
@Override
|
||||
public void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
|
||||
EJsonWriter.writeCollection(collection, jsonGenerator);
|
||||
public void writeCollection(Collection<Object> collection, JsonWriter jsonWriter) throws IOException {
|
||||
EbeanJsonAdapter.writeCollection(jsonWriter, collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map additionally specifying if the returned map should be modify
|
||||
* aware meaning that it can detect when it has been modified.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
|
||||
return EJsonReader.parseObject(json, modifyAware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(String json) throws IOException {
|
||||
return EJsonReader.parseObject(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a reader.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
|
||||
return EJsonReader.parseObject(reader, modifyAware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a reader.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(Reader reader) throws IOException {
|
||||
return EJsonReader.parseObject(reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(JsonParser parser) throws IOException {
|
||||
return EJsonReader.parseObject(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a Map taking a JsonParser and a starting token.
|
||||
*
|
||||
* <p>Used when the first token is checked to see if the value is null prior to calling this.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
|
||||
return EJsonReader.parseObject(parser, token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a modify aware List.
|
||||
*/
|
||||
@Override
|
||||
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
|
||||
return EJsonReader.parseList(json, modifyAware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List.
|
||||
*/
|
||||
@Override
|
||||
public List<Object> parseList(String json) throws IOException {
|
||||
return EJsonReader.parseList(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List taking a Reader.
|
||||
*/
|
||||
@Override
|
||||
public List<Object> parseList(Reader reader) throws IOException {
|
||||
return EJsonReader.parseList(reader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List taking a JsonParser.
|
||||
*/
|
||||
@Override
|
||||
public List<Object> parseList(JsonParser parser) throws IOException {
|
||||
return EJsonReader.parseList(parser, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json returning as a List taking into account the current token.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
|
||||
return (List<T>) EJsonReader.parse(parser, currentToken, false);
|
||||
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
|
||||
return blank(json) ? null : (Map<String, Object>) type(modifyAware).fromJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseObject(String json) throws IOException {
|
||||
return parseObject(json, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
|
||||
return parseObject(readAll(reader), modifyAware);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseObject(Reader reader) throws IOException {
|
||||
return parseObject(reader, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> parseObject(JsonReader parser) throws IOException {
|
||||
return (Map<String, Object>) PLAIN.fromJson(parser);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
|
||||
return (Map<String, Object>) EbeanJsonAdapter.read(parser, token, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
|
||||
return blank(json) ? null : (List<T>) type(modifyAware).fromJson(json);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> parseList(String json) throws IOException {
|
||||
return (List<Object>) parseList(json, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Object> parseList(Reader reader) throws IOException {
|
||||
return parseList(readAll(reader));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<Object> parseList(JsonReader parser) throws IOException {
|
||||
return (List<Object>) PLAIN.fromJson(parser);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
|
||||
return (List<T>) EbeanJsonAdapter.read(parser, currentToken, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
*/
|
||||
@Override
|
||||
public Object parse(String json) throws IOException {
|
||||
return EJsonReader.parse(json);
|
||||
return blank(json) ? null : PLAIN.fromJson(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
*/
|
||||
@Override
|
||||
public Object parse(Reader reader) throws IOException {
|
||||
return EJsonReader.parse(reader);
|
||||
return parse(readAll(reader));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json and return as a List or Map.
|
||||
*/
|
||||
@Override
|
||||
public Object parse(JsonParser parser) throws IOException {
|
||||
return EJsonReader.parse(parser);
|
||||
public Object parse(JsonReader parser) throws IOException {
|
||||
return PLAIN.fromJson(parser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json returning a Set that might be modify aware.
|
||||
*/
|
||||
@Override
|
||||
public <T> Set<T> parseSet(String json, boolean modifyAware) throws IOException {
|
||||
List<T> list = parseList(json, modifyAware);
|
||||
if (list == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (modifyAware) {
|
||||
return ((ModifyAwareList<T>) list).asSet();
|
||||
} else {
|
||||
return new LinkedHashSet<>(list);
|
||||
}
|
||||
return new LinkedHashSet<>(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the json returning as a Set taking into account the current token.
|
||||
*/
|
||||
@Override
|
||||
public <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
|
||||
public <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
|
||||
return new LinkedHashSet<>(parseList(parser, currentToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
package io.ebeaninternal.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import io.ebean.ModifyAwareType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.util.*;
|
||||
|
||||
final class EJsonReader {
|
||||
|
||||
static final JsonFactory json = new JsonFactory();
|
||||
private final JsonParser parser;
|
||||
private final boolean modifyAware;
|
||||
private final ModifyAwareFlag modifyAwareOwner;
|
||||
private int depth;
|
||||
private Stack stack;
|
||||
private Context currentContext;
|
||||
|
||||
EJsonReader(JsonParser parser, boolean modifyAware) {
|
||||
this.parser = parser;
|
||||
this.modifyAware = modifyAware;
|
||||
this.modifyAwareOwner = modifyAware ? new ModifyAwareFlag() : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
|
||||
return (Map<String, Object>) parse(json, modifyAware);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(String json) throws IOException {
|
||||
return (Map<String, Object>) parse(json);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(Reader reader) throws IOException {
|
||||
return (Map<String, Object>) parse(reader);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
|
||||
return (Map<String, Object>) parse(reader, modifyAware);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(JsonParser parser) throws IOException {
|
||||
return (Map<String, Object>) parse(parser);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
|
||||
return (Map<String, Object>) parse(parser, token, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
|
||||
return (List<T>) parse(json, modifyAware);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<Object> parseList(String json) throws IOException {
|
||||
return (List<Object>) parse(json);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<Object> parseList(Reader reader) throws IOException {
|
||||
return (List<Object>) parse(reader);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static List<Object> parseList(JsonParser parser, boolean modifyAware) throws IOException {
|
||||
return (List<Object>) parse(parser, modifyAware);
|
||||
}
|
||||
|
||||
static Object parse(String json) throws IOException {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return parse(new StringReader(json));
|
||||
}
|
||||
|
||||
static Object parse(String json, boolean modifyAware) throws IOException {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return parse(new StringReader(json), modifyAware);
|
||||
}
|
||||
|
||||
static Object parse(Reader reader) throws IOException {
|
||||
return parse(json.createParser(reader));
|
||||
}
|
||||
|
||||
static Object parse(Reader reader, boolean modifyAware) throws IOException {
|
||||
return parse(json.createParser(reader), modifyAware);
|
||||
}
|
||||
|
||||
static Object parse(JsonParser parser) throws IOException {
|
||||
return parse(parser, null, false);
|
||||
}
|
||||
|
||||
static Object parse(JsonParser parser, boolean modifyAware) throws IOException {
|
||||
return parse(parser, null, modifyAware);
|
||||
}
|
||||
|
||||
static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException {
|
||||
return new EJsonReader(parser, modifyAware).parseJson(token);
|
||||
}
|
||||
|
||||
private void startArray() {
|
||||
depth++;
|
||||
stack.push(currentContext);
|
||||
currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext();
|
||||
}
|
||||
|
||||
private void startObject() {
|
||||
depth++;
|
||||
stack.push(currentContext);
|
||||
currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext();
|
||||
}
|
||||
|
||||
private void endArray() {
|
||||
end();
|
||||
}
|
||||
|
||||
private void endObject() {
|
||||
end();
|
||||
}
|
||||
|
||||
private void end() {
|
||||
depth--;
|
||||
if (!stack.isEmpty()) {
|
||||
currentContext = stack.pop(currentContext);
|
||||
}
|
||||
if (modifyAwareOwner != null) {
|
||||
modifyAwareOwner.setMarkedDirty(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setValue(Object value) {
|
||||
currentContext.setValue(value);
|
||||
}
|
||||
|
||||
private void setValueNull() {
|
||||
currentContext.setValueNull();
|
||||
}
|
||||
|
||||
private Object parseJson(JsonToken token) throws IOException {
|
||||
|
||||
if (token == null) {
|
||||
token = parser.nextToken();
|
||||
// if it is a simple value just return it
|
||||
switch (token) {
|
||||
case VALUE_NULL:
|
||||
return null;
|
||||
case VALUE_FALSE:
|
||||
return Boolean.FALSE;
|
||||
case VALUE_TRUE:
|
||||
return Boolean.TRUE;
|
||||
case VALUE_STRING:
|
||||
return parser.getText();
|
||||
case VALUE_NUMBER_INT:
|
||||
return parser.getLongValue();
|
||||
case VALUE_NUMBER_FLOAT:
|
||||
return parser.getDecimalValue();
|
||||
}
|
||||
}
|
||||
|
||||
// it is a object or array, process the first JsonToken
|
||||
stack = new Stack();
|
||||
processJsonToken(token);
|
||||
|
||||
// process the rest of the object or array
|
||||
while (depth > 0) {
|
||||
token = parser.nextToken();
|
||||
processJsonToken(token);
|
||||
}
|
||||
|
||||
return currentContext.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the JsonToken for objects and arrays.
|
||||
*/
|
||||
private void processJsonToken(JsonToken token) throws IOException {
|
||||
switch (token) {
|
||||
case START_ARRAY:
|
||||
startArray();
|
||||
break;
|
||||
|
||||
case START_OBJECT:
|
||||
startObject();
|
||||
break;
|
||||
|
||||
case FIELD_NAME:
|
||||
currentContext.setKey(parser.getCurrentName());
|
||||
break;
|
||||
|
||||
case VALUE_STRING:
|
||||
setValue(parser.getValueAsString());
|
||||
break;
|
||||
|
||||
case VALUE_NUMBER_INT:
|
||||
setValue(parser.getLongValue());
|
||||
break;
|
||||
|
||||
case VALUE_NUMBER_FLOAT:
|
||||
setValue(parser.getDecimalValue());
|
||||
break;
|
||||
|
||||
case VALUE_TRUE:
|
||||
setValue(Boolean.TRUE);
|
||||
break;
|
||||
|
||||
case VALUE_FALSE:
|
||||
setValue(Boolean.FALSE);
|
||||
break;
|
||||
|
||||
case VALUE_NULL:
|
||||
setValueNull();
|
||||
break;
|
||||
|
||||
case END_OBJECT:
|
||||
endObject();
|
||||
break;
|
||||
|
||||
case END_ARRAY:
|
||||
endArray();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Stack {
|
||||
|
||||
private Context head;
|
||||
|
||||
private void push(Context context) {
|
||||
if (context != null) {
|
||||
context.next = head;
|
||||
head = context;
|
||||
}
|
||||
}
|
||||
|
||||
private Context pop(Context endingContext) {
|
||||
if (head == null) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
Context temp = head;
|
||||
head = head.next;
|
||||
temp.popContext(endingContext);
|
||||
return temp;
|
||||
}
|
||||
|
||||
private boolean isEmpty() {
|
||||
return head == null;
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class Context {
|
||||
Context next;
|
||||
|
||||
abstract void popContext(Context temp);
|
||||
|
||||
abstract Object getValue();
|
||||
|
||||
abstract void setValue(Object value);
|
||||
|
||||
abstract void setKey(String key);
|
||||
|
||||
abstract void setValueNull();
|
||||
}
|
||||
|
||||
private static class ObjectContext extends Context {
|
||||
|
||||
private final Map<String, Object> map;
|
||||
|
||||
private String key;
|
||||
|
||||
ObjectContext() {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
ObjectContext(ModifyAwareType owner) {
|
||||
map = new ModifyAwareMap<>(owner, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popContext(Context temp) {
|
||||
setValue(temp.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
Object getValue() {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
void setValue(Object value) {
|
||||
map.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
@Override
|
||||
void setValueNull() {
|
||||
map.put(key, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ArrayContext extends Context {
|
||||
|
||||
private final List<Object> values;
|
||||
|
||||
ArrayContext() {
|
||||
values = new ArrayList<>();
|
||||
}
|
||||
|
||||
ArrayContext(ModifyAwareType owner) {
|
||||
values = new ModifyAwareList<>(owner, new ArrayList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void popContext(Context temp) {
|
||||
values.add(temp.getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
Object getValue() {
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
void setValue(Object value) {
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
void setValueNull() {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
void setKey(String key) {
|
||||
// not expected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package io.ebeaninternal.json;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
final class EJsonWriter {
|
||||
|
||||
/**
|
||||
* Base jsonFactory implementation used when it is not passed in.
|
||||
*/
|
||||
static final JsonFactory jsonFactory = new JsonFactory();
|
||||
private final JsonGenerator jsonGenerator;
|
||||
|
||||
private EJsonWriter(JsonGenerator jsonGenerator) {
|
||||
this.jsonGenerator = jsonGenerator;
|
||||
}
|
||||
|
||||
static String write(Object object) throws IOException {
|
||||
StringWriter writer = new StringWriter(200);
|
||||
write(object, writer).close();
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
static JsonGenerator write(Object object, Writer writer) throws IOException {
|
||||
JsonGenerator generator = jsonFactory.createGenerator(writer);
|
||||
write(object, generator);
|
||||
generator.flush();
|
||||
return generator;
|
||||
}
|
||||
|
||||
static void write(Object object, JsonGenerator jsonGenerator) {
|
||||
new EJsonWriter(jsonGenerator).writeJson(object);
|
||||
}
|
||||
|
||||
static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
|
||||
new EJsonWriter(jsonGenerator).writeCollection(null, collection);
|
||||
}
|
||||
|
||||
private void writeJson(Object object) {
|
||||
writeJson(null, object);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void writeJson(String name, Object object) {
|
||||
try {
|
||||
if (object == null) {
|
||||
writeNull(name);
|
||||
|
||||
} else if (object instanceof Number) {
|
||||
writeNumber(name, (Number) object);
|
||||
|
||||
} else if (object instanceof String) {
|
||||
writeString(name, (String) object);
|
||||
|
||||
} else if (object instanceof Map) {
|
||||
writeMap(name, (Map<Object, Object>) object);
|
||||
|
||||
} else if (object instanceof Collection) {
|
||||
writeCollection(name, (Collection<Object>) object);
|
||||
|
||||
} else if (object instanceof Boolean) {
|
||||
writeBoolean(name, (Boolean) object);
|
||||
|
||||
} else if (object instanceof Date) {
|
||||
writeDate(name, (Date) object);
|
||||
|
||||
} else if (object instanceof Map.Entry<?, ?>) {
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
|
||||
writeJson(entry.getKey().toString(), entry.getValue());
|
||||
|
||||
} else {
|
||||
writeString(name, object.toString());
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBoolean(String name, Boolean object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeBoolean(object);
|
||||
} else {
|
||||
jsonGenerator.writeBooleanField(name, object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDate(String name, Date object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber(object.getTime());
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, object.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeNumber(String name, Number object) throws IOException {
|
||||
|
||||
if (object instanceof Long) {
|
||||
writeLong(name, object);
|
||||
|
||||
} else if (object instanceof Integer) {
|
||||
writeInteger(name, object);
|
||||
|
||||
} else if (object instanceof Double) {
|
||||
writeDouble(name, object);
|
||||
|
||||
} else if (object instanceof BigDecimal) {
|
||||
writeBigDecimal(name, object);
|
||||
|
||||
} else if (object instanceof BigInteger) {
|
||||
writeBigInteger(name, object);
|
||||
|
||||
} else {
|
||||
writeGeneralNumber(name, object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeGeneralNumber(String name, Number object) throws IOException {
|
||||
writeBigDecimal(name, new BigDecimal(object.toString()));
|
||||
}
|
||||
|
||||
private void writeBigDecimal(String name, Number object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber((BigDecimal) object);
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, (BigDecimal) object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBigInteger(String name, Number object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber((BigInteger) object);
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, object.longValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDouble(String name, Number object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber((Double) object);
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, (Double) object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLong(String name, Number object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber((Long) object);
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, (Long) object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeInteger(String name, Number object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNumber((Integer) object);
|
||||
} else {
|
||||
jsonGenerator.writeNumberField(name, (Integer) object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeNull(String name) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeNull();
|
||||
} else {
|
||||
jsonGenerator.writeNullField(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeString(String name, String object) throws IOException {
|
||||
if (name == null) {
|
||||
jsonGenerator.writeString(object);
|
||||
} else {
|
||||
jsonGenerator.writeStringField(name, object);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCollection(String name, Collection<Object> collection) throws IOException {
|
||||
if (name != null) {
|
||||
jsonGenerator.writeFieldName(name);
|
||||
}
|
||||
jsonGenerator.writeStartArray();
|
||||
for (Object object : collection) {
|
||||
writeJson(null, object);
|
||||
}
|
||||
jsonGenerator.writeEndArray();
|
||||
}
|
||||
|
||||
private void writeMap(String name, Map<Object, Object> map) throws IOException {
|
||||
|
||||
if (name != null) {
|
||||
jsonGenerator.writeFieldName(name);
|
||||
}
|
||||
jsonGenerator.writeStartObject();
|
||||
Set<Entry<Object, Object>> entrySet = map.entrySet();
|
||||
for (Entry<Object, Object> entry : entrySet) {
|
||||
writeJson(entry.getKey().toString(), entry.getValue());
|
||||
}
|
||||
jsonGenerator.writeEndObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package io.ebeaninternal.json;
|
||||
|
||||
import io.avaje.json.JsonAdapter;
|
||||
import io.avaje.json.JsonReader;
|
||||
import io.avaje.json.JsonReader.Token;
|
||||
import io.avaje.json.JsonWriter;
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import io.ebean.ModifyAwareType;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Ebean specific {@link JsonAdapter} that materializes JSON into plain Java
|
||||
* Map/List/scalar values - optionally wrapped in modify-aware collections so
|
||||
* that mutations after load are tracked as dirty.
|
||||
* <p>
|
||||
* This consolidates the prior EJsonReader/EJsonWriter behavior into a single
|
||||
* adapter that plugs into avaje {@code JsonMapper}.
|
||||
*/
|
||||
final class EbeanJsonAdapter implements JsonAdapter<Object> {
|
||||
|
||||
static final EbeanJsonAdapter PLAIN = new EbeanJsonAdapter(false);
|
||||
static final EbeanJsonAdapter MODIFY_AWARE = new EbeanJsonAdapter(true);
|
||||
|
||||
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
|
||||
|
||||
private final boolean modifyAware;
|
||||
|
||||
private EbeanJsonAdapter(boolean modifyAware) {
|
||||
this.modifyAware = modifyAware;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromJson(JsonReader reader) {
|
||||
return read(reader, null, modifyAware);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) {
|
||||
write(writer, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a value honoring an explicitly supplied current token (or current token when null).
|
||||
*/
|
||||
static Object read(JsonReader parser, Token token, boolean modifyAware) {
|
||||
ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null;
|
||||
Token effectiveToken = token == null ? parser.currentToken() : token;
|
||||
Object value;
|
||||
if (effectiveToken == null) {
|
||||
value = parseRawJson(parser.readRaw(), owner);
|
||||
} else {
|
||||
value = parseValue(parser, effectiveToken, owner);
|
||||
}
|
||||
if (owner != null) {
|
||||
owner.setMarkedDirty(false);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) {
|
||||
if (token == null) {
|
||||
token = parser.currentToken();
|
||||
if (token == null) {
|
||||
if (parser.isNullValue()) {
|
||||
return null;
|
||||
}
|
||||
return parseRawJson(parser.readRaw(), owner);
|
||||
}
|
||||
}
|
||||
switch (token) {
|
||||
case BEGIN_OBJECT:
|
||||
return parseObjectValue(parser, owner);
|
||||
case BEGIN_ARRAY:
|
||||
return parseArrayValue(parser, owner);
|
||||
case NUMBER:
|
||||
BigDecimal value = parser.readDecimal();
|
||||
return value.scale() <= 0 ? value.longValue() : value;
|
||||
case STRING:
|
||||
return parser.readString();
|
||||
case BOOLEAN:
|
||||
return parser.readBoolean();
|
||||
case NULL:
|
||||
parser.isNullValue();
|
||||
return null;
|
||||
default:
|
||||
return parseRawJson(parser.readRaw(), owner);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object parseRawJson(String json, ModifyAwareType owner) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
String content = json.trim();
|
||||
if (content.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try (JsonReader parser = JSON_STREAM.reader(content)) {
|
||||
return parseValue(parser, parser.currentToken(), owner);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> parseObjectValue(JsonReader parser, ModifyAwareType owner) {
|
||||
Map<String, Object> map = owner == null
|
||||
? new LinkedHashMap<>()
|
||||
: new ModifyAwareMap<>(owner, new LinkedHashMap<>());
|
||||
|
||||
parser.beginObject();
|
||||
while (parser.hasNextField()) {
|
||||
String fieldName = parser.nextField();
|
||||
map.put(fieldName, parseValue(parser, parser.currentToken(), owner));
|
||||
}
|
||||
parser.endObject();
|
||||
return map;
|
||||
}
|
||||
|
||||
private static List<Object> parseArrayValue(JsonReader parser, ModifyAwareType owner) {
|
||||
List<Object> list = owner == null
|
||||
? new ArrayList<>()
|
||||
: new ModifyAwareList<>(owner, new ArrayList<>());
|
||||
|
||||
parser.beginArray();
|
||||
while (parser.hasNextElement()) {
|
||||
list.add(parseValue(parser, parser.currentToken(), owner));
|
||||
}
|
||||
parser.endArray();
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the value to an existing JsonWriter (used for the raw stream paths).
|
||||
*/
|
||||
static void write(JsonWriter jsonWriter, Object object) {
|
||||
if (object == null) {
|
||||
jsonWriter.nullValue();
|
||||
} else if (object instanceof String) {
|
||||
jsonWriter.value((String) object);
|
||||
} else if (object instanceof Integer) {
|
||||
jsonWriter.value((Integer) object);
|
||||
} else if (object instanceof Long) {
|
||||
jsonWriter.value((Long) object);
|
||||
} else if (object instanceof Double) {
|
||||
jsonWriter.value((Double) object);
|
||||
} else if (object instanceof Float) {
|
||||
jsonWriter.value((Float) object);
|
||||
} else if (object instanceof BigDecimal) {
|
||||
jsonWriter.value((BigDecimal) object);
|
||||
} else if (object instanceof Boolean) {
|
||||
jsonWriter.value((Boolean) object);
|
||||
} else if (object instanceof Map<?, ?>) {
|
||||
writeMap(jsonWriter, (Map<?, ?>) object);
|
||||
} else if (object instanceof Collection<?>) {
|
||||
writeCollection(jsonWriter, (Collection<?>) object);
|
||||
} else {
|
||||
jsonWriter.value(object.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeMap(JsonWriter jsonWriter, Map<?, ?> map) {
|
||||
jsonWriter.beginObject();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
jsonWriter.name((String) entry.getKey());
|
||||
write(jsonWriter, entry.getValue());
|
||||
}
|
||||
jsonWriter.endObject();
|
||||
}
|
||||
|
||||
static void writeCollection(JsonWriter jsonWriter, Collection<?> collection) {
|
||||
jsonWriter.beginArray();
|
||||
for (Object element : collection) {
|
||||
write(jsonWriter, element);
|
||||
}
|
||||
jsonWriter.endArray();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
module io.ebean.core.json {
|
||||
|
||||
requires io.ebean.api;
|
||||
|
||||
requires transitive com.fasterxml.jackson.core;
|
||||
requires transitive io.avaje.json;
|
||||
exports io.ebeaninternal.json to io.ebean.test, io.ebean.core;
|
||||
|
||||
provides io.ebean.service.BootstrapService with io.ebeaninternal.json.DJsonService;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
@@ -16,21 +16,20 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.6.0</version>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<optional>true</optional>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-core</artifactId>
|
||||
<version>${avaje-json-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Provided scope for Postgres JSON/JSONB support -->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.2</version>
|
||||
<version>42.7.11</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user