Compare commits

..
215 changed files with 963 additions and 7285 deletions
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>composites</artifactId>
+1 -282
View File
@@ -136,31 +136,8 @@ 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).
**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
@@ -724,264 +701,6 @@ callers must consume it via try-with-resources to ensure the underlying resource
`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,35 +139,6 @@ 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)`
@@ -258,44 +229,6 @@ 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**
+26 -96
View File
@@ -1,39 +1,40 @@
# Guide: `@DbJson` / `@DbJsonB` mapping support
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
## Purpose
Ebean can map `@DbJson` and `@DbJsonB` properties in three ways:
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
- **Jackson `ObjectMapper`**, provided by **`ebean-jackson-mapper`**.
- **Avaje Jsonb**, provided by **`ebean-avajejsonb-mapper`** and its generated adapters.
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
for everything the built-in support does not handle.
The built-in mapper handles the common natural JSON types. Add one mapper module for
typed collections, POJOs, records, and other custom types.
This guide lists exactly which property types are handled built-in and which require
`ebean-jackson-mapper`.
> If a property type is **not** handled built-in and no mapper module is on the classpath,
> Ebean fails fast at startup:
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
> classpath, Ebean fails fast at startup:
>
> ```text
> Unsupported @DbJson mapping - missing JSON mapper dependency for <property>
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
> Jackson ObjectMapper not present for <property>
> ```
---
## Quick reference
| Property type | Built-in (avaje-json-core) | Mapper module |
|---|:---:|---|
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|---|:---:|:---:|
| `String` | ✅ | |
| `List<String>`, `List<Long>` | ✅ | |
| `Set<String>`, `Set<Long>` | ✅ | |
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
| `Map<String, String>` | ✅ | |
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | 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 |
| `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 | | |
---
@@ -58,10 +59,10 @@ Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
---
## Jackson `ObjectMapper`
## Everything else → Jackson `ObjectMapper`
`ebean-jackson-mapper` uses a Jackson `ObjectMapper` for the property types not handled
built-in:
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
requires `ebean-jackson-mapper`:
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
@@ -76,7 +77,7 @@ built-in:
---
### Adding `ebean-jackson-mapper`
## Adding `ebean-jackson-mapper`
```xml
<dependency>
@@ -91,79 +92,6 @@ 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
@@ -174,13 +102,15 @@ To migrate from Jackson to Avaje Jsonb, remove `ebean-jackson-mapper`, add
(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`.
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
type-aware mapper.
---
## Choosing
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
object/string maps) to avoid 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.
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.
+1 -6
View File
@@ -376,12 +376,7 @@ 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**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.
- Relationships are lazy-loaded by default
---
-42
View File
@@ -431,48 +431,6 @@ 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.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean api</name>
@@ -1,137 +0,0 @@
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.");
}
}
+95 -2
View File
@@ -1,9 +1,16 @@
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.
@@ -36,7 +43,12 @@ import java.util.List;
* }</pre>
*/
@NullMarked
public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
public interface DtoQuery<T> extends CancelableQuery {
/**
* Execute the query returning a list.
*/
List<T> findList();
/**
* Execute the query iterating a row at a time.
@@ -47,6 +59,56 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
*/
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>
@@ -152,6 +214,38 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
*/
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>
@@ -187,7 +281,6 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
*
* @return The PagedList
*/
@Override
PagedList<T> findPagedList();
}
@@ -10,7 +10,6 @@ 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.
@@ -422,26 +421,6 @@ 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>
@@ -1,89 +0,0 @@
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,10 +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.function.Consumer;
import java.util.function.Predicate;
import java.util.Optional;
import java.util.stream.Stream;
/**
@@ -21,12 +22,11 @@ import java.util.stream.Stream;
* @param <D> the target DTO type
*/
@NullMarked
public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
public interface MappedQuery<D> {
/**
* Execute the query returning the mapped DTO list.
*/
@Override
List<D> findList();
/**
@@ -38,7 +38,6 @@ public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
* ({@link PagedList#getTotalCount()}, {@link PagedList#hasNext()}, etc.) reflects the
* underlying entity query and is unaffected by the DTO mapping.
*/
@Override
PagedList<D> findPagedList();
/**
@@ -61,51 +60,33 @@ public interface MappedQuery<D> extends StreamableQuery<MappedQuery<D>, D> {
*
* }</pre>
*/
@Override
Stream<D> findStream();
/**
* 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.
* Execute the query returning a single mapped DTO, or {@code null} if there is no matching row.
*/
@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);
@Nullable
D findOne();
/**
* 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.
* Execute the query returning an optional mapped DTO.
*/
@Override
void findEachWhile(Predicate<D> consumer);
Optional<D> findOneOrEmpty();
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*/
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);
}
+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 QueryBuilder<Query<T>, T> {
public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
/**
* The lock type (strength) to use with query FOR UPDATE row locking.
@@ -2,7 +2,8 @@ package io.ebean;
import org.jspecify.annotations.Nullable;
import jakarta.persistence.EntityNotFoundException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
@@ -10,6 +11,8 @@ 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.
@@ -17,7 +20,7 @@ import java.util.function.Consumer;
* @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>, StreamableQuery<SELF, T> {
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T> {
/**
* Set root table alias.
@@ -143,16 +146,48 @@ 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>
@@ -644,24 +679,88 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
boolean exists();
/**
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
* if there is no matching bean.
* 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.
* <p>
* This is a convenience alternative to:
* <pre>{@code
* query.findOneOrEmpty()
* .orElseThrow(() -> new EntityNotFoundException(...));
*
* // assuming the sku of products is unique...
* Product product =
* new QProduct()
* .sku.equalTo("aa113")
* .findOne();
* ...
* }</pre>
* <p>
* 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.
* 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>
*/
@Override
default T findOneOrThrow() {
return findOneOrEmpty().orElseThrow(() ->
new EntityNotFoundException(getBeanType().getSimpleName() + " not found"));
}
@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();
/**
* Execute the query returning the set of objects.
@@ -806,6 +905,84 @@ 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>
@@ -871,4 +1048,33 @@ 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();
}
+55 -13
View File
@@ -3,6 +3,7 @@ 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;
@@ -40,7 +41,44 @@ import java.util.function.Predicate;
* }</pre>
*/
@NullMarked
public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow> {
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();
/**
* Execute the SqlQuery iterating a row at a time.
@@ -61,6 +99,16 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
*/
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>
@@ -91,6 +139,11 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
*/
void findEachRow(RowConsumer consumer);
/**
* Execute the query returning an optional row.
*/
Optional<SqlRow> findOneOrEmpty();
/**
* Set one of more positioned parameters.
* <p>
@@ -312,7 +365,7 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
*
* @param <T> The type of the scalar values
*/
interface TypeQuery<T> extends FindableQuery<TypeQuery<T>, T> {
interface TypeQuery<T> {
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
@@ -320,38 +373,27 @@ public interface SqlQuery extends Serializable, FindableQuery<SqlQuery, SqlRow>
*
* @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,15 +158,6 @@ 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.
*
@@ -1,101 +0,0 @@
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,15 +84,6 @@ 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>
@@ -5,18 +5,13 @@ package io.ebean.meta;
*/
public abstract class AbstractMetricVisitor implements MetricVisitor {
private final Mode mode;
private final boolean reset;
private final boolean collectTransactionMetrics;
private final boolean collectQueryMetrics;
private final boolean collectL2Metrics;
public AbstractMetricVisitor(boolean reset, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
this(reset ? Mode.RESET : Mode.CUMULATIVE,
collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
}
public AbstractMetricVisitor(Mode mode, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
this.mode = mode;
this.reset = reset;
this.collectTransactionMetrics = collectTransactionMetrics;
this.collectQueryMetrics = collectQueryMetrics;
this.collectL2Metrics = collectL2Metrics;
@@ -24,12 +19,7 @@ public abstract class AbstractMetricVisitor implements MetricVisitor {
@Override
public boolean reset() {
return mode == Mode.RESET;
}
@Override
public Mode mode() {
return mode;
return reset;
}
@Override
@@ -57,3 +47,4 @@ public abstract class AbstractMetricVisitor implements MetricVisitor {
// do nothing by default
}
}
@@ -30,16 +30,7 @@ public class BasicMetricVisitor extends AbstractMetricVisitor implements ServerM
* Construct specifying reset and what to collect.
*/
public BasicMetricVisitor(String name, Function<String,String> naming, boolean reset, boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
this(name, naming, reset ? Mode.RESET : Mode.CUMULATIVE,
collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
}
/**
* Construct specifying the collection mode and what to collect.
*/
public BasicMetricVisitor(String name, Function<String,String> naming, Mode mode,
boolean collectTransactionMetrics, boolean collectQueryMetrics, boolean collectL2Metrics) {
super(mode, collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
super(reset, collectTransactionMetrics, collectQueryMetrics, collectL2Metrics);
this.name = name;
this.naming = naming;
}
@@ -9,11 +9,6 @@ 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.
*/
@@ -7,12 +7,6 @@ import java.util.function.Function;
*/
public interface MetricVisitor {
enum Mode {
RESET,
CUMULATIVE,
DELTA
}
/**
* Return the naming convention that should be applied to the reported metric names.
*/
@@ -23,13 +17,6 @@ public interface MetricVisitor {
*/
boolean reset();
/**
* Return the metric collection mode.
*/
default Mode mode() {
return reset() ? Mode.RESET : Mode.CUMULATIVE;
}
/**
* Return true if we should visit the transaction metrics.
*/
@@ -37,16 +37,6 @@ public interface TimedMetric {
*/
TimedMetricStats collect(boolean reset);
/**
* Collect a snapshot using the given collection mode.
*
* <p>Implementations that do not support delta collection use cumulative
* collection for {@link MetricVisitor.Mode#DELTA}.</p>
*/
default TimedMetricStats collect(MetricVisitor.Mode mode) {
return collect(mode == MetricVisitor.Mode.RESET);
}
/**
* Visit non empty metrics.
*/
@@ -43,18 +43,4 @@ class MetaInfoManagerTest {
assertThat(manager.collectMetrics(false)).isSameAs(metrics);
}
@Test
void basicMetricVisitorSupportsExplicitCollectionModes() {
var reset = new BasicMetricVisitor("db", MetricNamingMatch.INSTANCE, MetricVisitor.Mode.RESET, true, true, true);
var cumulative = new BasicMetricVisitor("db", MetricNamingMatch.INSTANCE, MetricVisitor.Mode.CUMULATIVE, true, true, true);
var delta = new BasicMetricVisitor("db", MetricNamingMatch.INSTANCE, MetricVisitor.Mode.DELTA, true, true, true);
assertThat(reset.reset()).isTrue();
assertThat(reset.mode()).isEqualTo(MetricVisitor.Mode.RESET);
assertThat(cumulative.reset()).isFalse();
assertThat(cumulative.mode()).isEqualTo(MetricVisitor.Mode.CUMULATIVE);
assertThat(delta.reset()).isFalse();
assertThat(delta.mode()).isEqualTo(MetricVisitor.Mode.DELTA);
}
}
-113
View File
@@ -1,113 +0,0 @@
<?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.6.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.6.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.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.6.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.6.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>
@@ -1,223 +0,0 @@
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));
}
}
}
}
@@ -1,9 +0,0 @@
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;
}
@@ -1 +0,0 @@
io.ebean.avajejsonb.mapper.ScalarJsonAvajeJsonbMapper
@@ -1,56 +0,0 @@
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();
}
}
@@ -1,37 +0,0 @@
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);
}
}
@@ -1,130 +0,0 @@
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);
}
}
}
@@ -1,54 +0,0 @@
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;
}
}
@@ -1 +0,0 @@
entity-packages: org.example.avajejsonb
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-bench</artifactId>
+28 -34
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean bom</name>
@@ -89,25 +89,25 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -125,19 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.6.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-avajejsonb-mapper</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -161,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<!-- platforms -->
@@ -199,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -29,7 +29,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.12</version>
<version>42.7.11</version>
<optional>true</optional>
</dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -157,21 +157,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -334,12 +334,6 @@ 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,12 +1938,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
@Override
public int executeNow(SpiSqlUpdate sqlUpdate) {
return executeNow(sqlUpdate, null);
}
@Override
public int executeNow(SpiSqlUpdate sqlUpdate, @Nullable Transaction transaction) {
return persister.executeSqlUpdateNow(sqlUpdate, transaction);
return persister.executeSqlUpdateNow(sqlUpdate, null);
}
@Override
@@ -2,7 +2,6 @@ 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;
@@ -141,7 +140,7 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
server.executeBatch(this, transaction);
return -1;
}
return server.execute(this, transaction);
return server.execute(this);
} else {
// Hopefully this doesn't catch anyone out...
return DB.getDefault().execute(this);
@@ -151,18 +150,12 @@ public final class DefaultSqlUpdate implements Serializable, SpiSqlUpdate {
@Override
public int executeNow() {
if (server != null) {
return server.executeNow(this, transaction);
return server.executeNow(this);
} 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,10 +80,6 @@ public final class DtoMappingRequest {
return name;
}
public String dbName() {
return server.name();
}
public String hash() {
return hash;
}
@@ -15,7 +15,6 @@ 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;
@@ -27,7 +26,6 @@ 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();
@@ -93,6 +91,6 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType, dbName, name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
}
}
@@ -1335,24 +1335,6 @@ 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();
@@ -81,13 +81,6 @@ public final class BatchControl {
*/
private int bufferMax;
/**
* True while the batched requests are being executed. A persist performed from a
* BeanPersistController callback must not flush the batch that is executing it, the same way
* executeNow() stops a query from doing so.
*/
private boolean executing;
private final Queue[] queues = new Queue[2];
static final int DELETE_QUEUE = 0;
@@ -146,9 +139,8 @@ public final class BatchControl {
* to the depth.
*/
public int executeStatementOrBatch(PersistRequest request, boolean batch, boolean addBatch) throws BatchedSqlException {
if (!executing && (!batch || (batchFlushOnMixed && !isBeansEmpty()))) {
// flush when mixing beans and updateSql, unless we are inside the execution of the batch
// itself : flushing then would issue the statements queued behind the current one early
if (!batch || (batchFlushOnMixed && !isBeansEmpty())) {
// flush when mixing beans and updateSql
flush();
}
if (!batch) {
@@ -171,9 +163,8 @@ public final class BatchControl {
* according to the depth (object graph depth).
*/
public int executeOrQueue(PersistRequestBean<?> request, boolean batch) throws BatchedSqlException {
if (!executing && (!batch || (batchFlushOnMixed && !pstmtHolder.isEmpty()))) {
// flush when mixing beans and updateSql, unless we are inside the execution of the batch
// itself : flushing then would issue the statements queued behind the current one early
if (!batch || (batchFlushOnMixed && !pstmtHolder.isEmpty())) {
// flush when mixing beans and updateSql
flush();
}
if (!batch) {
@@ -235,9 +226,7 @@ public final class BatchControl {
void executeNow(ArrayList<PersistRequest> list) throws BatchedSqlException {
boolean old = transaction.isFlushOnQuery();
transaction.setFlushOnQuery(false);
boolean oldExecuting = executing;
executing = true;
// disable flush on query and on persist due transaction callbacks
// disable flush on query due transaction callbacks
try {
for (int i = 0; i < list.size(); i++) {
if (i % batchSize == 0) {
@@ -248,7 +237,6 @@ public final class BatchControl {
}
flushPstmtHolder();
} finally {
executing = oldExecuting;
transaction.setFlushOnQuery(old);
}
}
@@ -4,9 +4,11 @@ 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.
@@ -16,14 +18,12 @@ 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,7 +49,10 @@ final class InsertMetaOptionsPostgres implements InsertMetaOptions {
meta.sql(request, !withId, baseTable, false);
request.append(" on conflict ");
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
String constraintName = options.constraint();
if (constraintName != null) {
@@ -81,7 +84,6 @@ 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,9 +4,11 @@ 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.
@@ -19,14 +21,12 @@ 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,7 +55,10 @@ final class InsertMetaOptionsSqlite implements InsertMetaOptions {
meta.sql(request, !withId, baseTable, false);
request.append(" on conflict (");
List<String> uniqueColumns = InsertMetaOptionsSupport.uniqueColumns(desc, withId);
List<String> uniqueColumns = desc.uniqueProps().stream()
.flatMap(Arrays::stream)
.map(BeanProperty::dbColumn)
.collect(Collectors.toList());
String cols = options.uniqueColumns();
if (cols != null) {
@@ -82,7 +85,6 @@ 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()) {
@@ -1,75 +0,0 @@
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,19 +4,24 @@ 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 ValueAdder count = new ValueAdder();
private final LongAdder count = new LongAdder();
private String reportName;
DCountMetric(String name) {
this.name = name;
}
/**
* Add a value. Usually the value is Time or Bytes etc.
*/
@Override
public void add(long value) {
count.add(value);
@@ -24,12 +29,12 @@ final class DCountMetric implements CountMetric {
@Override
public void increment() {
count.add(1);
count.increment();
}
@Override
public boolean isEmpty() {
return count.currentValue() == 0;
return count.sum() == 0;
}
@Override
@@ -39,25 +44,12 @@ final class DCountMetric implements CountMetric {
@Override
public long get(boolean reset) {
return reset ? count.getAndReset() : count.cumulative();
return reset ? count.sumThenReset() : count.sum();
}
@Override
public void visit(MetricVisitor visitor) {
long val;
switch (visitor.mode()) {
case RESET:
val = count.getAndReset();
break;
case CUMULATIVE:
val = count.cumulative();
break;
case DELTA:
val = count.delta();
break;
default:
throw new IllegalStateException("Unknown metric collection mode");
}
long val = visitor.reset() ? count.sumThenReset() : count.sum();
if (val > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
visitor.visitCount(new DCountMetricStats(name, val));
@@ -20,7 +20,7 @@ final class DQueryPlanMetric implements QueryPlanMetric {
@Override
public void visit(MetricVisitor visitor) {
TimedMetricStats stats = metric.collect(visitor.mode());
TimedMetricStats stats = metric.collect(visitor.reset());
if (stats != null) {
String name = reportName != null ? reportName : reportName(visitor);
visitor.visitQuery(new Stats(name, meta, stats, collected));
@@ -3,6 +3,9 @@ package io.ebeaninternal.server.profile;
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.
* <p>
@@ -12,19 +15,14 @@ import io.ebean.metric.TimedMetric;
final class DTimedMetric implements TimedMetric {
private final String name;
private final ValueAdder count = new ValueAdder();
private final ValueAdder total = new ValueAdder();
private final ValueMax max;
private final LongAdder count = new LongAdder();
private final LongAdder total = new LongAdder();
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
private boolean collected;
private String reportName;
DTimedMetric(String name) {
this(name, new ValueMax());
}
DTimedMetric(String name, ValueMax max) {
this.name = name;
this.max = max;
}
@Override
@@ -34,7 +32,7 @@ final class DTimedMetric implements TimedMetric {
final long mean = totalMicros / batch;
count.add(batch);
total.add(totalMicros);
max.add(mean);
max.accumulate(mean);
}
}
@@ -45,14 +43,14 @@ final class DTimedMetric implements TimedMetric {
@Override
public void add(long value) {
count.add(1);
count.increment();
total.add(value);
max.add(value);
max.accumulate(value);
}
@Override
public boolean isEmpty() {
return count.currentValue() == 0;
return count.sum() == 0;
}
@Override
@@ -64,63 +62,30 @@ final class DTimedMetric implements TimedMetric {
@Override
public void visit(MetricVisitor visitor) {
final DTimeMetricStats stats = collect(visitor.mode());
if (stats != null) {
final long countSum = visitor.reset() ? count.sumThenReset() : count.sum();
if (countSum > 0) {
final String name = reportName != null ? reportName : reportName(visitor);
stats.setName(name);
visitor.visitTimed(stats);
visitor.visitTimed(stats(visitor.reset(), name, countSum));
}
}
@Override
public DTimeMetricStats collect(boolean reset) {
return collect(reset ? MetricVisitor.Mode.RESET : MetricVisitor.Mode.CUMULATIVE);
}
@Override
public DTimeMetricStats collect(MetricVisitor.Mode mode) {
final long maxValue = max.collect();
final long countSum;
switch (mode) {
case RESET:
countSum = count.getAndReset();
break;
case CUMULATIVE:
countSum = count.cumulative();
break;
case DELTA:
countSum = count.delta();
break;
default:
throw new IllegalStateException("Unknown metric collection mode");
}
final long countSum = reset ? count.sumThenReset() : count.sum();
if (countSum == 0) {
return null;
} else {
return stats(mode, name, countSum, maxValue);
return stats(reset, name, countSum);
}
}
/**
* Return the current statistics resetting the internal values if reset is true.
*/
private DTimeMetricStats stats(MetricVisitor.Mode mode, String name, long countSum, long maxValue) {
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
try {
final long totalSum;
switch (mode) {
case RESET:
totalSum = total.getAndReset();
break;
case CUMULATIVE:
totalSum = total.cumulative();
break;
case DELTA:
totalSum = total.delta();
break;
default:
throw new IllegalStateException("Unknown metric collection mode");
}
return new DTimeMetricStats(name, collected, countSum, totalSum, maxValue);
final long totalSum = reset ? total.sumThenReset() : total.sum();
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
} finally {
collected = true;
}
@@ -46,7 +46,7 @@ final class DTimedProfileLocation extends DProfileLocation implements TimedProfi
@Override
public void visit(MetricVisitor visitor) {
TimedMetricStats collect = timedMetric.collect(visitor.mode());
TimedMetricStats collect = timedMetric.collect(visitor.reset());
if (collect != null) {
final String name = reportName != null ? reportName : reportName(visitor, collect.name());
collect.setName(name);
@@ -1,42 +0,0 @@
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 cumulative() {
return value.sum();
}
long delta() {
long currentValue = value.sum();
long previous = previousValue.getAndSet(currentValue);
return currentValue >= previous ? currentValue - previous : currentValue;
}
long getAndReset() {
long currentValue = value.sumThenReset();
previousValue.set(0);
return currentValue;
}
void reset() {
value.reset();
previousValue.set(0);
}
long currentValue() {
return value.sum();
}
}
@@ -1,48 +0,0 @@
package io.ebeaninternal.server.profile;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.function.LongSupplier;
/**
* Accumulates a maximum value and publishes it in rolling 59-second windows.
*/
final class ValueMax {
private static final long WINDOW_NANOS = TimeUnit.SECONDS.toNanos(59);
private final LongSupplier nanoTime;
private final LongAccumulator value;
private volatile long published;
private long lastResetNanos;
ValueMax() {
this(System::nanoTime);
}
ValueMax(LongSupplier nanoTime) {
this.nanoTime = nanoTime;
this.value = new LongAccumulator(Math::max, 0);
this.lastResetNanos = nanoTime.getAsLong() - 2 * WINDOW_NANOS;
}
void add(long amount) {
value.accumulate(amount);
}
synchronized long collect() {
long now = nanoTime.getAsLong();
if (now - lastResetNanos >= WINDOW_NANOS) {
published = value.getThenReset();
lastResetNanos = now;
}
return published;
}
synchronized void reset() {
value.reset();
published = 0;
lastResetNanos = nanoTime.getAsLong() - 2 * WINDOW_NANOS;
}
}
@@ -274,7 +274,7 @@ public class CQueryPlan implements SpiQueryPlan {
@Override
public final DQueryPlanOutput createMeta(String bind, String planString) {
return new DQueryPlanOutput(beanType(), server.name(), name, hash, sql, profileLocation, bind, planString);
return new DQueryPlanOutput(beanType(), name, hash, sql, profileLocation, bind, planString);
}
public DataReader createDataReader(boolean unmodifiable, ResultSet rset) {
@@ -12,7 +12,6 @@ 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;
@@ -26,9 +25,8 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
private Instant whenCaptured;
private Object tenantId;
public DQueryPlanOutput(Class<?> beanType, String dbName, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
this.beanType = beanType;
this.dbName = dbName;
this.label = label;
this.hash = hash;
this.sql = sql;
@@ -37,11 +35,6 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
this.plan = plan;
}
@Override
public String dbName() {
return dbName;
}
@Override
public String hash() {
return hash;
@@ -21,13 +21,11 @@ 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);
}
@@ -79,6 +77,6 @@ public final class SqlQueryPlan implements SpiQueryPlan {
@Override
public SpiDbQueryPlan createMeta(String bind, String planString) {
return new DQueryPlanOutput(null, dbName, name, hash, sql, null, bind, planString);
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
}
}
@@ -11,8 +11,6 @@ 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;
/**
@@ -92,11 +90,6 @@ 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();
@@ -110,27 +103,6 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
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);
@@ -148,9 +120,4 @@ public final class DefaultMappedQuery<T, D> implements MappedQuery<D> {
query.usingConnection(connection);
return this;
}
@Override
public void cancel() {
query.cancel();
}
}
@@ -23,7 +23,6 @@ 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;
@@ -1657,30 +1656,6 @@ 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,7 +1,6 @@
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;
@@ -31,7 +30,6 @@ 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
@@ -90,13 +88,7 @@ public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
@Override
public int execute() {
return server.execute(this, transaction);
}
@Override
public DefaultOrmUpdate<T> usingTransaction(Transaction transaction) {
this.transaction = transaction;
return this;
return server.execute(this);
}
/**
@@ -281,17 +281,6 @@ 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);
@@ -337,17 +326,6 @@ 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,7 +7,6 @@ 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;
@@ -359,54 +358,6 @@ 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.
*
@@ -424,7 +375,6 @@ public final class OrmQueryDetail implements Serializable {
boolean fetchJoinFirstMany = allowOne;
sortFetchPaths(beanDescriptor, addIds);
convertIdFetches(beanDescriptor);
List<FetchEntry> pairs = sortByFetchPreference(beanDescriptor);
for (FetchEntry pair : pairs) {
@@ -151,22 +151,6 @@ 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;
@@ -428,37 +412,6 @@ 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.
*/
@@ -285,9 +285,7 @@ public class TransactionManager implements SpiTransactionManager {
private SpiTransaction createTransaction(TxScope txScope) {
if (txScope.isReadonly()) {
// Honor isolation on read-only scopes (e.g. @Transactional(readOnly=true, isolation=...))
SpiTransaction transaction = createReadOnlyTransaction(null, false);
return transactionFactory.setIsolationLevel(transaction, txScope.getIsolationLevel());
return createReadOnlyTransaction(null, false);
} else {
return createTransaction(true, txScope.getIsolationLevel());
}
@@ -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) : config.getObjectMapper();
this.jsonManager = new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection());
this.objectMapper = (objectMapperPresent) ? initObjectMapper(config) : null;
this.jsonManager = (objectMapperPresent) ? new TypeJsonManager(postgres, objectMapper, config.getJsonMutationDetection()) : null;
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 JSON mapper dependency for " + prop);
throw new IllegalArgumentException("Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper? Jackson ObjectMapper not present for " + prop);
}
if (MutationDetection.DEFAULT == prop.mutationDetection()) {
prop.setMutationDetection(jsonManager.mutationDetection());
@@ -2,7 +2,6 @@ package io.ebeaninternal.server.profile;
import io.ebean.meta.BasicMetricVisitor;
import io.ebean.meta.MetaCountMetric;
import io.ebean.meta.MetricVisitor;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -41,61 +40,4 @@ 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(5);
assertThat(counter.get(true)).isEqualTo(5);
assertThat(counter.get(true)).isEqualTo(0);
}
@Test
void valueAdderSupportsExplicitCollectionOperations() {
var values = new ValueAdder();
values.add(7);
assertThat(values.cumulative()).isEqualTo(7);
assertThat(values.delta()).isEqualTo(7);
values.add(5);
assertThat(values.cumulative()).isEqualTo(12);
assertThat(values.delta()).isEqualTo(5);
assertThat(values.getAndReset()).isEqualTo(12);
assertThat(values.cumulative()).isEqualTo(0);
assertThat(values.delta()).isEqualTo(0);
}
@Test
void visitorCanCollectDeltaWithoutResettingCumulativeValue() {
var counter = new DCountMetric("org.hello");
counter.add(7);
var cumulative = new BasicMetricVisitor("db", naming, MetricVisitor.Mode.CUMULATIVE, true, true, true);
counter.visit(cumulative);
assertThat(cumulative.countMetrics()).hasSize(1);
assertThat(cumulative.countMetrics().get(0).count()).isEqualTo(7);
var delta = new BasicMetricVisitor("db", naming, MetricVisitor.Mode.DELTA, true, true, true);
counter.visit(delta);
assertThat(delta.countMetrics()).hasSize(1);
assertThat(delta.countMetrics().get(0).count()).isEqualTo(7);
counter.add(5);
delta = new BasicMetricVisitor("db", naming, MetricVisitor.Mode.DELTA, true, true, true);
counter.visit(delta);
assertThat(delta.countMetrics()).hasSize(1);
assertThat(delta.countMetrics().get(0).count()).isEqualTo(5);
cumulative = new BasicMetricVisitor("db", naming, MetricVisitor.Mode.CUMULATIVE, true, true, true);
counter.visit(cumulative);
assertThat(cumulative.countMetrics().get(0).count()).isEqualTo(12);
}
}
@@ -5,22 +5,19 @@ import io.ebean.meta.MetaQueryMetric;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
class DQueryPlanMetricTest {
private final AtomicLong nanoTime = new AtomicLong();
Function<String, String> naming = (String name) -> "prefix[" + name.replace('.', '-') + "]";
@Test
void visit() {
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "dto.Object.lab", "lab", null, "sql", "hash");
DTimedMetric metric = new DTimedMetric("org.timed.plan", new ValueMax(nanoTime::get));
DTimedMetric metric = new DTimedMetric("org.timed.plan");
DQueryPlanMetric planMetric = new DQueryPlanMetric(meta, metric);
metric.add(560);
@@ -49,10 +46,10 @@ class DQueryPlanMetricTest {
}
@Test
void visitCumulativePublishesSharedMax() {
void visitCumulativeResetsMax() {
DQueryPlanMeta meta = new DQueryPlanMeta(Object.class, "dto.Object.lab", "lab", null, "sql", "hash");
DTimedMetric metric = new DTimedMetric("org.timed.plan", new ValueMax(nanoTime::get));
DTimedMetric metric = new DTimedMetric("org.timed.plan");
DQueryPlanMetric planMetric = new DQueryPlanMetric(meta, metric);
metric.add(560);
@@ -77,7 +74,7 @@ class DQueryPlanMetricTest {
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
assertThat(result.get(0).count()).isEqualTo(2);
assertThat(result.get(0).total()).isEqualTo(820);
assertThat(result.get(0).max()).isEqualTo(560);
assertThat(result.get(0).max()).isEqualTo(0);
}
metric.add(410);
@@ -90,14 +87,7 @@ class DQueryPlanMetricTest {
assertThat(result.get(0).name()).isEqualTo("prefix[dto-Object-lab]");
assertThat(result.get(0).count()).isEqualTo(3);
assertThat(result.get(0).total()).isEqualTo(1230);
assertThat(result.get(0).max()).isEqualTo(560);
assertThat(result.get(0).max()).isEqualTo(410);
}
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(59));
BasicMetricVisitor visitor = new BasicMetricVisitor("v", naming, false, true, true, true);
planMetric.visit(visitor);
List<MetaQueryMetric> result = visitor.queryMetrics();
assertThat(result).hasSize(1);
assertThat(result.get(0).max()).isEqualTo(410);
}
}
@@ -5,20 +5,16 @@ import io.ebean.meta.MetaTimedMetric;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
public class DTimedMetricTest {
private final AtomicLong nanoTime = new AtomicLong();
@Test
public void addSinceNanos() throws InterruptedException {
DTimedMetric metric = new DTimedMetric("addSinceNanos", new ValueMax(nanoTime::get));
DTimedMetric metric = new DTimedMetric("addSinceNanos");
long start = System.nanoTime();
Thread.sleep(11);
@@ -32,7 +28,6 @@ public class DTimedMetricTest {
metric.addSinceNanos(start);
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(59));
stats = metric.collect(true);
assertThat(stats.count()).isEqualTo(1);
assertThat(stats.total()).isGreaterThan(10);
@@ -42,7 +37,7 @@ public class DTimedMetricTest {
@Test
public void addBatchSince() throws InterruptedException {
DTimedMetric metric = new DTimedMetric("addSinceNanos", new ValueMax(nanoTime::get));
DTimedMetric metric = new DTimedMetric("addSinceNanos");
long start = System.nanoTime();
Thread.sleep(11);
@@ -57,7 +52,6 @@ public class DTimedMetricTest {
metric.addBatchSince(start, 2);
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(59));
stats = metric.collect(true);
assertThat(stats.count()).isEqualTo(2);
assertThat(stats.total()).isGreaterThan(10000);
@@ -98,8 +92,8 @@ public class DTimedMetricTest {
}
@Test
void collectCumulativePublishesSharedMax() {
DTimedMetric metric = new DTimedMetric("org.timed", new ValueMax(nanoTime::get));
void collectCumulativeResetsMax() {
DTimedMetric metric = new DTimedMetric("org.timed");
metric.add(560);
metric.add(500);
@@ -111,7 +105,7 @@ public class DTimedMetricTest {
stats = metric.collect(false);
assertThat(stats.count()).isEqualTo(2);
assertThat(stats.total()).isEqualTo(1060);
assertThat(stats.max()).isEqualTo(560);
assertThat(stats.max()).isEqualTo(0);
metric.add(160);
metric.add(100);
@@ -120,31 +114,6 @@ public class DTimedMetricTest {
stats = metric.collect(false);
assertThat(stats.count()).isEqualTo(5);
assertThat(stats.total()).isEqualTo(1470);
assertThat(stats.max()).isEqualTo(560);
nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(59));
stats = metric.collect(false);
assertThat(stats.max()).isEqualTo(160);
}
@Test
void cumulativeAndDeltaAreIndependent() {
DTimedMetric metric = new DTimedMetric("org.timed", new ValueMax(nanoTime::get));
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);
assertThat(delta.max()).isEqualTo(560);
cumulative = metric.collect(false);
assertThat(cumulative).isNull();
}
}
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean ddl generation</name>
@@ -28,14 +28,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -65,7 +65,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -15,7 +15,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -1,19 +1,14 @@
package io.ebean.core.type;
package io.ebean.jackson.mapper;
/**
* Helper that removes whitespace from JSON content.
* <p>
* Used to normalise PostgreSQL JSONB content before it is retained for mutation detection.
* Helper that removes whitespace from JSON. Used to normalise Postgres JSONB content.
*/
public final class JsonTrim {
private JsonTrim() {
}
final class JsonTrim {
/**
* Return JSON with whitespace trimmed.
*/
public static String trim(String json) {
static String trim(String json) {
if (json == null) {
return null;
}
@@ -23,7 +18,7 @@ public 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 {
@@ -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.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-opentelemetry</artifactId>
@@ -28,7 +28,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -71,21 +71,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean pgvector types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean postgis types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -47,7 +47,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.12</version>
<version>42.7.11</version>
<scope>provided</scope>
</dependency>
@@ -62,7 +62,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.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.6.0</version>
<version>18.2.0</version>
</parent>
<name>ebean querybean</name>
@@ -17,7 +17,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -59,14 +59,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -80,7 +80,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -738,11 +738,6 @@ 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.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-redis</artifactId>
@@ -29,35 +29,35 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -3,7 +3,7 @@ ebean:
dumpMetricsOptions: sql,hash,loc
test:
registerTestTenantProvider: false
redis: 8.6.2
redis: latest
# 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.6.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-redisson</artifactId>
@@ -29,35 +29,35 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.6.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -10,13 +10,13 @@ import jakarta.persistence.Entity;
@SuppressWarnings("unused")
@Cache(naturalKey = {"one", "two"})
@Entity
public class FOtherOne extends EBase {
public class OtherOne extends EBase {
private final String one;
private final String two;
private String notes;
public FOtherOne(String one, String two, String notes) {
public OtherOne(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 FPerson extends EBase {
public class Person extends EBase {
public enum Status {
NEW,
@@ -33,7 +33,7 @@ public class FPerson extends EBase {
*/
String key;
public FPerson(String name) {
public Person(String name) {
this.name = name;
this.status = Status.NEW;
}

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