Compare commits

..
77 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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 Bygrave 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
Rob Bygrave 3f6d565800 Version 16.9.0 2026-06-08 19:09:46 +12:00
Rob Bygrave 1408696912 Redesign of metric labels - secondary queries (lazy|query) now just u… (#3779)
* Redesign of metric labels - secondary queries (lazy|query) now just use parent + relativePath + type

# Query metric/plan label change — comparison

Improves the metric/plan name generated for **secondary** (`_lazy` / `_query`) loads so
they relate to their parent/root query, and unifies separators on `.`.

The secondary lazy name is now **always** `orm.<parent's full name>.<path>.<loadMode>`,
so it always prefixes the real parent metric.

## Root query name

| Root query source | Original | New |
|---|---|---|
| `setLabel("custMain")` | `orm.Customer_custMain` | `orm.Customer.custMain` |
| ProfileLocation `CustomerFinder.byName` | `orm.CustomerFinder.byName` | `orm.CustomerFinder.byName` *(same)* |
| ProfileLocation `DataLoader.loadAll` (Customer query) | `orm.Customer_DataLoader.loadAll` | `orm.Customer.DataLoader.loadAll` |
| Unlabeled, no location | `orm.Customer.findList` | `orm.Customer.findList` *(same)* |

## Secondary lazy (`contacts`) name

Original prefixes the **loaded** type (`Contact`) + the call-site location and uses `__`
between path and load mode. New prefixes the **parent's full name**.

| Root query source | Original lazy name | New lazy name |
|---|---|---|
| `setLabel("custMain")` *(profile location also present)* | `orm.Contact_CustomerFinder.findActive_contacts__lazy` — explicit label **lost** | `orm.Customer.custMain.contacts.lazy` |
| `setLabel("custMain")` *(no profile location)* | `orm.Contact_custMain_contacts__lazy` | `orm.Customer.custMain.contacts.lazy` |
| ProfileLocation `CustomerFinder.byName` | `orm.Contact_CustomerFinder.byName_contacts__lazy` | `orm.CustomerFinder.byName.contacts.lazy` |
| ProfileLocation `DataLoader.loadAll` | `orm.Contact_DataLoader.loadAll_contacts__lazy` | `orm.Customer.DataLoader.loadAll.contacts.lazy` |
| Unlabeled, no location | `orm.Contact.findList` *(own name; no parent path)* | `orm.Contact.findList` *(same)* |

## Problems fixed
- **(A) Secondary load didn't relate to its parent** — original prefixed the *loaded* type
  (`Contact`) + the *call-site* location, never the parent query's name. New name literally
  starts with the parent's full name.
- Explicit `setLabel` was **silently dropped** for secondary queries when a profile location
  existed.
- `__` path/loadMode separator and mixed `_`/`.` replaced by uniform `.`.

## Nested and `.query` secondary loads

Each secondary query name is `<immediate parent's full name>.<immediate path>.<loadMode>`,
so every hop literally prefixes its parent metric. Example with root `setLabel("custMain")`
on `Customer`, chain `Customer -> orders -> details`:

Nested lazy:
```
orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy
```

Secondary eager `.query` fetch:
```
orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query
```

The intermediate load mode (`.lazy.` / `.query.`) is retained so each name is an exact
extension of its immediate parent's name.

* Use ProfileLocation as leading metric name without <type> prefix

* docs: Add guide for ebean-query-metrics.md

* docs: Add guide for query plan capture
2026-06-08 19:03:43 +12:00
Rob Bygrave e0531a6315 Merge branch 'master' of github.com:ebean-orm/ebean 2026-06-08 19:01:17 +12:00
Rob Bygraveandrobin.bygrave 0023f0ce13 [open telemetry] Add query lable as an attribute to the query spans (#3778)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-08 10:42:12 +12:00
Rob Bygrave 032f4de857 Version 16.8.0 2026-06-06 20:06:30 +12:00
Rob Bygrave 59431814ce Merge branch 'master' of github.com:ebean-orm/ebean 2026-06-06 19:57:18 +12:00
Rob Bygrave e7194055be Merge pull request #3776 from ebean-orm/feature/PString_eqIfNotBlank
Add PString eqIfNotBlank() helper query expression
2026-06-06 18:37:59 +12:00
robin.bygrave 806c7cd752 Fix test with overlapping cases 2026-06-06 18:16:11 +12:00
Rob Bygrave dfc7f92160 Merge pull request #3777 from ebean-orm/fature/otel-query-hash
[open telemetry] Add query hash as an attribute to the query spans
2026-06-06 18:04:18 +12:00
robin.bygrave 26351325a8 Fix test with overlapping cases 2026-06-06 18:03:39 +12:00
robin.bygrave 2d37f9d01e [open telemetry] Add query hash as an attribute to the query spans 2026-06-06 17:55:59 +12:00
robin.bygrave d3fd03ce5b Add PString eqIfNotBlank() helper query expression
Just to make this relatively common case pretty nice and clean
2026-06-06 17:45:45 +12:00
Rob Bygrave a8e92791cd Version 16.7.0 2026-06-03 07:44:59 +12:00
Rob Bygrave 94e63cc30f Merge pull request #3774 from ebean-orm/feature/timedMetric-max-reset
Add collectMetrics(reset) option, with change to query time metric MA…
2026-06-02 14:48:15 +12:00
Rob Bygrave 3567263250 Merge pull request #3775 from ebean-orm/feature/fix-error-profiling-batch
open telemetry: Fix for IndexOutOfBoundsException with Batched PreparedStatements and open telemetry
2026-06-02 14:46:54 +12:00
robin.bygrave bc03f8d516 open telemetry: Fix for IndexOutOfBoundsException with Batched PreparedStatements and open telemetry
Caused by: java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
 at jdk.internal.util.Preconditions.outOfBounds(Unknown Source)
 at jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Unknown Source)
 at jdk.internal.util.Preconditions.checkIndex(Unknown Source)
 at java.util.Objects.checkIndex(Unknown Source)
 at java.util.ArrayList.get(Unknown Source)
 at io.ebeaninternal.server.persist.BatchedPstmt.profile(BatchedPstmt.java:132)
2026-06-02 14:42:44 +12:00
robin.bygrave 7035ff20eb Docs: Add a doc guide for adding open telemetry 2026-05-29 17:11:04 +12:00
Rob Bygrave f38479022a Merge pull request #3773 from ebean-orm/dependabot/maven/ebean-opentelemetry/io.opentelemetry-opentelemetry-api-1.62.0
Build(deps): Bump io.opentelemetry:opentelemetry-api from 1.51.0 to 1.62.0 in /ebean-opentelemetry
2026-05-21 22:12:11 +12:00
robin.bygrave 986cc905d8 Fix test only - Resource test entity table name invalid for Oracle 2026-05-21 22:10:58 +12:00
robin.bygrave 8f3fe688cb Fix test only - ResourceEntityTest SQL for Postgres uses ANY rather than IN 2026-05-21 22:05:59 +12:00
robin.bygrave eee26d9ed3 Add collectMetrics(reset) option, with change to query time metric MAX value to reset
So the desire here is to better support sending metrics to Prometheus style metrics collectors that prefer CUMULATIVE metrics rather than DELTA based metrics.

To do this, expose additional MetaInfoManager.collectMetrics(reset) method.

In supporting this, the MAX value really does need to reset even with CUMULATIVE metrics and act more like a gauge as otherwise it becomes almost useless as the max value over the lifetime. So we need to adjust MAX to always reset and act like a gauge to be useful in this CUMULATIVE metrics reporting mode.
2026-05-21 22:03:15 +12:00
dependabot[bot] 44bd60e586 Build(deps): Bump io.opentelemetry:opentelemetry-api
Bumps [io.opentelemetry:opentelemetry-api](https://github.com/open-telemetry/opentelemetry-java) from 1.51.0 to 1.62.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-java/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-java/compare/v1.51.0...v1.62.0)

---
updated-dependencies:
- dependency-name: io.opentelemetry:opentelemetry-api
  dependency-version: 1.62.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-14 16:44:36 +00:00
Rob Bygrave 20d7af6d25 Merge pull request #3772 from ebean-orm/feature/otel-isRecording
open telemetry: Use Span.current().isRecording() rather than isValid()
2026-05-14 16:22:57 +12:00
robin.bygrave e5aeda9d4e open telemetry: Use Span.current().isRecording() rather than isValid()
Change to use isRecording() to determine if ProfileStream should be
created for collecting profiling events
2026-05-14 16:20:28 +12:00
Sergey Kuznetsov 46bb1ca060 Add timeout support to UpdateQuery and DefaultUpdateQuery (#3771) 2026-05-14 15:25:07 +12:00
robin.bygrave 0652167101 docs: add next steps to test container guide
- Suggest TestEntityBuilder for test data creation
- Link to testing-with-testentitybuilder.md
2026-05-13 13:57:40 +12:00
robin.bygrave 6633293151 docs: update guide versions and fix defaults
- ebean.version 17.2.0 -> 17.5.0 (minimum for TestEntityBuilder)
- avaje-inject 11.5 -> 12.5
- Add io.avaje:junit:1.8 bundle to test dependencies
- Remove .skipDataSourceCheck(true) from database config example
- Add note about Configuration DI wiring in Step 3
2026-05-13 13:57:39 +12:00
Rob Bygrave 9e603f2848 Deprecate DatabaseConfig and DatabaseFactory, prefer Database.builder() (#3769)
* Deprecate DatabaseConfig and DatabaseFactory, prefer Database.builder()

Deprecate the DatabaseConfig way of creating Database instance.
Migrate to use Database.builder().

* Deprecate DatabaseConfig and DatabaseFactory, migrate test code

* Use @Deprecated(forRemoval = true) on DatabaseConfig and DatabaseFactory

* Tidy up Database javadoc for deprecation
2026-05-08 00:06:43 +12:00
Rob Bygrave fff345ffc8 Merge branch 'master' of github.com:ebean-orm/ebean 2026-05-07 22:13:16 +12:00
Rob Bygrave dd1fe845aa Remove the DefaultProfileHandler & DefaultProfileStream (Replaced by ebean-opentelemetry) (#3767)
So migrate to use opentelemetry, and remove these internal profilers as
they are never expected to be used now.
2026-05-07 22:10:08 +12:00
Rob Bygrave 80451c3c62 Performance: Improve OrmQueryProperties, include } into immutableHashSuffix (#3768) 2026-05-07 21:42:20 +12:00
Rob Bygrave 2d199737aa Docs: Add docs/notes on potential future L2 immutable bean cache 2026-05-07 21:36:45 +12:00
204 changed files with 4920 additions and 1308 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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.6.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.6.0</version>
<version>16.11.1</version>
</parent>
<artifactId>composites</artifactId>
+3
View File
@@ -11,8 +11,11 @@ Step-by-step guides for common Ebean tasks (Maven setup, database config, query
Key guides (fetch and follow when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
+19
View File
@@ -18,6 +18,20 @@ existing Maven project. Complete the steps in order.
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
## Migration & upgrades
| Guide | Description |
|-------|-------------|
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
## Observability
| Guide | Description |
|-------|-------------|
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
## Entity beans
| Guide | Description |
@@ -133,8 +147,12 @@ tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
@@ -158,6 +176,7 @@ tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
@@ -250,6 +250,40 @@ For each future set of entity bean changes:
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
### Protecting hand-edited and non-versioned migrations across regeneration
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
entity model**. It can therefore overwrite content you did not change in the
entity beans, including:
- **hand-edited DDL** in a generated versioned `.sql` file, and
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must **delete the file first**, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
cannot express — it is isolated and now protected from regeneration.
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
produces, review the diff after **every** regeneration and **restore** any
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
re-check that the new migration is listed and filenames match after renaming a
generated file.
> **Run the generator from the module directory.** The output path set via
> `setPathToResources(...)` is resolved relative to the **working directory**.
> Run `GenerateDbMigration` with the working directory set to the module that owns
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
---
## Understanding the output files
+158
View File
@@ -0,0 +1,158 @@
# Guide: Add Ebean OpenTelemetry tracing
## Purpose
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
most importantly, how to order startup so Ebean sees the intended global
OpenTelemetry instance.
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
or fixing `GlobalOpenTelemetry` double-registration errors.
---
## Overview
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
spans as children of the current active OpenTelemetry span. It does not create
top-level request, job, or Lambda invocation spans by itself.
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
`Database` is configured. For that reason, the application must build and register
the OpenTelemetry SDK before any Ebean `Database` beans are created.
Rules of thumb:
- Register the global OpenTelemetry instance once.
- Register it before building Ebean databases.
- Model that ordering as a real DI dependency.
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
multiple places.
---
## Step 1 - Add the dependency
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-opentelemetry</artifactId>
<version>${ebean.version}</version>
</dependency>
```
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
No manual Ebean plugin registration is normally required.
---
## Step 2 - Build OpenTelemetry before Ebean databases
Create one application-owned OpenTelemetry bean. For example, when using
`avaje-metrics-otel`:
```java
import io.avaje.config.Configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.metrics.otel.MetricsOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import java.time.Duration;
@Factory
class OpenTelemetryConfig {
@Bean
OpenTelemetry openTelemetry(Configuration config) {
return MetricsOpenTelemetry.builder()
.endpoint(config.get("otel.endpoint"))
.serviceName(config.get("otel.serviceName", "orders"))
.deploymentEnvironmentName(config.get("app.env", "local"))
.meterInterval(Duration.ofSeconds(30))
.traceInterval(Duration.ofSeconds(30))
.buildAndRegisterGlobal();
}
}
```
If you build the SDK directly, use the same principle: create the SDK once and
register that instance globally before any Ebean databases are built.
---
## Step 3 - Make database beans depend on OpenTelemetry
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
is intentionally present to make startup order deterministic: OpenTelemetry is
created and registered before Ebean configures the database and profile handler.
```java
import io.avaje.config.Configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;
import io.opentelemetry.api.OpenTelemetry;
@Factory
class DatabaseConfig {
@Bean
Database database(OpenTelemetry openTelemetry, Configuration config) {
var dataSource = DataSourceBuilder.create()
.url(config.get("db.url"))
.username(config.get("db.username"))
.password(config.get("db.password"));
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.build();
}
}
```
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
initialized first.
Do not invert the dependency by making OpenTelemetry depend on the Ebean
`Database`. That creates a startup cycle and can still initialize Ebean before the
global OpenTelemetry instance is ready.
---
## Step 4 - Create a parent span at the application boundary
Ebean transaction spans are child spans. They are only created when a recording
OpenTelemetry span is active on the current thread.
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
root span around the top-level request/job boundary. Ebean will then attach
transaction spans beneath that current span.
---
## Troubleshooting
### `GlobalOpenTelemetry.set has already been called`
This usually means more than one component is trying to register a global SDK, or
some startup path touched the global before the application registered its SDK.
Fixes:
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
call in the application.
2. Build that OpenTelemetry bean before Ebean `Database` beans.
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
### No Ebean spans appear
Check:
1. `ebean-opentelemetry` is on the runtime classpath.
2. OpenTelemetry is registered before Ebean databases are built.
3. There is a current recording parent span when Ebean transactions run.
4. Sampling is not dropping the parent trace.
@@ -1,4 +1,4 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 2: Database Configuration
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Database Configuration
## Purpose
@@ -148,7 +148,6 @@ Database database(Configuration config) {
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.skipDataSourceCheck(true)
.build();
}
```
@@ -156,6 +155,11 @@ Database database(Configuration config) {
If the project has a dedicated config-wrapper class (a `@Component` that reads config
keys), accept it as a parameter instead of `Configuration`.
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
> into the DI context. If you encounter "No dependency provided for
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
> (as shown in Step 2).
---
## Step 4 (Optional) — Add a read-only datasource
+13 -4
View File
@@ -85,7 +85,7 @@ already exist:
```xml
<properties>
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
<ebean.version>17.2.0</ebean.version>
<ebean.version>17.5.0</ebean.version>
</properties>
```
@@ -141,8 +141,17 @@ for Postgres test instances:
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>junit</artifactId>
<version>1.8</version>
<scope>test</scope>
</dependency>
```
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
avoiding the need to declare those dependencies separately.
---
## Step 4b — Add DI framework dependencies (if applicable)
@@ -156,12 +165,12 @@ annotation processor. Skip this step if the user chose Spring or no DI.
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
<version>11.5</version>
<version>12.5</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-test</artifactId>
<version>11.5</version>
<version>12.5</version>
<scope>test</scope>
</dependency>
```
@@ -185,7 +194,7 @@ the final `<annotationProcessorPaths>` block should include both:
<path> <!-- generate avaje-inject DI code -->
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>11.5</version>
<version>12.5</version>
</path>
</annotationProcessorPaths>
```
@@ -433,3 +433,13 @@ mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
```
On CI servers, omit this file so containers are cleaned up after each build.
---
## Next Steps
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
with auto-populated random values. See `testing-with-testentitybuilder.md`.
- **Proceed to Step 3** — production database configuration
(`add-ebean-postgres-database-config.md`). Verify this step passes with
`mvn verify` before continuing.
+262
View File
@@ -0,0 +1,262 @@
# Guide: Ebean query metrics and naming
## Purpose
This guide explains the metrics Ebean captures, how the metric **name** for a query
is derived, and how you influence that name with `setLabel(..)` and **profile
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
Use this guide when you want to identify a query in metrics/telemetry, when a query
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
---
## Overview
Ebean records timing and counter metrics for the work it does. Every metric has a
**name** whose leading segment identifies the kind of work:
| Prefix | What it measures | Example name |
|---|---|---|
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
| `orm.update.` | ORM update statement | `orm.update.<label>` |
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
| `l2n.` | L2 cache region | `l2n.customer.hit` |
The rest of this guide focuses on **`orm.` query names**, which is where labels and
profile locations apply.
---
## How an ORM query name is derived
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
of three sources, in priority order:
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
3. **Neither** — the bean type plus the query type (e.g. `findList`).
| Root query source | Resulting name |
|---|---|
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
could be used for any bean), so the bean type is prefixed. A profile location is already
unique and type-independent, so it is used as-is.
### Step 1 - Label a query explicitly
```java
List<Customer> customers = DB.find(Customer.class)
.setLabel("custMain")
.findList();
// metric name: orm.Customer.custMain
```
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
used as-is, and an unlabelled DTO query uses just the DTO type:
```java
DB.findDto(CustomerDto.class, sql)
.setLabel("byEmail")
.findList();
// metric name: dto.CustomerDto.byEmail
// profile location only -> dto.<location> (no type prefix)
// unlabelled -> dto.CustomerDto
```
### Step 2 - Use a profile location (preferred for finders / query beans)
A profile location identifies a query by its **call site** (`Class.method`) instead of a
hand-written label.
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
setup when using query beans / finders), Ebean assigns each query a profile location
derived from its call site — no code is required:
```java
List<Customer> customers = new QCustomer()
.status.eq(Status.ACTIVE)
.findList();
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
```
The enhancer derives the location from the calling code (the method that runs the query),
and for many call sites it includes the **source line number** (e.g.
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
names automatically.
**Setting one explicitly.** You can also set a profile location yourself, which is useful
without enhancement or to control the identity:
```java
ProfileLocation LOC = ProfileLocation.create();
List<Customer> customers = DB.find(Customer.class)
.setProfileLocation(LOC)
.where().eq("status", Status.ACTIVE)
.findList();
// metric name: orm.<DeclaringClass>.<method>
```
Factory choices:
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
- `ProfileLocation.createWithLine()` — includes the source line number
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
**distinct** names.
- `ProfileLocation.create("label")` — a named location (used for named transactions).
> Note: a location with no line number (`create()`, or a call site the enhancer emits
> without a line) means two different queries in the same method share one name. The
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
> same-method call sites in the name itself.
---
## Secondary (lazy / query) load naming
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
**secondary** query. Its name **extends the parent query's full name** with the relative
path and the load mode (`lazy` or `query`), joined with `.`:
```
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
```
So a secondary load is always an exact extension of its parent metric name, which makes
the relationship obvious in dashboards.
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
Lazy loading:
```
orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy
```
Secondary eager `fetchQuery()` loading:
```
orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query
```
The same applies with a **profile-location** root (no explicit `setLabel`):
```
orm.CustomerFinder.byName
orm.CustomerFinder.byName.contacts.lazy
```
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
type — it inherits the parent's name so it relates back to where the load originated.
---
## Inline SQL comment
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
profile-location label) as an inline SQL comment, which is useful for matching slow
queries in database logs back to application code:
```sql
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
```
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
label. Secondary queries use their full extended name
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
commented.
Disable it via the builder:
```java
Database.builder()
.includeLabelInSql(false)
.build();
```
---
## Collecting metrics at runtime
Read collected metrics through `Database.metaInfo()`:
```java
import io.ebean.meta.MetaQueryMetric;
import io.ebean.meta.ServerMetrics;
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
for (MetaQueryMetric q : metrics.queryMetrics()) {
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
q.name(), // e.g. orm.Customer.custMain
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
q.count(), q.total(), q.mean());
}
```
Key API:
- `database.metaInfo()``MetaInfoManager`.
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
reset; `visitMetrics(visitor)` for streaming.
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
---
## Mapping to avaje-metrics tags
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
form, with the bean type carried as a `type` tag:
```
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
```
Because the entity is available as the `type` tag, two different-entity queries that
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
Prometheus, StatsD) without needing the bean type in the name.
For the integration setup, see the avaje-metrics guide
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
---
## Troubleshooting
### A query shows up as `orm.<Bean>.findList` (no useful identity)
It has neither a label nor a profile location. Add `setLabel(..)` or a
`ProfileLocation`, or apply a profile location on the finder / query bean.
### Two queries in one method share a metric name
This happens when the profile location for those call sites has no line number. With
enhancement, many call sites already include a line number; for those that don't, use
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
different entity types.
### A secondary (lazy / query) load isn't grouped under its parent
Secondary names extend the parent's full name. If the parent has no label or profile
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
that. Give the root query a label or profile location for a stable parent name.
+242
View File
@@ -0,0 +1,242 @@
# Guide: Ebean query plan capture
## Purpose
This guide explains how to enable and configure **query plan capture** in Ebean — the
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
queries, so you can diagnose missing indexes and poor plans in production.
Use this guide when you want Ebean to record real query plans, when tuning the capture
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
---
## Overview
Query plan capture is a **two-phase** mechanism:
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
slower than a threshold, captures the actual **bind values** that were used. This is
cheap: it just remembers the parameters of a slow execution.
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
against the database to obtain the execution plan, producing `MetaQueryPlan` results
that are handed to a `QueryPlanListener`.
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
happens periodically or on demand, against representative bind values, rather than on
every slow query.
Two ways to trigger phase 2:
- **Automatic periodic capture** — a background timer collects plans on a schedule.
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
(this is what remote tooling such as ebean-insight uses).
Plan capable queries are:
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
does not double-count in `queryPlanInit`.
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
Specifically **excluded** are:
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
never captures bind values and cannot be `EXPLAIN`'d.
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
> incur no extra cost beyond their existing timing metric.
---
## Step 1 - Enable bind capture
Bind capture is the master switch; nothing is captured until it is on.
> **Security — bind values may contain PII.** Bind capture records the **actual
> parameter values** used by slow query executions, and those values are stored
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
> plan). They can therefore contain personal or otherwise sensitive data. Capture
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
> that data exposure is acceptable, restrict who can read captured plans, and
> prefer arming specific query hashes (Step 3) over a low global threshold so you
> capture the minimum needed.
```java
Database database = Database.builder()
.queryPlanEnable(true) // turn on bind capture
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
.build();
```
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
Equivalent `application.properties` (avaje-config / properties):
```properties
queryPlan.enable=true
queryPlan.thresholdMicros=100000
```
---
## Step 2 - Enable automatic periodic capture (optional)
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
```java
Database database = Database.builder()
.queryPlanEnable(true)
.queryPlanThresholdMicros(100_000)
.queryPlanCapture(true) // turn on the periodic capture timer
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
.queryPlanListener(capture -> {
for (var plan : capture.plans()) {
System.out.println(plan.label() + "\n" + plan.plan());
}
})
.build();
```
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
exceeded, bounding the database load. Default **10000** (10s).
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
Properties form:
```properties
queryPlan.enable=true
queryPlan.thresholdMicros=100000
queryPlan.capture=true
queryPlan.capturePeriodSecs=600
queryPlan.captureMaxTimeMillis=10000
queryPlan.captureMaxCount=10
```
---
## Step 3 - Capture on demand (foreground)
Instead of (or in addition to) the periodic timer, drive capture through
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
arms specific slow queries by their plan hash.
```java
import io.ebean.meta.MetaInfoManager;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.QueryPlanInit;
import io.ebean.meta.QueryPlanRequest;
MetaInfoManager meta = database.metaInfo();
// Phase 1: arm bind capture - either all plans or specific hashes
QueryPlanInit init = new QueryPlanInit();
init.setAll(true); // or init.add("<planHash>", 50_000);
init.thresholdMicros(100_000);
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
// ... let the application run so slow executions capture their bind values ...
// Phase 2: collect plans now (runs EXPLAIN)
QueryPlanRequest request = new QueryPlanRequest();
request.maxCount(10);
request.maxTimeMillis(10_000);
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
```
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
arms a specific plan (a hash of `"all"` is treated as all).
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
bound the work, mirroring the periodic settings.
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
`captureMicros()`, and `whenCaptured()`.
---
## Step 4 - EXPLAIN dialect
Ebean chooses the `EXPLAIN` statement per database platform:
| Platform | EXPLAIN used |
|---|---|
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
| Oracle | `EXPLAIN PLAN FOR <sql>` |
| SQL Server | platform-specific logger |
| H2 / MySQL / other | `explain <sql>` |
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
```java
Database.builder()
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
.build();
```
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
> executes** the query to produce real timings. For non-idempotent or expensive queries,
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
---
## Related setting: internal plan TTL
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
the same builder.
---
## Troubleshooting
### No plans are captured
1. `queryPlanEnable(true)` must be set — it is the master switch.
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
3. For periodic capture, also set `queryPlanCapture(true)`.
4. Queries must actually run slower than the threshold to have their binds captured.
### Plans appear but nothing is reported anywhere
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
### Capture adds noticeable database load
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
`queryPlanExplain` to a non-ANALYZE form.
### An unlabelled SqlQuery or update metric never offers plan capture
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
capture and are intentionally excluded.
@@ -0,0 +1,242 @@
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
## Purpose
This guide shows how to migrate legacy programmatic database creation code from:
- `new DatabaseConfig()`
- `DatabaseFactory.create(...)`
- old `setXxx(...)` builder-style configuration methods
…to the preferred builder-based style using:
- `Database.builder()`
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
- `DatabaseBuilder.build()`
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
---
## Preferred pattern
Prefer code shaped like this:
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.dataSourceBuilder(dataSource)
.register(true)
.defaultDatabase(true)
.build();
```
The important points are:
1. Start with `Database.builder()`
2. Configure via `DatabaseBuilder`
3. Finish with `.build()`
---
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
```
### Notes
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
- If existing code only uses standard builder methods, this change is usually mechanical.
- If existing code later reads configuration back, use `config.settings()`.
---
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
Database database = DatabaseFactory.create(config);
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
Database database = config.build();
```
### Short form
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.build();
```
---
## Step 3 — Replace `DatabaseFactory.create("name")`
### Before
```java
Database database = DatabaseFactory.create("other");
```
### After
```java
Database database = Database.builder()
.name("other")
.loadFromProperties()
.build();
```
### Important
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
---
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
Use those names when migrating older setup code.
| Legacy call | Preferred call |
|---|---|
| `setName("db")` | `name("db")` |
| `setRegister(false)` | `register(false)` |
| `setDefaultServer(false)` | `defaultDatabase(false)` |
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
| `setDbSchema("app")` | `dbSchema("app")` |
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
| `setRunMigration(true)` | `runMigration(true)` |
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
| `setPersistBatch(batch)` | `persistBatch(batch)` |
### Full example
#### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.setRegister(false);
config.setDefaultServer(false);
config.setDataSourceConfig(dataSource);
Database database = DatabaseFactory.create(config);
```
#### After
```java
Database database = Database.builder()
.name("db")
.register(false)
.defaultDatabase(false)
.dataSourceBuilder(dataSource)
.build();
```
---
## Step 5 — Verify semantics after migration
The migration should preserve behavior, but verify these points:
- `register(true)` is still the default
- `defaultDatabase(true)` is still the default
- call `loadFromProperties()` if the old code loaded configuration from properties
- for named databases, set the name before loading properties
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
---
## Manual-review cases
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
### `DatabaseFactory.createWithContextClassLoader(...)`
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
### `DatabaseFactory.initialiseContainer(...)`
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
### `DatabaseFactory.shutdown()`
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
### Variables or method signatures typed as `DatabaseConfig`
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
### Code that needs read access to builder settings
Use:
```java
DatabaseBuilder builder = Database.builder();
DatabaseBuilder.Settings settings = builder.settings();
```
rather than relying on the concrete `DatabaseConfig` type only to read getters.
---
## Automation notes for AI agents and bulk refactors
This migration is a good candidate for semi-automated upgrading.
### Safe mechanical rewrites
These are usually safe to rewrite automatically:
- `new DatabaseConfig()``Database.builder()`
- `DatabaseFactory.create(builder)``builder.build()`
- `DatabaseFactory.create("name")``Database.builder().name("name").loadFromProperties().build()`
- legacy `setXxx(...)` calls → preferred fluent builder methods
### Flag for manual review
Automatically flag, but do not blindly rewrite:
- `DatabaseFactory.createWithContextClassLoader(...)`
- `DatabaseFactory.initialiseContainer(...)`
- `DatabaseFactory.shutdown()`
- parameters, fields, or return types declared as `DatabaseConfig`
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
---
## Related guides
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
- [Guide index](README.md) — full list of Ebean setup and migration guides
+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"
---
+333
View File
@@ -0,0 +1,333 @@
# Immutable Bean Cache — notes on multi-level / remote caching
These notes capture design thoughts for a possible future multi-level immutable bean cache,
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
in addition to an in-JVM cache.
## Current important constraint
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
That means:
- `getIfPresent(id)` is on the **row read hot path**
- it must remain **cheap and local**
- it should **not** perform network I/O
- it should **not** deserialize remote payloads
- it should **not** trigger loading or record misses
## Strong recommendation
For any multi-level cache design:
- **L1 cache** = in-JVM cache of already materialized immutable beans
- **L2 cache** = remote/shared cache of serialized immutable snapshots
- **Loader** = Ebean query using the configured fetch group
With that split:
- `getIfPresent(id)` => **L1 only**
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
This preserves the `AssocOneHelp` fast path.
---
## Snapshot mindset
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
A cached value is specific to:
- bean type
- bean id
- tenant (if multi-tenant)
- fetch-group / cache identity
- serializer/schema version
This matters because a `Customer` cached with:
- `select("name,version")`
is not equivalent to a `Customer` cached with:
- `select("name,version").fetch("billingAddress", "line1,city")`
## Key design recommendation
Remote keys should include at least:
- bean type
- bean id
- tenant id (if applicable)
- cache/fetch-group identity
- optionally serializer/schema version
Example shape:
- `immutable:Customer:basic:42`
- `immutable:Customer:withAddresses:42`
---
## Recommended multi-level flow
### L1
Store actual read-only `EntityBean` instances.
Responsibilities:
- support `getIfPresent(id)`
- avoid repeated deserialize cost
- avoid network calls on row read path
### L2
Store serialized immutable snapshots.
Responsibilities:
- batch lookup only
- support cross-JVM sharing
- feed L1 with materialized immutable beans
### Loader
Use the existing query/fetch-group-based loader for misses.
### Suggested `getAll(ids)` flow
1. Check L1
2. Batch remaining ids to L2
3. Deserialize L2 hits into read-only beans
4. Put those beans into L1
5. Batch remaining misses to DB loader
6. Freeze / ensure read-only beans
7. Write through to L2
8. Put into L1
9. Negative-cache true misses if desired
---
## Invalidation is more important than serialization
Things to think about:
- update/delete invalidation across JVMs
- local L1 invalidation when L2 entry is removed
- ordering relative to DB commit
- multiple cache instances for the same bean type but different fetch groups
- tenant-scoped invalidation
Recommended direction:
- keep current immutable-cache invalidation semantics
- add a remote invalidation/event mechanism for L2-backed caches
- each JVM should evict affected L1 entries when notified
Examples:
- Redis: pub/sub or streams
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
---
## Serialization format considerations
## JSON
### Pros
- human readable / debuggable
- easier rolling upgrades
- field-name based, so generally more tolerant of schema evolution
- good fit for Redis strings or Postgres JSONB
- easier operational debugging
### Cons
- larger payloads
- more CPU to serialize/deserialize
- nested graphs / enums / dates / inheritance need disciplined handling
## Kryo / generic binary serialization
### Pros
- smaller payloads
- often faster than JSON
- can preserve object graphs efficiently
### Cons
- more fragile across versions and rolling deploys
- class registration / compatibility pain
- harder to inspect/debug
- tighter coupling to JVM/class layout
- riskier for long-lived shared cache entries
## Recommendation
For a first remote/shared implementation:
- prefer **JSON** or another self-describing structured format
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
---
## What to serialize
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
A cleaner model is:
- serialize a **snapshot representation**
- deserialize into a fresh entity bean
- mark loaded properties appropriately
- freeze / ensure read-only state
- store the resulting materialized bean in L1
This gives more control over:
- loaded-property semantics
- read-only state
- subtype handling
- schema/version evolution
## Practical recommendation
Remote cache entries should represent exactly the configured fetch-group snapshot.
That means:
- cache what the fetch group loaded
- include nested associations loaded by that fetch group
- treat it as a self-contained immutable snapshot
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
---
## Redis vs Postgres cache table
## Redis
### Good for
- low latency
- batch lookup via MGET / pipelining
- TTL/eviction support
- natural shared-cache use case
### Tradeoffs
- extra infrastructure
- memory cost
- invalidation/event coordination still required
## Postgres cache table (including unlogged-style approach)
### Good for
- simpler ops if Postgres is already present
- easy batch lookup with `IN (...)`
- fewer moving parts than introducing Redis
### Tradeoffs
- slower than Redis for hot shared-cache usage
- adds pressure to Postgres
- TTL/cleanup becomes application responsibility
- still network/database I/O, so should remain off the `getIfPresent()` hot path
## Recommendation
- if the goal is a serious shared L2 cache, Redis is the more natural fit
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
---
## Versioning / evolution
Whatever serializer is used, include versioning information.
Useful dimensions:
- serializer/schema version
- cache implementation version
- fetch-group/cache identity version
This helps when:
- fields are added/removed
- graph shape changes
- fetch-group definitions evolve
---
## Compression
If remote snapshots become large:
- compress only above a size threshold
- avoid compressing tiny payloads
This is especially relevant for JSON in Redis or Postgres L2.
---
## Observability
A multi-level cache should expose at least:
- L1 hit rate
- L2 hit rate
- DB loader rate
- deserialize failures
- invalidation counts
- average payload size
- cold-start amplification
Without this, it will be hard to judge whether the remote cache is helping.
---
## Overall recommended architecture
### Recommended model
- **L1**: actual read-only `EntityBean` instances
- **L2**: serialized immutable snapshots
- **Loader**: fetch-group-based DB query
### Method responsibilities
- `getIfPresent(id)` => **L1 only**
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
---
## Bottom line
If/when multi-level immutable caching is explored, the main points to preserve are:
1. keep `getIfPresent()` local-only
2. do remote work only in batched `getAll()`
3. key by type + id + tenant + fetch-group/cache identity
4. treat remote values as immutable snapshots
5. prefer JSON/self-describing format first
6. be cautious with generic binary serializers like Kryo
---
## Possible follow-up
If this becomes active design work later, consider promoting these notes into one of:
- a dedicated design note under `docs/notes/`
- a GitHub issue / discussion for design iteration
- a lightweight ADR if this becomes a committed architectural direction
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.6.0</version>
<version>16.11.1</version>
</parent>
<name>ebean api</name>
+14 -10
View File
@@ -23,11 +23,18 @@ import java.util.concurrent.Callable;
/**
* Provides the API for fetching and saving beans to a particular database.
*
* <h5>Constructing a Database</h5>
* <p>
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
* They can also be automatically constructed on demand using configuration information in
* the application.properties file. The underlying implementation is provided by
* {@link DatabaseFactory}.
*
* <h5>Registration with the DB singleton</h5>
* <p>
* When a Database instance is created it can be registered with the DB
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
* singleton is essentially a map of Database's that have been registered
* When a Database instance is created it can be registered with the {@link DB}
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
* singleton is essentially a map of {@link Database}'s that have been registered
* with it.
* <p>
* The Database can then be retrieved later via {@link DB#byName(String)}.
@@ -35,16 +42,10 @@ import java.util.concurrent.Callable;
* <h5>The 'default' Database</h5>
* <p>
* One Database can be designated as the 'default' or 'primary' Database
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
* such as {@link DB#find(Class)} etc are actually just a convenient way to
* call methods on the 'default/primary' Database.
*
* <h5>Constructing a Database</h5>
* <p>
* Databases are constructed by the DatabaseFactory. They can be created
* programmatically via {@link DatabaseFactory#create(DatabaseBuilder)} or they
* can be automatically constructed on demand using configuration information in
* the application.properties file.
*
* <h5>Example: Get a Database</h5>
* <pre>{@code
@@ -80,6 +81,7 @@ import java.util.concurrent.Callable;
* method. Example: a single thread requires more than one transaction.
*
* @see DB
* @see DatabaseBuilder
* @see DatabaseFactory
* @see DatabaseConfig
*/
@@ -94,11 +96,13 @@ public interface Database {
* // from application.properties / application.yaml
*
* Database db = Database.builder()
* .name("db")
* .loadFromProperties()
* .build();
*
* }</pre>
*/
@SuppressWarnings("removal")
static DatabaseBuilder builder() {
return new DatabaseConfig();
}
@@ -723,19 +723,6 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
/**
* Set the configuration for profiling.
*/
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
return setProfilingConfig(profilingConfig);
}
/**
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
*/
@Deprecated
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
/**
* Set the suffix appended to the base table to derive the view that contains the union
* of the base table and the history table in order to support asOf queries.
@@ -995,7 +982,7 @@ public interface DatabaseBuilder {
* <p>
* Use this to override the default known aggregation functions.
*/
DatabaseConfig aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
/**
* Set to true if all DB column and table names should use quoted identifiers.
@@ -2234,7 +2221,7 @@ public interface DatabaseBuilder {
*
* @param includeLabelInSql When true include a SQL inline comment in generated SELECT queries.
*/
DatabaseConfig includeLabelInSql(boolean includeLabelInSql);
DatabaseBuilder includeLabelInSql(boolean includeLabelInSql);
/**
* Set the naming convention to apply to metrics names.
@@ -2252,7 +2239,7 @@ public interface DatabaseBuilder {
/**
* Sets the length check mode.
*/
DatabaseConfig lengthCheck(LengthCheck lengthCheck);
DatabaseBuilder lengthCheck(LengthCheck lengthCheck);
/**
* Provides read access (getters) for the DatabaseBuilder configuration
@@ -2490,11 +2477,6 @@ public interface DatabaseBuilder {
*/
TenantCatalogProvider getTenantCatalogProvider();
/**
* Return the configuration for profiling.
*/
ProfilingConfig getProfilingConfig();
/**
* Return the DB schema to use.
*/
@@ -8,18 +8,18 @@ import jakarta.persistence.PersistenceException;
import java.util.concurrent.locks.ReentrantLock;
/**
* Creates Database instances.
* Low-level factory for creating {@link Database} instances.
* <p>
* This uses either DatabaseConfig or properties in the application.properties file to
* configure and create a Database instance.
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
* This factory remains for legacy creation entry points plus container lifecycle methods.
* <p>
* The Database instance can either be registered with the DB singleton or
* not. The DB singleton effectively holds a map of Database by a name.
* If the Database is registered with the DB singleton you can retrieve it
* The Database instance can either be registered with the {@link DB} singleton or
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
* If the Database is registered with the {@link DB} singleton you can retrieve it
* later via {@link DB#byName(String)}.
* <p>
* One Database can be nominated as the 'default/primary' Database. Many
* methods on the DB singleton such as {@link DB#find(Class)} are just a
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
* convenient way of using the 'default/primary' Database.
*/
public final class DatabaseFactory {
@@ -36,7 +36,8 @@ public final class DatabaseFactory {
* Initialise the container with clustering configuration.
* <p>
* Call this prior to creating any Database instances or alternatively set the
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
*/
public static void initialiseContainer(ContainerConfig containerConfig) {
lock.lock();
@@ -48,8 +49,11 @@ public final class DatabaseFactory {
}
/**
* Create using properties to configure the database.
* Create using configuration loaded from properties for the given database name.
*
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
*/
@Deprecated
public static Database create(String name) {
lock.lock();
try {
@@ -60,18 +64,9 @@ public final class DatabaseFactory {
}
/**
* Create using the DatabaseConfig object to configure the database.
*
* <pre>{@code
*
* DatabaseConfig config = new DatabaseConfig();
* config.setName("db");
* config.loadProperties();
*
* Database database = DatabaseFactory.create(config);
*
* }</pre>
* @deprecated migrate to {@link DatabaseBuilder#build()}.
*/
@Deprecated(forRemoval = true)
public static Database create(DatabaseBuilder builder) {
lock.lock();
try {
@@ -97,7 +92,8 @@ public final class DatabaseFactory {
}
/**
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
* context class loader.
*/
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
lock.lock();
@@ -92,6 +92,7 @@ final class DbContext {
/**
* Read, create and put of Databases.
*/
@SuppressWarnings("deprecation")
private Database getWithCreate(String name) {
lock.lock();
try {
@@ -205,4 +205,20 @@ public interface UpdateQuery<T> {
*/
int update();
/**
* Return the timeout used to execute this statement.
*/
int getTimeout();
/**
* Set a timeout on this query.
* <p>
* This will typically result in a call to setQueryTimeout() on a
* preparedStatement. If the timeout occurs an exception will be thrown - this
* will be a SQLException wrapped up in a PersistenceException.
* </p>
*
* @param secs the query timeout limit in seconds. Zero means there is no limit.
*/
UpdateQuery<T> setTimeout(int secs);
}
@@ -31,38 +31,15 @@ import java.util.function.Consumer;
import java.util.function.Function;
/**
* The configuration used for creating a Database.
* <p>
* Used to programmatically construct an Database and optionally register it
* with the DB singleton.
* <p>
* If you just use DB thout this programmatic configuration Ebean will read
* the application.properties file and take the configuration from there. This usually
* includes searching the class path and automatically registering any entity
* classes and listeners etc.
* <pre>{@code
*
* DatabaseConfig config = new DatabaseConfig();
*
* // read the ebean.properties and load
* // those settings into this DatabaseConfig object
* config.loadFromProperties();
*
* // explicitly register the entity beans to avoid classpath scanning
* config.addClass(Customer.class);
* config.addClass(User.class);
*
* Database db = DatabaseFactory.create(config);
*
* }</pre>
* Deprecated migrate to {@link Database#builder()} rather than constructing {@code DatabaseConfig} directly.
*
* <p>
* Note that DatabaseConfigProvider provides a standard Java ServiceLoader mechanism that can
* be used to apply configuration to the DatabaseConfig.
* Note that {@link DatabaseConfigProvider} provides a standard Java ServiceLoader mechanism that can
* be used to apply configuration to the {@link DatabaseBuilder}.
*
* @author emcgreal
* @author rbygrave
* @see DatabaseFactory
* @see Database#builder()
*/
public class DatabaseConfig implements DatabaseBuilder.Settings {
@@ -539,8 +516,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
private SlowQueryListener slowQueryListener;
private ProfilingConfig profilingConfig = new ProfilingConfig();
/**
* The mappingLocations for searching xml mapping.
*/
@@ -560,12 +535,14 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
private Function<String, String> metricNaming = MetricNamingMatch.INSTANCE;
/**
* Construct a Database Configuration for programmatically creating an Database.
* @deprecated migrate to {@link Database#builder()} and configure the returned {@link DatabaseBuilder}.
*/
@Deprecated(forRemoval = true)
public DatabaseConfig() {
}
@Override
@SuppressWarnings("removal")
public Database build() {
return DatabaseFactory.create(this);
}
@@ -1028,17 +1005,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
return this;
}
@Override
public ProfilingConfig getProfilingConfig() {
return profilingConfig;
}
@Override
public DatabaseConfig setProfilingConfig(ProfilingConfig profilingConfig) {
this.profilingConfig = profilingConfig;
return this;
}
@Override
public String getDbSchema() {
return dbSchema;
@@ -2137,7 +2103,6 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
*/
protected void loadSettings(PropertiesWrapper p) {
dbSchema = p.get("dbSchema", dbSchema);
profilingConfig.loadSettings(p, name);
platformConfig.loadSettings(p);
if (platformConfig.isAllQuotedIdentifiers()) {
adjustNamingConventionForAllQuoted();
@@ -3,14 +3,14 @@ package io.ebean.config;
import io.ebean.DatabaseBuilder;
/**
* Provides a ServiceLoader based mechanism to configure a DatabaseConfig.
* Provides a ServiceLoader based mechanism to configure a {@link DatabaseBuilder}.
* <p>
* Provide an implementation and register it via the standard Java ServiceLoader mechanism
* via a file at <code>META-INF/services/io.ebean.config.DatabaseConfigProvider</code>.
* </p>
* <p>
* If you are using a DI container like Spring or Guice you are unlikely to use this but instead use a
* spring specific configuration. When we are not using a DI container we may use this mechanism to
* spring specific configuration. When we are not using a DI container we may use this mechanism to
* explicitly register the entity beans and avoid classpath scanning.
* </p>
* <pre>{@code
@@ -18,7 +18,7 @@ import io.ebean.DatabaseBuilder;
* public class EbeanConfigProvider implements DatabaseConfigProvider {
*
* @Override
* public void apply(DatabaseConfig config) {
* public void apply(DatabaseBuilder config) {
*
* // register the entity bean classes explicitly
* config.addClass(Customer.class);
@@ -32,10 +32,9 @@ import io.ebean.DatabaseBuilder;
public interface DatabaseConfigProvider {
/**
* Apply the configuration to the DatabaseConfig.
* Apply the configuration to the {@link DatabaseBuilder}.
* <p>
* Typically we explicitly register entity bean classes and thus avoid classpath scanning.
* </p>
*/
void apply(DatabaseBuilder config);
}
@@ -1,148 +0,0 @@
package io.ebean.config;
/**
* Configuration for transaction profiling.
*/
public class ProfilingConfig {
/**
* When true transaction profiling is enabled.
*/
private boolean enabled;
/**
* Set true for verbose mode.
*/
private boolean verbose;
/**
* The minimum transaction execution time to be included in profiling.
*/
private long minimumMicros;
/**
* A specific set of profileIds to include in profiling.
*/
private int[] includeProfileIds = {};
/**
* The number of profiles to write per file.
*/
private long profilesPerFile = 1000;
private String directory = "profiling";
/**
* Return true if transaction profiling is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Set to true to enable transaction profiling.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Return true if verbose mode is used.
*/
public boolean isVerbose() {
return verbose;
}
/**
* Set to true to use verbose mode.
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Return the minimum transaction execution to be included in profiling.
*/
public long getMinimumMicros() {
return minimumMicros;
}
/**
* Set the minimum transaction execution to be included in profiling.
*/
public void setMinimumMicros(long minimumMicros) {
this.minimumMicros = minimumMicros;
}
/**
* Return the specific set of profileIds to include in profiling.
* When not set all transactions with profileIds are included.
*/
public int[] getIncludeProfileIds() {
return includeProfileIds;
}
/**
* Set a specific set of profileIds to include in profiling.
* When not set all transactions with profileIds are included.
*/
public void setIncludeProfileIds(int[] includeProfileIds) {
this.includeProfileIds = includeProfileIds;
}
/**
* Return the number of profiles to write to a single file.
*/
public long getProfilesPerFile() {
return profilesPerFile;
}
/**
* Set the number of profiles to write to a single file.
*/
public void setProfilesPerFile(long profilesPerFile) {
this.profilesPerFile = profilesPerFile;
}
/**
* Return the directory profiling files are put into.
*/
public String getDirectory() {
return directory;
}
/**
* Set the directory profiling files are put into.
*/
public void setDirectory(String directory) {
this.directory = directory;
}
/**
* Load setting from properties.
*/
public void loadSettings(PropertiesWrapper p, String name) {
enabled = p.getBoolean("profiling", enabled);
verbose = p.getBoolean("profiling.verbose", verbose);
directory = p.get("profiling.directory", directory);
profilesPerFile = p.getLong("profiling.profilesPerFile", profilesPerFile);
minimumMicros = p.getLong("profiling.minimumMicros", minimumMicros);
String includeIds = p.get("profiling.includeProfileIds");
if (includeIds != null) {
includeProfileIds = parseIds(includeIds);
}
}
private int[] parseIds(String includeIds) {
String[] ids = includeIds.split(",");
int[] vals = new int[ids.length];
for (int i = 0; i < ids.length; i++) {
vals[i] = Integer.parseInt(ids[i]);
}
return vals;
}
}
@@ -10,12 +10,23 @@ public interface MetaInfoManager {
/**
* Return the metrics for the database instance.
* <p>
* This will reset the metrics (reset counters back to zero etc) and
* will only return the non-empty metrics.
* This is equivalent to {@link #collectMetrics(boolean)} with reset set to true.
* It will reset the metrics (reset counters back to zero etc) and will only return
* the non-empty metrics.
* </p>
*/
ServerMetrics collectMetrics();
/**
* Return the metrics for the database instance using the given reset behavior.
* <p>
* When reset is false, count and total values remain cumulative between collections.
* </p>
*/
default ServerMetrics collectMetrics(boolean reset) {
return collectMetrics();
}
/**
* Visit the metrics resetting and collecting/reporting as desired.
*/
@@ -0,0 +1,135 @@
package io.ebean.meta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Canonical "v2" mapping of Ebean's internal flat metric names (e.g.
* {@code orm.Customer.findList}, {@code iud.User.save}, {@code txn.named.X},
* {@code l2.<region>.<op>}) into a metric family name plus a tag string following
* the label-tag convention.
*
* <p>This is the source-of-truth mapping for the v2 metrics JSON form
* ({@link ServerMetricsAsJson#writeV2(Appendable)}). The tag string is a canonical,
* sorted, comma separated list of {@code key:value} pairs, e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}.
*
* <table>
* <caption>Ebean prefix → family name + tags</caption>
* <tr><th>Ebean prefix</th><th>name</th><th>tags</th></tr>
* <tr><td>{@code iud.X}</td><td>{@code ebean.dml}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code orm.X}</td><td>{@code ebean.query}</td><td>{@code kind=orm, type=<bean>, label=X}</td></tr>
* <tr><td>{@code dto.X}</td><td>{@code ebean.query}</td><td>{@code kind=dto, type=<bean>, label=X}</td></tr>
* <tr><td>{@code sql.X}</td><td>{@code ebean.query}</td><td>{@code kind=sql, type=<bean>, label=X}</td></tr>
* <tr><td>{@code txn.named.X} / {@code txn.X}</td><td>{@code ebean.txn}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code l2.<region>.<op>}</td><td>{@code ebean.l2}</td><td>{@code op=<op>, region=<region>}</td></tr>
* <tr><td>(unrecognised)</td><td>{@code ebean.other}</td><td>{@code label=<original name>}</td></tr>
* </table>
*
* <p>The {@code kind} tag is the query category (orm/dto/sql) while the {@code type}
* tag is the queried bean/entity simple name. The {@code type} tag is omitted when
* the bean type is unknown.
*/
final class MetricNamingV2 {
/** Result of a name mapping: family name plus canonical tag string. */
static final class Mapped {
private final String name;
private final String tags;
Mapped(String name, String tags) {
this.name = name;
this.tags = tags;
}
String name() {
return name;
}
String tags() {
return tags;
}
}
private MetricNamingV2() {
}
/**
* Map an Ebean flat metric name (and optional bean type for query metrics) into
* the canonical family name plus tag string.
*/
static Mapped map(String ebeanName, String beanType) {
if (ebeanName == null || ebeanName.isEmpty()) {
return new Mapped("ebean.other", "");
}
int firstDot = ebeanName.indexOf('.');
if (firstDot <= 0) {
return new Mapped("ebean.other", tags("label", ebeanName));
}
String prefix = ebeanName.substring(0, firstDot);
String rest = ebeanName.substring(firstDot + 1);
switch (prefix) {
case "iud":
return new Mapped("ebean.dml", tags("label", rest));
case "orm":
return query("orm", rest, beanType);
case "dto":
return query("dto", rest, beanType);
case "sql":
return query("sql", rest, beanType);
case "txn":
String txnLabel = rest.startsWith("named.") ? rest.substring("named.".length()) : rest;
return new Mapped("ebean.txn", tags("label", txnLabel));
case "l2":
return l2(rest);
default:
return new Mapped("ebean.other", tags("label", ebeanName));
}
}
private static Mapped query(String kind, String label, String beanType) {
if (beanType == null || beanType.isEmpty()) {
return new Mapped("ebean.query", tags("kind", kind, "label", label));
}
return new Mapped("ebean.query", tags("kind", kind, "type", beanType, "label", label));
}
private static Mapped l2(String rest) {
int dot = rest.indexOf('.');
if (dot <= 0) {
return new Mapped("ebean.l2", tags("op", rest));
}
String region = rest.substring(0, dot);
String op = rest.substring(dot + 1);
return new Mapped("ebean.l2", tags("op", op, "region", region));
}
/**
* Build a canonical (sorted) {@code key:value,key2:value2} tag string from the given
* key/value pairs, skipping null/empty values and sanitising the reserved
* delimiter characters from values.
*/
private static String tags(String... keyValues) {
List<String> pairs = new ArrayList<>(keyValues.length / 2);
for (int i = 0; i + 1 < keyValues.length; i += 2) {
String value = keyValues[i + 1];
if (value != null && !value.isEmpty()) {
pairs.add(keyValues[i] + ':' + sanitize(value));
}
}
Collections.sort(pairs);
return String.join(",", pairs);
}
/**
* Replace the reserved tag delimiter characters ({@code ,} and {@code :}) so they
* cannot break the {@code key:value,key2:value2} encoding.
*/
private static String sanitize(String value) {
if (value.indexOf(',') < 0 && value.indexOf(':') < 0) {
return value;
}
return value.replace(',', '_').replace(':', '_');
}
}
@@ -19,6 +19,7 @@ final class MetricsAsJson implements ServerMetricsAsJson {
private Comparator<MetaTimedMetric> sortBy = SortMetric.NAME;
private int listCounter;
private int objKeyCounter;
private boolean v2;
MetricsAsJson(ServerMetrics metrics) {
this.metrics = metrics;
@@ -67,6 +68,13 @@ final class MetricsAsJson implements ServerMetricsAsJson {
collect();
}
@Override
public void writeV2(Appendable buffer) {
this.v2 = true;
this.writer = buffer;
collect();
}
private void collect() {
try {
start();
@@ -151,12 +159,26 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void metricStart(MetaMetric metric) throws IOException {
metricStart(metric, null);
}
private void metricStart(MetaMetric metric, String beanType) throws IOException {
if (listCounter++ > 0) {
writer.append(',').append(newLine);
}
objStart();
key("name");
val(metric.name());
if (v2) {
MetricNamingV2.Mapped mapped = MetricNamingV2.map(metric.name(), beanType);
key("name");
val(mapped.name());
if (!mapped.tags().isEmpty()) {
key("tags");
val(mapped.tags());
}
} else {
key("name");
val(metric.name());
}
}
private void metricEnd() throws IOException {
@@ -180,7 +202,8 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void logQuery(MetaQueryMetric metric) throws IOException {
metricStart(metric);
Class<?> beanType = metric.type();
metricStart(metric, beanType == null ? null : beanType.getSimpleName());
appendTiming(metric);
if (withHash) {
append("hash", metric.hash());
@@ -41,6 +41,17 @@ public interface ServerMetricsAsJson {
*/
void write(Appendable buffer);
/**
* Collect and write metrics as "v2" JSON to the given buffer.
* <p>
* The v2 form uses the canonical label-tag convention: each metric is written with
* a family {@code name} (e.g. {@code ebean.query}, {@code ebean.dml}) plus a
* {@code tags} string of sorted {@code key:value} pairs (e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}) rather than the flat
* prefixed name. Timing, hash, location and sql attributes are unchanged.
*/
void writeV2(Appendable buffer);
/**
* Return the metrics in raw JSON.
*/
@@ -34,6 +34,6 @@ public interface MetricFactory extends BootstrapService {
/**
* Create a Timed metric.
*/
QueryPlanMetric createQueryPlanMetric(Class<?> type, String label, ProfileLocation profileLocation, String sql);
QueryPlanMetric createQueryPlanMetric(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash);
}
@@ -0,0 +1,46 @@
package io.ebean.meta;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MetaInfoManagerTest {
@Test
void collectMetricsBooleanDefaultsToCollectMetrics() {
ServerMetrics metrics = new BasicMetricVisitor();
MetaInfoManager manager = new MetaInfoManager() {
@Override
public ServerMetrics collectMetrics() {
return metrics;
}
@Override
public void visitMetrics(MetricVisitor visitor) {
}
@Override
public BasicMetricVisitor visitBasic() {
return new BasicMetricVisitor();
}
@Override
public void resetAllMetrics() {
}
@Override
public List<MetaQueryPlan> queryPlanInit(QueryPlanInit initRequest) {
return List.of();
}
@Override
public List<MetaQueryPlan> queryPlanCollectNow(QueryPlanRequest request) {
return List.of();
}
};
assertThat(manager.collectMetrics(false)).isSameAs(metrics);
}
}
@@ -0,0 +1,93 @@
package io.ebean.meta;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MetricNamingV2Test {
private MetricNamingV2.Mapped map(String name, String beanType) {
return MetricNamingV2.map(name, beanType);
}
@Test
void orm_withBeanType() {
MetricNamingV2.Mapped m = map("orm.Customer.findList", "Customer");
assertThat(m.name()).isEqualTo("ebean.query");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList,type:Customer");
}
@Test
void orm_withoutBeanType() {
MetricNamingV2.Mapped m = map("orm.Customer.findList", null);
assertThat(m.name()).isEqualTo("ebean.query");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList");
}
@Test
void dto_andSql() {
assertThat(map("dto.CustomerDto.findRecent", "CustomerDto").tags())
.isEqualTo("kind:dto,label:CustomerDto.findRecent,type:CustomerDto");
assertThat(map("sql.query.fooBar", "Customer").tags())
.isEqualTo("kind:sql,label:query.fooBar,type:Customer");
}
@Test
void iud() {
MetricNamingV2.Mapped m = map("iud.User.save", null);
assertThat(m.name()).isEqualTo("ebean.dml");
assertThat(m.tags()).isEqualTo("label:User.save");
}
@Test
void txn_named_and_plain() {
assertThat(map("txn.named.ProcessJob", null).name()).isEqualTo("ebean.txn");
assertThat(map("txn.named.ProcessJob", null).tags()).isEqualTo("label:ProcessJob");
assertThat(map("txn.main", null).tags()).isEqualTo("label:main");
}
@Test
void l2_regionAndOp() {
MetricNamingV2.Mapped m = map("l2.customer.hit", null);
assertThat(m.name()).isEqualTo("ebean.l2");
assertThat(m.tags()).isEqualTo("op:hit,region:customer");
}
@Test
void l2_opOnly() {
assertThat(map("l2.hit", null).tags()).isEqualTo("op:hit");
}
@Test
void unrecognisedPrefix_isOther() {
MetricNamingV2.Mapped m = map("l2n.Customer.hit", null);
assertThat(m.name()).isEqualTo("ebean.other");
assertThat(m.tags()).isEqualTo("label:l2n.Customer.hit");
}
@Test
void noDot_isOther() {
assertThat(map("jvm", null).name()).isEqualTo("ebean.other");
assertThat(map("jvm", null).tags()).isEqualTo("label:jvm");
}
@Test
void nullOrEmpty() {
assertThat(map(null, null).name()).isEqualTo("ebean.other");
assertThat(map(null, null).tags()).isEmpty();
assertThat(map("", null).tags()).isEmpty();
}
@Test
void sanitisesReservedChars() {
MetricNamingV2.Mapped m = map("orm.Customer.weird", "Cust:om,er");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.weird,type:Cust_om_er");
}
@Test
void tagsAreSortedByKey() {
// kind < label < type alphabetically regardless of build order
assertThat(map("orm.X.find", "Bean").tags())
.isEqualTo("kind:orm,label:X.find,type:Bean");
}
}
@@ -0,0 +1,166 @@
package io.ebean.meta;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MetricsAsJsonV2Test {
@Test
void writeV2_usesFamilyNamesAndTags() {
ServerMetrics metrics = new FakeServerMetrics();
StringBuilder sb = new StringBuilder();
new MetricsAsJson(metrics).writeV2(sb);
String json = sb.toString();
assertThat(json).contains("\"db\":\"db1\"");
// query metric -> ebean.query with kind/type/label tags
assertThat(json).contains("\"name\":\"ebean.query\"");
assertThat(json).contains("\"tags\":\"kind:orm,label:Customer.findList,type:Customer\"");
// timed iud metric -> ebean.dml
assertThat(json).contains("\"name\":\"ebean.dml\"");
assertThat(json).contains("\"tags\":\"label:User.save\"");
// count metric (l2n not specially mapped) -> ebean.other
assertThat(json).contains("\"name\":\"ebean.other\"");
assertThat(json).contains("\"tags\":\"label:l2n.Customer.hit\"");
}
@Test
void write_v1_unchanged_usesFlatNames() {
ServerMetrics metrics = new FakeServerMetrics();
StringBuilder sb = new StringBuilder();
new MetricsAsJson(metrics).write(sb);
String json = sb.toString();
assertThat(json).contains("\"name\":\"orm.Customer.findList\"");
assertThat(json).contains("\"name\":\"iud.User.save\"");
assertThat(json).doesNotContain("\"tags\"");
}
static final class FakeServerMetrics implements ServerMetrics {
@Override
public String name() {
return "db1";
}
@Override
public ServerMetricsAsJson asJson() {
return new MetricsAsJson(this);
}
@Override
public List<MetricData> asData() {
return new java.util.ArrayList<>();
}
@Override
public List<MetaTimedMetric> timedMetrics() {
return new java.util.ArrayList<>(List.of(new FakeTimed("iud.User.save")));
}
@Override
public List<MetaQueryMetric> queryMetrics() {
return new java.util.ArrayList<>(List.of(new FakeQuery("orm.Customer.findList", Customer.class)));
}
@Override
public List<MetaCountMetric> countMetrics() {
return new java.util.ArrayList<>(List.of(new FakeCount("l2n.Customer.hit")));
}
}
static class Customer {
}
static class FakeTimed implements MetaTimedMetric {
private final String name;
FakeTimed(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public String location() {
return null;
}
@Override
public long count() {
return 3;
}
@Override
public long total() {
return 30;
}
@Override
public long max() {
return 20;
}
@Override
public long mean() {
return 10;
}
@Override
public boolean initialCollection() {
return false;
}
}
static final class FakeQuery extends FakeTimed implements MetaQueryMetric {
private final Class<?> type;
FakeQuery(String name, Class<?> type) {
super(name);
this.type = type;
}
@Override
public Class<?> type() {
return type;
}
@Override
public String label() {
return null;
}
@Override
public String sql() {
return null;
}
@Override
public String hash() {
return "h1";
}
}
static final class FakeCount implements MetaCountMetric {
private final String name;
FakeCount(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public long count() {
return 5;
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>16.6.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.6.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.6.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.6.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.6.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.6.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.6.0</version>
<version>16.11.1</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>16.6.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.6.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.6.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.6.0</version>
<version>16.11.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.6.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.
*/
@@ -10,18 +10,6 @@ import org.jspecify.annotations.Nullable;
*/
public interface SpiProfileHandler {
/**
* Process the collected transaction profiling information.
* <p>
* Note that profileId and totalMicros are part of the profilingData but passed separately as the handler
* may filter what it processed based on this information (ignore short transactions, only process specific
* profileId transactions etc).
* </p>
*
* @param transactionProfile The transaction profile that has just been collected
*/
void collectTransactionProfile(TransactionProfile transactionProfile);
/**
* Create a profiling stream for this transaction, or return null to not profile this transaction.
* <p>
@@ -345,9 +345,18 @@ public interface SpiQuery<T> extends Query<T>, SpiQueryFetch, TxnProfileEventCod
void fetchProperties(String path, OrmQueryProperties other);
/**
* Set the on a secondary query given the label, relativePath and profile location of the parent query.
* Set the label on a secondary query by extending the parent query's full plan
* name with the relative path and load mode (joined with '.').
* <p>
* The {@code parentName} is the parent query's full plan name (without the
* leading "orm."), so for a root query labelled "custMain" on Customer the
* secondary lazy load of contacts becomes {@code Customer.custMain.contacts.lazy}.
* The profile location of the parent query is also propagated.
*
* @param parentName the parent query's full plan name (no "orm." prefix)
* @param relativePath the path to the loaded property plus the load mode, e.g. {@code contacts.lazy}
*/
void setProfilePath(String label, String relativePath, @Nullable ProfileLocation profileLocation);
void setProfilePath(String parentName, String relativePath, @Nullable ProfileLocation profileLocation);
/**
* Set the query mode.
@@ -27,6 +27,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
protected String bindLog = "";
protected PreparedStatement pstmt;
protected long startNano;
protected Binder binder;
private final ReentrantLock lock = new ReentrantLock();
/**
@@ -144,6 +145,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
}
protected void executeAsSql(Binder binder) throws SQLException {
this.binder = binder;
lock.lock();
try {
query.checkCancelled();
@@ -188,7 +188,10 @@ final class DefaultBeanLoader {
query.setLazyLoadProperty(ebi.lazyLoadProperty());
if (draft) {
query.asDraft();
} else if (mode == SpiQuery.Mode.LAZYLOAD_BEAN && desc.isSoftDelete()) {
} else if (desc.isSoftDelete()
&& (mode == SpiQuery.Mode.LAZYLOAD_BEAN || mode == SpiQuery.Mode.REFRESH_BEAN)) {
// include soft-deleted rows when lazy loading or refreshing so a
// refresh() on a soft-deleted bean can reload it (issue #3641)
query.setIncludeSoftDeletes();
}
if (embeddedOwnerIndex > -1) {
@@ -35,14 +35,17 @@ final class DefaultMetaInfoManager implements MetaInfoManager {
@Override
public ServerMetrics collectMetrics() {
return visitBasic();
return collectMetrics(true);
}
@Override
public ServerMetrics collectMetrics(boolean reset) {
return collectBasic(reset);
}
@Override
public BasicMetricVisitor visitBasic() {
BasicMetricVisitor basic = new BasicMetricVisitor(server.name(), naming);
visitMetrics(basic);
return basic;
return collectBasic(true);
}
@Override
@@ -50,6 +53,12 @@ final class DefaultMetaInfoManager implements MetaInfoManager {
server.visitMetrics(new ResetVisitor());
}
private BasicMetricVisitor collectBasic(boolean reset) {
BasicMetricVisitor basic = new BasicMetricVisitor(server.name(), naming, reset, true, true, true);
visitMetrics(basic);
return basic;
}
/**
* Visitor that resets the statistics but doesn't collect them.
*/
@@ -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() {
@@ -405,12 +405,7 @@ public final class InternalConfiguration {
if (handler != null) {
return plugin(handler);
}
ProfilingConfig profilingConfig = config.getProfilingConfig();
if (!profilingConfig.isEnabled()) {
return new NoopProfileHandler();
}
return plugin(new DefaultProfileHandler(profilingConfig));
return new NoopProfileHandler();
}
/**
@@ -4,7 +4,11 @@ package io.ebeaninternal.server.core;
import io.ebean.RowConsumer;
import io.ebean.RowMapper;
import io.ebean.SqlRow;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.server.query.SqlQueryPlan;
import java.util.List;
import java.util.function.Consumer;
@@ -57,6 +61,23 @@ public interface RelationalQueryEngine {
*/
void collect(String label, long exeMicros);
/**
* Return true if query plan capture is enabled (the master switch). When false
* no SqlQuery plans are created or cached, so labelled queries incur no extra cost.
*/
boolean captureActive();
/**
* Obtain (creating and caching if necessary) the query plan for a labelled
* native SqlQuery, used to capture bind values for database query plan collection.
*/
SqlQueryPlan obtainPlan(String label, String sql, SpiEbeanServer server);
/**
* Initiate query plan bind capture for matching SqlQuery plans.
*/
void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list);
/**
* Visit the metrics.
*/
@@ -1,8 +1,11 @@
package io.ebeaninternal.server.core;
import io.ebean.*;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiSqlQuery;
import io.ebeaninternal.server.bind.DataBindCapture;
import io.ebeaninternal.server.query.SqlQueryPlan;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
@@ -12,6 +15,8 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static java.lang.System.Logger.Level.ERROR;
/**
* Wraps the objects involved in executing a SqlQuery.
*/
@@ -39,9 +44,28 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
@Override
protected void requestComplete() {
String label = query.getLabel();
if (label != null) {
long exeMicros = (System.nanoTime() - startNano) / 1000L;
queryEngine.collect(label, exeMicros);
if (label == null) {
return;
}
long exeMicros = (System.nanoTime() - startNano) / 1000L;
queryEngine.collect(label, exeMicros);
// capture bind values to later collect the database query plan
if (binder != null && queryEngine.captureActive()) {
SqlQueryPlan plan = queryEngine.obtainPlan(label, sql, server);
if (plan.collectFor(exeMicros)) {
captureBindForQueryPlan(plan, exeMicros);
}
}
}
private void captureBindForQueryPlan(SqlQueryPlan plan, long exeMicros) {
final long startNanos = System.nanoTime();
try {
DataBindCapture capture = DataBindCapture.of(server.dataTimeZone());
binder.bind(query.getBindParams(), capture, new StringBuilder());
plan.setBind(capture.bindCapture(), exeMicros, startNanos);
} catch (SQLException e) {
CoreLog.log.log(ERROR, "Error capturing SqlQuery bind values", e);
}
}
@@ -1,7 +1,10 @@
package io.ebeaninternal.server.dto;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -43,6 +46,18 @@ public final class DtoBeanDescriptor<T> {
}
}
/**
* Arm bind capture for matching (native SQL) DTO query plans.
*/
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
for (DtoQueryPlan plan : plans.values()) {
if (plan.supportsPlanCapture() && request.includeHash(plan.hash())) {
plan.queryPlanInit(request.thresholdMicros(plan.hash()));
list.add(plan.createMeta(null, null));
}
}
}
/**
* Return the named RawSql query.
*/
@@ -1,9 +1,12 @@
package io.ebeaninternal.server.dto;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import io.ebeaninternal.server.type.TypeManager;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -51,4 +54,14 @@ public final class DtoBeanManager {
value.visit(visitor);
}
}
/**
* Arm bind capture for matching (native SQL) DTO query plans.
*/
@SuppressWarnings("unchecked")
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
for (DtoBeanDescriptor value : descriptorMap.values()) {
value.queryPlanInit(request, list);
}
}
}
@@ -4,26 +4,56 @@ import io.ebean.ProfileLocation;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.QueryPlanMetric;
import io.ebeaninternal.api.SpiDtoQuery;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQueryBindCapture;
import io.ebeaninternal.api.SpiQueryPlan;
import io.ebeaninternal.server.query.CQueryPlan;
import io.ebeaninternal.server.util.Md5;
/**
* Request to map a resultSet columns for a query into a DTO bean.
*/
public final class DtoMappingRequest {
private final SpiEbeanServer server;
private final Class type;
private final String label;
private final ProfileLocation profileLocation;
private final String sql;
private final boolean relaxedMode;
private final DtoColumn[] columnMeta;
private final String name;
private final String hash;
private final boolean nativeSql;
public DtoMappingRequest(SpiDtoQuery query, String sql, DtoColumn[] columnMeta) {
public DtoMappingRequest(SpiEbeanServer server, SpiDtoQuery<?> query, String sql, DtoColumn[] columnMeta) {
this.server = server;
this.type = query.type();
this.label = query.planLabel();
this.profileLocation = query.profileLocation();
this.sql = sql;
this.relaxedMode = query.isRelaxedMode();
this.columnMeta = columnMeta;
this.nativeSql = query.ormQuery() == null;
this.name = deriveName(type.getSimpleName(), query.explicitLabel(), profileLocation);
String loc = profileLocation == null ? null : profileLocation.location();
this.hash = Md5.hash(sql, name, loc);
}
/**
* Derive the DTO query plan / metric name, mirroring the ORM convention:
* an explicit label is prefixed with the bean type for disambiguation, a
* profile location is used as-is (already a unique, type-independent
* identifier) and an unlabelled query uses just the bean type.
*/
private static String deriveName(String simpleName, String explicitLabel, ProfileLocation profileLocation) {
if (explicitLabel != null) {
return "dto." + CQueryPlan.planLabelWithType(explicitLabel, simpleName);
}
if (profileLocation != null) {
return "dto." + profileLocation.label();
}
return "dto." + simpleName;
}
public DtoColumn[] columnMeta() {
@@ -42,8 +72,41 @@ public final class DtoMappingRequest {
return sql;
}
public Class<?> type() {
return type;
}
public String name() {
return name;
}
public String hash() {
return hash;
}
public ProfileLocation profileLocation() {
return profileLocation;
}
/**
* Return true if this is a native SQL DtoQuery (capturable). ORM-backed DTO
* queries capture their query plan via the underlying ORM query plan instead.
*/
public boolean nativeSql() {
return nativeSql;
}
/**
* Create the bind capture for the given query plan. Returns the NOOP capture
* for ORM-backed DTO queries (captured via the ORM plan) or when query plan
* collection is disabled.
*/
public SpiQueryBindCapture createBindCapture(SpiQueryPlan queryPlan) {
return nativeSql ? server.createQueryBindCapture(queryPlan) : SpiQueryBindCapture.NOOP;
}
public QueryPlanMetric createMetric() {
return MetricFactory.get().createQueryPlanMetric(type, label, profileLocation, sql);
return MetricFactory.get().createQueryPlanMetric(type, name, label, profileLocation, sql, hash);
}
/**
@@ -2,13 +2,15 @@ package io.ebeaninternal.server.dto;
import io.ebean.core.type.DataReader;
import io.ebean.meta.MetricVisitor;
import io.ebeaninternal.api.SpiQueryPlan;
import io.ebeaninternal.server.bind.capture.BindCapture;
import java.sql.SQLException;
/**
* Knows how to read and map rows into a Bean.
*/
public interface DtoQueryPlan {
public interface DtoQueryPlan extends SpiQueryPlan {
/**
* Read the row data and return the DTO bean.
@@ -20,6 +22,22 @@ public interface DtoQueryPlan {
*/
void collect(long exeMicros);
/**
* Return true if the bind values for this (native SQL) query should be
* captured in order to later collect the database query plan.
*/
boolean collectFor(long exeMicros);
/**
* Return true if this plan supports query plan capture (native SQL only).
*/
boolean supportsPlanCapture();
/**
* Set the captured bind values used to later collect the database query plan.
*/
void setBind(BindCapture bindCapture, long exeMicros, long startNanos);
/**
* Visit the metric (if not empty).
*/
@@ -1,18 +1,37 @@
package io.ebeaninternal.server.dto;
import io.ebean.ProfileLocation;
import io.ebean.meta.MetricVisitor;
import io.ebean.metric.QueryPlanMetric;
import io.ebean.metric.TimedMetric;
import io.ebeaninternal.api.SpiDbQueryPlan;
import io.ebeaninternal.api.SpiQueryBindCapture;
import io.ebeaninternal.api.SpiQueryPlan;
import io.ebeaninternal.server.bind.capture.BindCapture;
import io.ebeaninternal.server.query.DQueryPlanOutput;
abstract class DtoQueryPlanBase implements DtoQueryPlan {
abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
private final QueryPlanMetric planMetric;
private final TimedMetric metric;
private final Class<?> beanType;
private final String name;
private final String hash;
private final String sql;
private final ProfileLocation profileLocation;
private final boolean nativeSql;
private final SpiQueryBindCapture bindCapture;
DtoQueryPlanBase(DtoMappingRequest request) {
this.planMetric = request.createMetric();
this.metric = planMetric.metric();
this.beanType = request.type();
this.name = request.name();
this.hash = request.hash();
this.sql = request.sql();
this.profileLocation = request.profileLocation();
this.nativeSql = request.nativeSql();
this.bindCapture = request.createBindCapture(this);
}
@Override
@@ -20,8 +39,58 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan {
metric.add(exeTime);
}
@Override
public boolean collectFor(long exeMicros) {
return bindCapture.collectFor(exeMicros);
}
@Override
public boolean supportsPlanCapture() {
return nativeSql;
}
@Override
public void setBind(BindCapture capture, long exeMicros, long startNanos) {
bindCapture.setBind(capture, exeMicros, startNanos);
}
@Override
public void visit(MetricVisitor visitor) {
planMetric.visit(visitor);
}
@Override
public Class<?> beanType() {
return beanType;
}
@Override
public String name() {
return name;
}
@Override
public String hash() {
return hash;
}
@Override
public String sql() {
return sql;
}
@Override
public ProfileLocation profileLocation() {
return profileLocation;
}
@Override
public void queryPlanInit(long thresholdMicros) {
bindCapture.queryPlanInit(thresholdMicros);
}
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
}
}
@@ -21,6 +21,8 @@ abstract class DLoadBaseContext {
protected final String fullPath;
protected final String path;
protected final String serverName;
final OrmQueryProperties queryProps;
@@ -38,6 +40,7 @@ abstract class DLoadBaseContext {
this.serverName = parent.server().name();
this.desc = desc;
this.queryProps = queryProps;
this.path = path;
this.fullPath = parent.fullPath(path);
this.hitCache = parent.isBeanCacheGet() && desc.isBeanCaching();
this.objectGraphNode = parent.objectGraphNode(path);
@@ -50,15 +53,21 @@ abstract class DLoadBaseContext {
* set onto the secondary query.
*/
void setLabel(SpiQuery<?> query) {
String label = parent.planLabel();
if (label != null) {
query.setProfilePath(label, pathMode(query), parent.profileLocation());
String prefix = parent.namePrefix();
if (prefix != null) {
query.setProfilePath(prefix, pathMode(query), parent.profileLocation());
}
}
/**
* Extend the parent query's full name with this load's immediate path segment
* and load mode. The parent name already carries everything up to this point,
* so only the immediate path (not the root relative fullPath) is appended to
* avoid duplicating intermediate path segments on nested secondary loads.
*/
private String pathMode(SpiQuery<?> query) {
final var loadMode = query.loadMode();
return fullPath == null ? '_' + loadMode : fullPath + "__" + loadMode;
return path == null ? loadMode : path + '.' + loadMode;
}
PersistenceContext persistenceContext() {
@@ -11,6 +11,7 @@ import io.ebeaninternal.server.autotune.ProfilingListener;
import io.ebeaninternal.server.core.OrmQueryRequest;
import io.ebeaninternal.server.deploy.*;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.query.CQueryPlan;
import io.ebeaninternal.server.querydefn.OrmQueryProperties;
import java.sql.Timestamp;
@@ -53,7 +54,7 @@ public final class DLoadContext implements LoadContext {
*/
private final String relativePath;
private final ObjectGraphOrigin origin;
private final String planLabel;
private final String namePrefix;
private final ProfileLocation profileLocation;
private final ProfilingListener profilingListener;
private final Map<String, ObjectGraphNode> nodePathMap = new HashMap<>();
@@ -81,7 +82,7 @@ public final class DLoadContext implements LoadContext {
this.disableReadAudit = false;
this.includeSoftDeletes = false;
this.relativePath = null;
this.planLabel = null;
this.namePrefix = null;
this.profileLocation = null;
this.profilingListener = null;
this.immutableCaches = Collections.emptyMap();
@@ -111,7 +112,7 @@ public final class DLoadContext implements LoadContext {
this.disableLazyLoading = query.isDisableLazyLoading();
this.useBeanCache = query.beanCacheMode();
this.profilingListener = query.profilingListener();
this.planLabel = query.planLabel();
this.namePrefix = deriveNamePrefix(query);
this.profileLocation = query.profileLocation();
this.immutableCaches = query.immutableBeanCaches();
this.secondaryProperties = query.isUnmodifiable() ? new HashSet<>() : null;
@@ -130,10 +131,41 @@ public final class DLoadContext implements LoadContext {
}
/**
* Return the query plan label of the origin query.
* Build the name prefix that secondary (lazy/query) loads extend with their
* path and load mode. This is the parent query's full plan name (without the
* leading "orm.").
* <p>
* For a root query with an explicit {@code setLabel(..)} that is the bean type
* combined with the label (for disambiguation). For a profile location the
* label is already a unique, type-independent identifier so it is used as-is.
* For a parent that is itself a secondary query the plan label already carries
* the full name, so it is used as-is (avoiding a duplicate prefix on nested
* lazy loads).
*/
String planLabel() {
return planLabel;
@Nullable
private String deriveNamePrefix(SpiQuery<?> query) {
final String label = query.planLabel();
if (label == null) {
return null;
}
if (query.loadMode() != null) {
// parent is itself a secondary - label already carries the full name
return label;
}
if (query.label() != null) {
// explicit setLabel - prefix the bean type for disambiguation
return CQueryPlan.planLabelWithType(label, rootDescriptor.simpleName());
}
// profile location - already a unique, type-independent identifier
return label;
}
/**
* Return the parent name prefix that secondary queries extend, or null.
*/
@Nullable
String namePrefix() {
return namePrefix;
}
/**
@@ -116,9 +116,12 @@ public final class BatchedPstmt implements SpiProfileTransactionEvent {
getGeneratedKeys();
}
postExecute();
addTimingMetrics();
list.clear();
transaction.profileEvent(this);
try {
addTimingMetrics();
transaction.profileEvent(this);
} finally {
list.clear();
}
}
private void addTimingMetrics() {
@@ -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() {
@@ -19,9 +19,7 @@ final class DTimeMetricStats implements TimedMetricStats {
this.collected = collected;
this.count = count;
this.total = total;
// collection is racy so sanitize the max value if it has not been set
// this most likely would happen when count = 1 so max = mean
this.max = max != Long.MIN_VALUE ? max : (count < 1 ? 0 : Math.round(total / count));
this.max = max;
}
@Override
@@ -17,7 +17,7 @@ final class DTimedMetric implements TimedMetric {
private final String name;
private final LongAdder count = new LongAdder();
private final LongAdder total = new LongAdder();
private final LongAccumulator max = new LongAccumulator(Math::max, Long.MIN_VALUE);
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
private boolean collected;
private String reportName;
@@ -84,11 +84,8 @@ final class DTimedMetric implements TimedMetric {
*/
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
try {
if (reset) {
return new DTimeMetricStats(name, collected, countSum, total.sumThenReset(), max.getThenReset());
} else {
return new DTimeMetricStats(name, collected, countSum, total.sum(), max.get());
}
final long totalSum = reset ? total.sumThenReset() : total.sum();
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
} finally {
collected = true;
}
@@ -569,7 +569,7 @@ public final class CQuery<T> implements DbReadContext, CancelableQuery, SpiProfi
public void profile() {
transaction()
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId(), query.getGeneratedSql());
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), loadedBeanCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
}
QueryIterator<T> readIterate(int bufferSize, OrmQueryRequest<T> request) {
@@ -321,6 +321,13 @@ final class CQueryBuilder {
return lastFound;
}
static String inlineSqlCommentLabel(String label, ProfileLocation profileLocation, boolean secondary, String simpleName) {
if (label != null) {
return secondary ? label : CQueryPlan.planLabelWithType(label, simpleName);
}
return profileLocation == null ? null : profileLocation.label();
}
private String wrapSelectCount(String sql) {
sql = "select count(*) from ( " + sql + ")";
if (selectCountWithAlias) {
@@ -645,14 +652,10 @@ final class CQueryBuilder {
if (type == SpiQuery.Type.SQ_EX || type == SpiQuery.Type.SQ_EXISTS) {
return "";
}
final var label = query.label();
final var label = CQueryBuilder.inlineSqlCommentLabel(query.label(), query.profileLocation(), query.loadMode() != null, request.descriptor().simpleName());
if (label != null) {
return dbPlatform.inlineSqlComment(label);
}
final var profileLocation = query.profileLocation();
if (profileLocation != null) {
return dbPlatform.inlineSqlComment(profileLocation.label());
}
return "";
}
@@ -165,7 +165,7 @@ final class CQueryFetchSingleAttribute implements SpiProfileTransactionEvent, Ca
public void profile() {
transaction()
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
}
Set<String> dependentTables() {
@@ -88,7 +88,7 @@ public class CQueryPlan implements SpiQueryPlan {
this.profileLocation = query.profileLocation();
this.location = (profileLocation == null) ? null : profileLocation.location();
this.label = query.planLabel();
this.name = deriveName(label, query.type(), request.descriptor().simpleName());
this.name = deriveName(label, query, request.descriptor().simpleName());
this.asOfTableCount = query.getAsOfTableCount();
this.sql = sqlRes.getSql();
this.sqlTree = sqlTree;
@@ -112,7 +112,7 @@ public class CQueryPlan implements SpiQueryPlan {
this.profileLocation = query.profileLocation();
this.location = (profileLocation == null) ? null : profileLocation.location();
this.label = query.planLabel();
this.name = deriveName(label, query.type(), request.descriptor().simpleName());
this.name = deriveName(label, query, request.descriptor().simpleName());
this.planKey = buildPlanKey(sql, logWhereSql);
this.asOfTableCount = 0;
this.sql = sql;
@@ -126,19 +126,58 @@ public class CQueryPlan implements SpiQueryPlan {
this.hash = Md5.hash(sql, name, location);
}
private String deriveName(String label, SpiQuery.Type type, String simpleName) {
private String deriveName(String label, SpiQuery<?> query, String simpleName) {
return deriveName(label, query.loadMode() != null, query.label() != null, query.type().label(), simpleName);
}
/**
* Derive the query plan / metric name.
*
* @param label the query plan label (null for an unlabelled query)
* @param secondary true if this is a secondary query (lazy or query load) where the label
* already carries the parent profile path, so the bean type is not prefixed
* @param explicitLabel true if the label came from an explicit {@code setLabel(..)} (as opposed
* to a profile location). An explicit label is prefixed with the bean type for
* disambiguation; a profile location is already a unique, type-independent
* identifier and is used as-is.
* @param typeLabel the query type label (used only when there is no plan label)
* @param simpleName the bean descriptor simple name
*/
static String deriveName(String label, boolean secondary, boolean explicitLabel, String typeLabel, String simpleName) {
if (label == null) {
return Str.add("orm.", simpleName, ".", type.label());
return Str.add("orm.", simpleName, ".", typeLabel);
}
if (secondary) {
// secondary (lazy/query) load - the label already carries the full parent
// name plus the path and load mode, so use it verbatim
return Str.add("orm.", label);
}
if (explicitLabel) {
return Str.add("orm.", planLabelWithType(label, simpleName));
}
// profile location - already a unique, type-independent identifier
return Str.add("orm.", label);
}
/**
* Combine the bean type simple name with an explicit (non-null) {@code setLabel}.
* <p>
* Used for a root query name and the parent prefix that secondary (lazy/query)
* loads extend, when the name comes from an explicit label. The bean type is
* prefixed for disambiguation, except when the label already starts with the
* bean type (avoiding duplication). Profile locations are not routed through
* here - they are unique identifiers and used as-is.
*/
public static String planLabelWithType(String label, String simpleName) {
int pos = simpleName.indexOf('.');
if (pos > 1) {
// element collection and label
return Str.add("orm.", simpleName.substring(0, pos), "_", label);
return Str.add(simpleName.substring(0, pos), "_", label);
}
if (label.startsWith(simpleName)) {
return Str.add("orm.", label);
return label;
}
return Str.add("orm.", simpleName, "_", label);
return Str.add(simpleName, ".", label);
}
private SpiQueryBindCapture initBindCapture(SpiQuery<?> query) {
@@ -137,7 +137,7 @@ final class CQueryRowCount implements SpiProfileTransactionEvent, CancelableQuer
public void profile() {
transaction()
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
}
Set<String> dependentTables() {
@@ -115,7 +115,7 @@ final class CQueryUpdate implements SpiProfileTransactionEvent, CancelableQuery
public void profile() {
transaction()
.profileStream()
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), query.getGeneratedSql());
.addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId(), queryPlan.hash(), query.getGeneratedSql());
}
@Override
@@ -9,7 +9,7 @@ import java.time.Instant;
/**
* Captured query plan details.
*/
final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private final Class<?> beanType;
private final String label;
@@ -24,7 +24,7 @@ final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private long captureMicros;
private Instant whenCaptured;
DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
this.beanType = beanType;
this.label = label;
this.hash = hash;
@@ -5,9 +5,12 @@ import io.ebean.RowMapper;
import io.ebean.SqlRow;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.ScalarType;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import io.ebean.metric.MetricFactory;
import io.ebean.metric.TimedMetricMap;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.core.RelationalQueryEngine;
import io.ebeaninternal.server.core.RelationalQueryRequest;
@@ -20,6 +23,7 @@ import jakarta.persistence.PersistenceException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -32,17 +36,20 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine
private final String dbTrueValue;
private final boolean binaryOptimizedUUID;
private final boolean autoCommitFalseOnFindIterate;
private final boolean queryPlanCapture;
private final TimedMetricMap timedMetricMap;
private final ConcurrentHashMap<String, SqlQueryPlan> plans = new ConcurrentHashMap<>();
private final int defaultFetchSizeFindEach;
private final int defaultFetchSizeFindList;
public DefaultRelationalQueryEngine(Binder binder, String dbTrueValue, boolean binaryOptimizedUUID,
int defaultFetchSizeFindEach, int defaultFetchSizeFindList,
boolean autoCommitFalseOnFindIterate) {
boolean autoCommitFalseOnFindIterate, boolean queryPlanCapture) {
this.binder = binder;
this.dbTrueValue = dbTrueValue == null ? "true" : dbTrueValue;
this.binaryOptimizedUUID = binaryOptimizedUUID;
this.autoCommitFalseOnFindIterate = autoCommitFalseOnFindIterate;
this.queryPlanCapture = queryPlanCapture;
this.timedMetricMap = MetricFactory.get().createTimedMetricMap("sql.query.");
this.defaultFetchSizeFindEach = defaultFetchSizeFindEach;
this.defaultFetchSizeFindList = defaultFetchSizeFindList;
@@ -53,6 +60,27 @@ public final class DefaultRelationalQueryEngine implements RelationalQueryEngine
timedMetricMap.add(label, exeMicros);
}
@Override
public boolean captureActive() {
return queryPlanCapture;
}
@Override
public SqlQueryPlan obtainPlan(String label, String sql, SpiEbeanServer server) {
return plans.computeIfAbsent(label + ':' + sql,
key -> new SqlQueryPlan(server, "sql.query." + label, sql));
}
@Override
public void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list) {
for (SqlQueryPlan plan : plans.values()) {
if (request.includeHash(plan.hash())) {
plan.queryPlanInit(request.thresholdMicros(plan.hash()));
list.add(plan.createMeta(null, null));
}
}
}
@Override
public void visitMetrics(MetricVisitor visitor) {
timedMetricMap.visit(visitor);
@@ -0,0 +1,82 @@
package io.ebeaninternal.server.query;
import io.ebean.ProfileLocation;
import io.ebeaninternal.api.SpiDbQueryPlan;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQueryBindCapture;
import io.ebeaninternal.api.SpiQueryPlan;
import io.ebeaninternal.server.bind.capture.BindCapture;
import io.ebeaninternal.server.util.Md5;
/**
* Query plan for a native SQL {@code SqlQuery}.
* <p>
* Unlike ORM and DTO query plans there is no associated bean type. The timing
* metric is held separately by the relational query engine (the {@code sql.query.*}
* {@code TimedMetricMap}); this plan only owns the bind capture used to later
* collect the database query plan via EXPLAIN.
*/
public final class SqlQueryPlan implements SpiQueryPlan {
private final String name;
private final String hash;
private final String sql;
private final SpiQueryBindCapture bindCapture;
SqlQueryPlan(SpiEbeanServer server, String name, String sql) {
this.name = name;
this.sql = sql;
this.hash = Md5.hash(sql, name);
this.bindCapture = server.createQueryBindCapture(this);
}
/**
* Return true if the bind values for this query should be captured (based on
* the query execution time exceeding the armed threshold).
*/
public boolean collectFor(long exeMicros) {
return bindCapture.collectFor(exeMicros);
}
/**
* Set the captured bind values used to later collect the database query plan.
*/
public void setBind(BindCapture capture, long exeMicros, long startNanos) {
bindCapture.setBind(capture, exeMicros, startNanos);
}
@Override
public Class<?> beanType() {
return null;
}
@Override
public String name() {
return name;
}
@Override
public String hash() {
return hash;
}
@Override
public String sql() {
return sql;
}
@Override
public ProfileLocation profileLocation() {
return null;
}
@Override
public void queryPlanInit(long thresholdMicros) {
bindCapture.queryPlanInit(thresholdMicros);
}
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
}
}
@@ -273,6 +273,12 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
return null;
}
@Nullable
@Override
public String explicitLabel() {
return label;
}
@Override
public void obtainLocation() {
if (profileLocation != null) {
@@ -255,9 +255,11 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
}
@Override
public final void setProfilePath(String label, String relativePath, @Nullable ProfileLocation profileLocation) {
public final void setProfilePath(String parentName, String relativePath, @Nullable ProfileLocation profileLocation) {
this.profileLocation = profileLocation;
this.label = (profileLocation == null ? label : profileLocation.label()) + '_' + relativePath;
// 'parentName' is the parent query's full plan name (bean type + label, or
// the full name of a parent secondary query) so extend it directly.
this.label = parentName + '.' + relativePath;
}
@Override
@@ -84,4 +84,15 @@ public final class DefaultUpdateQuery<T> implements UpdateQuery<T> {
public int update() {
return query.update();
}
@Override
public int getTimeout() {
return query.timeout();
}
@Override
public UpdateQuery<T> setTimeout(int secs) {
query.setTimeout(secs);
return this;
}
}
@@ -169,7 +169,7 @@ public final class OrmQueryProperties implements Serializable {
}
private static String buildImmutableQueryPlanHashSuffix(FetchConfig fetchConfig) {
return fetchConfig == null ? "" : "/c" + fetchConfig.hashCode();
return fetchConfig == null ? "}" : "/c" + fetchConfig.hashCode() + '}';
}
/**
@@ -468,7 +468,6 @@ public final class OrmQueryProperties implements Serializable {
filterMany.queryPlanHash(builder);
}
builder.append(immutableHashSuffix);
builder.append('}');
}
private static void appendSet(StringBuilder builder, Set<String> values) {
@@ -1,271 +0,0 @@
package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import io.ebean.config.ProfilingConfig;
import io.ebean.plugin.Plugin;
import io.ebean.plugin.SpiServer;
import io.ebean.util.IOUtils;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiProfileHandler;
import org.jspecify.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.System.Logger.Level.*;
import static java.time.temporal.ChronoField.*;
/**
* Default profile handler.
* <p>
* Uses ConcurrentLinkedQueue to minimise contention on threads calling collectTransactionProfile().
* <p>
* Uses a sleep backoff on the single threaded consumer that reads the profiles and writes them to files.
*/
public final class DefaultProfileHandler implements SpiProfileHandler, Plugin {
private static final System.Logger log = CoreLog.internal;
private static final DateTimeFormatter DTF;
static {
DTF = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendValue(YEAR, 4)
.appendValue(MONTH_OF_YEAR, 2)
.appendValue(DAY_OF_MONTH, 2)
.appendLiteral('-')
.appendValue(HOUR_OF_DAY, 2)
.appendValue(MINUTE_OF_HOUR, 2)
.appendValue(SECOND_OF_MINUTE, 2)
.appendLiteral('-')
.appendValue(MILLI_OF_SECOND, 3)
.toFormatter();
}
/**
* Low contention choice.
*/
private final Queue<TransactionProfile> queue = new ConcurrentLinkedQueue<>();
private final ExecutorService executor;
private final ReentrantLock lock = new ReentrantLock();
private final File dir;
private final long minMicros;
private final long profilesPerFile;
private final boolean verbose;
private volatile boolean shutdown;
private long profileCounter;
/**
* Slow down polling of transaction profiling queue.
*/
private int sleepBackoff;
private Writer out;
public DefaultProfileHandler(ProfilingConfig config) {
this.verbose = config.isVerbose();
this.minMicros = config.getMinimumMicros();
this.profilesPerFile = config.getProfilesPerFile();
// dedicated single threaded executor for consuming the
// profiling and writing it to file(s)
this.executor = Executors.newSingleThreadExecutor();
this.dir = new File(config.getDirectory());
if (!dir.exists() && !dir.mkdirs()) {
log.log(ERROR, "failed to mkdirs " + dir.getAbsolutePath());
}
incrementFile();
}
/**
* Low contention adding the transaction profile to the queue.
* Minimise the impact to the normal transaction processing (threads).
*/
@Override
public void collectTransactionProfile(TransactionProfile transactionProfile) {
queue.add(transactionProfile);
}
/**
* Create and return a ProfileStream, or null if location is null (implicit transactions
* are not profiled by the default file-based handler).
*/
@Override
public ProfileStream createProfileStream(@Nullable ProfileLocation location, String label) {
if (location == null) {
return null;
}
return new DefaultProfileStream(location, verbose);
}
private void flushCurrentFile() {
lock.lock();
try {
if (out != null) {
try {
out.close();
out = null;
} catch (IOException e) {
log.log(ERROR, "Failed to flush and close transaction profiling file ", e);
}
}
} finally {
lock.unlock();
}
}
/**
* Move to the next file to write to.
*/
private void incrementFile() {
lock.lock();
try {
flushCurrentFile();
try {
String now = DTF.format(LocalDateTime.now());
File file = new File(dir, "txprofile-" + now + ".tprofile");
out = IOUtils.newWriter(file);
} catch (IOException e) {
log.log(ERROR, "Not expected", e);
}
} finally {
lock.unlock();
}
}
/**
* Main loop for polling the queue and processing profiling messages.
*/
private void collect() {
try {
while (!shutdown) {
TransactionProfile profile = queue.poll();
if (profile == null) {
sleep();
} else if (include(profile)) {
write(profile);
}
}
flushCurrentFile();
} catch (Exception e) {
log.log(WARNING, "Error on collect", e);
}
}
/**
* Write the profile to the current file.
*/
private void write(TransactionProfile profile) {
try {
sleepBackoff = 0;
++profileCounter;
StringBuilder sb = new StringBuilder(80);
// header
sb.append(profile.getStartTime()).append(' ')
.append(profile.getLabel()).append(' ')
.append(profile.getTotalMicros()).append(' ');
// summary
appendSummary(profile, sb);
out.write(sb.toString());
if (verbose) {
out.write(' ');
out.write(profile.getData());
}
out.write('\n');
if (profileCounter % profilesPerFile == 0) {
incrementFile();
log.log(DEBUG, "profiled {0} transactions", profileCounter);
}
} catch (IOException e) {
log.log(WARNING, "Error writing transaction profiling", e);
}
}
private void appendSummary(TransactionProfile profile, StringBuilder sb) {
TransactionProfile.Summary summary = profile.getSummary();
sb.append("z:").append(rate(profile.getTotalMicros(), summary.persistCount + summary.queryCount)).append(' ');
sb.append("p:").append(rate(summary.persistMicros, summary.persistBeans)).append(' ');
sb.append("q:").append(rate(summary.queryMicros, summary.queryCount)).append(' ');
sb.append("qm:").append(summary.queryMax).append(' ');
sb.append("qt:").append(summary.queryMicros).append(' ');
sb.append("qc:").append(summary.queryCount).append(' ');
sb.append("qb:").append(summary.queryBeans).append(' ');
sb.append("pt:").append(summary.persistMicros).append(' ');
sb.append("pc:").append(summary.persistCount).append(' ');
sb.append("pb:").append(summary.persistBeans).append(' ');
sb.append("po:").append(summary.persistOneCount).append(' ');
sb.append("pz:").append(rate(summary.persistBeans, summary.persistCount));
}
private int rate(long micros, long count) {
return count < 1 ? 0 : (int) (micros / count);
}
/**
* Return true if the profile should be included (or false for ignored).
*/
private boolean include(TransactionProfile profile) {
return profile.getTotalMicros() >= minMicros;
}
/**
* Sleep backing off towards 250 millis when there is no activity.
* This seems to be simple and decent for our queue consumer.
*/
private void sleep() {
try {
// backoff sleep when nothing is happening
int sleepFor = Math.min(++sleepBackoff, 250);
Thread.sleep(sleepFor);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void configure(SpiServer server) {
// do nothing
}
@Override
public void online(boolean online) {
if (online) {
executor.submit(this::collect);
}
}
@Override
public void shutdown() {
shutdown = true;
log.log(TRACE, "shutting down");
try {
executor.shutdown();
if (!executor.awaitTermination(4, TimeUnit.SECONDS)) {
log.log(INFO, "Shut down timeout exceeded. Terminating profiling consumer thread.");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.log(WARNING, "Interrupt on shutdown", e);
}
flushCurrentFile();
}
}
@@ -1,94 +0,0 @@
package io.ebeaninternal.server.transaction;
import io.ebean.ProfileLocation;
import org.jspecify.annotations.Nullable;
/**
* Default transaction profiling event collection.
*/
public final class DefaultProfileStream implements ProfileStream {
private final long startNanos;
private final StringBuilder buffer;
private final TransactionProfile profile;
private final TransactionProfile.Summary summary;
DefaultProfileStream(@Nullable ProfileLocation location, boolean verbose) {
this.startNanos = System.nanoTime();
this.profile = new TransactionProfile(System.currentTimeMillis(), location);
this.summary = profile.getSummary();
this.buffer = (verbose) ? new StringBuilder(200) : null;
}
/**
* Return the time offset from the beginning of the transaction.
*/
@Override
public long offset() {
return ((System.nanoTime() - startNanos) / 1_000L);
}
private long exeMicros(long offset) {
return offset() - offset;
}
/**
* Add a query execution event.
*/
@Override
public void addQueryEvent(String event, long offset, String beanName, int beanCount, String queryId, String sql) {
long micros = exeMicros(offset);
summary.addQuery(micros, beanCount);
if (buffer != null) {
add(micros, event, offset, beanName, beanCount, queryId);
}
}
/**
* Add a persist event.
*/
@Override
public void addPersistEvent(String event, long offset, String beanName, int beanCount) {
long micros = exeMicros(offset);
summary.addPersist(micros, beanCount);
if (buffer != null) {
add(micros, event, offset, beanName, beanCount, "");
}
}
/**
* Add the commit/rollback event.
*/
@Override
public void addEvent(String event, long offset) {
long micros = exeMicros(offset);
summary.commitMicros = micros;
if (buffer != null) {
buffer.append(event).append(',');
buffer.append(offset).append(',');
buffer.append(micros).append(';');
}
}
private void add(long micros, String event, long offset, String beanName, int beanCount, String queryId) {
buffer.append(event).append(',');
buffer.append(offset).append(',');
buffer.append(micros).append(',');
buffer.append(beanName).append(',');
buffer.append(beanCount).append(',');
buffer.append(queryId).append(';');
}
/**
* End the transaction profiling.
*/
@Override
public void end(TransactionManager manager, String label) {
profile.setTotalMicros(offset());
if (buffer != null) {
profile.setData(buffer.toString());
}
manager.profileCollect(profile);
}
}
@@ -501,7 +501,7 @@ final class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEve
active = false;
manager.collectMetricReadOnly((System.nanoTime() - startNanos) / 1000L);
if (profileStream != null) {
profileStream.end(manager, null);
profileStream.end(null);
}
}

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