Compare commits

..
45 Commits
Author SHA1 Message Date
Rob Bygrave 0e6cd9db8c Version 16.11.1 2026-06-22 20:33:00 +12:00
Rob Bygrave 484ed5d859 Dependency: Bump ebean-test-containers to 8.2 2026-06-22 19:17:21 +12:00
Rob BygraveandGitHub 1545e68c3e Merge pull request #3790 from ebean-orm/feature/regression-inline-query-comment
Regression introduced by #3779 in sql inline comment for label (missing bean type prefix)
2026-06-18 22:28:57 +12:00
robin.bygrave 4fd32b45d2 Regression introduced by #3779 in sql inline comment for label (missing bean type prefix)
So when we used to get an inline comment like:
```sql
select /* Customer.hiLabel */ ...
```
We started to instead have (missing bean type):
```sql
select /* hiLabel */ ...
```
This fixes that regression that was introduced in #3779
2026-06-18 22:23:22 +12:00
Rob BygraveandGitHub d42f72c0a2 Merge pull request #3788 from mvanhorn/fix/3641-refresh-soft-deleted
Fix refresh on soft-deleted beans
2026-06-16 13:53:31 +12:00
mvanhorn 6d53e89a80 Fix refresh on soft-deleted beans 2026-06-14 02:25:47 -07:00
Rob Bygrave 7c5ee5b555 Version 16.11.0 2026-06-14 21:06:57 +12:00
Rob BygraveandGitHub a2337a096e Merge pull request #3787 from ebean-orm/feature/dep-bump-datasource
Dependency: Bump ebean-datasource to 10.9
2026-06-14 21:03:50 +12:00
Rob Bygrave 7bf5fc2798 Dependency: Bump ebean-datasource to 10.9 2026-06-14 21:02:46 +12:00
Rob Bygrave 987798add9 Merge branch 'master' of github.com:ebean-orm/ebean 2026-06-14 21:00:36 +12:00
robin.bygrave 6295faa351 Version 16.11.0-RC1 2026-06-13 10:46:40 +12:00
Rob BygraveandGitHub d9252a9c85 Merge pull request #3782 from leehaut/hotfix/lance-common-1
Add Redis Sentinel support and local integration tests for ebean-redis
2026-06-13 10:42:41 +12:00
Rob BygraveandGitHub ddd864852d Merge pull request #3783 from ebean-orm/feature/init-migrations-do-not-overwrite
db-migration: Change I__ init scripts to NOT overwrite (allow customisation)
2026-06-13 10:33:38 +12:00
robin.bygrave 7966d8bc1b docs: Improve around findList().stream() vs findStream() use 2026-06-13 10:23:17 +12:00
Rob BygraveandGitHub 66e6e83e60 Merge pull request #3785 from ebean-orm/feature/metrics-as-json-v2
Add MetricsAsJson.writeV2() using name + tags format
2026-06-12 21:00:43 +12:00
robin.bygrave 0e7f68e75a Add MetricsAsJson.writeV2() using name + tags format 2026-06-12 20:59:28 +12:00
robin.bygrave 7b8e1713dd Test: Fix for EA build to skip the TestMockitoMock 2026-06-11 21:31:22 +12:00
robin.bygrave 3ea9a5fa7f Test: Fix DtoQueryPlanCaptureTest by draining plans 2026-06-11 20:28:10 +12:00
robin.bygrave 4fab5dfe4b Test: support EA build via net.bytebuddy.experimental true 2026-06-11 19:59:12 +12:00
robin.bygrave 9fac046f39 Dep: bump ebean-datasource to 10.7 2026-06-11 18:57:34 +12:00
robin.bygrave 50bfd04987 Tests: Oracle CI, limit connections and prepared statements for oracle CI test 2026-06-11 16:56:04 +12:00
robin.bygrave d71ced32e8 Tests: Oracle CI, limit connections and prepared statements for oracle CI test 2026-06-11 16:26:29 +12:00
robin.bygrave 6722cf50e9 Tests: Fix DbMigrationTest for waits and SqlQueryPlanCaptureTest for draining 2026-06-11 14:53:55 +12:00
robin.bygrave 13cebbcacb Tests: Fix ResourceEntityTest for test pollution 2026-06-11 14:19:49 +12:00
robin.bygrave e70777bdb6 Tests: Adjust SqlQueryPlanCaptureTest (for clash on sql) 2026-06-11 13:30:46 +12:00
Rob BygraveandGitHub 86cfd185f3 Merge pull request #3784 from ebean-orm/feature/bump-jackson-core
dep: bump jackson-core to 2.22.0
2026-06-11 13:03:54 +12:00
robin.bygrave 3970847443 Tests: Run CI builds with -T C1 2026-06-11 13:02:32 +12:00
robin.bygrave b440c5ea27 Tests: Run CI build and CI h2database with -T C1 2026-06-11 12:53:40 +12:00
robin.bygrave 1836ff18a5 dep: bump jackson-core to 2.22.0 2026-06-11 09:17:31 +12:00
robin.bygrave efdee053cc db-migration: Change I__ init scripts to NOT overwrite (allow customisation)
This allows for customisation of the built in I__ init scripts.
For the case of postgres db partitions a customisation to support
unlogged tables was for example always being overwritten.

This change makes the I__ scripts a "add if not already exists".
Note that R__ repeatable scripts are "always overwrite"
2026-06-10 17:02:58 +12:00
robin.bygrave a1ee75ffec docs: update docs / guides for query bean optional predicates 2026-06-10 16:59:24 +12:00
robin.bygrave da3dd8b215 docs: findStream() preferred over findList().stream() 2026-06-10 16:31:36 +12:00
robin.bygrave 6d30e6ff82 docs: Improve docs / guides / writing query beans, section on Optional predicates 2026-06-10 16:29:17 +12:00
lance 453a320210 Add Redis Sentinel support and local integration tests for ebean-redis
Signed-off-by: lance <leehaut@gmail.com>
2026-06-09 22:17:25 +08:00
Rob Bygrave 61cc5e3459 Version 16.10.0 2026-06-09 07:57:32 +12:00
Rob Bygrave 23f23fa32b Bump ebean-agent to 16.10.0 2026-06-09 07:53:36 +12:00
Rob BygraveandGitHub f65c409bfe Merge pull request #3781 from ebean-orm/feature/sqlquery-plan-capture
SqlQuery - add support for query plan capture
2026-06-09 00:39:52 +12:00
robin.bygrave ec49824430 SqlQuery - update docs wrt plan capture 2026-06-09 00:28:13 +12:00
robin.bygrave 819aaece4d SqlQuery - add support for query plan capture 2026-06-09 00:20:53 +12:00
Rob BygraveandGitHub c275953582 Merge pull request #3780 from ebean-orm/feature/dto-plan-capture
DtoQuery updated to support query plan capture
2026-06-08 23:47:40 +12:00
robin.bygrave 82e8494f42 Add profile location test 2026-06-08 23:47:21 +12:00
robin.bygrave abacda2c8f Update docs 2026-06-08 23:34:19 +12:00
robin.bygrave a7d1253dba DtoQuery skip bind capture when it's actually an orm query 2026-06-08 23:31:52 +12:00
robin.bygrave a081d08621 DtoQuery initiate the bind capture 2026-06-08 23:24:13 +12:00
robin.bygrave 8c37b53bad DtoQuery modified to support query plan capture 2026-06-08 23:06:09 +12:00
121 changed files with 2876 additions and 343 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3
View File
@@ -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
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.0</version>
<version>16.11.1</version>
</parent>
<artifactId>composites</artifactId>
@@ -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
+7 -3
View File
@@ -20,7 +20,7 @@ Ebean records timing and counter metrics for the work it does. Every metric has
| Prefix | What it measures | Example name |
|---|---|---|
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
| `dto.` | DTO query | `dto.CustomerDto_byEmail` |
| `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>` |
@@ -61,13 +61,17 @@ List<Customer> customers = DB.find(Customer.class)
// metric name: orm.Customer.custMain
```
DTO queries support `setLabel(..)` too (the label should be unique to the DTO type):
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
// 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)
+40 -4
View File
@@ -32,7 +32,30 @@ Two ways to trigger phase 2:
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
(this is what remote tooling such as ebean-insight uses).
Only `orm.` SELECT-style queries are plan capable — raw SQL and update/DML metrics are not.
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.
---
@@ -40,6 +63,15 @@ Only `orm.` SELECT-style queries are plan capable — raw SQL and update/DML met
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
@@ -200,7 +232,11 @@ logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
`queryPlanExplain` to a non-ANALYZE form.
### An update/DML metric never offers plan capture
### An unlabelled SqlQuery or update metric never offers plan capture
Only `orm.` SELECT-style queries are plan capable. `orm.update.*`, `iud.*`, `sql.update.*`
and similar write metrics are intentionally excluded.
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.
+85
View File
@@ -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
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.0</version>
<version>16.11.1</version>
</parent>
<artifactId>ebean-bench</artifactId>
+28 -28
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<!-- Jackson core used internally by Ebean -->
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>16.9.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.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.9.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.
*/
@@ -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);
}
}
@@ -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() {
@@ -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 "";
}
@@ -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;
@@ -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) {
@@ -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) {
}
};
}
}
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
@@ -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();
@@ -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;
}
}
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.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.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
@@ -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))"
);
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.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.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.9.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";
}
}
}
@@ -0,0 +1,190 @@
package io.ebean.redis;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.cache.ServerCacheType;
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.Assumptions.assumeTrue;
/**
* Integration tests for {@link RedisCache} against local standalone and sentinel Redis.
*/
@Tag("redis-local")
class RedisCacheITest {
@BeforeEach
void assumeLocalRedisConfigured() {
assumeTrue(RedisLocalTestSupport.isConfigured());
}
abstract static class RedisCacheTestCase {
RedisConfig config;
Pool<Jedis> pool;
String cacheKey;
RedisCache cache;
@BeforeEach
void setUp() {
config = redisConfig();
assumeTrue(RedisLocalTestSupport.isReachable(config),
"Skip: Redis not reachable for " + modeLabel());
pool = config.createPool();
cacheKey = RedisTestFixtures.cacheKey("RedisCache");
cache = RedisTestFixtures.naturalKeyCache(pool, cacheKey);
}
@AfterEach
void tearDown() {
if (cache != null) {
cache.clear();
}
if (pool != null) {
pool.close();
}
}
abstract RedisConfig redisConfig();
abstract String modeLabel();
@Test
void putAndGet() {
cache.put("1", "one");
assertThat(cache.get("1")).isEqualTo("one");
assertThat(cache.getHitCount()).isEqualTo(1);
assertThat(cache.getMissCount()).isZero();
assertThat(cache.get("missing")).isNull();
assertThat(cache.getMissCount()).isEqualTo(1);
}
@Test
void getAll() {
cache.put("1", "one");
cache.put("2", "two");
Map<Object, Object> found = cache.getAll(Set.of("1", "2", "3"));
assertThat(found).containsEntry("1", "one").containsEntry("2", "two").doesNotContainKey("3");
assertThat(cache.getHitCount()).isEqualTo(2);
assertThat(cache.getMissCount()).isEqualTo(1);
}
@Test
void getAll_emptyKeys() {
assertThat(cache.getAll(Set.of())).isEmpty();
}
@Test
void putAll() {
Map<Object, Object> entries = new LinkedHashMap<>();
entries.put("a", "A");
entries.put("b", "B");
cache.putAll(entries);
assertThat(cache.getAll(Set.of("a", "b")))
.containsEntry("a", "A")
.containsEntry("b", "B");
}
@Test
void remove() {
cache.put("1", "one");
cache.remove("1");
assertThat(cache.get("1")).isNull();
}
@Test
void removeAll() {
cache.put("1", "one");
cache.put("2", "two");
cache.removeAll(Set.of("1", "2"));
assertThat(cache.getAll(Set.of("1", "2"))).isEmpty();
}
@Test
void clear() {
cache.put("1", "one");
cache.put("2", "two");
cache.clear();
assertThat(cache.getAll(Set.of("1", "2"))).isEmpty();
}
@Test
void statistics() {
cache.put("1", "one");
cache.get("1");
cache.get("missing");
cache.remove("1");
ServerCacheStatistics stats = cache.statistics(true);
assertThat(stats.getCacheName()).isEqualTo(cacheKey);
assertThat(stats.getHitCount()).isEqualTo(1);
assertThat(stats.getMissCount()).isEqualTo(1);
assertThat(stats.getPutCount()).isEqualTo(1);
assertThat(stats.getRemoveCount()).isEqualTo(1);
}
@Test
void expiration() {
String ttlCacheKey = RedisTestFixtures.cacheKey("ttl");
var options = RedisTestFixtures.defaultOptions();
options.setMaxSecsToLive(3600);
RedisCache expiringCache = new RedisCache(
pool,
RedisTestFixtures.cacheConfig(ServerCacheType.NATURAL_KEY, ttlCacheKey, options),
new io.ebean.redis.encode.EncodeSerializable()
);
try {
expiringCache.put("k1", "v1");
try (Jedis jedis = pool.getResource()) {
byte[] rawKey = (ttlCacheKey + ":k1").getBytes(StandardCharsets.UTF_8);
assertThat(jedis.ttl(rawKey)).isBetween(1L, 3600L);
}
} finally {
expiringCache.clear();
}
}
}
@Nested
@Disabled("Standalone")
class Standalone extends RedisCacheTestCase {
@Override
RedisConfig redisConfig() {
return RedisLocalTestSupport.standaloneConfig();
}
@Override
String modeLabel() {
return "standalone";
}
}
@Nested
@Disabled("Sentinel")
class Sentinel extends RedisCacheTestCase {
@Override
RedisConfig redisConfig() {
return RedisLocalTestSupport.sentinelConfig();
}
@Override
String modeLabel() {
return "sentinel";
}
}
}
@@ -1,16 +1,20 @@
package io.ebean.redis;
import java.util.Properties;
import java.util.Set;
import org.junit.jupiter.api.Test;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class RedisConfigTest {
/**
* Unit test for {@link RedisConfig}
**/
class RedisConfigTest {
@Test
public void loadProperties() {
void loadProperties() {
Properties p = new Properties();
p.setProperty("ebean.redis.server", "test-server");
p.setProperty("ebean.redis.port", "99");
@@ -25,6 +29,7 @@ public class RedisConfigTest {
RedisConfig config = new RedisConfig();
config.loadProperties(p);
assertThat(config.getMode()).isEqualTo(RedisConfig.Mode.STANDALONE);
assertThat(config.getServer()).isEqualTo("test-server");
assertThat(config.getPort()).isEqualTo(99);
assertThat(config.getMaxIdle()).isEqualTo(5);
@@ -33,14 +38,68 @@ public class RedisConfigTest {
assertThat(config.getMaxWaitMillis()).isEqualTo(8);
assertThat(config.getUsername()).isEqualTo("un");
assertThat(config.getPassword()).isEqualTo("pw");
assertThat(config.isSsl()).isEqualTo(true);
assertThat(config.isSsl()).isTrue();
}
@Test
public void test_defaultValues() {
void loadSentinelProperties() {
Properties p = new Properties();
p.setProperty("ebean.redis.mode", "sentinel");
p.setProperty("ebean.redis.masterName", "mymaster");
p.setProperty("ebean.redis.sentinels", "host1:26379, host2:26379 ,host3:26379");
p.setProperty("ebean.redis.password", "pw");
RedisConfig config = new RedisConfig();
config.loadProperties(p);
assertThat(config.getMode()).isEqualTo(RedisConfig.Mode.SENTINEL);
assertThat(config.getMasterName()).isEqualTo("mymaster");
assertThat(config.getSentinels()).containsExactlyInAnyOrder("host1:26379", "host2:26379", "host3:26379");
assertThat(config.getPassword()).isEqualTo("pw");
}
@Test
void sentinelRequiresMasterName() {
RedisConfig config = new RedisConfig();
config.setMode(RedisConfig.Mode.SENTINEL);
config.setSentinels(Set.of("host1:26379"));
assertThatThrownBy(config::createPool)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("masterName");
}
@Test
void sentinelRequiresSentinels() {
RedisConfig config = new RedisConfig();
config.setMode(RedisConfig.Mode.SENTINEL);
config.setMasterName("mymaster");
assertThatThrownBy(config::createPool)
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("sentinels");
}
@Test
void parseSentinels_empty() {
assertThat(RedisConfig.parseSentinels(null)).isEmpty();
assertThat(RedisConfig.parseSentinels(" ")).isEmpty();
}
@Test
void parseMode_unknown() {
assertThatThrownBy(() -> RedisConfig.parseMode("cluster"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("cluster");
}
@Test
void test_defaultValues() {
RedisConfig config = new RedisConfig();
assertThat(config.getMode()).isEqualTo(RedisConfig.Mode.STANDALONE);
assertThat(config.getUsername()).isNull();
assertThat(config.getPassword()).isNull();
assertThat(config.isSsl()).isEqualTo(false);
assertThat(config.isSsl()).isFalse();
assertThat(config.getSentinels()).isEmpty();
}
}
@@ -0,0 +1,190 @@
package io.ebean.redis;
import java.util.List;
import java.util.Set;
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.Assumptions.assumeTrue;
/**
* Integration tests against a locally deployed Redis (standalone and sentinel).
* <p>
* Requires {@code src/test/resources/redis-local.yml} — copy from
* {@code redis-local.yml.example}. Tests are skipped when the file is
* missing or Redis is unreachable.
* </p>
*/
@Tag("redis-local")
class RedisConnectionITest {
@BeforeEach
void assumeLocalRedisConfigured() {
assumeTrue(RedisLocalTestSupport.isConfigured());
}
@Nested
@Disabled("standalone")
class Standalone {
private RedisConfig config;
private Pool<Jedis> pool;
@BeforeEach
void connect() {
config = RedisLocalTestSupport.standaloneConfig();
assumeTrue(RedisLocalTestSupport.isReachable(config),
"Skip: standalone Redis not reachable at "
+ config.getServer() + ":" + config.getPort());
pool = config.createPool();
}
@AfterEach
void close() {
if (pool != null) {
pool.close();
}
}
@Test
void ping() {
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.ping()).isEqualToIgnoringCase("PONG");
}
}
@Test
void setGetDel() {
String key = RedisLocalTestSupport.testKey("standalone");
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.set(key, "value")).isEqualTo("OK");
assertThat(jedis.get(key)).isEqualTo("value");
assertThat(jedis.del(key)).isEqualTo(1L);
assertThat(jedis.get(key)).isNull();
}
}
@Test
void mget() {
String key1 = RedisLocalTestSupport.testKey("standalone-a");
String key2 = RedisLocalTestSupport.testKey("standalone-b");
try (Jedis jedis = pool.getResource()) {
jedis.set(key1, "a");
jedis.set(key2, "b");
List<String> values = jedis.mget(key1, key2);
assertThat(values).containsExactly("a", "b");
jedis.del(key1, key2);
}
}
@Test
void publish() {
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.publish("ebean-redis-it:channel", "hello")).isGreaterThanOrEqualTo(0L);
}
}
@Test
void loadFromYamlConfig() {
RedisConfig reloaded = RedisLocalTestSupport.standaloneConfig();
assertThat(reloaded.getMode()).isEqualTo(RedisConfig.Mode.STANDALONE);
assertThat(reloaded.getServer()).isEqualTo(config.getServer());
assertThat(reloaded.getPort()).isEqualTo(config.getPort());
assertThat(reloaded.getPassword()).isEqualTo(config.getPassword());
assertThat(reloaded.getMaxTotal()).isEqualTo(config.getMaxTotal());
assertThat(RedisLocalTestSupport.isReachable(reloaded)).isTrue();
}
}
@Nested
@Disabled("Sentinel")
class Sentinel {
private RedisConfig config;
private Pool<Jedis> pool;
@BeforeEach
void connect() {
config = RedisLocalTestSupport.sentinelConfig();
assumeTrue(!config.getSentinels().isEmpty(),
"Skip: ebean.redis.sentinels not configured in redis-local.yml");
assumeTrue(RedisLocalTestSupport.isReachable(config),
"Skip: sentinel Redis not reachable, master="
+ config.getMasterName() + " sentinels=" + config.getSentinels());
pool = config.createPool();
}
@AfterEach
void close() {
if (pool != null) {
pool.close();
}
}
@Test
void ping() {
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.ping()).isEqualToIgnoringCase("PONG");
}
}
@Test
void connectsToMaster() {
try (Jedis jedis = pool.getResource()) {
String role = jedis.info("replication").lines()
.filter(line -> line.startsWith("role:"))
.findFirst()
.orElse("");
assertThat(role).contains("master");
}
}
@Test
void setGetDel() {
String key = RedisLocalTestSupport.testKey("sentinel");
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.set(key, "sentinel-value")).isEqualTo("OK");
assertThat(jedis.get(key)).isEqualTo("sentinel-value");
assertThat(jedis.del(key)).isEqualTo(1L);
}
}
@Test
void mget() {
String key1 = RedisLocalTestSupport.testKey("sentinel-a");
String key2 = RedisLocalTestSupport.testKey("sentinel-b");
try (Jedis jedis = pool.getResource()) {
jedis.set(key1, "1");
jedis.set(key2, "2");
assertThat(jedis.mget(key1, key2)).containsExactly("1", "2");
jedis.del(key1, key2);
}
}
@Test
void publish() {
try (Jedis jedis = pool.getResource()) {
assertThat(jedis.publish("ebean-redis-it:sentinel-channel", "hello")).isGreaterThanOrEqualTo(0L);
}
}
@Test
void loadFromYamlConfig() {
RedisConfig reloaded = RedisLocalTestSupport.sentinelConfig();
assertThat(reloaded.getMode()).isEqualTo(RedisConfig.Mode.SENTINEL);
assertThat(reloaded.getMasterName()).isEqualTo(config.getMasterName());
assertThat(reloaded.getSentinels()).isEqualTo(Set.copyOf(config.getSentinels()));
assertThat(reloaded.getPassword()).isEqualTo(config.getPassword());
assertThat(reloaded.getMaxTotal()).isEqualTo(config.getMaxTotal());
assertThat(RedisLocalTestSupport.isReachable(reloaded)).isTrue();
}
}
}
@@ -0,0 +1,147 @@
package io.ebean.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.util.Pool;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* Loads local Redis connection settings for integration tests from {@code redis-local.yml}.
* <p>
* The YAML file supports multiple documents separated by {@code ---}, one per deployment mode.
* System properties such as {@code -Debean.redis.server=127.0.0.1} override file values via
* {@link RedisConfig#loadProperties(Properties)}.
* </p>
*/
final class RedisLocalTestSupport {
private static final String CONFIG_FILE = "/redis-local.yml";
private RedisLocalTestSupport() {
}
static boolean isConfigured() {
return RedisLocalTestSupport.class.getResource(CONFIG_FILE) != null;
}
static RedisConfig standaloneConfig() {
return configForMode(RedisConfig.Mode.STANDALONE);
}
static RedisConfig sentinelConfig() {
return configForMode(RedisConfig.Mode.SENTINEL);
}
static RedisConfig configForMode(RedisConfig.Mode mode) {
RedisConfig config = new RedisConfig();
config.loadProperties(propertiesForMode(mode));
return config;
}
static Properties propertiesForMode(RedisConfig.Mode mode) {
for (Properties document : loadDocuments()) {
RedisConfig.Mode documentMode = modeOf(document);
if (documentMode == mode) {
return document;
}
}
throw new IllegalStateException(
"No document for mode " + mode + " in redis-local.yml (expected ebean.redis.mode or ebean.redis.model)");
}
static List<Properties> loadDocuments() {
try (InputStream in = RedisLocalTestSupport.class.getResourceAsStream(CONFIG_FILE)) {
if (in == null) {
return List.of();
}
String content = new String(in.readAllBytes(), StandardCharsets.UTF_8);
List<Properties> documents = new ArrayList<>();
for (String document : splitDocuments(content)) {
documents.add(parseRedisDocument(document));
}
return documents;
} catch (IOException e) {
throw new IllegalStateException("Failed to load " + CONFIG_FILE, e);
}
}
static List<String> splitDocuments(String content) {
List<String> documents = new ArrayList<>();
StringBuilder current = new StringBuilder();
for (String line : content.split("\n", -1)) {
if ("---".equals(line.trim())) {
if (current.length() > 0) {
documents.add(current.toString());
current = new StringBuilder();
}
} else {
current.append(line).append('\n');
}
}
if (current.length() > 0) {
documents.add(current.toString());
}
return documents;
}
/**
* Parse a single YAML document with {@code ebean.redis} settings.
*/
static Properties parseRedisDocument(String document) {
Properties properties = new Properties();
boolean inEbean = false;
boolean inRedis = false;
for (String line : document.split("\n")) {
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#")) {
continue;
}
if ("ebean:".equals(trimmed)) {
inEbean = true;
inRedis = false;
continue;
}
if (inEbean && "redis:".equals(trimmed)) {
inRedis = true;
continue;
}
if (!inRedis || !trimmed.contains(":")) {
continue;
}
int colon = trimmed.indexOf(':');
String key = trimmed.substring(0, colon).trim();
String value = trimmed.substring(colon + 1).trim();
if ("model".equals(key)) {
key = "mode";
}
properties.setProperty("ebean.redis." + key, value);
}
return properties;
}
static boolean isReachable(RedisConfig config) {
try (Pool<Jedis> pool = config.createPool();
Jedis jedis = pool.getResource()) {
return "PONG".equalsIgnoreCase(jedis.ping());
} catch (Exception e) {
return false;
}
}
static String testKey(String prefix) {
return "ebean-redis-it:" + prefix + ":" + System.nanoTime();
}
private static RedisConfig.Mode modeOf(Properties document) {
String mode = document.getProperty("ebean.redis.mode");
if (mode == null || mode.isBlank()) {
throw new IllegalStateException("Missing ebean.redis.mode in redis-local.yml document: " + document);
}
return RedisConfig.parseMode(mode);
}
}
@@ -0,0 +1,63 @@
package io.ebean.redis;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
class RedisLocalYamlTest {
@Test
void parseStandaloneDocument() {
Properties props = RedisLocalTestSupport.parseRedisDocument(
"ebean:\n"
+ " redis:\n"
+ " model: standalone\n"
+ " server: 192.168.204.140\n"
+ " port: 6379\n"
+ " password: secret\n"
);
assertThat(props.getProperty("ebean.redis.mode")).isEqualTo("standalone");
assertThat(props.getProperty("ebean.redis.server")).isEqualTo("192.168.204.140");
assertThat(props.getProperty("ebean.redis.port")).isEqualTo("6379");
assertThat(props.getProperty("ebean.redis.password")).isEqualTo("secret");
}
@Test
void parseSentinelDocument() {
Properties props = RedisLocalTestSupport.parseRedisDocument(
"ebean:\n"
+ " redis:\n"
+ " mode: sentinel\n"
+ " sentinels: host1:26379,host2:26379\n"
+ " masterName: mymaster\n"
+ " password: secret\n"
);
assertThat(props.getProperty("ebean.redis.mode")).isEqualTo("sentinel");
assertThat(props.getProperty("ebean.redis.sentinels")).isEqualTo("host1:26379,host2:26379");
assertThat(props.getProperty("ebean.redis.masterName")).isEqualTo("mymaster");
}
@Test
void loadDocumentsFromClasspath() {
if (!RedisLocalTestSupport.isConfigured()) {
return;
}
List<Properties> documents = RedisLocalTestSupport.loadDocuments();
assertThat(documents).hasSizeGreaterThanOrEqualTo(2);
RedisConfig standalone = RedisLocalTestSupport.standaloneConfig();
assertThat(standalone.getMode()).isEqualTo(RedisConfig.Mode.STANDALONE);
assertThat(standalone.getServer()).isNotBlank();
assertThat(standalone.getPort()).isPositive();
RedisConfig sentinel = RedisLocalTestSupport.sentinelConfig();
assertThat(sentinel.getMode()).isEqualTo(RedisConfig.Mode.SENTINEL);
assertThat(sentinel.getMasterName()).isEqualTo("mymaster");
assertThat(sentinel.getSentinels()).hasSize(3);
}
}
@@ -0,0 +1,116 @@
package io.ebean.redis;
import io.ebean.BackgroundExecutor;
import io.ebean.Database;
import io.ebean.DatabaseBuilder;
import io.ebean.cache.ServerCacheConfig;
import io.ebean.cache.ServerCacheOptions;
import io.ebean.cache.ServerCacheType;
import io.ebean.redis.encode.EncodeSerializable;
import redis.clients.jedis.util.Pool;
import redis.clients.jedis.Jedis;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Shared fixtures for Redis cache integration tests.
*/
final class RedisTestFixtures {
private RedisTestFixtures() {
}
static String cacheKey(String suffix) {
return "ebean-redis-it:" + suffix + ":" + System.nanoTime();
}
static ServerCacheOptions defaultOptions() {
return new ServerCacheOptions();
}
static ServerCacheConfig naturalKeyConfig(String cacheKey) {
return cacheConfig(ServerCacheType.NATURAL_KEY, cacheKey, defaultOptions());
}
static ServerCacheConfig beanCacheConfig(String cacheKey) {
return cacheConfig(ServerCacheType.BEAN, cacheKey, defaultOptions());
}
static ServerCacheConfig collectionIdsConfig(String cacheKey) {
return cacheConfig(ServerCacheType.COLLECTION_IDS, cacheKey, defaultOptions());
}
static ServerCacheConfig queryCacheConfig(String cacheKey) {
return cacheConfig(ServerCacheType.QUERY, cacheKey, defaultOptions());
}
static ServerCacheConfig nearBeanCacheConfig(String cacheKey) {
ServerCacheOptions options = defaultOptions();
options.setNearCache(true);
return cacheConfig(ServerCacheType.BEAN, cacheKey, options);
}
static ServerCacheConfig cacheConfig(ServerCacheType type, String cacheKey, ServerCacheOptions options) {
return new ServerCacheConfig(type, cacheKey, "testCache", options, null, null);
}
static RedisCache naturalKeyCache(Pool<Jedis> pool, String cacheKey) {
return new RedisCache(pool, naturalKeyConfig(cacheKey), new EncodeSerializable());
}
static DatabaseBuilder.Settings databaseSettings(RedisConfig.Mode mode, Pool<Jedis> pool) {
return Database.builder()
.putServiceObject(pool)
.loadFromProperties(RedisLocalTestSupport.propertiesForMode(mode))
.settings();
}
static BackgroundExecutor backgroundExecutor() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "ebean-redis-test-bg");
thread.setDaemon(true);
return thread;
});
return new BackgroundExecutor() {
@Override
public <T> Future<T> submit(Callable<T> task) {
return executor.submit(task);
}
@Override
public Future<?> submit(Runnable task) {
return executor.submit(task);
}
@Override
public void execute(Runnable task) {
executor.execute(task);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) {
return executor.scheduleWithFixedDelay(task, initialDelay, delay, unit);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) {
return executor.scheduleAtFixedRate(task, initialDelay, period, unit);
}
@Override
public ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit) {
return executor.schedule(task, delay, unit);
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> task, long delay, TimeUnit unit) {
return executor.schedule(task, delay, unit);
}
};
}
}
@@ -0,0 +1,30 @@
# Copy to redis-local.yml and adjust for your local Redis deployment.
# Integration tests skip automatically when redis-local.yml is missing or Redis is unreachable.
# Multiple documents separated by --- ; one document per mode.
ebean:
redis:
mode: standalone
server: localhost
port: 6379
ssl: false
minIdle: 0
maxIdle: 8
maxTotal: 8
maxWaitMillis: 3000
timeout: 1000
password: changeme
---
ebean:
redis:
mode: sentinel
sentinels: localhost:26379,localhost:26380,localhost:26381
masterName: mymaster
ssl: false
minIdle: 0
maxIdle: 8
maxTotal: 8
maxWaitMillis: 3000
timeout: 1000
password: changeme
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.0</version>
<version>16.11.1</version>
</parent>
<artifactId>ebean-spring-txn</artifactId>
@@ -28,7 +28,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
@@ -77,7 +77,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
+34 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.9.0</version>
<version>16.11.1</version>
</parent>
<name>ebean test</name>
@@ -33,20 +33,20 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -149,14 +149,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.9.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
@@ -321,4 +321,32 @@
</plugins>
</build>
<profiles>
<!-- Early-access JDK build (mvn ... -Pea). Mockito's inline mock-maker uses
ByteBuddy to retransform Ebean-enhanced entity beans; on not-yet-supported
(early-access) JDK class file versions this fails with ClassFormatError in
the native Instrumentation.retransformClasses0 step (the
net.bytebuddy.experimental flag does not help - parsing already succeeds).
Skip those inline-mock tests on EA only; the main build (no -Pea) still runs
them on the supported JDK. EbeanServerFactory_MultiTenancy_Test fails on EA
only as collateral from TestMockitoMock's leaked Mockito verify state, so it
recovers once TestMockitoMock is excluded. -->
<profile>
<id>ea</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/TestMockitoMock.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
@@ -212,7 +212,7 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
}
List<String> sql = LoggedSql.stop();
assertSql(sql.get(0)).contains("select /*+ SomeHint */ /* explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
assertSql(sql.get(0)).contains("select /*+ SomeHint */ /* Contact.explicitId */ t0.id, t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
+ " fullName from contact t0 where t0.email is not null and t0.last_name is not null order by t0.last_name");
}
@@ -298,11 +298,11 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
List<String> sql = LoggedSql.stop();
if (isSqlServer()) {
assertSql(sql.get(0)).contains("select /* emailFullName */ top 10 t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
assertSql(sql.get(0)).contains("select /* Contact.emailFullName */ top 10 t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
+ " fullName from contact t0 where t0.email is not null and t0.last_name is not null order by t0.last_name");
} else {
assertSql(sql.get(0)).contains("select /* emailFullName */ t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
assertSql(sql.get(0)).contains("select /* Contact.emailFullName */ t0.email, " + concat("t0.last_name", ", ", "t0.first_name")
+ " fullName from contact t0 where t0.email is not null and t0.last_name is not null order by t0.last_name");
}
}
@@ -0,0 +1,168 @@
package io.ebean.xtest.base;
import io.ebean.DB;
import io.ebean.ProfileLocation;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.QueryPlanInit;
import io.ebean.meta.QueryPlanRequest;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Prototype: capture database query plans for native SQL DtoQuery.
*/
class DtoQueryPlanCaptureTest extends BaseTestCase {
public static class DCustPlanCapture {
Long id;
String name;
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
public static class DCustOrmBacked {
Long id;
String name;
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
private List<DCustPlanCapture> runNative() {
return DB.getDefault()
.findDto(DCustPlanCapture.class, "select id, name from o_customer where id > ?")
.setParameter(0)
.setLabel("custDtoPlan")
.findList();
}
private List<DCustOrmBacked> runOrmBacked() {
return DB.getDefault()
.find(Customer.class)
.select("id, name")
.asDto(DCustOrmBacked.class)
.setLabel("custOrmBacked")
.findList();
}
private static final ProfileLocation planLoc = ProfileLocation.create();
private List<DCustPlanCapture> runNativeWithLocation() {
return DB.getDefault()
.findDto(DCustPlanCapture.class, "select id, name from o_customer where id >= ?")
.setParameter(0)
.setProfileLocation(planLoc)
.findList();
}
private void drainCapturedPlans() {
QueryPlanRequest drain = new QueryPlanRequest();
drain.maxCount(100_000);
drain.maxTimeMillis(30_000);
DB.getDefault().metaInfo().queryPlanCollectNow(drain);
}
@Test
void nativeDtoQuery_capturesQueryPlan() {
ResetBasicData.reset();
// drain any query plans captured by earlier tests in the suite, otherwise the
// shared (process-global) capture map can exhaust the collect budget below before
// our plan is reached (order dependent, and slow to capture on Oracle).
drainCapturedPlans();
// build the plans first (default threshold -> no capture yet)
assertThat(runNative()).isNotEmpty();
runOrmBacked();
// arming should include the native DTO plan but NOT the ORM-backed DTO plan
QueryPlanInit init = new QueryPlanInit();
init.setAll(true);
init.thresholdMicros(1);
List<MetaQueryPlan> armed = DB.getDefault().metaInfo().queryPlanInit(init);
assertThat(armed)
.as("native DTO plan is armed")
.anyMatch(p -> "dto.DCustPlanCapture.custDtoPlan".equals(p.label()));
assertThat(armed)
.as("ORM-backed DTO plan is not armed via the DTO path")
.noneMatch(p -> "dto.DCustOrmBacked.custOrmBacked".equals(p.label()));
assertThat(armed)
.as("ORM-backed DTO captures via the underlying ORM query plan instead")
.anyMatch(p -> "orm.Customer.custOrmBacked".equals(p.label()));
// run again now that the native plan is armed -> bind values captured
assertThat(runNative()).isNotEmpty();
QueryPlanRequest request = new QueryPlanRequest();
request.maxCount(1000);
request.maxTimeMillis(10_000);
List<MetaQueryPlan> plans = DB.getDefault().metaInfo().queryPlanCollectNow(request);
MetaQueryPlan dtoPlan = plans.stream()
.filter(p -> "dto.DCustPlanCapture.custDtoPlan".equals(p.label()))
.findFirst()
.orElse(null);
assertThat(dtoPlan).as("captured a native DTO query plan").isNotNull();
assertThat(dtoPlan.sql()).contains("from o_customer where id > ?");
assertThat(dtoPlan.plan()).isNotEmpty();
}
@Test
void nativeDtoQuery_withProfileLocation_capturesQueryPlan() {
ResetBasicData.reset();
// drain stale captures first (see nativeDtoQuery_capturesQueryPlan)
drainCapturedPlans();
// build the plan first (default threshold -> no capture yet)
assertThat(runNativeWithLocation()).isNotEmpty();
// unlabelled DTO query uses the profile location as the plan name (no type prefix)
String expectedLabel = "dto." + planLoc.label();
QueryPlanInit init = new QueryPlanInit();
init.setAll(true);
init.thresholdMicros(1);
List<MetaQueryPlan> armed = DB.getDefault().metaInfo().queryPlanInit(init);
assertThat(armed)
.as("native DTO plan named after its profile location is armed")
.anyMatch(p -> expectedLabel.equals(p.label()));
// run again now that the plan is armed -> bind values captured
assertThat(runNativeWithLocation()).isNotEmpty();
QueryPlanRequest request = new QueryPlanRequest();
request.maxCount(1000);
request.maxTimeMillis(10_000);
List<MetaQueryPlan> plans = DB.getDefault().metaInfo().queryPlanCollectNow(request);
MetaQueryPlan dtoPlan = plans.stream()
.filter(p -> expectedLabel.equals(p.label()))
.findFirst()
.orElse(null);
assertThat(dtoPlan).as("captured a profile-location DTO query plan").isNotNull();
assertThat(dtoPlan.sql()).contains("from o_customer where id >= ?");
assertThat(dtoPlan.plan()).isNotEmpty();
}
}
@@ -316,7 +316,7 @@ class DtoQueryTest extends BaseTestCase {
MetaQueryMetric queryMetric = stats.get(0);
assertThat(queryMetric.label()).isEqualTo("basic");
assertThat(queryMetric.count()).isEqualTo(3);
assertThat(queryMetric.name()).isEqualTo("dto.DCust_basic");
assertThat(queryMetric.name()).isEqualTo("dto.DCust.basic");
server().findDto(DCust.class, "select c4.id, c4.name from o_customer c4 where lower(c4.name) = :name")
.setLabel("basic2")
@@ -331,7 +331,7 @@ class DtoQueryTest extends BaseTestCase {
log.info("stats " + stats);
String asJson = metric2.asJson().withHash(false).withNewLine(false).json();
assertThat(asJson).contains("dto.DCust_basic2");
assertThat(asJson).contains("dto.DCust.basic2");
}
@Test
@@ -31,7 +31,7 @@ public class FetchGroupTest extends BaseTestCase {
query.findList();
assertThat(sqlOf(query)).contains("select /* hello */ t0.id, t0.name, t0.status from");
assertThat(sqlOf(query)).contains("select /* Customer.hello */ t0.id, t0.name, t0.status from");
}
@@ -0,0 +1,74 @@
package io.ebean.xtest.base;
import io.ebean.DB;
import io.ebean.SqlRow;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.QueryPlanInit;
import io.ebean.meta.QueryPlanRequest;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.ResetBasicData;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Prototype: capture database query plans for native SQL SqlQuery.
*/
class SqlQueryPlanCaptureTest extends BaseTestCase {
private List<SqlRow> runQuery() {
return DB.getDefault()
.sqlQuery("select id, name from o_customer where id > ? and 'something' = 'something' ")
.setParameter(0)
.setLabel("custSqlPlan")
.findList();
}
private void drainCapturedPlans() {
QueryPlanRequest drain = new QueryPlanRequest();
drain.maxCount(100_000);
drain.maxTimeMillis(30_000);
DB.getDefault().metaInfo().queryPlanCollectNow(drain);
}
@Test
void sqlQuery_capturesQueryPlan() {
ResetBasicData.reset();
// drain any query plans captured by earlier tests in the suite, otherwise the
// shared (process-global) capture map can exhaust the collect budget below before
// our custSqlPlan is reached (order dependent).
drainCapturedPlans();
// build the plan first (default threshold -> no capture yet)
assertThat(runQuery()).isNotEmpty();
// arming an already-built SqlQuery plan should return its meta
QueryPlanInit init = new QueryPlanInit();
init.setAll(true);
init.thresholdMicros(1);
List<MetaQueryPlan> armed = DB.getDefault().metaInfo().queryPlanInit(init);
assertThat(armed)
.as("queryPlanInit includes the (already built) SqlQuery plan")
.anyMatch(p -> "sql.query.custSqlPlan".equals(p.label()));
// run again now that the plan is armed -> bind values captured
assertThat(runQuery()).isNotEmpty();
QueryPlanRequest request = new QueryPlanRequest();
request.maxCount(1000);
request.maxTimeMillis(10_000);
List<MetaQueryPlan> plans = DB.getDefault().metaInfo().queryPlanCollectNow(request);
MetaQueryPlan sqlPlan = plans.stream()
.filter(p -> "sql.query.custSqlPlan".equals(p.label()))
.findFirst()
.orElse(null);
assertThat(sqlPlan).as("captured a SqlQuery query plan").isNotNull();
assertThat(sqlPlan.sql()).contains("from o_customer where id > ?");
assertThat(sqlPlan.plan()).isNotEmpty();
}
}
@@ -51,7 +51,7 @@ public class DbMigrationTest extends BaseTestCase {
Platform.YUGABYTE,
})
@Test
public void testRunMigration() throws IOException, SQLException {
public void testRunMigration() throws IOException, SQLException, InterruptedException {
// Shutdown and reconnect - this prevents postgres from lock up
((DataSourcePool)server().dataSource()).offline();
((DataSourcePool)server().dataSource()).online();
@@ -208,7 +208,7 @@ public class DbMigrationTest extends BaseTestCase {
}
// do some history tests with V1.1 models
private void testVersioning() {
private void testVersioning() throws InterruptedException {
if (isOracle()) {
System.err.println("FIXME: Oracle history support seems to be broken");
return;
@@ -233,6 +233,9 @@ public class DbMigrationTest extends BaseTestCase {
hist.setId(2);
hist.setTestString(42L);
tmpServer.save(hist);
// ensure a distinct period start time so SQL Server temporal records a
// non zero-duration history row (otherwise the prior version is omitted)
Thread.sleep(20);
hist = tmpServer.find(EHistory.class).where().eq("testString", 42L).findOne();
assert hist != null;
hist.setTestString(45L);
@@ -250,6 +253,9 @@ public class DbMigrationTest extends BaseTestCase {
hist2.setTestString2("bar1");
hist2.setTestString3("baz1");
tmpServer.save(hist2);
// ensure a distinct period start time so SQL Server temporal records a
// non zero-duration history row (otherwise the prior version is omitted)
Thread.sleep(20);
hist2.setTestString("foo2");
hist2.setTestString2("bar2");
tmpServer.save(hist2);
@@ -14,6 +14,7 @@ import io.ebeaninternal.server.transaction.RemoteTransactionEvent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import org.tests.model.basic.VwCustomer;
import java.sql.Date;
@@ -35,6 +36,8 @@ class ResourceEntityTest {
@BeforeAll
static void setup() {
ResetBasicData.reset();
Locale en = Locale.GERMAN;
Locale de = Locale.ENGLISH;
@@ -589,23 +592,26 @@ class ResourceEntityTest {
customerEntity.setStatus(Customer.Status.ACTIVE);
customerEntity.setAnniversary(new Date(System.currentTimeMillis()));
DB.save(customerEntity);
ImmutableBeanCache<VwCustomer> cache = ImmutableBeanCaches.loading(VwCustomer.class, DB.getDefault(), FetchGroup.of(VwCustomer.class, "name"));
Object existingId = customerEntity.getId();
try {
ImmutableBeanCache<VwCustomer> cache = ImmutableBeanCaches.loading(VwCustomer.class, DB.getDefault(), FetchGroup.of(VwCustomer.class, "name"));
LoggedSql.start();
cache.getAll(Set.of(existingId));
List<String> initialLoadSql = LoggedSql.stop();
assertThat(initialLoadSql.stream().filter(sql -> sql.contains(" from o_customer "))).hasSize(1);
LoggedSql.start();
cache.getAll(Set.of(existingId));
List<String> initialLoadSql = LoggedSql.stop();
assertThat(initialLoadSql.stream().filter(sql -> sql.contains(" from o_customer "))).hasSize(1);
DB.sqlUpdate("update o_customer set name = name where id = ?")
.setParameter(existingId)
.execute();
DB.sqlUpdate("update o_customer set name = name where id = ?")
.setParameter(existingId)
.execute();
LoggedSql.start();
cache.getAll(Set.of(existingId));
List<String> postTouchSql = LoggedSql.stop();
assertThat(postTouchSql.stream().filter(sql -> sql.contains(" from o_customer "))).hasSize(1);
LoggedSql.start();
cache.getAll(Set.of(existingId));
List<String> postTouchSql = LoggedSql.stop();
assertThat(postTouchSql.stream().filter(sql -> sql.contains(" from o_customer "))).hasSize(1);
} finally {
DB.delete(Customer.class, existingId);
}
}
@Test
@@ -155,6 +155,33 @@ public class TestSoftDeleteBasic extends BaseTestCase {
assertThat(findInclude).isNotNull();
}
@Test
public void testRefreshSoftDeleted() {
EBasicSoftDelete bean = new EBasicSoftDelete();
bean.setName("refreshSoftDeleted");
DB.save(bean);
DB.delete(bean);
// obtain a handle to the soft-deleted bean
EBasicSoftDelete softDeleted = DB.find(EBasicSoftDelete.class)
.setId(bean.getId())
.setIncludeSoftDeletes()
.findOne();
assertThat(softDeleted).isNotNull();
// refresh() must not throw EntityNotFoundException for a soft-deleted bean (issue #3641)
DB.refresh(softDeleted);
assertThat(softDeleted.getName()).isEqualTo("refreshSoftDeleted");
assertThat(softDeleted.isDeleted()).isTrue();
// Cleanup created entity
DB.deletePermanent(softDeleted);
}
@Test
public void testFindSoftDeleted() {
@@ -2,4 +2,5 @@ ebean.test.platform=oracle
ebean.test.dbName=test_eb
datasource.default=oracle
ebean.lengthCheck=utf8
datasource.oracle.pstmtCacheSize=100
datasource.oracle.pstmtCacheSize=50
datasource.oracle.maxConnections=20
+2 -2
View File
@@ -15,8 +15,8 @@ mvn -T 4 clean package
mvn -T 4 deploy -pl '!composites,!platforms' -Pcentral -DskipTests
## git commit, git tag, git push --tags
git commit -am 'Version 16.9.0'
git tag 16.9.0
git commit -am 'Version 16.11.1'
git tag 16.11.1
git push --tags
## convert to javax

Some files were not shown because too many files have changed in this diff Show More