Compare commits

..
142 changed files with 719 additions and 5084 deletions
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.3.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.3.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**
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-bench</artifactId>
+28 -28
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -125,13 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.3.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) {
@@ -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();
@@ -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();
}
}
@@ -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.
*/
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.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.3.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.3.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.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.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.3.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.3.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.3.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.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.3.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.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
@@ -62,7 +62,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.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.3.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.3.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.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.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.3.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.3.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.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
+6 -6
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-spring-txn</artifactId>
@@ -28,14 +28,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.1</version>
<version>2.22.0</version>
<scope>test</scope>
</dependency>
@@ -77,7 +77,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
</parent>
<name>ebean test</name>
@@ -33,20 +33,20 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -149,14 +149,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -849,11 +849,6 @@ public class TDSpiEbeanServer extends TDSpiServer implements SpiEbeanServer {
return 0;
}
@Override
public int executeNow(SpiSqlUpdate sqlUpdate, Transaction transaction) {
return 0;
}
@Override
public void addBatch(SpiSqlUpdate sqlUpdate, SpiTransaction transaction) {
}
@@ -629,7 +629,7 @@ class EqlParserTest extends BaseTestCase {
List<OrderDetail> details = query.findList();
assertThat(details).isNotEmpty();
assertSql(query).contains("select sum(t0.order_qty), t0.order_id from o_order_detail t0 group by t0.order_id");
assertSql(query).contains("select sum(t0.order_qty), t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id group by t1.id");
}
@Test
@@ -1,7 +1,6 @@
package org.tests.basic;
import io.ebean.DB;
import io.ebean.Transaction;
import io.ebean.Update;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.AfterEach;
@@ -73,29 +72,4 @@ public class TestUpdate extends BaseTestCase {
Customer cust = DB.find(Customer.class).where().eq("name", "testUpdate3").findOne();
assertThat(cust.getSmallnote()).isEqualTo("Note #3");
}
@Test
public void testUsingTransaction_explicitNonAmbientTransaction() {
Update<Customer> update = DB.createUpdate(Customer.class,
"update customer set smallnote = :smallnote where name = :name")
.setParameter("name", "testUpdate1")
.setParameter("smallnote", "explicit txn note");
// ambient/current transaction on the thread - deliberately rolled back
try (Transaction ambient = DB.beginTransaction()) {
// explicit transaction - NOT the ambient/current one on the thread
try (Transaction explicit = DB.createTransaction()) {
update.usingTransaction(explicit);
int rows = update.execute();
assertThat(rows).isEqualTo(1);
explicit.commit();
}
// rolling back the ambient transaction does not affect the update - it ran
// against the explicit transaction which has already been committed above
ambient.rollback();
}
Customer cust = DB.find(Customer.class).where().eq("name", "testUpdate1").findOne();
assertThat(cust.getSmallnote()).isEqualTo("explicit txn note");
}
}
@@ -1,61 +0,0 @@
package org.tests.insert;
import io.ebean.annotation.Index;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import java.time.Instant;
/**
* Used to verify that insert on conflict do update excludes insert-only generated
* properties (such as {@code @WhenCreated}) from the generated update set clause.
*/
@Entity
public class EConflictWithCreated {
@Id
Long id;
@Index(unique = true)
String code;
@WhenCreated
Instant whenCreated;
@WhenModified
Instant whenUpdated;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Instant getWhenCreated() {
return whenCreated;
}
public void setWhenCreated(Instant whenCreated) {
this.whenCreated = whenCreated;
}
public Instant getWhenUpdated() {
return whenUpdated;
}
public void setWhenUpdated(Instant whenUpdated) {
this.whenUpdated = whenUpdated;
}
}
@@ -43,34 +43,6 @@ class TestInsertOnConflict extends BaseTestCase {
.build());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
@Test
void insertOnConflictUpdate_fallsBackToPrimaryKey_whenNoUniqueColumnsMapped() {
Database db = DB.getDefault();
LoggedSql.start();
var entity1 = new EStrIdBean();
entity1.setId("fallback-1");
entity1.setName("Example");
// no uniqueColumns()/constraint() explicitly set - id is not mapped @Column(unique=true)
// or @Index(unique=true) so this should fall back to using the primary key (id) column
db.insert(entity1, ON_CONFLICT_UPDATE);
var entity2 = new EStrIdBean();
entity2.setId("fallback-1");
entity2.setName("Updated");
db.insert(entity2, ON_CONFLICT_UPDATE);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
assertThat(sql.get(0)).contains("on conflict (id) do update set");
assertThat(sql.get(1)).contains("on conflict (id) do update set");
EStrIdBean found = db.find(EStrIdBean.class, "fallback-1");
assertThat(found).isNotNull();
assertThat(found.name()).isEqualTo("Updated");
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
@Test
void insertOnConflictUpdateExplicitTransaction() {
@@ -335,35 +307,6 @@ class TestInsertOnConflict extends BaseTestCase {
assertThat(list.get(0).getWhenUpdated()).isEqualTo(bean.getWhenUpdated());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE, Platform.SQLITE})
@Test
void insertOnConflictUpdate_excludesWhenCreatedFromUpdateSet() {
Database db = DB.getDefault();
db.truncate(EConflictWithCreated.class);
LoggedSql.start();
var bean = new EConflictWithCreated();
bean.setCode("abc");
db.insert(bean, ON_CONFLICT_UPDATE);
var bean2 = new EConflictWithCreated();
bean2.setCode("abc");
db.insert(bean2, ON_CONFLICT_UPDATE);
var sql = LoggedSql.stop();
assertThat(sql).hasSize(2);
// when_created is not included in the "do update set" clause - it is insert only
assertThat(sql.get(0)).contains("on conflict (code) do update set when_updated=excluded.when_updated");
assertThat(sql.get(0)).doesNotContain("when_created=excluded.when_created");
assertThat(sql.get(1)).contains("on conflict (code) do update set when_updated=excluded.when_updated");
assertThat(sql.get(1)).doesNotContain("when_created=excluded.when_created");
List<EConflictWithCreated> list = db.find(EConflictWithCreated.class).findList();
assertThat(list).hasSize(1);
// original whenCreated value is preserved rather than being overwritten by the 2nd insert
assertThat(list.get(0).getWhenCreated()).isEqualTo(bean.getWhenCreated());
}
@ForPlatform({Platform.POSTGRES, Platform.YUGABYTE})
@Test
void updateQueryReturning() throws SQLException {
@@ -93,7 +93,7 @@ public class TestMergeCustomer extends BaseTestCase {
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(6);
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
assertSql(sql.get(1)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
assertSqlBind(sql, 2, 3);
assertThat(sql.get(5)).contains("update mcustomer set name=?, version=?, shipping_address_id=?, billing_address_id=? where id=? and version=?");
@@ -122,7 +122,7 @@ public class TestMergeCustomer extends BaseTestCase {
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(8);
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
assertSql(sql.get(1)).contains("insert into maddress (id, street, city, version) values (?,?,?,?)");
assertSqlBind(sql.get(2));
assertThat(sql.get(4)).contains("update maddress set street=?, city=?, version=? where id=? and version=?");
@@ -155,7 +155,7 @@ public class TestMergeCustomer extends BaseTestCase {
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(9);
assertSql(sql.get(0)).contains("select t0.id, t0.billing_address_id, t0.shipping_address_id from mcustomer t0 where t0.id = ?");
assertSql(sql.get(0)).contains("select t0.id, t2.id, t1.id from mcustomer t0 left join maddress t2 on t2.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id where t0.id = ?");
// Additional check to see if the address with the unknown UUID is 'insert' or 'update'
assertSql(sql.get(1)).contains("select t0.id from maddress t0 where t0.id = ?");
@@ -317,7 +317,7 @@ public class TestMergeCustomer extends BaseTestCase {
List<String> sql = LoggedSql.stop();
if (isPersistBatchOnCascade()) {
assertSql(sql.get(0)).contains("select t0.id, t0.shipping_address_id, t0.billing_address_id, t1.id from mcustomer t0 left join mcontact t1 on t1.customer_id = t0.id where t0.id = ?");
assertSql(sql.get(0)).contains("select t0.id, t3.id, t1.id, t2.id from mcustomer t0 left join maddress t3 on t3.id = t0.shipping_address_id left join maddress t1 on t1.id = t0.billing_address_id left join mcontact t2 on t2.customer_id = t0.id where t0.id = ?");
if (isH2() || isHana()) {
// with nested OneToMany .. we need a second query to read the contact message ids
assertSql(sql.get(1)).contains("select t0.contact_id, t0.id from mcontact_message t0 where (t0.contact_id) in (?,?,?,?,?,?,?,?,?,?)");
@@ -1,64 +0,0 @@
package org.tests.query;
import io.ebean.DB;
import io.ebean.Query;
import io.ebean.text.PathProperties;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Order;
import org.tests.model.basic.ResetBasicData;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Fetching only the id of a *ToOne association should not require a join - the
* foreign key column is already present on the owning table (issue #3643).
*/
public class TestFetchIdOnly extends BaseTestCase {
@Test
public void test_withFetchPath() {
ResetBasicData.reset();
Query<Order> query = DB.find(Order.class)
.apply(PathProperties.parse("status,customer(id)"));
query.findList();
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
}
@Test
public void test_withSelect() {
ResetBasicData.reset();
Query<Order> query = DB.find(Order.class)
.select("status, customer");
query.findList();
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
}
@Test
public void test_withFetch() {
ResetBasicData.reset();
Query<Order> query = DB.find(Order.class)
.select("status")
.fetch("customer", "id");
query.findList();
assertSql(query.getGeneratedSql()).contains("select t0.id, t0.status, t0.kcustomer_id from o_order t0");
}
@Test
public void test_withFetch_whenIncludesMoreThanId_expectJoin() {
ResetBasicData.reset();
Query<Order> query = DB.find(Order.class)
.select("status")
.fetch("customer", "id, name");
query.findList();
assertThat(query.getGeneratedSql()).contains("join");
}
}
@@ -1,70 +0,0 @@
package org.tests.query;
import io.ebean.DB;
import io.ebean.xtest.BaseTestCase;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Customer;
import org.tests.model.basic.ResetBasicData;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@code findOneOrThrow()} - a convenience alternative to
* {@code findOneOrEmpty().orElseThrow(...)} that produces a decent default message
* when the query is effectively a find-by-id or single natural/unique key lookup.
*/
class TestFindOneOrThrow extends BaseTestCase {
@Test
void findOneOrThrow_whenFound_returnsBean() {
ResetBasicData.reset();
Customer existing = DB.find(Customer.class).setMaxRows(1).findList().get(0);
Customer found = DB.find(Customer.class).setId(existing.getId()).findOneOrThrow();
assertThat(found.getId()).isEqualTo(existing.getId());
}
@Test
void findOneOrThrow_whenNotFoundById_messageIncludesId() {
ResetBasicData.reset();
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999).findOneOrThrow())
.isInstanceOf(EntityNotFoundException.class)
.hasMessage("Customer not found for id: 999999");
}
@Test
void findOneOrThrow_whenNotFoundBySingleEqPredicate_messageIncludesPredicate() {
ResetBasicData.reset();
assertThatThrownBy(() -> DB.find(Customer.class)
.where().eq("name", "NonExistentCustomerXYZ")
.findOneOrThrow())
.isInstanceOf(EntityNotFoundException.class)
.hasMessage("Customer not found for name: NonExistentCustomerXYZ");
}
@Test
void findOneOrThrow_whenNotFoundByMultiplePredicates_fallsBackToGenericMessage() {
ResetBasicData.reset();
assertThatThrownBy(() -> DB.find(Customer.class)
.where().eq("name", "NonExistentCustomerXYZ").eq("id", 999999)
.findOneOrThrow())
.isInstanceOf(EntityNotFoundException.class)
.hasMessage("Customer not found");
}
@Test
void findOneOrThrow_withSupplier_usesSuppliedException() {
ResetBasicData.reset();
assertThatThrownBy(() -> DB.find(Customer.class).setId(999999)
.findOneOrThrow(() -> new IllegalStateException("custom message")))
.isInstanceOf(IllegalStateException.class)
.hasMessage("custom message");
}
}
@@ -462,11 +462,7 @@ public class TestQueryFilterMany extends BaseTestCase {
// nested "group.name" reference forces this to a query join so the filter is applied
// as a genuine WHERE clause (not misapplied to a LEFT JOIN's ON clause)
assertThat(sql).hasSize(2);
if (isPostgresCompatible()) {
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) = any(");
} else {
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) in (");
}
assertSql(sql.get(1)).contains(" from contact t0 left join contact_group t1 on t1.id = t0.group_id where (t0.customer_id) in (");
assertSql(sql.get(1)).contains(" and t1.name = ? and t0.cretime is not null");
}
}
@@ -58,11 +58,7 @@ public class TestQueryFilterManySimple extends BaseTestCase {
// nested "customer.status" reference forces this to a query join so the filter is
// applied as a genuine WHERE clause (not misapplied to a LEFT JOIN's ON clause)
assertThat(sql).hasSize(2);
if (isPostgresCompatible()) {
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) = any(");
} else {
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) in (");
}
assertThat(sql.get(1)).contains("from o_order t0 join o_customer t1 on t1.id = t0.kcustomer_id where t0.order_date is not null and (t0.kcustomer_id) in (");
assertThat(sql.get(1)).contains(" and t1.status = ?");
}
}
@@ -75,9 +75,9 @@ public class TestSubQuery extends BaseTestCase {
Query<OrderDetail> debugSq = sq.copy();
debugSq.findSingleAttribute();
if (isPostgresCompatible()) {
assertThat(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id = any(?)");
assertThat(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id = any(?)");
} else {
assertSql(debugSq.getGeneratedSql()).isEqualTo("select t0.order_id from o_order_detail t0 where t0.product_id in (?)");
assertSql(debugSq.getGeneratedSql()).isEqualTo("select t1.id from o_order_detail t0 join o_order t1 on t1.id = t0.order_id where t0.product_id in (?)");
}
Query<Order> query = DB.find(Order.class).select("shipDate").where().isIn("id", sq).query();
@@ -195,58 +195,4 @@ public class TestSqlUpdateInTxn extends BaseTestCase {
}
@Test
public void testUsingTransaction_explicitNonAmbientTransaction() {
AuditLog log = new AuditLog();
log.setDescription("initial");
DB.save(log);
SqlUpdate update = DB.sqlUpdate("update audit_log set description = ? where id = ?")
.setParameter(1, "explicit")
.setParameter(2, log.getId());
// ambient/current transaction on the thread - deliberately rolled back
try (Transaction ambient = DB.beginTransaction()) {
// explicit transaction - NOT the ambient/current one on the thread
try (Transaction explicit = DB.createTransaction()) {
update.usingTransaction(explicit);
int rows = update.execute();
assertThat(rows).isEqualTo(1);
explicit.commit();
}
// rolling back the ambient transaction does not affect the update - it ran
// against the explicit transaction which has already been committed above
ambient.rollback();
}
AuditLog reloaded = DB.find(AuditLog.class, log.getId());
assertThat(reloaded.getDescription()).isEqualTo("explicit");
}
@Test
public void testUsingTransaction_executeNow_explicitNonAmbientTransaction() {
AuditLog log = new AuditLog();
log.setDescription("initial");
DB.save(log);
SqlUpdate update = DB.sqlUpdate("update audit_log set description = ? where id = ?")
.setParameter(1, "explicitNow")
.setParameter(2, log.getId());
try (Transaction ambient = DB.beginTransaction()) {
try (Transaction explicit = DB.createTransaction()) {
update.usingTransaction(explicit);
int rows = update.executeNow();
assertThat(rows).isEqualTo(1);
explicit.commit();
}
ambient.rollback();
}
AuditLog reloaded = DB.find(AuditLog.class, log.getId());
assertThat(reloaded.getDescription()).isEqualTo("explicitNow");
}
}
+2 -2
View File
@@ -15,8 +15,8 @@ mvn -T 4 clean package
mvn -T 4 deploy -pl '!composites,!platforms' -Pcentral -DskipTests
## git commit, git tag, git push --tags
git commit -am 'Version 18.3.0'
git tag 18.3.0
git commit -am 'Version 18.2.0'
git tag 18.2.0
git push --tags
### convert to javax
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
</parent>
<name>kotlin querybean generator</name>
@@ -21,7 +21,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -35,7 +35,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -56,14 +56,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
+14 -14
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,67 +16,67 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hsqldb</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlanywhere</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<!-- Provided scope so that the H2HistoryTrigger can live in Ebean core
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,13 +16,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.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.3.0</version>
<version>18.2.0</version>
</parent>
<artifactId>platforms</artifactId>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -9,7 +9,7 @@
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<packaging>pom</packaging>
<name>ebean parent</name>
@@ -45,7 +45,7 @@
<ebean-persistence-api.version>3.2</ebean-persistence-api.version>
<ebean-types.version>3.0</ebean-types.version>
<ebean-annotation.version>8.11</ebean-annotation.version>
<ebean-annotation.version>8.6</ebean-annotation.version>
<ebean-ddl-runner.version>2.3</ebean-ddl-runner.version>
<ebean-migration-auto.version>1.2</ebean-migration-auto.version>
<ebean-migration.version>14.3.0</ebean-migration.version>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<version>18.2.0</version>
</parent>
<name>querybean generator</name>
@@ -45,7 +45,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
@@ -34,7 +34,6 @@ interface Constants {
String DTO_PATH = "io.ebean.annotation.DtoPath";
String DTO_REF = "io.ebean.annotation.DtoRef";
String DTO_CONVERT = "io.ebean.annotation.DtoConvert";
String DTO_CONVERTERS = "io.ebean.annotation.DtoConverters";
String DTO_MIXIN = "io.ebean.annotation.DtoMixin";
String DTO_CONVERTER_MANAGER = "io.ebean.DtoConverterManager";
String METAINF_SERVICES_DTOMAPPERREGISTER = "META-INF/services/io.ebean.config.DtoMapperRegister";
@@ -18,7 +18,6 @@ class DtoBeanMeta {
private final List<DtoPropertyMeta> properties = new ArrayList<>();
private final List<DtoMapperVariant> variants = new ArrayList<>();
private DtoBuilderMeta builderMeta;
private DtoSetterMeta setterMeta;
/** Cycle detection state - see {@link DtoMappingReader#checkForCycles()}. */
enum Visit { WHITE, GRAY, BLACK }
@@ -33,13 +32,11 @@ class DtoBeanMeta {
*/
private boolean nestedElsewhere;
DtoBeanMeta(TypeElement source, TypeElement target, String mapperPackage, String mapperName) {
DtoBeanMeta(TypeElement source, TypeElement target, String mapperPackage) {
this.source = source;
this.target = target;
this.mapperPackage = mapperPackage;
this.mapperShortName = mapperName != null && !mapperName.isEmpty()
? mapperName
: target.getSimpleName() + "Mapper";
this.mapperShortName = target.getSimpleName().toString() + "Mapper";
this.mapperFullName = mapperPackage + "." + mapperShortName;
}
@@ -100,20 +97,6 @@ class DtoBeanMeta {
this.builderMeta = builderMeta;
}
/**
* The detected+selected setter-based construction path for this target, or {@code null} if not
* applicable - see {@link DtoMappingReader#resolveSetter(DtoBeanMeta, String)}. Mutually
* exclusive with {@link #builderMeta()} - a builder, when selected, always takes priority (see
* {@link DtoMappingReader#resolveAndValidate()}).
*/
DtoSetterMeta setterMeta() {
return setterMeta;
}
void setterMeta(DtoSetterMeta setterMeta) {
this.setterMeta = setterMeta;
}
/**
* Return the distinct instance-dispatch {@code @DtoConvert} converters used by this mapper's
* properties (static-dispatch converters need no field/constructor wiring, so are excluded),
@@ -36,7 +36,7 @@ final class DtoMapperVariant {
return suffix;
}
/** Dto field names ({@code NESTED_ONE}/{@code NESTED_MANY}, or non-primitive {@code SCALAR}) this variant excludes. */
/** Dto field names (of {@code NESTED_ONE}/{@code NESTED_MANY} properties) this variant excludes. */
Set<String> excludedProperties() {
return excludedProperties;
}
@@ -41,7 +41,6 @@ class DtoMapperWriter {
writeConstructors();
writeFetchGroupMethod();
writeMapMethod();
writeMapToBuilderMethod();
if (!meta.variants().isEmpty()) {
writeBuildMethod();
writeVariantAccessors();
@@ -61,9 +60,6 @@ class DtoMapperWriter {
Set<String> allReferenced = new LinkedHashSet<>();
allReferenced.add(meta.sourceFullName());
allReferenced.add(meta.targetFullName());
if (meta.builderMeta() != null) {
allReferenced.add(meta.builderMeta().builderTypeFullName());
}
for (DtoPropertyMeta property : meta.properties()) {
if (property.nested() != null) {
allReferenced.add(property.nested().sourceFullName());
@@ -108,16 +104,9 @@ class DtoMapperWriter {
if (!meta.converterDeps().isEmpty()) {
imports.add("io.ebean.DtoConverterManager");
}
if (meta.properties().stream().anyMatch(DtoPropertyMeta::usesMapperSupport)) {
imports.add("io.ebean.DtoMapperSupport");
}
if (variableProperties().stream().anyMatch(DtoPropertyMeta::isListTarget)
|| meta.properties().stream().anyMatch(p -> p.isIgnored() && p.isListTarget())) {
if (variableProperties().stream().anyMatch(p -> p.kind() == DtoPropertyMeta.Kind.NESTED_MANY)) {
imports.add("java.util.List");
}
if (meta.builderMeta() != null) {
addImportIfNeeded(imports, meta.builderMeta().builderTypeFullName());
}
for (String imp : imports) {
writer.append("import %s;", imp).eol();
}
@@ -227,66 +216,30 @@ class DtoMapperWriter {
private List<String> fetchGroupChainCalls(Set<String> excludedFieldNames) {
List<DtoPropertyMeta> activeProperties = new ArrayList<>();
for (DtoPropertyMeta property : meta.properties()) {
if (!excludedFieldNames.contains(property.dtoFieldName()) && !property.isIgnored()) {
if (!excludedFieldNames.contains(property.dtoFieldName())) {
activeProperties.add(property);
}
}
Set<String> nestedAssocPaths = new LinkedHashSet<>();
for (DtoPropertyMeta property : activeProperties) {
if ((property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY)
&& !property.hasComputedSegment()) {
nestedAssocPaths.add(property.sourcePropertyPath().get(0));
} else if (property.kind() == DtoPropertyMeta.Kind.SCALAR && property.isListTarget()
&& !property.hasComputedSegment() && property.sourcePropertyPath().size() == 1) {
// a single-segment SCALAR property whose DTO field is a List with no registered nested
// DTO mapping of its own (e.g. @DtoConvert reducing a ToMany association) - still fully
// fetches its association path via a bare fetch(path) call below, so any other property
// targeting a sub-path of the same association must not also try a narrower select.
if (property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) {
nestedAssocPaths.add(property.sourcePropertyPath().get(0));
}
}
Set<String> rootSelect = new LinkedHashSet<>();
Map<String, List<String>> pathSelect = new LinkedHashMap<>();
Set<String> extraFetchPaths = new LinkedHashSet<>();
List<String> fetchCalls = new ArrayList<>();
for (DtoPropertyMeta property : activeProperties) {
switch (property.kind()) {
case NESTED_ONE:
case NESTED_MANY:
if (property.hasComputedSegment()) {
// a single-hop @DtoPath rename traversing a computed/derived getter (no backing
// field) that happens to target a nested DTO type - just as unfetchable via
// fetch(path, mapper.fetchGroup()) as the analogous SCALAR case, since "path" here
// isn't a real Ebean fetch path either. The nested mapper is still invoked directly
// against whatever the getter returns (see DtoMapperWriter#propertyValueExpression) -
// it's purely the FetchGroup derivation that must fall back to @DtoPath#requires().
extraFetchPaths.addAll(property.requiredFetchPaths());
break;
}
fetchCalls.add(String.format("fetch(\"%s\", %s.fetchGroup())",
property.sourcePropertyPath().get(0), mapperFieldName(property)));
break;
case SCALAR:
if (property.hasComputedSegment()) {
// the path traverses a computed/derived getter (no backing field) - its own segments
// past that point aren't real Ebean fetch paths, so don't add them to pathSelect/
// rootSelect at all; @DtoPath#requires() (plus the real prefix, if any) already names
// exactly what needs fetching instead - see DtoPropertyMeta#requiredFetchPaths().
extraFetchPaths.addAll(property.requiredFetchPaths());
break;
}
List<String> path = property.sourcePropertyPath();
if (path.size() == 1) {
if (property.isListTarget()) {
// a single-segment path whose DTO field type is a List, but with no registered
// nested DTO mapping of its own (e.g. a @DtoConvert-backed property reducing a
// ToMany association to a simpler element type) - the source side is still a real
// ToMany association, which needs an actual fetch/join (like NESTED_MANY below),
// not a plain root select() (that only works for scalar columns on the base table).
fetchCalls.add(String.format("fetch(\"%s\")", path.get(0)));
} else {
rootSelect.add(path.get(0));
}
rootSelect.add(path.get(0));
} else {
String fetchPath = String.join(".", path.subList(0, path.size() - 1));
if (nestedAssocPaths.contains(fetchPath)) {
@@ -301,15 +254,6 @@ class DtoMapperWriter {
break;
case REF:
default:
if (property.hasComputedSegment()) {
// the association has no backing field (a computed/derived getter) - "assoc" isn't a
// real Ebean property name, so it can't be handed to FetchGroup.select(...) directly;
// @DtoRef#requires() already names exactly what needs fetching instead - see
// DtoPropertyMeta#requiredFetchPaths(). The value mapping itself (source.getAssoc().
// getId()) still works via plain Java method invocation regardless.
extraFetchPaths.addAll(property.requiredFetchPaths());
break;
}
String assoc = property.sourcePropertyPath().get(0);
if (!nestedAssocPaths.contains(assoc)) {
rootSelect.add(assoc);
@@ -318,27 +262,8 @@ class DtoMapperWriter {
}
}
for (var entry : pathSelect.entrySet()) {
if (extraFetchPaths.contains(entry.getKey())) {
// an unrelated computed-segment property also needs a bare, full fetch(path) at this
// exact same path (emitted below) - FetchGroup's builder REPLACES (not merges) same-path
// fetch calls (OrmQueryDetail.fetch(...) is a plain Map.put keyed by path), so emitting
// both a narrowed fetch(path, "props") here and a bare fetch(path) below would leave
// only whichever call happens to be added last in effect, silently discarding the other's
// requirement depending on emission order. Skip the narrow entry - a full fetch(path) is
// always a safe superset of any narrower property selection, so let the bare fetch below
// win deterministically instead of depending on iteration order.
continue;
}
fetchCalls.add(String.format("fetch(\"%s\", \"%s\")", entry.getKey(), String.join(",", entry.getValue())));
}
for (String extraPath : extraFetchPaths) {
// already covered by another property's NESTED_ONE/MANY fetch of the exact same path (a
// full nested mapper.fetchGroup()) - that's richer than a bare fetch(path) (which would
// replace it and lose the nested mapper's own fetch requirements), so it must win instead.
if (!nestedAssocPaths.contains(extraPath)) {
fetchCalls.add(String.format("fetch(\"%s\")", extraPath));
}
}
List<String> calls = new ArrayList<>();
if (!rootSelect.isEmpty()) {
calls.add(String.format("select(\"%s\")", String.join(",", rootSelect)));
@@ -415,18 +340,7 @@ class DtoMapperWriter {
writer.append(" if (source == null) {").eol();
writer.append(" return null;").eol();
writer.append(" }").eol();
if (meta.builderMeta() != null) {
// the base (full) mapping is always exactly mapToBuilder(...).build() - shares that one
// chain rather than re-emitting a second copy of it here, whether or not named variants
// exist (build(...), below, is only ever needed for a named variant's own partial view)
if (meta.nestedElsewhere()) {
writer.append(" // dedup using DtoMapContext, same %s instance can be reached via more than one path in the graph", sourceShort).eol();
writer.append(" return context.computeIfAbsent(%s.class, source, s -> mapToBuilder(s, context).build());", targetShort).eol();
} else {
writer.append(" // DtoMapContext for nested mappers only").eol();
writer.append(" return mapToBuilder(source, context).build();").eol();
}
} else if (!variableProps.isEmpty()) {
if (!variableProps.isEmpty()) {
// named variants exist - route construction through the shared build(...) method, this
// (base) mapping includes every variable property (each still evaluated inline, at its own
// declared position, inside build() - see buildCallArgs())
@@ -440,139 +354,45 @@ class DtoMapperWriter {
}
} else if (meta.nestedElsewhere()) {
writer.append(" // dedup using DtoMapContext, same %s instance can be reached via more than one path in the graph", sourceShort).eol();
writeConstructionExpression(true, variableProps);
writeConstructionExpression(
String.format(" return context.computeIfAbsent(%s.class, source, s -> ", targetShort),
"s", variableProps, ")");
} else if (nestedProperties().isEmpty()) {
writer.append(" // skip DtoMapContext, only ever a top-level mapping").eol();
writeConstructionExpression(false, variableProps);
writeConstructionExpression(" return ", "source", variableProps, "");
} else {
writer.append(" // DtoMapContext for nested mappers only").eol();
writeConstructionExpression(false, variableProps);
writeConstructionExpression(" return ", "source", variableProps, "");
}
writer.append(" }").eol();
}
/**
* Additionally expose a {@code mapToBuilder(source)} accessor - returning the target's detected
* builder (see {@link DtoBeanMeta#builderMeta()}) one step before its final {@code build()}
* call - only when the target is constructed via a builder in the first place. Lets a caller set
* an {@code @DtoIgnore} property's real value (typically sourced from another query/service call
* entirely outside this mapper's own fetch graph) before finishing construction, e.g.
* {@code mapper.mapToBuilder(source).assignedMachines(loadMachines(source)).build()}.
* <p>
* When named variants exist, the actual guarded builder chain (each variable property behind
* its own {@code includeXxx} ternary) is written exactly once, in a private flagged overload -
* both this public (always-full) accessor and the shared {@link #writeBuildMethod()} (used by
* each variant's own partial view) delegate to it, rather than each keeping their own copy.
* Emit the target construction expression - either a positional constructor call or, when
* {@link DtoBeanMeta#builderMeta()} is present (see {@code @DtoMapping(builder = ...)}), a
* {@code Target.builder()....build()} fluent chain. {@code linePrefix} is written before the
* expression starts (e.g. {@code " return "} or a {@code computeIfAbsent(...)} lambda
* opener); {@code wrapperClosing} is any additional closing needed for an enclosing call this
* expression is nested inside (e.g. {@code ")"} to close an enclosing
* {@code computeIfAbsent(...)} call, or {@code ""} for a plain {@code return}) - the trailing
* {@code ";"} is always added here. Every property - including ones in {@code variableProps} -
* is evaluated inline, in true {@link DtoBeanMeta#properties()} declared order; a variable
* property's expression is simply guarded by its {@code includeXxx} boolean parameter (see
* {@link #writeBuildMethod()}), so no property's evaluation is ever hoisted out of its declared
* position (only non-empty when named variants exist - see {@link #variableProperties()}).
*/
private void writeMapToBuilderMethod() {
if (meta.builderMeta() == null) {
return;
}
String sourceShort = Split.shortName(meta.sourceFullName());
private void writeConstructionExpression(String linePrefix, String rootVariable, Set<DtoPropertyMeta> variableProps, String wrapperClosing) {
String targetShort = Split.shortName(meta.targetFullName());
String builderShort = meta.builderMeta().builderTypeShortName();
Set<DtoPropertyMeta> variableProps = variableProperties();
writer.append(" /**").eol();
writer.append(" * Map to the target's builder, one step before its final build() call - lets a caller").eol();
writer.append(" * set an @DtoIgnore property's real value before finishing construction.").eol();
writer.append(" */").eol();
writer.append(" public %s mapToBuilder(%s source) {", builderShort, sourceShort).eol();
writer.append(" return mapToBuilder(source, new DtoMapContext());").eol();
writer.append(" }").eol().eol();
writer.append(" public %s mapToBuilder(%s source, DtoMapContext context) {", builderShort, sourceShort).eol();
writer.append(" if (source == null) {").eol();
writer.append(" return null;").eol();
writer.append(" }").eol();
if (variableProps.isEmpty()) {
writer.append(" return %s.builder()", targetShort).eol();
writeBuilderChain("source", variableProps);
writer.append(" ;").eol();
} else {
// this (base) mapping always includes every variable property - delegate to the shared
// flagged overload below rather than re-emitting the chain here
writer.append(" return mapToBuilder(source, context%s);", buildCallArgs(variableProps)).eol();
}
writer.append(" }").eol().eol();
if (!variableProps.isEmpty()) {
writer.append(" private %s mapToBuilder(%s source, DtoMapContext context%s) {",
builderShort, sourceShort, buildMethodParams(variableProps)).eol();
writer.append(" return %s.builder()", targetShort).eol();
writeBuilderChain("source", variableProps);
writer.append(" ;").eol();
writer.append(" }").eol().eol();
}
}
/**
* Emit the target construction expression - a positional constructor call, a
* {@code Target.builder()....build()} fluent chain (when {@link DtoBeanMeta#builderMeta()} is
* present, see {@code @DtoMapping(builder = ...)}), or a setter-based
* {@code Target target = new Target(); target.setX(...); ...; return target;} statement block
* (when {@link DtoBeanMeta#setterMeta()} is present, see {@code @DtoMapping(setter = ...)}).
* <p>
* {@code computeIfAbsent} selects between a plain {@code return EXPR;} (the common case) and a
* {@code return context.computeIfAbsent(Target.class, source, s -> EXPR)} wrapper (only used
* when {@link DtoBeanMeta#nestedElsewhere()}, so the same source instance reached via more than
* one path in the graph maps to the same target instance) - the setter-based case needs its own
* block-lambda form (see {@link #writeSetterConstruction}) since, unlike the other two
* strategies, it's a multi-statement construction rather than a single expression. Every
* property - including ones in {@code variableProps} - is evaluated inline, in true
* {@link DtoBeanMeta#properties()} declared order; a variable property's expression is simply
* guarded by its {@code includeXxx} ternary (see {@link #writeBuildMethod()}), so no property's
* evaluation is ever hoisted out of its declared position (only non-empty when named variants
* exist - see {@link #variableProperties()}).
*/
private void writeConstructionExpression(boolean computeIfAbsent, Set<DtoPropertyMeta> variableProps) {
String targetShort = Split.shortName(meta.targetFullName());
String rootVariable = computeIfAbsent ? "s" : "source";
if (meta.setterMeta() != null) {
writeSetterConstruction(computeIfAbsent, targetShort, rootVariable, variableProps);
return;
}
String returnPrefix = computeIfAbsent
? String.format(" return context.computeIfAbsent(%s.class, source, s -> ", targetShort)
: " return ";
String closing = computeIfAbsent ? ")" : "";
if (meta.builderMeta() == null) {
writer.append("%snew %s(", returnPrefix, targetShort).eol();
writeConstructionArgs(rootVariable, variableProps, ")" + closing + ";");
writer.append("%snew %s(", linePrefix, targetShort).eol();
writeConstructionArgs(rootVariable, variableProps, ")" + wrapperClosing + ";");
} else {
writer.append("%s%s.builder()", returnPrefix, targetShort).eol();
writer.append("%s%s.builder()", linePrefix, targetShort).eol();
writeBuilderChain(rootVariable, variableProps);
writer.append(" .build()%s;", closing).eol();
writer.append(" .build()%s;", wrapperClosing).eol();
}
}
/**
* Emit setter-based (mutable JavaBean) construction - see {@code @DtoMapping(setter = ...)},
* {@link DtoSetterMeta}. Unlike a positional constructor call or a builder chain (both single
* expressions), this is a multi-statement local-variable-then-setter-calls block, so the
* {@code computeIfAbsent(...)}-wrapped (nested-elsewhere) case needs an explicit block lambda
* (curly braces + its own {@code return}) rather than a single continued expression.
*/
private void writeSetterConstruction(boolean computeIfAbsent, String targetShort, String rootVariable, Set<DtoPropertyMeta> variableProps) {
String indent = computeIfAbsent ? " " : " ";
if (computeIfAbsent) {
writer.append(" return context.computeIfAbsent(%s.class, source, s -> {", targetShort).eol();
}
writer.append("%s%s target = new %s();", indent, targetShort, targetShort).eol();
for (DtoPropertyMeta property : meta.properties()) {
writer.append("%starget.%s(%s);", indent, setterName(property.dtoFieldName()),
constructionValueExpression(property, rootVariable, variableProps)).eol();
}
writer.append("%sreturn target;", indent).eol();
if (computeIfAbsent) {
writer.append(" });").eol();
}
}
/** {@code referenceCode} -> {@code setReferenceCode} (standard JavaBean setter naming convention) - mirrors {@code DtoMappingReader.setterName}. */
private String setterName(String propertyName) {
return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
}
private void writeConstructionArgs(String rootVariable, Set<DtoPropertyMeta> variableProps, String closing) {
var properties = meta.properties();
for (int i = 0; i < properties.size(); i++) {
@@ -600,10 +420,6 @@ class DtoMapperWriter {
* to be excluded by a variant.
*/
private String constructionValueExpression(DtoPropertyMeta property, String rootVariable, Set<DtoPropertyMeta> variableProps) {
if (property.isIgnored()) {
// permanently excluded (see DtoIgnore) - never resolved from source, regardless of variant
return defaultValueFor(property);
}
String expression = propertyValueExpression(property, rootVariable);
return variableProps.contains(property)
? includeFlagName(property) + " ? " + expression + " : " + defaultValueFor(property)
@@ -654,25 +470,10 @@ class DtoMapperWriter {
String targetShort = Split.shortName(meta.targetFullName());
writer.append(" private %s build(%s source, DtoMapContext context%s) {",
targetShort, sourceShort, buildMethodParams(variableProps)).eol();
if (meta.builderMeta() != null) {
// reuse the very same flagged builder chain as the private mapToBuilder(...) overload
// (see writeMapToBuilderMethod()) - no second copy of it here
writer.append(" return mapToBuilder(source, context%s).build();", buildMethodArgs(variableProps)).eol();
} else {
writeConstructionExpression(false, variableProps);
}
writeConstructionExpression(" return ", "source", variableProps, "");
writer.append(" }").eol().eol();
}
/** Forward each variable property's own {@code includeXxx} parameter unchanged - used when one method delegates its flags to another (e.g. {@code build()} delegating to the flagged {@code mapToBuilder(...)} overload). */
private String buildMethodArgs(Set<DtoPropertyMeta> variableProps) {
StringBuilder sb = new StringBuilder();
for (DtoPropertyMeta property : variableProps) {
sb.append(", ").append(includeFlagName(property));
}
return sb.toString();
}
private String buildMethodParams(Set<DtoPropertyMeta> variableProps) {
StringBuilder sb = new StringBuilder();
for (DtoPropertyMeta property : variableProps) {
@@ -688,13 +489,12 @@ class DtoMapperWriter {
}
/**
* The empty/default value a variant supplies for a property it excludes - {@code List.of()}
* when the property's DTO field type is a {@code List} ({@link DtoPropertyMeta.Kind#NESTED_MANY},
* always list-shaped, or a {@link DtoPropertyMeta.Kind#SCALAR}/{@code @DtoConvert}-backed
* property whose field happens to be a {@code List}), {@code null} otherwise.
* The empty/default value a variant supplies for a property it excludes - {@code null} for a
* {@code NESTED_ONE}, {@code List.of()} for a {@code NESTED_MANY} (always valid since
* {@code NESTED_MANY} only ever derives from a {@code java.util.List<X>} target property).
*/
private String defaultValueFor(DtoPropertyMeta property) {
return property.isListTarget() ? "List.of()" : "null";
return property.kind() == DtoPropertyMeta.Kind.NESTED_MANY ? "List.of()" : "null";
}
/**
@@ -704,13 +504,6 @@ class DtoMapperWriter {
* {@code DtoMapContext} identity-dedup wrapper used by the base mapping when
* {@link DtoBeanMeta#nestedElsewhere()} - named variants are only ever used as an independent,
* top-level {@code query.mapTo(...)} result, never nested inside another DTO graph.
* <p>
* When the target uses a builder, each variant additionally gets its own named
* {@code mapToBuilder<Suffix>(source, context)} accessor (e.g. {@code mapToBuilderNoFleets(...)})
* - not a raw flags parameter, and not a method on the variant's own (deliberately {@code
* private}, inaccessible-by-type-from-outside) inner class - {@code mapToBuilder(...)} isn't
* part of the {@link DtoMapper} interface (builder types vary per target), so there'd be no way
* for external code to call it on a {@code DtoMapper<SOURCE, TARGET>}-typed variant view at all.
*/
private void writeVariantAccessors() {
Set<DtoPropertyMeta> variableProps = variableProperties();
@@ -723,35 +516,11 @@ class DtoMapperWriter {
writer.append(" return %s;", variant.fieldName()).eol();
writer.append(" }").eol().eol();
}
if (meta.builderMeta() != null) {
for (DtoMapperVariant variant : meta.variants()) {
writeVariantMapToBuilderMethods(variant, variableProps, sourceShort);
}
}
for (DtoMapperVariant variant : meta.variants()) {
writeVariantInnerClass(variant, variableProps, sourceShort, targetShort);
}
}
/**
* Write one named variant's {@code mapToBuilder<Suffix>(source, context)} (+ single-arg
* convenience overload) - delegates to the same shared, once-written flagged
* {@code mapToBuilder(source, context, includeXxx...)} overload (see
* {@link #writeMapToBuilderMethod()}) with this variant's own excluded properties fixed to
* {@code false}, so no property's chain expression is ever duplicated per variant.
*/
private void writeVariantMapToBuilderMethods(DtoMapperVariant variant, Set<DtoPropertyMeta> variableProps, String sourceShort) {
String builderShort = meta.builderMeta().builderTypeShortName();
writer.append(" /** {@code mapToBuilder(...)} for the {%s} variant - see {@link #%s()}. */",
variant.name(), variant.name()).eol();
writer.append(" public %s mapToBuilder%s(%s source, DtoMapContext context) {", builderShort, variant.suffix(), sourceShort).eol();
writer.append(" return mapToBuilder(source, context%s);", variantBuildArgs(variant, variableProps)).eol();
writer.append(" }").eol().eol();
writer.append(" public %s mapToBuilder%s(%s source) {", builderShort, variant.suffix(), sourceShort).eol();
writer.append(" return mapToBuilder%s(source, new DtoMapContext());", variant.suffix()).eol();
writer.append(" }").eol().eol();
}
private void writeVariantInnerClass(DtoMapperVariant variant, Set<DtoPropertyMeta> variableProps, String sourceShort, String targetShort) {
writer.append(" private final class %s implements DtoMapper<%s, %s> {", variant.innerClassName(), sourceShort, targetShort).eol().eol();
writer.append(" @Override").eol();
@@ -52,16 +52,6 @@ class DtoMappingReader {
/** {@code builder()} attribute value of each target's base (unnamed) registration. */
private final Map<String, String> builderModeByTarget = new LinkedHashMap<>();
/** {@code setter()} attribute value of each target's base (unnamed) registration. */
private final Map<String, String> setterModeByTarget = new LinkedHashMap<>();
/**
* {@code @DtoConverters}-registered type-pair conversion methods, keyed by
* {@link #converterKey(TypeMirror, TypeMirror)} (exact parameter type -> exact return type) -
* see {@link #collectTypeConverters(RoundEnvironment)}/{@link #autoTypeConverter}.
*/
private final Map<String, ExecutableElement> typeConverters = new LinkedHashMap<>();
private static final class RawVariant {
private final Element declaringElement;
private final TypeElement source;
@@ -118,7 +108,6 @@ class DtoMappingReader {
}
}
collectMixins(roundEnv);
collectTypeConverters(roundEnv);
}
/**
@@ -146,100 +135,10 @@ class DtoMappingReader {
ctx.logError(element, "@DtoMixin value() must be a class type");
continue;
}
String targetFqn = target.getQualifiedName().toString();
TypeElement existing = mixinsByTarget.get(targetFqn);
if (existing != null) {
ctx.logError(element, "Duplicate @DtoMixin for target %s - %s already declares a mixin for"
+ " it, only one @DtoMixin is allowed per target", targetFqn, existing.getQualifiedName());
continue;
}
mixinsByTarget.put(targetFqn, (TypeElement) element);
mixinsByTarget.put(target.getQualifiedName().toString(), (TypeElement) element);
}
}
/**
* Discover package/module-level {@code @DtoConverters({ConverterType.class, ...})}
* declarations (proactively, via {@code roundEnv.getElementsAnnotatedWith(...)} - same
* rationale as {@link #collectMixins}: a package-level annotation isn't tied to any specific
* field being resolved, so it can't be discovered lazily the way a per-property
* {@code @DtoConvert} is). Indexes every public, single-parameter, non-{@code void}-returning
* method declared directly on each referenced type by its exact (parameter type, return type)
* pair - see {@link #autoTypeConverter}.
*/
private void collectTypeConverters(RoundEnvironment roundEnv) {
TypeElement annotationType = ctx.elementUtils().getTypeElement(Constants.DTO_CONVERTERS);
if (annotationType == null) {
return;
}
for (Element element : roundEnv.getElementsAnnotatedWith(annotationType)) {
DtoConvertersPrism prism = DtoConvertersPrism.getInstanceOn(element);
if (prism == null) {
continue;
}
for (TypeMirror converterTypeMirror : prism.value()) {
TypeElement converterType = asTypeElement(converterTypeMirror);
if (converterType == null) {
ctx.logError(element, "@DtoConverters value() must be class types");
continue;
}
registerTypeConverterMethods(converterType);
}
}
}
/**
* Index every public, single-parameter, non-{@code void}-returning method declared directly on
* {@code converterType} by its exact (parameter type, return type) pair, logging an error for
* any pair that would otherwise ambiguously match more than one registered method (across this
* or any other {@code @DtoConverters}-registered type).
*/
private void registerTypeConverterMethods(TypeElement converterType) {
for (ExecutableElement method : ElementFilter.methodsIn(converterType.getEnclosedElements())) {
if (!method.getModifiers().contains(Modifier.PUBLIC) || method.getParameters().size() != 1
|| method.getReturnType().getKind() == TypeKind.VOID) {
continue;
}
TypeMirror paramType = method.getParameters().get(0).asType();
TypeMirror returnType = method.getReturnType();
String key = converterKey(paramType, returnType);
ExecutableElement existing = typeConverters.get(key);
if (existing != null) {
ctx.logError(method, "@DtoConverters: ambiguous type-pair conversion (%s -> %s) - %s.%s and %s.%s"
+ " both match this exact type pair; only one registered method per type pair is allowed",
paramType, returnType, ((TypeElement) existing.getEnclosingElement()).getQualifiedName(),
existing.getSimpleName(), converterType.getQualifiedName(), method.getSimpleName());
continue;
}
typeConverters.put(key, method);
}
}
/**
* Auto-apply a {@code @DtoConverters}-registered type-pair conversion when {@code explicit} is
* {@code null} (no per-property {@code @DtoConvert} present) and {@code sourceType} doesn't
* already exactly match {@code targetType}. Matched by exact type equality on both sides - no
* widening/supertype matching - so which method (if any) applies stays fully predictable. Never
* overrides an explicit per-property {@code @DtoConvert} - only ever consulted when
* {@code explicit} is already {@code null}.
*/
private DtoConverterMeta autoTypeConverter(DtoConverterMeta explicit, TypeMirror sourceType, TypeMirror targetType) {
if (explicit != null || sourceType == null || typeConverters.isEmpty()
|| ctx.typeUtils().isSameType(sourceType, targetType)) {
return explicit;
}
ExecutableElement method = typeConverters.get(converterKey(sourceType, targetType));
if (method == null) {
return null;
}
TypeElement converterType = (TypeElement) method.getEnclosingElement();
boolean isStatic = method.getModifiers().contains(Modifier.STATIC);
return new DtoConverterMeta(converterType.getQualifiedName().toString(), method.getSimpleName().toString(), isStatic);
}
private String converterKey(TypeMirror paramType, TypeMirror returnType) {
return paramType.toString() + "->" + returnType.toString();
}
private void addAnnotatedElements(RoundEnvironment roundEnv, String annotationTypeName, Set<Element> elements) {
TypeElement annotationType = ctx.elementUtils().getTypeElement(annotationTypeName);
if (annotationType != null) {
@@ -288,7 +187,6 @@ class DtoMappingReader {
resolveVariants();
for (DtoBeanMeta meta : byTargetName.values()) {
resolveBuilder(meta, builderModeByTarget.get(meta.targetFullName()));
resolveSetter(meta, setterModeByTarget.get(meta.targetFullName()));
}
List<DtoBeanMeta> result = excludeCycles();
markNestedElsewhere(result);
@@ -299,8 +197,7 @@ class DtoMappingReader {
* Attach each raw {@code @DtoMapping(name = ..., exclude = ...)} variant registration (see
* {@link #register}) to its base mapping's {@link DtoBeanMeta}, validating that a base exists,
* variant names are unique per target, the variant's source matches the base's source, and
* each excluded name resolves to a {@code NESTED_ONE}/{@code NESTED_MANY} property, or a
* non-primitive {@code SCALAR} property.
* each excluded name resolves to a {@code NESTED_ONE}/{@code NESTED_MANY} property.
*/
private void resolveVariants() {
for (var entry : variantsByTarget.entrySet()) {
@@ -335,12 +232,9 @@ class DtoMappingReader {
/**
* Validate and resolve one variant's {@code exclude()} property names against {@code meta}'s
* already-resolved properties - a {@code NESTED_ONE}/{@code NESTED_MANY} property can always be
* excluded (falls back to {@code null}/{@code List.of()}); a plain {@link DtoPropertyMeta.Kind#SCALAR}
* property (e.g. a {@code @DtoConvert}-backed property with no registered nested DTO mapping of
* its own) can also be excluded as long as its DTO field type isn't a Java primitive - there's
* no type-safe "absent" value for those. Returns {@code null} (with error(s) already logged) if
* anything doesn't resolve.
* already-resolved properties - only {@code NESTED_ONE}/{@code NESTED_MANY} properties can be
* excluded (there's no type-safe "absent" value for an arbitrary scalar type). Returns
* {@code null} (with error(s) already logged) if anything doesn't resolve.
*/
private Set<String> resolveExcludedProperties(DtoBeanMeta meta, RawVariant raw) {
if (raw.exclude().isEmpty()) {
@@ -356,36 +250,18 @@ class DtoMappingReader {
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") does not match"
+ " any property on target %s", raw.name(), propName, meta.targetFullName());
ok = false;
} else if (property.isIgnored()) {
} else if (property.kind() != DtoPropertyMeta.Kind.NESTED_ONE && property.kind() != DtoPropertyMeta.Kind.NESTED_MANY) {
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s is"
+ " already @DtoIgnore - it's permanently excluded from every mapping already, remove it from"
+ " this variant's exclude() list", raw.name(), propName, meta.targetFullName());
ok = false;
} else if (isExcludable(property)) {
result.add(propName);
} else if (property.kind() == DtoPropertyMeta.Kind.SCALAR) {
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s"
+ " is a primitive property - there's no type-safe \"absent\" value for it, so it can't be"
+ " excluded from a named variant", raw.name(), propName, meta.targetFullName());
+ " not a nested ToOne/ToMany DTO property - only nested properties can be excluded (there's"
+ " no type-safe \"absent\" value for an arbitrary scalar type)", raw.name(), propName, meta.targetFullName());
ok = false;
} else {
ctx.logError(raw.declaringElement(), "@DtoMapping(name = \"%s\") exclude(\"%s\") on target %s is"
+ " not a nested ToOne/ToMany DTO property or a non-primitive scalar - only those can be"
+ " excluded (there's no type-safe \"absent\" value for an arbitrary scalar type)", raw.name(), propName, meta.targetFullName());
ok = false;
result.add(propName);
}
}
return ok ? result : null;
}
/** {@code true} if {@code property} can be excluded from a named variant - see {@link #resolveExcludedProperties}. */
private boolean isExcludable(DtoPropertyMeta property) {
if (property.kind() == DtoPropertyMeta.Kind.NESTED_ONE || property.kind() == DtoPropertyMeta.Kind.NESTED_MANY) {
return true;
}
return property.kind() == DtoPropertyMeta.Kind.SCALAR && !property.isPrimitiveTarget();
}
private DtoPropertyMeta findProperty(DtoBeanMeta meta, String name) {
for (DtoPropertyMeta property : meta.properties()) {
if (property.dtoFieldName().equals(name)) {
@@ -461,111 +337,6 @@ class DtoMappingReader {
return new DtoBuilderMeta(builderType.getQualifiedName().toString());
}
/**
* Decide (and, if applicable, detect) the setter-based construction path for {@code meta} - see
* {@code @DtoMapping(setter = ...)} and {@link DtoSetterMeta}. A no-op if a builder was already
* selected for {@code meta} (see {@link #resolveBuilder}) - a builder always takes priority.
* {@code NEVER} always uses a positional constructor. {@code ALWAYS} requires a matching
* no-arg-constructor-plus-setters shape to be found (a codegen error otherwise). {@code AUTO}
* (the default) only attempts detection when the target has no positional constructor matching
* the mapped properties (arity-based - see {@link #hasPositionalConstructor}), silently falling
* back to a positional constructor call if no matching setter shape is found either.
*/
private void resolveSetter(DtoBeanMeta meta, String setterMode) {
if (meta.builderMeta() != null) {
return;
}
String mode = (setterMode == null || setterMode.isEmpty()) ? "AUTO" : setterMode;
if ("NEVER".equals(mode)) {
return;
}
boolean required = "ALWAYS".equals(mode);
if (!required && hasPositionalConstructor(meta.target(), meta.properties().size())) {
return;
}
DtoSetterMeta setter = detectSetter(meta, required);
if (setter != null) {
meta.setterMeta(setter);
}
}
/**
* Detect a plain mutable-JavaBean shape on {@code meta.target()}: a public no-arg constructor
* plus a void {@code setXxx(...)} setter for every one of {@code meta}'s properties. Returns
* {@code null} if any part of that shape is missing - logging a codegen error only when
* {@code required} (i.e. {@code setter = ALWAYS}); silent otherwise (i.e. {@code setter = AUTO},
* where a missing shape just means "use a positional constructor instead").
*/
private DtoSetterMeta detectSetter(DtoBeanMeta meta, boolean required) {
TypeElement target = meta.target();
if (!hasPublicNoArgConstructor(target)) {
if (required) {
ctx.logError(target, "@DtoMapping(setter = ALWAYS) on target %s but no public no-arg"
+ " constructor was found", meta.targetFullName());
}
return null;
}
for (DtoPropertyMeta property : meta.properties()) {
if (findSetter(target, property.dtoFieldName()) == null) {
if (required) {
ctx.logError(target, "@DtoMapping(setter = ALWAYS) on target %s but no public"
+ " \"%s(...)\" setter method was found", meta.targetFullName(), setterName(property.dtoFieldName()));
}
return null;
}
}
return new DtoSetterMeta();
}
private boolean hasPublicNoArgConstructor(TypeElement type) {
for (ExecutableElement ctor : ElementFilter.constructorsIn(type.getEnclosedElements())) {
if (ctor.getParameters().isEmpty() && ctor.getModifiers().contains(Modifier.PUBLIC)) {
return true;
}
}
return false;
}
/** {@code true} if {@code type} has any declared constructor with exactly {@code arity} parameters - the shape a positional constructor call assumes. */
private boolean hasPositionalConstructor(TypeElement type, int arity) {
for (ExecutableElement ctor : ElementFilter.constructorsIn(type.getEnclosedElements())) {
if (ctor.getParameters().size() == arity) {
return true;
}
}
return false;
}
/**
* Find a public single-arg {@code setXxx(...)} method on {@code type} - either {@code void}
* or fluent-style (returning {@code type} itself, e.g. {@code public Target setXxx(...) { ...;
* return this; }}). Either shape is accepted since the generated code always calls the setter
* as a bare statement and discards any return value.
*/
private ExecutableElement findSetter(TypeElement type, String propertyName) {
String setterName = setterName(propertyName);
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
if (method.getSimpleName().contentEquals(setterName) && method.getParameters().size() == 1
&& method.getModifiers().contains(Modifier.PUBLIC) && isVoidOrFluentReturn(method, type)) {
return method;
}
}
return null;
}
/** {@code true} if {@code method} returns {@code void}, or returns {@code declaringType} itself (fluent-style setter). */
private boolean isVoidOrFluentReturn(ExecutableElement method, TypeElement declaringType) {
TypeMirror returnType = method.getReturnType();
return returnType.getKind() == TypeKind.VOID
|| (returnType.getKind() == TypeKind.DECLARED
&& ctx.typeUtils().isSameType(ctx.typeUtils().erasure(returnType), ctx.typeUtils().erasure(declaringType.asType())));
}
/** {@code referenceCode} -> {@code setReferenceCode} (standard JavaBean setter naming convention). */
private String setterName(String propertyName) {
return "set" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
}
private ExecutableElement findStaticNoArgMethod(TypeElement type, String name) {
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
if (method.getSimpleName().contentEquals(name) && method.getModifiers().contains(Modifier.STATIC)
@@ -622,10 +393,8 @@ class DtoMappingReader {
return;
}
String mapperPackage = resolveMapperPackage(prism, targetType, declaringElement);
String mapperName = prism.mapperName() == null ? "" : prism.mapperName().trim();
byTargetName.put(targetFqn, new DtoBeanMeta(sourceType, targetType, mapperPackage, mapperName));
byTargetName.put(targetFqn, new DtoBeanMeta(sourceType, targetType, mapperPackage));
builderModeByTarget.put(targetFqn, prism.builder());
setterModeByTarget.put(targetFqn, prism.setter());
} else {
variantsByTarget.computeIfAbsent(targetFqn, t -> new ArrayList<>())
.add(new RawVariant(declaringElement, sourceType, name, prism.exclude()));
@@ -673,217 +442,63 @@ class DtoMappingReader {
private DtoPropertyMeta resolveProperty(VariableElement field, DtoBeanMeta meta) {
String name = field.getSimpleName().toString();
DtoIgnorePrism ignorePrism = prismOn(field, meta, DtoIgnorePrism::getInstanceOn);
DtoConverterMeta converter = resolveConverter(field, meta);
DtoRefPrism refPrism = prismOn(field, meta, DtoRefPrism::getInstanceOn);
DtoPathPrism pathPrism = prismOn(field, meta, DtoPathPrism::getInstanceOn);
if (ignorePrism != null) {
return resolveIgnoredProperty(field, meta, name, converter, refPrism, pathPrism);
}
if (refPrism != null && pathPrism != null) {
ctx.logError(field, "%s.%s carries both @DtoRef and @DtoPath - these are mutually exclusive"
+ " (@DtoRef always wins silently otherwise), remove whichever doesn't apply",
meta.targetFullName(), name);
}
if (refPrism != null) {
String assocName = (name.endsWith("Id") && name.length() > 2) ? name.substring(0, name.length() - 2) : name;
String assocGetter = getterName(meta.source(), assocName);
TypeElement assocType = getterReturnType(meta.source(), assocGetter);
String idGetter = getterName(assocType, "id");
boolean computedSegment = meta.source() == null || !hasField(meta.source(), assocName);
List<String> requiredFetchPaths = List.of();
if (computedSegment) {
// the association has no backing field on the source - a computed/derived getter (e.g.
// picking one entry out of a collection) rather than a real, fetchable Ebean relation, so
// its data dependencies can't be inferred automatically - same problem class, and same
// requires() escape hatch, as the analogous @DtoPath handling above. Unlike @DtoPath there's
// no "real prefix" to combine with, since @DtoRef only ever derives a single association
// name directly off the source (no dotted value() of its own).
if (refPrism.values.requires() == null) {
ctx.logError(field,
"@DtoRef on %s traverses '%s' which has no backing field on %s - it looks like a"
+ " computed/derived getter rather than a real, fetchable Ebean relation, so its data"
+ " dependencies can't be inferred automatically. Specify @DtoRef(requires = {...})"
+ " naming the real entity paths that must be fetched for it to execute safely, or"
+ " requires = {} if it genuinely needs nothing extra fetched, or remove @DtoRef and"
+ " compute this value another way (e.g. @DtoConvert).",
meta.targetFullName(), assocName, meta.source() != null ? meta.source().getSimpleName() : "?");
}
requiredFetchPaths = refPrism.requires();
String annotationDisplay = String.format("@DtoRef(requires = ...) on %s.%s", meta.targetFullName(), name);
for (String requiresPath : requiredFetchPaths) {
validateRequiresPath(field, meta.source(), requiresPath, annotationDisplay);
}
}
// @DtoRef has no failOnNull escape hatch - always default to the primitive's zero-equivalent
// rather than let a null-guarded getter chain auto-unbox to a NullPointerException.
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.REF, List.of(assocGetter, idGetter), List.of(assocName, "id"),
null, converter, field.asType().getKind().isPrimitive(), false, computedSegment, requiredFetchPaths, false, false);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.REF, List.of(assocGetter, idGetter), List.of(assocName, "id"), null, converter);
}
DtoPathPrism pathPrism = prismOn(field, meta, DtoPathPrism::getInstanceOn);
if (pathPrism != null) {
List<String> getters = new ArrayList<>();
List<String> properties = new ArrayList<>();
TypeElement currentType = meta.source();
TypeElement computedDeclaringType = null;
TypeElement lastOwnerType = null;
String lastGetter = null;
int computedFrom = -1;
for (String segment : pathPrism.value().split("\\.")) {
String getter = getterName(currentType, segment);
getters.add(getter);
properties.add(segment);
if (computedFrom < 0 && (currentType == null || !hasField(currentType, segment))) {
computedFrom = properties.size() - 1;
computedDeclaringType = currentType;
}
lastOwnerType = currentType;
lastGetter = getter;
currentType = currentType != null ? getterReturnType(currentType, getter) : null;
}
List<String> requiredFetchPaths = List.of();
if (computedFrom >= 0) {
// a segment with no backing field is a computed/derived getter, not a real, fetchable
// Ebean property - its own data dependencies (what it touches internally) can't be
// inferred from the path alone, so require the developer to spell them out explicitly
// via @DtoPath(requires = {...}) rather than silently generating a FetchGroup.fetch(...)
// call for a path segment Ebean doesn't actually recognise (which only fails at runtime).
// Computed here regardless of whether the path ultimately resolves to a plain SCALAR or a
// single-hop NESTED_ONE/MANY rename below - both cases share the exact same problem: a
// fake path segment that can't be handed to FetchGroup as a real fetch/select target.
String realPrefix = computedFrom == 0 ? null : String.join(".", properties.subList(0, computedFrom));
// pathPrism.requires() always returns List.of() whether the attribute was explicitly
// written as an empty array or omitted entirely - only pathPrism.values.requires() (which
// returns null for a defaulted/omitted member) can tell the two apart. That distinction
// matters here: an explicit requires = {} is the developer's way of confirming the
// computed getter genuinely needs nothing extra fetched, whereas omitting requires
// entirely means they haven't considered it yet - only the latter should fail the build.
if (pathPrism.values.requires() == null) {
String hint = realPrefix != null
? String.format(" (e.g. requires = \"%s\", or a deeper path under it your getter actually needs)", realPrefix)
: "";
ctx.logError(field,
"@DtoPath(\"%s\") on %s traverses '%s' which has no backing field on %s - it looks like"
+ " a computed/derived getter rather than a real, fetchable Ebean property, so its data"
+ " dependencies can't be inferred automatically. Specify @DtoPath(requires = {...})"
+ " naming the real entity paths that must be fetched for it to execute safely%s, or"
+ " requires = {} if it genuinely needs nothing extra fetched, or remove @DtoPath and"
+ " compute this value another way (e.g. @DtoConvert).",
pathPrism.value(), meta.targetFullName(), properties.get(computedFrom),
computedDeclaringType != null ? computedDeclaringType.getSimpleName() : "?", hint);
}
List<String> combined = new ArrayList<>();
if (realPrefix != null) {
combined.add(realPrefix);
}
combined.addAll(pathPrism.requires());
requiredFetchPaths = combined;
// realPrefix is already known-good (each of its segments was hasField-checked while
// walking value() above) - only the developer-declared requires() values themselves are
// unchecked and need validating here.
String annotationDisplay = String.format("@DtoPath(\"%s\").requires()", pathPrism.value());
for (String requiresPath : pathPrism.requires()) {
validateRequiresPath(field, meta.source(), requiresPath, annotationDisplay);
}
}
// a single-hop @DtoPath rename (e.g. @DtoPath("eboxStatus") on a field named "status")
// can still target a type with its own registered @DtoMapping - detect that the same way
// the plain (non-@DtoPath) branches below do, rather than always falling back to a raw
// scalar getter call that would fail to compile with a type mismatch against the nested
// DTO type. Multi-hop paths keep the existing scalar/flattening behaviour since fetch spec
// derivation for NESTED_ONE/MANY only supports a single association name. A computed
// segment here (the single segment has no backing field - e.g. @DtoPath("primaryContact")
// where getPrimaryContact() is a derived getter) is just as unfetchable as the SCALAR case
// above, so it carries the same computedFrom/requiredFetchPaths validation through - see
// DtoMapperWriter's NESTED_ONE/NESTED_MANY handling of DtoPropertyMeta#hasComputedSegment().
// derivation for NESTED_ONE/MANY only supports a single association name.
if (properties.size() == 1) {
TypeMirror fieldType = field.asType();
TypeMirror listElementType = listElementType(fieldType);
if (listElementType != null) {
DtoBeanMeta nested = lookupByTarget(listElementType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, getters, properties, nested,
computedFrom >= 0, requiredFetchPaths);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, getters, properties, nested);
}
} else {
DtoBeanMeta nested = lookupByTarget(fieldType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, getters, properties, nested,
computedFrom >= 0, requiredFetchPaths);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, getters, properties, nested);
}
}
}
// A multi-hop path can pass through a nullable intermediate relation - if the DTO field is
// primitive, the generated null-guarded getter chain would otherwise auto-unbox a null
// straight into a NullPointerException. Default to the primitive's zero-equivalent value,
// or fail fast with a clear message instead when @DtoPath(failOnNull = true).
boolean isListTarget = listElementType(field.asType()) != null;
DtoConverterMeta pathConverter = isListTarget ? converter
: autoTypeConverter(converter, lastOwnerType != null ? getterReturnTypeMirror(lastOwnerType, lastGetter) : null, field.asType());
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, pathConverter,
field.asType().getKind().isPrimitive(), pathPrism.failOnNull(), computedFrom >= 0, requiredFetchPaths,
isListTarget, false);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, getters, properties, null, converter);
}
TypeMirror fieldType = field.asType();
TypeMirror listElementType = listElementType(fieldType);
if (listElementType != null) {
DtoBeanMeta nested = lookupByTarget(listElementType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_MANY, List.of(getterName(meta.source(), name)), List.of(name), nested);
}
} else {
DtoBeanMeta nested = lookupByTarget(fieldType);
if (nested != null) {
rejectConverterOnNested(field, converter, name, meta);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.NESTED_ONE, List.of(getterName(meta.source(), name)), List.of(name), nested);
}
}
String getter = getterName(meta.source(), name);
DtoConverterMeta scalarConverter = listElementType != null ? converter
: autoTypeConverter(converter, getterReturnTypeMirror(meta.source(), getter), fieldType);
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getter), List.of(name), null, scalarConverter,
fieldType.getKind().isPrimitive(), listElementType != null);
}
/**
* Resolve a {@code @DtoIgnore} property - permanently excluded from every mapping (base and
* every named variant alike), always given its empty default rather than resolved from any
* source getter/path at all (see {@code DtoIgnore}'s javadoc). Validates it isn't combined with
* {@code @DtoRef}/{@code @DtoPath}/{@code @DtoConvert} (all pointless - there's no source
* expression to convert/redirect when nothing is ever read from the source) and isn't a
* primitive-typed field (no type-safe "absent" value for it).
*/
private DtoPropertyMeta resolveIgnoredProperty(VariableElement field, DtoBeanMeta meta, String name,
DtoConverterMeta converter, DtoRefPrism refPrism, DtoPathPrism pathPrism) {
if (converter != null || refPrism != null || pathPrism != null) {
ctx.logError(field, "@DtoIgnore on %s.%s cannot be combined with @DtoConvert/@DtoRef/@DtoPath -"
+ " an @DtoIgnore property is never resolved from the source at all, so there's no source"
+ " expression for those to act on. Remove @DtoIgnore or the other annotation(s).",
meta.targetFullName(), name);
}
if (field.asType().getKind().isPrimitive()) {
ctx.logError(field, "@DtoIgnore cannot be applied to primitive-typed property %s.%s - there's"
+ " no type-safe \"absent\" value for it", meta.targetFullName(), name);
}
return DtoPropertyMeta.ignored(name, listElementType(field.asType()) != null);
}
/**
* {@code @DtoConvert} only applies to {@code SCALAR}/{@code REF} value expressions
* ({@link DtoMapperWriter#propertyValueExpression}) - a {@code NESTED_ONE}/{@code NESTED_MANY}
* property's value comes entirely from its own nested mapper's {@code map(...)}/{@code
* mapList(...)} call, so a converter resolved for it would silently have no effect at all.
* Raise a compile error instead of letting the annotation quietly do nothing.
*/
private void rejectConverterOnNested(VariableElement field, DtoConverterMeta converter, String name, DtoBeanMeta meta) {
if (converter != null) {
ctx.logError(field, "@DtoConvert on %s.%s has no effect - it isn't supported on a nested DTO"
+ " graph property (NESTED_ONE/NESTED_MANY), only on SCALAR/REF properties. Remove"
+ " @DtoConvert, or perform the conversion inside the nested DTO's own mapper instead.",
meta.targetFullName(), name);
}
return new DtoPropertyMeta(name, DtoPropertyMeta.Kind.SCALAR, List.of(getterName(meta.source(), name)), List.of(name), null, converter);
}
/**
@@ -929,54 +544,15 @@ class DtoMappingReader {
return null;
}
String methodName = prism.method();
ExecutableElement method = findConverterMethod(field, converterType, methodName);
ExecutableElement method = findMethod(converterType, methodName);
if (method == null) {
ctx.logError(field, "@DtoConvert method \"%s\" not found on %s", methodName, converterType.getQualifiedName());
return null;
}
boolean isStatic = method.getModifiers().contains(Modifier.STATIC);
return new DtoConverterMeta(converterType.getQualifiedName().toString(), methodName, isStatic);
}
/**
* Resolve {@code @DtoConvert}'s {@code method()} on {@code converterType} - unlike
* {@link #findMethod}, this requires exactly one candidate taking a single parameter (the
* documented {@code @DtoConvert} contract: "taking the source property value and returning the
* converted DTO property value"). {@code findMethod} alone matches by simple name only, so a
* converter type with two same-named overloads (a very plausible shape for a shared conversion
* utility class, e.g. {@code format(Instant)} and {@code format(LocalDate)}) would silently bind
* to whichever one {@code ElementFilter.methodsIn} happens to return first, regardless of which
* one the developer actually intended - generating either a confusing compile error in the
* generated mapper (arity/type mismatch) or, worse, silently generating a call to the wrong
* overload if both happen to be call-compatible.
*/
private ExecutableElement findConverterMethod(VariableElement field, TypeElement converterType, String methodName) {
List<ExecutableElement> oneArgCandidates = new ArrayList<>();
boolean anyNameMatch = false;
for (ExecutableElement candidate : ElementFilter.methodsIn(converterType.getEnclosedElements())) {
if (!candidate.getSimpleName().contentEquals(methodName)) {
continue;
}
anyNameMatch = true;
if (candidate.getParameters().size() == 1) {
oneArgCandidates.add(candidate);
}
}
if (oneArgCandidates.size() == 1) {
return oneArgCandidates.get(0);
}
if (oneArgCandidates.isEmpty()) {
ctx.logError(field, "@DtoConvert method \"%s\" not found on %s taking exactly one parameter%s",
methodName, converterType.getQualifiedName(),
anyNameMatch ? " (a method with that name exists but doesn't take exactly one parameter)" : "");
return null;
}
ctx.logError(field, "@DtoConvert method \"%s\" on %s is ambiguous - %d overloads take exactly one"
+ " parameter, and @DtoConvert can't disambiguate by parameter type. Rename one of the"
+ " overloads so the reference is unambiguous.",
methodName, converterType.getQualifiedName(), oneArgCandidates.size());
return null;
}
private ExecutableElement findMethod(TypeElement type, String methodName) {
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
if (method.getSimpleName().contentEquals(methodName)) {
@@ -1039,29 +615,6 @@ class DtoMappingReader {
return getName;
}
/**
* Whether {@code type} (searching {@code type} and its superclass chain) declares a field
* named {@code propertyName} - used to distinguish a real, fetchable Ebean bean property (which
* always has a backing field once enhanced) from a computed/derived getter with no backing
* storage at all (e.g. a hand-written method that filters/derives a value from other
* properties). {@code type == null} (unresolvable) conservatively returns {@code true} so an
* already-unresolvable segment doesn't also get flagged as "computed" - it'll already have
* fallen back to a guessed getter name via {@link #getterName}.
*/
private boolean hasField(TypeElement type, String propertyName) {
if (type == null) {
return true;
}
for (TypeElement current = type; current != null; current = superclassOf(current)) {
for (VariableElement f : ElementFilter.fieldsIn(current.getEnclosedElements())) {
if (f.getSimpleName().contentEquals(propertyName)) {
return true;
}
}
}
return false;
}
/**
* Whether a no-arg method named {@code methodName} exists on {@code type} (searching
* {@code type} and its superclass chain), optionally constrained to a specific return
@@ -1089,61 +642,16 @@ class DtoMappingReader {
* which case later segments fall back to the guessed {@code getXxx()} name.
*/
private TypeElement getterReturnType(TypeElement type, String getterMethodName) {
TypeMirror mirror = getterReturnTypeMirror(type, getterMethodName);
return mirror != null ? asTypeElement(mirror) : null;
}
/**
* As {@link #getterReturnType(TypeElement, String)}, but returns the raw {@link TypeMirror}
* rather than converting it to a {@link TypeElement} - needed by {@link #validateRequiresPath}
* to detect a {@code java.util.List}-typed return (via {@link #listElementType(TypeMirror)}),
* which {@link #getterReturnType(TypeElement, String)}'s {@code asTypeElement} conversion can't
* distinguish from any other declared type.
*/
private TypeMirror getterReturnTypeMirror(TypeElement type, String getterMethodName) {
for (TypeElement current = type; current != null; current = superclassOf(current)) {
for (ExecutableElement method : ElementFilter.methodsIn(current.getEnclosedElements())) {
if (method.getParameters().isEmpty() && method.getSimpleName().contentEquals(getterMethodName)) {
return method.getReturnType();
return asTypeElement(method.getReturnType());
}
}
}
return null;
}
/**
* Validate that every dot-notation segment of a declared {@code @DtoPath(requires = ...)}/
* {@code @DtoRef(requires = ...)} path value names a real, fetchable Ebean property (one with a
* backing field) on {@code source} - these are meant to be real entity fetch paths handed
* straight through to {@code FetchGroup.fetch(...)}, so a typo here would otherwise silently
* reintroduce the exact "compiles cleanly, fails at runtime with {@code PersistenceException}"
* problem the {@code requires()} escape hatch itself exists to prevent - it just wouldn't be
* caught until much later, since {@code requires()} paths are trusted verbatim rather than
* walked/checked like {@code @DtoPath#value()}'s own segments are. Handles a {@code List}-typed
* intermediate hop (e.g. {@code "currentMachine.organisationMachines"}) by unwrapping to the
* element type via {@link #listElementType(TypeMirror)}, mirroring how a real fetch path can
* traverse a collection.
*/
private void validateRequiresPath(Element field, TypeElement source, String requiresPath, String annotationDisplay) {
TypeElement currentType = source;
for (String segment : requiresPath.split("\\.")) {
if (currentType == null) {
return; // already unresolvable upstream - don't cascade a confusing secondary error
}
if (!hasField(currentType, segment)) {
ctx.logError(field,
"%s names '%s' (in \"%s\") which has no backing field on %s - check for a typo, every"
+ " requires() path segment must be a real, fetchable Ebean property.",
annotationDisplay, segment, requiresPath, currentType.getSimpleName());
return;
}
String getter = getterName(currentType, segment);
TypeMirror returnMirror = getterReturnTypeMirror(currentType, getter);
TypeMirror elementType = returnMirror != null ? listElementType(returnMirror) : null;
currentType = asTypeElement(elementType != null ? elementType : returnMirror);
}
}
private TypeElement superclassOf(TypeElement type) {
return asTypeElement(type.getSuperclass());
}
@@ -25,83 +25,18 @@ class DtoPropertyMeta {
private final List<String> sourcePropertyPath;
private final DtoBeanMeta nested;
private final DtoConverterMeta converter;
private final boolean primitiveTarget;
private final boolean failOnNull;
private final boolean computedSegment;
private final List<String> requiredFetchPaths;
private final boolean listTarget;
private final boolean ignored;
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, null);
}
/**
* {@code @DtoIgnore} factory - a property permanently excluded from every mapping (base and
* every named variant alike), always given its empty default rather than resolved from any
* source getter/path at all - see {@code DtoIgnore}'s javadoc. Kept as {@link Kind#SCALAR} with
* empty getter/property paths since {@link DtoMapperWriter} short-circuits on {@link #isIgnored()}
* before ever consulting them.
*/
static DtoPropertyMeta ignored(String dtoFieldName, boolean listTarget) {
return new DtoPropertyMeta(dtoFieldName, Kind.SCALAR, List.of(), List.of(), null, null, false, false, false, List.of(), listTarget, true);
}
/**
* {@code NESTED_ONE}/{@code NESTED_MANY} constructor variant for a single-hop {@code @DtoPath}
* rename that traverses a computed/derived getter segment (no backing field) - see
* {@link #hasComputedSegment()}. Just as unfetchable via {@code FetchGroup.fetch(path, ...)} as
* the analogous {@link Kind#SCALAR} case, so it carries the same
* {@code computedSegment}/{@code requiredFetchPaths} through to {@code DtoMapperWriter}.
*/
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, boolean computedSegment, List<String> requiredFetchPaths) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, null, false, false, computedSegment, requiredFetchPaths, false, false);
}
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath, DtoBeanMeta nested, DtoConverterMeta converter) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, false, false);
}
/**
* Plain (no {@code @DtoPath}) {@link Kind#SCALAR} constructor variant that also records whether
* the DTO field type is a {@code java.util.List} - see {@code listTarget} on the full
* constructor.
*/
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean listTarget) {
this(dtoFieldName, kind, sourceGetterPath, sourcePropertyPath, nested, converter, primitiveTarget, false, false, List.of(), listTarget, false);
}
/**
* Full constructor - {@code primitiveTarget}/{@code failOnNull} only matter for a multi-hop
* ({@code sourceGetterPath.size() > 1}) {@link Kind#SCALAR}/{@link Kind#REF} property whose DTO
* field type is a Java primitive, per {@code @DtoPath#failOnNull()} - see
* {@link #sourceValueExpression(String)}. {@code computedSegment} is {@code true} only for a
* {@code @DtoPath} that traverses a computed/derived getter segment (no backing field) - see
* {@link #hasComputedSegment()}; kept separate from whether {@code requiredFetchPaths} happens
* to be empty, since {@code @DtoPath(requires = {})} legitimately declares "nothing extra
* needed" for a computed segment. {@code listTarget} is {@code true} when this property's DTO
* field type is a {@code java.util.List} - relevant only for {@link Kind#SCALAR} (e.g. a
* {@code @DtoConvert}-backed {@code List} property with no registered nested DTO mapping of its
* own, like a fleet list populated from ad-hoc SQL) - see
* {@code DtoMapperWriter#defaultValueFor}.
*/
DtoPropertyMeta(String dtoFieldName, Kind kind, List<String> sourceGetterPath, List<String> sourcePropertyPath,
DtoBeanMeta nested, DtoConverterMeta converter, boolean primitiveTarget, boolean failOnNull,
boolean computedSegment, List<String> requiredFetchPaths, boolean listTarget, boolean ignored) {
this.dtoFieldName = dtoFieldName;
this.kind = kind;
this.sourceGetterPath = sourceGetterPath;
this.sourcePropertyPath = sourcePropertyPath;
this.nested = nested;
this.converter = converter;
this.primitiveTarget = primitiveTarget;
this.failOnNull = failOnNull;
this.computedSegment = computedSegment;
this.requiredFetchPaths = requiredFetchPaths;
this.listTarget = listTarget;
this.ignored = ignored;
}
String dtoFieldName() {
@@ -144,70 +79,11 @@ class DtoPropertyMeta {
return converter;
}
/**
* {@code true} if this {@code @DtoPath} traverses a segment with no backing field (a computed/
* derived getter rather than a real, fetchable Ebean property) - in which case
* {@link #sourcePropertyPath()} must NOT be used to derive a {@code .fetch(path, "props")}
* ({@link Kind#SCALAR}) or {@code .fetch(path, mapper.fetchGroup())} ({@link Kind#NESTED_ONE}/
* {@link Kind#NESTED_MANY}) call (the path isn't a real Ebean fetch path), and
* {@link #requiredFetchPaths()} should be used instead (see {@code @DtoPath#requires()}).
*/
boolean hasComputedSegment() {
return computedSegment;
}
/**
* {@code true} when this property's DTO field type is a Java primitive - a primitive property
* has no type-safe "absent" value, so can never be excluded by a named
* {@code @DtoMapping(name = ..., exclude = ...)} variant.
*/
boolean isPrimitiveTarget() {
return primitiveTarget;
}
/**
* {@code true} when this property's DTO field type is a {@code java.util.List} - set for
* {@link Kind#NESTED_MANY} (always list-shaped) and also for a {@link Kind#SCALAR}/
* {@code @DtoConvert}-backed property whose DTO field happens to be a {@code List} with no
* registered nested DTO mapping of its own (e.g. a fleet list populated from ad-hoc SQL) -
* used to pick {@code List.of()} rather than {@code null} as the excluded/empty default for a
* named variant, see {@code DtoMapperWriter#defaultValueFor}.
*/
boolean isListTarget() {
return listTarget || kind == Kind.NESTED_MANY;
}
/**
* {@code true} when this property is marked {@code @DtoIgnore} - permanently excluded from
* every mapping (base and every named variant alike), always given its empty default rather
* than resolved from any source getter/path.
*/
boolean isIgnored() {
return ignored;
}
/**
* Real entity paths that must be added to the {@code FetchGroup} to support this property's
* computed/derived getter segment - the real prefix path (if any) followed by the declared
* {@code @DtoPath#requires()} paths. Empty by default when {@link #hasComputedSegment()} is
* {@code false}; can also legitimately be empty when it's {@code true} (an explicit
* {@code @DtoPath(requires = {})} confirming nothing extra is needed).
*/
List<String> requiredFetchPaths() {
return requiredFetchPaths;
}
/**
* Return a source expression chaining {@link #sourceGetterPath()} getters off the given root
* variable. A single getter is a plain call, e.g. {@code s.getName()}; a multi-hop chain (from
* {@code @DtoPath} or {@code @DtoRef}) null-guards each intermediate hop, e.g.
* {@code (s.getBillingAddress() == null ? null : s.getBillingAddress().getLine1())}.
* <p>
* That null-guarded chain always types as the boxed wrapper (one ternary branch is the
* {@code null} literal) - when {@link #primitiveTarget} is set (the DTO field is a Java
* primitive), the whole chain is additionally wrapped in a {@code DtoMapperSupport} call so it
* safely resolves to the primitive's zero-equivalent value (the default), or throws a clear
* exception instead, per {@code @DtoPath#failOnNull()} - see {@code DtoMapperSupport}.
*/
String sourceValueExpression(String rootVariable) {
if (sourceGetterPath.size() == 1) {
@@ -215,23 +91,7 @@ class DtoPropertyMeta {
}
StringBuilder sb = new StringBuilder();
appendGuardedChain(sb, rootVariable, 0);
String chain = sb.toString();
if (!primitiveTarget) {
return chain;
}
return failOnNull
? "DtoMapperSupport.require(" + chain + ", \"" + String.join(".", sourcePropertyPath) + "\")"
: "DtoMapperSupport.orZero(" + chain + ")";
}
/**
* {@code true} if {@link #sourceValueExpression(String)} wraps its chain in a
* {@code DtoMapperSupport} call - i.e. this is a multi-hop {@link Kind#SCALAR}/{@link Kind#REF}
* property whose DTO field type is primitive. Used to conditionally import
* {@code io.ebean.DtoMapperSupport} only when actually referenced.
*/
boolean usesMapperSupport() {
return primitiveTarget && sourceGetterPath.size() > 1;
return sb.toString();
}
private void appendGuardedChain(StringBuilder sb, String prefix, int index) {
@@ -245,4 +105,3 @@ class DtoPropertyMeta {
sb.append(')');
}
}
@@ -1,35 +0,0 @@
package io.ebean.querybean.generator;
/**
* Marks a detected setter-based (mutable JavaBean) construction path for a {@link DtoBeanMeta}'s
* target - see {@code @DtoMapping(setter = ...)}.
* <p>
* Detected via the plain JavaBean convention (the shape JAXB/XSD-generated legacy SOAP types
* commonly follow): a public no-arg constructor plus a public {@code setXxx(propertyType)} setter
* for every one of the target's mapped properties - either {@code void} or fluent-style
* (returning the target type itself, e.g. {@code public Target setXxx(...) { ...; return this; }})
* are both accepted, since the generated code always calls the setter as a bare statement and
* discards any return value. When present (and either explicitly
* requested via {@code setter = ALWAYS}, or no builder was selected and the target has no
* positional constructor matching the mapped properties under {@code setter = AUTO}), the
* generated mapper constructs the target via {@code Target target = new Target();
* target.setX(...); ...; return target;} instead of a positional constructor call or a builder
* chain.
* <p>
* A pure marker (mirrors {@link DtoBuilderMeta}) - unlike a builder, a setter-constructed target
* needs no separate intermediate type name, since the setters are called directly on the target
* type itself.
* <p>
* Deliberately no {@code mapToBuilder(...)}-style post-construction accessor is generated for this
* strategy: the returned target is already the final, fully mutable instance (its setters are
* required to be {@code public} - see above), so a caller can already override an
* {@code @DtoIgnore}/derived property directly, e.g.
* {@code Ebox ebox = mapper.map(source); ebox.setMachineSummaryInfo(loadSummary(source));} -
* exactly the pattern already used by hand-written mappers like central-access's
* {@code EboxMapper}. This is unlike the builder strategy, where the intermediate builder object
* is distinct from (and otherwise inaccessible after) the final, one-shot-{@code build()}ed,
* often-immutable target - {@code mapToBuilder(...)} exists there specifically to expose that
* otherwise-unreachable intermediate step.
*/
final class DtoSetterMeta {
}

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