Compare commits

...
59 Commits
Author SHA1 Message Date
Rob Bygrave a7fddf1981 Version 18.0.0 2026-06-22 21:11:45 +12:00
Rob Bygrave f610d03a1d Dependency: Bump ebean-agent to 18.0.0 2026-06-22 21:09:10 +12:00
Rob Bygrave 852f199ffb Merge pull request #3789 from ebean-orm/feature/avaje-json-core
Refactor from Jackson Core to avaje-json-core
2026-06-22 21:02:52 +12:00
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
robin.bygrave 56d33b1f1b Update documentation for the DbJson mapping support 2026-06-22 10:06:34 +12:00
robin.bygrave e9b00cbbd1 Add documentation for the DbJson mapping support 2026-06-19 22:10:54 +12:00
robin.bygrave 230d7a36a7 Simplify @DbJson Map dispatch and support Map<Enum,String>
Replace isMapValueTypeObject + isMapStringString with isBuiltinJsonMap:
 the built-in JSON map handles a String or enum key with a String, Object
 or wildcard value. This also routes Map<Enum,String> to the built-in type
 and fixes non-String/enum keys with Object value (e.g. Map<Integer,Object>)
 being mis-routed to the built-in type (ClassCastException) instead of the
 object mapper.
2026-06-19 21:47:20 +12:00
robin.bygrave 10bf5dbbbd Support Map<Enum,Object> in @DbJson(B) fields
Addresses #3735. Enum-keyed JSON maps previously failed (enum key cast to
 String). Add ScalarTypeJsonMapEnum which converts enum keys via their
 ScalarType (honouring @DbEnumValue) and reuses ScalarTypeJsonMap for all
 storage/platform handling, so it works for VARCHAR/CLOB/BLOB and Postgres
 JSON/JSONB with no per-storage variants.

 - JsonStorage now reports jdbcType(), enabling a shared storageFor(...) reused
   by the plain and enum-key Map types
 - DefaultTypeManager routes Map<Enum,Object> to the new type and calls
   setAccessible on the @DbEnumValue method (supports nested/non-public enums)
 - add TypeReflectHelper.getMapKeyTypeRaw and TestEnumKeyMap
2026-06-19 20:03:27 +12:00
robin.bygrave bffe741642 Simplify @DbJson(B) Map/List/Set type handling via JsonStorage strategy
Collapse the per-storage and per-platform ScalarType subclass explosion for the built-in JSON value types into two orthogonal strategies.

 - Add JsonStorage strategy (VARCHAR / CLOB / BLOB / Postgres) encapsulating
   how the raw JSON string is read from / bound to JDBC. Postgres vs
   non-Postgres is now a single reusable strategy rather than a subclass per
   value type.
 - Add ScalarTypeJsonValue<T> base holding the shared read / bind / L2-cache /
   json plumbing once, plus ScalarTypeJsonCollectionValue<T> adding the
   ScalarTypeArray (DB array column definition) aspect for List/Set.
 - Rewrite ScalarTypeJsonMap, ScalarTypeJsonList and ScalarTypeJsonSet as thin
   types: a typeFor factory selecting a JsonStorage + value marshalling that
   delegates to the avaje-JsonMapper-backed EJson facade.
 - Remove ScalarTypeJsonMapPostgres and ~16 nested storage/platform classes.
2026-06-19 17:27:19 +12:00
robin.bygrave a8189567dd Replace EJson internals with avaje JsonMapper
Reworks the DJsonService (the SpiJsonService SPI behind io.ebean.text.json.EJson) to use avaje JsonMapper instead of the bespoke EJsonReader/EJsonWriter, consolidating all read/write logic into a single JsonAdapter.

Changes

 - New EbeanJsonAdapter — a JsonAdapter<Object> that materializes JSON into plain or modify-aware Map/List/Set, with two shared singletons (PLAIN, MODIFY_AWARE). Preserves existing EJson semantics: - Integral numbers → Long, decimals → BigDecimal
 - Write coverage for String/Integer/Long/Double/Float/BigDecimal/Boolean/Map/Collection with a toString() fallback for other types (UUID, enum, etc.)
 - Modify-aware loads share a single ModifyAwareFlag owner per root, reset to non-dirty once after the load completes
 - DJsonService now builds one JsonMapper + two JsonMapper.Type<Object> and routes parse/write through them. Null serialization is retained via a serializeNulls(true) writer; null/blank-input guards, token-honoring entry points, and parseSet (modify-aware asSet()) are preserved.
 - EJsonReader/EJsonWriter are now unused and should be deleted (couldn't remove them in this environment).

Why

Simplifies Ebean's JSON handling by reusing avaje-json-core's JsonMapper rather than maintaining a parallel reader/writer, while keeping behavior identical.
2026-06-19 16:19:35 +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
robin.bygrave 313fdda857 Refactor from Jackson Core to avaje-json-core 2026-06-18 22:00:33 +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
239 changed files with 6050 additions and 2795 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.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
</parent>
<artifactId>composites</artifactId>
+2
View File
@@ -183,6 +183,8 @@ database.save(customer);
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
+3
View File
@@ -12,8 +12,11 @@ Key guides (fetch and follow when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- 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
+6
View File
@@ -23,12 +23,15 @@ existing Maven project. Complete the steps in order.
| Guide | Description |
|-------|-------------|
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
## Observability
| Guide | Description |
|-------|-------------|
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
## Entity beans
@@ -36,6 +39,7 @@ existing Maven project. Complete the steps in order.
|-------|-------------|
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
## Querying
@@ -149,6 +153,8 @@ Key guides (fetch and follow these when performing the relevant task):
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
@@ -250,6 +250,40 @@ For each future set of entity bean changes:
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
### Protecting hand-edited and non-versioned migrations across regeneration
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
entity model**. It can therefore overwrite content you did not change in the
entity beans, including:
- **hand-edited DDL** in a generated versioned `.sql` file, and
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must **delete the file first**, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
cannot express — it is isolated and now protected from regeneration.
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
produces, review the diff after **every** regeneration and **restore** any
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
re-check that the new migration is listed and filenames match after renaming a
generated file.
> **Run the generator from the module directory.** The output path set via
> `setPathToResources(...)` is resolved relative to the **working directory**.
> Run `GenerateDbMigration` with the working directory set to the module that owns
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
---
## Understanding the output files
+116
View File
@@ -0,0 +1,116 @@
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
## Purpose
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
for everything the built-in support does not handle.
This guide lists exactly which property types are handled built-in and which require
`ebean-jackson-mapper`.
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
> classpath, Ebean fails fast at startup:
>
> ```text
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
> Jackson ObjectMapper not present for <property>
> ```
---
## Quick reference
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|---|:---:|:---:|
| `String` | ✅ | |
| `List<String>`, `List<Long>` | ✅ | |
| `Set<String>`, `Set<Long>` | ✅ | |
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
| `Map<String, String>` | ✅ | |
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
| POJOs, records, or any other type | | ✅ |
---
## Built-in support (no Jackson required)
The built-in path materialises JSON into the *natural* JSON value types
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
for the following declared property types:
- **`String`** — stored as raw JSON text.
- **`List<String>`** and **`List<Long>`**.
- **`Set<String>`** and **`Set<Long>`**.
- **`Map<K, V>`** where:
- the key `K` is `String` or an **enum**, and
- the value `V` is `Object`, `String`, or a wildcard `?`.
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
are all built-in.
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
---
## Everything else → Jackson `ObjectMapper`
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
requires `ebean-jackson-mapper`:
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
- **POJOs, records, and any other custom type.**
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
---
## Adding `ebean-jackson-mapper`
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>${ebean.version}</version>
</dependency>
```
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
registers the mapper-based JSON support automatically.
---
## Notes
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
JSON keys.
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
(with a JSON fallback on platforms without array support) and supports more element types
than built-in `@DbJson` collections.
- The reason typed value/element collections need a real mapper is that the built-in path
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
type-aware mapper.
---
## Choosing
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
object/string maps) to avoid pulling in Jackson.
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
typed-value maps.
+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,91 @@
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
## Purpose
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
avaje-json-core APIs.
Use this when upgrading code that references:
- `com.fasterxml.jackson.core.JsonParser`
- `com.fasterxml.jackson.core.JsonGenerator`
- `com.fasterxml.jackson.core.JsonFactory`
The replacement types are:
- `io.avaje.json.JsonReader`
- `io.avaje.json.JsonWriter`
- `io.avaje.json.stream.JsonStream`
---
## Breaking API changes
| Previous API | New API |
|---|---|
| `JsonParser` | `JsonReader` |
| `JsonGenerator` | `JsonWriter` |
| `JsonFactory` | `JsonStream` |
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
---
## Typical migration rewrites
### Parser and generator signatures
```java
// before
void read(JsonParser parser)
void write(JsonGenerator generator)
// after
void read(JsonReader parser)
void write(JsonWriter generator)
```
### Database configuration
```java
// before
Database.builder().jsonFactory(factory)
// after
Database.builder().jsonStream(stream)
```
### JSON utility calls
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
If your code was calling those APIs with Jackson core types, switch to avaje types.
---
## Dependency and module notes
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
parsing/writing.
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
integrations.
---
## Behavior notes to verify during upgrade
1. Parser token handling is now based on avaje `JsonReader.Token`.
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
be validated in your tests if you previously depended on Jackson token quirks.
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
keep ObjectMapper wiring enabled.
---
## Validation checklist
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
+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"
---
+5 -7
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
</parent>
<name>ebean api</name>
@@ -70,15 +70,13 @@
<optional>true</optional>
</dependency>
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- provided scope for JsonNode support -->
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -1,6 +1,6 @@
package io.ebean;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.json.stream.JsonStream;
import io.ebean.annotation.*;
import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.*;
@@ -360,18 +360,18 @@ public interface DatabaseBuilder {
DatabaseBuilder putServiceObject(Object configObject);
/**
* Set the Jackson JsonFactory to use.
* Set the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
return setJsonFactory(jsonFactory);
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
return setJsonStream(jsonStream);
}
/**
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
*/
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
DatabaseBuilder setJsonStream(JsonStream jsonStream);
/**
* Set the JSON format to use for DateTime types.
@@ -2254,11 +2254,11 @@ public interface DatabaseBuilder {
boolean isAutoLoadModuleInfo();
/**
* Return the Jackson JsonFactory to use.
* Return the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
JsonFactory getJsonFactory();
JsonStream getJsonStream();
/**
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
@@ -60,7 +60,8 @@ public class ClassLoadConfig {
}
public boolean isJacksonCorePresent() {
return isPresent("com.fasterxml.jackson.core.JsonParser");
// Legacy method name retained for compatibility; now checks avaje JSON core.
return isPresent("io.avaje.json.JsonReader");
}
/**
@@ -158,4 +159,3 @@ public class ClassLoadConfig {
}
}
}
@@ -1,7 +1,7 @@
package io.ebean.config;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.config.Config;
import io.avaje.json.stream.JsonStream;
import io.ebean.*;
import io.ebean.annotation.MutationDetection;
import io.ebean.annotation.PersistBatch;
@@ -420,7 +420,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
* The default PersistenceContextScope used if one is not explicitly set on a query.
*/
private PersistenceContextScope persistenceContextScope = PersistenceContextScope.TRANSACTION;
private JsonFactory jsonFactory;
private JsonStream jsonStream;
private boolean localTimeWithNanos;
private boolean durationWithNanos;
private int maxCallStack = 5;
@@ -631,13 +631,13 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
}
@Override
public JsonFactory getJsonFactory() {
return jsonFactory;
public JsonStream getJsonStream() {
return jsonStream;
}
@Override
public DatabaseConfig setJsonFactory(JsonFactory jsonFactory) {
this.jsonFactory = jsonFactory;
public DatabaseConfig setJsonStream(JsonStream jsonStream) {
this.jsonStream = jsonStream;
return this;
}
@@ -0,0 +1,135 @@
package io.ebean.meta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Canonical "v2" mapping of Ebean's internal flat metric names (e.g.
* {@code orm.Customer.findList}, {@code iud.User.save}, {@code txn.named.X},
* {@code l2.<region>.<op>}) into a metric family name plus a tag string following
* the label-tag convention.
*
* <p>This is the source-of-truth mapping for the v2 metrics JSON form
* ({@link ServerMetricsAsJson#writeV2(Appendable)}). The tag string is a canonical,
* sorted, comma separated list of {@code key:value} pairs, e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}.
*
* <table>
* <caption>Ebean prefix → family name + tags</caption>
* <tr><th>Ebean prefix</th><th>name</th><th>tags</th></tr>
* <tr><td>{@code iud.X}</td><td>{@code ebean.dml}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code orm.X}</td><td>{@code ebean.query}</td><td>{@code kind=orm, type=<bean>, label=X}</td></tr>
* <tr><td>{@code dto.X}</td><td>{@code ebean.query}</td><td>{@code kind=dto, type=<bean>, label=X}</td></tr>
* <tr><td>{@code sql.X}</td><td>{@code ebean.query}</td><td>{@code kind=sql, type=<bean>, label=X}</td></tr>
* <tr><td>{@code txn.named.X} / {@code txn.X}</td><td>{@code ebean.txn}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code l2.<region>.<op>}</td><td>{@code ebean.l2}</td><td>{@code op=<op>, region=<region>}</td></tr>
* <tr><td>(unrecognised)</td><td>{@code ebean.other}</td><td>{@code label=<original name>}</td></tr>
* </table>
*
* <p>The {@code kind} tag is the query category (orm/dto/sql) while the {@code type}
* tag is the queried bean/entity simple name. The {@code type} tag is omitted when
* the bean type is unknown.
*/
final class MetricNamingV2 {
/** Result of a name mapping: family name plus canonical tag string. */
static final class Mapped {
private final String name;
private final String tags;
Mapped(String name, String tags) {
this.name = name;
this.tags = tags;
}
String name() {
return name;
}
String tags() {
return tags;
}
}
private MetricNamingV2() {
}
/**
* Map an Ebean flat metric name (and optional bean type for query metrics) into
* the canonical family name plus tag string.
*/
static Mapped map(String ebeanName, String beanType) {
if (ebeanName == null || ebeanName.isEmpty()) {
return new Mapped("ebean.other", "");
}
int firstDot = ebeanName.indexOf('.');
if (firstDot <= 0) {
return new Mapped("ebean.other", tags("label", ebeanName));
}
String prefix = ebeanName.substring(0, firstDot);
String rest = ebeanName.substring(firstDot + 1);
switch (prefix) {
case "iud":
return new Mapped("ebean.dml", tags("label", rest));
case "orm":
return query("orm", rest, beanType);
case "dto":
return query("dto", rest, beanType);
case "sql":
return query("sql", rest, beanType);
case "txn":
String txnLabel = rest.startsWith("named.") ? rest.substring("named.".length()) : rest;
return new Mapped("ebean.txn", tags("label", txnLabel));
case "l2":
return l2(rest);
default:
return new Mapped("ebean.other", tags("label", ebeanName));
}
}
private static Mapped query(String kind, String label, String beanType) {
if (beanType == null || beanType.isEmpty()) {
return new Mapped("ebean.query", tags("kind", kind, "label", label));
}
return new Mapped("ebean.query", tags("kind", kind, "type", beanType, "label", label));
}
private static Mapped l2(String rest) {
int dot = rest.indexOf('.');
if (dot <= 0) {
return new Mapped("ebean.l2", tags("op", rest));
}
String region = rest.substring(0, dot);
String op = rest.substring(dot + 1);
return new Mapped("ebean.l2", tags("op", op, "region", region));
}
/**
* Build a canonical (sorted) {@code key:value,key2:value2} tag string from the given
* key/value pairs, skipping null/empty values and sanitising the reserved
* delimiter characters from values.
*/
private static String tags(String... keyValues) {
List<String> pairs = new ArrayList<>(keyValues.length / 2);
for (int i = 0; i + 1 < keyValues.length; i += 2) {
String value = keyValues[i + 1];
if (value != null && !value.isEmpty()) {
pairs.add(keyValues[i] + ':' + sanitize(value));
}
}
Collections.sort(pairs);
return String.join(",", pairs);
}
/**
* Replace the reserved tag delimiter characters ({@code ,} and {@code :}) so they
* cannot break the {@code key:value,key2:value2} encoding.
*/
private static String sanitize(String value) {
if (value.indexOf(',') < 0 && value.indexOf(':') < 0) {
return value;
}
return value.replace(',', '_').replace(':', '_');
}
}
@@ -19,6 +19,7 @@ final class MetricsAsJson implements ServerMetricsAsJson {
private Comparator<MetaTimedMetric> sortBy = SortMetric.NAME;
private int listCounter;
private int objKeyCounter;
private boolean v2;
MetricsAsJson(ServerMetrics metrics) {
this.metrics = metrics;
@@ -67,6 +68,13 @@ final class MetricsAsJson implements ServerMetricsAsJson {
collect();
}
@Override
public void writeV2(Appendable buffer) {
this.v2 = true;
this.writer = buffer;
collect();
}
private void collect() {
try {
start();
@@ -151,12 +159,26 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void metricStart(MetaMetric metric) throws IOException {
metricStart(metric, null);
}
private void metricStart(MetaMetric metric, String beanType) throws IOException {
if (listCounter++ > 0) {
writer.append(',').append(newLine);
}
objStart();
key("name");
val(metric.name());
if (v2) {
MetricNamingV2.Mapped mapped = MetricNamingV2.map(metric.name(), beanType);
key("name");
val(mapped.name());
if (!mapped.tags().isEmpty()) {
key("tags");
val(mapped.tags());
}
} else {
key("name");
val(metric.name());
}
}
private void metricEnd() throws IOException {
@@ -180,7 +202,8 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void logQuery(MetaQueryMetric metric) throws IOException {
metricStart(metric);
Class<?> beanType = metric.type();
metricStart(metric, beanType == null ? null : beanType.getSimpleName());
appendTiming(metric);
if (withHash) {
append("hash", metric.hash());
@@ -41,6 +41,17 @@ public interface ServerMetricsAsJson {
*/
void write(Appendable buffer);
/**
* Collect and write metrics as "v2" JSON to the given buffer.
* <p>
* The v2 form uses the canonical label-tag convention: each metric is written with
* a family {@code name} (e.g. {@code ebean.query}, {@code ebean.dml}) plus a
* {@code tags} string of sorted {@code key:value} pairs (e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}) rather than the flat
* prefixed name. Timing, hash, location and sql attributes are unchanged.
*/
void writeV2(Appendable buffer);
/**
* Return the metrics in raw JSON.
*/
@@ -34,6 +34,6 @@ public interface MetricFactory extends BootstrapService {
/**
* Create a Timed metric.
*/
QueryPlanMetric createQueryPlanMetric(Class<?> type, String label, ProfileLocation profileLocation, String sql);
QueryPlanMetric createQueryPlanMetric(Class<?> type, String name, String label, ProfileLocation profileLocation, String sql, String hash);
}
@@ -1,8 +1,8 @@
package io.ebean.service;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import java.io.IOException;
import java.io.Reader;
@@ -32,12 +32,12 @@ public interface SpiJsonService extends BootstrapService {
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
void write(Object object, JsonGenerator jsonGenerator) throws IOException;
void write(Object object, JsonWriter jsonGenerator) throws IOException;
/**
* Write the collection as json array to the jsonGenerator.
*/
void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException;
void writeCollection(Collection<Object> collection, JsonWriter jsonGenerator) throws IOException;
/**
* Parse the json and return as a Map additionally specifying if the returned map should
@@ -61,17 +61,17 @@ public interface SpiJsonService extends BootstrapService {
Map<String, Object> parseObject(Reader reader) throws IOException;
/**
* Parse the json and return as a Map taking a JsonParser.
* Parse the json and return as a Map taking a JsonReader.
*/
Map<String, Object> parseObject(JsonParser parser) throws IOException;
Map<String, Object> parseObject(JsonReader parser) throws IOException;
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
* Parse the json and return as a Map taking a JsonReader and a starting token.
* <p>
* Used when the first token is checked to see if the value is null prior to calling this.
* </p>
*/
Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException;
Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException;
/**
* Parse the json and return as a modify aware List.
@@ -89,14 +89,14 @@ public interface SpiJsonService extends BootstrapService {
List<Object> parseList(Reader reader) throws IOException;
/**
* Parse the json and return as a List taking a JsonParser.
* Parse the json and return as a List taking a JsonReader.
*/
List<Object> parseList(JsonParser parser) throws IOException;
List<Object> parseList(JsonReader parser) throws IOException;
/**
* Parse the json returning as a List taking into account the current token.
*/
<T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException;
<T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException;
/**
* Parse the json and return as a List or Map.
@@ -111,7 +111,7 @@ public interface SpiJsonService extends BootstrapService {
/**
* Parse the json and return as a List or Map.
*/
Object parse(JsonParser parser) throws IOException;
Object parse(JsonReader parser) throws IOException;
/**
* Parse the json returning a Set that might be modify aware.
@@ -121,5 +121,5 @@ public interface SpiJsonService extends BootstrapService {
/**
* Parse the json returning as a Set taking into account the current token.
*/
<T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException;
<T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException;
}
@@ -1,8 +1,7 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.XBootstrapService;
import io.ebean.service.SpiJsonService;
@@ -38,14 +37,14 @@ public class EJson {
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
public static void write(Object object, JsonGenerator jsonGenerator) throws IOException {
public static void write(Object object, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
plugin.write(object, jsonGenerator);
}
/**
* Write the collection as json array to the jsonGenerator.
*/
public static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
public static void writeCollection(Collection<Object> collection, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
plugin.writeCollection(collection, jsonGenerator);
}
@@ -79,19 +78,19 @@ public class EJson {
}
/**
* Parse the json and return as a Map taking a JsonParser.
* Parse the json and return as a Map taking a JsonReader.
*/
public static Map<String, Object> parseObject(JsonParser parser) throws IOException {
public static Map<String, Object> parseObject(JsonReader parser) throws IOException {
return plugin.parseObject(parser);
}
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
* Parse the json and return as a Map taking a JsonReader and a starting token.
* <p>
* Used when the first token is checked to see if the value is null prior to calling this.
* </p>
*/
public static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
public static Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return plugin.parseObject(parser, token);
}
@@ -117,16 +116,16 @@ public class EJson {
}
/**
* Parse the json and return as a List taking a JsonParser.
* Parse the json and return as a List taking a JsonReader.
*/
public static List<Object> parseList(JsonParser parser) throws IOException {
public static List<Object> parseList(JsonReader parser) throws IOException {
return plugin.parseList(parser);
}
/**
* Parse the json returning as a List taking into account the current token.
*/
public static <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
public static <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
return plugin.parseList(parser, currentToken);
}
@@ -147,7 +146,7 @@ public class EJson {
/**
* Parse the json and return as a List or Map.
*/
public static Object parse(JsonParser parser) throws IOException {
public static Object parse(JsonReader parser) throws IOException {
return plugin.parse(parser);
}
@@ -161,7 +160,7 @@ public class EJson {
/**
* Parse the json returning as a Set taking into account the current token.
*/
public static <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
public static <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
return plugin.parseSet(parser, currentToken);
}
}
@@ -1,6 +1,6 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.PersistenceContext;
/**
@@ -25,9 +25,9 @@ public interface JsonBeanReader<T> {
}
/**
* Create a new reader taking the context from the existing one but using a new JsonParser.
* Create a new reader taking the context from the existing one but using a new JsonReader.
*/
JsonBeanReader<T> forJson(JsonParser moreJson);
JsonBeanReader<T> forJson(JsonReader moreJson);
/**
* Add a bean explicitly to the persistence context.
@@ -1,7 +1,6 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.FetchPath;
import io.ebean.plugin.BeanType;
@@ -49,14 +48,14 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> T toBean(Class<T> cls, JsonParser parser) throws JsonIOException;
<T> T toBean(Class<T> cls, JsonReader parser) throws JsonIOException;
/**
* Convert json parser input into a Bean of a specific type additionally using JsonReadOptions..
*
* @throws JsonIOException When IOException occurs
*/
<T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> T toBean(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Read json parser input into a given Bean. <br>
@@ -65,19 +64,19 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> void toBean(T target, JsonParser parser) throws JsonIOException;
<T> void toBean(T target, JsonReader parser) throws JsonIOException;
/**
* Read json parser input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details modified.
* See {@link #toBean(Class, JsonReader)} for details modified.
*
* @throws JsonIOException When IOException occurs
*/
<T> void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Read json reader input into a given Bean.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -85,7 +84,7 @@ public interface JsonContext {
/**
* Read json reader input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details modified.
* See {@link #toBean(Class, JsonReader)} for details modified.
*
* @throws JsonIOException When IOException occurs
*/
@@ -93,7 +92,7 @@ public interface JsonContext {
/**
* Read json string input into a given Bean.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -101,7 +100,7 @@ public interface JsonContext {
/**
* Read json string input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -113,7 +112,7 @@ public interface JsonContext {
* Note that JsonOption provides an option for setting a persistence context and also enabling further lazy loading. Further lazy
* loading requires a persistence context so if that is set on then a persistence context is created if there is not one set.
*/
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Create and return a new bean reading for the bean type given the JSON options and source.
@@ -122,7 +121,7 @@ public interface JsonContext {
* further lazy loading. Further lazy loading requires a persistence context so if that is set
* on then a persistence context is created if there is not one set.
*/
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Convert json string input into a list of beans of a specific type.
@@ -157,14 +156,14 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> List<T> toList(Class<T> cls, JsonParser json) throws JsonIOException;
<T> List<T> toList(Class<T> cls, JsonReader json) throws JsonIOException;
/**
* Convert json parser input into a list of beans of a specific type additionally using JsonReadOptions.
*
* @throws JsonIOException When IOException occurs
*/
<T> List<T> toList(Class<T> cls, JsonParser json, JsonReadOptions options) throws JsonIOException;
<T> List<T> toList(Class<T> cls, JsonReader json, JsonReadOptions options) throws JsonIOException;
/**
* Use the genericType to determine if this should be converted into a List or
@@ -188,7 +187,7 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException;
Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException;
/**
* Return the bean or collection as JSON string.
@@ -212,11 +211,11 @@ public interface JsonContext {
void toJson(Object value, Writer writer) throws JsonIOException;
/**
* Write the bean or collection to the JsonGenerator.
* Write the bean or collection to the JsonWriter.
*
* @throws JsonIOException When IOException occurs
*/
void toJson(Object value, JsonGenerator generator) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator) throws JsonIOException;
/**
* Return the bean or collection as JSON string using FetchPath.
@@ -231,15 +230,15 @@ public interface JsonContext {
void toJson(Object value, Writer writer, FetchPath fetchPath) throws JsonIOException;
/**
* Write the bean or collection to the JsonGenerator using the FetchPath.
* Write the bean or collection to the JsonWriter using the FetchPath.
*/
void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator, FetchPath fetchPath) throws JsonIOException;
/**
* Deprecated in favour of using PathProperties by itself.
* Write json to the JsonGenerator using the JsonWriteOptions.
* Write json to the JsonWriter using the JsonWriteOptions.
*/
void toJson(Object value, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator, JsonWriteOptions options) throws JsonIOException;
/**
* Deprecated in favour of using PathProperties by itself.
@@ -264,27 +263,27 @@ public interface JsonContext {
boolean isSupportedType(Type genericType);
/**
* Create and return a new JsonGenerator for the given writer.
* Create and return a new JsonWriter for the given writer.
*
* @throws JsonIOException When IOException occurs
*/
JsonGenerator createGenerator(Writer writer) throws JsonIOException;
io.avaje.json.JsonWriter createGenerator(Writer writer) throws JsonIOException;
/**
* Create and return a new JsonParser for the given reader.
* Create and return a new JsonReader for the given reader.
*
* @throws JsonIOException When IOException occurs
*/
JsonParser createParser(Reader reader) throws JsonIOException;
JsonReader createParser(Reader reader) throws JsonIOException;
/**
* Write a scalar types known to Ebean to Jackson.
* Write scalar types known to Ebean to JsonWriter.
* <p>
* Ebean has built in support for java8 and Joda types as well as the other
* standard JDK types like URI, URL, UUID etc. This is a fast simple way to
* write any of those types to Jackson.
* write any of those types.
* </p>
*/
void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException;
void writeScalar(io.avaje.json.JsonWriter generator, Object scalarValue) throws IOException;
}
@@ -1,19 +1,17 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import java.io.InputStream;
import java.math.BigDecimal;
/**
* Wraps an underlying JsonGenerator taking into account null suppression and exposing isIncludeEmpty() etc.
* Wraps an underlying JsonWriter taking into account null suppression and exposing isIncludeEmpty() etc.
*/
public interface JsonWriter {
/**
* Return the Jackson core JsonGenerator.
* Return the underlying JsonWriter.
*/
JsonGenerator gen();
io.avaje.json.JsonWriter gen();
/**
* Return true if null values should be included in JSON output.
+1 -1
View File
@@ -8,6 +8,7 @@ module io.ebean.api {
requires transitive java.sql;
requires transitive io.avaje.config;
requires transitive io.avaje.json;
requires transitive org.jspecify;
requires transitive jakarta.persistence.api;
requires transitive io.ebean.annotation;
@@ -16,7 +17,6 @@ module io.ebean.api {
requires static org.slf4j;
requires static io.ebean.types;
requires static com.fasterxml.jackson.core;
requires static com.fasterxml.jackson.databind;
exports io.ebean;
@@ -0,0 +1,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.8.0</version>
<version>18.0.0</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.8.0</version>
<version>18.0.0</version>
</parent>
<name>ebean bom</name>
@@ -89,25 +89,25 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -125,13 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
</dependencies>
+5 -7
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</parent>
<artifactId>ebean-core-json</artifactId>
<name>ebean-core-json</name>
@@ -16,15 +16,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
</dependencies>
@@ -1,190 +1,170 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.mapper.JsonMapper;
import io.avaje.json.stream.JsonStream;
import io.ebean.service.SpiJsonService;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility that converts between JSON content and simple java Maps/Lists.
* <p>
* Backed by avaje {@link JsonMapper} using {@link EbeanJsonAdapter} which
* preserves Ebean's modify-aware collection and number semantics.
*/
public final class DJsonService implements SpiJsonService {
/**
* Write the nested Map/List as json.
*/
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private static final JsonMapper MAPPER = JsonMapper.builder().jsonStream(JSON_STREAM).build();
private static final JsonMapper.Type<Object> PLAIN = MAPPER.type(EbeanJsonAdapter.PLAIN);
private static final JsonMapper.Type<Object> MODIFY_AWARE = MAPPER.type(EbeanJsonAdapter.MODIFY_AWARE);
private static JsonMapper.Type<Object> type(boolean modifyAware) {
return modifyAware ? MODIFY_AWARE : PLAIN;
}
private static boolean blank(String content) {
return content == null || content.trim().isEmpty();
}
private static String readAll(Reader reader) throws IOException {
StringBuilder builder = new StringBuilder();
char[] buffer = new char[2048];
int len;
while ((len = reader.read(buffer)) != -1) {
builder.append(buffer, 0, len);
}
return builder.toString();
}
@Override
public String write(Object object) throws IOException {
return EJsonWriter.write(object);
StringWriter writer = new StringWriter();
write(object, writer);
return writer.toString();
}
/**
* Write the nested Map/List as json to the writer.
*/
@Override
public void write(Object object, Writer writer) throws IOException {
EJsonWriter.write(object, writer);
JsonWriter jsonWriter = JSON_STREAM.writer(writer);
jsonWriter.serializeNulls(true);
PLAIN.toJson(object, jsonWriter);
jsonWriter.flush();
}
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
@Override
public void write(Object object, JsonGenerator jsonGenerator) throws IOException {
EJsonWriter.write(object, jsonGenerator);
public void write(Object object, JsonWriter jsonWriter) throws IOException {
PLAIN.toJson(object, jsonWriter);
}
/**
* Write the collection as json array to the jsonGenerator.
*/
@Override
public void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
EJsonWriter.writeCollection(collection, jsonGenerator);
public void writeCollection(Collection<Object> collection, JsonWriter jsonWriter) throws IOException {
EbeanJsonAdapter.writeCollection(jsonWriter, collection);
}
/**
* Parse the json and return as a Map additionally specifying if the returned map should be modify
* aware meaning that it can detect when it has been modified.
*/
@Override
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(json, modifyAware);
}
/**
* Parse the json and return as a Map.
*/
@Override
public Map<String, Object> parseObject(String json) throws IOException {
return EJsonReader.parseObject(json);
}
/**
* Parse the json and return as a Map taking a reader.
*/
@Override
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(reader, modifyAware);
}
/**
* Parse the json and return as a Map taking a reader.
*/
@Override
public Map<String, Object> parseObject(Reader reader) throws IOException {
return EJsonReader.parseObject(reader);
}
/**
* Parse the json and return as a Map taking a JsonParser.
*/
@Override
public Map<String, Object> parseObject(JsonParser parser) throws IOException {
return EJsonReader.parseObject(parser);
}
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
*
* <p>Used when the first token is checked to see if the value is null prior to calling this.
*/
@Override
public Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
return EJsonReader.parseObject(parser, token);
}
/**
* Parse the json and return as a modify aware List.
*/
@Override
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseList(json, modifyAware);
}
/**
* Parse the json and return as a List.
*/
@Override
public List<Object> parseList(String json) throws IOException {
return EJsonReader.parseList(json);
}
/**
* Parse the json and return as a List taking a Reader.
*/
@Override
public List<Object> parseList(Reader reader) throws IOException {
return EJsonReader.parseList(reader);
}
/**
* Parse the json and return as a List taking a JsonParser.
*/
@Override
public List<Object> parseList(JsonParser parser) throws IOException {
return EJsonReader.parseList(parser, false);
}
/**
* Parse the json returning as a List taking into account the current token.
*/
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
return (List<T>) EJsonReader.parse(parser, currentToken, false);
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return blank(json) ? null : (Map<String, Object>) type(modifyAware).fromJson(json);
}
@Override
public Map<String, Object> parseObject(String json) throws IOException {
return parseObject(json, false);
}
@Override
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return parseObject(readAll(reader), modifyAware);
}
@Override
public Map<String, Object> parseObject(Reader reader) throws IOException {
return parseObject(reader, false);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser) throws IOException {
return (Map<String, Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return (Map<String, Object>) EbeanJsonAdapter.read(parser, token, false);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return blank(json) ? null : (List<T>) type(modifyAware).fromJson(json);
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(String json) throws IOException {
return (List<Object>) parseList(json, false);
}
@Override
public List<Object> parseList(Reader reader) throws IOException {
return parseList(readAll(reader));
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(JsonReader parser) throws IOException {
return (List<Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
return (List<T>) EbeanJsonAdapter.read(parser, currentToken, false);
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(String json) throws IOException {
return EJsonReader.parse(json);
return blank(json) ? null : PLAIN.fromJson(json);
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(Reader reader) throws IOException {
return EJsonReader.parse(reader);
return parse(readAll(reader));
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(JsonParser parser) throws IOException {
return EJsonReader.parse(parser);
public Object parse(JsonReader parser) throws IOException {
return PLAIN.fromJson(parser);
}
/**
* Parse the json returning a Set that might be modify aware.
*/
@Override
public <T> Set<T> parseSet(String json, boolean modifyAware) throws IOException {
List<T> list = parseList(json, modifyAware);
if (list == null) {
return null;
}
if (modifyAware) {
return ((ModifyAwareList<T>) list).asSet();
} else {
return new LinkedHashSet<>(list);
}
return new LinkedHashSet<>(list);
}
/**
* Parse the json returning as a Set taking into account the current token.
*/
@Override
public <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
public <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
return new LinkedHashSet<>(parseList(parser, currentToken));
}
}
@@ -1,356 +0,0 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.ebean.ModifyAwareType;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.*;
final class EJsonReader {
static final JsonFactory json = new JsonFactory();
private final JsonParser parser;
private final boolean modifyAware;
private final ModifyAwareFlag modifyAwareOwner;
private int depth;
private Stack stack;
private Context currentContext;
EJsonReader(JsonParser parser, boolean modifyAware) {
this.parser = parser;
this.modifyAware = modifyAware;
this.modifyAwareOwner = modifyAware ? new ModifyAwareFlag() : null;
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json) throws IOException {
return (Map<String, Object>) parse(json);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader) throws IOException {
return (Map<String, Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(reader, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser) throws IOException {
return (Map<String, Object>) parse(parser);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
return (Map<String, Object>) parse(parser, token, false);
}
@SuppressWarnings("unchecked")
static <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return (List<T>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(String json) throws IOException {
return (List<Object>) parse(json);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(Reader reader) throws IOException {
return (List<Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(JsonParser parser, boolean modifyAware) throws IOException {
return (List<Object>) parse(parser, modifyAware);
}
static Object parse(String json) throws IOException {
if (json == null) {
return null;
}
return parse(new StringReader(json));
}
static Object parse(String json, boolean modifyAware) throws IOException {
if (json == null) {
return null;
}
return parse(new StringReader(json), modifyAware);
}
static Object parse(Reader reader) throws IOException {
return parse(json.createParser(reader));
}
static Object parse(Reader reader, boolean modifyAware) throws IOException {
return parse(json.createParser(reader), modifyAware);
}
static Object parse(JsonParser parser) throws IOException {
return parse(parser, null, false);
}
static Object parse(JsonParser parser, boolean modifyAware) throws IOException {
return parse(parser, null, modifyAware);
}
static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException {
return new EJsonReader(parser, modifyAware).parseJson(token);
}
private void startArray() {
depth++;
stack.push(currentContext);
currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext();
}
private void startObject() {
depth++;
stack.push(currentContext);
currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext();
}
private void endArray() {
end();
}
private void endObject() {
end();
}
private void end() {
depth--;
if (!stack.isEmpty()) {
currentContext = stack.pop(currentContext);
}
if (modifyAwareOwner != null) {
modifyAwareOwner.setMarkedDirty(false);
}
}
private void setValue(Object value) {
currentContext.setValue(value);
}
private void setValueNull() {
currentContext.setValueNull();
}
private Object parseJson(JsonToken token) throws IOException {
if (token == null) {
token = parser.nextToken();
// if it is a simple value just return it
switch (token) {
case VALUE_NULL:
return null;
case VALUE_FALSE:
return Boolean.FALSE;
case VALUE_TRUE:
return Boolean.TRUE;
case VALUE_STRING:
return parser.getText();
case VALUE_NUMBER_INT:
return parser.getLongValue();
case VALUE_NUMBER_FLOAT:
return parser.getDecimalValue();
}
}
// it is a object or array, process the first JsonToken
stack = new Stack();
processJsonToken(token);
// process the rest of the object or array
while (depth > 0) {
token = parser.nextToken();
processJsonToken(token);
}
return currentContext.getValue();
}
/**
* Process the JsonToken for objects and arrays.
*/
private void processJsonToken(JsonToken token) throws IOException {
switch (token) {
case START_ARRAY:
startArray();
break;
case START_OBJECT:
startObject();
break;
case FIELD_NAME:
currentContext.setKey(parser.getCurrentName());
break;
case VALUE_STRING:
setValue(parser.getValueAsString());
break;
case VALUE_NUMBER_INT:
setValue(parser.getLongValue());
break;
case VALUE_NUMBER_FLOAT:
setValue(parser.getDecimalValue());
break;
case VALUE_TRUE:
setValue(Boolean.TRUE);
break;
case VALUE_FALSE:
setValue(Boolean.FALSE);
break;
case VALUE_NULL:
setValueNull();
break;
case END_OBJECT:
endObject();
break;
case END_ARRAY:
endArray();
break;
default:
break;
}
}
private static final class Stack {
private Context head;
private void push(Context context) {
if (context != null) {
context.next = head;
head = context;
}
}
private Context pop(Context endingContext) {
if (head == null) {
throw new NoSuchElementException();
}
Context temp = head;
head = head.next;
temp.popContext(endingContext);
return temp;
}
private boolean isEmpty() {
return head == null;
}
}
private abstract static class Context {
Context next;
abstract void popContext(Context temp);
abstract Object getValue();
abstract void setValue(Object value);
abstract void setKey(String key);
abstract void setValueNull();
}
private static class ObjectContext extends Context {
private final Map<String, Object> map;
private String key;
ObjectContext() {
map = new LinkedHashMap<>();
}
ObjectContext(ModifyAwareType owner) {
map = new ModifyAwareMap<>(owner, new LinkedHashMap<>());
}
@Override
public void popContext(Context temp) {
setValue(temp.getValue());
}
@Override
Object getValue() {
return map;
}
@Override
void setValue(Object value) {
map.put(key, value);
}
@Override
void setKey(String key) {
this.key = key;
}
@Override
void setValueNull() {
map.put(key, null);
}
}
private static class ArrayContext extends Context {
private final List<Object> values;
ArrayContext() {
values = new ArrayList<>();
}
ArrayContext(ModifyAwareType owner) {
values = new ModifyAwareList<>(owner, new ArrayList<>());
}
@Override
public void popContext(Context temp) {
values.add(temp.getValue());
}
@Override
Object getValue() {
return values;
}
@Override
void setValue(Object value) {
values.add(value);
}
@Override
void setValueNull() {
// ignore
}
@Override
void setKey(String key) {
// not expected
}
}
}
@@ -1,212 +0,0 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
final class EJsonWriter {
/**
* Base jsonFactory implementation used when it is not passed in.
*/
static final JsonFactory jsonFactory = new JsonFactory();
private final JsonGenerator jsonGenerator;
private EJsonWriter(JsonGenerator jsonGenerator) {
this.jsonGenerator = jsonGenerator;
}
static String write(Object object) throws IOException {
StringWriter writer = new StringWriter(200);
write(object, writer).close();
return writer.toString();
}
static JsonGenerator write(Object object, Writer writer) throws IOException {
JsonGenerator generator = jsonFactory.createGenerator(writer);
write(object, generator);
generator.flush();
return generator;
}
static void write(Object object, JsonGenerator jsonGenerator) {
new EJsonWriter(jsonGenerator).writeJson(object);
}
static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
new EJsonWriter(jsonGenerator).writeCollection(null, collection);
}
private void writeJson(Object object) {
writeJson(null, object);
}
@SuppressWarnings("unchecked")
private void writeJson(String name, Object object) {
try {
if (object == null) {
writeNull(name);
} else if (object instanceof Number) {
writeNumber(name, (Number) object);
} else if (object instanceof String) {
writeString(name, (String) object);
} else if (object instanceof Map) {
writeMap(name, (Map<Object, Object>) object);
} else if (object instanceof Collection) {
writeCollection(name, (Collection<Object>) object);
} else if (object instanceof Boolean) {
writeBoolean(name, (Boolean) object);
} else if (object instanceof Date) {
writeDate(name, (Date) object);
} else if (object instanceof Map.Entry<?, ?>) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
writeJson(entry.getKey().toString(), entry.getValue());
} else {
writeString(name, object.toString());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeBoolean(String name, Boolean object) throws IOException {
if (name == null) {
jsonGenerator.writeBoolean(object);
} else {
jsonGenerator.writeBooleanField(name, object);
}
}
private void writeDate(String name, Date object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber(object.getTime());
} else {
jsonGenerator.writeNumberField(name, object.getTime());
}
}
private void writeNumber(String name, Number object) throws IOException {
if (object instanceof Long) {
writeLong(name, object);
} else if (object instanceof Integer) {
writeInteger(name, object);
} else if (object instanceof Double) {
writeDouble(name, object);
} else if (object instanceof BigDecimal) {
writeBigDecimal(name, object);
} else if (object instanceof BigInteger) {
writeBigInteger(name, object);
} else {
writeGeneralNumber(name, object);
}
}
private void writeGeneralNumber(String name, Number object) throws IOException {
writeBigDecimal(name, new BigDecimal(object.toString()));
}
private void writeBigDecimal(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((BigDecimal) object);
} else {
jsonGenerator.writeNumberField(name, (BigDecimal) object);
}
}
private void writeBigInteger(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((BigInteger) object);
} else {
jsonGenerator.writeNumberField(name, object.longValue());
}
}
private void writeDouble(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Double) object);
} else {
jsonGenerator.writeNumberField(name, (Double) object);
}
}
private void writeLong(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Long) object);
} else {
jsonGenerator.writeNumberField(name, (Long) object);
}
}
private void writeInteger(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Integer) object);
} else {
jsonGenerator.writeNumberField(name, (Integer) object);
}
}
private void writeNull(String name) throws IOException {
if (name == null) {
jsonGenerator.writeNull();
} else {
jsonGenerator.writeNullField(name);
}
}
private void writeString(String name, String object) throws IOException {
if (name == null) {
jsonGenerator.writeString(object);
} else {
jsonGenerator.writeStringField(name, object);
}
}
private void writeCollection(String name, Collection<Object> collection) throws IOException {
if (name != null) {
jsonGenerator.writeFieldName(name);
}
jsonGenerator.writeStartArray();
for (Object object : collection) {
writeJson(null, object);
}
jsonGenerator.writeEndArray();
}
private void writeMap(String name, Map<Object, Object> map) throws IOException {
if (name != null) {
jsonGenerator.writeFieldName(name);
}
jsonGenerator.writeStartObject();
Set<Entry<Object, Object>> entrySet = map.entrySet();
for (Entry<Object, Object> entry : entrySet) {
writeJson(entry.getKey().toString(), entry.getValue());
}
jsonGenerator.writeEndObject();
}
}
@@ -0,0 +1,181 @@
package io.ebeaninternal.json;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.ModifyAwareType;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Ebean specific {@link JsonAdapter} that materializes JSON into plain Java
* Map/List/scalar values - optionally wrapped in modify-aware collections so
* that mutations after load are tracked as dirty.
* <p>
* This consolidates the prior EJsonReader/EJsonWriter behavior into a single
* adapter that plugs into avaje {@code JsonMapper}.
*/
final class EbeanJsonAdapter implements JsonAdapter<Object> {
static final EbeanJsonAdapter PLAIN = new EbeanJsonAdapter(false);
static final EbeanJsonAdapter MODIFY_AWARE = new EbeanJsonAdapter(true);
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private final boolean modifyAware;
private EbeanJsonAdapter(boolean modifyAware) {
this.modifyAware = modifyAware;
}
@Override
public Object fromJson(JsonReader reader) {
return read(reader, null, modifyAware);
}
@Override
public void toJson(JsonWriter writer, Object value) {
write(writer, value);
}
/**
* Read a value honoring an explicitly supplied current token (or current token when null).
*/
static Object read(JsonReader parser, Token token, boolean modifyAware) {
ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null;
Token effectiveToken = token == null ? parser.currentToken() : token;
Object value;
if (effectiveToken == null) {
value = parseRawJson(parser.readRaw(), owner);
} else {
value = parseValue(parser, effectiveToken, owner);
}
if (owner != null) {
owner.setMarkedDirty(false);
}
return value;
}
private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) {
if (token == null) {
token = parser.currentToken();
if (token == null) {
if (parser.isNullValue()) {
return null;
}
return parseRawJson(parser.readRaw(), owner);
}
}
switch (token) {
case BEGIN_OBJECT:
return parseObjectValue(parser, owner);
case BEGIN_ARRAY:
return parseArrayValue(parser, owner);
case NUMBER:
BigDecimal value = parser.readDecimal();
return value.scale() <= 0 ? value.longValue() : value;
case STRING:
return parser.readString();
case BOOLEAN:
return parser.readBoolean();
case NULL:
parser.isNullValue();
return null;
default:
return parseRawJson(parser.readRaw(), owner);
}
}
private static Object parseRawJson(String json, ModifyAwareType owner) {
if (json == null) {
return null;
}
String content = json.trim();
if (content.isEmpty()) {
return null;
}
try (JsonReader parser = JSON_STREAM.reader(content)) {
return parseValue(parser, parser.currentToken(), owner);
}
}
private static Map<String, Object> parseObjectValue(JsonReader parser, ModifyAwareType owner) {
Map<String, Object> map = owner == null
? new LinkedHashMap<>()
: new ModifyAwareMap<>(owner, new LinkedHashMap<>());
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
map.put(fieldName, parseValue(parser, parser.currentToken(), owner));
}
parser.endObject();
return map;
}
private static List<Object> parseArrayValue(JsonReader parser, ModifyAwareType owner) {
List<Object> list = owner == null
? new ArrayList<>()
: new ModifyAwareList<>(owner, new ArrayList<>());
parser.beginArray();
while (parser.hasNextElement()) {
list.add(parseValue(parser, parser.currentToken(), owner));
}
parser.endArray();
return list;
}
/**
* Write the value to an existing JsonWriter (used for the raw stream paths).
*/
static void write(JsonWriter jsonWriter, Object object) {
if (object == null) {
jsonWriter.nullValue();
} else if (object instanceof String) {
jsonWriter.value((String) object);
} else if (object instanceof Integer) {
jsonWriter.value((Integer) object);
} else if (object instanceof Long) {
jsonWriter.value((Long) object);
} else if (object instanceof Double) {
jsonWriter.value((Double) object);
} else if (object instanceof Float) {
jsonWriter.value((Float) object);
} else if (object instanceof BigDecimal) {
jsonWriter.value((BigDecimal) object);
} else if (object instanceof Boolean) {
jsonWriter.value((Boolean) object);
} else if (object instanceof Map<?, ?>) {
writeMap(jsonWriter, (Map<?, ?>) object);
} else if (object instanceof Collection<?>) {
writeCollection(jsonWriter, (Collection<?>) object);
} else {
jsonWriter.value(object.toString());
}
}
private static void writeMap(JsonWriter jsonWriter, Map<?, ?> map) {
jsonWriter.beginObject();
for (Map.Entry<?, ?> entry : map.entrySet()) {
jsonWriter.name((String) entry.getKey());
write(jsonWriter, entry.getValue());
}
jsonWriter.endObject();
}
static void writeCollection(JsonWriter jsonWriter, Collection<?> collection) {
jsonWriter.beginArray();
for (Object element : collection) {
write(jsonWriter, element);
}
jsonWriter.endArray();
}
}
@@ -1,8 +1,7 @@
module io.ebean.core.json {
requires io.ebean.api;
requires transitive com.fasterxml.jackson.core;
requires transitive io.avaje.json;
exports io.ebeaninternal.json to io.ebean.test, io.ebean.core;
provides io.ebean.service.BootstrapService with io.ebeaninternal.json.DJsonService;
+5 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,14 +16,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- Provided scope for Postgres JSON/JSONB support -->
@@ -1,7 +1,7 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.text.StringFormatter;
import io.ebean.text.StringParser;
@@ -177,13 +177,44 @@ public interface ScalarType<T> extends StringParser, StringFormatter, ScalarData
void writeData(DataOutput dataOutput, T value) throws IOException;
/**
* Read the value from JsonParser.
* Read the value from JsonReader.
*/
T jsonRead(JsonParser parser) throws IOException;
default T jsonRead(JsonReader parser) throws IOException {
JsonReader.Token token = parser.currentToken();
if (token == JsonReader.Token.NULL) {
parser.isNullValue();
return null;
}
if (token == JsonReader.Token.STRING) {
return parse(parser.readString());
}
return parse(parser.readRaw());
}
/**
* Write the value to the JsonGenerator.
* Write the value to the JsonWriter.
*/
void jsonWrite(JsonGenerator writer, T value) throws IOException;
default void jsonWrite(JsonWriter writer, T value) throws IOException {
if (value == null) {
writer.nullValue();
return;
}
String formatted = formatValue(value);
if (formatted == null) {
writer.nullValue();
return;
}
DocPropertyType docType = docType();
if (docType == DocPropertyType.OBJECT || docType == DocPropertyType.LIST || docType == DocPropertyType.ROOT || likelyRawJson(formatted)) {
writer.rawValue(formatted);
} else {
writer.value(formatted);
}
}
private static boolean likelyRawJson(String formatted) {
String trimmed = formatted.trim();
return !trimmed.isEmpty() && (trimmed.charAt(0) == '{' || trimmed.charAt(0) == '[');
}
}
@@ -1,8 +1,8 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.ebean.config.JsonConfig;
import io.ebean.core.type.DataBinder;
import io.ebean.core.type.DataReader;
@@ -83,20 +83,31 @@ public abstract class ScalarTypeBaseDate<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
if (JsonToken.VALUE_NUMBER_INT == parser.getCurrentToken()) {
return convertFromMillis(parser.getLongValue());
} else {
return convertFromDate(Date.valueOf(parser.getText()));
public T jsonRead(JsonReader parser) throws IOException {
Token token = parser.currentToken();
if (Token.NUMBER == token) {
return convertFromMillis(parser.readLong());
}
if (Token.STRING == token) {
return convertFromDate(Date.valueOf(parser.readString()));
}
String raw = parser.readRaw();
if (raw == null || "null".equals(raw)) {
return null;
}
if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
return convertFromDate(Date.valueOf(raw.substring(1, raw.length() - 1)));
}
return convertFromMillis(Long.parseLong(raw));
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
public void jsonWrite(JsonWriter writer, T value) throws IOException {
if (mode == JsonConfig.Date.ISO8601) {
writer.writeString(toIsoFormat(value));
writer.value(toIsoFormat(value));
} else {
writer.writeNumber(convertToMillis(value));
writer.value(convertToMillis(value));
}
}
@@ -1,7 +1,8 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.ebean.config.JsonConfig;
import java.io.DataInput;
@@ -99,35 +100,53 @@ public abstract class ScalarTypeBaseDateTime<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
switch (parser.getCurrentToken()) {
case VALUE_NUMBER_INT: {
return convertFromMillis(parser.getLongValue());
}
case VALUE_NUMBER_FLOAT: {
BigDecimal value = parser.getDecimalValue();
Timestamp timestamp = ScalarTypeUtils.toTimestamp(value);
return convertFromTimestamp(timestamp);
}
default: {
return fromJsonISO8601(parser.getText());
}
public T jsonRead(JsonReader parser) throws IOException {
Token token = parser.currentToken();
if (token == Token.NUMBER) {
return readNumber(parser.readDecimal());
}
if (token == Token.STRING) {
return fromStringValue(parser.readString());
}
String raw = parser.readRaw();
if (raw == null || "null".equals(raw)) {
return null;
}
if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
return fromStringValue(raw.substring(1, raw.length() - 1));
}
return readNumber(new BigDecimal(raw));
}
private T fromStringValue(String value) {
if (value.indexOf('-') == -1 && Character.isDigit(value.charAt(0))) {
return readNumber(new BigDecimal(value));
}
return fromJsonISO8601(value);
}
private T readNumber(BigDecimal value) {
if (value.scale() <= 0) {
return convertFromMillis(value.longValue());
}
Timestamp timestamp = ScalarTypeUtils.toTimestamp(value);
return convertFromTimestamp(timestamp);
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
public void jsonWrite(JsonWriter writer, T value) throws IOException {
switch (mode) {
case ISO8601: {
writer.writeString(toJsonISO8601(value));
writer.value(toJsonISO8601(value));
break;
}
case NANOS: {
writer.writeNumber(toJsonNanos(value));
writer.value(toJsonNanos(value));
break;
}
default: {
writer.writeNumber(convertToMillis(value));
writer.value(convertToMillis(value));
}
}
}
@@ -1,7 +1,7 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import java.io.DataInput;
import java.io.DataOutput;
@@ -104,13 +104,16 @@ public abstract class ScalarTypeBaseVarchar<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
return parse(parser.getValueAsString());
public T jsonRead(JsonReader parser) throws IOException {
if (parser.isNullValue()) {
return null;
}
return parse(parser.readString());
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
writer.writeString(format(value));
public void jsonWrite(JsonWriter writer, T value) throws IOException {
writer.value(format(value));
}
@Override
@@ -4,8 +4,7 @@ module io.ebean.core.type {
requires transitive java.sql;
requires transitive io.ebean.api;
requires transitive io.avaje.json;
requires static org.postgresql.jdbc;
requires static com.fasterxml.jackson.core;
}
+7 -15
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.8.0</version>
<version>18.0.0</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
</dependency>
<dependency>
@@ -138,14 +138,6 @@
<!-- <optional>true</optional>-->
<!-- </dependency>-->
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -165,21 +157,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.8.0</version>
<version>18.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.8.0</version>
<version>18.0.0</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.
*/
@@ -1,6 +1,6 @@
package io.ebeaninternal.api;
import com.fasterxml.jackson.core.JsonGenerator;
import io.avaje.json.JsonWriter;
import io.ebean.plugin.BeanType;
import io.ebean.text.json.JsonContext;
import io.ebean.text.json.JsonWriteOptions;
@@ -17,7 +17,7 @@ public interface SpiJsonContext extends JsonContext {
/**
* Create a Json Writer for writing beans as JSON.
*/
SpiJsonWriter createJsonWriter(JsonGenerator gen, JsonWriteOptions options);
SpiJsonWriter createJsonWriter(JsonWriter gen, JsonWriteOptions options);
/**
* Create a Json Writer for writing beans as JSON supplying a writer.
@@ -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.
@@ -1,8 +1,8 @@
package io.ebeaninternal.api.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebeaninternal.server.deploy.BeanDescriptor;
@@ -14,7 +14,9 @@ public interface SpiJsonReader {
PersistenceContext persistenceContext();
SpiJsonReader forJson(JsonParser moreJson);
SpiJsonReader forJson(JsonReader moreJson);
SpiJsonReader forJson(String moreJson);
<T> void persistenceContextPut(Object beanId, T currentBean);
@@ -22,9 +24,9 @@ public interface SpiJsonReader {
ObjectMapper mapper();
JsonParser parser();
JsonReader parser();
JsonToken nextToken() throws IOException;
Token nextToken() throws IOException;
void pushPath(String path);
@@ -1,7 +1,7 @@
package io.ebeaninternal.server.changelog;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebean.event.changelog.ChangeType;
@@ -15,13 +15,13 @@ import java.util.Map;
*/
final class ChangeJsonBuilder {
private final JsonFactory jsonFactory = new JsonFactory();
private final JsonStream jsonStream = JsonStream.builder().build();
/**
* Write the bean change as JSON.
*/
void writeBeanJson(Writer writer, BeanChange bean, ChangeSet changeSet) throws IOException {
try (JsonGenerator generator = jsonFactory.createGenerator(writer)) {
try (JsonWriter generator = jsonStream.writer(writer)) {
writeBeanChange(generator, bean, changeSet);
generator.flush();
}
@@ -30,58 +30,67 @@ final class ChangeJsonBuilder {
/**
* Write the bean change as JSON document containing the transaction header details.
*/
private void writeBeanChange(JsonGenerator gen, BeanChange bean, ChangeSet changeSet) throws IOException {
gen.writeStartObject();
gen.writeNumberField("ts", bean.getEventTime());
gen.writeStringField("change", bean.getEvent().getCode());
gen.writeStringField("type", bean.getType());
gen.writeStringField("id", bean.getId().toString());
private void writeBeanChange(JsonWriter gen, BeanChange bean, ChangeSet changeSet) {
gen.beginObject();
gen.name("ts");
gen.value(bean.getEventTime());
gen.name("change");
gen.value(bean.getEvent().getCode());
gen.name("type");
gen.value(bean.getType());
gen.name("id");
gen.value(bean.getId().toString());
if (bean.getTenantId() != null) {
gen.writeStringField("tenantId", bean.getTenantId().toString());
gen.name("tenantId");
gen.value(bean.getTenantId().toString());
}
writeBeanTransactionDetails(gen, changeSet);
writeBeanValues(gen, bean);
gen.writeEndObject();
gen.endObject();
}
/**
* Denormalise by writing the transaction header details.
*/
private void writeBeanTransactionDetails(JsonGenerator gen, ChangeSet changeSet) throws IOException {
private void writeBeanTransactionDetails(JsonWriter gen, ChangeSet changeSet) {
String source = changeSet.getSource();
if (source != null) {
gen.writeStringField("source", source);
gen.name("source");
gen.value(source);
}
String userId = changeSet.getUserId();
if (userId != null) {
gen.writeStringField("userId", userId);
gen.name("userId");
gen.value(userId);
}
String userIpAddress = changeSet.getUserIpAddress();
if (userIpAddress != null) {
gen.writeStringField("userIpAddress", userIpAddress);
gen.name("userIpAddress");
gen.value(userIpAddress);
}
Map<String, String> userContext = changeSet.getUserContext();
if (userContext != null && !userContext.isEmpty()) {
gen.writeObjectFieldStart("userContext");
gen.name("userContext");
gen.beginObject();
for (Map.Entry<String, String> entry : userContext.entrySet()) {
gen.writeStringField(entry.getKey(), entry.getValue());
gen.name(entry.getKey());
gen.value(entry.getValue());
}
gen.writeEndObject();
gen.endObject();
}
}
/**
* For insert and update write the new/old values.
*/
private void writeBeanValues(JsonGenerator gen, BeanChange bean) throws IOException {
private void writeBeanValues(JsonWriter gen, BeanChange bean) {
if (bean.getEvent() != ChangeType.DELETE) {
gen.writeFieldName("data");
gen.writeRaw(":");
gen.writeRaw(bean.getData());
gen.name("data");
gen.rawValue(bean.getData());
String oldData = bean.getOldData();
if (oldData != null) {
gen.writeRaw(",\"oldData\":");
gen.writeRaw(oldData);
gen.name("oldData");
gen.rawValue(oldData);
}
}
}
@@ -27,6 +27,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
protected String bindLog = "";
protected PreparedStatement pstmt;
protected long startNano;
protected Binder binder;
private final ReentrantLock lock = new ReentrantLock();
/**
@@ -144,6 +145,7 @@ public abstract class AbstractSqlQueryRequest implements CancelableQuery {
}
protected void executeAsSql(Binder binder) throws SQLException {
this.binder = binder;
lock.lock();
try {
query.checkCancelled();
@@ -188,7 +188,10 @@ final class DefaultBeanLoader {
query.setLazyLoadProperty(ebi.lazyLoadProperty());
if (draft) {
query.asDraft();
} else if (mode == SpiQuery.Mode.LAZYLOAD_BEAN && desc.isSoftDelete()) {
} else if (desc.isSoftDelete()
&& (mode == SpiQuery.Mode.LAZYLOAD_BEAN || mode == SpiQuery.Mode.REFRESH_BEAN)) {
// include soft-deleted rows when lazy loading or refreshing so a
// refresh() on a soft-deleted bean can reload it (issue #3641)
query.setIncludeSoftDeletes();
}
if (embeddedOwnerIndex > -1) {
@@ -2276,7 +2276,10 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
if (initRequest.isAll()) {
queryPlanManager.setDefaultThreshold(initRequest.thresholdMicros());
}
return descriptorManager.queryPlanInit(initRequest);
List<MetaQueryPlan> list = descriptorManager.queryPlanInit(initRequest);
dtoBeanManager.queryPlanInit(initRequest, list);
relationalQueryEngine.queryPlanInit(initRequest, list);
return list;
}
List<MetaQueryPlan> queryPlanCollectNow(QueryPlanRequest request) {
@@ -2,9 +2,11 @@ package io.ebeaninternal.server.core;
import io.ebean.QueryIterator;
import io.ebean.core.type.DataReader;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiDtoQuery;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.bind.DataBindCapture;
import io.ebeaninternal.server.dto.DtoColumn;
import io.ebeaninternal.server.dto.DtoMappingRequest;
import io.ebeaninternal.server.dto.DtoQueryPlan;
@@ -19,6 +21,8 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static java.lang.System.Logger.Level.ERROR;
/**
* Wraps the objects involved in executing a DtoQuery.
*/
@@ -87,6 +91,22 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
if (plan != null) {
long exeMicros = (System.nanoTime() - startNano) / 1000L;
plan.collect(exeMicros);
// native SQL only (binder set in executeAsSql); ORM-backed DTO queries
// capture the query plan via the underlying ORM query plan instead.
if (binder != null && plan.collectFor(exeMicros)) {
captureBindForQueryPlan(exeMicros);
}
}
}
private void captureBindForQueryPlan(long exeMicros) {
final long startNanos = System.nanoTime();
try {
DataBindCapture capture = DataBindCapture.of(server.dataTimeZone());
binder.bind(query.getBindParams(), capture, new StringBuilder());
plan.setBind(capture.bindCapture(), exeMicros, startNanos);
} catch (SQLException e) {
CoreLog.log.log(ERROR, "Error capturing DTO bind values", e);
}
}
@@ -127,7 +147,7 @@ public final class DtoQueryRequest<T> extends AbstractSqlQueryRequest {
}
private DtoMappingRequest mappingRequest() throws SQLException {
return new DtoMappingRequest(query, sql, readMeta());
return new DtoMappingRequest(server, query, sql, readMeta());
}
private DtoColumn[] readMeta() throws SQLException {
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.core;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.json.stream.JsonStream;
import io.ebean.DatabaseBuilder;
import io.ebean.ExpressionFactory;
import io.ebean.annotation.Platform;
@@ -89,7 +89,7 @@ public final class InternalConfiguration {
private final boolean jacksonCorePresent;
private final ExpressionFactory expressionFactory;
private final SpiBackgroundExecutor backgroundExecutor;
private final JsonFactory jsonFactory;
private final JsonStream jsonStream;
private final DocStoreFactory docStoreFactory;
private final List<Plugin> plugins = new ArrayList<>();
private final MultiValueBind multiValueBind;
@@ -108,7 +108,7 @@ public final class InternalConfiguration {
this.tableModState = new TableModState();
this.logManager = initLogManager();
this.docStoreFactory = initDocStoreFactory(service(DocStoreFactory.class));
this.jsonFactory = config.getJsonFactory();
this.jsonStream = config.getJsonStream();
this.clusterManager = clusterManager;
this.backgroundExecutor = backgroundExecutor;
this.bootupClasses = bootupClasses;
@@ -292,7 +292,7 @@ public final class InternalConfiguration {
}
SpiJsonContext createJsonContext(SpiEbeanServer server) {
return jacksonCorePresent ? new DJsonContext(server, jsonFactory, typeManager) : null;
return jacksonCorePresent ? new DJsonContext(server, jsonStream, typeManager) : null;
}
AutoTuneService createAutoTuneService(SpiEbeanServer server) {
@@ -306,7 +306,7 @@ public final class InternalConfiguration {
RelationalQueryEngine createRelationalQueryEngine() {
return new DefaultRelationalQueryEngine(binder, config.getDatabaseBooleanTrue(), config.getPlatformConfig().getDbUuid().useBinaryOptimized(),
config.getJdbcFetchSizeFindEach(), config.getJdbcFetchSizeFindList(), databasePlatform.autoCommitFalseOnFindIterate());
config.getJdbcFetchSizeFindEach(), config.getJdbcFetchSizeFindList(), databasePlatform.autoCommitFalseOnFindIterate(), config.isQueryPlanEnable());
}
OrmQueryEngine createOrmQueryEngine() {
@@ -4,7 +4,11 @@ package io.ebeaninternal.server.core;
import io.ebean.RowConsumer;
import io.ebean.RowMapper;
import io.ebean.SqlRow;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.MetricVisitor;
import io.ebean.meta.QueryPlanInit;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.server.query.SqlQueryPlan;
import java.util.List;
import java.util.function.Consumer;
@@ -57,6 +61,23 @@ public interface RelationalQueryEngine {
*/
void collect(String label, long exeMicros);
/**
* Return true if query plan capture is enabled (the master switch). When false
* no SqlQuery plans are created or cached, so labelled queries incur no extra cost.
*/
boolean captureActive();
/**
* Obtain (creating and caching if necessary) the query plan for a labelled
* native SqlQuery, used to capture bind values for database query plan collection.
*/
SqlQueryPlan obtainPlan(String label, String sql, SpiEbeanServer server);
/**
* Initiate query plan bind capture for matching SqlQuery plans.
*/
void queryPlanInit(QueryPlanInit request, List<MetaQueryPlan> list);
/**
* Visit the metrics.
*/
@@ -1,8 +1,11 @@
package io.ebeaninternal.server.core;
import io.ebean.*;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiSqlQuery;
import io.ebeaninternal.server.bind.DataBindCapture;
import io.ebeaninternal.server.query.SqlQueryPlan;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
@@ -12,6 +15,8 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static java.lang.System.Logger.Level.ERROR;
/**
* Wraps the objects involved in executing a SqlQuery.
*/
@@ -39,9 +44,28 @@ public final class RelationalQueryRequest extends AbstractSqlQueryRequest {
@Override
protected void requestComplete() {
String label = query.getLabel();
if (label != null) {
long exeMicros = (System.nanoTime() - startNano) / 1000L;
queryEngine.collect(label, exeMicros);
if (label == null) {
return;
}
long exeMicros = (System.nanoTime() - startNano) / 1000L;
queryEngine.collect(label, exeMicros);
// capture bind values to later collect the database query plan
if (binder != null && queryEngine.captureActive()) {
SqlQueryPlan plan = queryEngine.obtainPlan(label, sql, server);
if (plan.collectFor(exeMicros)) {
captureBindForQueryPlan(plan, exeMicros);
}
}
}
private void captureBindForQueryPlan(SqlQueryPlan plan, long exeMicros) {
final long startNanos = System.nanoTime();
try {
DataBindCapture capture = DataBindCapture.of(server.dataTimeZone());
binder.bind(query.getBindParams(), capture, new StringBuilder());
plan.setBind(capture.bindCapture(), exeMicros, startNanos);
} catch (SQLException e) {
CoreLog.log.log(ERROR, "Error capturing SqlQuery bind values", e);
}
}
@@ -2,8 +2,11 @@ package io.ebeaninternal.server.deploy;
import io.ebean.PersistenceIOException;
import io.ebean.bean.BeanDiffVisitor;
import io.ebean.config.JsonConfig;
import io.ebeaninternal.api.json.SpiJsonWriter;
import io.ebeaninternal.server.json.WriteJson;
import io.ebeaninternal.server.util.ArrayStack;
import io.avaje.json.stream.JsonStream;
import java.io.IOException;
import java.io.StringWriter;
@@ -13,26 +16,18 @@ import java.io.StringWriter;
*/
final class BeanChangeJson implements BeanDiffVisitor {
private final StringWriter newData;
private final StringWriter oldData;
private final SpiJsonWriter newJson;
private final SpiJsonWriter oldJson;
private final StringWriter data;
private final SpiJsonWriter json;
private final boolean writeNew;
private final ArrayStack<BeanDescriptor<?>> stack = new ArrayStack<>();
private BeanDescriptor<?> descriptor;
BeanChangeJson(BeanDescriptor<?> descriptor, boolean statelessUpdate) {
BeanChangeJson(BeanDescriptor<?> descriptor, boolean writeNew) {
this.descriptor = descriptor;
this.newData = new StringWriter(200);
this.newJson = descriptor.createJsonWriter(newData);
newJson.writeStartObject();
if (statelessUpdate) {
this.oldJson = null;
this.oldData = null;
} else {
this.oldData = new StringWriter(200);
this.oldJson = descriptor.createJsonWriter(oldData);
oldJson.writeStartObject();
}
this.writeNew = writeNew;
this.data = new StringWriter(200);
this.json = new WriteJson(JsonStream.builder().build().writer(data), JsonConfig.Include.ALL);
json.writeStartObject();
}
@Override
@@ -40,10 +35,7 @@ final class BeanChangeJson implements BeanDiffVisitor {
try {
BeanProperty prop = descriptor.propertiesIndex[position];
if (prop.isDbUpdatable()) {
prop.jsonWriteValue(newJson, newVal);
if (oldJson != null) {
prop.jsonWriteValue(oldJson, oldVal);
}
prop.jsonWriteValue(json, writeNew ? newVal : oldVal);
}
} catch (IOException e) {
throw new PersistenceIOException(e);
@@ -55,18 +47,12 @@ final class BeanChangeJson implements BeanDiffVisitor {
stack.push(descriptor);
BeanPropertyAssocOne<?> embedded = (BeanPropertyAssocOne<?>)descriptor.propertiesIndex[position];
descriptor = embedded.targetDescriptor();
newJson.writeStartObject(embedded.name());
if (oldJson != null) {
oldJson.writeStartObject(embedded.name());
}
json.writeStartObject(embedded.name());
}
@Override
public void visitPop() {
newJson.writeEndObject();
if (oldJson != null) {
oldJson.writeEndObject();
}
json.writeEndObject();
descriptor = stack.pop();
}
@@ -75,28 +61,14 @@ final class BeanChangeJson implements BeanDiffVisitor {
*/
void flush() {
try {
newJson.writeEndObject();
newJson.flush();
if (oldJson != null) {
oldJson.writeEndObject();
oldJson.flush();
}
json.writeEndObject();
json.flush();
} catch (IOException e) {
throw new PersistenceIOException(e);
}
}
/**
* Return the new values JSON.
*/
String newJson() {
return newData.toString();
}
/**
* Return the old values JSON.
*/
String oldJson() {
return oldData == null ? null : oldData.toString();
String json() {
return data.toString();
}
}
@@ -792,10 +792,17 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
*/
private BeanChange updateBeanChange(PersistRequestBean<T> request) {
try {
BeanChangeJson changeJson = new BeanChangeJson(this, request.isStatelessUpdate());
request.intercept().addDirtyPropertyValues(changeJson);
changeJson.flush();
return beanChange(ChangeType.UPDATE, request.beanId(), changeJson.newJson(), changeJson.oldJson());
BeanChangeJson newValues = new BeanChangeJson(this, true);
request.intercept().addDirtyPropertyValues(newValues);
newValues.flush();
String oldData = null;
if (!request.isStatelessUpdate()) {
BeanChangeJson oldValues = new BeanChangeJson(this, false);
request.intercept().addDirtyPropertyValues(oldValues);
oldValues.flush();
oldData = oldValues.json();
}
return beanChange(ChangeType.UPDATE, request.beanId(), newValues.json(), oldData);
} catch (RuntimeException e) {
log.log(ERROR, "Failed to write ChangeLog entry for update", e);
return null;
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.EntityBean;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -49,25 +49,25 @@ class BeanDescriptorElementEmbeddedMap<T> extends BeanDescriptorElementEmbedded<
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
String fieldName = parser.nextFieldName();
if (fieldName == null) {
break;
}
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
if (stringKey) {
parser.nextToken();
Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here?
add.addKeyValue(fieldName, val);
} else {
parser.nextFieldName();
parser.beginObject();
parser.nextField();
Object key = scalarTypeKey.jsonRead(parser);
parser.nextFieldName();
parser.nextField();
Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here?
parser.endObject();
add.addKeyValue(key, val);
}
} while (true);
}
parser.endObject();
return add.collection();
}
@@ -1,7 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.ebean.PersistenceIOException;
import io.ebean.SqlUpdate;
import io.ebean.bean.EntityBean;
@@ -40,15 +39,13 @@ class BeanDescriptorElementScalar<T> extends BeanDescriptorElement<T> {
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
JsonToken token = parser.nextToken();
if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) {
break;
}
parser.beginArray();
while (parser.hasNextElement()) {
add.addElement(scalarType.jsonRead(parser));
} while (true);
}
parser.endArray();
return add.collection();
}
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.EntityBean;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -49,25 +49,25 @@ class BeanDescriptorElementScalarMap<T> extends BeanDescriptorElement<T> {
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
String fieldName = parser.nextFieldName();
if (fieldName == null) {
break;
}
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
if (stringKey) {
parser.nextToken();
Object val = scalarTypeVal.jsonRead(parser);
add.addKeyValue(fieldName, val);
} else {
parser.nextFieldName();
parser.beginObject();
parser.nextField();
Object key = scalarTypeKey.jsonRead(parser);
parser.nextFieldName();
parser.nextField();
Object val = scalarTypeVal.jsonRead(parser);
parser.endObject();
add.addKeyValue(key, val);
}
} while (true);
}
parser.endObject();
return add.collection();
}
@@ -1,10 +1,7 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.bean.EntityBean;
import io.ebean.text.json.EJson;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -34,7 +31,8 @@ final class BeanDescriptorJsonHelp<T> {
InheritInfo localInheritInfo = inheritInfo.readType(bean.getClass());
String discValue = localInheritInfo.getDiscriminatorStringValue();
String discColumn = localInheritInfo.getDiscriminatorColumn();
writeJson.gen().writeStringField(discColumn, discValue);
writeJson.gen().name(discColumn);
writeJson.gen().value(discValue);
localInheritInfo.desc().jsonWriteProperties(writeJson, bean);
}
writeJson.writeEndObject();
@@ -66,44 +64,38 @@ final class BeanDescriptorJsonHelp<T> {
@SuppressWarnings("unchecked")
T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T target) throws IOException {
JsonParser parser = jsonRead.parser();
//noinspection StatementWithEmptyBody
if (parser.getCurrentToken() == JsonToken.START_OBJECT) {
// start object token read by Jackson already
} else {
// check for null or start object
JsonToken token = parser.nextToken();
if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) {
return null;
}
if (JsonToken.START_OBJECT != token) {
throw new JsonParseException(parser, "Unexpected token " + token + " - expecting start_object", parser.getCurrentLocation());
}
JsonReader parser = jsonRead.parser();
if (parser.isNullValue()) {
return null;
}
Token token = parser.currentToken();
if (token != Token.BEGIN_OBJECT) {
throw new IllegalStateException("Unexpected token " + token + " - expecting BEGIN_OBJECT at: " + parser.location());
}
if (desc.inheritInfo == null || !withInheritance) {
return jsonReadObject(jsonRead, path, target);
}
ObjectNode node = jsonRead.mapper().readTree(parser);
if (node.isNull()) {
Map<String, Object> node = EJson.parseObject(parser);
if (node == null) {
return null;
}
JsonParser newParser = node.traverse();
SpiJsonReader newReader = jsonRead.forJson(newParser);
// check for the discriminator value to determine the correct sub type
String discColumn = inheritInfo.getRoot().getDiscriminatorColumn();
JsonNode discNode = node.get(discColumn);
if (discNode == null || discNode.isNull()) {
Object discValue = node.get(discColumn);
String rawObject = EJson.write(node);
SpiJsonReader newReader = jsonRead.forJson(rawObject);
if (discValue == null) {
if (!desc.isAbstractType()) {
return desc.jsonReadObject(newReader, path, target);
}
String msg = "Error reading inheritance discriminator - expected [" + discColumn + "] but no json key?";
throw new JsonParseException(newParser, msg, parser.getCurrentLocation());
throw new IllegalStateException(msg);
}
BeanDescriptor<T> inheritDesc = (BeanDescriptor<T>) inheritInfo.readType(discNode.asText()).desc();
BeanDescriptor<T> inheritDesc = (BeanDescriptor<T>) inheritInfo.readType(String.valueOf(discValue)).desc();
return inheritDesc.jsonReadObject(newReader, path, target);
}
@@ -124,35 +116,30 @@ final class BeanDescriptorJsonHelp<T> {
if (path != null) {
readJson.pushPath(path);
}
JsonReader parser = readJson.parser();
parser.beginObject();
// unmapped properties, send to JsonReadBeanVisitor later
Map<String, Object> unmappedProperties = null;
do {
JsonParser parser = readJson.parser();
JsonToken event = parser.nextToken();
if (JsonToken.FIELD_NAME == event) {
String key = parser.getCurrentName();
BeanProperty p = desc.beanProperty(key);
if (p != null) {
if (p.isVersion() && readJson.update() ) {
// skip version prop during update
p.jsonRead(readJson);
} else {
p.jsonRead(readJson, bean);
}
while (parser.hasNextField()) {
String key = parser.nextField();
BeanProperty p = desc.beanProperty(key);
if (p != null) {
if (p.isVersion() && readJson.update()) {
// skip version prop during update
p.jsonRead(readJson);
} else {
// read an unmapped property
if (unmappedProperties == null) {
unmappedProperties = new LinkedHashMap<>();
}
unmappedProperties.put(key, EJson.parse(parser));
p.jsonRead(readJson, bean);
}
} else if (JsonToken.END_OBJECT == event) {
break;
} else {
throw new RuntimeException("Unexpected token " + event + " - expecting key or end_object at: " + parser.getCurrentLocation());
// read an unmapped property
if (unmappedProperties == null) {
unmappedProperties = new LinkedHashMap<>();
}
unmappedProperties.put(key, EJson.parse(parser));
}
} while (true);
}
parser.endObject();
if (unmappedProperties != null) {
desc.setUnmappedJson(bean, unmappedProperties);
@@ -1,6 +1,7 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.DataIntegrityException;
import io.ebean.ModifyAwareType;
import io.ebean.ValuePair;
@@ -1437,14 +1438,14 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
}
public Object jsonRead(SpiJsonReader ctx) throws IOException {
JsonToken event = ctx.nextToken();
if (JsonToken.VALUE_NULL == event) {
JsonReader parser = ctx.parser();
if (parser.isNullValue()) {
return null;
} else {
// expect to read non-null json value
Object objValue;
if (scalarType != null) {
objValue = scalarType.jsonRead(ctx.parser());
objValue = scalarType.jsonRead(parser);
} else {
try {
objValue = ctx.readValueUsingObjectMapper(propertyType);
@@ -1,15 +1,14 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollection.ModifyListenMode;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
import io.ebean.plugin.PropertyAssocMany;
import io.ebean.text.PathProperties;
@@ -1043,9 +1042,8 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
*/
private Object jsonReadCollection(String json) throws IOException {
SpiJsonReader ctx = descriptor.createJsonReader(json);
JsonParser parser = ctx.parser();
JsonToken event = parser.nextToken();
if (JsonToken.VALUE_NULL == event) {
JsonReader parser = ctx.parser();
if (parser.isNullValue()) {
return null;
}
return jsonReadCollection(ctx, null, null);
@@ -1068,20 +1066,18 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
BeanCollection<?> collection = createEmpty(parentBean);
BeanCollectionAdd add = beanCollectionAdd(collection);
Map<Object, T> existingBeans = extractBeans(targets);
do {
JsonReader parser = readJson.parser();
parser.beginArray();
while (parser.hasNextElement()) {
EntityBean detailBean = getDetailBean(readJson, existingBeans);
if (detailBean == null) {
// read the entire array
break;
if (detailBean != null) {
add.addEntityBean(detailBean);
if (parentBean != null && childMasterProperty != null) {
// bind detail bean back to master via mappedBy property
childMasterProperty.setValue(detailBean, parentBean);
}
}
add.addEntityBean(detailBean);
if (parentBean != null && childMasterProperty != null) {
// bind detail bean back to master via mappedBy property
childMasterProperty.setValue(detailBean, parentBean);
}
} while (true);
}
return collection;
}
@@ -1092,18 +1088,79 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
BeanProperty idProperty = targetDescriptor.idProperty();
if (targets == null || idProperty == null) {
return (EntityBean) targetDescriptor.jsonRead(readJson, name, null);
} else {
JsonToken token = readJson.parser().nextToken();
if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) {
return null;
}
// extract the id. We have to buffer the JSON;
ObjectNode node = readJson.mapper().readTree(readJson.parser());
SpiJsonReader jsonReader = readJson.forJson(node.traverse());
JsonNode idNode = node.get(idProperty.name());
Object id = idNode == null ? null : idProperty.jsonRead(readJson.forJson(idNode.traverse()));
return (EntityBean) targetDescriptor.jsonRead(jsonReader, name, targets.get(id));
}
/// ROB Checkme
JsonReader parser = readJson.parser();
if (parser.isNullValue()) {
return null;
}
EntityBean incomingBean = (EntityBean) targetDescriptor.jsonRead(readJson, name, null);
Object id = idProperty.getValue(incomingBean);
EntityBean existingBean = (EntityBean) targets.get(id);
if (existingBean == null) {
return incomingBean;
}
mergeLoadedBean(targetDescriptor, incomingBean, existingBean);
return existingBean;
}
private void mergeLoadedBean(BeanDescriptor<?> descriptor, EntityBean incomingBean, EntityBean existingBean) {
EntityBeanIntercept incomingIntercept = incomingBean._ebean_getIntercept();
for (BeanProperty property : descriptor.propertiesBaseScalar()) {
if (incomingIntercept.isLoadedProperty(property.propertyIndex())) {
property.setValueIntercept(existingBean, property.getValue(incomingBean));
}
}
for (BeanPropertyAssocMany<?> property : descriptor.propertiesMany()) {
if (incomingIntercept.isLoadedProperty(property.propertyIndex())) {
Object mergedValue = mergeLoadedMany(property, property.getValue(incomingBean), property.getValue(existingBean));
property.setValueIntercept(existingBean, mergedValue);
}
}
}
private Object mergeLoadedMany(BeanPropertyAssocMany<?> property, Object incomingValue, Object existingValue) {
if (!(incomingValue instanceof Collection<?>)) {
return incomingValue;
}
if (!(existingValue instanceof Collection<?>)) {
return incomingValue;
}
Collection<?> incomingCollection = (Collection<?>) incomingValue;
Collection<?> existingCollection = (Collection<?>) existingValue;
BeanProperty idProperty = property.targetDescriptor.idProperty();
if (idProperty == null) {
return incomingValue;
}
Map<Object, EntityBean> existingById = new LinkedHashMap<>();
for (Object existingElement : existingCollection) {
if (existingElement instanceof EntityBean) {
EntityBean existingBean = (EntityBean) existingElement;
Object id = idProperty.getValue(existingBean);
if (id != null) {
existingById.put(id, existingBean);
}
}
}
BeanCollection<?> mergedCollection = property.createEmpty(null);
BeanCollectionAdd add = property.beanCollectionAdd(mergedCollection);
for (Object incomingElement : incomingCollection) {
if (!(incomingElement instanceof EntityBean)) {
continue;
}
EntityBean incomingBean = (EntityBean) incomingElement;
Object id = idProperty.getValue(incomingBean);
EntityBean existingBean = id == null ? null : existingById.get(id);
if (existingBean == null) {
add.addEntityBean(incomingBean);
} else {
mergeLoadedBean(property.targetDescriptor, incomingBean, existingBean);
add.addEntityBean(existingBean);
}
}
return mergedCollection;
}
/**
@@ -1,8 +1,7 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -38,17 +37,17 @@ class BeanPropertyAssocManyJsonHelp {
if (!this.many.jsonDeserialize) {
return;
}
JsonParser parser = readJson.parser();
JsonToken event = parser.nextToken();
if (JsonToken.VALUE_NULL == event) {
JsonReader parser = readJson.parser();
if (parser.isNullValue()) {
return;
}
if (many.isTransient()) {
jsonReadTransientUsingObjectMapper(readJson, parentBean);
return;
}
if (JsonToken.START_ARRAY != event && JsonToken.START_OBJECT != event) {
throw new JsonParseException(parser, "Unexpected token " + event + " - expecting start array or object");
Token event = parser.currentToken();
if (Token.BEGIN_ARRAY != event && Token.BEGIN_OBJECT != event) {
throw new IllegalStateException("Unexpected token " + event + " - expecting start array or object");
}
if (readJson.update()) {
many.setValueIntercept(parentBean, many.jsonReadCollection(readJson, parentBean, many.getValue(parentBean)));
@@ -29,11 +29,11 @@ class BeanPropertyAssocManyJsonTransient {
TypeFactory typeFactory = mapper.getTypeFactory();
JavaType target = typeFactory.constructType(many.targetType());
MapType jacksonType = typeFactory.constructMapType(LinkedHashMap.class, TypeFactory.unknownType(), target);
value = mapper.readValue(readJson.parser(), jacksonType);
value = mapper.readValue(readJson.parser().readRaw(), jacksonType);
} else {
// read list or set using Jackson object mapper
CollectionType jacksonType = mapper.getTypeFactory().constructCollectionType(manyType.getCollectionType(), many.targetType());
value = mapper.readValue(readJson.parser(), jacksonType);
value = mapper.readValue(readJson.parser().readRaw(), jacksonType);
}
many.setValue(parentBean, value);
}
@@ -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);
}
}
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.json;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.PersistenceIOException;
import io.ebean.bean.PersistenceContext;
import io.ebean.text.json.JsonBeanReader;
@@ -45,7 +45,7 @@ public final class DJsonBeanReader<T> implements JsonBeanReader<T> {
}
@Override
public JsonBeanReader<T> forJson(JsonParser moreJson) {
public JsonBeanReader<T> forJson(JsonReader moreJson) {
return new DJsonBeanReader<>(desc, readJson.forJson(moreJson));
}
}
@@ -1,12 +1,9 @@
package io.ebeaninternal.server.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.PrettyPrinter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.FetchPath;
import io.ebean.bean.EntityBean;
import io.ebean.config.JsonConfig;
@@ -28,7 +25,6 @@ import io.ebeaninternal.util.ParamTypeHelper.TypeInfo;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
@@ -45,30 +41,22 @@ import java.util.Set;
*/
public final class DJsonContext implements SpiJsonContext {
private static final PrettyPrinter PRETTY_PRINTER = new Pretty();
private final SpiEbeanServer server;
private final JsonFactory jsonFactory;
private final JsonStream jsonStream;
private final Object defaultObjectMapper;
private final JsonConfig.Include defaultInclude;
private final DJsonScalar jsonScalar;
private static class Pretty extends DefaultPrettyPrinter {
Pretty() {
_objectFieldValueSeparatorWithSpaces = ": ";
}
}
public DJsonContext(SpiEbeanServer server, JsonFactory jsonFactory, TypeManager typeManager) {
public DJsonContext(SpiEbeanServer server, JsonStream jsonStream, TypeManager typeManager) {
this.server = server;
this.jsonFactory = (jsonFactory != null) ? jsonFactory : new JsonFactory();
this.jsonStream = jsonStream;
this.defaultObjectMapper = this.server.config().getObjectMapper();
this.defaultInclude = this.server.config().getJsonInclude();
this.jsonScalar = new DJsonScalar(typeManager);
}
@Override
public void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException {
public void writeScalar(JsonWriter generator, Object scalarValue) throws IOException {
jsonScalar.write(generator, scalarValue);
}
@@ -78,31 +66,34 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public JsonGenerator createGenerator(Writer writer) throws JsonIOException {
public JsonWriter createGenerator(Writer writer) throws JsonIOException {
JsonWriter jsonWriter = stream().writer(writer);
jsonWriter.serializeNulls(defaultInclude == JsonConfig.Include.ALL);
jsonWriter.serializeEmpty(defaultInclude != JsonConfig.Include.NON_EMPTY);
return jsonWriter;
}
@Override
public JsonReader createParser(Reader reader) throws JsonIOException {
try {
return jsonFactory.createGenerator(writer);
return createParser(readAll(reader));
} catch (IOException e) {
throw new JsonIOException(e);
}
}
@Override
public JsonParser createParser(Reader reader) throws JsonIOException {
try {
return jsonFactory.createParser(reader);
} catch (IOException e) {
throw new JsonIOException(e);
}
private JsonStream stream() {
return (jsonStream != null) ? jsonStream : JsonStream.builder().build();
}
@Override
public <T> T toBean(Class<T> cls, String json) throws JsonIOException {
return toBean(cls, new StringReader(json));
return toBean(cls, createParser(json));
}
@Override
public <T> T toBean(Class<T> cls, String json, JsonReadOptions options) throws JsonIOException {
return toBean(cls, new StringReader(json), options);
return toBean(cls, createParser(json), options);
}
@Override
@@ -116,15 +107,15 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public <T> T toBean(Class<T> cls, JsonParser parser) throws JsonIOException {
public <T> T toBean(Class<T> cls, JsonReader parser) throws JsonIOException {
return toBean(cls, parser, null);
}
@Override
public <T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException {
public <T> T toBean(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException {
BeanDescriptor<T> desc = getDescriptor(cls);
try {
return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false), null, null);
return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream()), null, null);
} catch (IOException e) {
throw new JsonIOException(e);
}
@@ -132,12 +123,12 @@ public final class DJsonContext implements SpiJsonContext {
@Override
public <T> void toBean(T target, String json) throws JsonIOException {
toBean(target, new StringReader(json));
toBean(target, createParser(json));
}
@Override
public <T> void toBean(T target, String json, JsonReadOptions options) throws JsonIOException {
toBean(target, new StringReader(json), options);
toBean(target, createParser(json), options);
}
@Override
@@ -151,42 +142,42 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public <T> void toBean(T target, JsonParser parser) throws JsonIOException {
public <T> void toBean(T target, JsonReader parser) throws JsonIOException {
toBean(target, parser, null);
}
@SuppressWarnings("unchecked")
@Override
public <T> void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException {
public <T> void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException {
BeanDescriptor<T> desc = (BeanDescriptor<T>) getDescriptor(target.getClass());
try {
desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null), null, target);
desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null, stream()), null, target);
} catch (IOException e) {
throw new JsonIOException(e);
}
}
@Override
public <T> DJsonBeanReader<T> createBeanReader(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException {
public <T> DJsonBeanReader<T> createBeanReader(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException {
BeanDescriptor<T> desc = getDescriptor(cls);
return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false));
return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream()));
}
@Override
public <T> DJsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException {
public <T> DJsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException {
BeanDescriptor<T> desc = (BeanDescriptor<T>) beanType;
SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false);
SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false, stream());
return new DJsonBeanReader<>(desc, readJson);
}
@Override
public <T> List<T> toList(Class<T> cls, String json) throws JsonIOException {
return toList(cls, new StringReader(json));
return toList(cls, createParser(json));
}
@Override
public <T> List<T> toList(Class<T> cls, String json, JsonReadOptions options) throws JsonIOException {
return toList(cls, new StringReader(json), options);
return toList(cls, createParser(json), options);
}
@Override
@@ -200,35 +191,30 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public <T> List<T> toList(Class<T> cls, JsonParser src) throws JsonIOException {
public <T> List<T> toList(Class<T> cls, JsonReader src) throws JsonIOException {
return toList(cls, src, null);
}
@Override
public <T> List<T> toList(Class<T> cls, JsonParser src, JsonReadOptions options) throws JsonIOException {
public <T> List<T> toList(Class<T> cls, JsonReader src, JsonReadOptions options) throws JsonIOException {
BeanDescriptor<T> desc = getDescriptor(cls);
SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false);
SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false, stream());
try {
JsonToken currentToken = src.getCurrentToken();
if (currentToken != JsonToken.START_ARRAY) {
JsonToken event = src.nextToken();
if (event != JsonToken.START_ARRAY) {
throw new JsonParseException(src, "Expecting start_array event but got " + event);
}
if (src.isNullValue()) {
return null;
}
Token currentToken = src.currentToken();
if (currentToken != Token.BEGIN_ARRAY) {
throw new JsonIOException("Expecting BEGIN_ARRAY but got " + currentToken);
}
List<T> list = new ArrayList<>();
do {
// CHECKME: Should we update the list
src.beginArray();
while (src.hasNextElement()) {
T bean = desc.jsonRead(readJson, null, null);
if (bean == null) {
break;
} else {
if (bean != null) {
list.add(bean);
}
} while (true);
}
return list;
} catch (IOException e) {
throw new JsonIOException(e);
@@ -237,7 +223,7 @@ public final class DJsonContext implements SpiJsonContext {
@Override
public Object toObject(Type genericType, String json) throws JsonIOException {
return toObject(genericType, createParser(new StringReader(json)));
return toObject(genericType, createParser(json));
}
@Override
@@ -245,8 +231,22 @@ public final class DJsonContext implements SpiJsonContext {
return toObject(genericType, createParser(json));
}
private JsonReader createParser(String json) {
return stream().reader(json);
}
private String readAll(Reader reader) throws IOException {
StringBuilder builder = new StringBuilder();
char[] buffer = new char[2048];
int len;
while ((len = reader.read(buffer)) != -1) {
builder.append(buffer, 0, len);
}
return builder.toString();
}
@Override
public Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException {
public Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException {
TypeInfo info = ParamTypeHelper.getTypeInfo(genericType);
ManyType manyType = info.getManyType();
switch (manyType) {
@@ -262,19 +262,19 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public void toJson(Object value, JsonGenerator generator) throws JsonIOException {
public void toJson(Object value, JsonWriter generator) throws JsonIOException {
// generator passed in so don't close it
toJsonNoClose(value, generator, null);
}
@Override
public void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException {
public void toJson(Object value, JsonWriter generator, FetchPath fetchPath) throws JsonIOException {
// generator passed in so don't close it
toJsonNoClose(value, generator, JsonWriteOptions.pathProperties(fetchPath));
}
@Override
public void toJson(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException {
public void toJson(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException {
// generator passed in so don't close it
toJsonNoClose(o, generator, options);
}
@@ -303,9 +303,9 @@ public final class DJsonContext implements SpiJsonContext {
}
/**
* Write to the JsonGenerator and close when complete.
* Write to the JsonWriter and close when complete.
*/
private void toJsonWithClose(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException {
private void toJsonWithClose(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException {
try {
toJsonInternal(o, generator, options);
generator.close();
@@ -315,9 +315,9 @@ public final class DJsonContext implements SpiJsonContext {
}
/**
* Write to the JsonGenerator and without closing it (as it was created externally).
* Write to the JsonWriter and without closing it (as it was created externally).
*/
private void toJsonNoClose(Object o, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException {
private void toJsonNoClose(Object o, JsonWriter generator, JsonWriteOptions options) throws JsonIOException {
try {
toJsonInternal(o, generator, options);
} catch (IOException e) {
@@ -342,9 +342,9 @@ public final class DJsonContext implements SpiJsonContext {
private String toJsonString(Object value, JsonWriteOptions options, boolean pretty) throws JsonIOException {
StringWriter writer = new StringWriter(500);
try (JsonGenerator gen = createGenerator(writer)) {
try (JsonWriter gen = createGenerator(writer)) {
if (pretty) {
gen.setPrettyPrinter(PRETTY_PRINTER);
gen.pretty(true);
}
toJsonInternal(value, gen, options);
} catch (IOException e) {
@@ -354,29 +354,23 @@ public final class DJsonContext implements SpiJsonContext {
}
@SuppressWarnings("unchecked")
private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions options) throws IOException {
private void toJsonInternal(Object value, JsonWriter gen, JsonWriteOptions options) throws IOException {
if (value == null) {
gen.writeNull();
gen.nullValue();
} else if (value instanceof Number) {
gen.writeNumber(((Number) value).doubleValue());
gen.jsonValue(value);
} else if (value instanceof Boolean) {
gen.writeBoolean((Boolean) value);
gen.value((Boolean) value);
} else if (value instanceof String) {
gen.writeString((String) value);
// } else if (o instanceof JsonElement) {
gen.value((String) value);
} else if (value instanceof Map<?, ?>) {
toJsonFromMap((Map<Object, Object>) value, gen, options);
} else if (value instanceof Collection<?>) {
toJsonFromCollection((Collection<?>) value, null, gen, options);
} else if (value instanceof EntityBean) {
BeanDescriptor<?> d = getDescriptor(value.getClass());
WriteJson writeJson = createWriteJson(gen, options);
d.jsonWrite(writeJson, (EntityBean) value, null);
} else {
jsonScalar.write(gen, value);
}
@@ -385,8 +379,8 @@ public final class DJsonContext implements SpiJsonContext {
@Override
public SpiJsonReader createJsonRead(BeanType<?> beanType, String json) {
BeanDescriptor<?> desc = (BeanDescriptor<?>) beanType;
JsonParser parser = createParser(new StringReader(json));
return new ReadJson(desc, parser, null, defaultObjectMapper, false);
JsonReader parser = createParser(json);
return new ReadJson(desc, parser, null, defaultObjectMapper, false, stream());
}
@Override
@@ -395,11 +389,11 @@ public final class DJsonContext implements SpiJsonContext {
}
@Override
public SpiJsonWriter createJsonWriter(JsonGenerator gen, JsonWriteOptions options) {
public SpiJsonWriter createJsonWriter(JsonWriter gen, JsonWriteOptions options) {
return createWriteJson(gen, options);
}
private WriteJson createWriteJson(JsonGenerator gen, JsonWriteOptions options) {
private WriteJson createWriteJson(JsonWriter gen, JsonWriteOptions options) {
FetchPath pathProps = (options == null) ? null : options.getPathProperties();
Map<String, JsonWriteBeanVisitor<?>> visitors = (options == null) ? null : options.getVisitorMap();
return new WriteJson(server,
@@ -411,46 +405,50 @@ public final class DJsonContext implements SpiJsonContext {
options == null || options.isIncludeLoadedImplicit());
}
private <T> void toJsonFromCollection(Collection<T> collection, String key, JsonGenerator gen, JsonWriteOptions options) throws IOException {
private <T> void toJsonFromCollection(Collection<T> collection, String key, JsonWriter gen, JsonWriteOptions options) throws IOException {
if (key != null) {
gen.writeFieldName(key);
gen.name(key);
}
gen.writeStartArray();
gen.beginArray();
WriteJson writeJson = createWriteJson(gen, options);
for (T bean : collection) {
BeanDescriptor<?> d = getDescriptor(bean.getClass());
d.jsonWrite(writeJson, (EntityBean) bean, null);
if (bean == null) {
gen.nullValue();
} else if (bean instanceof EntityBean) {
BeanDescriptor<?> d = getDescriptor(bean.getClass());
d.jsonWrite(writeJson, (EntityBean) bean, null);
} else {
EJson.write(bean, gen);
}
}
gen.writeEndArray();
gen.endArray();
}
private void toJsonFromMap(Map<Object, Object> map, JsonGenerator gen, JsonWriteOptions options) throws IOException {
private void toJsonFromMap(Map<Object, Object> map, JsonWriter gen, JsonWriteOptions options) throws IOException {
Set<Entry<Object, Object>> entrySet = map.entrySet();
Iterator<Entry<Object, Object>> it = entrySet.iterator();
WriteJson writeJson = createWriteJson(gen, options);
gen.writeStartObject();
gen.beginObject();
while (it.hasNext()) {
Entry<Object, Object> entry = it.next();
String key = entry.getKey().toString();
Object value = entry.getValue();
if (value == null) {
gen.writeNullField(key);
gen.name(key);
gen.nullValue();
} else if (value instanceof Collection<?>) {
toJsonFromCollection((Collection<?>) value, key, gen, options);
} else if (value instanceof EntityBean) {
BeanDescriptor<?> d = getDescriptor(value.getClass());
d.jsonWrite(writeJson, (EntityBean) value, key);
} else {
if (value instanceof Collection<?>) {
toJsonFromCollection((Collection<?>) value, key, gen, options);
} else if (value instanceof EntityBean) {
BeanDescriptor<?> d = getDescriptor(value.getClass());
d.jsonWrite(writeJson, (EntityBean) value, key);
} else {
EJson.write(entry, gen);
}
gen.name(key);
EJson.write(value, gen);
}
}
gen.writeEndObject();
gen.endObject();
}
/**
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.json;
import com.fasterxml.jackson.core.JsonGenerator;
import io.avaje.json.JsonWriter;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.server.type.TypeManager;
@@ -19,21 +19,18 @@ public final class DJsonScalar {
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void write(JsonGenerator gen, Object value) throws IOException {
public void write(JsonWriter gen, Object value) throws IOException {
if (value instanceof String) {
gen.writeString((String) value);
gen.value((String) value);
} else if (value instanceof List) {
// expected for @DbArray values
List list = (List)value;
gen.writeRaw('[');
gen.beginArray();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
gen.writeRaw(',');
}
write(gen, list.get(i));
}
gen.writeRaw(']');
gen.endArray();
} else {
ScalarType scalarType = typeManager.type(value.getClass());
@@ -1,8 +1,10 @@
package io.ebeaninternal.server.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.stream.BufferRecycleStrategy;
import io.avaje.json.stream.JsonStream;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.PersistenceContext;
@@ -22,8 +24,9 @@ import java.util.Map;
*/
public final class ReadJson implements SpiJsonReader {
private final JsonStream jsonStream;
private final BeanDescriptor<?> rootDesc;
private final JsonParser parser;
private final JsonReader parser;
private final PathStack pathStack;
/**
* Map of the JsonReadBeanVisitor keyed by path.
@@ -38,9 +41,10 @@ public final class ReadJson implements SpiJsonReader {
/**
* Construct with parser and readOptions.
*/
public ReadJson(BeanDescriptor<?> desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper, boolean update) {
public ReadJson(BeanDescriptor<?> desc, JsonReader parser, JsonReadOptions readOptions, Object objectMapper, boolean update, JsonStream jsonStream) {
this.rootDesc = desc;
this.parser = parser;
this.jsonStream = jsonStream;
this.objectMapper = objectMapper;
this.persistenceContext = initPersistenceContext(readOptions);
this.loadContext = initLoadContext(desc, readOptions);
@@ -54,7 +58,8 @@ public final class ReadJson implements SpiJsonReader {
/**
* Construct when transferring load context, persistence context, object mapper etc to a new ReadJson instance.
*/
private ReadJson(JsonParser moreJson, ReadJson source) {
private ReadJson(JsonReader moreJson, ReadJson source) {
this.jsonStream = source.jsonStream;
this.parser = moreJson;
this.rootDesc = source.rootDesc;
this.pathStack = source.pathStack;
@@ -91,13 +96,22 @@ public final class ReadJson implements SpiJsonReader {
}
/**
* Return a new instance of ReadJson using the existing context but with a new JsonParser.
* Return a new instance of ReadJson using the existing context but with a new JsonReader.
*/
@Override
public SpiJsonReader forJson(JsonParser moreJson) {
public SpiJsonReader forJson(JsonReader moreJson) {
return new ReadJson(moreJson, this);
}
@Override
public SpiJsonReader forJson(String moreJson) {
JsonReader nestedReader = JsonStream.builder()
.bufferRecycling(BufferRecycleStrategy.NO_RECYCLING)
.build()
.reader(moreJson);
return new ReadJson(nestedReader, this);
}
/**
* Add the bean to the persistence context.
*/
@@ -152,19 +166,19 @@ public final class ReadJson implements SpiJsonReader {
}
/**
* Return the JsonParser.
* Return the JsonReader.
*/
@Override
public JsonParser parser() {
public JsonReader parser() {
return parser;
}
/**
* Return the next JsonToken from the underlying parser.
* Return the current token from the underlying parser.
*/
@Override
public JsonToken nextToken() throws IOException {
return parser.nextToken();
public Token nextToken() throws IOException {
return parser.currentToken();
}
/**
@@ -209,7 +223,7 @@ public final class ReadJson implements SpiJsonReader {
*/
@Override
public Object readValueUsingObjectMapper(Class<?> propertyType) throws IOException {
return mapper().readValue(parser, propertyType);
return mapper().readValue(parser.readRaw(), propertyType);
}
/**
@@ -1,7 +1,7 @@
package io.ebeaninternal.server.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonWriter;
import io.ebean.FetchPath;
import io.ebean.bean.EntityBean;
import io.ebean.config.JsonConfig;
@@ -24,7 +24,7 @@ import java.util.Set;
public final class WriteJson implements SpiJsonWriter {
private final SpiEbeanServer server;
private final JsonGenerator generator;
private final JsonWriter generator;
private final FetchPath fetchPath;
private final Map<String, JsonWriteBeanVisitor<?>> visitors;
private final PathStack pathStack;
@@ -37,7 +37,7 @@ public final class WriteJson implements SpiJsonWriter {
/**
* Construct for full bean use (normal).
*/
public WriteJson(SpiEbeanServer server, JsonGenerator generator, FetchPath fetchPath,
public WriteJson(SpiEbeanServer server, JsonWriter generator, FetchPath fetchPath,
Map<String, JsonWriteBeanVisitor<?>> visitors, Object objectMapper, JsonConfig.Include include,
boolean includeLoadedImplicit) {
@@ -50,12 +50,14 @@ public final class WriteJson implements SpiJsonWriter {
this.includeLoadedImplicit = includeLoadedImplicit;
this.parentBeans = new ArrayStack<>();
this.pathStack = new PathStack();
this.generator.serializeNulls(isIncludeNull());
this.generator.serializeEmpty(isIncludeEmpty());
}
/**
* Construct for Json scalar use.
*/
public WriteJson(JsonGenerator generator, JsonConfig.Include include) {
public WriteJson(JsonWriter generator, JsonConfig.Include include) {
this.generator = generator;
this.include = include;
this.includeLoadedImplicit = true;
@@ -65,6 +67,8 @@ public final class WriteJson implements SpiJsonWriter {
this.objectMapper = null;
this.parentBeans = null;
this.pathStack = null;
this.generator.serializeNulls(isIncludeNull());
this.generator.serializeEmpty(isIncludeEmpty());
}
/**
@@ -84,7 +88,7 @@ public final class WriteJson implements SpiJsonWriter {
}
@Override
public JsonGenerator gen() {
public JsonWriter gen() {
return generator;
}
@@ -95,171 +99,113 @@ public final class WriteJson implements SpiJsonWriter {
@Override
public void writeStartObject(String key) {
try {
if (key != null) {
generator.writeFieldName(key);
}
generator.writeStartObject();
} catch (IOException e) {
throw new JsonIOException(e);
if (key != null) {
generator.name(key);
}
generator.beginObject();
}
@Override
public void writeStartObject() {
try {
generator.writeStartObject();
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.beginObject();
}
@Override
public void writeEndObject() {
try {
generator.writeEndObject();
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.endObject();
}
@Override
public void writeStartArray(String key) {
try {
if (key != null) {
generator.writeFieldName(key);
}
generator.writeStartArray();
} catch (IOException e) {
throw new JsonIOException(e);
if (key != null) {
generator.name(key);
}
generator.beginArray();
}
@Override
public void writeStartArray() {
try {
generator.writeStartArray();
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.beginArray();
}
@Override
public void writeEndArray() {
try {
generator.writeEndArray();
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.endArray();
}
@Override
public void writeRaw(String text) {
try {
generator.writeRaw(text);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.rawChunkStart();
generator.rawChunk(text);
generator.rawChunkEnd();
}
@Override
public void writeRawValue(String text) {
try {
generator.writeRawValue(text);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.rawValue(text);
}
@Override
public void writeFieldName(String name) {
try {
generator.writeFieldName(name);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
}
@Override
public void writeNullField(String name) {
if (isIncludeNull()) {
try {
generator.writeNullField(name);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.nullValue();
}
}
@Override
public void writeNumberField(String name, long value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeNumberField(String name, double value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeNumberField(String name, int value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeNumberField(String name, short value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeNumberField(String name, float value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value((double) value);
}
@Override
public void writeNumberField(String name, BigDecimal value) {
try {
generator.writeNumberField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeStringField(String name, String value) {
try {
generator.writeStringField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeBinary(InputStream is, int length) {
try {
generator.writeBinary(is, length);
generator.value(is.readNBytes(length));
} catch (IOException e) {
throw new JsonIOException(e);
}
@@ -267,83 +213,49 @@ public final class WriteJson implements SpiJsonWriter {
@Override
public void writeBinaryField(String name, byte[] value) {
try {
generator.writeBinaryField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeBooleanField(String name, boolean value) {
try {
generator.writeBooleanField(name, value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.name(name);
generator.value(value);
}
@Override
public void writeBoolean(boolean value) {
try {
generator.writeBoolean(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeString(String value) {
try {
generator.writeString(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeNumber(int value) {
try {
generator.writeNumber(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeNumber(long value) {
try {
generator.writeNumber(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeNumber(double value) {
try {
generator.writeNumber(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeNumber(BigDecimal value) {
try {
generator.writeNumber(value);
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.value(value);
}
@Override
public void writeNull() {
try {
generator.writeNull();
} catch (IOException e) {
throw new JsonIOException(e);
}
generator.nullValue();
}
@Override
@@ -375,55 +287,39 @@ public final class WriteJson implements SpiJsonWriter {
@Override
public void beginAssocMany(String key) {
try {
pathStack.pushPathKey(key);
if (key != null) {
generator.writeFieldName(key);
}
generator.writeStartArray();
} catch (IOException e) {
throw new JsonIOException(e);
pathStack.pushPathKey(key);
if (key != null) {
generator.name(key);
}
generator.beginArray();
}
@Override
public void endAssocMany() {
try {
pathStack.pop();
generator.writeEndArray();
} catch (IOException e) {
throw new JsonIOException(e);
}
pathStack.pop();
generator.endArray();
}
@Override
public void beginAssocManyMap(String key, boolean elementCollection) {
try {
pathStack.pushPathKey(key);
if (key != null) {
generator.writeFieldName(key);
}
if (elementCollection) {
generator.writeStartObject();
} else {
generator.writeStartArray();
}
} catch (IOException e) {
throw new JsonIOException(e);
pathStack.pushPathKey(key);
if (key != null) {
generator.name(key);
}
if (elementCollection) {
generator.beginObject();
} else {
generator.beginArray();
}
}
@Override
public void endAssocManyMap(boolean elementCollection) {
try {
pathStack.pop();
if (elementCollection) {
generator.writeEndObject();
} else {
generator.writeEndArray();
}
} catch (IOException e) {
throw new JsonIOException(e);
pathStack.pop();
if (elementCollection) {
generator.endObject();
} else {
generator.endArray();
}
}
@@ -465,8 +361,8 @@ public final class WriteJson implements SpiJsonWriter {
}
}
try {
generator.writeFieldName(name);
objectMapper().writeValue(generator, value);
generator.name(name);
generator.rawValue(objectMapper().writeValueAsString(value));
} catch (IOException e) {
throw new JsonIOException(e);
}
@@ -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() {

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