mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e6cd9db8c | ||
|
|
484ed5d859 | ||
|
|
1545e68c3e | ||
|
|
4fd32b45d2 | ||
|
|
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 |
@@ -40,5 +40,5 @@ jobs:
|
||||
# - name: Maven single test
|
||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||
- name: Build with Maven
|
||||
run: mvn -T 8 clean test -Pdefault
|
||||
run: mvn -T 1C clean test -Pdefault
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: db2
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: H2Database
|
||||
run: mvn -T 8 clean package
|
||||
run: mvn -T 1C clean package
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mariadb 10.11
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mysql
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: oracle
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: postgres
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: sqlserver 2022
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
|
||||
@@ -13,6 +13,9 @@ ebean-profiling*.xml
|
||||
profiling/
|
||||
.DS_Store
|
||||
|
||||
# Local Redis integration test credentials
|
||||
ebean-redis/src/test/resources/redis-local.yml
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
|
||||
@@ -14,6 +14,8 @@ Key guides (fetch and follow when performing the relevant task):
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
|
||||
@@ -29,6 +29,8 @@ existing Maven project. Complete the steps in order.
|
||||
| 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
|
||||
|
||||
@@ -149,6 +151,8 @@ Key guides (fetch and follow these when performing the relevant task):
|
||||
- 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
|
||||
|
||||
@@ -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,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.
|
||||
@@ -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"
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-bench</artifactId>
|
||||
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean bom</name>
|
||||
@@ -89,25 +89,25 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -125,13 +125,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -155,37 +155,37 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>kotlin-querybean-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-spring-txn</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- platforms -->
|
||||
@@ -193,91 +193,91 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-clickhouse</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-db2</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-h2</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-hana</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mariadb</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-mysql</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-nuodb</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-oracle</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlite</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-sqlserver</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<name>ebean-core-json</name>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
@@ -16,7 +16,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
+7
-7
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-core</artifactId>
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-json</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -52,7 +52,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -165,21 +165,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ public interface SpiDtoQuery<T> extends DtoQuery<T>, SpiSqlBinding {
|
||||
*/
|
||||
String planLabel();
|
||||
|
||||
/**
|
||||
* Return the explicit label (from {@code setLabel}) or null. Unlike
|
||||
* {@link #planLabel()} this does not fall back to the profile location.
|
||||
*/
|
||||
String explicitLabel();
|
||||
|
||||
/**
|
||||
* Obtain the location if necessary.
|
||||
*/
|
||||
|
||||
@@ -345,9 +345,18 @@ public interface SpiQuery<T> extends Query<T>, SpiQueryFetch, TxnProfileEventCod
|
||||
void fetchProperties(String path, OrmQueryProperties other);
|
||||
|
||||
/**
|
||||
* Set the on a secondary query given the label, relativePath and profile location of the parent query.
|
||||
* Set the label on a secondary query by extending the parent query's full plan
|
||||
* name with the relative path and load mode (joined with '.').
|
||||
* <p>
|
||||
* The {@code parentName} is the parent query's full plan name (without the
|
||||
* leading "orm."), so for a root query labelled "custMain" on Customer the
|
||||
* secondary lazy load of contacts becomes {@code Customer.custMain.contacts.lazy}.
|
||||
* The profile location of the parent query is also propagated.
|
||||
*
|
||||
* @param parentName the parent query's full plan name (no "orm." prefix)
|
||||
* @param relativePath the path to the loaded property plus the load mode, e.g. {@code contacts.lazy}
|
||||
*/
|
||||
void setProfilePath(String label, String relativePath, @Nullable ProfileLocation profileLocation);
|
||||
void setProfilePath(String parentName, String relativePath, @Nullable ProfileLocation profileLocation);
|
||||
|
||||
/**
|
||||
* Set the query mode.
|
||||
|
||||
@@ -27,6 +27,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
|
||||
protected String bindLog = "";
|
||||
protected PreparedStatement pstmt;
|
||||
protected long startNano;
|
||||
protected Binder binder;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
@@ -144,6 +145,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
|
||||
}
|
||||
|
||||
protected void executeAsSql(Binder binder) throws SQLException {
|
||||
this.binder = binder;
|
||||
lock.lock();
|
||||
try {
|
||||
query.checkCancelled();
|
||||
|
||||
@@ -188,7 +188,10 @@ final class DefaultBeanLoader {
|
||||
query.setLazyLoadProperty(ebi.lazyLoadProperty());
|
||||
if (draft) {
|
||||
query.asDraft();
|
||||
} else if (mode == SpiQuery.Mode.LAZYLOAD_BEAN && desc.isSoftDelete()) {
|
||||
} else if (desc.isSoftDelete()
|
||||
&& (mode == SpiQuery.Mode.LAZYLOAD_BEAN || mode == SpiQuery.Mode.REFRESH_BEAN)) {
|
||||
// include soft-deleted rows when lazy loading or refreshing so a
|
||||
// refresh() on a soft-deleted bean can reload it (issue #3641)
|
||||
query.setIncludeSoftDeletes();
|
||||
}
|
||||
if (embeddedOwnerIndex > -1) {
|
||||
|
||||
@@ -2276,7 +2276,10 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
|
||||
if (initRequest.isAll()) {
|
||||
queryPlanManager.setDefaultThreshold(initRequest.thresholdMicros());
|
||||
}
|
||||
return descriptorManager.queryPlanInit(initRequest);
|
||||
List<MetaQueryPlan> list = descriptorManager.queryPlanInit(initRequest);
|
||||
dtoBeanManager.queryPlanInit(initRequest, list);
|
||||
relationalQueryEngine.queryPlanInit(initRequest, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
List<MetaQueryPlan> queryPlanCollectNow(QueryPlanRequest request) {
|
||||
|
||||
@@ -2,9 +2,11 @@ package io.ebeaninternal.server.core;
|
||||
|
||||
import io.ebean.QueryIterator;
|
||||
import io.ebean.core.type.DataReader;
|
||||
import io.ebeaninternal.api.CoreLog;
|
||||
import io.ebeaninternal.api.SpiDtoQuery;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.server.bind.DataBindCapture;
|
||||
import io.ebeaninternal.server.dto.DtoColumn;
|
||||
import io.ebeaninternal.server.dto.DtoMappingRequest;
|
||||
import io.ebeaninternal.server.dto.DtoQueryPlan;
|
||||
@@ -19,6 +21,8 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
|
||||
/**
|
||||
* Wraps the objects involved in executing a DtoQuery.
|
||||
*/
|
||||
@@ -87,6 +91,22 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
|
||||
if (plan != null) {
|
||||
long exeMicros = (System.nanoTime() - startNano) / 1000L;
|
||||
plan.collect(exeMicros);
|
||||
// native SQL only (binder set in executeAsSql); ORM-backed DTO queries
|
||||
// capture the query plan via the underlying ORM query plan instead.
|
||||
if (binder != null && plan.collectFor(exeMicros)) {
|
||||
captureBindForQueryPlan(exeMicros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureBindForQueryPlan(long exeMicros) {
|
||||
final long startNanos = System.nanoTime();
|
||||
try {
|
||||
DataBindCapture capture = DataBindCapture.of(server.dataTimeZone());
|
||||
binder.bind(query.getBindParams(), capture, new StringBuilder());
|
||||
plan.setBind(capture.bindCapture(), exeMicros, startNanos);
|
||||
} catch (SQLException e) {
|
||||
CoreLog.log.log(ERROR, "Error capturing DTO bind values", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +147,7 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
|
||||
}
|
||||
|
||||
private DtoMappingRequest mappingRequest() throws SQLException {
|
||||
return new DtoMappingRequest(query, sql, readMeta());
|
||||
return new DtoMappingRequest(server, query, sql, readMeta());
|
||||
}
|
||||
|
||||
private DtoColumn[] readMeta() throws SQLException {
|
||||
|
||||
@@ -306,7 +306,7 @@ public final class InternalConfiguration {
|
||||
|
||||
RelationalQueryEngine createRelationalQueryEngine() {
|
||||
return new DefaultRelationalQueryEngine(binder, config.getDatabaseBooleanTrue(), config.getPlatformConfig().getDbUuid().useBinaryOptimized(),
|
||||
config.getJdbcFetchSizeFindEach(), config.getJdbcFetchSizeFindList(), databasePlatform.autoCommitFalseOnFindIterate());
|
||||
config.getJdbcFetchSizeFindEach(), config.getJdbcFetchSizeFindList(), databasePlatform.autoCommitFalseOnFindIterate(), config.isQueryPlanEnable());
|
||||
}
|
||||
|
||||
OrmQueryEngine createOrmQueryEngine() {
|
||||
|
||||
@@ -4,7 +4,11 @@ package io.ebeaninternal.server.core;
|
||||
import io.ebean.RowConsumer;
|
||||
import io.ebean.RowMapper;
|
||||
import io.ebean.SqlRow;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.server.query.SqlQueryPlan;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@@ -57,6 +61,23 @@ public interface RelationalQueryEngine {
|
||||
*/
|
||||
void collect(String label, long exeMicros);
|
||||
|
||||
/**
|
||||
* Return true if query plan capture is enabled (the master switch). When false
|
||||
* no SqlQuery plans are created or cached, so labelled queries incur no extra cost.
|
||||
*/
|
||||
boolean captureActive();
|
||||
|
||||
/**
|
||||
* Obtain (creating and caching if necessary) the query plan for a labelled
|
||||
* native SqlQuery, used to capture bind values for database query plan collection.
|
||||
*/
|
||||
SqlQueryPlan obtainPlan(String label, String sql, SpiEbeanServer server);
|
||||
|
||||
/**
|
||||
* Initiate query plan bind capture for matching SqlQuery plans.
|
||||
*/
|
||||
void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list);
|
||||
|
||||
/**
|
||||
* Visit the metrics.
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package io.ebeaninternal.server.core;
|
||||
|
||||
import io.ebean.*;
|
||||
import io.ebeaninternal.api.CoreLog;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiSqlQuery;
|
||||
import io.ebeaninternal.server.bind.DataBindCapture;
|
||||
import io.ebeaninternal.server.query.SqlQueryPlan;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
@@ -12,6 +15,8 @@ import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
|
||||
/**
|
||||
* Wraps the objects involved in executing a SqlQuery.
|
||||
*/
|
||||
@@ -39,9 +44,28 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
|
||||
@Override
|
||||
protected void requestComplete() {
|
||||
String label = query.getLabel();
|
||||
if (label != null) {
|
||||
long exeMicros = (System.nanoTime() - startNano) / 1000L;
|
||||
queryEngine.collect(label, exeMicros);
|
||||
if (label == null) {
|
||||
return;
|
||||
}
|
||||
long exeMicros = (System.nanoTime() - startNano) / 1000L;
|
||||
queryEngine.collect(label, exeMicros);
|
||||
// capture bind values to later collect the database query plan
|
||||
if (binder != null && queryEngine.captureActive()) {
|
||||
SqlQueryPlan plan = queryEngine.obtainPlan(label, sql, server);
|
||||
if (plan.collectFor(exeMicros)) {
|
||||
captureBindForQueryPlan(plan, exeMicros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void captureBindForQueryPlan(SqlQueryPlan plan, long exeMicros) {
|
||||
final long startNanos = System.nanoTime();
|
||||
try {
|
||||
DataBindCapture capture = DataBindCapture.of(server.dataTimeZone());
|
||||
binder.bind(query.getBindParams(), capture, new StringBuilder());
|
||||
plan.setBind(capture.bindCapture(), exeMicros, startNanos);
|
||||
} catch (SQLException e) {
|
||||
CoreLog.log.log(ERROR, "Error capturing SqlQuery bind values", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.ebeaninternal.server.dto;
|
||||
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -43,6 +46,18 @@ public final class DtoBeanDescriptor<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm bind capture for matching (native SQL) DTO query plans.
|
||||
*/
|
||||
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
|
||||
for (DtoQueryPlan plan : plans.values()) {
|
||||
if (plan.supportsPlanCapture() && request.includeHash(plan.hash())) {
|
||||
plan.queryPlanInit(request.thresholdMicros(plan.hash()));
|
||||
list.add(plan.createMeta(null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the named RawSql query.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package io.ebeaninternal.server.dto;
|
||||
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebeaninternal.server.type.TypeManager;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -51,4 +54,14 @@ public final class DtoBeanManager {
|
||||
value.visit(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Arm bind capture for matching (native SQL) DTO query plans.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
|
||||
for (DtoBeanDescriptor value : descriptorMap.values()) {
|
||||
value.queryPlanInit(request, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,26 +4,56 @@ import io.ebean.ProfileLocation;
|
||||
import io.ebean.metric.MetricFactory;
|
||||
import io.ebean.metric.QueryPlanMetric;
|
||||
import io.ebeaninternal.api.SpiDtoQuery;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQueryBindCapture;
|
||||
import io.ebeaninternal.api.SpiQueryPlan;
|
||||
import io.ebeaninternal.server.query.CQueryPlan;
|
||||
import io.ebeaninternal.server.util.Md5;
|
||||
|
||||
/**
|
||||
* Request to map a resultSet columns for a query into a DTO bean.
|
||||
*/
|
||||
public final class DtoMappingRequest {
|
||||
|
||||
private final SpiEbeanServer server;
|
||||
private final Class type;
|
||||
private final String label;
|
||||
private final ProfileLocation profileLocation;
|
||||
private final String sql;
|
||||
private final boolean relaxedMode;
|
||||
private final DtoColumn[] columnMeta;
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final boolean nativeSql;
|
||||
|
||||
public DtoMappingRequest(SpiDtoQuery query, String sql, DtoColumn[] columnMeta) {
|
||||
public DtoMappingRequest(SpiEbeanServer server, SpiDtoQuery<?> query, String sql, DtoColumn[] columnMeta) {
|
||||
this.server = server;
|
||||
this.type = query.type();
|
||||
this.label = query.planLabel();
|
||||
this.profileLocation = query.profileLocation();
|
||||
this.sql = sql;
|
||||
this.relaxedMode = query.isRelaxedMode();
|
||||
this.columnMeta = columnMeta;
|
||||
this.nativeSql = query.ormQuery() == null;
|
||||
this.name = deriveName(type.getSimpleName(), query.explicitLabel(), profileLocation);
|
||||
String loc = profileLocation == null ? null : profileLocation.location();
|
||||
this.hash = Md5.hash(sql, name, loc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the DTO query plan / metric name, mirroring the ORM convention:
|
||||
* an explicit label is prefixed with the bean type for disambiguation, a
|
||||
* profile location is used as-is (already a unique, type-independent
|
||||
* identifier) and an unlabelled query uses just the bean type.
|
||||
*/
|
||||
private static String deriveName(String simpleName, String explicitLabel, ProfileLocation profileLocation) {
|
||||
if (explicitLabel != null) {
|
||||
return "dto." + CQueryPlan.planLabelWithType(explicitLabel, simpleName);
|
||||
}
|
||||
if (profileLocation != null) {
|
||||
return "dto." + profileLocation.label();
|
||||
}
|
||||
return "dto." + simpleName;
|
||||
}
|
||||
|
||||
public DtoColumn[] columnMeta() {
|
||||
@@ -42,8 +72,41 @@ public final class DtoMappingRequest {
|
||||
return sql;
|
||||
}
|
||||
|
||||
public Class<?> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public ProfileLocation profileLocation() {
|
||||
return profileLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if this is a native SQL DtoQuery (capturable). ORM-backed DTO
|
||||
* queries capture their query plan via the underlying ORM query plan instead.
|
||||
*/
|
||||
public boolean nativeSql() {
|
||||
return nativeSql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the bind capture for the given query plan. Returns the NOOP capture
|
||||
* for ORM-backed DTO queries (captured via the ORM plan) or when query plan
|
||||
* collection is disabled.
|
||||
*/
|
||||
public SpiQueryBindCapture createBindCapture(SpiQueryPlan queryPlan) {
|
||||
return nativeSql ? server.createQueryBindCapture(queryPlan) : SpiQueryBindCapture.NOOP;
|
||||
}
|
||||
|
||||
public QueryPlanMetric createMetric() {
|
||||
return MetricFactory.get().createQueryPlanMetric(type, label, profileLocation, sql);
|
||||
return MetricFactory.get().createQueryPlanMetric(type, name, label, profileLocation, sql, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,13 +2,15 @@ package io.ebeaninternal.server.dto;
|
||||
|
||||
import io.ebean.core.type.DataReader;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebeaninternal.api.SpiQueryPlan;
|
||||
import io.ebeaninternal.server.bind.capture.BindCapture;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Knows how to read and map rows into a Bean.
|
||||
*/
|
||||
public interface DtoQueryPlan {
|
||||
public interface DtoQueryPlan extends SpiQueryPlan {
|
||||
|
||||
/**
|
||||
* Read the row data and return the DTO bean.
|
||||
@@ -20,6 +22,22 @@ public interface DtoQueryPlan {
|
||||
*/
|
||||
void collect(long exeMicros);
|
||||
|
||||
/**
|
||||
* Return true if the bind values for this (native SQL) query should be
|
||||
* captured in order to later collect the database query plan.
|
||||
*/
|
||||
boolean collectFor(long exeMicros);
|
||||
|
||||
/**
|
||||
* Return true if this plan supports query plan capture (native SQL only).
|
||||
*/
|
||||
boolean supportsPlanCapture();
|
||||
|
||||
/**
|
||||
* Set the captured bind values used to later collect the database query plan.
|
||||
*/
|
||||
void setBind(BindCapture bindCapture, long exeMicros, long startNanos);
|
||||
|
||||
/**
|
||||
* Visit the metric (if not empty).
|
||||
*/
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
package io.ebeaninternal.server.dto;
|
||||
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.metric.QueryPlanMetric;
|
||||
import io.ebean.metric.TimedMetric;
|
||||
import io.ebeaninternal.api.SpiDbQueryPlan;
|
||||
import io.ebeaninternal.api.SpiQueryBindCapture;
|
||||
import io.ebeaninternal.api.SpiQueryPlan;
|
||||
import io.ebeaninternal.server.bind.capture.BindCapture;
|
||||
import io.ebeaninternal.server.query.DQueryPlanOutput;
|
||||
|
||||
abstract class DtoQueryPlanBase implements DtoQueryPlan {
|
||||
abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
|
||||
|
||||
private final QueryPlanMetric planMetric;
|
||||
|
||||
private final TimedMetric metric;
|
||||
private final Class<?> beanType;
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String sql;
|
||||
private final ProfileLocation profileLocation;
|
||||
private final boolean nativeSql;
|
||||
private final SpiQueryBindCapture bindCapture;
|
||||
|
||||
DtoQueryPlanBase(DtoMappingRequest request) {
|
||||
this.planMetric = request.createMetric();
|
||||
this.metric = planMetric.metric();
|
||||
this.beanType = request.type();
|
||||
this.name = request.name();
|
||||
this.hash = request.hash();
|
||||
this.sql = request.sql();
|
||||
this.profileLocation = request.profileLocation();
|
||||
this.nativeSql = request.nativeSql();
|
||||
this.bindCapture = request.createBindCapture(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -20,8 +39,58 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan {
|
||||
metric.add(exeTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean collectFor(long exeMicros) {
|
||||
return bindCapture.collectFor(exeMicros);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPlanCapture() {
|
||||
return nativeSql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBind(BindCapture capture, long exeMicros, long startNanos) {
|
||||
bindCapture.setBind(capture, exeMicros, startNanos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MetricVisitor visitor) {
|
||||
planMetric.visit(visitor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> beanType() {
|
||||
return beanType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileLocation profileLocation() {
|
||||
return profileLocation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryPlanInit(long thresholdMicros) {
|
||||
bindCapture.queryPlanInit(thresholdMicros);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiDbQueryPlan createMeta(String bind, String planString) {
|
||||
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ abstract class DLoadBaseContext {
|
||||
|
||||
protected final String fullPath;
|
||||
|
||||
protected final String path;
|
||||
|
||||
protected final String serverName;
|
||||
|
||||
final OrmQueryProperties queryProps;
|
||||
@@ -38,6 +40,7 @@ abstract class DLoadBaseContext {
|
||||
this.serverName = parent.server().name();
|
||||
this.desc = desc;
|
||||
this.queryProps = queryProps;
|
||||
this.path = path;
|
||||
this.fullPath = parent.fullPath(path);
|
||||
this.hitCache = parent.isBeanCacheGet() && desc.isBeanCaching();
|
||||
this.objectGraphNode = parent.objectGraphNode(path);
|
||||
@@ -50,15 +53,21 @@ abstract class DLoadBaseContext {
|
||||
* set onto the secondary query.
|
||||
*/
|
||||
void setLabel(SpiQuery<?> query) {
|
||||
String label = parent.planLabel();
|
||||
if (label != null) {
|
||||
query.setProfilePath(label, pathMode(query), parent.profileLocation());
|
||||
String prefix = parent.namePrefix();
|
||||
if (prefix != null) {
|
||||
query.setProfilePath(prefix, pathMode(query), parent.profileLocation());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the parent query's full name with this load's immediate path segment
|
||||
* and load mode. The parent name already carries everything up to this point,
|
||||
* so only the immediate path (not the root relative fullPath) is appended to
|
||||
* avoid duplicating intermediate path segments on nested secondary loads.
|
||||
*/
|
||||
private String pathMode(SpiQuery<?> query) {
|
||||
final var loadMode = query.loadMode();
|
||||
return fullPath == null ? '_' + loadMode : fullPath + "__" + loadMode;
|
||||
return path == null ? loadMode : path + '.' + loadMode;
|
||||
}
|
||||
|
||||
PersistenceContext persistenceContext() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.ebeaninternal.server.autotune.ProfilingListener;
|
||||
import io.ebeaninternal.server.core.OrmQueryRequest;
|
||||
import io.ebeaninternal.server.deploy.*;
|
||||
import io.ebeaninternal.server.el.ElPropertyValue;
|
||||
import io.ebeaninternal.server.query.CQueryPlan;
|
||||
import io.ebeaninternal.server.querydefn.OrmQueryProperties;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
@@ -53,7 +54,7 @@ public final class DLoadContext implements LoadContext {
|
||||
*/
|
||||
private final String relativePath;
|
||||
private final ObjectGraphOrigin origin;
|
||||
private final String planLabel;
|
||||
private final String namePrefix;
|
||||
private final ProfileLocation profileLocation;
|
||||
private final ProfilingListener profilingListener;
|
||||
private final Map<String, ObjectGraphNode> nodePathMap = new HashMap<>();
|
||||
@@ -81,7 +82,7 @@ public final class DLoadContext implements LoadContext {
|
||||
this.disableReadAudit = false;
|
||||
this.includeSoftDeletes = false;
|
||||
this.relativePath = null;
|
||||
this.planLabel = null;
|
||||
this.namePrefix = null;
|
||||
this.profileLocation = null;
|
||||
this.profilingListener = null;
|
||||
this.immutableCaches = Collections.emptyMap();
|
||||
@@ -111,7 +112,7 @@ public final class DLoadContext implements LoadContext {
|
||||
this.disableLazyLoading = query.isDisableLazyLoading();
|
||||
this.useBeanCache = query.beanCacheMode();
|
||||
this.profilingListener = query.profilingListener();
|
||||
this.planLabel = query.planLabel();
|
||||
this.namePrefix = deriveNamePrefix(query);
|
||||
this.profileLocation = query.profileLocation();
|
||||
this.immutableCaches = query.immutableBeanCaches();
|
||||
this.secondaryProperties = query.isUnmodifiable() ? new HashSet<>() : null;
|
||||
@@ -130,10 +131,41 @@ public final class DLoadContext implements LoadContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query plan label of the origin query.
|
||||
* Build the name prefix that secondary (lazy/query) loads extend with their
|
||||
* path and load mode. This is the parent query's full plan name (without the
|
||||
* leading "orm.").
|
||||
* <p>
|
||||
* For a root query with an explicit {@code setLabel(..)} that is the bean type
|
||||
* combined with the label (for disambiguation). For a profile location the
|
||||
* label is already a unique, type-independent identifier so it is used as-is.
|
||||
* For a parent that is itself a secondary query the plan label already carries
|
||||
* the full name, so it is used as-is (avoiding a duplicate prefix on nested
|
||||
* lazy loads).
|
||||
*/
|
||||
String planLabel() {
|
||||
return planLabel;
|
||||
@Nullable
|
||||
private String deriveNamePrefix(SpiQuery<?> query) {
|
||||
final String label = query.planLabel();
|
||||
if (label == null) {
|
||||
return null;
|
||||
}
|
||||
if (query.loadMode() != null) {
|
||||
// parent is itself a secondary - label already carries the full name
|
||||
return label;
|
||||
}
|
||||
if (query.label() != null) {
|
||||
// explicit setLabel - prefix the bean type for disambiguation
|
||||
return CQueryPlan.planLabelWithType(label, rootDescriptor.simpleName());
|
||||
}
|
||||
// profile location - already a unique, type-independent identifier
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the parent name prefix that secondary queries extend, or null.
|
||||
*/
|
||||
@Nullable
|
||||
String namePrefix() {
|
||||
return namePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,8 +28,8 @@ public final class DMetricFactory implements MetricFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryPlanMetric createQueryPlanMetric(Class<?> type, String label, ProfileLocation profileLocation, String sql) {
|
||||
return new DQueryPlanMetric(new DQueryPlanMeta(type, label, profileLocation, sql), new DTimedMetric(label));
|
||||
public QueryPlanMetric createQueryPlanMetric(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash) {
|
||||
return new DQueryPlanMetric(new DQueryPlanMeta(type, name, label, profileLocation, sql, hash), new DTimedMetric(label));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.ebeaninternal.server.profile;
|
||||
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebeaninternal.server.util.Md5;
|
||||
|
||||
final class DQueryPlanMeta {
|
||||
|
||||
@@ -12,18 +11,13 @@ final class DQueryPlanMeta {
|
||||
private final String sql;
|
||||
private final String hash;
|
||||
|
||||
DQueryPlanMeta(Class<?> type, String label, ProfileLocation profileLocation, String sql) {
|
||||
DQueryPlanMeta(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash) {
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
this.profileLocation = profileLocation;
|
||||
this.sql = sql;
|
||||
String name = "dto." + type.getSimpleName();
|
||||
if (label != null) {
|
||||
name += "_" + label;
|
||||
}
|
||||
this.name = name;
|
||||
String loc = profileLocation == null ? null : profileLocation.location();
|
||||
this.hash = Md5.hash(sql, name, loc);
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
|
||||
@@ -569,7 +569,7 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId(), query.getGeneratedSql());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
QueryIterator<T> readIterate(int bufferSize, OrmQueryRequest<T> request) {
|
||||
|
||||
@@ -321,6 +321,13 @@ final class CQueryBuilder {
|
||||
return lastFound;
|
||||
}
|
||||
|
||||
static String inlineSqlCommentLabel(String label, ProfileLocation profileLocation, boolean secondary, String simpleName) {
|
||||
if (label != null) {
|
||||
return secondary ? label : CQueryPlan.planLabelWithType(label, simpleName);
|
||||
}
|
||||
return profileLocation == null ? null : profileLocation.label();
|
||||
}
|
||||
|
||||
private String wrapSelectCount(String sql) {
|
||||
sql = "select count(*) from ( " + sql + ")";
|
||||
if (selectCountWithAlias) {
|
||||
@@ -645,14 +652,10 @@ final class CQueryBuilder {
|
||||
if (type == SpiQuery.Type.SQ_EX || type == SpiQuery.Type.SQ_EXISTS) {
|
||||
return "";
|
||||
}
|
||||
final var label = query.label();
|
||||
final var label = CQueryBuilder.inlineSqlCommentLabel(query.label(), query.profileLocation(), query.loadMode() != null, request.descriptor().simpleName());
|
||||
if (label != null) {
|
||||
return dbPlatform.inlineSqlComment(label);
|
||||
}
|
||||
final var profileLocation = query.profileLocation();
|
||||
if (profileLocation != null) {
|
||||
return dbPlatform.inlineSqlComment(profileLocation.label());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ final class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent, Ca
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
Set<String> dependentTables() {
|
||||
|
||||
@@ -88,7 +88,7 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
this.profileLocation = query.profileLocation();
|
||||
this.location = (profileLocation == null) ? null : profileLocation.location();
|
||||
this.label = query.planLabel();
|
||||
this.name = deriveName(label, query.type(), request.descriptor().simpleName());
|
||||
this.name = deriveName(label, query, request.descriptor().simpleName());
|
||||
this.asOfTableCount = query.getAsOfTableCount();
|
||||
this.sql = sqlRes.getSql();
|
||||
this.sqlTree = sqlTree;
|
||||
@@ -112,7 +112,7 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
this.profileLocation = query.profileLocation();
|
||||
this.location = (profileLocation == null) ? null : profileLocation.location();
|
||||
this.label = query.planLabel();
|
||||
this.name = deriveName(label, query.type(), request.descriptor().simpleName());
|
||||
this.name = deriveName(label, query, request.descriptor().simpleName());
|
||||
this.planKey = buildPlanKey(sql, logWhereSql);
|
||||
this.asOfTableCount = 0;
|
||||
this.sql = sql;
|
||||
@@ -126,19 +126,58 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
this.hash = Md5.hash(sql, name, location);
|
||||
}
|
||||
|
||||
private String deriveName(String label, SpiQuery.Type type, String simpleName) {
|
||||
private String deriveName(String label, SpiQuery<?> query, String simpleName) {
|
||||
return deriveName(label, query.loadMode() != null, query.label() != null, query.type().label(), simpleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the query plan / metric name.
|
||||
*
|
||||
* @param label the query plan label (null for an unlabelled query)
|
||||
* @param secondary true if this is a secondary query (lazy or query load) where the label
|
||||
* already carries the parent profile path, so the bean type is not prefixed
|
||||
* @param explicitLabel true if the label came from an explicit {@code setLabel(..)} (as opposed
|
||||
* to a profile location). An explicit label is prefixed with the bean type for
|
||||
* disambiguation; a profile location is already a unique, type-independent
|
||||
* identifier and is used as-is.
|
||||
* @param typeLabel the query type label (used only when there is no plan label)
|
||||
* @param simpleName the bean descriptor simple name
|
||||
*/
|
||||
static String deriveName(String label, boolean secondary, boolean explicitLabel, String typeLabel, String simpleName) {
|
||||
if (label == null) {
|
||||
return Str.add("orm.", simpleName, ".", type.label());
|
||||
return Str.add("orm.", simpleName, ".", typeLabel);
|
||||
}
|
||||
if (secondary) {
|
||||
// secondary (lazy/query) load - the label already carries the full parent
|
||||
// name plus the path and load mode, so use it verbatim
|
||||
return Str.add("orm.", label);
|
||||
}
|
||||
if (explicitLabel) {
|
||||
return Str.add("orm.", planLabelWithType(label, simpleName));
|
||||
}
|
||||
// profile location - already a unique, type-independent identifier
|
||||
return Str.add("orm.", label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine the bean type simple name with an explicit (non-null) {@code setLabel}.
|
||||
* <p>
|
||||
* Used for a root query name and the parent prefix that secondary (lazy/query)
|
||||
* loads extend, when the name comes from an explicit label. The bean type is
|
||||
* prefixed for disambiguation, except when the label already starts with the
|
||||
* bean type (avoiding duplication). Profile locations are not routed through
|
||||
* here - they are unique identifiers and used as-is.
|
||||
*/
|
||||
public static String planLabelWithType(String label, String simpleName) {
|
||||
int pos = simpleName.indexOf('.');
|
||||
if (pos > 1) {
|
||||
// element collection and label
|
||||
return Str.add("orm.", simpleName.substring(0, pos), "_", label);
|
||||
return Str.add(simpleName.substring(0, pos), "_", label);
|
||||
}
|
||||
if (label.startsWith(simpleName)) {
|
||||
return Str.add("orm.", label);
|
||||
return label;
|
||||
}
|
||||
return Str.add("orm.", simpleName, "_", label);
|
||||
return Str.add(simpleName, ".", label);
|
||||
}
|
||||
|
||||
private SpiQueryBindCapture initBindCapture(SpiQuery<?> query) {
|
||||
|
||||
@@ -137,7 +137,7 @@ final class CQueryRowCount implements SpiProfileTransactionEvent, CancelableQuer
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
Set<String> dependentTables() {
|
||||
|
||||
@@ -115,7 +115,7 @@ final class CQueryUpdate implements SpiProfileTransactionEvent, CancelableQuery
|
||||
public void profile() {
|
||||
transaction()
|
||||
.profileStream()
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
|
||||
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.time.Instant;
|
||||
/**
|
||||
* Captured query plan details.
|
||||
*/
|
||||
final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
|
||||
private final Class<?> beanType;
|
||||
private final String label;
|
||||
@@ -24,7 +24,7 @@ final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
private long captureMicros;
|
||||
private Instant whenCaptured;
|
||||
|
||||
DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
|
||||
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
|
||||
this.beanType = beanType;
|
||||
this.label = label;
|
||||
this.hash = hash;
|
||||
|
||||
+29
-1
@@ -5,9 +5,12 @@ import io.ebean.RowMapper;
|
||||
import io.ebean.SqlRow;
|
||||
import io.ebean.core.type.DataReader;
|
||||
import io.ebean.core.type.ScalarType;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebean.metric.MetricFactory;
|
||||
import io.ebean.metric.TimedMetricMap;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQuery;
|
||||
import io.ebeaninternal.server.core.RelationalQueryEngine;
|
||||
import io.ebeaninternal.server.core.RelationalQueryRequest;
|
||||
@@ -20,6 +23,7 @@ import jakarta.persistence.PersistenceException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -32,17 +36,20 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine
|
||||
private final String dbTrueValue;
|
||||
private final boolean binaryOptimizedUUID;
|
||||
private final boolean autoCommitFalseOnFindIterate;
|
||||
private final boolean queryPlanCapture;
|
||||
private final TimedMetricMap timedMetricMap;
|
||||
private final ConcurrentHashMap<String, SqlQueryPlan> plans = new ConcurrentHashMap<>();
|
||||
private final int defaultFetchSizeFindEach;
|
||||
private final int defaultFetchSizeFindList;
|
||||
|
||||
public DefaultRelationalQueryEngine(Binder binder, String dbTrueValue, boolean binaryOptimizedUUID,
|
||||
int defaultFetchSizeFindEach, int defaultFetchSizeFindList,
|
||||
boolean autoCommitFalseOnFindIterate) {
|
||||
boolean autoCommitFalseOnFindIterate, boolean queryPlanCapture) {
|
||||
this.binder = binder;
|
||||
this.dbTrueValue = dbTrueValue == null ? "true" : dbTrueValue;
|
||||
this.binaryOptimizedUUID = binaryOptimizedUUID;
|
||||
this.autoCommitFalseOnFindIterate = autoCommitFalseOnFindIterate;
|
||||
this.queryPlanCapture = queryPlanCapture;
|
||||
this.timedMetricMap = MetricFactory.get().createTimedMetricMap("sql.query.");
|
||||
this.defaultFetchSizeFindEach = defaultFetchSizeFindEach;
|
||||
this.defaultFetchSizeFindList = defaultFetchSizeFindList;
|
||||
@@ -53,6 +60,27 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine
|
||||
timedMetricMap.add(label, exeMicros);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean captureActive() {
|
||||
return queryPlanCapture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlQueryPlan obtainPlan(String label, String sql, SpiEbeanServer server) {
|
||||
return plans.computeIfAbsent(label + ':' + sql,
|
||||
key -> new SqlQueryPlan(server, "sql.query." + label, sql));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
|
||||
for (SqlQueryPlan plan : plans.values()) {
|
||||
if (request.includeHash(plan.hash())) {
|
||||
plan.queryPlanInit(request.thresholdMicros(plan.hash()));
|
||||
list.add(plan.createMeta(null, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitMetrics(MetricVisitor visitor) {
|
||||
timedMetricMap.visit(visitor);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebean.ProfileLocation;
|
||||
import io.ebeaninternal.api.SpiDbQueryPlan;
|
||||
import io.ebeaninternal.api.SpiEbeanServer;
|
||||
import io.ebeaninternal.api.SpiQueryBindCapture;
|
||||
import io.ebeaninternal.api.SpiQueryPlan;
|
||||
import io.ebeaninternal.server.bind.capture.BindCapture;
|
||||
import io.ebeaninternal.server.util.Md5;
|
||||
|
||||
/**
|
||||
* Query plan for a native SQL {@code SqlQuery}.
|
||||
* <p>
|
||||
* Unlike ORM and DTO query plans there is no associated bean type. The timing
|
||||
* metric is held separately by the relational query engine (the {@code sql.query.*}
|
||||
* {@code TimedMetricMap}); this plan only owns the bind capture used to later
|
||||
* collect the database query plan via EXPLAIN.
|
||||
*/
|
||||
public final class SqlQueryPlan implements SpiQueryPlan {
|
||||
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String sql;
|
||||
private final SpiQueryBindCapture bindCapture;
|
||||
|
||||
SqlQueryPlan(SpiEbeanServer server, String name, String sql) {
|
||||
this.name = name;
|
||||
this.sql = sql;
|
||||
this.hash = Md5.hash(sql, name);
|
||||
this.bindCapture = server.createQueryBindCapture(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the bind values for this query should be captured (based on
|
||||
* the query execution time exceeding the armed threshold).
|
||||
*/
|
||||
public boolean collectFor(long exeMicros) {
|
||||
return bindCapture.collectFor(exeMicros);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the captured bind values used to later collect the database query plan.
|
||||
*/
|
||||
public void setBind(BindCapture capture, long exeMicros, long startNanos) {
|
||||
bindCapture.setBind(capture, exeMicros, startNanos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> beanType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String sql() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProfileLocation profileLocation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queryPlanInit(long thresholdMicros) {
|
||||
bindCapture.queryPlanInit(thresholdMicros);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpiDbQueryPlan createMeta(String bind, String planString) {
|
||||
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
|
||||
}
|
||||
}
|
||||
@@ -273,6 +273,12 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String explicitLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void obtainLocation() {
|
||||
if (profileLocation != null) {
|
||||
|
||||
@@ -255,9 +255,11 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setProfilePath(String label, String relativePath, @Nullable ProfileLocation profileLocation) {
|
||||
public final void setProfilePath(String parentName, String relativePath, @Nullable ProfileLocation profileLocation) {
|
||||
this.profileLocation = profileLocation;
|
||||
this.label = (profileLocation == null ? label : profileLocation.label()) + '_' + relativePath;
|
||||
// 'parentName' is the parent query's full plan name (bean type + label, or
|
||||
// the full name of a parent secondary query) so extend it directly.
|
||||
this.label = parentName + '.' + relativePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,7 +13,7 @@ public interface ProfileStream {
|
||||
/**
|
||||
* Add a query event.
|
||||
*/
|
||||
void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String sql);
|
||||
void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String hash, String sql);
|
||||
|
||||
/**
|
||||
* Add a persist event.
|
||||
|
||||
@@ -16,7 +16,7 @@ class DQueryPlanMetricTest {
|
||||
@Test
|
||||
void visit() {
|
||||
|
||||
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "lab", null, "sql");
|
||||
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "dto.Object.lab", "lab", null, "sql", "hash");
|
||||
DTimedMetric metric = new DTimedMetric("org.timed.plan");
|
||||
DQueryPlanMetric planMetric = new DQueryPlanMetric(meta, metric);
|
||||
|
||||
@@ -28,7 +28,7 @@ class DQueryPlanMetricTest {
|
||||
List<MetaQueryMetric> result = visitor.queryMetrics();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object_lab]");
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
|
||||
assertThat(result.get(0).count()).isEqualTo(2);
|
||||
assertThat(result.get(0).total()).isEqualTo(820);
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class DQueryPlanMetricTest {
|
||||
List<MetaQueryMetric> result = visitor.queryMetrics();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object_lab]");
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
|
||||
assertThat(result.get(0).count()).isEqualTo(1);
|
||||
assertThat(result.get(0).total()).isEqualTo(410);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class DQueryPlanMetricTest {
|
||||
@Test
|
||||
void visitCumulativeResetsMax() {
|
||||
|
||||
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "lab", null, "sql");
|
||||
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "dto.Object.lab", "lab", null, "sql", "hash");
|
||||
DTimedMetric metric = new DTimedMetric("org.timed.plan");
|
||||
DQueryPlanMetric planMetric = new DQueryPlanMetric(meta, metric);
|
||||
|
||||
@@ -60,7 +60,7 @@ class DQueryPlanMetricTest {
|
||||
List<MetaQueryMetric> result = visitor.queryMetrics();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object_lab]");
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
|
||||
assertThat(result.get(0).count()).isEqualTo(2);
|
||||
assertThat(result.get(0).total()).isEqualTo(820);
|
||||
assertThat(result.get(0).max()).isEqualTo(560);
|
||||
@@ -71,7 +71,7 @@ class DQueryPlanMetricTest {
|
||||
List<MetaQueryMetric> result = visitor.queryMetrics();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object_lab]");
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
|
||||
assertThat(result.get(0).count()).isEqualTo(2);
|
||||
assertThat(result.get(0).total()).isEqualTo(820);
|
||||
assertThat(result.get(0).max()).isEqualTo(0);
|
||||
@@ -84,7 +84,7 @@ class DQueryPlanMetricTest {
|
||||
List<MetaQueryMetric> result = visitor.queryMetrics();
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object_lab]");
|
||||
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
|
||||
assertThat(result.get(0).count()).isEqualTo(3);
|
||||
assertThat(result.get(0).total()).isEqualTo(1230);
|
||||
assertThat(result.get(0).max()).isEqualTo(410);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import io.ebean.ProfileLocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -115,4 +116,65 @@ class CQueryBuilderTest {
|
||||
|
||||
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inlineSqlCommentLabel_rootExplicitLabel_prefixesBeanType() {
|
||||
String label = CQueryBuilder.inlineSqlCommentLabel("fetchMachineFleets", null, false, "COrganisationMachine");
|
||||
assertThat(label).isEqualTo("COrganisationMachine.fetchMachineFleets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inlineSqlCommentLabel_rootExplicitLabel_avoidsDuplicatePrefix() {
|
||||
String label = CQueryBuilder.inlineSqlCommentLabel("COrganisationMachine.fetchMachineFleets", null, false, "COrganisationMachine");
|
||||
assertThat(label).isEqualTo("COrganisationMachine.fetchMachineFleets");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inlineSqlCommentLabel_secondaryLabel_usedAsIs() {
|
||||
String label = CQueryBuilder.inlineSqlCommentLabel("COrganisationMachine.fetchMachineFleets.contacts.lazy", null, true, "Contact");
|
||||
assertThat(label).isEqualTo("COrganisationMachine.fetchMachineFleets.contacts.lazy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inlineSqlCommentLabel_profileLocationFallback() {
|
||||
String label = CQueryBuilder.inlineSqlCommentLabel(null, profileLocation("Finder.byName"), false, "COrganisationMachine");
|
||||
assertThat(label).isEqualTo("Finder.byName");
|
||||
}
|
||||
|
||||
private static ProfileLocation profileLocation(String label) {
|
||||
return new ProfileLocation() {
|
||||
@Override
|
||||
public boolean obtain() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String location() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String label() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fullLocation() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(long executionTime) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean trace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTraceCount(int traceCount) {
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.ebeaninternal.server.query;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for the query plan / metric name derivation.
|
||||
*/
|
||||
class CQueryPlanNameTest {
|
||||
|
||||
@Test
|
||||
void deriveName_root_unlabelled() {
|
||||
assertThat(CQueryPlan.deriveName(null, false, false, "findList", "Customer"))
|
||||
.isEqualTo("orm.Customer.findList");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deriveName_root_explicitLabel() {
|
||||
assertThat(CQueryPlan.deriveName("custMain", false, true, "findList", "Customer"))
|
||||
.isEqualTo("orm.Customer.custMain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deriveName_root_profileLocation_startsWithBeanType() {
|
||||
// profile location used as-is (no bean type prefix)
|
||||
assertThat(CQueryPlan.deriveName("CustomerFinder.byName", false, false, "findList", "Customer"))
|
||||
.isEqualTo("orm.CustomerFinder.byName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deriveName_root_profileLocation_differentClass() {
|
||||
// profile location is a unique, type-independent identifier - not prefixed with the bean type
|
||||
assertThat(CQueryPlan.deriveName("DataLoader.loadAll", false, false, "findList", "Customer"))
|
||||
.isEqualTo("orm.DataLoader.loadAll");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deriveName_secondary_usesLabelVerbatim() {
|
||||
// secondary query label already carries the full parent name + path + load mode
|
||||
assertThat(CQueryPlan.deriveName("Customer.custMain.contacts.lazy", true, true, "findList", "Contact"))
|
||||
.isEqualTo("orm.Customer.custMain.contacts.lazy");
|
||||
assertThat(CQueryPlan.deriveName("CustomerFinder.byName.contacts.lazy", true, true, "findList", "Contact"))
|
||||
.isEqualTo("orm.CustomerFinder.byName.contacts.lazy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planLabelWithType_explicitLabel() {
|
||||
assertThat(CQueryPlan.planLabelWithType("custMain", "Customer"))
|
||||
.isEqualTo("Customer.custMain");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planLabelWithType_startsWithBeanType() {
|
||||
assertThat(CQueryPlan.planLabelWithType("CustomerFinder.byName", "Customer"))
|
||||
.isEqualTo("CustomerFinder.byName");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planLabelWithType_differentClass() {
|
||||
assertThat(CQueryPlan.planLabelWithType("DataLoader.loadAll", "Customer"))
|
||||
.isEqualTo("Customer.DataLoader.loadAll");
|
||||
}
|
||||
|
||||
@Test
|
||||
void planLabelWithType_elementCollection() {
|
||||
// element collection descriptor simple name contains a dot - keeps underscore form
|
||||
assertThat(CQueryPlan.planLabelWithType("custMain", "Customer.phones"))
|
||||
.isEqualTo("Customer_custMain");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean ddl generation</name>
|
||||
@@ -28,14 +28,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
+12
-3
@@ -397,12 +397,21 @@ public class DefaultDbMigration implements DbMigration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write (or override) the "repeatable" migration script.
|
||||
* Write the "repeatable" or "init" migration script.
|
||||
* <p>
|
||||
* Repeatable ({@code R__}) scripts are always (re)written from their source
|
||||
* (typically {@code extra-ddl.xml} views), but an init ({@code I__}) script is
|
||||
* only written when it does not already exist. Init scripts run once to
|
||||
* bootstrap a database and can be hand-tuned after first generation.
|
||||
*/
|
||||
private void writeExtraDdl(File migrationDir, DdlScript script) throws IOException {
|
||||
void writeExtraDdl(File migrationDir, DdlScript script) throws IOException {
|
||||
String fullName = repeatableMigrationName(script.isInit(), script.getName());
|
||||
logger.log(DEBUG, "writing repeatable script {0}", fullName);
|
||||
File file = new File(migrationDir, fullName);
|
||||
if (script.isInit() && file.exists()) {
|
||||
logger.log(DEBUG, "skip existing init script {0}", fullName);
|
||||
return;
|
||||
}
|
||||
logger.log(DEBUG, "writing repeatable script {0}", fullName);
|
||||
try (Writer writer = IOUtils.newWriter(file)) {
|
||||
writer.write(script.getValue());
|
||||
writer.flush();
|
||||
|
||||
+55
@@ -1,7 +1,15 @@
|
||||
package io.ebeaninternal.dbmigration;
|
||||
|
||||
import io.ebeaninternal.extraddl.model.DdlScript;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@@ -30,4 +38,51 @@ class DefaultDbMigrationTest {
|
||||
migration.checkDropVersion("1.0", "1.0.0");
|
||||
migration.checkDropVersion("1.0", "1.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeExtraDdl_initScript_notOverwrittenWhenExists(@TempDir Path dir) throws IOException {
|
||||
File migrationDir = dir.toFile();
|
||||
|
||||
migration.writeExtraDdl(migrationDir, initScript("partition help", "ORIGINAL"));
|
||||
File initFile = new File(migrationDir, "I__partition_help.sql");
|
||||
assertThat(initFile).exists().content().isEqualTo("ORIGINAL");
|
||||
|
||||
// hand-tune the generated init script
|
||||
Files.writeString(initFile.toPath(), "CUSTOMISED");
|
||||
|
||||
// regeneration must not clobber an existing init script
|
||||
migration.writeExtraDdl(migrationDir, initScript("partition help", "ORIGINAL"));
|
||||
assertThat(initFile).content().isEqualTo("CUSTOMISED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeExtraDdl_repeatableScript_alwaysRewritten(@TempDir Path dir) throws IOException {
|
||||
File migrationDir = dir.toFile();
|
||||
|
||||
migration.writeExtraDdl(migrationDir, repeatableScript("my view", "V1"));
|
||||
File file = new File(migrationDir, "R__my_view.sql");
|
||||
assertThat(file).exists().content().isEqualTo("V1");
|
||||
|
||||
Files.writeString(file.toPath(), "STALE");
|
||||
|
||||
// repeatable scripts are regenerated from their source every time
|
||||
migration.writeExtraDdl(migrationDir, repeatableScript("my view", "V1"));
|
||||
assertThat(file).content().isEqualTo("V1");
|
||||
}
|
||||
|
||||
private static DdlScript initScript(String name, String value) {
|
||||
return script(name, value, true);
|
||||
}
|
||||
|
||||
private static DdlScript repeatableScript(String name, String value) {
|
||||
return script(name, value, false);
|
||||
}
|
||||
|
||||
private static DdlScript script(String name, String value, boolean init) {
|
||||
DdlScript script = new DdlScript();
|
||||
script.setName(name);
|
||||
script.setValue(value);
|
||||
script.setInit(init);
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core-type</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean net postgis types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
@@ -28,7 +28,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -71,21 +71,21 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ final class OtelProfileStream implements ProfileStream {
|
||||
static final AttributeKey<String> DB_QUERY_TEXT = AttributeKey.stringKey("db.query.text");
|
||||
static final AttributeKey<Long> DB_QUERY_TIME = AttributeKey.longKey("db.query.time");
|
||||
static final AttributeKey<String> EBEAN_BEAN_TYPE = AttributeKey.stringKey("ebean.bean_type");
|
||||
static final AttributeKey<String> EBEAN_QUERY_LABEL = AttributeKey.stringKey("ebean.query_label");
|
||||
static final AttributeKey<String> EBEAN_QUERY_HASH = AttributeKey.stringKey("ebean.query_hash");
|
||||
static final AttributeKey<Long> EBEAN_ROW_COUNT = AttributeKey.longKey("ebean.row_count");
|
||||
static final AttributeKey<Long> EBEAN_TOTAL_MICROS = AttributeKey.longKey("ebean.total_micros");
|
||||
|
||||
@@ -45,18 +47,24 @@ final class OtelProfileStream implements ProfileStream {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String sql) {
|
||||
public void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String hash, String sql) {
|
||||
long exeMicros = offset() - offset;
|
||||
var now = Instant.now();
|
||||
String operation = operationName(event);
|
||||
String name = queryId != null ? queryId : operation + " " + beanName;
|
||||
Span child = childSpanBuilder(name, now.minus(exeMicros, ChronoUnit.MICROS))
|
||||
SpanBuilder builder = childSpanBuilder(name, now.minus(exeMicros, ChronoUnit.MICROS))
|
||||
.setAttribute(DB_OPERATION, operation)
|
||||
.setAttribute(EBEAN_BEAN_TYPE, beanName)
|
||||
.setAttribute(EBEAN_ROW_COUNT, (long) beanCount)
|
||||
.setAttribute(DB_QUERY_TEXT, sql)
|
||||
.setAttribute(DB_QUERY_TIME, exeMicros)
|
||||
.startSpan();
|
||||
.setAttribute(DB_QUERY_TIME, exeMicros);
|
||||
if (hash != null) {
|
||||
builder.setAttribute(EBEAN_QUERY_HASH, hash);
|
||||
}
|
||||
if (queryId != null) {
|
||||
builder.setAttribute(EBEAN_QUERY_LABEL, queryId);
|
||||
}
|
||||
Span child = builder.startSpan();
|
||||
child.end(now);
|
||||
}
|
||||
|
||||
|
||||
+11
-6
@@ -119,7 +119,7 @@ class OtelProfileHandlerTest {
|
||||
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(null, null);
|
||||
assertNotNull(stream);
|
||||
// First query event should update the transaction span name
|
||||
stream.addQueryEvent("fm", stream.offset(), "Customer", 5, "qplan-1", "select ...");
|
||||
stream.addQueryEvent("fm", stream.offset(), "Customer", 5, "qplan-1", "h1", "select ...");
|
||||
stream.addEvent("c", 0);
|
||||
stream.end(null);
|
||||
} finally {
|
||||
@@ -141,7 +141,7 @@ class OtelProfileHandlerTest {
|
||||
assertNotNull(stream);
|
||||
long offset = stream.offset();
|
||||
// Simulate some time passing then record a find_many
|
||||
stream.addQueryEvent("fm", offset, "Order", 42, "plan-abc", "select ...");
|
||||
stream.addQueryEvent("fm", offset, "Order", 42, "plan-abc", "abc123hash", "select ...");
|
||||
stream.addEvent("c", 0);
|
||||
stream.end(null);
|
||||
} finally {
|
||||
@@ -155,6 +155,8 @@ class OtelProfileHandlerTest {
|
||||
assertEquals("Order", querySpan.getAttributes().get(OtelProfileStream.EBEAN_BEAN_TYPE));
|
||||
assertEquals(42L, querySpan.getAttributes().get(OtelProfileStream.EBEAN_ROW_COUNT));
|
||||
assertEquals("select ...", querySpan.getAttributes().get(OtelProfileStream.DB_QUERY_TEXT));
|
||||
assertEquals("abc123hash", querySpan.getAttributes().get(OtelProfileStream.EBEAN_QUERY_HASH));
|
||||
assertEquals("plan-abc", querySpan.getAttributes().get(OtelProfileStream.EBEAN_QUERY_LABEL));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -163,8 +165,8 @@ class OtelProfileHandlerTest {
|
||||
try (Scope ignored = parent.makeCurrent()) {
|
||||
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(mockLocation("MyService.doAll"), null);
|
||||
assertNotNull(stream);
|
||||
stream.addQueryEvent("fo", stream.offset(), "User", 1, "p1", "select from user");
|
||||
stream.addQueryEvent("fm", stream.offset(), "Order", 10, "p2", "select from order");
|
||||
stream.addQueryEvent("fo", stream.offset(), "User", 1, "p1", "h1", "select from user");
|
||||
stream.addQueryEvent("fm", stream.offset(), "Order", 10, "p2", "h2", "select from order");
|
||||
stream.addEvent("c", 0);
|
||||
stream.end(null);
|
||||
} finally {
|
||||
@@ -184,15 +186,18 @@ class OtelProfileHandlerTest {
|
||||
try (Scope ignored = parent.makeCurrent()) {
|
||||
OtelProfileStream stream = (OtelProfileStream) handler.createProfileStream(mockLocation("MyService.emptySql"), null);
|
||||
assertNotNull(stream);
|
||||
stream.addQueryEvent("fo", stream.offset(), "User", 1, "p1", "");
|
||||
stream.addQueryEvent("fo", stream.offset(), "User", 1, null, null, "");
|
||||
stream.addEvent("c", 0);
|
||||
stream.end(null);
|
||||
} finally {
|
||||
parent.end();
|
||||
}
|
||||
SpanData querySpan = findSpan("p1");
|
||||
SpanData querySpan = findSpan("find_one User");
|
||||
assertNotNull(querySpan);
|
||||
assertEquals("", querySpan.getAttributes().get(OtelProfileStream.DB_QUERY_TEXT));
|
||||
// null hash / label should not set the attributes
|
||||
assertNull(querySpan.getAttributes().get(OtelProfileStream.EBEAN_QUERY_HASH));
|
||||
assertNull(querySpan.getAttributes().get(OtelProfileStream.EBEAN_QUERY_LABEL));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean pgvector types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean postgis types</name>
|
||||
@@ -19,14 +19,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- provided scope -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean querybean</name>
|
||||
@@ -17,7 +17,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -59,14 +59,14 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-ddl-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -26,6 +26,35 @@ public final class PString<R> extends PBaseComparable<R, String> {
|
||||
super(name, root, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is equal to if the value is non-null and not blank, otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is like {@code eqIfPresent()} but additionally treats an empty or whitespace-only value as
|
||||
* "not present". When present, the value is trimmed before being used in the equal to expression.
|
||||
* <p>
|
||||
* This is a helper for building queries in fluid style where a String predicate is effectively
|
||||
* optional, avoiding a separate {@code if} block and {@code trimToNull()} style helper.
|
||||
*
|
||||
* <pre>{@code
|
||||
* List<Customer> customers = new QCustomer()
|
||||
* .name.eqIfNotBlank(nameFilter)
|
||||
* .status.eqIfPresent(statusFilter)
|
||||
* .findList();
|
||||
* }</pre>
|
||||
*
|
||||
* @param value the value which can be null or blank
|
||||
* @return the root query bean instance
|
||||
*/
|
||||
public R eqIfNotBlank(@Nullable String value) {
|
||||
if (value != null) {
|
||||
String trimmed = value.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
expr().eq(_name, trimmed);
|
||||
}
|
||||
}
|
||||
return _root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case insensitive is equal to.
|
||||
*
|
||||
|
||||
@@ -78,7 +78,7 @@ public class QCustomerAndOrTest {
|
||||
query.findList();
|
||||
|
||||
assertThat(query.getGeneratedSql()).isEqualTo(
|
||||
"select /* hiLabel */ _cust.id from be_customer _cust where (" +
|
||||
"select /* Customer.hiLabel */ _cust.id from be_customer _cust where (" +
|
||||
"_cust.name = ? or exists (select 1 from be_contact contact where " +
|
||||
"contact.first_name = ? and contact.customer_id = _cust.id))"
|
||||
);
|
||||
|
||||
@@ -190,7 +190,7 @@ class QOrderTest {
|
||||
final List<String> sql = LoggedSql.stop();
|
||||
assertThat(sql).hasSize(2);
|
||||
assertThat(sql.get(0)).contains("select /* QOrderTest.fetchQueryWithBatch */ t0.id, t0.name, t0.phone_number from be_customer t0");
|
||||
assertThat(sql.get(1)).contains("select /* QOrderTest.fetchQueryWithBatch_contacts__query */ t0.customer_id, t0.id, t0.first_name, t0.last_name, t0.email from be_contact t0 where");
|
||||
assertThat(sql.get(1)).contains("select /* QOrderTest.fetchQueryWithBatch.contacts.query */ t0.customer_id, t0.id, t0.first_name, t0.last_name, t0.email from be_contact t0 where");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -288,7 +288,7 @@ class QOrderTest {
|
||||
// assert fetching customer via fetchQuery
|
||||
assertThat(sql).hasSize(2);
|
||||
assertThat(sql.get(0)).contains("select /* QOrderTest.viaFetchGraph_withNested_fetchQuery */ t0.id, t0.status, t0.customer_id from o_order t0 where t0.id = ?");
|
||||
assertThat(sql.get(1)).contains("select /* QOrderTest.viaFetchGraph_withNested_fetchQuery_customer__query */ t0.id, t0.name, t0.phone_number from be_customer t0 where t0.id = ?");
|
||||
assertThat(sql.get(1)).contains("select /* QOrderTest.viaFetchGraph_withNested_fetchQuery.customer.query */ t0.id, t0.name, t0.phone_number from be_customer t0 where t0.id = ?");
|
||||
|
||||
assertThat(found.getCustomer().getPhoneNumber().getMsisdn()).isEqualTo("Ph1");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.querytest;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import org.example.domain.Customer;
|
||||
import org.example.domain.query.QCustomer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.example.domain.query.QCustomer.Alias.name;
|
||||
|
||||
class QStringEqIfNotBlankTest {
|
||||
|
||||
@Test
|
||||
void present_addsEqPredicate() {
|
||||
var q = new QCustomer()
|
||||
.select(name)
|
||||
.name.eqIfNotBlank("rob")
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).contains("where t0.name = ?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void null_skipsPredicate() {
|
||||
var q = new QCustomer()
|
||||
.select(name)
|
||||
.name.isNotNull()
|
||||
.name.eqIfNotBlank(null)
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).doesNotContain("t0.name = ?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankWhitespace_skipsPredicate() {
|
||||
var q = new QCustomer()
|
||||
.select(name)
|
||||
.name.isNotNull()
|
||||
.name.eqIfNotBlank(" ")
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).doesNotContain("t0.name = ?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void empty_skipsPredicate() {
|
||||
var q = new QCustomer()
|
||||
.select(name)
|
||||
.name.isNotNull()
|
||||
.name.eqIfNotBlank("")
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).doesNotContain("t0.name = ?");
|
||||
}
|
||||
|
||||
@Test
|
||||
void present_trimsBoundValue() {
|
||||
Database db = DB.getDefault();
|
||||
String unique = "eqIfNotBlank-" + System.nanoTime();
|
||||
|
||||
Customer customer = new Customer();
|
||||
customer.name = unique;
|
||||
db.save(customer);
|
||||
|
||||
Customer found = new QCustomer(db)
|
||||
.name.eqIfNotBlank(" " + unique + " ")
|
||||
.findOne();
|
||||
|
||||
assertThat(found).isNotNull();
|
||||
assertThat(found.name).isEqualTo(unique);
|
||||
|
||||
db.delete(customer);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class QueryAlsoIfTest {
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).isEqualTo("select /* QueryAlsoIfTest.also */ t0.id, t0.name from be_customer t0 where t0.name is not null and t0.email is not null");
|
||||
assertThat(q.getGeneratedSql()).contains("from be_customer t0 where t0.name is not null and t0.email is not null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,7 +61,7 @@ class QueryAlsoIfTest {
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).isEqualTo("select /* QueryAlsoIfTest.applyIfPresent */ t0.id, t0.name from be_customer t0 where t0.name is not null and t0.status = ?");
|
||||
assertThat(q.getGeneratedSql()).contains("from be_customer t0 where t0.name is not null and t0.status = ?");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +74,7 @@ class QueryAlsoIfTest {
|
||||
.query();
|
||||
|
||||
q.findList();
|
||||
assertThat(q.getGeneratedSql()).isEqualTo("select /* QueryAlsoIfTest.notApplyIfPresent */ t0.id, t0.name from be_customer t0 where t0.name is not null");
|
||||
assertThat(q.getGeneratedSql()).contains("from be_customer t0 where t0.name is not null");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ebean-redis</artifactId>
|
||||
@@ -29,35 +29,35 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>16.7.0</version>
|
||||
<version>16.11.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import io.ebean.metric.TimedMetricStats;
|
||||
import io.ebean.redis.encode.Encode;
|
||||
import io.ebean.redis.encode.EncodePrefixKey;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.Transaction;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
import redis.clients.jedis.params.ScanParams;
|
||||
import redis.clients.jedis.params.SetParams;
|
||||
import redis.clients.jedis.resps.ScanResult;
|
||||
@@ -32,7 +32,7 @@ final class RedisCache implements ServerCache {
|
||||
private static final String CURSOR_0 = "0";
|
||||
private static final byte[] CURSOR_0_BYTES = SafeEncoder.encode(CURSOR_0);
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
private final Pool<Jedis> jedisPool;
|
||||
private final String cacheKey;
|
||||
private final EncodePrefixKey keyEncode;
|
||||
private final Encode valueEncode;
|
||||
@@ -47,7 +47,7 @@ final class RedisCache implements ServerCache {
|
||||
private final CountMetric hitCount;
|
||||
private final CountMetric missCount;
|
||||
|
||||
RedisCache(JedisPool jedisPool, ServerCacheConfig config, Encode valueEncode) {
|
||||
RedisCache(Pool<Jedis> jedisPool, ServerCacheConfig config, Encode valueEncode) {
|
||||
this.jedisPool = jedisPool;
|
||||
this.cacheKey = config.getCacheKey();
|
||||
this.keyEncode = new EncodePrefixKey(config.getCacheKey());
|
||||
|
||||
@@ -18,6 +18,8 @@ import io.ebeaninternal.server.cache.DefaultServerQueryCache;
|
||||
import redis.clients.jedis.BinaryJedisPubSub;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisSentinelPool;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
import redis.clients.jedis.util.SafeEncoder;
|
||||
|
||||
import java.io.*;
|
||||
@@ -62,7 +64,7 @@ final class RedisCacheFactory implements ServerCacheFactory {
|
||||
private final EncodeBeanData encodeBeanData = new EncodeBeanData();
|
||||
private final EncodeSerializable encodeSerializable = new EncodeSerializable();
|
||||
private final BackgroundExecutor executor;
|
||||
private final JedisPool jedisPool;
|
||||
private final Pool<Jedis> jedisPool;
|
||||
private final NearCacheNotify nearCacheNotify;
|
||||
private final TimedMetric metricOutNearCache;
|
||||
private final TimedMetric metricOutTableMod;
|
||||
@@ -87,25 +89,34 @@ final class RedisCacheFactory implements ServerCacheFactory {
|
||||
if (config.isDisableL2Cache()) {
|
||||
this.jedisPool = null;
|
||||
} else {
|
||||
this.jedisPool = getJedisPool(config);
|
||||
this.jedisPool = getPool(config);
|
||||
new DaemonTopicRunner(jedisPool, new CacheDaemonTopic()).run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the JedisPool to use (only 1 at this stage).
|
||||
* Return the connection pool to use.
|
||||
*/
|
||||
private JedisPool getJedisPool(DatabaseBuilder.Settings config) {
|
||||
private Pool<Jedis> getPool(DatabaseBuilder.Settings config) {
|
||||
JedisPool jedisPool = config.getServiceObject(JedisPool.class);
|
||||
if (jedisPool != null) {
|
||||
return jedisPool;
|
||||
}
|
||||
JedisSentinelPool sentinelPool = config.getServiceObject(JedisSentinelPool.class);
|
||||
if (sentinelPool != null) {
|
||||
return sentinelPool;
|
||||
}
|
||||
RedisConfig redisConfig = config.getServiceObject(RedisConfig.class);
|
||||
if (redisConfig == null) {
|
||||
redisConfig = new RedisConfig();
|
||||
}
|
||||
redisConfig.loadProperties(config.getProperties());
|
||||
log.log(INFO, "using l2cache redis host {0}:{1}", redisConfig.getServer(), redisConfig.getPort());
|
||||
if (redisConfig.getMode() == RedisConfig.Mode.SENTINEL) {
|
||||
log.log(INFO, "using l2cache redis sentinel master {0} sentinels {1}",
|
||||
redisConfig.getMasterName(), redisConfig.getSentinels());
|
||||
} else {
|
||||
log.log(INFO, "using l2cache redis host {0}:{1}", redisConfig.getServer(), redisConfig.getPort());
|
||||
}
|
||||
return redisConfig.createPool();
|
||||
}
|
||||
|
||||
@@ -262,6 +273,12 @@ final class RedisCacheFactory implements ServerCacheFactory {
|
||||
private void processTableNotify(String rawMessage) {
|
||||
long nanos = System.nanoTime();
|
||||
try {
|
||||
if (listener == null) {
|
||||
if (logger.isLoggable(DEBUG)) {
|
||||
logger.log(DEBUG, "Ignoring tableMod, listener not registered yet: {0}", rawMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (logger.isLoggable(DEBUG)) {
|
||||
logger.log(DEBUG, "processTableNotify {0}", rawMessage);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
package io.ebean.redis;
|
||||
|
||||
import redis.clients.jedis.DefaultJedisClientConfig;
|
||||
import redis.clients.jedis.HostAndPort;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisClientConfig;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.JedisSentinelPool;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Deployment configuration for redis.
|
||||
*/
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* Redis deployment mode.
|
||||
*/
|
||||
public enum Mode {
|
||||
/** Single redis server (default). */
|
||||
STANDALONE,
|
||||
/** Redis sentinel high availability. */
|
||||
SENTINEL
|
||||
}
|
||||
|
||||
private Mode mode = Mode.STANDALONE;
|
||||
private String server = "localhost";
|
||||
private int port = 6379;
|
||||
private String masterName;
|
||||
private Set<String> sentinels = Set.of();
|
||||
private int maxTotal = 200;
|
||||
private int maxIdle = 200;
|
||||
private int minIdle = 1;
|
||||
@@ -23,18 +45,101 @@ public class RedisConfig {
|
||||
private boolean ssl;
|
||||
|
||||
/**
|
||||
* Return a new JedisPool based on the configuration.
|
||||
* Return a new connection pool based on the configuration.
|
||||
*/
|
||||
public JedisPool createPool() {
|
||||
public Pool<Jedis> createPool() {
|
||||
JedisPoolConfig poolConfig = poolConfig();
|
||||
if (mode == Mode.SENTINEL) {
|
||||
return createSentinelPool(poolConfig);
|
||||
}
|
||||
return createStandalonePool(poolConfig);
|
||||
}
|
||||
|
||||
private JedisPoolConfig poolConfig() {
|
||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||
poolConfig.setMaxTotal(maxTotal);
|
||||
poolConfig.setMaxIdle(maxIdle);
|
||||
poolConfig.setMinIdle(minIdle);
|
||||
poolConfig.setMaxWaitMillis(maxWaitMillis);
|
||||
poolConfig.setMaxWait(Duration.ofMillis(maxWaitMillis));
|
||||
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
|
||||
return poolConfig;
|
||||
}
|
||||
|
||||
private JedisPool createStandalonePool(JedisPoolConfig poolConfig) {
|
||||
return new JedisPool(poolConfig, server, port, timeout, username, password, ssl);
|
||||
}
|
||||
|
||||
private JedisSentinelPool createSentinelPool(JedisPoolConfig poolConfig) {
|
||||
if (masterName == null || masterName.isBlank()) {
|
||||
throw new IllegalStateException("ebean.redis.masterName must be set when mode is sentinel");
|
||||
}
|
||||
if (sentinels.isEmpty()) {
|
||||
throw new IllegalStateException("ebean.redis.sentinels must be set when mode is sentinel");
|
||||
}
|
||||
if (ssl) {
|
||||
JedisClientConfig masterConfig = DefaultJedisClientConfig.builder()
|
||||
.connectionTimeoutMillis(timeout)
|
||||
.socketTimeoutMillis(timeout)
|
||||
.user(username)
|
||||
.password(password)
|
||||
.ssl(true)
|
||||
.build();
|
||||
JedisClientConfig sentinelConfig = DefaultJedisClientConfig.builder()
|
||||
.connectionTimeoutMillis(timeout)
|
||||
.socketTimeoutMillis(timeout)
|
||||
.user(username)
|
||||
.password(password)
|
||||
.ssl(true)
|
||||
.build();
|
||||
return new JedisSentinelPool(masterName, parseSentinelHosts(sentinels), poolConfig, masterConfig, sentinelConfig);
|
||||
}
|
||||
return new JedisSentinelPool(masterName, sentinels, poolConfig, timeout, timeout, username, password, 0, null);
|
||||
}
|
||||
|
||||
static Set<String> parseSentinels(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (String part : value.split(",")) {
|
||||
String trimmed = part.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
|
||||
static Set<HostAndPort> parseSentinelHosts(Set<String> sentinels) {
|
||||
Set<HostAndPort> hosts = new LinkedHashSet<>();
|
||||
for (String sentinel : sentinels) {
|
||||
hosts.add(HostAndPort.from(sentinel));
|
||||
}
|
||||
return Set.copyOf(hosts);
|
||||
}
|
||||
|
||||
static Mode parseMode(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return Mode.STANDALONE;
|
||||
}
|
||||
String mode = value.trim().toLowerCase();
|
||||
if ("standalone".equals(mode)) {
|
||||
return Mode.STANDALONE;
|
||||
}
|
||||
if ("sentinel".equals(mode)) {
|
||||
return Mode.SENTINEL;
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown ebean.redis.mode: " + value);
|
||||
}
|
||||
|
||||
public Mode getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getServer() {
|
||||
return server;
|
||||
}
|
||||
@@ -51,6 +156,22 @@ public class RedisConfig {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getMasterName() {
|
||||
return masterName;
|
||||
}
|
||||
|
||||
public void setMasterName(String masterName) {
|
||||
this.masterName = masterName;
|
||||
}
|
||||
|
||||
public Set<String> getSentinels() {
|
||||
return sentinels;
|
||||
}
|
||||
|
||||
public void setSentinels(Set<String> sentinels) {
|
||||
this.sentinels = sentinels != null ? Set.copyOf(sentinels) : Set.of();
|
||||
}
|
||||
|
||||
public int getMaxTotal() {
|
||||
return maxTotal;
|
||||
}
|
||||
@@ -125,8 +246,11 @@ public class RedisConfig {
|
||||
|
||||
public void loadProperties(Properties properties) {
|
||||
Reader reader = new Reader(properties);
|
||||
this.mode = parseMode(reader.get("ebean.redis.mode", null));
|
||||
this.server = reader.get("ebean.redis.server", server);
|
||||
this.port = reader.getInt("ebean.redis.port", port);
|
||||
this.masterName = reader.get("ebean.redis.masterName", masterName);
|
||||
this.sentinels = parseSentinels(reader.get("ebean.redis.sentinels", null));
|
||||
this.ssl = reader.getBool("ebean.redis.ssl", ssl);
|
||||
this.minIdle = reader.getInt("ebean.redis.minIdle", minIdle);
|
||||
this.maxIdle = reader.getInt("ebean.redis.maxIdle", maxIdle);
|
||||
@@ -157,7 +281,7 @@ public class RedisConfig {
|
||||
|
||||
long getLong(String key, long defaultVal) {
|
||||
final String val = get(key, null);
|
||||
return val != null ? Integer.parseInt(val.trim()) : defaultVal;
|
||||
return val != null ? Long.parseLong(val.trim()) : defaultVal;
|
||||
}
|
||||
|
||||
boolean getBool(String key, boolean defaultVal) {
|
||||
@@ -165,5 +289,4 @@ public class RedisConfig {
|
||||
return val != null ? Boolean.parseBoolean(val.trim()) : defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package io.ebean.redis.topic;
|
||||
|
||||
import io.avaje.applog.AppLog;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
@@ -23,10 +23,10 @@ public final class DaemonTopicRunner {
|
||||
|
||||
private static final long reconnectWaitMillis = 1000;
|
||||
|
||||
private final JedisPool jedisPool;
|
||||
private final Pool<Jedis> jedisPool;
|
||||
private final DaemonTopic daemonTopic;
|
||||
|
||||
public DaemonTopicRunner(JedisPool jedisPool, DaemonTopic daemonTopic) {
|
||||
public DaemonTopicRunner(Pool<Jedis> jedisPool, DaemonTopic daemonTopic) {
|
||||
this.jedisPool = jedisPool;
|
||||
this.daemonTopic = daemonTopic;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package io.ebean.redis;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import io.ebean.DatabaseBuilder;
|
||||
import io.ebean.cache.ServerCache;
|
||||
import io.ebean.cache.ServerCacheNotification;
|
||||
import io.ebean.cache.ServerCacheNotify;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link RedisCacheFactory} against local standalone and sentinel Redis.
|
||||
*/
|
||||
@Tag("redis-local")
|
||||
class RedisCacheFactoryITest {
|
||||
|
||||
@BeforeEach
|
||||
void assumeLocalRedisConfigured() {
|
||||
assumeTrue(RedisLocalTestSupport.isConfigured());
|
||||
}
|
||||
|
||||
abstract static class FactoryTestCase {
|
||||
|
||||
RedisConfig config;
|
||||
Pool<Jedis> pool;
|
||||
RedisCacheFactory factory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
config = redisConfig();
|
||||
assumeTrue(RedisLocalTestSupport.isReachable(config),
|
||||
"Skip: Redis not reachable for " + modeLabel());
|
||||
pool = config.createPool();
|
||||
DatabaseBuilder.Settings settings = RedisTestFixtures.databaseSettings(mode(), pool);
|
||||
factory = new RedisCacheFactory(settings, RedisTestFixtures.backgroundExecutor());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (pool != null) {
|
||||
pool.close();
|
||||
}
|
||||
}
|
||||
|
||||
abstract RedisConfig redisConfig();
|
||||
|
||||
abstract RedisConfig.Mode mode();
|
||||
|
||||
abstract String modeLabel();
|
||||
|
||||
@Test
|
||||
void createsNaturalKeyCache_roundTrip() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-nk");
|
||||
ServerCache cache = factory.createCache(RedisTestFixtures.naturalKeyConfig(cacheKey));
|
||||
assertThat(cache).isInstanceOf(RedisCache.class);
|
||||
|
||||
cache.put("1", "one");
|
||||
assertThat(cache.get("1")).isEqualTo("one");
|
||||
|
||||
RedisCache redisCache = (RedisCache) cache;
|
||||
assertThat(redisCache.getHitCount()).isEqualTo(1);
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsBeanCache() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-bean");
|
||||
ServerCache cache = factory.createCache(RedisTestFixtures.beanCacheConfig(cacheKey));
|
||||
assertThat(cache).isInstanceOf(RedisCache.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCollectionIdsCache() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-coll");
|
||||
ServerCache cache = factory.createCache(RedisTestFixtures.collectionIdsConfig(cacheKey));
|
||||
assertThat(cache).isInstanceOf(RedisCache.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsNearCache_asDuelCache() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-near");
|
||||
ServerCache cache = factory.createCache(RedisTestFixtures.nearBeanCacheConfig(cacheKey));
|
||||
assertThat(cache).isInstanceOf(DuelCache.class);
|
||||
|
||||
cache.put("1", "near");
|
||||
assertThat(cache.get("1")).isEqualTo("near");
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryCache_isSingletonPerKey() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-query");
|
||||
ServerCache first = factory.createCache(RedisTestFixtures.queryCacheConfig(cacheKey));
|
||||
ServerCache second = factory.createCache(RedisTestFixtures.queryCacheConfig(cacheKey));
|
||||
assertThat(first).isSameAs(second);
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryCacheClear_doesNotThrow() {
|
||||
String cacheKey = RedisTestFixtures.cacheKey("factory-query-clear");
|
||||
ServerCache cache = factory.createCache(RedisTestFixtures.queryCacheConfig(cacheKey));
|
||||
assertNotNull(cache);
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheNotify_publishTableMod_doesNotThrow() {
|
||||
ServerCacheNotify notify = factory.createCacheNotify(n -> {
|
||||
});
|
||||
assertNotNull(notify);
|
||||
notify.notify(new ServerCacheNotification(Set.of("foo", "bar")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheNotify_tableMod_notifiesOtherFactory() throws InterruptedException {
|
||||
CopyOnWriteArrayList<ServerCacheNotification> notifications = new CopyOnWriteArrayList<>();
|
||||
DatabaseBuilder.Settings settings = RedisTestFixtures.databaseSettings(mode(), pool);
|
||||
RedisCacheFactory otherFactory = new RedisCacheFactory(settings, RedisTestFixtures.backgroundExecutor());
|
||||
otherFactory.createCacheNotify(notifications::add);
|
||||
|
||||
// allow redis-sub thread to connect
|
||||
Thread.sleep(300);
|
||||
|
||||
ServerCacheNotify notify = factory.createCacheNotify(n -> {
|
||||
});
|
||||
notify.notify(new ServerCacheNotification(Set.of("foo", "bar")));
|
||||
|
||||
Thread.sleep(500);
|
||||
assertThat(notifications).isNotEmpty();
|
||||
assertThat(notifications.get(0).getDependentTables()).contains("foo", "bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheNotify_emptyTables_doesNotThrow() {
|
||||
ServerCacheNotify notify = factory.createCacheNotify(n -> {
|
||||
});
|
||||
|
||||
assertThat(notify).isNotNull();
|
||||
notify.notify(new ServerCacheNotification(Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesInjectedPool() {
|
||||
try (Jedis jedis = pool.getResource()) {
|
||||
String marker = RedisTestFixtures.cacheKey("factory-pool");
|
||||
jedis.set(marker, "ok");
|
||||
assertThat(jedis.get(marker)).isEqualTo("ok");
|
||||
jedis.del(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Disabled("Standalone")
|
||||
class Standalone extends FactoryTestCase {
|
||||
@Override
|
||||
RedisConfig redisConfig() {
|
||||
return RedisLocalTestSupport.standaloneConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
RedisConfig.Mode mode() {
|
||||
return RedisConfig.Mode.STANDALONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
String modeLabel() {
|
||||
return "standalone";
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@Disabled("Sentinel")
|
||||
class Sentinel extends FactoryTestCase {
|
||||
@Override
|
||||
RedisConfig redisConfig() {
|
||||
return RedisLocalTestSupport.sentinelConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
RedisConfig.Mode mode() {
|
||||
return RedisConfig.Mode.SENTINEL;
|
||||
}
|
||||
|
||||
@Override
|
||||
String modeLabel() {
|
||||
return "sentinel";
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user