Compare commits

...
Author SHA1 Message Date
robin.bygrave 72ce587629 Add dbName() to MetaQueryPlan - easier to support multi-db query plan capture handling 2026-08-14 18:03:02 +12:00
robin.bygrave 77e98a3f54 Metrics - support both CUMULATIVE and DELTA metrics collection concurrently
ebean insight [and StatsD] work off DELTA metrics, where as OTEL
and Prometheus want CUMULATIVE. With this change we can have a metrics
collection for ebean insight using DELTA mode and have a second collection
use CUMULATIVE for reporting to OTEL - for the case of fan-out metrics
going to 2 places.

This isn't strictly needed when only one collection mode is used.
2026-08-14 16:31:28 +12:00
robin.bygrave 3c4e87f3f7 Docs: Update docs on OneToOne mapping with mappedBy 2026-08-06 21:53:14 +12:00
Rob Bygrave f5eee70ec4 Bump test modules to 18.4.0 2026-07-28 22:35:46 +12:00
Rob Bygrave 46a73f6250 Version 18.4.0 2026-07-28 22:29:48 +12:00
Rob Bygrave 4a46cc0706 Tests: use redis 8.6.2 explicitly in tests 2026-07-28 22:26:49 +12:00
Rob Bygrave 94630b7e1e Tests: Rename ebean-reddison test entities 2026-07-28 21:42:48 +12:00
robin.bygrave aac85eec89 Tests: Move redisson tests to use database 1 / isolate from ebean-redis tests 2026-07-28 21:23:41 +12:00
robin.bygrave a8ec4ee437 Tests: Improve flakey test TestHistoryOneToOne 2026-07-28 21:18:56 +12:00
Rob Bygrave 12b9d0eaae Bump ebean-migration to 14.4.0 - adds rebaseMigrationHistory feature (#3873) 2026-07-28 21:09:27 +12:00
Rob Bygraveandrobin.bygrave eeb32514be DB2 exists - use existsWithCaseWhen true, similar to #3849 (#3872)
* DB2 exists - use existsWithCaseWhen true, similar to #3849

Fixes exists() query regression with DB2 by using the platform flag existsWithCaseWhen = true;

* DB2 using existsFromClause = " from sysibm.sysdummy1";

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-28 20:18:34 +12:00
robin.bygrave 02936735c1 Tests: Disable query plan capture tests for DB2 2026-07-28 19:40:14 +12:00
dependabot[bot] 57713a9686 Build(deps): Bump org.postgresql:postgresql in /ebean-core-type (#3870)
Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.11 to 42.7.12.
- [Release notes](https://github.com/pgjdbc/pgjdbc/releases)
- [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.11...REL42.7.12)

---
updated-dependencies:
- dependency-name: org.postgresql:postgresql
  dependency-version: 42.7.12
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 10:01:59 +12:00
dependabot[bot] 4076b0fcbd Build(deps): Bump org.postgresql:postgresql in /ebean-postgis-types (#3869)
Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.11 to 42.7.12.
- [Release notes](https://github.com/pgjdbc/pgjdbc/releases)
- [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.11...REL42.7.12)

---
updated-dependencies:
- dependency-name: org.postgresql:postgresql
  dependency-version: 42.7.12
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 10:01:34 +12:00
Rob Bygraveandrobin.bygrave 49f6bef2fb Add ebean-avajejsonb-mapper module - Mapping DbJson content using avaje jsonb library (no reflection, graalvm support) (#3871)
* Add ebean-avajejsonb-mapper module - Mapping DbJson content using avaje jsonb library (no reflection, graalvm support)

* Fix flakey test TestInheritance

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-24 10:01:03 +12:00
Rob Bygraveandrobin.bygrave d6ba4967b6 ORM Update - add usingTransaction() to support using explicit transaction (#3868)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-22 09:45:54 +12:00
Rob Bygraveandrobin.bygrave 0d0ec79f1e SqlUpdate - add usingTransaction() to support using explicit transaction (#3867)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-21 09:59:23 +12:00
Rob Bygraveandrobin.bygrave 1737ae13db Modify SqlQuery.TypedQuery to implement FindableQuery (common interface) (#3866)
Add usingConnection() and canel() to support common FindableQuery interface

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-20 23:27:51 +12:00
Rob Bygraveandrobin.bygrave 17b641b163 MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery) (#3865)
* MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery)

* MappedQuery - add findEach() findEachWhile() to MappedQuery (and promote to StreamableQuery)

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-20 23:12:37 +12:00
Rob Bygraveandrobin.bygrave 4f6168dfed Add FinableQuery, StreamableQuery common interfaces for Query, DtoQuery, MappedQuery, SqlQuery (#3864)
* Add FinableQuery, StreamableQuery common interfaces for Query, DtoQuery, MappedQuery, SqlQuery

* QueryBean.cancel() added to support the new common FindableQuery interface

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-20 22:44:42 +12:00
robin.bygrave a03a098e1b Tests: fix tests for Postgres any syntax 2026-07-20 22:06:30 +12:00
Rob Bygrave 78f17fcd46 Merge pull request #3863 from ebean-orm/feature/mapped-query-cancellable
Change MappedQuery to extend CancelableQuery - make it cancelable
2026-07-20 22:04:15 +12:00
Rob Bygrave 6895a4c057 Merge pull request #3862 from ebean-orm/dependabot/maven/ebean-spring-txn/com.fasterxml.jackson.core-jackson-databind-2.22.1
Build(deps-dev): Bump com.fasterxml.jackson.core:jackson-databind from 2.22.0 to 2.22.1 in /ebean-spring-txn
2026-07-20 21:59:24 +12:00
robin.bygrave 2362c0816a Change MappedQuery to extend CancelableQuery - make it cancelable 2026-07-20 21:57:22 +12:00
robin.bygrave 37567e8fe8 Change MappedQuery to extend CancelableQuery - make it cancelable 2026-07-20 21:50:59 +12:00
robin.bygrave ce8dc36cf7 Actual version 18.3.0 deployed to central 2026-07-17 10:20:02 +12:00
dependabot[bot] 05f5291359 Build(deps-dev): Bump com.fasterxml.jackson.core:jackson-databind
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.22.0 to 2.22.1.
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-16 22:10:37 +00:00
Rob Bygrave 1173962398 Merge pull request #3861 from ebean-orm/feture/dto-more
dto mapping: add DtoConverters
2026-07-17 10:09:49 +12:00
robin.bygrave 56f86e0263 dto mapping: add DtoConverters 2026-07-17 10:01:54 +12:00
Rob Bygrave 8751a1a04d Merge pull request #3860 from ebean-orm/feature/insert-on-conflict-pk-fallback
With InsertOnConflict fallback to PK constraint when no Unique constr…
2026-07-17 00:04:00 +12:00
robin.bygrave 14e6e3f824 With InsertOnConflict fallback to PK constraint when no Unique constraint and id value provided
This is for the case where the primary key is a supplied value (like a string, iso code, serial
number, bar code etc)
2026-07-16 23:51:29 +12:00
Rob Bygrave a9d37b83d7 Merge pull request #3859 from ebean-orm/feature/findOneOrThrow
enh: Add findOneOrThrow() with decent derived message for EntityNotFo…
2026-07-16 23:28:12 +12:00
robin.bygrave 857e92583d enh: Add findOneOrThrow() with decent derived message for EntityNotFoundException
Adding this as I am seeing a LOT of cases where ebean can derive a good enough
error message (id case, plus single unique key case) that it is worth adding
this syntax sugar.

Yes we have discussed this a well it's not so useful for the multi-lingual /
 non-english case and also marginal when the error message needs to be built
 case.
2026-07-16 23:15:15 +12:00
Rob Bygrave 14056bd7fb Merge pull request #3858 from ebean-orm/feature/3643-fetch-id-only-fk
Fold id-only *ToOne fetches into parent select, avoiding unneeded join (#3643)
2026-07-16 22:33:12 +12:00
robin.bygrave 149ed8318d Fold id-only *ToOne fetches into parent select, avoiding unneeded join (#3643)
When a *ToOne association is fetched with only its id property, the
foreign key column already exists on the owning table so no join is
required. Previously this always generated an unnecessary join.

- OrmQueryProperties: add includesExactly() and withAddedInclude() to
  support adding an include without mutating a shared/cached instance
  (the included set and its precomputed hash prefix are immutable
  since #3761/#3763, so an "add" must produce a new instance).

- OrmQueryDetail: add convertIdFetches(), a bottom-up pass over
  fetchPaths that replaces an id-only *ToOne fetch with its FK
  property folded into the parent's select, removing the now-unneeded
  fetch path. Skips exported (mappedBy) one-to-one associations, which
  have no local FK column and always require a join. Tracks paths
  still depended on by a surviving child fetch so their join isn't
  incorrectly folded away.

- Add TestFetchIdOnly covering PathProperties, select(), and fetch()
  usages, plus a control case confirming the join is retained when
  more than the id is fetched.

- Update EqlParserTest, TestSubQuery and TestMergeCustomer assertions
  to reflect the new join-free SQL.
2026-07-16 22:28:42 +12:00
Rob Bygrave 2fe4c6c456 Merge pull request #3857 from ebean-orm/feature/dto-mapper-computed
dto mapping: support calculated getter methods with and without requi…
2026-07-16 22:07:52 +12:00
robin.bygrave 19b9e82afd dto mapping: add some negative tests 2026-07-16 22:02:44 +12:00
robin.bygrave 01ab7edc8c Bump ebean-annotation to 8.9 with DtoRef.requires property 2026-07-16 21:41:08 +12:00
robin.bygrave bd0b274973 dto mapping: various fixes
Fix #2 — @DtoRef never checked hasField(): Added requires() to @DtoRef
 (same explicit-empty semantics as @DtoPath). DtoMappingReader now
 detects a computed association getter and requires an explicit
 requires(); DtoMapperWriter's REF case routes computed segments
 through extraFetchPaths instead of a broken select(assoc).

Fix #3 — requires() values never validated: Added DtoMappingReader.validateRequiresPath(...), walking every requires() segment against the real property graph (with List unwrapping via a new getterReturnTypeMirror helper). A typo now fails at compile time instead of resurfacing as a runtime PersistenceException. New negative

Fix #4 — bare requires() fetch vs narrowed sibling @DtoPath fetch collision: Traced into Ebean's OrmQueryDetail.fetch(...) and confirmed same-path fetch calls replace, not merge (Map.put). The old dedup logic had priority backwards, silently letting a narrow selection win and drop a computed getter's real dependency. Fixed by prioritizing the full fetch. Proved the regression test was meaningful by reverting the fix and confirming it fails with LazyInitialisationException: Property not loaded: line1, then restored the fix and confirmed it passes.
2026-07-16 21:36:17 +12:00
robin.bygrave 7b7a86201d dto mapping: support calculated getter methods with and without requires fetch properties 2026-07-16 20:38:03 +12:00
Rob Bygrave 5317fb0d5c Merge pull request #3856 from ebean-orm/feature/dto-failOnNull
dto mapping: Add @DtoPath( failOnNull=true) option for null -> primit…
2026-07-16 19:27:15 +12:00
robin.bygrave 022958f417 dto mapping: need ebean-annotation 8.7 2026-07-16 18:56:54 +12:00
robin.bygrave 3431eee81a dto mapping: Add @DtoPath( failOnNull=true) option for null -> primitive handling 2026-07-16 18:26:40 +12:00
robin.bygrave 470326830a Bump test versions to 18.3.0 2026-07-16 18:25:17 +12:00
Rob Bygraveandrobin.bygrave b6225b6ae4 InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated) (#3855)
* Version 18.3.0

* InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated)

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 17:14:40 +12:00
robin.bygrave f38f0ffa25 InsertOnConflict - change to exclude non-updatable and generated on insert only (e.g. @WhenCreated) 2026-07-16 17:12:46 +12:00
Rob Bygraveandrobin.bygrave 6c939d72f8 dto extensions - usingMaster, findStream etc (#3854)
* DtoQuery - add usingMaster, usingTransaction, usingConnection options.

* DtoQuery - Support DtoPath renaming a ToOne or ToMany

* MappedQuery - Add findStream() support

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 17:04:22 +12:00
robin.bygrave d083b27e91 Version 18.3.0 2026-07-16 16:57:52 +12:00
191 changed files with 6271 additions and 909 deletions
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>composites</artifactId>
+292 -2
View File
@@ -136,8 +136,31 @@ already recognise, which is worth spelling out explicitly so it's easy to "grok
or the query can't group correctly at all. Fixed so `REF` always contributes its association name to the
root `select(...)` (deduped against any existing `NESTED_ONE`/`NESTED_MANY` fetch of the same path).
### Read-only entity memory overhead: `InterceptReadOnly`
**Bug found and fixed (validation phase, testing against `central-access`): primitive-typed field +
nullable intermediate hop = unboxing `NullPointerException`.** A multi-hop `@DtoPath` (or `@DtoRef`,
which is always 2-hop) null-guards each intermediate getter with a ternary, e.g.
`(source.getOrganisation() == null ? null : source.getOrganisation().getId())`. That ternary's static
type is always the boxed wrapper (`Long`), since one branch is the `null` literal - fine when the DTO
field is itself a reference type (`Long organisationId`), but when the DTO field is a **primitive**
(`long organisationId`), passing that boxed expression to the constructor auto-unboxes it, throwing an
unhelpful `NullPointerException` at runtime whenever the relation really is `null`. This compiled clean
and only failed at runtime with real (nullable) production data - exactly the kind of gap a hand-written
mapper would defensively guard against (e.g. `cEbox.getOrganisation() == null ? 0 : ...getId()`) but
generated code didn't.
Fixed in the generator: when a multi-hop `SCALAR`/`REF` property's DTO field type is primitive, the
whole null-guarded chain is now wrapped in a small runtime helper (`io.ebean.DtoMapperSupport`) that
resolves it safely:
- **Default** (`@DtoPath` with no `failOnNull`, or any `@DtoRef`): silently defaults to the primitive's
zero-equivalent value (`0`/`false`/etc.) - matches the old hand-written-mapper convention.
- **`@DtoPath(failOnNull = true)`**: throws a clear `IllegalStateException` naming the offending property
path instead, for callers who'd rather fail fast than silently mask a null they don't expect.
`@DtoRef` has no `failOnNull` attribute (it has no other attributes at all) - it always uses the
default (silent zero) behaviour. See `PrimitiveNullPathDto`/`PrimitiveNullPathFailOnNullDto` /
`TestPrimitiveNullPath` for regression coverage.
### Read-only entity memory overhead: `InterceptReadOnly`
`setUnmodifiable(true)` isn't just a behavioural fail-fast flag - it also swaps the per-bean intercept
implementation to `InterceptReadOnly`, which is deliberately minimal: just a `boolean[] loaded` (one flag
per property) and a `boolean frozen`, plus the inherited owner reference and `fullyLoadedBean` flag. Compare
@@ -578,7 +601,7 @@ the chain.
### mapTo(Dto.class) runtime wiring (implemented)
`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`/
`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
`findStream()`/`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
On first use it resolves the generated `DtoMapper<S, D>` for the query's `(getBeanType(), dtoType)`
pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all generated
`DtoMapperRegister`s, analogous to `DtoBeanManager`), then:
@@ -599,6 +622,15 @@ caller retry against the master data source after a read-replica failure by call
`usingMaster(true)` on the *same* `MappedQuery` instance and re-invoking a find method - there's no
need to rebuild the query and call `.mapTo(...)` again.
`MappedQuery<D>.findStream()` mirrors `QueryBuilder#findStream()` - the underlying entity query is
streamed (supporting very large result sets, potentially using multiple persistence contexts
internally) and each entity is mapped to its target DTO lazily as the stream is consumed. One
`DtoMapContext` is shared across the whole stream (not per-element), so identity de-duplication of
nested DTOs (e.g. several `Contact`s sharing the same `Customer`) still holds even when the source
entities are never materialized into one `List` at all. As with the entity-level `findStream()`,
callers must consume it via try-with-resources to ensure the underlying resources are closed.
## Still open / to revisit during implementation
- Whether `.fetch(...)` calls can still be layered on top of a `mapTo(Dto.class)` query for explicit
@@ -692,6 +724,264 @@ need to rebuild the query and call `.mapTo(...)` again.
`DtoConverterManager`) are all constructed eagerly during `Database` startup, which can be
triggered by whichever test class in the module happens to run first.
- **Fixed (validation phase, found via `central-access`): `@DtoPath` through a computed/derived
getter now fails at compile time, with an explicit `requires()` escape hatch.** `@DtoPath`
assumes every dotted segment names a real, fetchable Ebean bean property - so a path like
`@DtoPath("currentMachine.organisationMachine.registrationPlate")`, where `getOrganisationMachine()`
is a hand-written derived getter (not a real relation/column), used to **compile cleanly** (the
codegen had no way to tell it apart from a real property from source alone) but **fail at
runtime** with a `PersistenceException: No property found for [organisationMachine] in
expression ...`, because the generated `FetchGroup` builder tried to `fetch`/`select` it as if it
were a real Ebean property.
- Two genuinely separate sub-problems: (1) *detecting* that a path segment isn't a real,
fetchable property - solvable at compile time, since a real persistent property always has a
backing field (Ebean requires one to enhance), checked via `javax.lang.model`
(`ElementFilter.fieldsIn(...)` over the type + superclass chain, see `DtoMappingReader.hasField(...)`);
versus (2) *knowing what the computed getter needs fetched* to execute safely - not solvable at
compile time without full static/bytecode analysis of the getter's method body, out of scope.
- Resolution: don't attempt to infer (2) automatically. When `DtoMappingReader` detects a `@DtoPath`
segment with no backing field, it now fails fast at compile time (`ctx.logError(...)`) unless the
developer explicitly declares the real entity paths that must be fetched via
`@DtoPath(requires = {...})` (dot-notation, same convention as `@DtoPath`'s own `value()`) - e.g.
`@DtoPath(value = "primaryContact.lastName", requires = "contacts")` where `getPrimaryContact()`
picks the first entry out of the `contacts` collection. The real prefix before the computed
segment (if any) is automatically combined with the declared `requires()` paths, so the developer
doesn't need to redundantly repeat it. Declared paths are emitted as bare `.fetch(path)` calls in
the generated `FetchGroup` (distinct from the `.fetch(path, "props")` shape used for ordinary
scalar `@DtoPath` properties, since there's no specific target property list to narrow to here).
- **The zero-extra-fetch case is also supported, via an explicit `requires = {}`** - e.g.
`@DtoPath(value = "idBadge", requires = {})` where `getIdBadge()` derives purely from `id`
(always fetched regardless). An explicit empty array confirms "nothing extra needed", distinct
from omitting `requires()` entirely ("not yet considered", still a compile error) - `requires()`
itself can't tell the two cases apart (both read back as an empty `List`), so `DtoMappingReader`
checks the avaje-prism-generated `DtoPathPrism.values.requires()` instead, which returns `null`
only when the member was left at its default (i.e. omitted from source). `DtoPropertyMeta`
correspondingly carries `hasComputedSegment()` as its own boolean flag (set whenever a computed
segment was detected at all), independent of whether `requiredFetchPaths()` happens to be empty -
an earlier version conflated the two (inferring "has a computed segment" from "has a non-empty
requiredFetchPaths list"), which broke exactly this explicit-empty case by falling through to the
ordinary scalar `.select(...)` path and failing at runtime with `PersistenceException: Property
not found - idBadge` (`idBadge` isn't a real Ebean property, so it can't be selected).
- Implemented in `ebean-annotation` (`DtoPath.requires()`), and `querybean-generator`
(`DtoMappingReader` computed-segment detection/validation, `DtoPropertyMeta.requiredFetchPaths()`/
`hasComputedSegment()`, `DtoMapperWriter.fetchGroupChainCalls()` bare-fetch emission). Test
coverage: `tests/test-dto-mapping` `ComputedPathDto`/`TestComputedPath` (happy path, `requires`
correctly fetches the dependency and the mapped value is correct), `ComputedPathNoFetchDto`/
`TestComputedPathNoFetch` (explicit `requires = {}`, genuinely nothing extra needed), and
`querybean-generator`'s `DtoMapperComputedPathTest` (negative case - omitting `requires` on a
computed segment is a compile-time `ERROR` diagnostic, verified via direct `javax.tools.JavaCompiler`
compilation, mirroring `DtoMapperFetchPathCollisionTest`).
- Known gap: the dedup between the computed segment's required fetch paths and existing
`pathSelect`/`nestedAssocPaths` keys in `DtoMapperWriter` is a simplified exact-path-string check
(skip emitting a duplicate `.fetch(path)`), not full collision detection like the existing
NESTED_ONE/MANY vs `@DtoPath` check - a bare `fetch(path)` and an existing `fetch(path,
"specific,props")` for the same path string are not merged/reconciled, just left as two separate
calls if that edge case arises.
- **Fixed: a single-hop `@DtoPath` rename through a computed/derived getter whose return type is
itself a registered nested DTO (`NESTED_ONE`/`NESTED_MANY`, not `SCALAR`) bypassed the
computed-segment validation above entirely.** E.g. `@DtoPath("primaryContact")` where the DTO
field's declared type is `ContactDto` (a type with its own `@DtoMapping(source = Contact.class,
target = ContactDto.class)`) and `getPrimaryContact()` is a computed getter with no backing
field on `Customer`. This resolves to a single-segment path, so `DtoMappingReader.resolveProperty()`
took its `properties.size() == 1` nested-lookup shortcut and returned early - before the
`computedFrom`/`requires()` validation block (added for the `SCALAR` case above) ever ran. The
generated `FetchGroup` then emitted a broken `fetch("primaryContact",
contactMapper.fetchGroup())` call (`"primaryContact"` isn't a real Ebean fetch path), failing at
runtime rather than compile time - the exact class of bug the `SCALAR` fix was meant to close off
entirely.
- Resolution: restructured `resolveProperty()` so the computed-segment detection/validation block
runs *before* the `properties.size() == 1` nested-lookup branch, so both `SCALAR` and
`NESTED_ONE`/`NESTED_MANY` paths share the same detection/validation. `DtoPropertyMeta` gained a
matching constructor overload for `NESTED_ONE`/`NESTED_MANY` carrying `computedSegment`/
`requiredFetchPaths`. In `DtoMapperWriter.fetchGroupChainCalls()`, a `NESTED_ONE`/`NESTED_MANY`
property with `hasComputedSegment()` true is routed into `extraFetchPaths` (the same bare
`.fetch(path)` mechanism as the `SCALAR` case) instead of emitting `fetch(path,
mapper.fetchGroup())` - since the nested mapper's own `FetchGroup` requirements can't be
meaningfully attached under a path name that doesn't exist on the source entity.
- Note the nested mapper's *own* fetch requirements (e.g. if `ContactDto` itself needed
`customer.billingAddress`) are **not** automatically propagated up through a computed segment -
only whatever the computed getter itself needs (via `requires()`) is fetched. The nested
mapper's `map(...)` call still works via plain Java method invocation regardless (Ebean
transparent lazy loading covers any gap), but relying on that silently reintroduces N+1 queries,
so the nested DTO used through a computed segment should ideally be a "leaf" shape needing
nothing beyond what `requires()` already declares.
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()` restructuring,
`DtoPropertyMeta`'s new constructor overload, `DtoMapperWriter.fetchGroupChainCalls()`). Test
coverage: `tests/test-dto-mapping` `ContactLeafDto`/`ComputedNestedDto`/`TestComputedNestedPath`
(happy path - generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken
`fetch("primaryContact", ...)` call, and the mapped value is correct end-to-end), and
`querybean-generator`'s `DtoMapperComputedPathTest#dtoPathThroughComputedGetter_targetingNestedDto_withoutRequires_expectCompileError`
(negative case, mirroring the `SCALAR` one). The `NESTED_MANY` variant (a computed getter
returning a `List` of a type with its own registered nested DTO mapping) shares the identical
code path but had no dedicated regression test until later confirmed via `Customer
.getRecentContacts()` / `ComputedNestedListDto` / `TestComputedNestedListPath` (coverage only,
not a bug fix - passed cleanly first try, confirming the shared code path does work end-to-end
for both `NESTED_ONE` and `NESTED_MANY`).
- **Fixed: `@DtoRef` never checked for a computed/derived association getter at all.** Unlike
`@DtoPath`, `@DtoRef`'s association name (derived by stripping the `Id` suffix off the field
name, e.g. `primaryContactId` -> `primaryContact`) was never checked against `hasField(...)` -
so `@DtoRef` on a computed getter (e.g. `getPrimaryContact()` picking the first entry out of a
`contacts` collection) compiled cleanly and generated a broken `FetchGroup.select("primaryContact")`
call (`"primaryContact"` isn't a real Ebean property), failing at runtime rather than compile
time - the same class of bug as the original `@DtoPath` fix, just entirely unaddressed for
`@DtoRef`'s separate code path.
- Resolution: `@DtoRef` gained its own `requires()` attribute (dot-notation, same convention and
explicit-empty semantics as `@DtoPath#requires()`, using the same `DtoRefPrism.values.requires()
== null` omitted-vs-explicit-empty technique). `DtoMappingReader`'s `@DtoRef` branch now checks
`hasField(meta.source(), assocName)` and fails fast at compile time (`ctx.logError(...)`) when
the association has no backing field and `requires()` wasn't specified. `DtoPropertyMeta`'s
`REF` properties now carry `computedSegment`/`requiredFetchPaths` through the existing fields
(no new constructor needed - the full constructor already had the right shape).
`DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case now checks `hasComputedSegment()` and
routes into `extraFetchPaths` (bare `.fetch(path)`) instead of `rootSelect.add(assoc)` when
true - the value expression itself (`source.getPrimaryContact().getId()`, null-guarded) is
unaffected, since it's plain Java method invocation regardless of whether the association name
is a real Ebean property.
- Implemented in `ebean-annotation` (`DtoRef.requires()`), and `querybean-generator`
(`DtoMappingReader`'s `@DtoRef` branch, `DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case).
Test coverage: `tests/test-dto-mapping` `ComputedRefDto`/`TestComputedRefPath` (happy path -
generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken `select("primaryContact")`
call, and the mapped id is correct end-to-end), and `querybean-generator`'s
`DtoMapperComputedPathTest#dtoRefThroughComputedGetter_withoutRequires_expectCompileError`
(negative case, mirroring the `@DtoPath` ones).
- **Fixed: `requires()` path values themselves were never validated against the source type's
real property graph.** `@DtoPath(requires = {...})`/`@DtoRef(requires = {...})` values are
handed straight through to `FetchGroup.fetch(...)` unmodified - a typo (e.g. `requires =
"contactz"` for the real `contacts` property) compiled cleanly, since only the *computed
segment itself* was checked against `hasField(...)`, not the developer-declared dependency
paths meant to fix it. That silently reintroduced the exact runtime `PersistenceException` the
whole `requires()` escape hatch exists to prevent, just one step removed and harder to spot.
- Resolution: added `DtoMappingReader.validateRequiresPath(...)`, which walks each dot-notation
segment of a declared `requires()` value from the source root (`meta.source()`), checking
`hasField(...)` at every hop exactly like `@DtoPath#value()`'s own segments are checked, and
unwrapping a `java.util.List`-typed intermediate hop to its element type (via
`listElementType(TypeMirror)`) so a collection segment followed by a further hop resolves
correctly - needed a new `getterReturnTypeMirror(...)` helper (returning the raw `TypeMirror`
rather than converting straight to `TypeElement`, which can't distinguish a `List` from any
other declared type) alongside the existing `getterReturnType(...)`. Called for every entry in
`pathPrism.requires()`/`refPrism.requires()` right after they're read, for both the `@DtoPath`
and `@DtoRef` branches. The already-validated real prefix (segments before the computed one in
a `@DtoPath#value()`) is intentionally *not* re-validated, since it was already checked while
walking `value()` itself.
- Implemented in `querybean-generator` (`DtoMappingReader.validateRequiresPath(...)`,
`getterReturnTypeMirror(...)`, called from both the `@DtoPath` and `@DtoRef` branches). Test
coverage: `querybean-generator`'s
`DtoMapperComputedPathTest#dtoPathRequires_withTypoInPathValue_expectCompileError` (negative
case - a typo'd `requires()` segment is a compile-time `ERROR` diagnostic); existing
`tests/test-dto-mapping`/`central-access` suites (real multi-segment `requires()` values like
`"currentMachine.organisationMachines"`) continue to pass unchanged, confirming the validation
doesn't false-positive on legitimate paths.
- **Fixed: a bare, full `requires()` fetch and a sibling property's narrowed `@DtoPath` fetch of
the exact same path silently conflicted, with the narrow one always (incorrectly) winning.**
`DtoMapperWriter.fetchGroupChainCalls()`'s dedup logic used to skip emitting a computed
segment's bare `fetch(path)` call whenever another property's `@DtoPath` already had a narrowed
`fetch(path, "specific,props")` entry for that exact path string - on the assumption the two
were interchangeable/redundant. They aren't: `FetchGroup`'s builder (`OrmQueryDetail.fetch(...)`)
keys fetch calls by path in a plain `Map` and **replaces** rather than merges same-path entries,
so whichever call format was emitted meant the *other* was silently discarded. Since the narrow
entry was always emitted first and the bare one skipped whenever it existed, the narrow selection
always won - meaning a computed getter's `requires()` declaration could be completely ignored
whenever an unrelated sibling `@DtoPath` happened to narrow-select the exact same path, leaving
whatever extra properties the computed getter actually touches unfetched (a silent lazy load, or
a hard `LazyInitialisationException` outside a persistence context).
- Resolution: reversed the priority - `fetchGroupChainCalls()` now skips a narrowed `pathSelect`
entry when `extraFetchPaths` (the computed segment's `requires()`) declares the exact same
path, letting the bare, full `fetch(path)` call win instead. This is always safe since a full
fetch is a superset of any narrower property selection - the narrow entry's own properties are
included within it regardless. The existing `nestedAssocPaths` priority (a `NESTED_ONE`/
`NESTED_MANY` property's full `fetch(path, mapper.fetchGroup())` always wins over a bare
`fetch(path)`) was correct already and left unchanged - a nested mapper's own `FetchGroup` is
strictly richer than either form and must not be replaced by either.
- Implemented in `querybean-generator` (`DtoMapperWriter.fetchGroupChainCalls()`). Test coverage:
`tests/test-dto-mapping` `FetchCollisionDto`/`TestFetchCollisionPath`, plus a new computed
getter `Customer.getBillingSummary()` (reads `billingAddress.getLine1()`, deliberately a
different `Address` property to the `city` narrowly selected by a sibling `@DtoPath` on the
same DTO) - confirmed to reproduce `LazyInitialisationException: Property not loaded: line1`
when the fix is reverted, and pass cleanly (correct `line1`-derived value, generated
`FetchGroup` is `.select("id").fetch("billingAddress")` with no narrowed variant at all) with
it in place.
- **Fixed: two `@DtoMixin` companion types targeting the same DTO class silently conflicted, with
the second-processed one winning.** `DtoMappingReader.collectMixins()` keyed a single
`mixinsByTarget` map by the target DTO's FQN, and `Map.put(...)` unconditionally overwrote any
existing entry - so if two mixin interfaces (e.g. a legitimate one plus an accidental duplicate,
or two independently-added mixins that both happened to target the same generated/unowned DTO)
both declared `@DtoMixin(SameDto.class)`, whichever was visited last by
`roundEnv.getElementsAnnotatedWith(...)` silently won, and *all* of the other mixin's
`@DtoPath`/`@DtoRef`/`@DtoConvert` overlays were discarded with no diagnostic at all.
- Resolution: `collectMixins()` now checks for an existing registration before storing a new one
and raises a compile `ERROR` naming both the target and the already-registered mixin's
qualified name, rather than silently overwriting it.
- Implemented in `querybean-generator` (`DtoMappingReader.collectMixins()`). Test coverage: new
negative compile-error test `DtoMapperComputedPathTest#duplicateDtoMixin_forSameTarget_expectCompileError`
(two minimal `@DtoMixin(FooDto.class)` interfaces both declaring a `bar()` method, compiled
together, asserting the `Duplicate @DtoMixin` diagnostic is raised); existing
`tests/test-dto-mapping` `TestDtoMixin` (single, legitimate mixin usage) continues to pass
unchanged.
- **Fixed: `@DtoRef` and `@DtoPath` both present on the same field silently conflicted, with
`@DtoRef` always (invisibly) winning.** `resolveProperty()` checked `refPrism != null` first and
returned immediately whenever present, so a field carrying both annotations at once - whether by
copy/paste mistake, a half-finished rename from one style to the other, or simple confusion
between the two escape hatches - had its `@DtoPath` completely ignored with no diagnostic at all.
- Resolution: `resolveProperty()` now resolves both prisms upfront and raises a compile `ERROR`
naming the field when both are present, rather than silently picking `@DtoRef` and discarding
`@DtoPath`.
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`). Test coverage: new
negative compile-error test `DtoMapperComputedPathTest#dtoRefAndDtoPath_onSameField_expectCompileError`
(a field carrying both `@DtoRef` and `@DtoPath("bar.id")` over a real, non-computed association,
isolating the conflict diagnostic from the separate computed-getter `requires()` diagnostics).
- **Fixed: `@DtoConvert` on a `NESTED_ONE`/`NESTED_MANY` property was silently ignored.**
`resolveProperty()` resolves the property's `DtoConverterMeta` unconditionally up front (before
it's known whether the property will resolve to `SCALAR`/`REF`/`NESTED_ONE`/`NESTED_MANY`), but
only the `SCALAR`/`REF` `DtoPropertyMeta` constructors actually accept/store a converter - the
`NESTED_ONE`/`NESTED_MANY` constructor calls never took one, so a resolved converter was simply
dropped on the floor with no diagnostic. A developer adding `@DtoConvert` to a nested-DTO field
(e.g. hoping to post-process the nested mapper's result) would see it silently do nothing -
`DtoMapperWriter.propertyValueExpression()`'s `NESTED_ONE`/`NESTED_MANY` cases call straight into
`mapperFieldName(property) + ".map(...)"`/`".mapList(...)"` with no converter wrapping at all.
- Resolution: added `rejectConverterOnNested(...)`, called at each of the four call sites that
construct a `NESTED_ONE`/`NESTED_MANY` `DtoPropertyMeta` (the single-hop `@DtoPath`-rename
branch's two cases, and the plain non-`@DtoPath` branch's two cases) - raises a compile `ERROR`
naming the field whenever a converter was resolved for it, rather than silently discarding it.
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`,
`rejectConverterOnNested()`). Test coverage: new negative compile-error test
`DtoMapperComputedPathTest#dtoConvertOnNestedOne_expectCompileError` (a `NESTED_ONE` field
carrying `@DtoConvert` over a legitimately nested, separately-`@DtoMapping`-registered type);
existing `tests/test-dto-mapping` suite (no nested property currently combines `@DtoConvert`
with `NESTED_ONE`/`NESTED_MANY`) continues to pass unchanged, confirming no false positives on
plain nested properties.
- **Fixed: `@DtoConvert(method = ...)` resolution ignored parameter arity/overloads.** The shared
`findMethod(type, name)` helper (also used for the builder's `build()` lookup and `@DtoMixin`
companion-method lookup) matches purely by simple name - the first `ExecutableElement` found -
with no arity or parameter-type check at all. For `@DtoConvert` specifically this is a real risk:
its documented contract is a method "taking the source property value and returning the
converted DTO property value" (i.e. exactly one parameter), but a shared/reusable conversion
utility class is a very plausible place to have multiple same-named overloads (e.g. `format
(Instant)` and `format(LocalDate)`) - `findMethod` would silently bind to whichever one
`ElementFilter.methodsIn` happened to return first, independent of which one the developer
actually meant, generating either a confusing arity/type-mismatch compile error in the generated
mapper or, if both overloads happened to be call-compatible, silently invoking the wrong one.
- Resolution: added a dedicated `findConverterMethod(...)` (used only by `resolveConverter()`,
leaving the shared `findMethod()` untouched for the builder/mixin call sites which have their
own, different arity expectations) that filters same-named candidates down to those taking
exactly one parameter. Zero matches raises a clear "not found ... taking exactly one
parameter" error; more than one match (multiple 1-arg overloads sharing the name) raises an
"ambiguous - N overloads take exactly one parameter" error, since `@DtoConvert` has no
parameter-type-based way to disambiguate and the developer must rename one of the overloads.
- Implemented in `querybean-generator` (`DtoMappingReader.resolveConverter()`,
`findConverterMethod()`). Test coverage: new negative compile-error tests
`DtoMapperComputedPathTest#dtoConvertMethod_withAmbiguousOverloads_expectCompileError` (two
same-named 1-arg overloads) and `#dtoConvertMethod_withWrongArity_expectCompileError` (a
same-named 0-arg method, no 1-arg candidate at all); existing `tests/test-dto-mapping`
converter usage (a single, unambiguous 1-arg method per converter type) continues to resolve
and pass unchanged.
## References
+67
View File
@@ -139,6 +139,35 @@ while keeping DTOs as plain, framework-unattached classes.
querybean-generator codegen support (static/instance dispatch, constructor wiring deduplicated by converter
type). Test coverage: `tests/test-dto-mapping` `TestDtoConvert`.*
- **Type-pair (package-level) custom scalar conversion**
Motivated by real hand-written mapper code (`EboxMapper`, central-access): the same conversion repeats
across many unrelated properties on one target - `DateUtils.toCalendar(...)` on ~9 fields,
`parseEnum(EnumType.class, value)` on ~3 - under today's `@DtoConvert` every one of those properties must
carry its own repeated annotation. MapStruct solves this by letting a conversion method be defined once
(in the mapper or a `uses = {...}` helper) and auto-applying it to *every* property whose source/target
types match that method's signature - no per-field wiring. Proposed: a package-level, repeatable
`@DtoConverters({ConverterType.class, ...})` (sibling to `@DtoMapping` in `package-info.java`) - the
generator indexes every public static/instance method on the referenced type(s) by `(paramType ->
returnType)`, then for any property whose source getter type doesn't already match the target field type
and which carries no explicit per-property `@DtoConvert`, looks up that type pair and wires it in
automatically (same static-vs-instance/`DtoConverterManager` dispatch rules as `@DtoConvert` today). An
explicit per-property `@DtoConvert` always overrides the type-level default. Deliberately no built-in
conversions shipped by Ebean itself (no implicit `Enum.valueOf`/`.name()`) - the app still owns
exception/null-handling semantics (e.g. `parseEnum`'s catch-and-null-on-bad-value), just declares it once
instead of per-field.
**Status: implemented.** `@DtoConverters(ConverterType.class, ...)` (a single non-repeatable annotation
taking a `Class<?>[]`, `@Target({PACKAGE, MODULE})`) is registered once per package/module alongside
`@DtoMapping`. The generator indexes every public, single-arg, non-void method on each referenced type by
exact `(paramType -> returnType)`; any SCALAR property (plain or `@DtoPath`-renamed) with no explicit
`@DtoConvert` and a source/target type mismatch is auto-wired to the matching method (a duplicate/ambiguous
type pair across the registered types is a compile-time processor error). List-element-wise conversion and
`@DtoRef` (FK-id) properties are out of scope. Test coverage:
`tests/test-dto-mapping/.../TestDtoConverters.java` (`UuidConverters`/`UuidShortCodeConverter`,
`ContactTypeConverterDto`) - covers same-name auto-dispatch, `@DtoPath`-renamed auto-dispatch, and explicit
`@DtoConvert` overriding the registered default.
*Inspiration: `EboxMapper` (central-access) hand-written pattern; MapStruct type-signature-matched
conversion methods.*
- **`@DtoMixin` for DTOs that cannot be annotated directly**
Some DTOs are generated (e.g. from an OpenAPI spec) and not editable/annotatable, so `@DtoPath`/
`@DtoConvert`/`@DtoRef` cannot always be placed directly on the DTO. Introduce a `@DtoMixin(Target.class)`
@@ -229,6 +258,44 @@ instead (see "Recipe: adding extra caller-supplied fields after mapping" in
*Inspiration: `UserService`/`User` (central-access).*
*Status: implemented.*
- **Setter-based (mutable JavaBean) target construction**
Motivated by `EboxMapper` (central-access): its target types (`Ebox`, `MachineSummaryInfo`, from
`nz.co.eroad.schema.eroadtypes`, JAXB/XSD-generated legacy SOAP shapes) are plain mutable JavaBeans - a
public no-arg constructor plus a `void setXxx(...)` setter per property - neither a positional constructor
match nor a RecordBuilder-style fluent builder (see section G above). The generator currently only
recognizes those two construction strategies, so this common third shape (typical of JAXB/XSD-generated
and many hand-written mutable POJOs) can't be targeted by `@DtoMapping` at all today. Proposed: detect a
no-arg constructor plus a `void setXxx(propertyType)` setter per mapped property as a third construction
strategy, generating `Target target = new Target(); target.setX(...); ...; return target;` (mirroring the
existing `build = AUTO | ALWAYS | NEVER` override precedent from section G for explicit control over which
strategy applies). Would also unblock the `mapToBuilder()`-style "populate ignored/derived properties after
the generated mapping, before finishing construction" pattern for these targets (currently only available
for builder-shaped targets) - relevant to `EboxMapper`'s `machineSummaryInfo` (a genuinely composite,
multi-association derived value, out of reach of `@DtoConvert`/`@DtoPath` regardless of this gap, but a
natural fit for the same "map base fields via codegen, then set the derived one by hand" pattern already
used for `Fleet.assignedMachines`/`assignedDrivers`).
*Inspiration: `EboxMapper` (central-access); JAXB/XSD-generated SOAP DTO shapes generally.*
**Status: implemented.** `@DtoMapping(setter = AUTO | ALWAYS | NEVER)` mirrors `builder()`'s override
precedent. Detection requires a public no-arg constructor plus a public `setXxx(...)` setter for every
mapped property - either `void` or fluent-style (returning the target type itself, e.g. `public Target
setXxx(...) { ...; return this; }`); the generated code always calls the setter as a bare statement and
discards any return value, so either shape works identically. A builder, when selected, always takes
priority over setter-based construction. Under the default `AUTO`, setter-based construction is only
attempted when the target has no positional constructor matching the mapped properties (arity-based) and
no builder was selected - existing positional-constructor and builder-shaped targets are entirely
unaffected. `ALWAYS` requires the shape (codegen-time error otherwise); `NEVER` always uses a positional
constructor. Generated shape: `Target target = new Target(); target.setX(...); ...; return target;` (a
`computeIfAbsent(...)`-wrapped block-lambda variant when the target is nested elsewhere in the graph).
Deliberately **no** `mapToBuilder(...)`-style post-construction accessor is generated for this strategy -
the returned target is already the final, fully mutable instance (setters are required to be `public`), so
a caller can already call e.g. `dto.setExternalRef(...)` directly on the mapped result, exactly the pattern
`EboxMapper` already uses by hand; this is unlike the builder strategy, where the intermediate builder is
otherwise unreachable after its one-shot `build()` call. Test coverage:
`tests/test-dto-mapping/.../TestDtoSetterConstruction.java` (`ContactSetterDto`) - covers auto-detected
setter-chain construction plus post-construction population of two `@DtoIgnore` properties (a plain scalar
and a `List`) via their public setters; plus `ContactSetterFluentDto` - covers the fluent-setter-return-shape
variant.
### H. Record entity sources
- **Record-style (bare/fluent) accessors on the source (entity) side**
+96 -26
View File
@@ -1,40 +1,39 @@
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
# Guide: `@DbJson` / `@DbJsonB` mapping support
## Purpose
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
Ebean can map `@DbJson` and `@DbJsonB` properties in three 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.
- **Jackson `ObjectMapper`**, provided by **`ebean-jackson-mapper`**.
- **Avaje Jsonb**, provided by **`ebean-avajejsonb-mapper`** and its generated adapters.
This guide lists exactly which property types are handled built-in and which require
`ebean-jackson-mapper`.
The built-in mapper handles the common natural JSON types. Add one mapper module for
typed collections, POJOs, records, and other custom types.
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
> classpath, Ebean fails fast at startup:
> If a property type is **not** handled built-in and no mapper module is on the classpath,
> Ebean fails fast at startup:
>
> ```text
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
> Jackson ObjectMapper not present for <property>
> Unsupported @DbJson mapping - missing JSON mapper dependency for <property>
> ```
---
## Quick reference
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|---|:---:|:---:|
| Property type | Built-in (avaje-json-core) | Mapper module |
|---|:---:|---|
| `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 | | |
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | Jackson or Avaje Jsonb |
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | Jackson or Avaje Jsonb |
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | Jackson or Avaje Jsonb |
| POJOs, records, or any other type | | Jackson or Avaje Jsonb |
---
@@ -59,10 +58,10 @@ Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
---
## Everything else → Jackson `ObjectMapper`
## Jackson `ObjectMapper`
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
requires `ebean-jackson-mapper`:
`ebean-jackson-mapper` uses a Jackson `ObjectMapper` for the property types not handled
built-in:
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
@@ -77,7 +76,7 @@ requires `ebean-jackson-mapper`:
---
## Adding `ebean-jackson-mapper`
### Adding `ebean-jackson-mapper`
```xml
<dependency>
@@ -92,6 +91,79 @@ registers the mapper-based JSON support automatically.
---
## Avaje Jsonb
`ebean-avajejsonb-mapper` uses Avaje Jsonb adapters. Annotate each JSON payload type with
`@Json`, or use `@Json.Import`, and configure `avaje-jsonb-generator` as an annotation
processor.
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-avajejsonb-mapper</artifactId>
<version>${ebean.version}</version>
</dependency>
```
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-jsonb-generator</artifactId>
<version>${avaje-jsonb.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```
For example:
```java
import io.avaje.jsonb.Json;
@Json
public class Address {
public String line1;
public String city;
}
```
Avaje Jsonb preserves a property's declared generic type, so `List<Address>` and other
parameterised JSON values use the generated `Address` adapter.
### Avaje JsonNode
`@DbJson` and `@DbJsonB` properties declared as `io.avaje.json.node.JsonNode` are supported
when the application includes `avaje-json-node`. Its Jsonb component supplies the JSON tree
adapters; no application-generated adapter is needed for the node hierarchy.
```xml
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-node</artifactId>
<version>${avaje-jsonb.version}</version>
</dependency>
```
## Mapper module selection
Use exactly one mapper module in an application: `ebean-jackson-mapper` or
`ebean-avajejsonb-mapper`. Ebean selects a single `ScalarJsonMapper` service provider, so
having both modules on the runtime classpath is not supported.
The `ebean` composite dependency includes `ebean-jackson-mapper`. Applications using Avaje
Jsonb should depend on the individual Ebean modules they need instead of that composite.
To migrate from Jackson to Avaje Jsonb, remove `ebean-jackson-mapper`, add
`ebean-avajejsonb-mapper`, and generate adapters for each JSON payload type.
---
## Notes
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
@@ -102,15 +174,13 @@ registers the mapper-based JSON support automatically.
(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.
only produces natural JSON types — for example a JSON number always parses to `Long`.
---
## 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.
object/string maps) to avoid an additional mapper module.
- Add **`ebean-jackson-mapper`** or **`ebean-avajejsonb-mapper`** when you need rich POJO JSON
columns or typed collections / typed-value maps.
+6 -1
View File
@@ -376,7 +376,12 @@ public class Customer {
**Important:**
- Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs)
- `mappedBy` means Order.customer is the owner
- Relationships are lazy-loaded by default
- Relationships are lazy-loaded by default**except** `@OneToOne(mappedBy=...)`,
which defaults to `FetchType.EAGER` and adds a `left join` to every default
select of the owning entity. Explicitly set `fetch = FetchType.LAZY` on
`@OneToOne(mappedBy=...)` fields unless the association is needed on
(almost) every load. See "`@OneToOne(mappedBy=...)` is EAGER by default" in
the query-beans guide for details.
---
+42
View File
@@ -431,6 +431,48 @@ List<Customer> customers = new QCustomer()
If the caller needs multiple to-many paths or a paged query, be suspicious of a
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
### `@OneToOne(mappedBy=...)` is EAGER by default — mark it LAZY
The non-owning side of a `@OneToOne` (the side with `mappedBy`) defaults to
`FetchType.EAGER` per JPA, same as `@ManyToOne`. Unlike a `@ManyToOne`
reference (which is FK-only until `.fetch()`'d), Ebean's default select for an
EAGER `@OneToOne(mappedBy=...)` still adds a `left join` to the target table
on **every** query for the owning entity — even a plain `findById()` — because
there is no local FK column to use as a lazy reference; the only way to know
the associated row exists is to join to it.
If that association is rarely needed (e.g. a rarely-read child/detail table),
this join executes on every load of the parent, including in hot-path list
queries, and can dominate query cost as more such associations accumulate.
**Always set `fetch = FetchType.LAZY` on `@OneToOne(mappedBy=...)`
associations unless the association is genuinely needed on (almost) every
load:**
```java
@OneToOne(mappedBy = "device", fetch = FetchType.LAZY)
private SensorBoard sensorBoard;
```
This correctly excludes the join from Ebean's default select clause (verified
for FK-based, non-shared-primary-key `@OneToOne` relationships — the common
case). Callers that do need the association can still `.fetch("sensorBoard")`
explicitly on the query bean.
**Caveat:** the exclusion is driven by Ebean's default-select-clause
mechanism. It is bypassed if the query has already been switched into an
"all properties" mode by something other than the deploy-time
`FetchType.LAZY`/`EAGER` metadata (for example, an active AutoTune profile
that supplies its own tuned property set). Confirm the join is actually gone
by checking generated SQL (`LoggedSql` in tests, or query logging) after
making this change — don't assume it's excluded from the annotation alone.
### Agent rule
Default new `@OneToOne(mappedBy=...)` fields to `fetch = FetchType.LAZY`
unless there's a clear reason the association is needed on every load. This
is a one-line, low-risk change that avoids an always-on join.
---
## Step 8 - Use DTO projection when the caller does not need entity beans
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean api</name>
@@ -0,0 +1,137 @@
package io.ebean;
/**
* Runtime helpers used by generated {@link DtoMapper} implementations to safely resolve a
* primitive-typed DTO field whose value is derived from a multi-hop {@code @DtoPath} that
* traverses a nullable intermediate relation.
* <p>
* A {@code null}-guarded getter-chain (e.g. {@code source.getOrganisation() == null ? null :
* source.getOrganisation().getId()}) always types as the boxed wrapper (since one branch is the
* {@code null} literal). When the DTO's target field is a primitive (e.g. {@code long
* organisationId}), passing that boxed expression to the constructor auto-unboxes it - which
* throws a raw, unhelpful {@link NullPointerException} if the relation really is {@code null}.
* <p>
* These methods give the generated mapper a choice, controlled by {@code @DtoPath#failOnNull()}:
* default to the primitive's zero-equivalent value ({@code orZero} methods, the default), or
* throw a clear, descriptive exception naming the offending property path ({@code require}
* methods, opted into via {@code failOnNull = true}).
*
* @see io.ebean.annotation.DtoPath
*/
public final class DtoMapperSupport {
private DtoMapperSupport() {
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static long orZero(Long value) {
return value == null ? 0L : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static int orZero(Integer value) {
return value == null ? 0 : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static short orZero(Short value) {
return value == null ? 0 : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static byte orZero(Byte value) {
return value == null ? 0 : value;
}
/** Return {@code 0.0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static double orZero(Double value) {
return value == null ? 0.0 : value;
}
/** Return {@code 0.0f} if {@code value} is {@code null}, otherwise its unboxed value. */
public static float orZero(Float value) {
return value == null ? 0.0f : value;
}
/** Return {@code false} if {@code value} is {@code null}, otherwise its unboxed value. */
public static boolean orZero(Boolean value) {
return value != null && value;
}
/** Return {@code '\u0000'} if {@code value} is {@code null}, otherwise its unboxed value. */
public static char orZero(Character value) {
return value == null ? '\u0000' : value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static long require(Long value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static int require(Integer value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static short require(Short value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static byte require(Byte value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static double require(Double value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static float require(Float value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static boolean require(Boolean value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static char require(Character value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
private static IllegalStateException failure(String path) {
return new IllegalStateException(
"@DtoPath(\"" + path + "\") resolved to null via a nullable intermediate relation, but the"
+ " target DTO field is primitive and failOnNull=true - either handle the null case in"
+ " source data, use a boxed wrapper type for the DTO field, or remove failOnNull to"
+ " default to the primitive's zero-equivalent value instead.");
}
}
+2 -95
View File
@@ -1,16 +1,9 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Query for performing native SQL queries that return DTO Bean's.
@@ -43,12 +36,7 @@ import java.util.stream.Stream;
* }</pre>
*/
@NullMarked
public interface DtoQuery<T> extends CancelableQuery {
/**
* Execute the query returning a list.
*/
List<T> findList();
public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
/**
* Execute the query iterating a row at a time.
@@ -59,56 +47,6 @@ public interface DtoQuery<T> extends CancelableQuery {
*/
QueryIterator<T> findIterate();
/**
* Execute the query returning a Stream.
* <p>
* Note that the Stream holds resources related to the underlying
* resultSet and potentially connection and MUST be closed. We should use
* the Stream in a <em>try with resource block</em>.
*/
Stream<T> findStream();
/**
* Execute the query iterating a row at a time.
* <p>
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
* </p>
*/
void findEach(Consumer<T> consumer);
/**
* Execute the query iterating the results and batching them for the consumer.
* <p>
* This runs like findEach streaming results from the database but just collects the results
* into batches to pass to the consumer.
*
* @param batch The number of dto beans to collect before given them to the consumer
* @param consumer The consumer to process the batch of DTO beans
*/
void findEach(int batch, Consumer<List<T>> consumer);
/**
* Execute the query iterating a row at a time with the ability to stop consuming part way through.
* <p>
* Returning false after processing a row stops the iteration through the query results.
* </p>
* <p>
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
* </p>
*/
void findEachWhile(Predicate<T> consumer);
/**
* Execute the query returning a single bean.
*/
@Nullable
T findOne();
/**
* Execute the query returning an optional bean.
*/
Optional<T> findOneOrEmpty();
/**
* Bind all the parameters using index positions.
* <p>
@@ -214,38 +152,6 @@ public interface DtoQuery<T> extends CancelableQuery {
*/
DtoQuery<T> setBufferFetchSizeHint(int bufferFetchSizeHint);
/**
* Use the explicit transaction to execute the query.
*/
DtoQuery<T> usingTransaction(Transaction transaction);
/**
* Execute the query using the given connection.
*/
DtoQuery<T> usingConnection(Connection connection);
/**
* Ensure that the master DataSource is used if there is a read only data source
* being used (that is using a read replica database potentially with replication lag).
* <p>
* When the database is configured with a read-only DataSource via
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)} then
* by default when a query is run without an active transaction, it uses the read-only data
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
default DtoQuery<T> usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
DtoQuery<T> usingMaster(boolean useMaster);
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
@@ -281,6 +187,7 @@ public interface DtoQuery<T> extends CancelableQuery {
*
* @return The PagedList
*/
@Override
PagedList<T> findPagedList();
}
@@ -10,6 +10,7 @@ import java.sql.Timestamp;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* List of Expressions that make up a where or having clause.
@@ -421,6 +422,26 @@ public interface ExpressionList<T> {
*/
Optional<T> findOneOrEmpty();
/**
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
* if there is no matching bean.
*
* @see Query#findOneOrThrow()
*/
default T findOneOrThrow() {
return query().findOneOrThrow();
}
/**
* Execute the query returning a single bean or throwing the exception produced by the
* given supplier if there is no matching bean.
*
* @see Query#findOneOrThrow(Supplier)
*/
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
return query().findOneOrThrow(exceptionSupplier);
}
/**
* Execute find row count query in a background thread.
* <p>
@@ -0,0 +1,89 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import jakarta.persistence.EntityNotFoundException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Common find operations shared by the query types that can execute and return
* results - {@link SqlQuery}, {@link DtoQuery}, {@link MappedQuery} and {@link QueryBuilder}.
*
* @param <SELF> The query type (used for method chaining)
* @param <T> The type of the result
*/
@NullMarked
public interface FindableQuery<SELF extends FindableQuery<SELF, T>, T> extends CancelableQuery {
/**
* Execute the query returning the list of results.
*/
List<T> findList();
/**
* Execute the query returning a single result, or {@code null} if there is no matching row.
* <p>
* If more than 1 row is found for this query then a PersistenceException is thrown.
*/
@Nullable
T findOne();
/**
* Execute the query returning an optional result.
*/
Optional<T> findOneOrEmpty();
/**
* Execute the query returning a single result or throwing a
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
*/
default T findOneOrThrow() {
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
}
/**
* Execute the query returning a single result or throwing the exception produced
* by the given supplier if there is no matching row.
*/
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
return findOneOrEmpty().orElseThrow(exceptionSupplier);
}
/**
* Execute the query using the given transaction.
*/
SELF usingTransaction(Transaction transaction);
/**
* Execute the query using the given connection.
*/
SELF usingConnection(Connection connection);
/**
* Ensure that the master DataSource is used if there is a read only data source
* being used (that is using a read replica database potentially with replication lag).
* <p>
* When the database is configured with a read-only DataSource via
* say {@link DatabaseBuilder#readOnlyDataSource(DataSource)} then
* by default when a query is run without an active transaction, it uses the read-only data
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
default SELF usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
SELF usingMaster(boolean useMaster);
}
@@ -1,11 +1,11 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.sql.Connection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Query that maps an entity graph query result to a nested DTO graph, produced by
@@ -21,11 +21,12 @@ import java.util.Optional;
* @param <D> the target DTO type
*/
@NullMarked
public interface MappedQuery<D> {
public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
/**
* Execute the query returning the mapped DTO list.
*/
@Override
List<D> findList();
/**
@@ -37,33 +38,74 @@ public interface MappedQuery<D> {
* ({@link PagedList#getTotalCount()}, {@link PagedList#hasNext()}, etc.) reflects the
* underlying entity query and is unaffected by the DTO mapping.
*/
@Override
PagedList<D> findPagedList();
/**
* Execute the query returning a single mapped DTO, or {@code null} if there is no matching row.
* Execute the query returning the result as a Stream of mapped DTOs.
* <p>
* Mirrors {@link QueryBuilder#findStream()} - the underlying entity graph query is streamed
* (supporting very large queries iterating any number of results, potentially using multiple
* persistence contexts internally) and each entity is mapped to its target DTO lazily as the
* stream is consumed, sharing one {@link DtoMapContext} across the whole stream so that
* repeated references to the same source entity still de-duplicate to the same DTO instance.
* <pre>{@code
*
* // use try with resources to ensure Stream is closed
*
* try (Stream<CustomerDto> stream = query.mapTo(CustomerDto.class).findStream()) {
* stream
* .map(...)
* .collect(...);
* }
*
* }</pre>
*/
@Nullable
D findOne();
@Override
Stream<D> findStream();
/**
* Execute the query returning an optional mapped DTO.
* Execute the query processing the mapped DTOs one at a time.
* <p>
* Mirrors {@link QueryBuilder#findEach(Consumer)} - the underlying entity graph query is
* streamed one entity at a time and each entity is mapped to its target DTO lazily as it is
* consumed, sharing one {@link DtoMapContext} across the whole callback so that repeated
* references to the same source entity still de-duplicate to the same DTO instance.
* <p>
* This method is appropriate to process very large query results as the mapped DTOs are
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
*
* @param consumer the consumer used to process the mapped DTOs.
*/
Optional<D> findOneOrEmpty();
@Override
void findEach(Consumer<D> consumer);
/**
* Execute findEach streaming query batching the mapped DTOs for consuming.
* <p>
* Mirrors {@link QueryBuilder#findEach(int, Consumer)} - typically used when we want to do
* further processing on the mapped DTOs in batch form, for example 100 at a time. Each batch
* shares one {@link DtoMapContext} with the rest of the query so that repeated references to
* the same source entity still de-duplicate to the same DTO instance.
*
* @param batch The number of mapped DTOs processed in the batch
* @param consumer Process the batch of mapped DTOs
*/
@Override
void findEach(int batch, Consumer<List<D>> consumer);
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
* Execute the query using callbacks to process the resulting mapped DTOs one at a time,
* with the ability to stop processing part way through.
* <p>
* Mirrors {@link QueryBuilder#findEachWhile(Predicate)} - returning {@code false} after
* processing a DTO stops the iteration through the query results. Sharing one
* {@link DtoMapContext} across the whole callback so that repeated references to the same
* source entity still de-duplicate to the same DTO instance.
*
* @param consumer the consumer used to process the mapped DTOs, returning {@code false} to
* stop processing.
*/
MappedQuery<D> usingMaster(boolean useMaster);
/**
* Use the explicit transaction to execute the query.
*/
MappedQuery<D> usingTransaction(Transaction transaction);
/**
* Execute the query using the given connection.
*/
MappedQuery<D> usingConnection(Connection connection);
@Override
void findEachWhile(Predicate<D> consumer);
}
+1 -1
View File
@@ -151,7 +151,7 @@ import org.jspecify.annotations.Nullable;
* @param <T> the type of Entity bean this query will fetch.
*/
@NullMarked
public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
public interface Query<T> extends QueryBuilder<Query<T>, T> {
/**
* The lock type (strength) to use with query FOR UPDATE row locking.
@@ -2,8 +2,7 @@ package io.ebean;
import org.jspecify.annotations.Nullable;
import javax.sql.DataSource;
import java.sql.Connection;
import jakarta.persistence.EntityNotFoundException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
@@ -11,8 +10,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Build and execute an ORM query.
@@ -20,7 +17,7 @@ import java.util.stream.Stream;
* @param <SELF> The type of the builder
* @param <T> The entity bean type
*/
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T> {
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T>, StreamableQuery<SELF, T> {
/**
* Set root table alias.
@@ -146,48 +143,16 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
*/
SELF copy();
/**
* Execute the query using the given transaction.
*/
SELF usingTransaction(Transaction transaction);
/**
* Execute this query using immutable bean cache values for matching bean types.
*/
SELF using(ImmutableBeanCache<?> beanCache);
/**
* Execute the query using the given connection.
*/
SELF usingConnection(Connection connection);
/**
* Execute the query using the given database.
*/
SELF usingDatabase(Database database);
/**
* Ensure that the master DataSource is used if there is a read only data source
* being used (that is using a read replica database potentially with replication lag).
* <p>
* When the database is configured with a read-only DataSource via
* say {@link io.ebean.config.DatabaseConfig#setReadOnlyDataSource(DataSource)} then
* by default when a query is run without an active transaction, it uses the read-only data
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
default SELF usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
SELF usingMaster(boolean useMaster);
/**
* Set the base table to use for this query.
* <p>
@@ -679,88 +644,24 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
boolean exists();
/**
* Execute the query returning either a single bean or null (if no matching
* bean is found).
* <p>
* If more than 1 row is found for this query then a PersistenceException is
* thrown.
* <p>
* This is useful when your predicates dictate that your query should only
* return 0 or 1 results.
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
* if there is no matching bean.
* <p>
* This is a convenience alternative to:
* <pre>{@code
*
* // assuming the sku of products is unique...
* Product product =
* new QProduct()
* .sku.equalTo("aa113")
* .findOne();
* ...
* query.findOneOrEmpty()
* .orElseThrow(() -> new EntityNotFoundException(...));
* }</pre>
* <p>
* It is also useful with finding objects by their id when you want to specify
* further join information to optimise the query.
* <p>
* <pre>{@code
*
* // Fetch order 42 and additionally fetch join its order details...
* Order order =
* new QOrder()
* .fetch("details") // eagerly load the order details
* .id.equalTo(42)
* .findOne();
*
* // the order details were eagerly loaded
* List<OrderDetail> details = order.getDetails();
* ...
* }</pre>
* The exception message is a best effort - it uses the id when this is effectively a
* find-by-id query, or the single equality predicate when the query is filtered by what
* looks like a natural/unique key, otherwise a generic "not found" message.
*/
@Nullable
T findOne();
/**
* Execute the query returning an optional bean.
*/
Optional<T> findOneOrEmpty();
/**
* Execute the query returning the list of objects.
* <p>
* This query will execute against the EbeanServer that was used to create it.
* <p>
* <pre>{@code
*
* List<Customer> customers =
* new QCustomer()
* .name.ilike("rob%")
* .findList();
*
* }</pre>
*
* @see Query#findList()
*/
List<T> findList();
/**
* Execute the query returning the result as a Stream.
* <p>
* Note that this can support very large queries iterating
* any number of results. To do so internally it can use
* multiple persistence contexts.
* </p>
* <pre>{@code
*
* // use try with resources to ensure Stream is closed
*
* try (Stream<Customer> stream = query.findStream()) {
* stream
* .map(...)
* .collect(...);
* }
*
* }</pre>
*/
Stream<T> findStream();
@Override
default T findOneOrThrow() {
return findOneOrEmpty().orElseThrow(() ->
new EntityNotFoundException(getBeanType().getSimpleName() + " not found"));
}
/**
* Execute the query returning the set of objects.
@@ -905,84 +806,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
*/
<A> Set<A> findSingleAttributeSet();
/**
* Execute the query processing the beans one at a time.
* <p>
* This method is appropriate to process very large query results as the
* beans are consumed one at a time and do not need to be held in memory
* (unlike #findList #findSet etc)
* <p>
* Note that internally Ebean can inform the JDBC driver that it is expecting larger
* resultSet and specifically for MySQL this hint is required to stop it's JDBC driver
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
* generally preferable.
* <p>
* Compared with #findEachWhile this will always process all the beans where as
* #findEachWhile provides a way to stop processing the query result early before
* all the beans have been read.
* <p>
* This method is functionally equivalent to findIterate() but instead of using an
* iterator uses the Consumer interface which is better suited to use with closures.
*
* <pre>{@code
*
* new QCustomer()
* .status.equalTo(Status.NEW)
* .orderBy().id.asc()
* .findEach((Customer customer) -> {
*
* // do something with customer
* System.out.println("-- visit " + customer);
* });
*
* }</pre>
*
* @param consumer the consumer used to process the queried beans.
*/
void findEach(Consumer<T> consumer);
/**
* Execute findEach streaming query batching the results for consuming.
* <p>
* This query execution will stream the results and is suited to consuming
* large numbers of results from the database.
* <p>
* Typically, we use this batch consumer when we want to do further processing on
* the beans and want to do that processing in batch form, for example - 100 at
* a time.
*
* @param batch The number of beans processed in the batch
* @param consumer Process the batch of beans
*/
void findEach(int batch, Consumer<List<T>> consumer);
/**
* Execute the query using callbacks to a visitor to process the resulting
* beans one at a time.
* <p>
* This method is functionally equivalent to findIterate() but instead of using an
* iterator uses the Predicate interface which is better suited to use with closures.
*
* <pre>{@code
*
* new QCustomer()
* .status.equalTo(Status.NEW)
* .orderBy().id.asc()
* .findEachWhile((Customer customer) -> {
*
* // do something with customer
* System.out.println("-- visit " + customer);
*
* // return true to continue processing or false to stop
* return (customer.getId() < 40);
* });
*
* }</pre>
*
* @param consumer the consumer used to process the queried beans.
*/
void findEachWhile(Predicate<T> consumer);
/**
* Return versions of a @History entity bean.
* <p>
@@ -1048,33 +871,4 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
*/
<K> FutureMap<K,T> findFutureMap();
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
* <p>
* <pre>{@code
*
* PagedList<Order> pagedList =
* new QOrder()
* .setFirstRow(50)
* .setMaxRows(20)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadRowCount();
*
* List<Order> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalRowCount();
*
* }</pre>
*
* @return The PagedList
*/
PagedList<T> findPagedList();
}
+13 -55
View File
@@ -3,7 +3,6 @@ package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import javax.sql.DataSource;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Collection;
@@ -41,44 +40,7 @@ import java.util.function.Predicate;
* }</pre>
*/
@NullMarked
public interface SqlQuery extends Serializable, CancelableQuery {
/**
* Execute the query using the given transaction.
*/
SqlQuery usingTransaction(Transaction transaction);
/**
* Execute the query using the given connection.
*/
SqlQuery usingConnection(Connection connection);
/**
* Ensure that the master DataSource is used if there is a read only data source
* being used (that is using a read replica database potentially with replication lag).
* <p>
* When the database is configured with a read-only DataSource via
* say {@link io.ebean.DatabaseBuilder#readOnlyDataSource(DataSource)}then
* by default when a query is run without an active transaction, it uses the read-only data
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
default SqlQuery usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
SqlQuery usingMaster(boolean useMaster);
/**
* Execute the query returning a list.
*/
List<SqlRow> findList();
public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow> {
/**
* Execute the SqlQuery iterating a row at a time.
@@ -99,16 +61,6 @@ public interface SqlQuery extends Serializable, CancelableQuery {
*/
void findEachWhile(Predicate<SqlRow> consumer);
/**
* Execute the query returning a single row or null.
* <p>
* If this query finds 2 or more rows then it will throw a
* PersistenceException.
* </p>
*/
@Nullable
SqlRow findOne();
/**
* Execute the query reading each row from ResultSet using the RowConsumer.
* <p>
@@ -139,11 +91,6 @@ public interface SqlQuery extends Serializable, CancelableQuery {
*/
void findEachRow(RowConsumer consumer);
/**
* Execute the query returning an optional row.
*/
Optional<SqlRow> findOneOrEmpty();
/**
* Set one of more positioned parameters.
* <p>
@@ -365,7 +312,7 @@ public interface SqlQuery extends Serializable, CancelableQuery {
*
* @param <T> The type of the scalar values
*/
interface TypeQuery<T> {
interface TypeQuery<T> extends FindableQuery<TypeQuery<T>, T> {
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
@@ -373,27 +320,38 @@ public interface SqlQuery extends Serializable, CancelableQuery {
*
* @see SqlQuery#usingMaster(boolean)
*/
@Override
TypeQuery<T> usingMaster(boolean useMaster);
/**
* Execute the query using the given transaction.
*/
@Override
TypeQuery<T> usingTransaction(Transaction transaction);
/**
* Execute the query using the given connection.
*/
@Override
TypeQuery<T> usingConnection(Connection connection);
/**
* Return the single value.
*/
@Nullable
@Override
T findOne();
/**
* Return the single value that is optional.
*/
@Override
Optional<T> findOneOrEmpty();
/**
* Return the list of values.
*/
@Override
List<T> findList();
/**
@@ -158,6 +158,15 @@ public interface SqlUpdate {
*/
int executeNow();
/**
* Set an explicit transaction to use to execute this statement.
* <p>
* When not set, {@link #execute()} and {@link #executeNow()} use whatever transaction
* is currently active on the thread (or auto-commit if none is active) - consistent
* with {@link Database#execute(SqlUpdate, Transaction)}.
*/
SqlUpdate usingTransaction(Transaction transaction);
/**
* Execute when addBatch() has been used to batch multiple bind executions.
*
@@ -0,0 +1,101 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* A {@link FindableQuery} that additionally supports streaming results and paging.
*
* @param <SELF> The query type (used for method chaining)
* @param <T> The type of the result
*/
@NullMarked
public interface StreamableQuery<SELF extends StreamableQuery<SELF, T>, T> extends FindableQuery<SELF, T> {
/**
* Execute the query returning the result as a Stream.
* <p>
* Note that this can support very large queries iterating any number of results.
* To do so internally it can use multiple persistence contexts.
* <p>
* Note that the Stream holds resources related to the underlying resultSet and
* potentially connection and MUST be closed. We should use the Stream in a
* <em>try with resource block</em>.
* <pre>{@code
*
* // use try with resources to ensure Stream is closed
*
* try (Stream<T> stream = query.findStream()) {
* stream
* .map(...)
* .collect(...);
* }
*
* }</pre>
*/
Stream<T> findStream();
/**
* Return a PagedList for this query using firstRow and maxRows.
* <p>
* The benefit of using this over findList() is that it provides functionality to get the
* total row count etc.
* <p>
* If maxRows is not set on the query prior to calling findPagedList() then a
* PersistenceException is thrown.
*
* @return The PagedList
*/
PagedList<T> findPagedList();
/**
* Execute the query processing the results one at a time.
* <p>
* This method is appropriate to process very large query results as the results are
* consumed one at a time and do not need to be held in memory (unlike {@link #findList()}).
* <p>
* Note that internally Ebean can inform the JDBC driver that it is expecting a larger
* resultSet and specifically for MySQL this hint is required to stop its JDBC driver
* from buffering the entire resultSet. As such, for smaller resultSets findList() is
* generally preferable.
* <p>
* Compared with {@link #findEachWhile(Predicate)} this will always process all the results
* whereas findEachWhile() provides a way to stop processing the query result early before
* all the results have been read.
*
* @param consumer the consumer used to process the queried results.
*/
void findEach(Consumer<T> consumer);
/**
* Execute findEach streaming query batching the results for consuming.
* <p>
* This query execution will stream the results and is suited to consuming
* large numbers of results from the database.
* <p>
* Typically, we use this batch consumer when we want to do further processing on
* the results and want to do that processing in batch form, for example - 100 at
* a time.
*
* @param batch The number of results processed in the batch
* @param consumer Process the batch of results
*/
void findEach(int batch, Consumer<List<T>> consumer);
/**
* Execute the query using callbacks to process the resulting results one at a time,
* with the ability to stop processing part way through.
* <p>
* Returning {@code false} after processing a result stops the iteration through the
* query results.
*
* @param consumer the consumer used to process the queried results, returning
* {@code false} to stop processing.
*/
void findEachWhile(Predicate<T> consumer);
}
@@ -84,6 +84,15 @@ public interface Update<T> {
*/
int execute();
/**
* Set an explicit transaction to use to execute this statement.
* <p>
* When not set, {@link #execute()} uses whatever transaction is currently active on
* the thread (or auto-commit if none is active) - consistent with
* {@link Database#execute(Update, Transaction)}.
*/
Update<T> usingTransaction(Transaction transaction);
/**
* Set an ordered bind parameter.
* <p>
@@ -9,6 +9,11 @@ import java.time.Instant;
*/
public interface MetaQueryPlan {
/**
* Return the name of the database for the query.
*/
String dbName();
/**
* Return the bean type for the query.
*/
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ebean-avajejsonb-mapper</artifactId>
<name>ebean-avajejsonb-mapper</name>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jsonb</artifactId>
<version>${avaje-jsonb.version}</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-node</artifactId>
<version>${avaje-jsonb.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2database.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-jsonb-generator</artifactId>
<version>${avaje-jsonb.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean-maven-plugin.version}</version>
<executions>
<execution>
<id>test</id>
<phase>process-test-classes</phase>
<configuration>
<packages>org/example/avajejsonb/**</packages>
<transformArgs>debug=0</transformArgs>
</configuration>
<goals>
<goal>testEnhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,223 @@
package io.ebean.avajejsonb.mapper;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DataBinder;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.JsonTrim;
import io.ebean.core.type.PostgresHelper;
import io.ebean.core.type.ScalarJsonManager;
import io.ebean.core.type.ScalarJsonMapper;
import io.ebean.core.type.ScalarJsonRequest;
import io.ebean.core.type.ScalarType;
import io.ebean.core.type.ScalarTypeBase;
import io.ebean.text.TextException;
import jakarta.persistence.PersistenceException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.sql.Types;
import java.util.concurrent.ConcurrentHashMap;
/**
* Supports {@code @DbJson} properties using Avaje Jsonb.
*/
public final class ScalarJsonAvajeJsonbMapper implements ScalarJsonMapper {
private final ConcurrentHashMap<Type, JsonType<Object>> jsonTypes = new ConcurrentHashMap<>();
@Override
public <A extends Annotation> Class<A> markerAnnotation() {
return null;
}
@Override
public ScalarType<?> createType(ScalarJsonRequest request) {
Type genericType = genericType(request);
JsonType<Object> jsonType = jsonTypes.computeIfAbsent(genericType, type -> jsonb(request.manager()).type(type));
if (request.mode() == MutationDetection.NONE) {
return new NoMutationDetection(request.manager(), jsonType, request.dbType(), request.docType());
}
return new GenericObject(request.manager(), jsonType, request.dbType(), request.docType());
}
private Jsonb jsonb(ScalarJsonManager manager) {
Object mapper = manager.mapper();
return mapper instanceof Jsonb ? (Jsonb) mapper : Jsonb.instance();
}
private Type genericType(ScalarJsonRequest request) {
Class<?> type = request.beanType();
while (type != null) {
try {
Field field = type.getDeclaredField(request.name());
return field.getGenericType();
} catch (NoSuchFieldException e) {
type = type.getSuperclass();
}
}
throw new IllegalStateException("Field not found to match " + request.name());
}
private static final class NoMutationDetection extends Base<Object> {
NoMutationDetection(ScalarJsonManager jsonManager, JsonType<Object> jsonType, int dbType, DocPropertyType docType) {
super(Object.class, jsonManager, jsonType, dbType, docType);
}
}
private static final class GenericObject extends Base<Object> {
private final boolean jsonb;
GenericObject(ScalarJsonManager jsonManager, JsonType<Object> jsonType, int dbType, DocPropertyType docType) {
super(Object.class, jsonManager, jsonType, dbType, docType);
this.jsonb = "jsonb".equals(pgType);
}
@Override
public boolean mutable() {
return true;
}
@Override
public boolean jsonMapper() {
return true;
}
@Override
public Object read(DataReader reader) throws SQLException {
String json = reader.getString();
if (jsonb) {
json = JsonTrim.trim(json);
}
reader.pushJson(json);
return parseJson(json);
}
@Override
public void bind(DataBinder binder, Object value) throws SQLException {
String rawJson = binder.popJson();
if (rawJson == null && value != null) {
rawJson = formatValue(value);
}
bindJson(binder, value, rawJson);
}
}
private static abstract class Base<T> extends ScalarTypeBase<T> {
private final JsonType<T> jsonType;
protected final String pgType;
private final DocPropertyType docType;
Base(Class<T> cls, ScalarJsonManager jsonManager, JsonType<T> jsonType, int dbType, DocPropertyType docType) {
super(cls, false, dbType);
this.jsonType = jsonType;
this.pgType = jsonManager.postgresType(dbType);
this.docType = docType;
}
@Override
public T read(DataReader reader) throws SQLException {
return parseJson(reader.getString());
}
@Override
public void bind(DataBinder binder, T value) throws SQLException {
bindJson(binder, value, value == null ? null : formatValue(value));
}
final T parseJson(String json) {
if (json == null || json.isEmpty()) {
return null;
}
try {
return jsonType.fromJson(json);
} catch (RuntimeException e) {
throw new TextException("Failed to parse JSON [{}] as " + jsonType, json, e);
}
}
final void bindJson(DataBinder binder, Object value, String rawJson) throws SQLException {
if (pgType != null) {
binder.setObject(PostgresHelper.asObject(pgType, rawJson));
} else if (value == null) {
binder.setNull(Types.VARCHAR);
} else {
binder.setString(rawJson);
}
}
@Override
public final Object toJdbcType(Object value) {
return value;
}
@Override
@SuppressWarnings("unchecked")
public final T toBeanType(Object value) {
return (T) value;
}
@Override
public final String formatValue(T value) {
try {
return jsonType.toJson(value);
} catch (RuntimeException e) {
throw new PersistenceException("Unable to create JSON", e);
}
}
@Override
public final T parse(String value) {
return parseJson(value);
}
@Override
public final DocPropertyType docType() {
return docType;
}
@Override
public final T jsonRead(JsonReader parser) {
if (parser.isNullValue()) {
return null;
}
return parseJson(parser.readRaw());
}
@Override
public final void jsonWrite(JsonWriter writer, T value) throws IOException {
if (value == null) {
writer.nullValue();
} else {
writer.rawValue(formatValue(value));
}
}
@Override
public final T readData(DataInput dataInput) throws IOException {
return dataInput.readBoolean() ? parse(dataInput.readUTF()) : null;
}
@Override
public final void writeData(DataOutput dataOutput, T value) throws IOException {
if (value == null) {
dataOutput.writeBoolean(false);
} else {
dataOutput.writeBoolean(true);
dataOutput.writeUTF(format(value));
}
}
}
}
@@ -0,0 +1,9 @@
import io.ebean.avajejsonb.mapper.ScalarJsonAvajeJsonbMapper;
module io.ebean.avajejsonb.mapper {
requires io.avaje.jsonb;
requires io.ebean.core.type;
provides io.ebean.core.type.ScalarJsonMapper with ScalarJsonAvajeJsonbMapper;
}
@@ -0,0 +1 @@
io.ebean.avajejsonb.mapper.ScalarJsonAvajeJsonbMapper
@@ -0,0 +1,56 @@
package io.ebean.avajejsonb.mapper;
import io.avaje.json.node.JsonNode;
import io.avaje.json.node.JsonObject;
import io.ebean.Database;
import io.ebean.DatabaseBuilder;
import org.example.avajejsonb.JsonbEntity;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
class JsonbDatabaseTest {
@Test
void dbJson_roundTripsJsonbPayloadsAndJsonNode() {
Database database = buildDatabase();
try {
JsonbEntity entity = new JsonbEntity();
entity.setPayload(new JsonbPayload("main", 1));
entity.setPayloads(List.of(new JsonbPayload("first", 2), new JsonbPayload("second", 3)));
entity.setNode(JsonObject.create().add("name", "node").add("count", 4));
database.save(entity);
JsonbEntity found = database.find(JsonbEntity.class, entity.getId());
assertThat(found.getPayload()).isEqualTo(new JsonbPayload("main", 1));
assertThat(found.getPayloads()).containsExactly(new JsonbPayload("first", 2), new JsonbPayload("second", 3));
JsonNode node = found.getNode();
assertThat(node.extract("name")).isEqualTo("node");
assertThat(node.extract("count", 0)).isEqualTo(4);
} finally {
database.shutdown();
}
}
private static Database buildDatabase() {
DatabaseBuilder config = Database.builder();
config.setName("avajeJsonbMapper");
config.setDefaultServer(false);
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
Properties properties = new Properties();
properties.setProperty("datasource.avajeJsonbMapper.username", "sa");
properties.setProperty("datasource.avajeJsonbMapper.password", "");
properties.setProperty("datasource.avajeJsonbMapper.databaseUrl", "jdbc:h2:mem:avajeJsonbMapper");
properties.setProperty("datasource.avajeJsonbMapper.databaseDriver", "org.h2.Driver");
config.loadFromProperties(properties);
config.addClass(JsonbEntity.class);
return config.build();
}
}
@@ -0,0 +1,37 @@
package io.ebean.avajejsonb.mapper;
import io.avaje.jsonb.Json;
import java.util.Objects;
@Json
public class JsonbPayload {
public String name;
public int count;
public JsonbPayload() {
}
JsonbPayload(String name, int count) {
this.name = name;
this.count = count;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof JsonbPayload)) {
return false;
}
JsonbPayload other = (JsonbPayload) object;
return count == other.count && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, count);
}
}
@@ -0,0 +1,130 @@
package io.ebean.avajejsonb.mapper;
import io.avaje.json.node.JsonNode;
import io.avaje.json.node.JsonObject;
import io.avaje.jsonb.Json;
import io.ebean.annotation.MutationDetection;
import io.ebean.core.type.DocPropertyType;
import io.ebean.core.type.ScalarJsonManager;
import io.ebean.core.type.ScalarJsonRequest;
import io.ebean.core.type.ScalarType;
import org.junit.jupiter.api.Test;
import java.sql.Types;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
class ScalarJsonAvajeJsonbMapperTest {
private static final ScalarJsonManager JSON_MANAGER = new ScalarJsonManager() {
@Override
public MutationDetection mutationDetection() {
return MutationDetection.HASH;
}
@Override
public Object mapper() {
return null;
}
@Override
public String postgresType(int dbType) {
return null;
}
};
private final ScalarJsonAvajeJsonbMapper mapper = new ScalarJsonAvajeJsonbMapper();
@Test
void pojo_roundTripsThroughGeneratedJsonbAdapter() {
ScalarType<Object> scalarType = scalarType("document", MutationDetection.HASH);
Payload payload = new Payload("hello", 42);
String json = scalarType.formatValue(payload);
assertThat(json).isEqualTo("{\"name\":\"hello\",\"count\":42}");
assertThat(scalarType.parse(json)).isEqualTo(payload);
assertThat(scalarType.mutable()).isTrue();
assertThat(scalarType.jsonMapper()).isTrue();
}
@Test
void genericList_roundTripsUsingPropertyGenericType() {
ScalarType<Object> scalarType = scalarType("payloads", MutationDetection.HASH);
List<Payload> payloads = List.of(new Payload("one", 1), new Payload("two", 2));
String json = scalarType.formatValue(payloads);
assertThat(json).isEqualTo("[{\"name\":\"one\",\"count\":1},{\"name\":\"two\",\"count\":2}]");
assertThat(scalarType.parse(json)).isEqualTo(payloads);
}
@Test
void jsonNode_roundTripsThroughAvajeJsonNodeComponent() {
ScalarType<Object> scalarType = scalarType("node", MutationDetection.HASH);
JsonNode node = JsonObject.create().add("name", "node").add("count", 3);
String json = scalarType.formatValue(node);
assertThat(json).isEqualTo("{\"name\":\"node\",\"count\":3}");
JsonNode parsed = (JsonNode) scalarType.parse(json);
assertThat(parsed.extract("name")).isEqualTo("node");
assertThat(parsed.extract("count", 0)).isEqualTo(3);
}
@Test
void mutationDetectionNone_isNotMutable() {
ScalarType<Object> scalarType = scalarType("document", MutationDetection.NONE);
assertThat(scalarType.mutable()).isFalse();
assertThat(scalarType.jsonMapper()).isFalse();
}
@SuppressWarnings("unchecked")
private ScalarType<Object> scalarType(String property, MutationDetection mutationDetection) {
var request = new ScalarJsonRequest(JSON_MANAGER, Types.VARCHAR, DocPropertyType.OBJECT, Entity.class, mutationDetection, property);
return (ScalarType<Object>) mapper.createType(request);
}
private static final class Entity {
Payload document;
List<Payload> payloads;
JsonNode node;
}
@Json
static class Payload {
public String name;
public int count;
Payload() {
}
Payload(String name, int count) {
this.name = name;
this.count = count;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof Payload)) {
return false;
}
Payload other = (Payload) object;
return count == other.count && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
return Objects.hash(name, count);
}
}
}
@@ -0,0 +1,54 @@
package org.example.avajejsonb;
import io.avaje.json.node.JsonNode;
import io.ebean.avajejsonb.mapper.JsonbPayload;
import io.ebean.annotation.DbJson;
import io.ebean.annotation.DbJsonB;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.util.List;
@Entity
public class JsonbEntity {
@Id
long id;
@DbJson
JsonbPayload payload;
@DbJson
List<JsonbPayload> payloads;
@DbJsonB
JsonNode node;
public long getId() {
return id;
}
public JsonbPayload getPayload() {
return payload;
}
public void setPayload(JsonbPayload payload) {
this.payload = payload;
}
public List<JsonbPayload> getPayloads() {
return payloads;
}
public void setPayloads(List<JsonbPayload> payloads) {
this.payloads = payloads;
}
public JsonNode getNode() {
return node;
}
public void setNode(JsonNode node) {
this.node = node;
}
}
@@ -0,0 +1 @@
entity-packages: org.example.avajejsonb
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-bench</artifactId>
+34 -28
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean bom</name>
@@ -89,25 +89,25 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -125,13 +125,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-avajejsonb-mapper</artifactId>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -155,37 +161,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +199,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-core-json</artifactId>
<name>ebean-core-json</name>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -29,7 +29,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.11</version>
<version>42.7.12</version>
<optional>true</optional>
</dependency>
@@ -1,14 +1,19 @@
package io.ebean.jackson.mapper;
package io.ebean.core.type;
/**
* Helper that removes whitespace from JSON. Used to normalise Postgres JSONB content.
* Helper that removes whitespace from JSON content.
* <p>
* Used to normalise PostgreSQL JSONB content before it is retained for mutation detection.
*/
final class JsonTrim {
public final class JsonTrim {
private JsonTrim() {
}
/**
* Return JSON with whitespace trimmed.
*/
static String trim(String json) {
public static String trim(String json) {
if (json == null) {
return null;
}
@@ -18,7 +23,7 @@ final class JsonTrim {
boolean quoted = false;
for (int i = 0; i < len; i++) {
char c = json.charAt(i);
if (c == '\"') {
if (c == '"') {
if (!escaped) {
quoted = !quoted;
} else {
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<dependency>
@@ -157,21 +157,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
@@ -334,6 +334,12 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
*/
int executeNow(SpiSqlUpdate sqlUpdate);
/**
* Execute the sql update regardless of transaction batch mode using the given
* explicit transaction (or the current ambient transaction when null).
*/
int executeNow(SpiSqlUpdate sqlUpdate, @Nullable Transaction transaction);
/**
* Create a query bind capture for the given query plan.
*/
@@ -1938,7 +1938,12 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
@Override
public int executeNow(SpiSqlUpdate sqlUpdate) {
return persister.executeSqlUpdateNow(sqlUpdate, null);
return executeNow(sqlUpdate, null);
}
@Override
public int executeNow(SpiSqlUpdate sqlUpdate, @Nullable Transaction transaction) {
return persister.executeSqlUpdateNow(sqlUpdate, transaction);
}
@Override
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.core;
import io.ebean.DB;
import io.ebean.SqlUpdate;
import io.ebean.Transaction;
import io.ebean.Update;
import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.SpiEbeanServer;
@@ -140,7 +141,7 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
server.executeBatch(this, transaction);
return -1;
}
return server.execute(this);
return server.execute(this, transaction);
} else {
// Hopefully this doesn't catch anyone out...
return DB.getDefault().execute(this);
@@ -150,12 +151,18 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
@Override
public int executeNow() {
if (server != null) {
return server.executeNow(this);
return server.executeNow(this, transaction);
} else {
throw new IllegalStateException("server is null?");
}
}
@Override
public SqlUpdate usingTransaction(Transaction transaction) {
this.transaction = (SpiTransaction) transaction;
return this;
}
@Override
public int[] executeBatch() {
if (server == null) {
@@ -80,6 +80,10 @@ public final class DtoMappingRequest {
return name;
}
public String dbName() {
return server.name();
}
public String hash() {
return hash;
}
@@ -15,6 +15,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
private final QueryPlanMetric planMetric;
private final TimedMetric metric;
private final Class<?> beanType;
private final String dbName;
private final String name;
private final String hash;
private final String sql;
@@ -26,6 +27,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
this.planMetric = request.createMetric();
this.metric = planMetric.metric();
this.beanType = request.type();
this.dbName = request.dbName();
this.name = request.name();
this.hash = request.hash();
this.sql = request.sql();
@@ -91,6 +93,6 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType, dbName, name, hash, sql, profileLocation, bind, planString);
}
}
@@ -1335,6 +1335,24 @@ public class DefaultExpressionList<T> implements SpiExpressionList<T> {
return null;
}
/**
* Return a "propertyName: value" description when this expression list is a single
* simple equality predicate (a candidate natural/unique key), otherwise null.
* <p>
* Used to build a decent default message for {@code findOneOrThrow()} when the query
* isn't a simple find-by-id.
*/
@Nullable
public String singleEqDescription() {
if (list.size() == 1 && list.get(0) instanceof SimpleExpression) {
SimpleExpression simple = (SimpleExpression) list.get(0);
if (simple.isOpEquals()) {
return simple.getPropName() + ": " + simple.getValue();
}
}
return null;
}
@Override
public ExpressionList<T> clear() {
list.clear();
@@ -4,11 +4,9 @@ import io.ebean.InsertOptions;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Postgres specific generation of insert on conflict.
@@ -18,12 +16,14 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
private final InsertMeta meta;
private final BeanDescriptor<?> desc;
private final String baseTable;
private final List<String> nonUpdatableColumns;
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
InsertMetaOptionsPostgres(InsertMeta meta, BeanDescriptor<?> desc) {
this.meta = meta;
this.desc = desc;
this.baseTable = desc.baseTable();
this.nonUpdatableColumns = InsertMetaOptionsSupport.nonUpdatableColumns(desc);
}
@Override
@@ -49,10 +49,7 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
meta.sql(request, !withId, baseTable, false);
request.append(" on conflict ");
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
String constraintName = options.constraint();
if (constraintName != null) {
@@ -84,6 +81,7 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
List<String> columns = request.columns();
columns.removeAll(uniqueColumns);
columns.removeAll(nonUpdatableColumns);
if (withId) {
BeanProperty idProperty = desc.idProperty();
if (idProperty != null && !idProperty.isEmbedded()) {
@@ -4,11 +4,9 @@ import io.ebean.InsertOptions;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* SQLite specific generation of insert on conflict.
@@ -21,12 +19,14 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
private final InsertMeta meta;
private final BeanDescriptor<?> desc;
private final String baseTable;
private final List<String> nonUpdatableColumns;
private final Map<String, String> sqlCache = new ConcurrentHashMap<>();
InsertMetaOptionsSqlite(InsertMeta meta, BeanDescriptor<?> desc) {
this.meta = meta;
this.desc = desc;
this.baseTable = desc.baseTable();
this.nonUpdatableColumns = InsertMetaOptionsSupport.nonUpdatableColumns(desc);
}
@Override
@@ -55,10 +55,7 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
meta.sql(request, !withId, baseTable, false);
request.append(" on conflict (");
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
String cols = options.uniqueColumns();
if (cols != null) {
@@ -85,6 +82,7 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
private void setColumns(boolean withId, GenerateDmlRequest request, List<String> uniqueColumns) {
List<String> columns = request.columns();
columns.removeAll(uniqueColumns);
columns.removeAll(nonUpdatableColumns);
if (withId) {
BeanProperty idProperty = desc.idProperty();
if (idProperty != null && !idProperty.isEmbedded()) {
@@ -0,0 +1,75 @@
package io.ebeaninternal.server.persist.dml;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import io.ebeaninternal.server.deploy.generatedproperty.GeneratedProperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* Shared support for the platform specific insert on conflict do update generation.
*/
final class InsertMetaOptionsSupport {
private InsertMetaOptionsSupport() {
}
/**
* Return the columns to use as the on conflict target.
* <p>
* Uses columns explicitly mapped as unique via {@code @Column(unique=true)} or
* {@code @Index(unique=true)}. When there are none of those, falls back to the primary
* key column(s) given the primary key constraint is inherently unique. This fallback only
* applies when the id value is included in the insert (withId) as an insert relying on a
* database generated id (identity/sequence) is never going to naturally conflict on that id.
*/
static List<String> uniqueColumns(BeanDescriptor<?> desc, boolean withId) {
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
if (uniqueColumns.isEmpty() && withId) {
uniqueColumns = idColumns(desc);
}
return uniqueColumns;
}
private static List<String> idColumns(BeanDescriptor<?> desc) {
BeanProperty idProperty = desc.idProperty();
if (idProperty == null) {
return List.of();
}
if (idProperty.isEmbedded() && idProperty instanceof BeanPropertyAssocOne) {
List<String> columns = new ArrayList<>();
for (BeanProperty embedded : ((BeanPropertyAssocOne<?>) idProperty).properties()) {
columns.add(embedded.dbColumn());
}
return columns;
}
return List.of(idProperty.dbColumn());
}
/**
* Return the columns that should be excluded from the generated "do update set" clause
* as they are not updatable, e.g. {@code @Column(updatable=false)} or a generated property
* that is insert only such as {@code @WhenCreated}/{@code @WhoCreated}.
*/
static List<String> nonUpdatableColumns(BeanDescriptor<?> desc) {
List<String> columns = new ArrayList<>();
for (BeanProperty prop : desc.propertiesNonTransient()) {
if (!prop.isDbUpdatable() || isInsertOnlyGenerated(prop)) {
columns.add(prop.dbColumn());
}
}
return columns;
}
private static boolean isInsertOnlyGenerated(BeanProperty prop) {
GeneratedProperty gen = prop.generatedProperty();
return gen != null && gen.includeInInsert() && !gen.includeInUpdate();
}
}
@@ -4,15 +4,13 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.metric.CountMetric;
import io.ebean.metric.CountMetricStats;
import java.util.concurrent.atomic.LongAdder;
/**
* Used to collect counter metrics.
*/
final class DCountMetric implements CountMetric {
private final String name;
private final LongAdder count = new LongAdder();
private final ValueAdder count = new ValueAdder();
private String reportName;
DCountMetric(String name) {
@@ -29,12 +27,12 @@ final class DCountMetric implements CountMetric {
@Override
public void increment() {
count.increment();
count.add(1);
}
@Override
public boolean isEmpty() {
return count.sum() == 0;
return count.currentValue() == 0;
}
@Override
@@ -44,12 +42,12 @@ final class DCountMetric implements CountMetric {
@Override
public long get(boolean reset) {
return reset ? count.sumThenReset() : count.sum();
return count.get(reset);
}
@Override
public void visit(MetricVisitor visitor) {
long val = visitor.reset() ? count.sumThenReset() : count.sum();
long val = count.get(visitor.reset());
if (val > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
visitor.visitCount(new DCountMetricStats(name, val));
@@ -4,7 +4,6 @@ import io.ebean.meta.MetricVisitor;
import io.ebean.metric.TimedMetric;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
/**
* Used to collect timed execution statistics.
@@ -15,8 +14,8 @@ import java.util.concurrent.atomic.LongAdder;
final class DTimedMetric implements TimedMetric {
private final String name;
private final LongAdder count = new LongAdder();
private final LongAdder total = new LongAdder();
private final ValueAdder count = new ValueAdder();
private final ValueAdder total = new ValueAdder();
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
private boolean collected;
private String reportName;
@@ -43,14 +42,14 @@ final class DTimedMetric implements TimedMetric {
@Override
public void add(long value) {
count.increment();
count.add(1);
total.add(value);
max.accumulate(value);
}
@Override
public boolean isEmpty() {
return count.sum() == 0;
return count.currentValue() == 0;
}
@Override
@@ -62,16 +61,17 @@ final class DTimedMetric implements TimedMetric {
@Override
public void visit(MetricVisitor visitor) {
final long countSum = visitor.reset() ? count.sumThenReset() : count.sum();
final boolean reset = visitor.reset();
final long countSum = count.get(reset);
if (countSum > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
visitor.visitTimed(stats(visitor.reset(), name, countSum));
visitor.visitTimed(stats(reset, name, countSum));
}
}
@Override
public DTimeMetricStats collect(boolean reset) {
final long countSum = reset ? count.sumThenReset() : count.sum();
final long countSum = count.get(reset);
if (countSum == 0) {
return null;
} else {
@@ -84,7 +84,7 @@ final class DTimedMetric implements TimedMetric {
*/
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
try {
final long totalSum = reset ? total.sumThenReset() : total.sum();
final long totalSum = total.get(reset);
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
} finally {
collected = true;
@@ -0,0 +1,36 @@
package io.ebeaninternal.server.profile;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* Accumulates a value while supporting cumulative and reset-based delta reads.
*/
final class ValueAdder {
private final LongAdder value = new LongAdder();
private final AtomicLong previousValue = new AtomicLong();
void add(long amount) {
value.add(amount);
}
long get(boolean reset) {
long currentValue = value.sum();
if (!reset) {
return currentValue;
}
long previous = previousValue.getAndSet(currentValue);
return currentValue >= previous ? currentValue - previous : currentValue;
}
void reset() {
value.reset();
previousValue.set(0);
}
long currentValue() {
return value.sum();
}
}
@@ -274,7 +274,7 @@ public class CQueryPlan implements SpiQueryPlan {
@Override
public final DQueryPlanOutput createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType(), name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType(), server.name(), name, hash, sql, profileLocation, bind, planString);
}
public DataReader createDataReader(boolean unmodifiable, ResultSet rset) {
@@ -12,6 +12,7 @@ import java.time.Instant;
public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private final Class<?> beanType;
private final String dbName;
private final String label;
private final ProfileLocation profileLocation;
@@ -25,8 +26,9 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private Instant whenCaptured;
private Object tenantId;
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
public DQueryPlanOutput(Class<?> beanType, String dbName, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
this.beanType = beanType;
this.dbName = dbName;
this.label = label;
this.hash = hash;
this.sql = sql;
@@ -35,6 +37,11 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
this.plan = plan;
}
@Override
public String dbName() {
return dbName;
}
@Override
public String hash() {
return hash;
@@ -21,11 +21,13 @@ public final class SqlQueryPlan implements SpiQueryPlan {
private final String name;
private final String hash;
private final String sql;
private final String dbName;
private final SpiQueryBindCapture bindCapture;
SqlQueryPlan(SpiEbeanServer server, String name, String sql) {
this.name = name;
this.sql = sql;
this.dbName = server.name();
this.hash = Md5.hash(sql, name);
this.bindCapture = server.createQueryBindCapture(this);
}
@@ -77,6 +79,6 @@ public final class SqlQueryPlan implements SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
return new DQueryPlanOutput(null, dbName, name, hash, sql, null, bind, planString);
}
}
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.DtoMapContext;
import io.ebean.DtoMapper;
import io.ebean.MappedQuery;
import io.ebean.PagedList;
@@ -10,6 +11,9 @@ import io.ebeaninternal.api.SpiQuery;
import java.sql.Connection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* Default implementation of {@link MappedQuery} backing {@code query.mapTo(dtoType)}.
@@ -88,12 +92,45 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
return Optional.ofNullable(findOne());
}
@Override
public D findOneOrThrow() {
return mapper().map(query.findOneOrThrow());
}
@Override
public PagedList<D> findPagedList() {
DtoMapper<T, D> m = mapper();
return new MappedPagedList<>(query.findPagedList(), m);
}
@Override
public Stream<D> findStream() {
DtoMapper<T, D> m = mapper();
DtoMapContext context = new DtoMapContext();
return query.findStream().map(source -> m.map(source, context));
}
@Override
public void findEach(Consumer<D> consumer) {
DtoMapper<T, D> m = mapper();
DtoMapContext context = new DtoMapContext();
query.findEach(source -> consumer.accept(m.map(source, context)));
}
@Override
public void findEach(int batch, Consumer<List<D>> consumer) {
DtoMapper<T, D> m = mapper();
DtoMapContext context = new DtoMapContext();
query.findEach(batch, sourceBatch -> consumer.accept(m.mapList(sourceBatch, context)));
}
@Override
public void findEachWhile(Predicate<D> consumer) {
DtoMapper<T, D> m = mapper();
DtoMapContext context = new DtoMapContext();
query.findEachWhile(source -> consumer.test(m.map(source, context)));
}
@Override
public MappedQuery<D> usingMaster(boolean useMaster) {
query.usingMaster(useMaster);
@@ -111,4 +148,9 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
query.usingConnection(connection);
return this;
}
@Override
public void cancel() {
query.cancel();
}
}
@@ -23,6 +23,7 @@ import io.ebeaninternal.server.query.NativeSqlQueryPlanKey;
import io.ebeaninternal.server.rawsql.SpiRawSql;
import io.ebeaninternal.server.transaction.ExternalJdbcTransaction;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceException;
import java.sql.Connection;
import java.sql.Timestamp;
@@ -1656,6 +1657,30 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
return server.findOneOrEmpty(this);
}
@Override
public final T findOneOrThrow() {
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException(notFoundMessage()));
}
/**
* Build a decent default "not found" message using the id (if this is effectively a
* find-by-id query) or, failing that, a single simple equality predicate (a likely
* natural/unique key). Falls back to a generic message for anything more complex.
*/
private String notFoundMessage() {
String type = beanDescriptor.type().getSimpleName();
if (isFindById()) {
return type + " not found for id: " + id;
}
if (whereExpressions != null) {
String desc = whereExpressions.singleEqDescription();
if (desc != null) {
return type + " not found for " + desc;
}
}
return type + " not found";
}
@Override
public final FutureIds<T> findFutureIds() {
return server.findFutureIds(this);
@@ -1,6 +1,7 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.Database;
import io.ebean.Transaction;
import io.ebean.Update;
import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.SpiUpdate;
@@ -30,6 +31,7 @@ public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
private String generatedSql;
private final String baseTable;
private final OrmUpdateType type;
private transient Transaction transaction;
/**
* Create with a specific server. This means you can use the
@@ -88,7 +90,13 @@ public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
@Override
public int execute() {
return server.execute(this);
return server.execute(this, transaction);
}
@Override
public DefaultOrmUpdate<T> usingTransaction(Transaction transaction) {
this.transaction = transaction;
return this;
}
/**
@@ -281,6 +281,17 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
return this;
}
@Override
public TypeQuery<T> usingConnection(Connection connection) {
DefaultRelationalQuery.this.usingConnection(connection);
return this;
}
@Override
public void cancel() {
DefaultRelationalQuery.this.cancel();
}
@Override
public T findOne() {
return findSingleAttribute(type);
@@ -326,6 +337,17 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
return this;
}
@Override
public TypeQuery<T> usingConnection(Connection connection) {
DefaultRelationalQuery.this.usingConnection(connection);
return this;
}
@Override
public void cancel() {
DefaultRelationalQuery.this.cancel();
}
@Nullable
@Override
public T findOne() {
@@ -7,6 +7,7 @@ import io.ebeaninternal.api.SpiExpressionList;
import io.ebeaninternal.api.SpiQueryManyJoin;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import io.ebeaninternal.server.el.ElPropertyValue;
@@ -358,6 +359,54 @@ public final class OrmQueryDetail implements Serializable {
}
}
/**
* After sorting, fold any fetch path that only fetches a *ToOne association's id
* into a plain select of the parent path instead - the foreign key column is already
* present on the owning table so the join can be avoided entirely.
* <p>
* Iterates the fetch paths backwards (deepest first) so that a path already folded
* away does not stop a shallower ancestor also being folded, while a path that must
* remain a real join protects its own parent (added to {@code nonRemovable}) from
* being folded, as the parent's join is required to reach it.
*/
private void convertIdFetches(BeanDescriptor<?> desc) {
Set<String> nonRemovable = new HashSet<>();
String[] paths = fetchPaths.keySet().toArray(new String[0]);
int i = paths.length;
while (i-- > 0) {
String path = paths[i];
ElPropertyDeploy el = desc.elPropertyDeploy(path);
OrmQueryProperties prop = fetchPaths.get(path);
if (nonRemovable.contains(path)) {
// a still-existing child fetch depends on this join, don't remove
} else if (el == null) {
throw new PersistenceException("Invalid fetch path " + path + " from " + desc.fullName());
} else if (el.beanProperty() instanceof BeanPropertyAssocOne) {
BeanPropertyAssocOne<?> assoc = (BeanPropertyAssocOne<?>) el.beanProperty();
// exported (mappedBy) one-to-one has no local foreign key column - it always needs the join
if (assoc.hasForeignKeyConstraint() && !assoc.isOneToOneExported()) {
if (prop.includesExactly(assoc.targetDescriptor().idName())) {
String parentPath = prop.getParentPath();
OrmQueryProperties parentProp = parentPath == null ? baseProps : fetchPaths.get(parentPath);
if (parentProp != null && parentProp.hasProperties()) {
OrmQueryProperties newParentProp = parentProp.withAddedInclude(assoc.name());
if (parentPath == null) {
baseProps = newParentProp;
} else {
fetchPaths.put(parentPath, newParentProp);
}
fetchPaths.remove(path);
prop = null;
}
}
}
}
if (prop != null && prop.getParentPath() != null) {
nonRemovable.add(prop.getParentPath());
}
}
}
/**
* Mark 'fetch joins' to 'many' properties over to 'query joins' where needed.
*
@@ -375,6 +424,7 @@ public final class OrmQueryDetail implements Serializable {
boolean fetchJoinFirstMany = allowOne;
sortFetchPaths(beanDescriptor, addIds);
convertIdFetches(beanDescriptor);
List<FetchEntry> pairs = sortByFetchPreference(beanDescriptor);
for (FetchEntry pair : pairs) {
@@ -151,6 +151,22 @@ public final class OrmQueryProperties implements Serializable {
: buildImmutableQueryPlanHashSuffix(sourceFetchConfig);
}
/**
* Copy constructor with a replacement included set (used by {@link #withAddedInclude(String)}).
*/
private OrmQueryProperties(OrmQueryProperties source, Set<String> replacementIncluded) {
this.fetchConfig = source.fetchConfig;
this.parentPath = source.parentPath;
this.path = source.path;
this.allProperties = source.allProperties;
this.cache = source.cache;
this.filterMany = source.filterMany;
this.markForQueryJoin = source.markForQueryJoin;
this.included = immutableIncluded(replacementIncluded);
this.immutableHashPrefix = buildImmutableQueryPlanHashPrefix(path, this.included);
this.immutableHashSuffix = source.immutableHashSuffix;
}
private static Set<String> immutableIncluded(Set<String> included) {
if (included == null) {
return null;
@@ -412,6 +428,37 @@ public final class OrmQueryProperties implements Serializable {
return included == null || included.contains(propName);
}
/**
* Return true if the included properties are exactly the single given property.
* <p>
* Used to detect a fetch/select of a *ToOne association that only includes the
* target's id property - a candidate for folding into the parent select as a plain
* foreign key property (avoiding an unnecessary join).
*/
boolean includesExactly(String property) {
return included != null && included.size() == 1 && included.contains(property);
}
/**
* Return a new instance with the given property added to the included set.
* <p>
* Used to fold an id-only *ToOne fetch into this select as a plain foreign key
* property. A new instance is returned (rather than mutating {@link #included} in
* place) as this instance's included set is immutable and may be shared/cached
* (e.g. via FetchGroup reuse).
*/
OrmQueryProperties withAddedInclude(String property) {
if (allProperties) {
return this;
}
Set<String> newIncluded = new LinkedHashSet<>();
if (included != null) {
newIncluded.addAll(included);
}
newIncluded.add(property);
return new OrmQueryProperties(this, newIncluded);
}
/**
* Mark this path as needing to be a query join.
*/
@@ -88,8 +88,8 @@ public final class DefaultTypeManager implements TypeManager {
this.databasePlatform = config.getDatabasePlatform();
this.postgres = isPostgresCompatible(config.getDatabasePlatform());
this.objectMapperPresent = config.getClassLoadConfig().isJacksonObjectMapperPresent();
this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null;
this.jsonManager = (objectMapperPresent) ? new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection()) : null;
this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : config.getObjectMapper();
this.jsonManager = new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection());
this.extraTypeFactory = new DefaultTypeFactory(config);
this.arrayTypeListFactory = arrayTypeListFactory(config.getDatabasePlatform());
this.arrayTypeSetFactory = arrayTypeSetFactory(config.getDatabasePlatform());
@@ -413,7 +413,7 @@ public final class DefaultTypeManager implements TypeManager {
private ScalarType<?> createJsonObjectMapperType(DeployProperty prop, int dbType, DocPropertyType docType) {
if (jsonMapper == null) {
throw new IllegalArgumentException("Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper? Jackson ObjectMapper not present for " + prop);
throw new IllegalArgumentException("Unsupported @DbJson mapping - missing JSON mapper dependency for " + prop);
}
if (MutationDetection.DEFAULT == prop.mutationDetection()) {
prop.setMutationDetection(jsonManager.mutationDetection());
@@ -40,4 +40,19 @@ class DCountMetricTest {
assertThat(result2.get(0).count()).isEqualTo(12);
}
}
@Test
void cumulativeAndDeltaAreIndependent() {
DCountMetric counter = new DCountMetric("org.hello");
counter.add(7);
assertThat(counter.get(false)).isEqualTo(7);
assertThat(counter.get(false)).isEqualTo(7);
assertThat(counter.get(true)).isEqualTo(7);
counter.add(5);
assertThat(counter.get(false)).isEqualTo(12);
assertThat(counter.get(true)).isEqualTo(5);
assertThat(counter.get(true)).isEqualTo(0);
}
}
@@ -116,4 +116,25 @@ public class DTimedMetricTest {
assertThat(stats.total()).isEqualTo(1470);
assertThat(stats.max()).isEqualTo(160);
}
@Test
void cumulativeAndDeltaAreIndependent() {
DTimedMetric metric = new DTimedMetric("org.timed");
metric.add(560);
metric.add(500);
DTimeMetricStats cumulative = metric.collect(false);
assertThat(cumulative.count()).isEqualTo(2);
assertThat(cumulative.total()).isEqualTo(1060);
metric.add(160);
DTimeMetricStats delta = metric.collect(true);
assertThat(delta.count()).isEqualTo(3);
assertThat(delta.total()).isEqualTo(1220);
cumulative = metric.collect(false);
assertThat(cumulative.count()).isEqualTo(3);
assertThat(cumulative.total()).isEqualTo(1220);
}
}
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean ddl generation</name>
@@ -28,14 +28,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -65,7 +65,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -15,7 +15,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -1,6 +1,6 @@
package io.ebean.jackson.mapper;
import io.ebean.core.type.JsonTrim;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean net postgis types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-opentelemetry</artifactId>
@@ -28,7 +28,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -71,21 +71,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean pgvector types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean postgis types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -47,7 +47,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.11</version>
<version>42.7.12</version>
<scope>provided</scope>
</dependency>
@@ -62,7 +62,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<name>ebean querybean</name>
@@ -17,7 +17,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -59,14 +59,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
@@ -80,7 +80,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -738,6 +738,11 @@ public abstract class QueryBean<T, R extends QueryBean<T, R>> implements IQueryB
return root;
}
@Override
public void cancel() {
query.cancel();
}
@Override
public final R usingTransaction(Transaction transaction) {
query.usingTransaction(transaction);
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-redis</artifactId>
@@ -29,35 +29,35 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -3,7 +3,7 @@ ebean:
dumpMetricsOptions: sql,hash,loc
test:
registerTestTenantProvider: false
redis: latest
redis: 8.6.2
# shutdown: stop # stop | remove
platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate
+6 -6
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-redisson</artifactId>
@@ -29,35 +29,35 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
@@ -10,13 +10,13 @@ import jakarta.persistence.Entity;
@SuppressWarnings("unused")
@Cache(naturalKey = {"one", "two"})
@Entity
public class OtherOne extends EBase {
public class FOtherOne extends EBase {
private final String one;
private final String two;
private String notes;
public OtherOne(String one, String two, String notes) {
public FOtherOne(String one, String two, String notes) {
this.one = one;
this.two = two;
this.notes = notes;
@@ -11,7 +11,7 @@ import java.time.LocalDate;
@Cache(enableQueryCache = true, nearCache = true, naturalKey = "name")
@CacheBeanTuning(maxSecsToLive = 1)
@Entity
public class Person extends EBase {
public class FPerson extends EBase {
public enum Status {
NEW,
@@ -33,7 +33,7 @@ public class Person extends EBase {
*/
String key;
public Person(String name) {
public FPerson(String name) {
this.name = name;
this.status = Status.NEW;
}
@@ -7,12 +7,12 @@ import jakarta.persistence.Entity;
@Cache(naturalKey = "name")
@Entity
public class RCust extends EBase {
public class FRCust extends EBase {
@Index(unique = true)
String name;
public RCust(String name) {
public FRCust(String name) {
this.name = name;
}
@@ -8,7 +8,7 @@ import jakarta.persistence.ManyToOne;
@Cache
@Entity
public class UChild extends Model {
public class FUChild extends Model {
@Id
long id;
@@ -16,9 +16,9 @@ public class UChild extends Model {
String name;
@ManyToOne
final UParent parent;
final FUParent parent;
public UChild(UParent parent, String name) {
public FUChild(FUParent parent, String name) {
this.parent = parent;
this.name = name;
}
@@ -15,7 +15,7 @@ import java.util.UUID;
@Cache(enableQueryCache = true)
@CacheBeanTuning(maxSecsToLive = 1)
@Entity
public class UParent extends Model {
public class FUParent extends Model {
@Id
private UUID id;
@@ -23,9 +23,9 @@ public class UParent extends Model {
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private final List<UChild> children = new ArrayList<>();
private final List<FUChild> children = new ArrayList<>();
public UParent(String name) {
public FUParent(String name) {
this.name = name;
}
@@ -45,7 +45,7 @@ public class UParent extends Model {
this.name = name;
}
public List<UChild> children() {
public List<FUChild> children() {
return children;
}
}
@@ -9,7 +9,7 @@ import java.util.List;
@Cache
@Entity
public class TestOne extends Model {
public class RTestOne extends Model {
@Id
private String id;
@@ -18,9 +18,9 @@ public class TestOne extends Model {
private String otherUnique;
@OneToMany(mappedBy = "testOne", cascade = CascadeType.ALL, orphanRemoval = true)
private List<TestTwo> testTwos = new ArrayList<>();
private List<RTestTwo> testTwos = new ArrayList<>();
public TestOne(String id, String otherUnique) {
public RTestOne(String id, String otherUnique) {
this.id = id;
this.otherUnique = otherUnique;
}
@@ -37,11 +37,11 @@ public class TestOne extends Model {
this.otherUnique = otherUnique;
}
public List<TestTwo> getTestTwos() {
public List<RTestTwo> getTestTwos() {
return testTwos;
}
public void setTestTwos(List<TestTwo> testTwos) {
public void setTestTwos(List<RTestTwo> testTwos) {
this.testTwos = testTwos;
}
}
}
@@ -7,16 +7,16 @@ import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@Entity
public class TestTwo extends Model {
public class RTestTwo extends Model {
@Id
private String id;
@ManyToOne
@JoinColumn
private TestOne testOne;
private RTestOne testOne;
public TestTwo(String id) {
public RTestTwo(String id) {
this.id = id;
}
@@ -24,11 +24,11 @@ public class TestTwo extends Model {
return id;
}
public TestOne getTestOne() {
public RTestOne getTestOne() {
return testOne;
}
public void setTestOne(TestOne testOne) {
public void setTestOne(RTestOne testOne) {
this.testOne = testOne;
}
}
}
@@ -3,8 +3,8 @@ package org.integration;
import io.ebean.DB;
import io.ebean.Database;
import io.ebean.redisson.DuelCache;
import org.domain.Person;
import org.domain.query.QPerson;
import org.domain.FPerson;
import org.domain.query.QFPerson;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -37,65 +37,65 @@ class ClusterTest {
@Test
void testBothNear() throws InterruptedException {
new QPerson()
new QFPerson()
.name.eq("Someone")
.delete();
Person foo = new Person("Someone");
FPerson foo = new FPerson("Someone");
foo.save();
DB.cacheManager().clearAll();
db.metaInfo().resetAllMetrics();
other.metaInfo().resetAllMetrics();
Person fooA = DB.find(Person.class, foo.getId());
FPerson fooA = DB.find(FPerson.class, foo.getId());
allowAsyncMessaging(); // allow time for background cache load
Person fooB = other.find(Person.class, foo.getId());
FPerson fooB = other.find(FPerson.class, foo.getId());
DuelCache dualCacheA = db.cacheManager().beanCache(Person.class).unwrap(DuelCache.class);
DuelCache dualCacheA = db.cacheManager().beanCache(FPerson.class).unwrap(DuelCache.class);
assertCounts(dualCacheA, 0, 1, 0, 1);
fooA = DB.find(Person.class, foo.getId());
fooA = DB.find(FPerson.class, foo.getId());
assertCounts(dualCacheA, 1, 1, 0, 1);
fooB = other.find(Person.class, foo.getId());
fooA = DB.find(Person.class, foo.getId());
fooB = other.find(FPerson.class, foo.getId());
fooA = DB.find(FPerson.class, foo.getId());
assertCounts(dualCacheA, 2, 1, 0, 1);
fooB = other.find(Person.class, foo.getId());
DuelCache dualCacheB = other.cacheManager().beanCache(Person.class).unwrap(DuelCache.class);
fooB = other.find(FPerson.class, foo.getId());
DuelCache dualCacheB = other.cacheManager().beanCache(FPerson.class).unwrap(DuelCache.class);
assertCounts(dualCacheB, 2, 1, 1, 0);
}
@Test
void test() throws InterruptedException {
for (int i = 0; i < 10; i++) {
Person foo = new Person("name " + i);
FPerson foo = new FPerson("name " + i);
foo.save();
}
other.cacheManager().clearAll();
other.metaInfo().resetAllMetrics();
DuelCache dualCache = other.cacheManager().beanCache(Person.class).unwrap(DuelCache.class);
DuelCache dualCache = other.cacheManager().beanCache(FPerson.class).unwrap(DuelCache.class);
Person foo0 = other.find(Person.class, 1);
FPerson foo0 = other.find(FPerson.class, 1);
assertCounts(dualCache, 0, 1, 0, 1);
other.find(Person.class, 1);
other.find(FPerson.class, 1);
assertCounts(dualCache, 1, 1, 0, 1);
other.find(Person.class, 1);
other.find(FPerson.class, 1);
assertCounts(dualCache, 2, 1, 0, 1);
other.find(Person.class, 1);
other.find(FPerson.class, 1);
assertCounts(dualCache, 3, 1, 0, 1);
other.find(Person.class, 2);
other.find(FPerson.class, 2);
assertCounts(dualCache, 3, 2, 0, 2);
foo0.setName("name2");
foo0.save();
allowAsyncMessaging();
Person foo3 = other.find(Person.class, 1);
FPerson foo3 = other.find(FPerson.class, 1);
assertThat(foo3.getName()).isEqualTo("name2");
assertCounts(dualCache, 3, 3, 1, 2);
@@ -103,7 +103,7 @@ class ClusterTest {
foo0.save();
allowAsyncMessaging();
foo3 = other.find(Person.class, 1);
foo3 = other.find(FPerson.class, 1);
assertThat(foo3.getName()).isEqualTo("name3");
assertCounts(dualCache, 3, 4, 2, 2);
}
@@ -5,10 +5,10 @@ import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheStatistics;
import io.ebeaninternal.server.cache.CachedBeanData;
import org.domain.*;
import org.domain.query.QOtherOne;
import org.domain.query.QPerson;
import org.domain.query.QRCust;
import org.domain.test.TestOne;
import org.domain.query.QFOtherOne;
import org.domain.query.QFPerson;
import org.domain.query.QFRCust;
import org.domain.test.RTestOne;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
@@ -21,8 +21,8 @@ import static org.assertj.core.api.Assertions.assertThat;
class IntegrationTest {
private static OtherOne findOther(String a, String b) {
return new QOtherOne()
private static FOtherOne findOther(String a, String b) {
return new QFOtherOne()
.one.eq(a)
.two.eq(b)
.findOne();
@@ -30,22 +30,22 @@ class IntegrationTest {
@Test
void uuid_getPut() {
UParent b0 = new UParent("b0");
b0.children().add(new UChild(b0, "b0c0"));
b0.children().add(new UChild(b0, "b0c1"));
FUParent b0 = new FUParent("b0");
b0.children().add(new FUChild(b0, "b0c0"));
b0.children().add(new FUChild(b0, "b0c1"));
b0.save();
ServerCache beanCache = DB.cacheManager().beanCache(UParent.class);
ServerCache beanCache = DB.cacheManager().beanCache(FUParent.class);
beanCache.clear();
beanCache.statistics(true);
UParent found0 = DB.find(UParent.class, b0.id());
FUParent found0 = DB.find(FUParent.class, b0.id());
assertThat(found0.name()).isEqualTo("b0");
List<UChild> children = found0.children();
List<FUChild> children = found0.children();
assertThat(children).hasSize(2);
UParent found1 = DB.find(UParent.class, b0.id());
FUParent found1 = DB.find(FUParent.class, b0.id());
assertThat(found1.name()).isEqualTo("b0");
DB.delete(found1);
@@ -57,13 +57,13 @@ class IntegrationTest {
@Test
void mget_when_emptyCollectionOfIds() {
List<RCust> f0 = new QRCust()
List<FRCust> f0 = new QFRCust()
.setIdIn(Collections.emptyList())
.findList();
assertThat(f0).isEmpty();
List<RCust> f1 = new QRCust()
List<FRCust> f1 = new QFRCust()
.id.in(Collections.emptyList())
.findList();
@@ -73,18 +73,18 @@ class IntegrationTest {
@Test
void mput_via_setIdIn() throws InterruptedException {
ServerCache beanCache = DB.cacheManager().beanCache(RCust.class);
ServerCache beanCache = DB.cacheManager().beanCache(FRCust.class);
beanCache.clear();
beanCache.statistics(true);
List<RCust> people = new ArrayList<>();
List<FRCust> people = new ArrayList<>();
for (String name : new String[]{"mp0", "mp1", "mp2"}) {
people.add(new RCust(name));
people.add(new FRCust(name));
}
DB.saveAll(people);
List<Long> ids = people.stream().map(RCust::getId).collect(Collectors.toList());
List<Long> ids = people.stream().map(FRCust::getId).collect(Collectors.toList());
List<RCust> f0 = new QRCust()
List<FRCust> f0 = new QFRCust()
.setIdIn(ids) // using collection argument
.findList();
@@ -95,7 +95,7 @@ class IntegrationTest {
Thread.sleep(5);
// we will hit the cache this time
List<RCust> f1 = new QRCust()
List<FRCust> f1 = new QFRCust()
.setIdIn(ids.toArray()) // using varargs argument
.findList();
@@ -104,7 +104,7 @@ class IntegrationTest {
assertThat(stats1.getHitCount()).isEqualTo(3);
// we will hit the cache again
List<RCust> f2 = new QRCust()
List<FRCust> f2 = new QFRCust()
.setIdIn(ids) // using collection argument
.findList();
assertThat(f2).hasSize(3);
@@ -115,18 +115,18 @@ class IntegrationTest {
@Test
void mput_via_propertyInExpression() throws InterruptedException {
ServerCache beanCache = DB.cacheManager().beanCache(RCust.class);
ServerCache beanCache = DB.cacheManager().beanCache(FRCust.class);
beanCache.clear();
beanCache.statistics(true);
List<RCust> people = new ArrayList<>();
List<FRCust> people = new ArrayList<>();
for (String name : new String[]{"mpx0", "mpx1", "mpx2"}) {
people.add(new RCust(name));
people.add(new FRCust(name));
}
DB.saveAll(people);
List<Long> ids = people.stream().map(RCust::getId).collect(Collectors.toList());
List<Long> ids = people.stream().map(FRCust::getId).collect(Collectors.toList());
List<RCust> f0 = new QRCust()
List<FRCust> f0 = new QFRCust()
.id.in(ids)
.findList();
@@ -137,7 +137,7 @@ class IntegrationTest {
Thread.sleep(5);
// we will hit the cache this time
List<RCust> f1 = new QRCust()
List<FRCust> f1 = new QFRCust()
.id.in(ids)
.findList();
@@ -146,7 +146,7 @@ class IntegrationTest {
assertThat(stats1.getHitCount()).isEqualTo(3);
// we will hit the cache again
List<RCust> f2 = new QRCust()
List<FRCust> f2 = new QFRCust()
.id.isIn(ids)
.findList();
assertThat(f2).hasSize(3);
@@ -156,18 +156,18 @@ class IntegrationTest {
@Test
void testOtherOne() {
DB.save(new OtherOne("A", "B", "ab"));
DB.save(new OtherOne("A", "C", "ac"));
DB.save(new OtherOne("B", "B", "bb"));
DB.save(new FOtherOne("A", "B", "ab"));
DB.save(new FOtherOne("A", "C", "ac"));
DB.save(new FOtherOne("B", "B", "bb"));
ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(OtherOne.class);
ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(FOtherOne.class);
nkeyCache.clear();
nkeyCache.statistics(true);
OtherOne ab0 = findOther("A", "B");
OtherOne ab1 = findOther("A", "B");
OtherOne ab2 = findOther("A", "B");
OtherOne bb = findOther("B", "B");
FOtherOne ab0 = findOther("A", "B");
FOtherOne ab1 = findOther("A", "B");
FOtherOne ab2 = findOther("A", "B");
FOtherOne bb = findOther("B", "B");
assertThat(ab0).isNotNull();
assertThat(ab1).isNotNull();
@@ -183,21 +183,21 @@ class IntegrationTest {
insertSomePeople();
Person fiona = findByName("Fiona");
FPerson fiona = findByName("Fiona");
fiona.setName("Fortuna");
fiona.setLocalDate(LocalDate.now());
fiona.update();
Thread.sleep(100);
Person one = findById(1);
FPerson one = findById(1);
assertThat(one).isNotNull();
for (int i = 1; i < 4; i++) {
System.out.println("hit " + findById(i));
}
List<Person> one2 = nameStartsWith("fo");
List<FPerson> one2 = nameStartsWith("fo");
assertThat(one2).hasSize(1);
one2 = nameStartsWith("j");
@@ -207,7 +207,7 @@ class IntegrationTest {
assertThat(one2).hasSize(2);
List<Person> byNames = findByNames("Jack", "Rob");
List<FPerson> byNames = findByNames("Jack", "Rob");
assertThat(byNames).hasSize(2);
byNames = findByNames("Jack", "Rob", "Moby");
@@ -228,40 +228,40 @@ class IntegrationTest {
System.out.println("one2 " + one2);
DB.cacheManager().clear(Person.class);
DB.cacheManager().clear(FPerson.class);
System.out.println("done");
}
private void insertSomePeople() {
List<Person> people = new ArrayList<>();
List<FPerson> people = new ArrayList<>();
for (String name : new String[]{"Jack", "John", "Rob", "Moby", "Fiona"}) {
people.add(new Person(name));
people.add(new FPerson(name));
}
DB.saveAll(people);
}
private Person findByName(String name) {
return new QPerson()
private FPerson findByName(String name) {
return new QFPerson()
.name.eq(name)
.findOne();
}
private List<Person> findByNames(String... names) {
return new QPerson()
private List<FPerson> findByNames(String... names) {
return new QFPerson()
.name.in(names)
.setUseCache(true)
.findList();
}
private Person findById(int id) {
return new QPerson()
private FPerson findById(int id) {
return new QFPerson()
.id.eq(id)
.findOne();
}
private List<Person> nameStartsWith(String pattern) {
return new QPerson()
private List<FPerson> nameStartsWith(String pattern) {
return new QFPerson()
.name.istartsWith(pattern)
.setUseQueryCache(true)
.findList();
@@ -273,15 +273,15 @@ class IntegrationTest {
*/
@Test
void versionGated_newerCached_staleWriteIsIgnored() throws InterruptedException {
ServerCache beanCache = DB.cacheManager().beanCache(RCust.class);
ServerCache beanCache = DB.cacheManager().beanCache(FRCust.class);
beanCache.clear();
RCust cust = new RCust("stale-test-orig");
FRCust cust = new FRCust("stale-test-orig");
DB.save(cust);
long id = cust.getId();
// prime cache at version 1
DB.find(RCust.class, id);
DB.find(FRCust.class, id);
Thread.sleep(150);
Object staleV1 = beanCache.get(id);
assertThat(staleV1).isNotNull();
@@ -289,7 +289,7 @@ class IntegrationTest {
// update to version 2; ensure v2 is in cache
cust.setName("stale-test-updated");
DB.save(cust);
DB.find(RCust.class, id);
DB.find(FRCust.class, id);
Thread.sleep(150);
// stale write attempt: v2 is cached, v1 should be rejected
@@ -302,7 +302,7 @@ class IntegrationTest {
assertThat(((CachedBeanData) staleV2).getVersion()).isEqualTo(2L);
beanCache.statistics(true);
RCust found = DB.find(RCust.class, id);
FRCust found = DB.find(FRCust.class, id);
ServerCacheStatistics stats = beanCache.statistics(true);
assertThat(stats.getHitCount()).isEqualTo(1);
assertThat(found.getVersion()).isEqualTo(2L);
@@ -316,14 +316,14 @@ class IntegrationTest {
*/
@Test
void zeroVersion_equalVersion_staleWriteIsNotBlocked() throws InterruptedException {
ServerCache beanCache = DB.cacheManager().beanCache(TestOne.class);
ServerCache beanCache = DB.cacheManager().beanCache(RTestOne.class);
beanCache.clear();
TestOne t1 = new TestOne("zvw-test", "unique-a");
RTestOne t1 = new RTestOne("zvw-test", "unique-a");
DB.save(t1);
// prime cache at version 0 (no @Version field)
DB.find(TestOne.class, "zvw-test");
DB.find(RTestOne.class, "zvw-test");
Thread.sleep(150);
Object staleV0 = beanCache.get("zvw-test");
assertThat(staleV0).isNotNull();
@@ -331,7 +331,7 @@ class IntegrationTest {
// update; ensure new v0 is in cache
t1.setOtherUnique("unique-b");
DB.save(t1);
DB.find(TestOne.class, "zvw-test");
DB.find(RTestOne.class, "zvw-test");
Thread.sleep(150);
// stale write: v0 in cache, incoming v0 — must NOT be blocked (0 > 0 is false)
@@ -339,7 +339,7 @@ class IntegrationTest {
// stale data should now be in cache (unlike the versioned case above)
beanCache.statistics(true);
TestOne found = DB.find(TestOne.class, "zvw-test");
RTestOne found = DB.find(RTestOne.class, "zvw-test");
ServerCacheStatistics stats = beanCache.statistics(true);
assertThat(stats.getHitCount()).isEqualTo(1);
assertThat(found.getOtherUnique()).isEqualTo("unique-a");
@@ -4,8 +4,7 @@ import io.ebean.DB;
import io.ebean.Database;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheStatistics;
import org.domain.RCust;
import org.junit.jupiter.api.Assertions;
import org.domain.FRCust;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
@@ -36,27 +35,27 @@ class RedissonTenantAwareCacheTest {
void singleBean_tenantA_hits_tenantB_misses() {
Database db = buildTenantDb();
try {
RCust cust = new RCust("t-single-iso");
FRCust cust = new FRCust("t-single-iso");
DB.save(cust);
long id = cust.getId();
ServerCache beanCache = db.cacheManager().beanCache(RCust.class);
ServerCache beanCache = db.cacheManager().beanCache(FRCust.class);
beanCache.clear();
// Tenant A: first load goes to DB
TENANT.set("tenantA");
beanCache.statistics(true); // reset counters
assertThat(db.find(RCust.class, id)).isNotNull();
assertThat(db.find(FRCust.class, id)).isNotNull();
assertThat(Objects.requireNonNull(beanCache.statistics(true)).getMissCount()).isEqualTo(1);
// Tenant A: second load must hit cache
assertThat(db.find(RCust.class, id)).isNotNull();
assertThat(db.find(FRCust.class, id)).isNotNull();
assertThat(Objects.requireNonNull(beanCache.statistics(true)).getHitCount()).isEqualTo(1);
// Tenant B: same ID, different tenant key must miss
TENANT.set("tenantB");
beanCache.statistics(true); // reset counters
assertThat(db.find(RCust.class, id)).isNotNull();
assertThat(db.find(FRCust.class, id)).isNotNull();
ServerCacheStatistics statsB = beanCache.statistics(true);
assertNotNull(statsB);
assertThat(statsB.getHitCount()).isEqualTo(0);
@@ -72,26 +71,26 @@ class RedissonTenantAwareCacheTest {
void getAll_tenantA_hits_tenantB_misses() throws InterruptedException {
Database db = buildTenantDb();
try {
List<RCust> custs = new ArrayList<>();
List<FRCust> custs = new ArrayList<>();
for (String n : new String[]{"tga0", "tga1", "tga2"}) {
custs.add(new RCust(n));
custs.add(new FRCust(n));
}
DB.saveAll(custs);
List<Long> ids = custs.stream().map(RCust::getId).collect(Collectors.toList());
List<Long> ids = custs.stream().map(FRCust::getId).collect(Collectors.toList());
ServerCache beanCache = db.cacheManager().beanCache(RCust.class);
ServerCache beanCache = db.cacheManager().beanCache(FRCust.class);
beanCache.clear();
// Tenant A: first batch load DB misses, cache populated
TENANT.set("tenantA");
List<RCust> listA0 = db.find(RCust.class).where().idIn(ids).setUseCache(true).findList();
List<FRCust> listA0 = db.find(FRCust.class).where().idIn(ids).setUseCache(true).findList();
assertThat(listA0).hasSize(3);
Thread.sleep(10);
// Tenant A: second batch load all 3 must be cache hits
beanCache.statistics(true); // reset
List<RCust> listA1 = db.find(RCust.class).where().idIn(ids).setUseCache(true).findList();
List<FRCust> listA1 = db.find(FRCust.class).where().idIn(ids).setUseCache(true).findList();
assertThat(listA1).hasSize(3);
ServerCacheStatistics statsA = beanCache.statistics(true);
assertNotNull(statsA);
@@ -101,7 +100,7 @@ class RedissonTenantAwareCacheTest {
// Tenant B: same IDs, different tenant all 3 must miss
TENANT.set("tenantB");
beanCache.statistics(true); // reset
List<RCust> listB = db.find(RCust.class).where().idIn(ids).setUseCache(true).findList();
List<FRCust> listB = db.find(FRCust.class).where().idIn(ids).setUseCache(true).findList();
assertThat(listB).hasSize(3);
ServerCacheStatistics statsB = beanCache.statistics(true);
assertNotNull(statsB);
@@ -3,7 +3,7 @@ ebean:
dumpMetricsOptions: sql,hash,loc
test:
registerTestTenantProvider: false
redis: latest
redis: 8.6.2
# shutdown: stop # stop | remove
platform: h2 # h2, postgres, mysql, oracle, sqlserver, sqlite
ddlMode: dropCreate # none | dropCreate | create | migration | createOnly | migrationDropCreate
@@ -1,7 +1,8 @@
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
database: 0
# ebean-redis test clients use database 0 in the shared Redis container.
database: 1
connectionMinimumIdleSize: 2
connectionPoolSize: 10
idleConnectionTimeout: 3000
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<version>18.4.0</version>
</parent>
<artifactId>ebean-spring-txn</artifactId>
@@ -28,14 +28,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.0</version>
<version>2.22.1</version>
<scope>test</scope>
</dependency>
@@ -77,7 +77,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.2.0</version>
<version>18.4.0</version>
<scope>test</scope>
</dependency>

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