Compare commits

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

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

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

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

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

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

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

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

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

---------

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

* DtoQuery - Support DtoPath renaming a ToOne or ToMany

* MappedQuery - Add findStream() support

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 17:04:22 +12:00
robin.bygrave d083b27e91 Version 18.3.0 2026-07-16 16:57:52 +12:00
Rob Bygraveandrobin.bygrave 30314f163d DtoQuery - add usingMaster, usingTransaction, usingConnection options. (#3853)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 15:50:07 +12:00
robin.bygrave b065b030bc Bump ebean-agent to 18.3.0 2026-07-16 10:33:29 +12:00
robin.bygrave 837ba91126 Tests: Improve Redis testing - flush redis after test runs 2026-07-16 09:55:26 +12:00
robin.bygrave e18c664c98 Tests: Improve RedissonCacheFactoryTest 2026-07-16 08:54:17 +12:00
Rob Bygraveandrobin.bygrave 636bb28df3 #2540 Add DTO mapping from Entity beans via generated mapper code (#3852)
* #2540 Add DTO mapping from Entity beans via generated mapper code

A large feature, refer to the docs / guides etc. This is a bit like MapStruct in that it generates mapping code via annotation processing but it ALSO creates an optimised FetchGroup for the destination DTO graph and it also implements a generic interface such that it can be fluently used via query.mapTo().

The value is that it automates the mapping code from entity -> dto plus optimises the query used to support building the DTO graph.

Expected to be better to use than MapStruct for ebean orm users.

* Bump ebean-annotation to 8.6 with the dto annotations

* Adjust the build order to via test dependency on ebean-api

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-16 08:40:41 +12:00
Rob Bygraveandrobin.bygrave 84a311e521 Bug: filterMany() expressions when additional nested fetch has predicates in wrong place (#3851)
* Bug: filterMany() expressions when additional nested fetch has predicates in wrong place

When a filterMany() expression only references the many-root's own properties (e.g. .eq("status", ...)),
but the query also has an additional nested fetch beneath that many-path (e.g. .fetch("orders.customer")
or .fetch("contacts.group")), the filter predicate was incorrectly attached to the last/deepest join's
ON clause instead of the many-root's own join — silently misapplying the filter.

Fix: A hybrid approach:

- If the filterMany expression references only root-level properties → attach the predicate directly on the many-root's own join (correct).

 - If it references deeper/nested properties requiring "extra joins" (a separate existing mechanism) → fall back to the original end-of-subtree behavior, since no SqlTreeNode exists to attach to.

 - includeFilterMany() made idempotent so both mechanisms coexist safely.

* Bug: filterMany() with nested-property predicates silently ineffective
filterMany() expressions referencing a nested/dotted property (e.g.
.eq("group.name", "x"), alone or mixed with root properties) were
attached to a LEFT JOIN's ON clause via the "extra join" mechanism.

That only nulls the extra join's own columns on non-match - it does
not exclude the parent/many-side row, so the predicate had no actual
filtering effect (confirmed by data, not just SQL shape).

## Fix:

force any many-property fetch whose filterMany expression
references a nested property onto a query-join (secondary select
restoring filterMany's original always-fetchQuery behaviour for this
with a genuine WHERE clause) instead of leaving it as an inline JOIN, case.

- SpiExpressionValidation: track all visited property names (allProperties()), not just unknown ones.
- OrmQueryProperties: filterManyHasNestedProperty() walks the filterMany expression for any dotted property reference.
- OrmQueryDetail.markQueryJoins(): route nested-property filterMany chunks to markForQueryJoin() instead of the inline fetch-join slot, without consuming that slot for other many-paths.

Also simplifies the earlier root-only-predicate join-placement fix
(CQueryPredicates/DefaultDbSqlContext/SqlTreeNodeManyRoot) now that
a nested-property filterMany can never reach the JOIN-based code
path: removed the now-unreachable "deepest path" fallback branch,
the includeFilterMany() idempotency guard, and the redundant fallback
call in SqlTreeNodeManyRoot - isFilterManyAttachPoint() remains as
the single, always-used attach mechanism.

Updated TestQueryFilterMany/TestQueryFilterManySimple assertions to
expect the corrected query-join (2 statement) behaviour.

* Simplify SpiExpressionValidation with boolean nestedProperty flag

* Tidy up comments

* Fix OrmQueryProperties.filterManyHasNestedProperty() with real nested property check

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-15 23:16:18 +12:00
Rob Bygraveandrobin.bygrave 791ac13750 Fix findCount()/exists() wrapping a raw CTE (WITH clause) for SQL Server (#3850)
SQL Server does not support a WITH clause (CTE) nested inside a subquery
or derived table - only at the start of a statement. buildRowCountQuery()
and buildExistsQuery() wrap raw sql as `select count(*) from (<sql>)` /
`select case when exists(<sql>) then ...`, which breaks when <sql> starts
with a CTE header (e.g. RawSqlBuilder.withPlaceholders() queries), causing
"Incorrect syntax near the keyword 'with'" on SQL Server.

Add CQueryBuilder.topLevelSelectStart()/splitCteHeader() to detect and
hoist a leading WITH clause in front of the wrapping SELECT rather than
wrapping it along with the rest of the query. This is a no-op for the
common case (no leading CTE) and is portable across platforms.

- ebean-core: CQueryBuilder - hoist leading CTE header in
  wrapSelectCount()/wrapSelectExists()

- Add unit tests in CQueryBuilderTest reproducing the exact failing SQL

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 15:10:57 +12:00
Rob Bygraveandrobin.bygrave d54a24b27d #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0 (#3849)
* #3848 Fix regression for Sql Server and Oracle with exists() introduced in 18.2.0

Fix #3848: exists() generates invalid scalar SQL on SQL Server and Oracle

Query.exists() generated `select exists(<subquery>)`, which is valid on
Postgres/H2/MySQL but rejected by SQL Server and Oracle since both only
support EXISTS as a predicate, not as a directly selectable scalar boolean
expression.                                                                                                                                                                              ┃
                                                                                                                                                                                            ┃
Add DatabasePlatform.existsWithCaseWhen/existsFromClause capability flags
and use them in CQueryBuilder.wrapSelectExists() to generate
`select case when exists(<subquery>) then 1 else 0 end` on platforms that                                                                                                                ┃
need it, with an additional ` from dual` suffix for Oracle (which requires                                                                                                               ┃
a FROM clause on every select). CQueryExists reads the boolean result via                                                                                                                ┃
ResultSet.getBoolean(1), which correctly interprets the resulting 0/1 int.

* Fix tests TestInsertCheckUnique for Oracle and SQL Server

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 10:13:16 +12:00
Rob Bygraveandrobin.bygrave 73c9ff8d80 Throw exception on duplicate Database registration (#3838) (#3847)
* Throw exception on duplicate Database registration (#3838)

DatabaseFactory.create() with register(true) previously returned the
existing Database instance when another instance was already registered
under the same name (added in #3759). This silently caused two
independently created Database instances (e.g. with different
DataSourceConfig.url values) to collide/share state, since the
second "instance" was actually the first one in disguise.

Change this to throw an IllegalStateException instead, forcing
callers to either use a unique DatabaseConfig name or setRegister(false)
when the Database is not intended to be registered/looked up by name.

* Also handle deregistration on shutdown

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-14 00:34:49 +12:00
Rob Bygraveandrobin.bygrave bdbe743f56 #3275: Fix NPE when update/delete with @IdClass mapping (#3846)
## Root cause:

For @IdClass composite keys, the "id" is cached only in EntityBeanIntercept.ownerId(), populated only when reading from DB or during insert derivation. A fresh bean instance passed straight to DB.update()/DB.delete() had this null, causing an NPE in BindableIdEmbedded.dmlBind().

## Fix:

dmlBind() now defensively derives the id (reusing the existing insert-time derivation logic, extracted into a shared deriveId() helper) when it's null but derivable — covering both update and delete, since they share the same DML bind path.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 23:10:54 +12:00
Rob Bygraveandrobin.bygrave ed2026b858 #3110: Fix cache key with enum type (#3845)
## Root cause:

Finder.byId() on a @Cache entity converts the id to a String cache key via IdBinder.cacheKey() = scalarType.format(value). For enums, format() returns .name() (e.g. "APPROVED"), not the db-mapped value. On a  cache hit, BeanDescriptorCacheHelp.loadBeanDirect() converts that cache-key string back via desc.convertId() → IdBinderSimple.convertId() → scalarType.toBeanType(), which expects a db value (e.g. "2" for @EnumValue("2")),  not the enum's Java name — causing Integer.parseInt("APPROVED") to crash in EnumToDbIntegerMap.getBeanValue().

## Fix:

IdBinderSimple.convertId() now special-cases String input to use scalarType.parse() (the true inverse of  format()) instead of toBeanType() (the inverse of toJdbcType()/db-value). This mirrors an existing, already-correct pattern in IdBinderEmbedded.convertId(), which already handles String cache-key round-tripping via parse(). Verified  this doesn't affect other id types (Long/Integer/etc.) since parse() and toBeanType() are equivalent for them — the mismatch only exists for Enum-mapped types.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 22:42:16 +12:00
Rob Bygraveandrobin.bygrave 8d21f91f62 #2477: Support @DbArray inside an @Embeddable (#3844)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 21:45:11 +12:00
Rob Bygraveandrobin.bygrave 6d1d58b8c2 #1989: Add findPagedList() support to DtoQuery (when based on orm query) (#3843)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 20:34:25 +12:00
Rob Bygraveandrobin.bygrave a943ee9225 #2263: MySql DDL - generate inline comments for MySql DDL generation (#3842)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 15:56:06 +12:00
Rob Bygraveandrobin.bygrave 94f252f423 #1700: Add support for @OrderColumn on @ManyToMany (#3841)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-13 15:55:03 +12:00
Rob Bygrave 275f8ad9f9 #2393: @OrderColumn was not maintained for @ElementCollection — no order column populated on insert and no order by on fetch. (#3840)
## Root cause:
AnnotationAssocManys only parsed @OrderColumn inside the @OneToMany branch, and even if parsed, BeanDescriptorManager.checkMappedByOneToMany() returned early for element collections before reaching the order-column setup. The element collection's synthetic descriptor also isn't registered in deployInfoMap, so it couldn't reuse the existing @OneToMany lookup path.

## Fix (in ebean-core):

- BeanDescriptorManager: extracted makeOrderColumn(DeployBeanPropertyAssocMany, DeployBeanDescriptor) so the synthetic order property can be built against an explicit target descriptor, not just one looked up via deployInfoMap.

- AnnotationAssocManys.readElementCollection(): now reads @OrderColumn, sets fetchOrderBy, and builds the order property directly on the element descriptor before it's converted to a runtime BeanDescriptor.

- SaveManyElementCollection: binds a sequential 0-based index as the order column value on insert (element collections delete-then-reinsert the whole collection in list order, so this is a natural fit).

DDL generation and the fetch order by needed no changes - both already generalize over any BeanDescriptor's order column property once it exists.

Added EcolPerson / TestElementCollectionOrderColumn covering insert population, order preserved on reload, and order by present on fetch-join SQL.

Known limitation: @OrderColumn on a Map-based element collection (SaveManyElementCollectionMap) is not wired up - not part of this issue and not a standard JPA combination (Maps use @MapKeyColumn).
2026-07-13 15:11:49 +12:00
Rob Bygrave 9ac3bd71f6 Issue #2840: @DbJson/@DbJsonB Map<String,Object> fields ignored mutationDetection and always used ModifyAware-wrapper dirty checking — so NONE (and HASH) had no effect, unlike plain Object-typed @DbJson fields which correctly honored these modes. (#3839)
Root cause: The built-in ScalarTypeJsonMap/List/Set types (used for Map<String,Object>, simple List/Set) hard-coded mutable()=true and isDirty() to always do a ModifyAware check, regardless of the configured MutationDetection. Only SOURCE accidentally worked because it happened to route through BeanPropertyJsonMapper.

 Fix (in ebean-core, io.ebeaninternal.server.type):

 - ScalarTypeJsonValue now derives mutable (false only for NONE) and keepSource/jsonMapper() (true for HASH/SOURCE) from the property's MutationDetection, instead of a single hard-coded keepSource boolean.

- ScalarTypeJsonMap, ScalarTypeJsonMapEnum, ScalarTypeJsonList, ScalarTypeJsonSet, and ScalarTypeJsonCollectionValue now accept/pass through MutationDetection instead of a raw boolean.

- DefaultTypeManager.dbJsonType() passes the property's own mutationDetection() straight through for these collection types (per your choice: DEFAULT still always means ModifyAware for collections — only an explicit NONE/HASH/SOURCE annotation changes behavior; the DatabaseConfig-wide default is not applied to these types).

- @DbArray fallback call sites (PlatformArrayTypeJsonList/Set) updated to pass MutationDetection.DEFAULT, preserving unchanged behavior.
2026-07-13 10:24:25 +12:00
Rob Bygraveandrobin.bygrave bb46c88db1 Fix findVersions() unbound bind parameters on sql2011 history platforms (#3837)
On standards-based platforms (MariaDB, SQL Server, Oracle, DB2, HANA) the root table always generates a 'for system_time between ? and ?' clause for TemporalMode.VERSIONS, but bind values were only supplied  for findVersionsBetween(), leaving findVersions() with 2 unbound placeholders and shifted bind positions ("Parameter at position N is not set").

 Match the bind guard to the SQL generation condition and default start/end to epoch/now when not explicit.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-07 21:05:22 +12:00
Rob Bygraveandrobin.bygrave 28e6108315 Bump mysql driver to mysql-connector-j (#3836)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-07 10:17:09 +12:00
robin.bygrave c20d1cdf9b Fix CI test execution with redis reused across ebean-redis and ebean-redisson modules 2026-07-07 10:09:46 +12:00
robin.bygrave 007409d21c Bump postgresql to 42.7.11 2026-07-07 09:21:14 +12:00
Rob Bygrave 8b61069b3f #3441 Handle type coercion in hits against bean cache (#3835) 2026-07-06 22:04:09 +12:00
Rob BygraveandRoland Praml a82dcb2509 Fix #3041 check uniqueness cache skipclean (#3834)
* checkUniqueness supports queryCache and skipClean

* Keep the DB.checkUniqueness() as per original (not overload there)

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 22:03:52 +12:00
Rob BygraveandRoland Praml 08bb170cb2 Fix #3156 inherited property access (#3833)
* Properties of inherited models can be used on different child models

* Adjust typos only

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-07-06 21:08:35 +12:00
Rob Bygraveandrobin.bygrave a2f954a60e #3529 - Fix for M2M property is empty in preDelete of BeanPersistAdapter (#3830)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-06 19:47:37 +12:00
Andrey Glushkov 1d654e350b ebean-redisson - initial commit (#3711) (#3829)
* ebean-redisson - initial commit

* Thread.sleep(150) in IntegrationTest - ebean-redisson

* Fix two tenant-aware cache bugs in ebean-redisson + add test coverage

CacheCodec.getMapKeyDecoder() was returning only the tenant portion of
"id:tenantId" Redis field names (substring after the first colon).  This
caused every tenant-aware getAll() call to miss: the decoded key did not
match the lookup key, so the DuelCache near-cache warm-up path also
failed to populate.

RedissonCache.getAll() was returning String keys instead of the original
key objects passed in.  This broke the ServerCache contract and caused
DuelCache.near.putAll() to store entries under plain strings, making all
subsequent near.get(originalId) calls miss even when the remote cache had
the data.

Additional issues found while reviewing against ebean-redis:
- RServerCacheNotify.notify() was calling listener.notify() locally,
  causing a second redundant cache invalidation on the originating node
  (ebean-redis does not do this).
- processTableNotify() had no null-guard on listener, risking NPE if a
  remote table-mod message arrived before createCacheNotify() was called.
- errorOnWrite() was throwing RuntimeException; cache writes must be
  best-effort and only log on failure.

Tests added:
- RedissonCacheTest: direct cache tests for all operations (put/get,
  getAll, putAll, remove, removeAll, clear, statistics, TTL, maxSize trim)
- RedissonCacheFactoryTest: factory tests covering cache type creation,
  DuelCache for near caches, query-cache singleton, and cross-factory
  cluster notification
- CacheCodecTest: key encoder/decoder regression test that pins the
  "full string returned" behaviour for both plain and tenant-aware keys
- SerializableCodecTest, VersionGatedCodecTest: codec round-trip tests
- TenantAwareCacheTest: integration test that creates a tenant-aware
  Database and verifies that single-bean finds and multi-ID findList()
  calls are isolated per tenant at the Redis level

* 18.2.0 - updated version

* RedissonCache/FactoryTest: start Redis container directly in @BeforeAll

Tests were skipped whenever they ran before the integration tests triggered
DB/Redis container startup. Each test class now calls
RedissonTestFixtures.startRedis() (RedisContainer.builder("latest").start())
which is idempotent and requires no other test class to run first.
assumeTrue(isReachable()) remains as a fallback for Docker-less environments.
2026-07-06 19:43:40 +12:00
robin.bygrave 6dd1763e54 Fix test TestRawSqlWithPlaceholders for Postgres HAVING clause limitation
Postgres having can't use column alias from select clause
2026-07-06 16:42:51 +12:00
Rob Bygraveandrobin.bygrave b32f3bcad6 Fix test-java16 parent etc (#3831)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-06 16:23:56 +12:00
Rob Bygrave e525d4e159 Version 18.2.0 2026-07-03 19:38:05 +12:00
Rob Bygraveandrobin.bygrave 4e639cb88a Add deletePermanent() - hard delete for soft delete capable beans (#3828)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 18:23:18 +12:00
Rob Bygraveandrobin.bygrave fc78b2af80 Add RawSqlBuilder.withPlaceholders() for CTEs and other complex SQL (#3649) (#3827)
* Add RawSqlBuilder.withPlaceholders() for CTEs and other complex SQL (#3649)

RawSqlBuilder.parse() uses keyword-based scanning to locate SELECT
columns and the WHERE/HAVING injection points. This fails for CTEs,
window functions and other complex SQL where "select"/"from" keywords
appear in places the scanner doesn't expect.

withPlaceholders(sql) skips SELECT/FROM column parsing entirely and
only locates the ${where}, ${andWhere}, ${having} and ${andHaving}
placeholder positions, requiring explicit columnMapping() calls (as
with unparsed()). This lets dynamic where()/having() expressions be
injected into otherwise unparseable SQL.

Also fixes two bugs in the underlying placeholder-position splitting:
- static SQL following a ${having}/${andHaving} placeholder (e.g. a
  trailing ORDER BY) was silently dropped when both a where and a
  having placeholder were present
- using only ${having}/${andHaving} (no where placeholder) caused a
  dynamically added HAVING clause to be appended after trailing
  static SQL, producing invalid SQL

Changes:
- RawSqlBuilder.withPlaceholders(sql) + SpiRawSqlService.withPlaceholders()
- DRawSqlParser.parseAsTemplate() / parseTemplate() - placeholder-position
  only parsing, correctly splitting preWhere/preHaving/trailing SQL
- CQueryBuilderRawSql - skip column-list and "select" prefix handling
  in template mode (signalled by an empty preFrom)
- Unit tests in DRawSqlServiceTest covering where/andWhere/having/andHaving
  placeholder combinations, including the two fixed edge cases
- Integration tests in TestRawSqlWithPlaceholders (ebean-test) covering
  CTE queries with dynamic where/having and verifying generated SQL

* Add examples test using query beans

* Add docs guides for RawSql

* Add ${orderBy} ${andOrderBy}

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 18:22:42 +12:00
Rob Bygraveandrobin.bygrave 29647ebe1a Fire BeanPersistController when only ManyToMany collection changes (#3652) (#3826)
When only a @ManyToMany collection is modified and saved, the parent bean has no dirty scalar properties so BeanPersistController preUpdate/postUpdate were never invoked — only the junction table rows were written.

Fix mirrors the existing preElementCollectionUpdate() mechanism: add
preManyToManyUpdate() on PersistRequestBean and call it from
SaveManyBeans.saveAssocManyIntersection() when the intersection actually
changes (vanillaCollection, forcedUpdate, or a tracked BeanCollection
with non-empty additions or removals). The !dirty guard in
preManyToManyUpdate() prevents double-firing when the bean itself is
also dirty.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-03 11:43:16 +12:00
Rob Bygraveandrobin.bygrave 38146fdce5 Support option to have always non-null @Embeddable(s) refereneces (#3825)
(#3702)

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 22:02:15 +12:00
Rob Bygraveandrobin.bygrave 864aff6c3b Implement query.exists() via select exists(...) (#3824)
* Implement query.exists() via select exists(...)

Previously query.exists() was implemented by running a findIds() limited to 1 row and checking whether the list was empty. This PR replaces that with a dedicated select exists(select 1 ...) execution path.

## Changes:

• CQueryExists — new query executor, analogous to CQueryCount, that runs select exists(...) and returns the boolean result directly from the JDBC ResultSet
• CQueryBuilder.buildExistsQuery() — builds select exists(select 1 ...) SQL, reusing the query plan cache on repeat calls
• CQueryEngine.findExists() / DefaultOrmQueryEngine / OrmQueryEngine interface — wire the new executor through the standard query engine stack, including SQL logging, summary logging, and query cache put
• DefaultServer.exists() — delegate to request.findExists() instead of findIds()

## Tests added to TestQueryExists:

• testExistsBoolean_returnsFalse — verifies false is returned when no rows match
• testExistsBoolean_withJoin — exercises the join SQL path in buildExistsQuery; also checks false when the join yields no match
• testExistsBoolean_queryPlanReuse — runs the same query twice and asserts the generated SQL is identical, confirming the plan cache is hit on the second call

* Fix TestQueryExists only

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 18:32:50 +12:00
Rob Bygraveandrobin.bygrave a161a42742 #3676 Add DbMigration.setAddForeignKeySkipCheck(true) (#3822)
Provides an option to include IDE -- @formatter:off style comments into the db migration generated sql.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 17:45:32 +12:00
Rob Bygraveandrobin.bygrave 24da374aa8 Fix #3821 fetchQuery secondary tables not included in query cache dep… (#3823)
* Fix #3821 fetchQuery secondary tables not included in query cache dependent tables

When a query uses fetchQuery("path"), the secondary table(s) must be registered as dependent tables in the query cache entry so that updates to those tables properly invalidate the cache.

## Fix:
In CQueryPlan, after building the SQL-tree dependent tables, walk the query's OrmQueryDetail for any fetchQuery paths and resolve each path through the root BeanDescriptor to its target entity's baseTable(). These tables are merged into dependentTables at plan construction time (once, then cached in the plan).

This means the fix applies uniformly whether paths were specified via fetchQuery("path") calls or via query.select(fetchGroup) — both populate the same OrmQueryDetail.

### Tests added to TestQueryCacheTableDependency:

• fetchQuery_oneToMany_invalidatesQueryCacheOnSecondaryTableUpdate — updating a contact invalidates a Customer query cache that includes fetchQuery("contacts"); asserts the refreshed result contains the updated phone number

• fetchQuery_manyToOne_invalidatesQueryCacheOnSecondaryTableUpdate — updating a root record invalidates an ECacheChild query cache that includes fetchQuery("root")

* Ensure tests cleanup their test data

* query.secondaryQuery() (called during prepareQuery()) removes fetchQuery paths from OrmQueryDetail before CQueryPlan is built. The existing buildDependentTables iterated detail.entries() looking for isQueryFetch() entries — but they were already gone.

Changes:

1. OrmQueryRequest.java — added secondaryQueries() getter to expose the already-stored SpiQuerySecondary field.
2. CQueryPlan.java — rewrote buildDependentTables to accept SpiQuerySecondary instead of OrmQueryDetail. It now iterates secondary.queryJoins() (the paths already removed from detail) and adds each path's base table to the dependent tables set.

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 17:44:10 +12:00
Rob Bygraveandrobin.bygrave 398848378d Fix asOf() queries bypassing bean cache isolation (#3820)
* Fix asOf() queries bypassing bean cache isolation

asOf() queries must never read from or write to the bean cache since the cache holds only current state. Previously, lazy loads and secondary queries spawned by an asOf parent could return stale  current data from the cache instead of the historical snapshot.

- DefaultServer.findId(): skip bean cache check when isAsOfQuery()
  so the findById fast-path doesn't return current state for temporal queries

- DLoadContext: force useBeanCache=CacheMode.OFF when asOf != null so hitCache=false for all lazy load batch cache checks, and propagate CacheMode.OFF to secondary (+query) joins

- LoadBeanRequest.configureQuery(): when !loadCache (parent context disabled cache), explicitly set CacheMode.OFF on the lazy-load SQL query — previously left at CacheMode.AUTO which caused cache reads/writes even when the parent had cache disabled

Fixes #3713

* Simplify asOf to CacheMode.OFF

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 09:06:36 +12:00
Rob Bygraveandrobin.bygrave 688eee532c Fix #3817 incorrect type deserialization for @DbJson field in generic superclass (#3819)
In createJsonObjectMapperType(), remove the instanceof DeployBeanProperty
branch that overrode ownerType() with getField().getDeclaringClass().

For a generic superclass B<T>, getDeclaringClass() returns B with T
unresolved, so Jackson treated the field as Object and deserialized to
LinkedHashMap. DeployBeanProperty.ownerType() already returns
desc.getBeanType() (the concrete subclass), which gives Jackson the full
supertype context needed to resolve T correctly.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-02 00:08:47 +12:00
Rob BygraveandIliya Ivanov 88d7b5e85a Support @ManyToOne inside @Embeddable for query predicates and joins (#3816)
* Make embeddable diggable

* WIP

* WIP

* WIP

* WIP

* WIP

* Support @ManyToOne inside @Embeddable for query predicates and joins

 Fix path resolution for @ManyToOne associations nested inside @Embeddable
 classes. Previously, a query like .where().eq("address.country.name", "NZ")
 on an entity with an @Embedded EAddr (containing @ManyToOne Country country)
 would throw PersistenceException: Embedded Property country.name not found.

 Changes:
 - BeanPropertyAssocOne.buildElPropertyValue: correctly resolve @ManyToOne
   paths inside embedded by extracting first segment, validating via
   embeddedPropsMap, then delegating deeper traversal to the overridden
   property; restore embedded flag on ElPropertyChain for scalar leaves
 - BeanPropertyAssoc.targetDescriptor(): lazy-init fix for override copies
   created by BeanEmbeddedMetaFactory (initialise() is not called on them)
 - BeanDescriptor.extraJoin(): restore !assocProp.isEmbedded() guard to
   prevent null-table ExtraJoin for composite keys and other embeddables
 - ElPropertyChainBuilder/ElPropertyChain: restore embedded field and prefix
   computation so alias resolution works correctly for paths through embedded
   (e.g. outer.datePeriod.date1 → prefix "outer" not "outer.datePeriod")
 - SqlTreeBuilder.buildExtraJoins: remove erroneous removeAll on
   orderByIncludes that broke Formula2 placeholder resolution and distinct
   on aggregation queries with many-side order-by joins
 - SqlTreeAlias.addJoin: fix indentation
 - Tests: add three test cases to TestEmbeddedManyToOne covering WHERE
   predicate through embedded FK column, through association property
   (requiring a JOIN), and combined with fetch

* Throw PersistenceException with unknown path

---------

Co-authored-by: Iliya Ivanov <i.ivanov@proforge.org>
2026-07-01 23:33:40 +12:00
Rob Bygraveandrobin.bygrave 985e3b12b1 Fix ON_CONFLICT_NOTHING cascade causing FK violation (#3712) (#3818)
When inserting a bean with InsertOptions.ON_CONFLICT_NOTHING that has cascade children (OneToMany / exported OneToOne), a unique constraint conflict caused the parent insert to be silently skipped (0 rows), but Ebean still cascaded and attempted to insert the children — resulting in a FK violation.

Changes

• BeanDescriptor.hasCascadeChildren() — returns true when the bean has save-cascade children (OneToMany or exported OneToOne) that hold a FK back to this bean.
• PersistRequestBean.setInsertOptions() — when ON_CONFLICT_NOTHING is used and the bean has cascade children, sets skipBatchForTopLevel = true so the parent INSERT executes immediately (non-batched). This ensures the row count is known before any cascade runs. Beans without cascade children are unaffected and continue to batch normally.
• PersistRequestBean.checkRowCount() — when the parent INSERT returns 0 rows under ON_CONFLICT_NOTHING, sets insertConflictSkipped = true and returns early, leaving the bean unmarked as loaded/persisted.
• DmlHandler.checkRowCount() — skips postExecute() when the insert was conflict-skipped.
• DefaultPersister.insert() — guards saveAssocMany() with !request.isInsertConflictSkipped(), preventing cascade saves when the parent was not actually inserted.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-01 23:32:02 +12:00
Andrey Glushkov 942bab5efb Fix: DuplicateKeyException when cascade-saving through a @ManyToOne to an unmodifiable bean (address #3813) (#3814)
* Fix DuplicateKeyException with unmodifiable cascaded bean

* Fix broken test TestDeleteByQuery.maxDelete_when_beanCaching_expect_selectThenDelete
2026-07-01 22:43:33 +12:00
Rob Bygrave c114ebaec5 Merge pull request #3815 from ebean-orm/feature/add-sqlite-ioc
Sqlite: Add Insert On Conflict support (#3770)
2026-07-01 22:02:22 +12:00
robin.bygrave 9acc555e8e Sqlite: Add Insert On Conflict support (#3770) 2026-07-01 21:58:16 +12:00
Rob Bygrave 73a646e7dc Merge pull request #3812 from dragkes/feature/invalidate-one-to-one-and-collection-cache
Invalidate one to one and collection cache (address #3811)
2026-07-01 21:21:19 +12:00
Rob Bygrave bdda502d55 Merge pull request #3810 from ebean-orm/tenant-support-for-query-plans
Tenant support for query plans
2026-07-01 21:08:33 +12:00
robin.bygrave 6d447a6c1a Add back the rollback() into CQueryBindCapture 2026-07-01 21:04:59 +12:00
robin.bygrave 16872fdb30 Add back the rollback() into CQueryBindCapture 2026-07-01 21:00:19 +12:00
robin.bygrave ec9303762e Fix the CQueryPlanManager conflict edit? 2026-07-01 20:50:34 +12:00
Andrey Glushkov a59c70054b Tests 2026-07-01 11:48:28 +03:00
Andrey Glushkov 25bce83afc Merge remote-tracking branch 'origin/master' into feature/invalidate-one-to-one-and-collection-cache
# Conflicts:
#	ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java
2026-07-01 11:46:30 +03:00
Rob Bygrave e372579b15 Merge branch 'master' into tenant-support-for-query-plans 2026-07-01 20:33:31 +12:00
robin.bygrave f470782481 Whitespace and remove excess comments only 2026-07-01 20:29:36 +12:00
Andrey Glushkov d7b9f6688a Invalidate inverse association caches on owning-side FK change 2026-07-01 11:25:27 +03:00
Rob Bygraveandrobin.bygrave 66bcfd107d Add tenant-partitioned L2 caches (#2956) (#3809)
* Add tenant-partitioned L2 caches (#2956)

 When `tenantPartitionedCache` is enabled, each tenant gets its own
 cache namespace (keys include the tenant id), improving cache-hit
 ratio by preventing cross-tenant key collisions.

 Refactor BeanDescriptorCacheHelp to abstract base class with two
 concrete subclasses:
 - BeanDescriptorCacheHelpFixed - static cache refs (original behaviour)
 - BeanDescriptorCacheHelpPartitioned - per-request Supplier<> lookup
   so the correct tenant-scoped cache is resolved on each access

 Fix background-thread invalidation: clear(name) in partitioned mode
 now scans all matching cache entries by key prefix rather than calling
 tenantProvider.currentId() which is null/wrong on background executor
 threads.

 Add SpiCacheManager.clearTenant(tenantId) to allow removal of all
 cache entries for a deactivated tenant, preventing unbounded memory
 growth in long-running multi-tenant deployments.

 Also load cache settings (cacheMaxSize, cacheMaxIdleTime, etc.) from
 application properties - these were previously missing from loadSettings().

* Fix versions in test-java16

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-01 20:04:08 +12:00
robin.bygrave 3d2cc2d4af Version 18.1.0 2026-07-01 13:28:23 +12:00
robin.bygrave c73330e9de Bump ebean-agent to 18.1.0 2026-07-01 13:21:42 +12:00
Rob Bygraveandrobin.bygrave a317d669f0 Deps: Bump ebean-datasource to 10.10 (exclude isValid from metrics) (#3808)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-01 12:57:23 +12:00
Rob Bygrave 291966cea9 Test and Fix for #3806 - @OneToMany query that populates from the immutable cache gets NPE (#3807)
## Root cause

  A findList() triggers a secondary @OneToMany query that populates from the immutable cache. ImmutableBeanCaches.QueryLoader runs find(type).setUnmodifiable(true)...findMap(), which hits the L2 bean cache (cacheIdLookup → loadBeanDirect). Two coupled bugs in the unmodifiable path:

## 2 Bugs:
   1. Null pc (the NPE). In BeanDescriptorCacheHelp.loadBeanDirect, the if (context == null) context = new DefaultPersistenceContext() fallback was nested inside if (!unmodifiable). So an unmodifiable load passed a null context to CachedBeanDataToBean.load; converting a @ManyToOne (refBean → contextGet → pc.get) NPE'd — exactly Eddie's trace.
   2. Mutable reference can't freeze. Once a context was provided, refBean created the @ManyToOne ref via createRef → a mutable InterceptReadWrite bean. The subsequent unmodifiableFreeze then threw UnsupportedOperationException("never expected") (only InterceptReadOnly is freezable).

  ## Fix (2 files)

   - BeanDescriptorCacheHelp.loadBeanDirect — always ensure a non-null context before CachedBeanDataToBean.load (hoisted out of !unmodifiable).
   - BeanPropertyAssocOne.setCacheDataValue/refBean — when the owning bean is unmodifiable (derived via !(intercept instanceof InterceptReadWrite), the same idiom CachedBeanDataToBean already uses), create the ref with createReference(unmodifiable, false, id, pc) → a freezable InterceptReadOnly reference. Also guards contextGet against null. Modifiable path is unchanged.

## Notes
  The "fetch(path) without a FetchGroup" clue

  That's the trigger, not a misuse. A restricting FetchGroup can exclude the @ManyToOne, so no refBean runs and the bug stays hidden. The default/fetch("path") select includes the FK, so the cached-bean conversion creates the assoc-one reference and hits the bug. His usage was fine — this was an Ebean gap (no immutable-cached entity with a @ManyToOne was test-covered).
2026-07-01 12:54:12 +12:00
robin.bygrave ad05ed051b Modify SequenceIdGenerator to internally use ArrayDeque 2026-06-30 22:49:19 +12:00
Rob Bygraveandrobin.bygrave 01b8c3dbcb Multi-tenant aware DB sequence id generation (replaces #2305) (#3805)
## Problem

SequenceIdGenerator captured a single DataSource at deploy time and held one shared pre-fetch buffer. Under multi-tenancy this is wrong:

 - TenantMode.DB/DB_WITH_MASTER — there is no bootstrap DataSource, so BeanDescriptorManager passed null; sequence allocation couldn't resolve the current tenant's database.
 - TenantMode.SCHEMA/CATALOG — a single shared buffer let one tenant's pre-fetched ids be handed to another (cross-tenant bleed), and pre-fetch used a connection that wasn't scoped to the requesting tenant.

(Supersedes the per-datasource delegator approach in #2305, which leaked via a WeakHashMap whose values strongly referenced the keys, only handled DB mode, and re-resolved the tenant on the background thread.)

## Fix:

Make SequenceIdGenerator itself tenant aware, keeping all platform modules untouched.

 - New TenantConnectionSource (ebean-api, additive): optional interface a DataSource may implement — currentTenantId() + connectionForTenant(tenantId).
 - SequenceIdGenerator: the shared idList/lock/loading flag become a per-tenant TenantBuffer keyed by tenantId in a ConcurrentHashMap. Connections are obtained per tenant. Background pre-fetch captures the tenant at submit time (the executor thread has no tenant in scope) and fetches by explicit tenantId — fixing a latent ThreadLocal-propagation bug.
 - SequenceDataSource (ebean-core): adapts DataSourceSupplier to TenantConnectionSource; routes to the tenant DB (DB mode) or sets schema/catalog (SCHEMA/CATALOG).
 - Wiring: InternalConfiguration exposes the DataSourceSupplier; BeanDescriptorManager wraps it only for dynamic-datasource tenant modes.

## Performance (single-tenant unaffected)

 - A cached single buffer field short-circuits the ConcurrentHashMap for the non-tenant key.
 - NONE/PARTITION pass the plain DataSource (not wrapped), so tenantSource == null and the hot path is just a couple of cheap branches — equivalent to the original.

## Compatibility

 - Platform constructor signature (be, ds, seqName, allocationSize) unchanged — no changes to the 9 platform modules.
 - One protected-method signature changed: getMoreIds(int) → getMoreIds(Object tenantKey, int). Rarely overridden (subclasses override getSql/readIds), but a source-incompat for any external custom platform that did.

## Tests

 - TenantSequenceTest — DB-per-tenant: tenant 1 → 1,2,3; tenant 2 independently → 1.
 - SequenceBatchIdGeneratorTest adapted to the per-tenant buffer.
 - Existing sequence + multitenancy suites pass.

## Potential Follow-ups (not in this PR)

 - Add a SCHEMA-mode test.
 - Optional removeTenant(tenantId) hook if unbounded tenant churn is a concern (buffers hold only Longs + a lock, no DataSource, so no real leak).
 - SimpleSequenceIdGenerator (non-batching) left as-is — already uses the txn connection.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-30 22:28:28 +12:00
robin.bygrave 443b68a3b0 For #3082 - Fix and test for History findVersions() that joins to other history table
Bug (#3083): findVersions() on a @History root that joins to another @History entity bound the joined table's effective-date predicate with a null as-of timestamp → systime <@ null (Postgres) / sys_period_start <= null (H2) → zero rows.

  Fix (CQueryPredicates.bind(), +6 lines): when the as-of is null but there are history-view join predicates to bind (asOfTableCount > 0 — only ever true for findVersions/findVersionsBetween), default to the current timestamp. Semantics: all versions of the root, joined to related @History entities as they are now. The root table is untouched (still returns all versions).

  Test ported from #3082 :
   - New HistoryManyToOne entity (@History + @SoftDelete) — kept lean, dropped the PR's unneeded @OneToMany
   - HistorylessOneToOne gains @ManyToOne HistoryManyToOne
   - New testVersionsWithHistoryOverHistoryless() — replaced the PR's always-failing placeholder (assertSql(...).contains("this does not exist")) with real assertions: count == 1 and SQL contains no asOf null bind
   - Left the existing @OneToOne unchanged (the PR's optional = false tweak to the shared model wasn't needed)
2026-06-30 21:08:30 +12:00
Andrey Glushkov 98f0d42b7e Fix: defer savepoint cache changes to parent transaction commit (#3804) 2026-06-30 18:47:24 +12:00
Andrey Glushkovandrobin.bygrave 959951203d Address #3801 (#3802)
* Fix NPE resolving generic types across multi-level mapped superclass hierarchies

The fix introduced in ebc90e0e resolved TypeVariables only one level at a time:
mapGenerics(beanType) read only the direct generic superclass, so for a chain like
A extends B<String> / B<T> extends C<T>, processing C's fields produced an empty
map, genericTypeMap.get(TypeVariable) returned null, propertyType became null, and
AnnotationFields.readField threw an NPE calling prop.getPropertyType().isEnum().

Fix: build the full type-variable map once for the concrete bean type using
TypeResolver.getTypeVariableMap, which walks the entire superclass/interface
hierarchy and composes TypeVariable bindings transitively. The same map is passed
at every level of the recursive createProperties walk, so any TypeVariable at any
depth resolves correctly.

Type-resolution helpers (resolveType, resolveToClass, resolveCollectionTarget,
ResolvedParameterizedType) are consolidated in TypeReflectHelper so they are
shared across callers and independently testable.

Tests: TypeReflectHelperTest covers single and multi-level TypeVariable resolution
and collection-element resolution. QProductWithGenericTest adds an integration
regression test using a two-level generic chain
(ProductWithGenericMiddle extends GenericMiddleModel<Long> extends GenericBaseModel<Long>).

* Restore prior format only on DeployCreateProperties

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-30 16:18:41 +12:00
Rob Bygrave c47fbd411b Merge pull request #3800 from ebean-orm/feature/default-mutation-detection
Fix NPE in DefaultTypeManager.keepSource() when no ebean-jackson-mapper to default mutation detection to NONE
2026-06-29 13:16:43 +12:00
robin.bygrave 4b75820fd6 Fix NPE in DefaultTypeManager.keepSource() when no ebean-jackson-mapper to default mutation detection to NONE
Otherwise, can produce NullPointerException:

``
Caused by: java.lang.NullPointerException
	at io.ebeaninternal.server.type.DefaultTypeManager.keepSource(DefaultTypeManager.java:340)
	at io.ebeaninternal.server.type.DefaultTypeManager.dbJsonType(DefaultTypeManager.java:327)
	at io.ebeaninternal.server.deploy.parse.DeployUtil.setDbJsonType(DeployUtil.java:207)
	at io.ebeaninternal.server.deploy.parse.DeployUtil.setDbJsonBType(DeployUtil.java:201)
	at io.ebeaninternal.server.deploy.parse.AnnotationFields.initDbJson(AnnotationFields.java:227)
	at io.ebeaninternal.server.deploy.parse.AnnotationFields.readField(AnnotationFields.java:133)
	at io.ebeaninternal.server.deploy.parse.AnnotationFields.parse(AnnotationFields.java:62)
	at io.ebeaninternal.server.deploy.parse.ReadAnnotations.readInitial(ReadAnnotations.java:29)
	...
``
2026-06-29 12:48:06 +12:00
Rob Bygraveandrobin.bygrave 97bd0e1bb8 Add Transaction.setGeneratedPropertiesEnabled(boolean) (#3799)
Adds transaction-level control over whether Ebean auto-generates values for @WhenCreated, @WhenModified, @WhoCreated and @WhoModified properties.

Motivation

Backup/restore scenarios need to preserve the original audit timestamps and user values when re-inserting exported data. Without this, every save overwrites those fields with the current time/user.

Usage

 try (Transaction txn = DB.beginTransaction()) {
   txn.setGeneratedPropertiesEnabled(false);
   bean.setWhenCreated(originalTimestamp);
   bean.setWhenModified(originalTimestamp);
   DB.save(bean);
   txn.commit();
 }

Behaviour

 - Disabled (false): generated property values are only written if the property currently has a null value. Any non-null value set on the bean is preserved.
 - @Version is unaffected: the version property always auto-increments regardless of this setting, preserving optimistic locking integrity.
 - Default is true: all existing behaviour is unchanged.

Files changed

 - Transaction — new setGeneratedPropertiesEnabled(boolean) with javadoc
 - SpiTransaction — new isGeneratedPropertiesEnabled()
 - SpiTransactionProxy — delegates both methods
 - JdbcTransaction — field + implementation (default true)
 - NoTransaction, ImplicitReadOnlyTransaction — no-op setter, true getter
 - PersistRequestBean — onInsertGeneratedProperties, onUpdateGeneratedProperties, onFailedUpdateUndoGeneratedProperties all gate on isGeneratedPropertiesEnabled()
 - TestGeneratedProperties — 3 new tests covering insert-preserves, insert-null-still-filled, and update-preserves

Supersedes

PR #2943 — same feature, renamed from setOverwriteGeneratedProperties to setGeneratedPropertiesEnabled for a clearer, positive-sense API.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-26 18:53:29 +12:00
dependabot[bot] 0f5c7e4390 Build(deps-dev): Bump com.fasterxml.jackson.core:jackson-databind (#3797)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) from 2.14.1 to 2.22.0.
- [Commits](https://github.com/FasterXML/jackson/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-25 22:33:35 +12:00
Rob Bygraveandrobin.bygrave d7d040d031 Add @Formula2 support on @ManyToOne association properties (#3798)
Extends @Formula2 so it can be placed on a @ManyToOne field. Instead of embedding physical SQL aliases (as @Formula(join=...) requires), a @Formula2 expression uses logical bean paths — the required joins are derived automatically.

  Example
``
   // Before — physical SQL, brittle alias wiring:
   @Formula(select = "coalesce(${ta}.some_bean_id, j1.some_bean_id)", join = PARENTS_JOIN)
   @ManyToOne EBasic effectiveBean;
```
```
   // After — logical paths, joins resolved automatically:
   @Formula2("coalesce(someBean.id, parent.someBean.id)")
   @ManyToOne EBasic effectiveBean;
``
  The generated SQL is identical; the annotation is far more readable and maintainable.

  What changed

  Annotation parsing (AnnotationAssocOnes) — @Formula2 on a @ManyToOne is now recognised and the expression parsed into a select fragment and a set of dependency join paths.

  Query tree building (SqlTreeBuilder) — three scenarios all handled correctly:

   - Fetched as a tree node — dependency joins are inserted before the formula2 join using addChildFirst
   - Partial parent fetch — dependency joins are registered even when the parent chunk only selects its ID column
   - Predicate-only (where clause, no fetch) — addFormula2JoinsFromPredicates runs before buildSelectChain so dependency join paths are populated before buildExtraJoins constructs the extra-join tree; IncludesDistiller uses addChildFirst to preserve ordering within the extra-join tree

  Init ordering (BeanDescriptorManager) — initFormula2Properties() moved to a dedicated pass 5, after all descriptors are fully initialised, so cross-descriptor path resolution (e.g. parent.parent.someBean.id) is always safe.

  SqlTreeNodeExtraJoin — gained addChildFirst() to match SqlTreeNodeBean, enabling formula2 dependency joins to be prepended ahead of the formula2 property join in the extra-join tree.

  Fixes

  Supersedes and resolves #2773 — the reported bug (wrong join ordering when combining fetch and where on a formula-joined field) is eliminated for ChildPerson.effectiveBean and ParentPerson.effectiveBean, which are now expressed as @Formula2.

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-25 22:18:10 +12:00
robin.bygrave 6ec7c61594 Build: Modify test build.yml to exclude test-java16 by default
The test-java16 module needs to run AFTER a mvn install now due
to the SequencedSet/SequencedMap MR-JAR setup (as without the mvn
install it picks up BeanSet/BeanMap from target/classes and that's
the Java 11 version of BeanSet/BeanMap.
2026-06-25 13:05:59 +12:00
robin.bygrave 0126470391 Update gh workflows to use 21 (due to SequencedSet support with MR-JAR) 2026-06-24 22:12:05 +12:00
Rob Bygraveandrobin.bygrave d5f32690ea Support aggregation functions like sum on Formula2 (#3796)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-24 21:44:40 +12:00
Rob Bygraveandrobin.bygrave d7a3417fe4 Refactor internals only - rename methods on DeployProperty (#3795)
Refactor rename only, no change in logic here

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-24 19:07:24 +12:00
Rob Bygrave 141f98f5d7 Merge pull request #3794 from ebean-orm/feature/dto-dbjson
FEATURE: DbJson Support for Dto-Queries (was #3143)
2026-06-24 18:58:16 +12:00
robin.bygrave fd74fed34f Tidy DtoMetaDeployProperty and DtoMetaProperty 2026-06-24 18:54:10 +12:00
robin.bygrave 274411b8fa Add some missing @Override annotations 2026-06-24 18:23:17 +12:00
robin.bygraveandCopilot 691f153d89 Fix DtoMetaDeployProperty: remove unused Method param; fix findField superclass traversal; wire up findMetaAnnotations for setter annotation support
- DtoMetaDeployProperty: remove unused Method parameter from constructor
- DtoMetaProperty.findField: use loop variable 'type' (not outer 'dtoType') so
  superclass fields are correctly found
- DtoMetaProperty constructor: call findMetaAnnotations() which merges both
  field and setter annotations, rather than reading field annotations only
- DefaultTypeManager: keep both MethodType import and Annotation import
- Fix missing imports in EbeanServerFactory_ServerConfigStart_Test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 18:16:41 +12:00
Roland Praml dd1acf58a0 DbJson Support for Dto-Queries 2026-06-24 18:14:45 +12:00
Rob Bygrave 7bf8f7fbce With Database register(true) check for existing registered Database by name (#3759)
And return the existing Database instance if present.

In theory this should not be needed. There are some test setups that hit this situation.
2026-06-24 18:02:45 +12:00
Rob Bygraveandrobin.bygrave 490c70a7b7 PR #3656 fix - lazy initialised O2M relation are not correctly persisted on subsequent saves (#3793)
- Root cause: SaveManyBeans.removeAssocManyOrphans() only called setModifyListening when insertedParent=true. A first save with null O2M skipped it; the lazily-initialized collection on subsequent saves had no listen mode → .clear() untracked → orphan not deleted.
 - Fix: Added else { setListenMode(c, many); } to set the listen mode for existing collections that were never initialized (uses the existing null-guard helper).
 - Tests: testModifyListenModeSet2 now passes; all 26 cascade tests green.

-------------
Original #3656 description:

We found an issue, when O2M relations are not correctly persisted to the DB.

This happens, when

a bean is saved and the O2M property is empty
something is added and cleared again in two subsequent saves.
the same master-bean object has to be used
The issue here is, that the BeanCollection is lazily initialized with no modifyListenMode set after the first save.
This happens only for O2M relations with no order column. (Others work fine) See: https://github.com/ebean-orm/ebean-agent/blob/d4c40f1ce85c58f99cb0a85152aaa0a0075a9c01/ebean-agent/src/main/java/io/ebean/enhance/entity/FieldMeta.java#L469

And we need also a save, where the bean is saved with an empty/null value in the O2M property.
Subsequent saves will not update the modifyListenMode. See SaveManyBeans

      if (insertedParent) {
        // after insert set the modify listening mode for private owned etc
        c.setModifyListening(many.modifyListenMode());
      }

We found this in one of our unit-tests, where we've configured a bean for different states. It is probably something, that should not be too critical in real code, as you normally save a bean only once (When the bean was retrieved from DB, it should not occur)

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-24 17:05:11 +12:00
Rob Bygrave ca19a78c23 Two related tidy-ups to query-cache dependent-table handling. (#3792)
This is a replacement for #3150

 **1. Read `dependentTables` from the query plan (single source of truth)**

 `OrmQueryRequest` accumulated query-cache `dependentTables` into a field via
 `addDependentTables(...)`, fed from per-CQuery `dependentTables()` helpers. The
 `CQueryPlan` already owns this set, so the copying is redundant.

 - `putToQueryCache(...)` now reads `dependentTables` directly from the request's
   `CQueryPlan` at cache-put time.
 - Removed the `OrmQueryRequest.dependentTables` field and `addDependentTables(Set)`.
 - Removed the now-unused `dependentTables()` helpers from `CQuery`,
   `CQueryRowCount` and `CQueryFetchSingleAttribute` (and their `Set` imports).

 This is behaviour-preserving: each CQuery is built with exactly the plan stored
 under `request.queryPlanKey`, so `request.queryPlan().dependentTables()` is the
 same set the per-CQuery helpers returned.

 **2. Fix a query-plan trim race that could null the plan at put time**

 A freshly built `CQueryPlan` was stored in the plan cache with
 `lastQueryTime == 0`, making it immediately eligible for `trimQueryPlans`
 (runs every 60s; TTL default 300s) during its *first* execution. If the trim
 fired mid-execution, `request.queryPlan()` could return `null` at put time — and
 a cache entry with `null` dependentTables is never invalidated by table
 modifications (`TableModState.isValid`), i.e. latent stale data.

 - `CQueryPlanStats` now initialises `lastQueryTime` to construction time, so a
   new plan is only trim-eligible after a genuine TTL idle. `trimQueryPlans` is
   the sole consumer of `lastQueryTime()`, so this is safe.
 - `putToQueryCache(...)` skips the put when the plan is `null` (fail safe rather
   than caching an un-invalidatable entry) for the residual pathological case
   (a single query running past the TTL on first execution).

 ### Tests

 - Added `testFindSingleAttributeOnDependent` and `testFindListOnDependent` to
   `TestQueryCacheTableDependency`, asserting cache-hit then dependent-table
   invalidation for the single-attribute and findMany paths (the count path was
   already covered).
 - All cache/query tests pass: 69 in `ebean-test` `org.tests.cache.**`, 929 in
   `ebean-core` cache + query packages.
2026-06-23 22:44:28 +12:00
Rob Bygraveandrobin.bygrave 66d599faa2 Add support for Java 21 SequencedSet and SequencedMap (#3302)
* Add support for Java 21 SequencedSet and SequencedMap

Such that these can be used in place of Set and Map if desired.

* Refactor BeanSet, BeanList, BeanMap replacing setActualSet|List|Map

Replace with collectionAdd() and refresh() methods.

* Refactor rename method getBeanCollectionAdd() -> collectionAdd()

* Build needs to use Java 21 to support the multi-release jar

* Update SequencedSet etc from recent changes

* Update build, needs package to use MR-JAR

---------

Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-06-23 21:43:19 +12:00
Rob Bygrave e3dad5bd44 Merge pull request #3791 from ebean-orm/feature/formula2
Add support for @Formula2 with logical paths and automatic joins
2026-06-23 20:14:57 +12:00
robin.bygrave f50d56faee Add support for @Formula2 with logical paths and automatic joins
@Formula2 is a logical-path alternative to @Formula and a full replacement for it.

 Where @Formula requires physical SQL (${ta} placeholders and hand-written joins),
 @Formula2 takes a property-path expression and resolves the required joins
 automatically:

     @Formula2("coalesce(familyName, parent.familyName)")
     String derivedFamilyName;

 The path prefixes (parent, parent.parent, ...) define the joins needed to
 satisfy the formula. Supported everywhere @Formula is:

   - select()      — root and nested-fetch paths, auto-joined
   - where()       — predicate paths trigger the extra joins
   - orderBy()     — order-by paths trigger the extra joins
   - having()
   - DDL           — excluded from generated columns (read-only, like @Formula)

 Default behaviour matches @Formula: included in the default select unless marked
 @Transient, in which case it is opt-in but still auto-joins when selected.

 Implementation:
   - Parse @Formula2 after associations are wired, into a logical select fragment
     plus the set of join paths (BeanDescriptor/BeanProperty).
   - DeployPropertyParser registers formula2 join paths into query includes so the
     existing predicate-include -> extra-join machinery builds the LEFT JOINs.
   - ElPropertyChain prefixes all ${}/${path} placeholders for nested-path use.
   - SqlTreeBuilder accumulates formula2 joins for select paths.
2026-06-23 19:02:37 +12:00
Rob Bygrave a7fddf1981 Version 18.0.0 2026-06-22 21:11:45 +12:00
Rob Bygrave f610d03a1d Dependency: Bump ebean-agent to 18.0.0 2026-06-22 21:09:10 +12:00
Rob Bygrave 852f199ffb Merge pull request #3789 from ebean-orm/feature/avaje-json-core
Refactor from Jackson Core to avaje-json-core
2026-06-22 21:02:52 +12:00
Rob Bygrave 0e6cd9db8c Version 16.11.1 2026-06-22 20:33:00 +12:00
Rob Bygrave 484ed5d859 Dependency: Bump ebean-test-containers to 8.2 2026-06-22 19:17:21 +12:00
robin.bygrave 56d33b1f1b Update documentation for the DbJson mapping support 2026-06-22 10:06:34 +12:00
robin.bygrave e9b00cbbd1 Add documentation for the DbJson mapping support 2026-06-19 22:10:54 +12:00
robin.bygrave 230d7a36a7 Simplify @DbJson Map dispatch and support Map<Enum,String>
Replace isMapValueTypeObject + isMapStringString with isBuiltinJsonMap:
 the built-in JSON map handles a String or enum key with a String, Object
 or wildcard value. This also routes Map<Enum,String> to the built-in type
 and fixes non-String/enum keys with Object value (e.g. Map<Integer,Object>)
 being mis-routed to the built-in type (ClassCastException) instead of the
 object mapper.
2026-06-19 21:47:20 +12:00
robin.bygrave 10bf5dbbbd Support Map<Enum,Object> in @DbJson(B) fields
Addresses #3735. Enum-keyed JSON maps previously failed (enum key cast to
 String). Add ScalarTypeJsonMapEnum which converts enum keys via their
 ScalarType (honouring @DbEnumValue) and reuses ScalarTypeJsonMap for all
 storage/platform handling, so it works for VARCHAR/CLOB/BLOB and Postgres
 JSON/JSONB with no per-storage variants.

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

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

Changes

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

Why

Simplifies Ebean's JSON handling by reusing avaje-json-core's JsonMapper rather than maintaining a parallel reader/writer, while keeping behavior identical.
2026-06-19 16:19:35 +12:00
Rob Bygrave 1545e68c3e Merge pull request #3790 from ebean-orm/feature/regression-inline-query-comment
Regression introduced by #3779 in sql inline comment for label (missing bean type prefix)
2026-06-18 22:28:57 +12:00
robin.bygrave 4fd32b45d2 Regression introduced by #3779 in sql inline comment for label (missing bean type prefix)
So when we used to get an inline comment like:
```sql
select /* Customer.hiLabel */ ...
```
We started to instead have (missing bean type):
```sql
select /* hiLabel */ ...
```
This fixes that regression that was introduced in #3779
2026-06-18 22:23:22 +12:00
robin.bygrave 313fdda857 Refactor from Jackson Core to avaje-json-core 2026-06-18 22:00:33 +12:00
Rob Bygrave d42f72c0a2 Merge pull request #3788 from mvanhorn/fix/3641-refresh-soft-deleted
Fix refresh on soft-deleted beans
2026-06-16 13:53:31 +12:00
mvanhorn 6d53e89a80 Fix refresh on soft-deleted beans 2026-06-14 02:25:47 -07:00
Rob Bygrave 7c5ee5b555 Version 16.11.0 2026-06-14 21:06:57 +12:00
Rob Bygrave a2337a096e Merge pull request #3787 from ebean-orm/feature/dep-bump-datasource
Dependency: Bump ebean-datasource to 10.9
2026-06-14 21:03:50 +12:00
Rob Bygrave 7bf5fc2798 Dependency: Bump ebean-datasource to 10.9 2026-06-14 21:02:46 +12:00
Rob Bygrave 987798add9 Merge branch 'master' of github.com:ebean-orm/ebean 2026-06-14 21:00:36 +12:00
robin.bygrave 6295faa351 Version 16.11.0-RC1 2026-06-13 10:46:40 +12:00
Rob Bygrave d9252a9c85 Merge pull request #3782 from leehaut/hotfix/lance-common-1
Add Redis Sentinel support and local integration tests for ebean-redis
2026-06-13 10:42:41 +12:00
Rob Bygrave ddd864852d Merge pull request #3783 from ebean-orm/feature/init-migrations-do-not-overwrite
db-migration: Change I__ init scripts to NOT overwrite (allow customisation)
2026-06-13 10:33:38 +12:00
robin.bygrave 7966d8bc1b docs: Improve around findList().stream() vs findStream() use 2026-06-13 10:23:17 +12:00
Rob Bygrave 66e6e83e60 Merge pull request #3785 from ebean-orm/feature/metrics-as-json-v2
Add MetricsAsJson.writeV2() using name + tags format
2026-06-12 21:00:43 +12:00
robin.bygrave 0e7f68e75a Add MetricsAsJson.writeV2() using name + tags format 2026-06-12 20:59:28 +12:00
robin.bygrave 7b8e1713dd Test: Fix for EA build to skip the TestMockitoMock 2026-06-11 21:31:22 +12:00
robin.bygrave 3ea9a5fa7f Test: Fix DtoQueryPlanCaptureTest by draining plans 2026-06-11 20:28:10 +12:00
robin.bygrave 4fab5dfe4b Test: support EA build via net.bytebuddy.experimental true 2026-06-11 19:59:12 +12:00
robin.bygrave 9fac046f39 Dep: bump ebean-datasource to 10.7 2026-06-11 18:57:34 +12:00
robin.bygrave 50bfd04987 Tests: Oracle CI, limit connections and prepared statements for oracle CI test 2026-06-11 16:56:04 +12:00
robin.bygrave d71ced32e8 Tests: Oracle CI, limit connections and prepared statements for oracle CI test 2026-06-11 16:26:29 +12:00
robin.bygrave 6722cf50e9 Tests: Fix DbMigrationTest for waits and SqlQueryPlanCaptureTest for draining 2026-06-11 14:53:55 +12:00
robin.bygrave 13cebbcacb Tests: Fix ResourceEntityTest for test pollution 2026-06-11 14:19:49 +12:00
robin.bygrave e70777bdb6 Tests: Adjust SqlQueryPlanCaptureTest (for clash on sql) 2026-06-11 13:30:46 +12:00
Rob Bygrave 86cfd185f3 Merge pull request #3784 from ebean-orm/feature/bump-jackson-core
dep: bump jackson-core to 2.22.0
2026-06-11 13:03:54 +12:00
robin.bygrave 3970847443 Tests: Run CI builds with -T C1 2026-06-11 13:02:32 +12:00
robin.bygrave b440c5ea27 Tests: Run CI build and CI h2database with -T C1 2026-06-11 12:53:40 +12:00
robin.bygrave 1836ff18a5 dep: bump jackson-core to 2.22.0 2026-06-11 09:17:31 +12:00
robin.bygrave efdee053cc db-migration: Change I__ init scripts to NOT overwrite (allow customisation)
This allows for customisation of the built in I__ init scripts.
For the case of postgres db partitions a customisation to support
unlogged tables was for example always being overwritten.

This change makes the I__ scripts a "add if not already exists".
Note that R__ repeatable scripts are "always overwrite"
2026-06-10 17:02:58 +12:00
robin.bygrave a1ee75ffec docs: update docs / guides for query bean optional predicates 2026-06-10 16:59:24 +12:00
robin.bygrave da3dd8b215 docs: findStream() preferred over findList().stream() 2026-06-10 16:31:36 +12:00
robin.bygrave 6d30e6ff82 docs: Improve docs / guides / writing query beans, section on Optional predicates 2026-06-10 16:29:17 +12:00
lance 453a320210 Add Redis Sentinel support and local integration tests for ebean-redis
Signed-off-by: lance <leehaut@gmail.com>
2026-06-09 22:17:25 +08:00
Rob Bygrave 61cc5e3459 Version 16.10.0 2026-06-09 07:57:32 +12:00
Rob Bygrave 23f23fa32b Bump ebean-agent to 16.10.0 2026-06-09 07:53:36 +12:00
Rob Bygrave f65c409bfe Merge pull request #3781 from ebean-orm/feature/sqlquery-plan-capture
SqlQuery - add support for query plan capture
2026-06-09 00:39:52 +12:00
robin.bygrave ec49824430 SqlQuery - update docs wrt plan capture 2026-06-09 00:28:13 +12:00
robin.bygrave 819aaece4d SqlQuery - add support for query plan capture 2026-06-09 00:20:53 +12:00
Rob Bygrave c275953582 Merge pull request #3780 from ebean-orm/feature/dto-plan-capture
DtoQuery updated to support query plan capture
2026-06-08 23:47:40 +12:00
robin.bygrave 82e8494f42 Add profile location test 2026-06-08 23:47:21 +12:00
robin.bygrave abacda2c8f Update docs 2026-06-08 23:34:19 +12:00
robin.bygrave a7d1253dba DtoQuery skip bind capture when it's actually an orm query 2026-06-08 23:31:52 +12:00
robin.bygrave a081d08621 DtoQuery initiate the bind capture 2026-06-08 23:24:13 +12:00
robin.bygrave 8c37b53bad DtoQuery modified to support query plan capture 2026-06-08 23:06:09 +12:00
Rob Bygrave 3f6d565800 Version 16.9.0 2026-06-08 19:09:46 +12:00
Rob Bygrave 1408696912 Redesign of metric labels - secondary queries (lazy|query) now just u… (#3779)
* Redesign of metric labels - secondary queries (lazy|query) now just use parent + relativePath + type

# Query metric/plan label change — comparison

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

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

## Root query name

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

## Secondary lazy (`contacts`) name

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

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

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

## Nested and `.query` secondary loads

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

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

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

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

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

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

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

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

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

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

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

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

* Deprecate DatabaseConfig and DatabaseFactory, migrate test code

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

* Tidy up Database javadoc for deprecation
2026-05-08 00:06:43 +12:00
Rob Bygrave fff345ffc8 Merge branch 'master' of github.com:ebean-orm/ebean 2026-05-07 22:13:16 +12:00
Rob Bygrave dd1fe845aa Remove the DefaultProfileHandler & DefaultProfileStream (Replaced by ebean-opentelemetry) (#3767)
So migrate to use opentelemetry, and remove these internal profilers as
they are never expected to be used now.
2026-05-07 22:10:08 +12:00
Rob Bygrave 80451c3c62 Performance: Improve OrmQueryProperties, include } into immutableHashSuffix (#3768) 2026-05-07 21:42:20 +12:00
Rob Bygrave 2d199737aa Docs: Add docs/notes on potential future L2 immutable bean cache 2026-05-07 21:36:45 +12:00
Rob Bygrave 63a1e9907d Version 16.6.0 2026-05-06 23:06:25 +12:00
Rob Bygrave 67db089646 Bump ebean-agent version, no effective change 2026-05-06 22:56:49 +12:00
dependabot[bot] 26478924a7 Build(deps): Bump org.postgresql:postgresql in /ebean-pgvector-types (#3766)
Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.2 to 42.7.11.
- [Release notes](https://github.com/pgjdbc/pgjdbc/releases)
- [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.2...REL42.7.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 22:50:12 +12:00
dependabot[bot] 3cb22fb0cc Build(deps): Bump org.postgresql:postgresql in /ebean-postgis-types (#3765)
Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.2 to 42.7.11.
- [Release notes](https://github.com/pgjdbc/pgjdbc/releases)
- [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.2...REL42.7.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-06 22:49:42 +12:00
Rob Bygrave 4dde7064ba Add ImmutableBeanCache - some parts of the query result graph populated via ImmutableBeanCache (#3753)
* Change [implicit] Lazy loading to be by TYPE rather than by PATH

We have some graph models where a common type (in the tests it is Label)
is used in many different *paths* of the graph. When we are loading via
*path* then all the loading of the Labels isn't in a single batch / load context
but instead split into different load contexts PER PATH.

This change, means that lazy loading operates by TYPE instead of by PATH.

In the tests, all the Labels are read via a single lazy loading query rather
that one lazy loading query per PATH, and this is more efficient.

* ImmutableBeanCache part 1

* ImmutableBeanCache part 2

* ImmutableBeanCache part 3 - add ImmutableBeanCaches

* ImmutableBeanCache - add propagation of caches to secondary queries

* ImmutableBeanCache - use and improve BeanDescriptor.merge() to positional

Using positional avoids the lookup by property name.

* ImmutableBeanCache - add unit testing with BeanDescriptorMergeTest

* ImmutableBeanCache - add testing for the further lazy loading on mutable beans

Also add doc / guides

* Add ImmutableCacheBuilder with underlying DefaultServerCache implementation

This provides a cache with maxSize, maxIdleSeconds, maxSecondsToLive

* Improve javadoc and doc / guides for ImmutableBeanCache

* Add cache invalidation for ImmutableBeanCache

* Use immutable cache direct hits for unmodifiable assoc-one refs

 - add ImmutableBeanCache.getIfPresent() as a non-loading probe
 - use immutable cache hits in AssocOneHelp / AssocOneHelpRefInherit
 - avoid creating ref beans and merge/copy for unmodifiable cache hits
 - keep existing mutable-query and miss/backfill behavior unchanged
2026-05-06 22:35:30 +12:00
Rob BygraveandRob Bygrave 681221c0ff Add ebean-opentelemetry module (#3752)
* Enhance ProfileStream to support Open Telemetry

* Add ebean-opentelemetry module

* Add the sql query text as span attribute.

Note: This is a breaking API change here but I'm pretty confident
that no one is using the ProfileStream API - hmmm.

* Otel: Use the query label and transaction label in span name if available

* SpiProfileHandler: Change such that if service loaded, doesn't need ProfileConfig

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2026-05-06 21:49:31 +12:00
Rob Bygrave d6ce093ac5 Bench: Add general perf benchmark to FetchGroupSelectApplyBenchmark 2026-05-05 23:03:34 +12:00
Rob Bygrave 515b8e5958 Performance improvement in OrmQueryDetail with copyInto() (#3763)
Relatively minor improvement from reusing existing OrmQueryDetail
2026-05-05 22:54:58 +12:00
Rob Bygrave 86b4af1321 Performance improvement in OrmQueryProperties with immutable hash prefix and suffix (#3762) 2026-05-05 22:42:09 +12:00
Rob Bygrave eb73318f53 Performance improvement in OrmQueryProperties when appending Set<String> 2026-05-05 21:55:26 +12:00
Rob Bygrave 96a3ab9d79 Performance improvement in OrmQueryProperties for FetchGroup reuse (#3761) 2026-05-05 21:35:35 +12:00
Rob Bygrave 9954ad1ea7 Docs: Update add ebean postgres guide with gate on DI question 2026-05-04 23:50:19 +12:00
robin.bygrave 2928247ba7 Docs: Add the mkdir 2026-05-01 16:45:43 +12:00
robin.bygrave 3066bb4f74 Docs: Setup add a Step 4b - add di dependencies bit 2026-05-01 16:07:58 +12:00
robin.bygrave a96144cdaa Docs: Adjust setup guide with initial DI / no DI question 2026-05-01 16:03:11 +12:00
robin.bygrave 50e20c3aa0 Docs: update docs / skills location to separate git repo 2026-05-01 15:38:37 +12:00
robin.bygrave f07ca68eb3 Docs: Add docs/skill/ebean-orm 2026-05-01 15:09:54 +12:00
robin.bygrave 0ec7ddb82d Docs: Change the ordering so that test container setup proceeds the main database configuration 2026-05-01 14:59:36 +12:00
robin.bygrave 8074a24d03 Docs: Improve docs with DB -> database and wording around getters/setters/accessors 2026-05-01 14:52:43 +12:00
Rob Bygrave 7e4a2db9aa Add Query alsoIfPresent() as helper for Nullable conditional expressions (#3756)
Add this as a helper for when using alsoIf(BooleanPredicate) seems overkill
and there isn't a built in IfPresent option for the desired expression and
wanting to keep the fluid style.
2026-04-28 23:24:50 +12:00
Rob Bygrave 292dcf4b0e Add IfPresent options for like, ilike, startsWith, istartsWith, contains, icontains (#3755) 2026-04-28 23:06:57 +12:00
robin.bygrave f44bf59cb6 Docs: Improve testing-with-testentitybuilder.md - better wiring example 2026-04-24 17:52:44 +12:00
robin.bygrave 7c3741e258 Docs: Add llms.txt to assist agents
Note that this is effectively a duplication of the website llms.txt
2026-04-24 10:18:00 +12:00
robin.bygrave afadaa7208 Docs: Add a template AGENTS.md to assist agent onboarding 2026-04-24 09:32:37 +12:00
Rob Bygraveandrobin.bygrave 46b7510e6c TestEntityBuilder - add saveAll() helper method (#3751)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-04-22 15:10:04 +12:00
Rob Bygrave bcb0b049ff Tests for Oracle, drop pstmtCache to 100
The default was bumped to 300. Seeing ORA-01000: maximum open cursors exceeded
in the Oracle integration test so thinking it is due to the increase in
default preparedStatement cache.
2026-04-22 00:15:25 +12:00
Rob Bygrave 42bfaaf2bf Tests for Oracle, drop pstmtCache to 100
The default was bumped to 300. Seeing ORA-01000: maximum open cursors exceeded
in the Oracle integration test so thinking it is due to the increase in
default preparedStatement cache.
2026-04-22 00:14:20 +12:00
Rob Bygrave 2ca39f1419 Fix test TestErrorBindLog for DB2 2026-04-22 00:03:22 +12:00
Rob Bygrave de2914e550 Fix test TestQueryJoinOnFormula for Postgres distinct on 2026-04-22 00:00:53 +12:00
robin.bygrave 6eda7044a0 Docs: Improve testing-with-testentitybuilder.md 2026-04-21 13:01:18 +12:00
robin.bygrave e4adf25022 Docs: Improve testing-with-testentitybuilder.md 2026-04-20 15:49:44 +12:00
robin.bygrave ad5483100e Improve README.md with reference to llms.txt 2026-04-20 14:55:01 +12:00
robin.bygrave 6d9ed670ef Docs: Improve doc guides wording 2026-04-16 16:48:27 +12:00
robin.bygrave 9e2d821475 Docs: Add LIBRARY.md 2026-04-16 13:53:22 +12:00
Rob Bygrave 80ce7a41a2 Docs: guides - improve writing query beans guide 2026-04-14 00:46:37 +12:00
Rob Bygrave 6458bb0f1c Docs: Add docs / guides for writing queries and using transactions 2026-04-14 00:34:21 +12:00
Rob Bygrave 7ba550cc0d Docs: Add docs / guides for ebean entity bean recommendations 2026-04-12 23:49:41 +12:00
Rob Bygrave fff8dfb8b1 Docs: Add docs / guides for ebean entity bean recommendations 2026-04-12 23:43:54 +12:00
Rob Bygrave d0af7b1788 Docs: Add docs / guides for testing with TestEntityBuilder 2026-04-12 22:51:51 +12:00
Rob Bygrave ebb0305cf3 Version 16.5.0 2026-04-12 22:31:24 +12:00
Rob Bygrave d03d36bba4 Update ebean-agent 2026-04-12 22:27:52 +12:00
Rob Bygrave 88408edd24 Bump ebean-datasource dependency to 10.5 with increase default prepared statement cache (#3749)
Default prepared statement cache increased from 100 to 300. This could have the
effect of increased memory consumption for applications traded off with potentially
improved performance due to me cached prepared statements for larger applications.

Note that the Postgres itself has a default of 250.
2026-04-12 22:27:02 +12:00
Rob Bygrave 0aad35b840 Merge branch 'master' of github.com:ebean-orm/ebean 2026-04-12 17:21:04 +12:00
Rob Bygrave e7979b285d Docs: docs / guides - add reference link to ebean-datasource guides 2026-04-12 17:20:45 +12:00
Roland Praml 91920b00dc M2M on child path produce wrong query on extra join (#3653)
* Testcase for ManyToMany with extra join

* Fix query generation for extra ManyToMany joins

* Fix other tests due different query
2026-04-12 15:07:08 +12:00
Rob Bygrave bdfe016d2d Refactor DLoadBeanContext extract helper method ensureBatchInContext() 2026-04-12 15:02:07 +12:00
Rob BygraveandRoland Praml b2670f9cc0 Lazy load pre delete 2 (#3748)
* Lazy load in pre-delete does not work

* Re-add deleted beans before lazy-load will happen

* Put the contextClear() calls into a finally

This should mean that any beans put into the context would be
cleared even if there was an issue with the loadBean() call.

---------

Co-authored-by: Roland Praml <roland.praml@foconis.de>
2026-04-12 15:00:28 +12:00
dsa-ferreiraandRob Bygrave ebc90e0e82 FEATURE: Add support for Generic mapped superclass (#3692)
* creates support for generics in mapped superclasses

* Changes DeployCreateProperties to allow processing of inheritance hierarchies with generics

* adds tests for ebean-querybean

* bumps querybean-generator version

* fixes tests by changing table names for ProductWithGenericLong and ProductWithGenericString

* Restore format, this reduces the diff

* Restore format, this reduces the diff

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2026-04-12 13:31:49 +12:00
Rob Bygrave c4230c780f Add usingMaster() to SqlQuery.TypeQuery (#3744) 2026-04-12 12:49:39 +12:00
Rob Bygrave 794f933310 Feat: Add test entity builder, ease creation of test entity instances populated with random values (#3747)
* Docs: modify guides README with links to the available guides

* Add TestEntityBuilder for building test entity instances populated by random values

* Improve TestEntityBuilder for emails, BigDecimal precision/scale, protected method allow overriding

* Improve TestEntityBuilder for emails, use PersonOther
2026-04-12 12:46:49 +12:00
AntoineDuComptoirDesPharmacies c8a7a263a9 #3129 (#3746)
This Pull Request aim to fix the problem of DBJSONB dirty detection listed in #3129 which was due to PostgreSQL JSONB key reordering while storing value.
This cause Ebean to mark @DbJsonB properties as dirty on every load (triggering unnecessary UPDATEs and version increments) because the raw DB JSON key order differed from Jackson's serialization order.
Currently, Ebean is using CRC32 Checksum to compare but it is field ordering-dependent.

Introduce JsonContentHash:
A streaming order-independent structural hash of JSON content using Jackson's JsonParser.
Object keys are combined with commutative addition (a + b == b + a) so key ordering does not affect the hash, while array elements use positional hashing to preserve semantic ordering.
The hash uses FNV-1a for strings and MurmurHash3's fmix64 finalizer (both public domain) for mixing, producing a 64-bit hash with strong avalanche properties.

Changes:
- Add JsonContentHash utility (streaming, zero allocation, O(n) time)
- SourceMutableValue: use fast string equality with canonical hash fallback
- ChecksumMutableValue: replace CRC32 with JsonContentHash (also upgrades collision resistance from 2^32 to 2^64)
- No API changes, no schema changes, readSet() untouched
2026-04-12 12:46:13 +12:00
robin.bygrave aeef6d0ea2 Docs: modify guides README with snippets devs should copy n paste into their README etc to help guide the AI agents to the appropriate ebean guides 2026-04-11 01:25:23 +12:00
robin.bygrave a99aef8b00 Docs: modify guides section, add initialConnections with explanation 2026-04-11 01:09:30 +12:00
robin.bygrave 6f17cc6327 Docs: Add docs / guides for adding db migration generation 2026-04-11 01:00:08 +12:00
robin.bygrave 903947b3cb Docs: Add docs / guides for lombok use 2026-04-11 00:24:27 +12:00
robin.bygrave 797f75f7b7 Docs: Add docs / guides for postgres test container setup 2026-04-11 00:07:45 +12:00
Rob Bygrave af443ef2f2 Merge pull request #3745 from ebean-orm/docs/guides-one
Docs: Add docs / guides for step-by-step instructions for AI agents
2026-04-10 23:42:33 +12:00
robin.bygrave ad4f027835 Docs: Add docs / guides for step-by-step instructions for AI agents 2026-04-10 23:42:05 +12:00
Rob Bygrave 367eba8685 Version 16.4.0 2026-04-10 08:03:12 +12:00
Rob Bygrave be542635a5 Bump ebean-test-containers to 8.0 (major bump to mark the port fix) (#3742)
Docker changed and that broke how ebean-test-containers detected the
currently assigned port for a container. That was fixed in 7.18 but
thinking its a good idea to mark that relatively important bug fix
with a bump of the major version to 8.0.

Everyone using ebean-test-containers should consider updating the
ebean-test-containers dependency to 8.0 or 7.18 (both have the fix).
2026-04-10 08:00:04 +12:00
Rob Bygrave 6e9d0a91e0 Fix for @Column on timestamp defined as timestamp(255) (#3740)
As per https://github.com/ebean-orm/ebean/discussions/3720

```
  @Column
  ZonedDateTime zonedDateTime2;
```
... resulting in DDL generated as `timestamp(255)`. This is
occurring when the JPA dependency is used like:
jakarta.persistence:jakarta.persistence-api:3.2.0
rather than using the transitive dependency that ebean includes.

Workaround:
Remove the jakarta.persistence:jakarta.persistence-api:3.2.0 dependency.

Fix:
The fix here is to use a timestamp type that has a maximum precision.
When a precision/length is specified greater than the maximum precision
then the fallback type is used which is `timestamp` without any precision.
2026-04-10 00:46:34 +12:00
Rob Bygrave a99ef3ebf2 Bump ebean-datasource dependency to 10.4, resets metrics on initialisation (#3739) 2026-04-09 22:23:55 +12:00
Andrey GlushkovandRob Bygrave 19afa845d1 FEATURE: inTuples() expressions support for natural key cache lookup (#3732)
* `inTuples()` support for natural key cache lookup

* Simplify NaturalKeyEntryBasic.addInPairs()

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2026-04-09 19:46:33 +12:00
Rob Bygrave 239900cd3b Bump ebean-agent to 16.4.0 with support for Java 26 (#3738) 2026-04-09 19:15:36 +12:00
robin.bygrave 73ac39971c Update test TestErrorBindLog for DB2 2026-04-08 22:55:41 +12:00
robin.bygrave 3a876252ec Update test TestQueryJoinOnFormula, restrict platforms for specific test
Restrict the new test_findCount_formulaJoin_subqueryWithOrderBy_issue3686
to H2 and Postgres for now. Not supported on Oracle and SQL Server.
2026-04-08 22:33:06 +12:00
robin.bygrave 231b52ba88 Update test TestErrorBindLog to improve failure message 2026-04-08 22:27:46 +12:00
robin.bygrave 808019cf3d Modify tests, move setRegister(false) setDefaultServer(false) before loadFromProperties() 2026-04-08 22:15:11 +12:00
Rob Bygrave 364520455f Add some logging for test BeanPersistControllerTest 2026-04-08 08:58:38 +12:00
Rob Bygrave e92621a489 Try old ebean agent, for strange CI build issue (#3737) 2026-04-08 08:51:33 +12:00
Rob Bygrave ccd1b7b8ec Bump ebean-test-containers to 7.18 (#3736) 2026-04-08 08:31:02 +12:00
thomas-lcdp d144273307 ebean-core#3686: use parenthesis-aware ORDER BY removal in buildRowCountQuery to avoid breaking nested subqueries (#3729) 2026-03-27 23:18:15 +13:00
Rob Bygrave 41c5ebdcd7 Bump ebean-agent with ASM 9.9.1 and Java 26 support (#3731) 2026-03-27 23:15:00 +13:00
Rob Bygrave 9e711efecb Merge pull request #3730 from ebean-orm/feature/add-docs-for-graalvm-support
Add GraalVM native image support documentation
2026-03-27 22:06:00 +13:00
robin.bygrave ef7fd76f14 Add GraalVM native image support documentation 2026-03-27 22:05:21 +13:00
robin.bygrave b7e3ddbedd Add GraalVM native image badge to README.md 2026-03-27 21:55:29 +13:00
Rob Bygrave 69c932816a Version 16.3.0 2026-02-16 21:07:49 +13:00
Rob Bygrave a1facf527b Bump ebean-agent and avaje junit 2026-02-16 21:03:37 +13:00
Rob Bygrave ec42ebf66d Bump ebean-datasource to 10.3 (#3727)
This version has moved the connection initialisation that sets the
autoCommit mode to be after the clientInfo and any initial sql has
been executed
2026-02-16 20:24:58 +13:00
Rob Bygrave 5dede70801 Fix for #3722 Remove isSkipCacheExplicit() and use isSkipCache()
This change is to reduce PUTs causing cache pollution where
changes that are rolled back are PUT into the bean cache.

Effectively remove the isSkipCacheExplicit() feature and just
use the existing isSkipCache(). This means that skipCacheAfterWrite
is used and PUTs after a database write/insert/update/delete are
effectively skipped.
2026-02-16 19:49:56 +13:00
Rob Bygrave b2f02ecc1f Fix for #3723 FilterMany predicates added to incorrect join clause (#3725)
The change that moved the filterMany predicates to the join had an
issue that extra joins may be needed to support those predicates.

Prior to this change, the extra predicates where effectively just
added to the end of the joins, rather than as a "child" node in
the SqlTreeNode tree.

This fix is in SqlTreeBuilder, where the list of top level extra
joins are first tried to be added as a child to a parent node, and
only if we can't find the parent added at the end.
2026-02-15 21:47:17 +13:00
dependabot[bot] 15fa7cd1c2 Bump org.assertj:assertj-core from 3.27.6 to 3.27.7 in /ebean-test (#3718)
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.6 to 3.27.7.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.6...assertj-build-3.27.7)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 10:02:49 +13:00
Rob Bygrave 8dced38cd5 Bump kotlin maven plugin version in build (#3724)
* Bump kotlin maven plugin version in build

* For build bump kotlin-maven-plugin version to 2.3.10
2026-02-15 09:59:16 +13:00
Rob Bygrave 7addba7c93 Version 16.2.1 2026-02-02 23:22:22 +13:00
Rob Bygrave 00b45c1642 For query usingMaster support passing a boolean (#3721)
This is to support the use case where other logic is used to determine if a query
should be "forced" to use the master data source or not, and so it's easier to
pass that as a boolean to the usingMaster() method.
2026-02-02 23:17:37 +13:00
Rob Bygrave a3d9711fe6 Version 16.2.0 2025-11-23 22:18:54 +13:00
Rob Bygrave 1239c50723 Bump ebean-agent to 16.2.0 2025-11-23 22:16:29 +13:00
Rob Bygrave d85d8449e2 #3695 Behaviour change: findSingleAttribute() to throw when multiple … (#3697)
* #3695 Behaviour change: findSingleAttribute() to throw when multiple rows returned by query

On the plus, this makes the behaviour consistent with all the other findOne() methods (ORM, DTO, SqlQuery) and this very much feels like buggy behaviour [to just get the first result and ignore any subsequent results in the ResultSet]

On the negative of merging this bug fix, any application code relying on the existing behaviour with this fix will break and throw a NonUniqueResultException at RUNTIME (not ideal). However, for these cases where the application now throws an exception, people might not be aware that they were relying on this behaviour and this exception could be useful to highlight that (a potential non-deterministic query result was being used and a potential source of bugs was being).

* Change SqlQuery + Mapper + findOne() also to throw NonUniqueResultException
2025-11-23 22:14:44 +13:00
dependabot[bot] baf71cbab3 Bump com.microsoft.sqlserver:mssql-jdbc in /ebean-test (#3694)
Bumps [com.microsoft.sqlserver:mssql-jdbc](https://github.com/Microsoft/mssql-jdbc) from 10.2.0.jre8 to 11.1.0.jre8-preview.
- [Release notes](https://github.com/Microsoft/mssql-jdbc/releases)
- [Changelog](https://github.com/microsoft/mssql-jdbc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Microsoft/mssql-jdbc/commits)

---
updated-dependencies:
- dependency-name: com.microsoft.sqlserver:mssql-jdbc
  dependency-version: 11.1.0.jre8-preview
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-23 21:01:00 +13:00
Rob Bygrave ff0ca35e54 #3701 Improve error message when missing dependency ebean-jackson-mapper (#3708) 2025-11-23 20:25:16 +13:00
Rob BygraveandRoman Parshikov 0fc9a1ac2f Bug fix and change filterMany to put predicates into JOIN clause, Fix for #3626 (#3707)
* Problem with filterMany

* Missing id

* Other test

* Fix

* Update TestQueryFilterMany.java

* #3706 Bug fix and change filterMany to put predicates into JOIN clause, Fix for #3626

* Fix tests for Postgres, Oracle, Sql Server

---------

Co-authored-by: Roman Parshikov <promansew@gmail.com>
2025-11-23 20:20:13 +13:00
Rob Bygrave f7b4edb193 Merge pull request #3704 from ebean-orm/feature/bump-test-deps-bytebuddy
Bump test dependencies byte buddy and assertj
2025-11-20 09:35:09 +13:00
robin.bygrave 284c8d4d21 Bump test dependencies byte buddy and assertj 2025-11-20 09:23:39 +13:00
Rob Bygrave 59a5c5f9bd Bump dependency avaje-config to 4.2 (#3699) 2025-11-11 07:59:40 +13:00
Rob Bygrave 5b04d6eca3 #3551 Log warning for use of mapping column to Class (#3698)
I think it was a mistake for Ebean to support Class<?> from a security perspective. Instead, Ebean should just use a String <-> Varchar and leave if up to the application to take that String and convert it to a class [and then that potential Class initialisation is owned by the application code and all security considerations around that are owned by the application code].
2025-11-11 07:57:15 +13:00
Ryszard TrojnackiandRob Bygrave d5547808a2 PGvector support (#3696)
* Add pgvector-types module.

* Add missing binder definitions.
Add missing PGbit type registration.
Few tests.

* Add cached bean test.

* Prefer final classes, minor formating only

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2025-11-09 16:55:51 +13:00
Rob Bygrave 5606c95de5 Version 16.1.1 2025-10-22 22:45:21 +13:00
Rob Bygrave 4c2bd408b0 Bump ebean-agent 2025-10-22 22:42:00 +13:00
Rob Bygrave db76d5c27c Test only - add Postgres JSONB raw() expression example 2025-10-22 22:03:09 +13:00
Rob Bygrave f92c2f8c14 Test only - add test for string id InsertOnConflict 2025-10-22 22:02:40 +13:00
Rob Bygrave 51c3065419 3683 - Add AggregateFormulaContext to support group_concat etc (#3691)
- Add AggregateFormulaContext with ability to override the default via DatabaseBuilder
- Add group_concat, string_agg, listagg to set of known aggregation functions
2025-10-21 22:54:55 +13:00
Rob Bygrave e46dd66292 Test only - bump to Postgres 17 for PostgresReadOnlyDatabaseTest 2025-10-21 20:55:52 +13:00
Rob Bygrave 3e1451bee7 Merge branch 'master' of github.com:ebean-orm/ebean 2025-10-21 20:54:23 +13:00
Rob Bygrave 4ac3c4ddb1 3682 @Aggregation is applied to query if entity also has a @lob column (#3689)
* Version 16.1.0

* #3682 @Aggregation is applied to query if entity also has a @Lob column

The underling issue was that Aggregation columns should have been marked as default LAZY. This impact was only hit when @Lob column are included which by default lazy fetched, and that means the default select clause determined.
2025-10-20 20:55:18 +13:00
Rob Bygrave ee8e48902b Merge branch 'master' of github.com:ebean-orm/ebean 2025-10-19 18:07:18 +13:00
Rob Bygrave c37e752ace #3681 javadoc query cache (#3687)
* Version 16.1.0

* Improve javadoc for Query setUseQueryCache() and unmodifiable

Since 16.x a query using the query cache returns unmodifiable object graphs.
2025-10-14 20:04:38 +13:00
Rob Bygrave c9a6f822ac Version 16.1.0 2025-09-19 18:48:28 +12:00
Rob Bygrave 2231ef9fd1 Bump ebean-agent to 16.1.0 with Java 25 support (#3680)
Note that the IntelliJ plugin has been updated and published. It should be public in a few days.
2025-09-19 16:25:24 +12:00
Rob Bygrave 3ff1905318 Bump ebean-test-containers to 7.15 with updated ClickHouse support 2025-09-19 16:25:00 +12:00
Rob Bygrave 9b4fecff96 Add support for AutoCommit true transactions (#3679)
* Add support for AutoCommit true transactions

* Update DocStoreTransactionManager
2025-09-19 16:22:59 +12:00
Rob Bygrave 6f356fd55e #3674 Add @Nullable to Query getId(), getForUpdateLockWait(), getForUpdateLockType() (#3675)
These methods return nullable results, add JSpecify `@Nullable` to correctly indicate this.
2025-09-06 17:22:13 +12:00
Rob Bygrave 591f260f5b Retain filterMany expression when select fetchGroup applied after filterMany (#3673) 2025-09-02 22:35:43 +12:00
Rob Bygrave b361b10571 Version 16.0.1 2025-09-02 14:09:03 +12:00
Rob Bygrave c0e4e548ee Update the kotlin-maven-plugin for tests and use EA maven profile (#3671)
* Update the kotlin-maven-plugin for tests

* Add a profile for EA that excludes kotlin-querybean-generator

* For build workflow use maven profile "default"

To ensure the kotlin-querybean-generator, test modules run

* For build workflow use maven profile "default"

To ensure the kotlin-querybean-generator, test modules run
2025-09-02 14:04:59 +12:00
Rob Bygrave 92f3245919 Bump ebean-agent to 16.0.1 2025-09-02 13:37:45 +12:00
Rob Bygrave 59c2290916 Bump ebean-migration dependency, fix for setting dbSchema on migrations (#3670)
Ref: https://github.com/ebean-orm/ebean-migration/pull/163
2025-09-02 12:52:41 +12:00
Rob Bygrave 1135eb3f99 #3665 Fix for delete() Skips Bean due to Hash Collision (#3669)
Change the underlying storage of deleting beans to be by type by id so:

  private Map<Class<?>, Set<Object>> deletingBeans;
2025-08-31 22:32:27 +12:00
Rob Bygrave 1dccdff7bf #3664 Fix / support for extra JoinColumns on ManyToOne (#3668)
* #3664 Fix / support for extra JoinColumns on ManyToOne

Allows for extra JoinColumns on ManyToOne. The extra JoinColumn(s) are
expected to be useful for the case of table partitioning where the extra
join column is used to partition the table. In the test case, the partition
column would be the org_id column and common to both tables (same partition key).

* #3664 Fix / support for extra JoinColumns on ManyToOne

Allows for extra JoinColumns on ManyToOne. The extra JoinColumn(s) are
expected to be useful for the case of table partitioning where the extra
join column is used to partition the table. In the test case, the partition
column would be the org_id column and common to both tables (same partition key).

* #3664 Extend test

* #3664 Tidy up test
2025-08-31 12:52:17 +12:00
Rob Bygrave 5f74a53e3e #3666 Fix for NPE with filterMany() containing or() (#3667)
For FilterMany with QueryBeans, it creates a queryBean to build the filterMany predicates with. This query bean needed 2 changes for this fix:

1. Needs to set it's internal "root" such that it supports chaining (required for or() etc)
2. The ExpressionFactory needs to be explicitly passed to the expression list (rather than get it from the query which is actually null in this case).
2025-08-31 10:08:07 +12:00
Rob Bygrave 1f9e652ea9 Version 16.0.0 2025-08-25 08:21:49 +12:00
Rob Bygrave 4e592c97ff Update how to deploy 2025-08-05 23:30:18 +12:00
Rob Bygrave 760d19a10d Update how to deploy 2025-08-05 23:16:42 +12:00
Rob Bygrave d2693c3ff2 Version 16.0.0-RC4 2025-08-05 23:15:20 +12:00
Rob Bygrave 3324b2bda4 Use autocommit false with findIterate for Postgres (#3662)
* Use autocommit false with findIterate for Postgres

* Use autocommit false with findIterate for Postgres

* Use autocommit false with FindDto queries for Postgres

* Use autocommit false with SqlQuery findIterate queries for Postgres

* Ensure that TransactionFactory.createReadOnlyTransaction() returns an ImplicitReadOnlyTransaction

This change changes TransactionFactoryTenant.createReadOnlyTransaction() to return a ImplicitReadOnlyTransaction, as that is now used to support Postgres use of cursors with findIterate style queries.
2025-08-05 23:03:28 +12:00
Rob Bygrave 1b5e066562 #3654 Fix querybean-generator compiler warning for unclaimed annotations (#3655)
warning: No processor claimed any of these annotations: /io.ebean.typequery.TypeQueryBean,/io.ebean.typequery.Generated

This fix is required when the compiler is run with -Werror specified

An alternative workaround is to use compiler arg -Xlint:-processing rather than -Xlint:all
2025-07-16 20:34:56 +12:00
Rob Bygrave 104e4ec756 Bump ebean-test-containers to 7.14 - Yugabyte read committed support enabled by default (#3651) 2025-07-14 09:03:39 +12:00
Rob Bygrave 74a2967a6e Tests Yugabyte - comment out the TestQueryForUpdate using repeatable read
... as its noise there is anyone else reads the logs
2025-07-12 09:38:06 +12:00
Rob Bygrave e693b12a97 Tests DB2 - ignore TestRawExpressionInterpolation for DB2 2025-07-12 09:30:16 +12:00
Rob Bygrave 48fb9b34bb Tests Yugabyte - faster test using forUpdate 2025-07-12 09:18:01 +12:00
Rob Bygrave f64400400a Tests Oracle - ignore the TestRawExpressionInterpolation for Oracle 2025-07-12 09:09:34 +12:00
Rob Bygrave d7cc081942 Tests Yugabyte - see if everything else passes 2025-07-12 09:04:02 +12:00
Rob Bygrave 3351548b60 Fix use of JSpecify Nullable annotation in query bean generation 2025-07-11 23:45:31 +12:00
Rob Bygrave 8d9858f57a Test only - Update logging for test TestQueryForUpdate 2025-07-11 23:35:03 +12:00
Rob Bygrave 9b520bc176 Bump avaje-config to 4.1 2025-07-11 23:22:16 +12:00
Rob Bygrave 44fd8e3a56 Adjust TestQueryForUpdate for Yugabyte - use markAsDirty on repeatable read test 2025-07-11 23:11:45 +12:00
Rob Bygrave 6910016c4d Adjust TestQueryForUpdate for Yugabyte - use markAsDirty
To see if the update has an impact on the 2nd thread waiting
2025-07-11 22:58:02 +12:00
Rob Bygrave 5f8a3c0591 Adjust TestQueryForUpdate for Yugabyte - check repeatable read isolation level 2025-07-11 22:05:31 +12:00
Rob Bygrave 0ae6956c1c Bump ebean-datasource 10.1 with support for initialConnections (#3650)
So in a K8s deployment, this adds support for having an initial number of connections to create initially (higher that min connections). This allows for a smoother deployment, by effectively having initially more connections than min in order to immediately take production load, and then over time trim back down towards the min connections.

So in k8s I am setting min, initial and max connections, where initial connections is high enough to take full production load straight after readiness true.
2025-07-11 21:26:58 +12:00
Rob Bygrave 2e9c5d8548 Merge branch 'query-cache-oom-for-too-large-beans' 2025-07-11 21:19:21 +12:00
Rob Bygrave 93edc840b3 Add a test that hits the new branch of code 2025-07-11 21:18:00 +12:00
Josiah NoelandRob Bygrave 34311785fa Fix Jackson NCDF Error (#3639)
* fix jackson NCDF error

split jackson SPI impl into it's own module

* Restore format

* Restore format

* Trim up dependencies

* Rename module to ebean-core-json

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2025-07-11 20:53:25 +12:00
Rob Bygrave 63929f8ff3 Bump test Yugabyte version to 2.20.11.0-b34 2025-07-11 20:26:12 +12:00
Jonas Fröhler d918c5117f no import changes 2025-07-01 09:05:48 +02:00
Jonas Fröhler dd845a7b0c Fix imports 2025-07-01 09:02:26 +02:00
Jonas Fröhler df0e4d57f0 Clean up 2025-06-27 12:17:19 +02:00
Jonas Fröhler 9c7fa25d49 Query Cache OOM test and potential draft for the fix 2025-06-27 12:14:01 +02:00
Rob Bygrave e82c1a4140 Version 16.0.0-rc3 2025-05-26 21:00:32 +12:00
Rob Bygrave 389bf48e3e Merge pull request #3636 from ebean-orm/feature/explicit-shutdownHook
Add DatabaseBuilder.shutdownHook option (programmatic disabling registration of JVM shutdown hook)
2025-05-26 17:24:25 +12:00
Rob Bygrave 3b3b44543e Bump ebean-test-containers, programmatically get DatabaseBuilder from test container 2025-05-26 17:23:27 +12:00
Rob Bygrave 297f334da5 Add DatabaseBuilder.shutdownHook option (programmatic disabling registration of JVM shutdown hook) 2025-05-26 17:21:15 +12:00
Rob Bygrave 78c7439445 test only - rename TestRawExpressionInterpolation 2025-05-15 08:00:52 +12:00
Rob Bygrave 2043ba6411 Merge pull request #3634 from ebean-orm/feature/native-image-otherTypes
[native-image] Modify querybean-generator to include AttributeConverters in generated reflect-config.json
2025-05-14 20:49:52 +12:00
Rob Bygrave 437c58001b [native-image] Modify querybean-generator to include AttributeConverters in generated reflect-config.json 2025-05-14 20:48:07 +12:00
Rob Bygrave 0e812efa7e Merge branch 'master' of github.com:ebean-orm/ebean 2025-05-14 20:27:56 +12:00
Rob Bygrave 80d4d21986 Merge pull request #3632 from FOCONIS/reorg-args-placeholder
Db2: Reorg args placeholder
2025-05-06 20:44:00 +12:00
Noemi Praml fe66b8c242 fix DbMigrationTest 2025-05-06 09:49:18 +02:00
Noemi Praml 206b2b2d3b migrations 2025-05-06 08:06:48 +02:00
Noemi Praml c0bc3faea0 fix test 2025-05-06 08:06:40 +02:00
Noemi Praml 3d07e7d804 add: reorgARgs Placeholder 2025-05-06 08:00:58 +02:00
Rob Bygrave fc124e8318 Tidy pom only 2025-04-30 00:02:30 +12:00
Rob Bygrave f4fd4f4b09 Version 16.0.0-RC2 2025-04-30 00:01:52 +12:00
Rob Bygrave 16f5311885 Bump parent pom to 5.1 for new maven central deploy 2025-04-29 23:41:48 +12:00
Rob Bygrave 75a0675977 Merge pull request #3630 from ebean-orm/feature/fix-ea-build-kotlin
Fix EA build, Kotlin not supporting 25-EA release
2025-04-29 23:37:10 +12:00
Rob Bygrave 80ebf4a1ee Fix EA build, Kotlin not supporting 25-EA release
It barfs trying to parse the 25-EA version, means the kotlin-querybean-generator fails the build for the Java Early Release versions.

With this, it will skip the kotlin modules for the Java Early Access release builds.
2025-04-29 23:36:22 +12:00
Rob Bygrave 4185beb5aa Merge pull request #3629 from ebean-orm/feature/bump-datasource-10
Bump ebean-datasource to 10.0 (Virtual Threads support via MR Jar)
2025-04-29 23:17:54 +12:00
Rob Bygrave 79908c0164 Bump ebean-datasource to 10.0 (Virtual Threads support via MR Jar)
Note that this still supports Java 11.

The 10.0 version of ebean-datasource uses Multi-Version jar such that if running using Java 21+ then it will use Virtual Threads for the background Heartbeat validation and also background closing of connections.
2025-04-29 23:09:49 +12:00
Rob Bygrave b5a16f42ca #3626 Add tests for bean property name instead of db column 2025-04-23 22:33:18 +12:00
Gonçalo Andrade a320a132b3 Fix incorrect usage of bean property name instead of corresponding column 2025-04-22 14:51:38 +01:00
Rob Bygrave 9e84e93511 Merge pull request #3621 from FOCONIS/new-db-versions
New db versions + testcontainers 7.8
2025-04-17 23:18:05 +12:00
Noemi Praml 6fb40c71ef update StartDb2 2025-04-16 15:22:08 +02:00
Noemi Praml 202b9ae94a update to testcontainers 7.8 2025-04-16 14:52:21 +02:00
Noemi Praml d79b452378 new db2 version 2025-04-16 14:19:11 +02:00
Noemi Praml 78a408dd10 new sqlserver version 2025-04-16 14:19:11 +02:00
Noemi Praml 6af974e432 new mariadb version 2025-04-16 14:19:11 +02:00
Rob Bygrave 04a8475291 Merge pull request #3623 from ebean-orm/feature/readOnly-DataSource-mark-nullable
Explicitly mark Database.readOnlyDataSource() as @Nullable
2025-04-15 22:25:06 +12:00
Rob Bygrave c56345f92e Explicitly mark Database.readOnlyDataSource() as @Nullable
Using JSpecify @Nullable on the return for Database.readOnlyDataSource()
2025-04-15 22:24:14 +12:00
Rob Bygrave ed0a46955e Explicitly mark Database.readOnlyDataSource() as @Nullable
Using JSpecify @Nullable on the return for Database.readOnlyDataSource()
2025-04-15 22:22:08 +12:00
Rob Bygrave 3c4b04a893 Version 16.0.0-rc1 2025-04-09 23:18:27 +12:00
Rob Bygrave ee403bdb83 Merge pull request #3620 from ebean-orm/feature/bump-ebean-agent
Bump the ebean-agent to 14.12.0, support for Lombok SuperBuilder
2025-04-09 23:02:46 +12:00
Rob Bygrave b18abe2221 Bump the ebean-agent to 14.12.0, support for Lombok SuperBuilder 2025-04-09 22:52:39 +12:00
Rob Bygrave d68f372d99 Merge pull request #3619 from ebean-orm/feature/column-length-255
For Postgres @Column(length=255) means varchar(255)
2025-04-09 20:22:56 +12:00
Rob Bygrave c8f769aa2a For Postgres @Column(length=255) means varchar(255)
The `@Column` length defaults to 0 rather than 255, this allows us to detect when it has been explicitly set and thus for Postgres we can now have:

@Column(length=255) mapping to -> varchar(255)
@Column mapping to -> varchar
2025-04-09 20:07:56 +12:00
Rob Bygrave bde626fd45 Merge pull request #3618 from ebean-orm/feature/bump-datasource-95
Bump ebean-datasource to 9.5
2025-04-09 20:04:23 +12:00
Rob Bygrave b828b73b2c Bump ebean-datasource to 9.5
When using DataSourceBuilder for both main and readOnly datasource, the maxConnections for the readOnly will default to the main pool maxConnections.
2025-04-09 20:02:14 +12:00
Rob Bygrave 2795a8578f Merge pull request #3616 from ebean-orm/feature/lazyLoadBatch-100
Increase default lazyLoadBatchSize from 10 to 100
2025-04-04 21:15:23 +13:00
Rob Bygrave bd0a3c2303 Increase default lazyLoadBatchSize from 10 to 100 2025-04-04 21:14:47 +13:00
Rob Bygrave 21e50e9d00 Merge pull request #3615 from ebean-orm/feature/3612-redis-exception
#3612 Catch all RedisException (rather than only IOException)
2025-04-04 20:47:44 +13:00
Rob Bygrave 88a47bf0a9 #3612 Catch all RedisException (rather than only IOException) 2025-04-04 20:45:59 +13:00
Rob Bygrave d9a45245f4 Version 16.0.0-beta1 2025-04-03 19:35:14 +13:00
Rob Bygrave 7be72df788 Merge pull request #3570 from ebean-orm/feature/improved-readOnly-immutable
Unmodifiable - Improved read only immutable entity beans / graphs
2025-04-02 22:24:19 +13:00
Rob Bygrave 42685335e3 Version 14.11.0 2025-04-01 21:32:48 +13:00
Rob Bygrave 478a2be405 Merge pull request #3611 from ebean-orm/feature/fix-native-image-mappedSuper
Fix NPE with native-image with @MappedSuperclass
2025-04-01 20:45:17 +13:00
Rob Bygrave ebcac9a503 Fix NPE with native-image with @MappedSuperclass
@MappedSuperclass was excluded from the generated reflect-config.json, this PR fixes that.

The missing reflect-config entry for MappedSuperclass means that with native-image the @Id property isn't found when its on a MappedSuperclass, and that ended up as a NPE in BeanPropertyAssocOne.
2025-04-01 20:29:26 +13:00
Rob Bygrave 996239c29f Merge pull request #3610 from ebean-orm/feature/configure-explain
Add support to configure EXPLAIN options for query plan capture
2025-04-01 08:07:46 +13:00
Rob Bygrave caf2772fb3 Put Postgres explain back to default format text for now 2025-04-01 08:07:04 +13:00
Rob Bygrave 1482c4c84f Add support to configure EXPLAIN options for query plan capture
e.g. DatabaseBuilder.queryPlanExplain("explain (verbose)")

Also changes Postgres default explain to be:
explain (analyze, costs, verbose, buffers, format json)
2025-03-31 23:41:16 +13:00
Rob Bygrave 792d52b794 Merge pull request #3609 from ebean-orm/feature/queryPlanInit
Change QueryPlanInit to support thresholdMicros per hash
2025-03-30 21:41:53 +13:00
Rob Bygrave aa4ea4904f Change QueryPlanInit to support thresholdMicros per hash
Effectively means that we can provide a thresholdMicros per query plan hash [for the plans that we want collected]
2025-03-30 21:35:20 +13:00
Rob Bygrave bab839fb38 Change QueryPlanInit to support thresholdMicros per hash
Effectively means that we can provide a thresholdMicros per query plan hash [for the plans that we want collected]
2025-03-30 18:33:13 +13:00
Rob Bygrave dde2567ccd Merge pull request #3608 from ebean-orm/feature/bump-postgres-16
Test only - align sqlserver test configuration
2025-03-26 22:50:48 +13:00
Rob Bygrave 7e4d070b75 Test only - update sqlserver test docker image to 2017-CU31-ubuntu-18.04 2025-03-26 22:50:22 +13:00
Rob Bygrave 3151e3f82b Test only - align sqlserver test configuration 2025-03-26 22:41:00 +13:00
Rob Bygrave c630aaff0e Merge pull request #3607 from ebean-orm/feature/bump-postgres-16
Test only - align mariadb test configuration
2025-03-26 22:37:34 +13:00
Rob Bygrave 67ec848f15 Test only - align mariadb test configuration 2025-03-26 22:37:09 +13:00
Rob Bygrave 57f0734381 Test only - align mariadb test configuration 2025-03-26 22:32:18 +13:00
Rob Bygrave cbd5bf23c9 Merge pull request #3606 from ebean-orm/feature/bump-postgres-16
Test only - align mysql and db2 test configuration
2025-03-26 22:31:51 +13:00
Rob Bygrave 5dc664a63f Test only - align mysql and db2 test configuration 2025-03-26 22:27:35 +13:00
Rob Bygrave d9b53677f2 Merge pull request #3605 from ebean-orm/feature/bump-postgres-16
Test only - add logging for datasource to BaseTestCase
2025-03-26 22:17:56 +13:00
Rob Bygrave f55c2cf3b6 Test only - add logging for datasource to BaseTestCase 2025-03-26 22:14:24 +13:00
Rob Bygrave c98def7ab7 Merge pull request #3604 from ebean-orm/feature/bump-postgres-16
Test only - add logging for datasource to BaseTestCase
2025-03-26 22:06:58 +13:00
Rob Bygrave 705e158a70 Test only - add logging for datasource to BaseTestCase 2025-03-26 22:06:35 +13:00
Rob Bygrave 569e782e99 Merge pull request #3603 from ebean-orm/feature/bump-postgres-16
Update Postgres and Postgis test containers to default to 16
2025-03-26 22:05:59 +13:00
Rob Bygrave 265b92fb12 Update Postgres and Postgis test containers to default to 16 2025-03-26 21:43:08 +13:00
Rob Bygrave 74311fad0a Update test postgres docker image to 16 2025-03-26 21:34:31 +13:00
Rob Bygrave 1eded5682a Merge pull request #3599 from ebean-orm/feature/bump-datasource-dependency2
Bump ebean-datasource dependency to 9.3
2025-03-21 18:42:53 +13:00
Rob Bygrave ff21bfe1de Bump ebean-datasource dependency to 9.3 2025-03-21 00:35:41 +13:00
Rob Bygrave 86db7fadfd Merge pull request #3598 from ebean-orm/feature/tidy-QueryCacheEntry
Tidy QueryCacheEntry, final class and use accessors
2025-03-20 23:58:56 +13:00
Rob Bygrave e48b327967 Tidy QueryCacheEntry, final class and use accessors 2025-03-20 23:57:56 +13:00
Rob Bygrave 964b3ccf8e Merge pull request #3597 from ebean-orm/feature/3515-extend
#3515 Bug in QueryCache invalidation
2025-03-20 23:55:10 +13:00
Rob Bygrave eb6906ad2b #3515 Bug in QueryCache invalidation
Effectively the bug fix is in SqlTreeNodeRoot.dependentTables() to include the baseTable in the set of dependent tables
2025-03-20 23:47:21 +13:00
Rob Bygrave 86e5ea3627 Merge pull request #3596 from ebean-orm/feature/tidy-future-queries
Change findFuture queries such that they can use the read only DataSource
2025-03-19 23:06:38 +13:00
Rob Bygrave 3a5e8290e0 Change findFuture queries such that they can use the read only DataSource
Prior to this change they made an explicit transaction if one was needed, and this would always use the Main DataSource.

With this change, when there is no transaction assigned to the query and no active transaction, then the find future query will use the ReadOnly DataSource if it has been configured (e.g. ReadOnly DataSource might point to a read replica database instance).
2025-03-19 23:00:19 +13:00
Rob Bygrave 929604e33d Merge pull request #3593 from LeComptoirDesPharmacies/feature/findFutureMap
Add feature findFutureMap
2025-03-19 22:12:06 +13:00
Rob Bygrave 2eed68ad61 Merge pull request #3595 from ebean-orm/feature/bump-avaje-config2
Bump avaje-config dependency to 4.0
2025-03-19 20:57:13 +13:00
Rob Bygrave f705560b8b Bump avaje-config dependency to 4.0 2025-03-19 20:56:40 +13:00
Rob Bygrave 6d791c7b50 Add BeanAccessException extends UnsupportedOperationException
And:

LazyInitialisationException extends BeanAccessException ...
UnmodifiableEntityException extends BeanAccessException ...
2025-03-18 22:54:34 +13:00
Rob Bygrave 1afe42f4c6 Merge branch 'master' into feature/improved-readOnly-immutable 2025-03-18 22:43:00 +13:00
Rob Bygrave 4e7f3c13a0 Merge pull request #3592 from ebean-orm/feature/improved-readOnly-immutable-part2
Remove readOnly, migrate to unmodifiable (or use normal mutable)
2025-03-18 22:41:12 +13:00
Thomas a35db6bdee Add feature findFutureMap 2025-03-18 09:19:15 +01:00
Rob Bygrave d7366dec0f Tidy readOnly removal 2025-03-18 20:59:04 +13:00
Rob Bygrave ceee1d7489 Remove readOnly from Query 2025-03-18 20:49:26 +13:00
Rob Bygrave 1b23319fa4 Remove readOnly from EntityBeanIntercept and BeanCollection 2025-03-18 20:32:22 +13:00
Rob Bygrave 6f726973ea Remove most internal use of readOnly 2025-03-18 19:53:13 +13:00
Rob Bygrave 82284b19d5 Bean cache sharable instances as unmodifiable + bean cache queries can also be unmodifiable
Entities without *any* relationships can support "sharable instances" with bean caching. This change restores that and uses unmodifiable instances (rather than the old ReadOnly).

Currently, for a query to get shared instances it now needs to explicitly use setUnmodifiable(true) where as before it defaulted to shared instances so that is a behaviour change.

Additionally, bean cache queries were not honouring setUnmodifiable(true) and with these changes they now do.
2025-03-16 23:19:54 +13:00
Rob Bygrave 798d01abd1 LazyInitialisationException, plus javadoc 2025-03-16 21:12:25 +13:00
Rob Bygrave 5136a5a708 Support unmodifiable Embedded and EmbeddedId 2025-03-13 22:52:45 +13:00
Rob Bygrave fa25d7cc8a Using Query Cache now implies unmodifiable
- Query Cache now holds unmodifiable collections
- Can no longer have readOnly=false with queryCache=true
- Reference beans now also honor unmodifiable
- Effectively no longer does bean cache lookup for reference beans (which it defaulted to when cacheSharableBeans true, e.g. Country entity bean)
2025-03-11 23:50:34 +13:00
Rob Bygrave c13e09257c Version 14.10.0 2025-03-11 21:26:00 +13:00
Rob Bygrave ed65433640 Merge pull request #3574 from ebean-orm/feature/3459-postgres-varchar
#3459 Postgres varchar default to no length (aka text)
2025-03-11 20:01:26 +13:00
Rob Bygrave 08890e9d5a Merge pull request #3590 from ebean-orm/feature/3580-lookwriter-logWarn
Adjust LookupWriter to log at WARN rather than ERROR due to Quarkus dev mode
2025-03-10 23:56:19 +13:00
Rob Bygrave 4273c4af6a Adjust LookupWriter to log at WARN rather than ERROR due to Quarkus dev mode
Die to Quarkus dev mode (#3541 #3582) we want to log FilerException at WARN level rather than ERROR. This is because Quarkus dev mode will invoke the annotation processor more than usual, so we expect to fail with the FilerException.
2025-03-10 23:42:38 +13:00
Rob Bygrave 51424d9eea Merge pull request #3580 from SentryMan/feature/lookup-generation
[querybean-generator] Generate lookup for modules
2025-03-10 23:31:35 +13:00
Rob Bygrave cf2ab33903 Merge pull request #3589 from ebean-orm/feature/bump-ebean-agent-dependency
Bump ebean-agent to 14.10.0
2025-03-10 23:28:05 +13:00
Rob Bygrave 90d8b4fb04 Bump ebean-agent to 14.10.0 2025-03-10 23:27:02 +13:00
Rob Bygrave ff4eeff264 Merge pull request #3588 from ebean-orm/feature/3586-querybean-gen
[query bean generation] #3586 public constructor for Embedded bean
2025-03-10 08:02:03 +13:00
Rob Bygrave 64ba343e35 [query bean generation] #3586 public constructor for Embedded bean 2025-03-10 07:59:33 +13:00
Rob Bygrave af3c59c0fd Merge pull request #3587 from ebean-orm/feature/bump-test-containers-77
Bump ebean-test-containers dependency - improved SQL Server handling
2025-03-10 07:40:31 +13:00
Rob Bygrave ae55d28026 Bump ebean-test-containers dependency - improved SQL Server handling
Improvments around collation changes that end up making SQL Server startup flaky
2025-03-10 07:40:02 +13:00
Rob Bygrave ddffc8eee2 Merge pull request #3581 from apflieger/#3573-initfields-prevent-secondary-query
OneToMany initialisation via List.of() - was Tests only - new test on multiple many joins
2025-03-10 07:30:40 +13:00
Rob Bygrave 7f1a3a46cf #3581 ebean-agent 14.10.0 handling for OneToMany initialisation 2025-03-10 07:12:20 +13:00
Rob Bygrave a59f75a468 Merge pull request #3585 from nedge/feature/Create_test_entities
Simple Test Entities in preparation for type scoped loading
2025-03-05 08:06:39 +13:00
Eddie Mc Greal 6f1c66ecd6 Simple Test Entities in preparation for type scoped loading 2025-03-04 14:17:08 +01:00
Rob Bygrave 25f6fb6d6f test only - Almost fix the model for test_manyNonRoot_RootHasNoMany
The test was using field access??? when that isn't valid unless we specifically turn on enhancement to support that.

Using proper method access though stops the test from failing.
2025-03-03 21:42:15 +13:00
Rob Bygrave 852def8536 test only - use assertThat hasSize instead of assertEquals for test_manyNonRoot_RootHasNoMany 2025-03-03 21:34:22 +13:00
Rob Bygrave dfb29fd75f Merge pull request #3578 from mkurz/fix_3577
Work around `mysqldump` bug by moving comment one line up
2025-03-03 21:26:40 +13:00
Rob Bygrave 80c3501197 Merge pull request #3583 from ebean-orm/feature/revert-3541-3582
[querybean-generator] #3582 - Revert #3541 "FilerException trying to write EntityClassRegister with Quarkus dev mode / hot reload "
2025-03-03 21:25:39 +13:00
Rob Bygrave b4ff5748fc Merge pull request #3579 from FOCONIS/rollbac-or-commit-before-close
Rollback/Commit raw connections before close
2025-03-03 21:25:12 +13:00
Rob Bygrave 64f01f0543 [querybean-generator] #3582 - Revert #3541 "FilerException trying to write EntityClassRegister with Quarkus dev mode / hot reload "
This reverts commit 69c25309 and applies the required "Fix" in the initModuleInfoBean() method to catch FilerException and log it at WARN level
2025-03-03 20:49:19 +13:00
Arnaud Pflieger fee6e0f480 Tests only - new test on multiple many joins
This test reproduce the problem of #3573
The field initialization `= List.of();` prevents eager secondary query.
2025-02-28 12:20:18 +01:00
Josiah Noel 50b44c4529 Update LookupWriter.java 2025-02-28 00:29:03 -05:00
Josiah Noel f0c2ba0e28 [Feature] Generate lookup for modules
Generate Lookup Class for modular projects
2025-02-27 20:47:16 -05:00
Roland Praml 7643dc9b22 Rollback/Commit raw transactions before close 2025-02-27 10:56:41 +01:00
Matthias Kurz c49a1b3d71 Work around mysqldump bug by moving comment one line up
It seems like mysqldump just adds ;; to the last line and does not
check if that line is a comment. To work around that it's good enough
to move the comment one line up - as long as the END does not end with ;
(which here it does not so that is ok)
2025-02-26 15:36:20 +01:00
Rob Bygrave 11e783938c Merge pull request #3576 from ebean-orm/feature/bump-ebean-test-containers
#3511 bump ebean-test-containers dependency
2025-02-26 23:18:43 +13:00
Rob Bygrave 3845861e24 test only - Fix tests due to automatic inline comment included in generated sql like:
select /* TestQueryDefaultBatchSize.test_findEach_details__lazy */ t0.order_id, t0.id,

select /* TestQueryDefaultBatchSize.test_findEach_details__lazy */ t0.order_id
2025-02-26 23:15:51 +13:00
Rob Bygrave d04181865c test only - update TestOrderByWithMany which is flaky 2025-02-26 23:10:55 +13:00
Rob Bygrave 00d7db31d6 test only - update TestQueryFindIterate which is flaky 2025-02-26 23:07:53 +13:00
Rob Bygrave 6880b5673a #3511 bump ebean-test-containers dependency
ebean-test-containers version 7.6 supports extraDb for MySql and MariaDb, resolving #3511 with extraDb configuration for each extra schema/database required with MySql or MariaDb.
2025-02-26 22:24:05 +13:00
Rob Bygrave e18b59a9dd Remove deprecated ExtendedServer.setClock() migrate to DatabaseBuilder.clock() (#3566)
So we can no longer dynamically change the Clock being used and instead for testing purposes need to create the Database with a test clock instance as needed.

Refer to the ExtendedServerTest for an example
2025-02-26 22:16:12 +13:00
Rob Bygrave cc48dced09 #3572 DDL support for non-nullable JoinColumn column (#3575) 2025-02-26 21:13:50 +13:00
Rob Bygrave b13bdc95be #3459 Postgres varchar default to no length (aka text)
Change Postgres platform to default a String to varchar [with no length defined] rather than varchar(255)
2025-02-26 18:53:08 +13:00
Rob Bygrave 0a5c3ced21 Test only - update TestQueryMultiManyOrder sql asserts
No effective change.
2025-02-26 00:59:57 +13:00
Rob Bygrave 8ff5f7f72a Recursive freeze on entity beans
Noting that the freeze needs to occur after secondary queries have executed
2025-02-26 00:00:07 +13:00
Rob Bygrave 71944bb8fa Rename flags -> loaded, add message with property name to UnmodifiableEntityException 2025-02-23 22:25:46 +13:00
Rob Bygrave 24888fc01e Change InterceptReadWrite to use UnmodifiableEntityException
Add unmodified to DefaultOrmQuery query plan description
Fix incorrect merge conflict
2025-02-21 22:24:48 +13:00
Rob Bygrave 645d5bb613 Merge branch 'master' into feature/improved-readOnly-immutable 2025-02-21 22:08:38 +13:00
Rob Bygrave 4a6f1a39e0 Add UnloadedPropertyException and UnmodifiableEntityException, change existing use of IllegalStateException
- Adds new exceptions UnloadedPropertyException and UnmodifiableEntityException
- Change InterceptReadOnly to use these exception instead of IllegalStateException
- Change collections BeanSet, BeanList, BeanMap from using IllegalStateException to UnsupportedOperationException [to bring these in line with JDK unmodifiable collections]
2025-02-21 22:05:49 +13:00
Rob Bygrave 40637bd68c Allow OneToOne to map to ManyToOne (#3569) 2025-02-19 20:00:15 +13:00
Rob BygraveandJonas Pöhler 8dd438be61 #3564 Fix for nested transaction with batch-mode does not flush (#3565)
* ADD: Testcase for showing nested transaction with batch-mode not flushing batch on commit

* Update test only - TestNestedTransaction.test_txn_nestedWithInnerBatch()

Update the test to show that we desire the nested transaction changes to be "visible" after a commit regardless of flushOnQuery etc

* #3564 Fix for nested transaction with batch-mode does not flush

- Adds flush() into ScopeTrans.commitTransaction()
- Related fixes in DefaultServer execute() and executeCall() where it was performing extra unnecessary nesting on PersistenceException

* Touch

---------

Co-authored-by: Jonas Pöhler <jonas.poehler@foconis.de>
2025-02-18 07:33:48 +13:00
Rob Bygrave 5e1ad29c5d Build - add workflow_dispatch trigger to main build 2025-02-17 07:46:20 +13:00
Rob Bygrave 9eeb137907 Tests only - update test-kotlin dependency 2025-02-17 07:33:56 +13:00
Rob Bygrave 578a041c9e Version 14.9.0 2025-02-12 19:56:14 +13:00
Rob Bygrave b0b2a0d320 Bump ebean-agent to 14.9.0 with module path support for @Transactional 2025-02-12 19:36:48 +13:00
Josiah NoelandRob Bygrave de81dfc067 Feature: Add a MethodHandles.Lookup SPI (#3535)
* ServiceLoad lookups

* remove more reflection

* address comments

* Update DefaultTypeManager.java

* Javadoc and format changes

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2025-02-12 19:30:05 +13:00
Rob Bygrave 2bd53cb52b Merge pull request #3560 from ebean-orm/feature/3533-aop-transactional
#3533 Support @Transactional on module path
2025-02-12 18:53:57 +13:00
Rob Bygrave a6d792de64 Merge pull request #3563 from ebean-orm/feature/remove-deprecated-queryOrder
Remove deprecated order() migrate to orderBy()
2025-02-12 18:52:53 +13:00
Rob Bygrave c73d83ef33 Merge pull request #3562 from ebean-orm/feature/remove-deprecated-queryUpdTxn
Remove deprecated query.update(txn) & query.delete(txn)
2025-02-12 18:52:18 +13:00
Rob Bygrave 9f1b4ba758 Remove deprecated order() migrate to orderBy() 2025-02-12 18:47:16 +13:00
Rob Bygrave 1f7117da25 Merge pull request #3561 from ebean-orm/feature/remove-deprecated-servletContextListener
Remove deprecated ServletContextListener
2025-02-12 18:43:55 +13:00
Rob Bygrave f0ceab43bf Remove deprecated query.update(txn) & query.delete(txn)
Migrate to query.usingTransaction(txn)
2025-02-12 18:42:23 +13:00
Rob Bygrave 1107e4ea4c Remove deprecated ServletContextListener 2025-02-12 18:38:17 +13:00
Rob Bygrave 4dd8a5dfea Merge pull request #3558 from ebean-orm/feature/3557-filterMany-removal
#3557 [Query beans] Remove deprecated filterMany(String expressions, Object... params) from generated query beans
2025-02-12 18:01:39 +13:00
Rob Bygrave 195edf0355 Merge pull request #3514 from ebean-orm/feature/querybean-txn-ctor
Remove deprecated QueryBean constructor that takes transaction, migrate to query.usingTransaction()
2025-02-12 18:01:03 +13:00
Rob Bygrave e9c438adab Merge branch 'master' into feature/querybean-txn-ctor 2025-02-12 15:05:57 +13:00
Rob Bygrave e45cf5fab9 #3533 Bump ebean-agent to use the new AOPTransactionScope 2025-02-12 00:27:07 +13:00
Rob Bygrave 1bf9d1dd4f Merge pull request #3559 from ebean-orm/feature/findCount-noId
FindCount on entity bean without @Id + platform wants column alias
2025-02-12 00:01:24 +13:00
Rob Bygrave 657920c4a2 #3533 Support @Transactional on module path
As HelpScopeTrans isn't in an exported package it's use fails with an error in module path. This adds AOPTransactionScope in the exported package io.ebean.plugin and moves the required methods from SpiEbeanServer to SpiServer to support this move.

This also requires an associate change in ebean-agent to support this feature which will be in ebean-agent 14.9.0.
2025-02-11 23:57:18 +13:00
Rob Bygrave 6c47107436 FindCount on entity bean without @Id + platform wants column alias
Internally in CQueryBuilder only use query.setSelectId(); for findCount when the entity bean has a @Id property [which it doesn't strictly need and likely won't have for view based entities]
2025-02-11 23:04:02 +13:00
Rob Bygrave 88cf4e001a #3557 [Query beans] Remove deprecated filterMany(String expressions, Object... params) from generated query beans, migrate to filterManyRaw() 2025-02-11 22:06:20 +13:00
Rob Bygrave 7f1fcbcf92 Merge pull request #3556 from ebean-orm/feature/test-containers-bump
Bump ebean-test-containers to 7.5
2025-02-10 21:55:18 +13:00
Rob Bygrave 95d5d5e467 Bump ebean-test-containers to 7.5 2025-02-10 21:54:54 +13:00
Rob Bygrave 2dde23d577 Merge pull request #3555 from ebean-orm/feature/3503-kotlin-querybean-gen-DbMap
#3503 - Fix kotlin-querybean-generator for use of @DbMap
2025-02-10 14:01:09 +13:00
Rob Bygrave 1ec26d13f9 #3503 - Fix kotlin-querybean-generator for use of @DbMap 2025-02-10 11:35:44 +13:00
Rob Bygrave bb1b55e897 Reduce frequency of running yugabyte CI test 2025-02-09 22:45:10 +13:00
Rob Bygrave fb07447df4 Fix test only - test-kotlin bump kotlin-querybean-generator version 2025-02-09 22:43:02 +13:00
Rob Bygrave ad18b6573c Version 14.8.2 2025-02-09 22:07:28 +13:00
Rob Bygrave 11632c3a6e Merge pull request #3554 from ebean-orm/feature/3536-kotlin-querybean-generator
#3536 kotlin-querybean-generator update for filterMany() changes
2025-02-09 21:37:38 +13:00
Rob Bygrave b066f9c072 #3536 kotlin-querybean-generator update for filterMany() changes
Brings the kotlin-querybean-generator up to date with the [java] querybean-generator in terms of how it has specific "associated beans" for ToOne and ToMany type relationships
2025-02-09 21:24:51 +13:00
Rob Bygrave 1e38ed9efc Test only, add Embedded bean as an inner class for querybean-generation 2025-02-09 21:19:39 +13:00
Roland Praml 2d48dd2fef Pass create/configOptions to DB2 test container (#3550)
This allows to pass collation/pagesize settings via the
`ebean.test.createOptions=` property to the container
2025-02-08 19:47:20 +13:00
Rob Bygrave 60aa899a34 #3552 Column alias required for SQL Server + findCount + distinct (#3553)
* #3552 Column alias required for SQL Server + findCount + distinct

- Modifies CQueryBuilder with the main fix which is to use the database platform selectCountWithColumnAlias for all distinct queries [and later disable it for single column case]

- Modifies SqlTreeBuilder because we need to NOT use the column alias for the count distinct SINGLE COLUMN case. So change here to:
  - build the root SqlTreeNode first as part of constructor
  - use rootNode.isSingleProperty() to NOT use column alias for the single column case
  - Hence need to initialise DefaultDbSqlContext AFTER the root SqlTreeNode

  - Modifies DefaultDbSqlContext with no effective change, just columnIndex++ ONLY when we are actually using column alias

 - Modifies H2Platform to also use selectCountWithColumnAlias true

* #3552 selectCountWithColumnAlias true for MariaDB and MySql

Plus fix TestRowCount for Oracle platform
2025-02-08 19:33:03 +13:00
Rob Bygrave c38c75fdec Merge pull request #3549 from ebean-orm/feature/3546
#3546 Reduce compiler warning on query bean generated code
2025-01-28 23:34:23 +13:00
Rob Bygrave cf275f4edd #3546 Reduce compiler warning on query bean generated code
This method is effectively internal and called by generated code that also is Deprecated(forRemoval = true)
2025-01-28 23:31:23 +13:00
Rob Bygrave 121f4afed8 #3545 Support @SQL entity with a OneToOne with EmbeddedId
- @Sql entity
- Has a OneToOne or ManyToOne to a bean that uses @EmbeddedId
2025-01-28 23:26:09 +13:00
Rob Bygrave c8621c3a29 Put back the querybean-generator version 2025-01-16 21:42:02 +13:00
Rob Bygrave f83da15d5e Merge pull request #3542 from ebean-orm/feature/3542-quarkus
[querybean-generator] FilerException trying to write EntityClassRegister with Quarkus dev mode / hot reload #3541
2025-01-16 16:46:46 +13:00
Rob Bygrave 52729d9802 Missing import and adjust warning message 2025-01-16 16:40:43 +13:00
Rob Bygrave 69c25309da [querybean-generator] FilerException trying to write EntityClassRegister with Quarkus dev mode / hot reload #3541
Drop to log at WARN level when we catch FilerException such that Quarkus dev mode / hot reload works
2025-01-16 16:31:52 +13:00
Rob Bygrave 723a12fd66 #3538 Restoring batchMode etc with nested transaction scopes
This change is a simplification such that these batch attributes are always stored and restored for nested ScopeTrans.

There are no expensive side effects of doing it this way.

This fixes issues where the batch attributes are changed after the ScopeTrans is created (like the test for this PR).
2025-01-16 08:15:22 +13:00
Noemi Praml 5cf74f1ee9 Add: failing Unittest 2025-01-08 14:43:49 +01:00
Rob Bygrave 2c18b4fe97 Version 14.8.1 2024-12-20 15:34:07 +13:00
Rob Bygrave 9c9fb70999 Bump ebean agent to align 2024-12-20 15:24:08 +13:00
Rob Bygrave 18e6374a60 Merge pull request #3532 from ebean-orm/feature/sql-play-ebean
#3408 play-ebean inline sql comment in test generated scripts
2024-12-20 14:59:54 +13:00
Rob Bygrave 3fea5ce93d #3408 play-ebean inline sql comment in test generated scripts 2024-12-20 14:58:10 +13:00
Rob Bygrave 43675468ae Merge pull request #3523 from FOCONIS/streaming-tests
set defaultFetchBuffer in findEach/findList in SQL and DTO Queries
2024-12-19 11:04:01 +13:00
Rob Bygrave 1c038f4629 Merge pull request #3408 from mkurz/meta_datastored-procedures-DDL
Add metadata to stored procedures for 3rd party libs
2024-12-19 10:47:14 +13:00
Rob Bygrave 49b6f3dec9 Merge pull request #3530 from SentryMan/fix/dto-lookup
FIX: IllegalAccessError on modular DTO queries
2024-12-19 09:07:45 +13:00
Josiah Noel 346e30217a Merge remote-tracking branch 'upstream/master' into fix/dto-lookup 2024-12-18 14:56:15 -05:00
Rob Bygrave 5dc165debf Fix test TestLazyJoin2 2024-12-19 08:42:37 +13:00
Josiah Noel d5cb7c3ff1 fix illegal access error on modular dtos
When using a dto query on a JPMS application, I get an IllegalAccessError even though I export and open my package.
2024-12-18 00:20:08 -05:00
Roland Praml 4d08dce033 set defaultFetchBuffer in findEach/findList in relational/orm Queries 2024-11-27 17:05:15 +01:00
Roland Praml beef4407e3 Add failing testcase 2024-11-27 17:02:15 +01:00
Matthias Kurz e99f922de6 Add metadata to stored procedures for 3rd party libs
This way a third party can parse the meta data and knows how
to handle the bodies of a stored procedure
2024-11-26 23:36:30 +01:00
Rob Bygrave 1fef4f7c11 Add explicit query.setUnmodifiable(true) [for readOnly=true + disableLazyLoad=true]
Although this is ok, makes me think that another option is just to have query.setReadOnly(true)
to mean ... readOnly + disableLazyLoad + error reading unloaded property or collection. As in,
readOnly true without these extra things does not that good [as in the existing readOnly does
not seem very good/safe/useful to use].
2024-11-20 23:01:59 +13:00
Rob Bygrave 103e8323f2 Merge pull request #3519 from FOCONIS/no-codechange-test-cleanup
No productive code change: cleanup basic test data
2024-11-07 21:32:35 +13:00
Noemi Praml 59ed0bb015 fix delete contacts 2024-11-05 14:04:32 +01:00
Noemi Praml c0eb896e1f No productive code change: cleanup basic test data (#114)
* Erste Tests bereinigt

* fix

* noch mehr Tests bereinigt

* modify output

* reformat
2024-11-05 13:16:47 +01:00
Noemi Praml 3ab3f81fa3 Add failing concurrent test 2024-10-31 08:21:37 +01:00
Rob Bygrave ddb4b9d3e5 Remove deprecated QueryBean constructor that takes transaction, migrate to query.usingTransaction()
Remove the constructor for QueryBean that takes transaction. Migrate code to use query.usingTransaction() instead.
2024-10-31 18:22:43 +13:00
Rob Bygrave 7035b4c74c tests only: Update versions in test / test-kotlin maven plugin and apt 2024-10-31 18:19:36 +13:00
Rob Bygrave 85a575e353 Merge pull request #3513 from ebean-orm/feature/bump-parent-javadoc
Bump java11-oss parent and tidy javadoc
2024-10-31 17:14:52 +13:00
robin.bygrave eb05be286c Bump java11-oss parent and tidy javadoc 2024-10-31 14:55:43 +13:00
Rob Bygrave 9f5a77fdaa Merge pull request #3509 from FOCONIS/javadoc-fixes
RFC: Fix some javadoc issues
2024-10-31 14:47:46 +13:00
Roland Praml 42ee75f212 RFC: Fix some javadoc issues 2024-10-25 08:46:34 +02:00
Rob Bygrave 4610dbb26e Version 14.8.0 2024-10-25 14:28:58 +13:00
Rob Bygrave b26382f99c Merge pull request #3507 from ebean-orm/feature/agent-1480
Bump ebean-agent to 14.8.0
2024-10-25 14:18:20 +13:00
Rob Bygrave 2fba89f846 Bump ebean-agent to 14.8.0
This ebean agent is required to support the fluid QueryBuilder API change.
2024-10-25 14:17:46 +13:00
Rob Bygrave e4f13da868 Merge branch 'dynamic-property-group-by' 2024-10-24 16:13:01 +13:00
Rob Bygrave 32cd081fce #3504 Tidy test only and final method 2024-10-24 16:12:45 +13:00
Rob Bygrave e6f92eaceb ReadOnly Immutable query WIP 2024-10-24 16:02:50 +13:00
Rob Bygrave f6da332998 Add test for TestReflectiveModificationUpdate 2024-10-24 08:37:02 +13:00
Alexander Wagner d8f321d87b Fix: dynamic property group by with alias 2024-10-22 13:24:12 +02:00
Rob Bygrave c868d8a23b Fix test for Postgres TestDistinctOnQuery 2024-10-22 19:00:52 +13:00
Rob Bygrave d77c14d866 Fix test for Postgres TestDistinctOnQuery 2024-10-22 08:14:01 +13:00
Rob Bygrave 423d906848 Merge pull request #3499 from ebean-orm/feature/3489
Potentially breaking change: Throw PersistenceException for unknown property in select clause
2024-10-18 19:25:46 +13:00
Rob Bygrave 100d33cc94 Potentially breaking change: Throw PersistenceException for unknown property in select clause 2024-10-18 19:12:53 +13:00
Rob Bygrave 20cf6a9c00 Merge pull request #3498 from ebean-orm/feature/migrate-avaje-lang-to-jspecify
Migrate to JSpecify NullMarked/Nullable from avaje.lang NonNullApi/Nullable
2024-10-18 16:56:19 +13:00
Rob Bygrave e2c12268f7 Merge branch 'master' into feature/migrate-avaje-lang-to-jspecify 2024-10-18 16:52:53 +13:00
Rob Bygrave 937f6296e1 Merge pull request #3497 from ebean-orm/feature/fluid-self-querybuilder
Fix SELF of QueryBuilder, QueryBean, IQueryBean for fluid use
2024-10-18 16:48:26 +13:00
Rob Bygrave 24749567d2 Migrate to JSpecify NullMarked/Nullable from avaje.lang NonNullApi/Nullable 2024-10-18 16:46:41 +13:00
Rob Bygrave 7045240778 Merge branch 'transaction-readonly' 2024-10-18 16:27:18 +13:00
Rob Bygrave 6ceea85b94 Move markNotQueryOnly() into init transaction methods of PersistRequest 2024-10-18 16:26:49 +13:00
Rob Bygrave e04ac50800 Fix SELF of QueryBuilder, QueryBean, IQueryBean for fluid use
Currently, the SELF response type of QueryBuilder returns as Object when really we want it to return the SELF generic type of QueryBuilder, QueryBean, IQueryBean etc

Note that this change requires an updated ebean-agent
2024-10-18 16:18:39 +13:00
Noemi Praml a452fc0e21 call request.markNotQueryOnly 2024-10-17 16:30:42 +02:00
Noemi Praml 4438d5db7d Fixes in DefaultPersister 2024-10-17 16:19:19 +02:00
Noemi Praml 9ca6fa31e5 more tests 2024-10-17 16:19:03 +02:00
Rob Bygrave e0a00bea34 Merge pull request #3496 from mumbler6/fix-flaky
fixed flaky test in ToStringBuilderTest.java
2024-10-17 21:36:15 +13:00
mumbler6 b24b549298 fixed flaky test in ToStringBuilderTest.java 2024-10-15 22:41:51 -05:00
Rob Bygrave 51d0f4efcc Fix test TestOrderedList for Postgres ANY 2024-10-15 22:14:45 +13:00
Noemi Praml e08985d7c5 suggested solution 2024-10-14 16:53:48 +02:00
Noemi Praml 4f984ff2d4 add: failing test 2024-10-14 16:53:00 +02:00
Rob Bygrave b6843c6db0 Version 14.7.0 2024-10-10 23:48:29 +13:00
Rob Bygrave 57782c05b5 Update ebean-agent to 14.7.0 (Updates to ASM 9.7.1) 2024-10-10 23:46:37 +13:00
Rob Bygrave a2ee8b61f8 Merge pull request #3487 from FOCONIS/batch-ordering-bug
Bug: Batch ordering - Regression introduced in version 13.26.1, insertAll() leaves batchMode=true enabled ... when it should not
2024-10-10 22:31:15 +13:00
Rob Bygrave 1225a2776f Merge pull request #3485 from FOCONIS/ordercolumn-cache-queries
Fix orderBy for cached many requests
2024-10-10 22:30:23 +13:00
Rob Bygrave a499c8c26a Merge pull request #3493 from ebean-orm/feature/3490
#3490 filterMany generates invalid sql when the many side uses @EmbeddedId to model composite primary key
2024-10-10 22:27:58 +13:00
Rob Bygrave 8e627c756f #3490 filterMany generates invalid sql when the many side uses @EmbeddedId to model composite primary key 2024-10-10 22:24:24 +13:00
Rob Bygrave 45cc78bb0a #3490 filterMany generates invalid sql when the many side uses @EmbeddedId to model composite primary key 2024-10-10 22:11:20 +13:00
Rob Bygrave 38a8e95b36 #3491 Update javadocs and SqlQuery Postgres ANY tests 2024-10-10 20:08:03 +13:00
Rob Bygrave bca381104b Merge pull request #3491 from ajcamilo/master
Add setArrayParameter to SqlQuery
2024-10-10 20:07:08 +13:00
André Camilo 7e0c6795ff add setArrayParameter to SqlQuery 2024-10-09 13:24:52 +01:00
Roland Praml 8b4804be22 Fix: after DB.inserAll, transaction may stay in batchmode 2024-10-08 13:16:37 +02:00
Roland Praml a8418a3d75 Created test for batch escalation 2024-10-08 13:16:32 +02:00
Rob Bygrave bb0f63e38a Merge pull request #3488 from ebean-orm/dependabot/maven/ebean-test/commons-io-commons-io-2.14.0
Bump commons-io:commons-io from 2.7 to 2.14.0 in /ebean-test
2024-10-08 18:28:32 +13:00
Rob Bygrave 978ed26b78 Update ebean-agent 14.6.1 for #3484 2024-10-08 18:23:46 +13:00
Rob Bygrave 5cfa7295f8 Update tests for test-kotlin with latest agent 2024-10-08 18:17:08 +13:00
Roland Praml 1c0a811c01 Tenant support for query plans 2024-10-07 15:07:38 +02:00
Noemi Praml 88d8b4fb5e Fix orderBy for cached many requests 2024-10-07 15:04:46 +02:00
dependabot[bot] 93155d3c8f Bump commons-io:commons-io from 2.7 to 2.14.0 in /ebean-test
Bumps commons-io:commons-io from 2.7 to 2.14.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-03 18:46:07 +00:00
Rob Bygrave 6d5f740337 TestQueryPaging example to H2 and Postgres due to use of nulls first
nulls first isn't supports by mysql
2024-09-20 21:21:57 +12:00
Rob Bygrave bd06b57144 Add valhalla EA build 2024-09-18 20:49:40 +12:00
Rob Bygrave 77383f4d29 Version 14.6.0 2024-09-17 22:23:13 +12:00
Rob Bygrave 29fcbbfe97 Bump ebean-agent to 14.6.0 2024-09-17 22:09:44 +12:00
Rob Bygrave d9e0a35efb Merge pull request #3481 from ebean-orm/feature/boostrap-service
Use io.ebean.service.BootstrapService as common marker interface for service loading bootstrapped services
2024-09-17 22:03:52 +12:00
Rob Bygrave 0c01ecbac7 Merge pull request #3478 from ebean-orm/feature/3462
#3462 Add Paging as alternative to maxRows + firstRow + orderBy
2024-09-17 22:03:13 +12:00
Rob Bygrave 5095583cc8 Merge pull request #3473 from ebean-orm/feature/3455-query-beans-improvement
#3455 - Improve query beans such that filterMany() expressions are only on ToMany relationships
2024-09-17 22:02:40 +12:00
Rob Bygrave 92f04a2353 Use io.ebean.service.BootstrapService as common marker interface for service loading bootstrapped services 2024-09-17 21:58:19 +12:00
Rob Bygrave acbce35182 Merge pull request #3480 from ebean-orm/feature/emptylist
Change EmptyPagedList to be deprecated from public use
2024-09-16 21:07:40 +12:00
Rob Bygrave d6165f4b17 Change EmptyPagedList to be deprecated from public use
This should never have been public really
2024-09-16 20:58:40 +12:00
Rob Bygrave 95656360a9 Mark OrderBy constructors as deprecated
The plan is to migrate OrderBy into an interface
2024-09-15 19:21:17 +12:00
Rob Bygrave 65d7df5488 #3462 Add Paging as alternative to maxRows + firstRow + orderBy
Paging is an alternative to specifying the maxRows + firstRow + orderBy on a query.

Example:
```java

    var orderBy = OrderBy.of("lastName desc nulls first, firstName asc");
    var paging = Paging.of(0, 100, orderBy);

    DB.find(Contact.class)
      .setPaging(paging)
      .where().startsWith("lastName", "foo")
      .findList();
```
2024-09-12 22:54:35 +12:00
Rob Bygrave 12648cd37d Fix test TestQueryFilterMany 2024-09-12 20:31:37 +12:00
Rob Bygrave edbd13c075 Version 14.5.2 2024-09-11 20:58:13 +12:00
Rob Bygrave 0964980a69 Merge pull request #3474 from ebean-orm/feature/3453
#3453 - Different filterMany behavior when no matches found
2024-09-11 20:27:50 +12:00
Rob Bygrave dc7d349366 #3453 - Different filterMany behavior when no matches found 2024-09-10 23:11:44 +12:00
Rob Bygrave a2e66bcb89 #3455 - Improve query beans such that filterMany() expressions are only on ToMany relationships
Use protected helper methods on TQAssocBean for the filterMany() methods generated onto AssocMany beans.
2024-09-10 00:24:41 +12:00
Rob Bygrave 4d79815779 #3455 - Improve query beans such that filterMany() expressions are only on ToMany relationships
Changes the query bean code generation for "Associated beans" to have separate AssocOne and AssocMany for *ToOne and *ToMany relationships.

Moves the filterMany() expressions such that they are only available on the *ToMany relationships.
2024-09-09 23:26:13 +12:00
Rob Bygrave cbac00a89f Merge pull request #3472 from ebean-orm/feature/3461-take-2
#3461 - Add also() for query beans
2024-09-09 22:18:40 +12:00
Rob Bygrave 33b2c398f6 #3461 - Add also() for query beans 2024-09-09 22:13:04 +12:00
Rob Bygrave 416f50e782 Merge pull request #3471 from ebean-orm/feature/bump-agent
Bump ebean-agent, will support future improvement to query beans with…
2024-09-09 21:59:20 +12:00
Rob Bygrave 2e8a6ceb53 Bump ebean-agent, will support future improvement to query beans with AssocOne AssocMany 2024-09-09 21:54:09 +12:00
Rob Bygrave f0fd9003ad Version 14.5.1 2024-09-02 22:03:55 +12:00
Rob Bygrave dfeffb7c5e Bump ebean-agent, no effective change 2024-09-02 21:50:21 +12:00
Rob Bygrave 4b0fcb1d29 Merge pull request #3469 from ebean-orm/feature/orderBy2
#3466 - Duplicated entities in findMany when a ManyToOne relation is fetched
2024-09-02 20:53:42 +12:00
Rob Bygrave a7fe5d2b84 #3466 - Duplicated entities in findMany when a ManyToOne relation is fetched (Postgresql)
Internally in OrmQueryDetail.markQueryJoins() the SpiQueryManyJoin is determined and returned. This change uses this and effectively removes the code from OrmQueryRequest.determineMany() and BeanDescriptor.manyProperty() which was the source of this issue (performing a similar task but not correctly taking the full path into account and hence the source of this bug).
2024-09-02 19:28:11 +12:00
Rob Bygrave 30327fe33e Improve existing test asserts around order by clause (#3467) 2024-09-02 08:25:19 +12:00
Rob Bygrave 1e013331d4 Improve existing test asserts around order by clause 2024-09-02 08:19:02 +12:00
Rob Bygrave f8964707d9 #3463 - Ignore queryBean.select(null) calls (#3464) 2024-08-27 21:01:51 +12:00
Rob Bygrave 86c7de2de8 ebean-test: Modify postgis platform to support both net.postgis and og.postgis (#3449)
* ebean-test: Modify postgis platform to support both net.postgis and org.postgis

Use Class.forName() to test existence of net.postgis.jdbc.DriverWrapperLW and if available use that driver rather than the older org.postgis.DriverWrapperLW one.

Also support explicitly specifying the driver to use via test property - ebean.test.postgis.driver

* Update test for net postgis to use a separate container

* Remove NetPostgisSetup as PostgisSetup handles both cases now
2024-08-27 20:44:03 +12:00
Michael Benz 53b4d94933 Update README.md fixing broken Foconis logo (#3457)
Updated the Foconis AG logo with the new Foconis Analytics URL which was broken because of a deep link to a Yoomla logo that is no longer available.
2024-08-27 20:40:36 +12:00
Rob Bygrave b0ec23e53c Update tests to ensure ResetBasicData.reset() was called early 2024-08-04 18:59:46 +12:00
Rob Bygrave 20caae532c Adjust ResetBasicData with more output 2024-08-04 18:15:22 +12:00
Rob Bygrave 4369481c1a Adjust ResetBasicData with more output 2024-08-04 18:11:36 +12:00
Rob Bygrave c94426b6b4 Update tests config for ebean-net-postgis-types 2024-07-22 22:01:42 +12:00
Rob Bygrave b2ff32e88c Update tests config for ebean-net-postgis-types 2024-07-22 21:47:48 +12:00
Rob Bygrave 3bd8dfa7bf Update tests config for ebean-net-postgis-types 2024-07-22 20:12:27 +12:00
Rob Bygrave 0e7abdd137 Version 14.5.0 2024-07-22 08:33:34 +12:00
Ryszard Trojnacki 9135f2cd3e New Postgis types / Java modules (#3448)
* Added modules `ebean-net-postgis-types`, `ebean-net-postgis` for new `net.postgis` geometry types without support for GeoLatte.

* Added missing `uses` entry in `module-info.java` for `io.ebean.api`.
2024-07-22 07:31:30 +12:00
Rob Bygrave 6785d5703e Update tests for DB2 forUpdate() 2024-07-17 08:01:26 +12:00
Rob Bygrave 0a95ac7fa0 Update tests for DB2 forUpdate() 2024-07-16 22:18:29 +12:00
Rob Bygrave bbd5636689 #3444 - DBName with hyphens creates invalid code (#3447) 2024-07-16 20:22:08 +12:00
Noemi Praml 9f0eaef3d8 Db2 forupdate fix (#3446)
* ADD failing test for db2

* ADD: possible fix
2024-07-16 19:37:20 +12:00
Rob Bygrave a5d2a574d3 #3433 - Put test back from desc -> description as desc as a column name isn't widely supported by other databases 2024-07-08 21:54:35 +12:00
Rob Bygrave 6e7adbb8e4 #3433 - Entity property called desc used with query order by desc generates bad sql (#3438) 2024-07-04 00:05:01 +12:00
Rob Bygrave 8913bead89 [ebean-migration] #3434 - Connection is closed error running migration with MySql and Hikari (#3437) 2024-07-02 23:47:14 +12:00
Rob Bygrave cdf1cd8487 [ebean-redis] Fix for #3432 empty value for natural keys (#3436) 2024-07-02 22:06:57 +12:00
Rob Bygrave 2582c4b653 Bump to ebean-agent 14.5.0/15.5.0 with ASM 9.7 for JDK 22 and 23 support (#3435) 2024-07-02 18:08:08 +12:00
Rob Bygrave a9e3e2ea43 Version 14.4.0 2024-06-26 22:33:04 +12:00
Rob Bygrave 834ce971b3 Bump ebean-agent, no functional change 2024-06-26 22:19:58 +12:00
Rob Bygrave 5c27f89677 Change includeLabelInSql to default to true (#3431)
To disable this use ebean.includeLabelInSql=false or set this via
`DatabaseBuilder.includeLabelInSql(false)``
2024-06-26 20:32:19 +12:00
Rob Bygrave bfbbb7e7a7 Bump ebean-datasource dependency to 9.0 with automatic Lambda mode detection (#3430)
* Bump ebean-datasource dependency to 9.0 with automatic Lambda mode detection

ebean-datasource 9.0 automatically detects when it is running in AWS Lambda
and will set validateOnHeartbeat to false. In Lambda we don't want to validate
connections in background threads that can suspend in Lambda.
2024-06-26 20:27:49 +12:00
Rob Bygrave a8947c7d2f Bump ebean-migration dependency to 14.1.0 with fastMode enabled by default (#3429)
ebean-migration 14.1.0 defaults fastMode to true. This means that it first
runs a fast check that all migrations have been run. If that does not succeed
for any reason it runs the normal migration process.
2024-06-26 20:15:21 +12:00
Rob Bygrave d5401efb08 #3423 JtaTransactionManager keeps reference to already closed scope (and Transaction/connection ThreadLocal) when JtaTxnListener#afterCompletion is called by a different thread (#3424)
Bugfix for "Long/slow transaction reaper" in Wildfire that can close/rollback a transaction in a different thread. Fix is to use active flag to inactivate the transaction and detect that case in JtaTransactionManager.getCurrentTransaction()
2024-06-21 07:57:31 +12:00
Rob Bygrave 925ceaf441 Update build actions/setup-java@v4 2024-06-14 21:47:43 +12:00
Rob Bygrave c5fea13ca5 Update build actions/setup-java@v4 2024-06-14 21:42:06 +12:00
Rob Bygrave 6fa3759786 Add 23 to build 2024-06-14 21:34:54 +12:00
Rob Bygrave 1c5288b6ec Version 14.3.1 2024-06-10 19:26:40 +12:00
Rob Bygrave b485fb7440 Bump ebean-agent to 14.3.1 2024-06-10 19:02:41 +12:00
Rob Bygrave b07f909dd3 Bump parent pom to 4.2 with EA changes to annotation processing (#3417)
* Bump parent pom to 4.2 with EA changes to annotation processing

Annotation processing needs to be explicitly on in JDK 24 EA builds

* Bump bytebuddy version in ebean-test and use experimental=true for EA builds

* Use parent pom 4.3 with bytebuddy.experimental=true by default for surefire
2024-06-10 18:36:40 +12:00
Rob Bygrave fa92e7a4b7 Merge pull request #3416 from ebean-orm/feature/querybean-gen-suppresswarnings
Change QueryBean to use protected constructors, suppress warning on generated query beans
2024-06-10 12:39:53 +12:00
Rob Bygrave 1823ef288a Change QueryBean to use protected constructors, suppress warning on generated query beans 2024-06-07 22:48:04 +12:00
Rob Bygrave 7b2128b02f Merge pull request #3415 from ebean-orm/feature/3412
#3412 Use existing/parent transaction with fetch query on ToMany paths
2024-06-07 22:31:24 +12:00
Rob Bygrave 3c84a902e8 Followup for #3412 - Refactor DefaultBeanLoader move query creation logic into LoadBeanRequest
Move the query creation, using transaction and configure into LoadBeanRequest. This code then looks more like that of LoadManyRequest.
2024-06-07 22:26:48 +12:00
Rob Bygrave 3ce95c34cb #3412 Use existing/parent transaction with fetch query on ToMany paths
Fix such that LoadManyRequest uses the parent requests transaction rather
than obtain a new read only transaction to perform the fetch query.
2024-06-07 22:16:02 +12:00
Rob Bygrave d817dcb549 Merge pull request #3413 from ebean-orm/feature/fix-logging-naturalKey
Fix logging message for naturalKey MPUT to include the natural key properties
2024-05-31 21:57:24 +12:00
Rob Bygrave 5f218e1fdb Merge pull request #3409 from kaiyaok2/fix_NIO
Fixed non-idempotent tests in `TestCacheSerialization`
2024-05-31 21:56:33 +12:00
Rob Bygrave 59daf5689a Fix logging message for naturalKey MPUT to include the natural key properties 2024-05-31 21:52:38 +12:00
kaiyaok2 44efed5cd9 fixed non-idempotent tests 2024-05-26 23:57:12 +08:00
Rob Bygrave be28314e31 Update test kotlin to use kotlin-querybean-generator 14.3.0
Fixes the test as we now use QueryBean etc rather than TQRootBean
2024-05-16 00:05:55 +12:00
Rob Bygrave 053d82b54a Version 14.3.0 2024-05-14 22:10:08 +12:00
Rob Bygrave 0141e2effa Merge pull request #3401 from ebean-orm/feature/agent-14.3.0
Bump to ebean-agent 14.3.0
2024-05-14 21:18:12 +12:00
Rob Bygrave a33bb4d4c8 Bump to ebean-agent 14.3.0 2024-05-14 21:15:13 +12:00
Rob Bygrave d0348210be Merge pull request #3398 from ebean-orm/feature/NoTimeZone
mysql: Support "NoTimeZone" to bypass MySQL JDBC use of Timezone Calendar
2024-05-01 20:30:56 +12:00
Rob Bygrave 753cf66cf0 Merge pull request #3397 from ebean-orm/feature/distinctOn
Postgres: Add support for DISTINCT ON query clause
2024-05-01 20:30:41 +12:00
Rob Bygrave 4a8b4dd5fb mysql: Support "NoTimeZone" to bypass MySQL JDBC use of Timezone Calendar
Refer #3393 #3394
2024-05-01 20:24:10 +12:00
robin.bygrave 2faf7008db #3397 DISTINCT ON subquery support 2024-05-01 20:07:16 +12:00
Rob Bygrave 76c25cbe83 Postgres: Add support for DISTINCT ON query clause - fix test ebean.properties 2024-05-01 01:04:33 +12:00
Rob Bygrave 5c36a725f4 Postgres: Add support for DISTINCT ON query clause 2024-05-01 00:50:37 +12:00
Rob Bygrave b99d4f5a51 Merge pull request #3396 from ebean-orm/feature/limitOffset-with-labelComment
Include inline sql hint and comment in limit/offset sql
2024-04-30 23:57:04 +12:00
Rob Bygrave 7d18942357 Include inline sql hint and comment in limit/offset sql 2024-04-30 23:46:36 +12:00
Rob Bygrave e51d9ba2ab Fix test test-java16 using maven.compiler.release 2024-04-14 22:26:20 +12:00
Rob Bygrave 1b1dce90ed Merge pull request #3385 from ebean-orm/feature/rename-tqrootbean
Rename TQRootBean to QueryBean
2024-04-12 22:47:07 +12:00
Rob Bygrave 405834fa45 Merge branch 'master' into feature/rename-tqrootbean 2024-04-12 22:46:55 +12:00
Rob Bygrave 7720229af9 Merge pull request #3389 from ebean-orm/feature/bump-datasource-2
Bump ebean-datasource, ebean-test-containers, ebean-agent
2024-04-12 21:51:51 +12:00
Rob Bygrave 89a294414e Bump ebean-agent 14.2.1 2024-04-12 21:49:56 +12:00
Rob Bygrave 7bbfc00b86 Bump ebean-datasource and ebean-test-containers
ebean-datasource 8.14 has experimental lambda mode
ebean-test-containers has helper method to create S3Client for Localstack
2024-04-11 23:20:37 +12:00
Rob Bygrave d6c76de0a2 Merge pull request #3387 from ebean-orm/feature/bump-parent-4_1-byteBuddy
Bump parent to 4.1 with Byte-Buddy net.bytebuddy.experimental=true
2024-04-09 21:32:32 +12:00
Rob Bygrave 36f3986689 Bump avaje junit composite and bytebuddy to 1.14.13 2024-04-09 21:06:52 +12:00
Rob Bygrave f0f4f13ec6 Bump parent to 4.1 with Byte-Buddy net.bytebuddy.experimental=true
Uses maven-surefire-plugin with:
<argLine>-XX:+EnableDynamicAgentLoading -Dnet.bytebuddy.experimental=true</argLine>
2024-04-09 21:03:59 +12:00
Rob Bygrave 08466d42e9 Fix test TestLazyLoadMany with orderBy on query 2024-04-05 22:56:05 +13:00
Rob Bygrave 1f841982a5 Rename TQRootBean to QueryBean 2024-04-05 22:08:26 +13:00
Rob Bygrave b34704a1fa Fix kotlin test to use latest kotlin-querybean-generator 2024-04-05 00:29:17 +13:00
Rob Bygrave 0128d64841 Version 14.1.0 2024-04-04 22:54:23 +13:00
Rob Bygrave 68b313f778 Update ebean-agent to 14.1.0
This version allows for removal of query bean setRoot() which is no longer needed.
2024-04-04 22:42:06 +13:00
Rob Bygrave fad32c58d8 Merge pull request #3384 from ebean-orm/feature/rename-iquerybean
Rename QueryBean interface to IQueryBean (because we want to rename TQRootBean)
2024-04-04 22:36:59 +13:00
Rob Bygrave 8f1ebdb89a Rename QueryBean interface to IQueryBean (because we want to rename TQRootBean)
We want to rename TQRootBean to QueryBean. I can't do that right now because we need to make ebean-agent aware of that first. So yes, perhaps not ideal with IQueryBean but I think it will make sense once TQRootBean is renamed to QueryBean.
2024-04-04 22:35:55 +13:00
Rob Bygrave f7703efde2 Merge pull request #3383 from ebean-orm/feature/orderById
#3379 Followup, Move orderById() to common QueryBuilder interface
2024-04-04 22:09:34 +13:00
Rob Bygrave c5ffccb649 Merge pull request #3382 from ebean-orm/fix/3381-pc-clear
Fix for #3381 - Empty OneToMany when overflowing LazyLoadBatchSize an…
2024-04-04 22:08:57 +13:00
Rob Bygrave 3e65705bf9 #3379 Followup, Move orderById() to common QueryBuilder interface
Effectively this adds it to QueryBeans (where it was missing)
2024-04-04 22:05:17 +13:00
Rob Bygrave 7469a38bbc #3375 Followup, Temporarily add back setRoot() method to allow time for IntelliJ plugin to be released
Adding this means that it will work with an old ebean-agent. It takes a few days to get a new IntelliJ plugin released and we can't wait due to wanting to release the bug fix for #3381
2024-04-04 21:53:18 +13:00
Rob Bygrave 7ea4311662 Fix for #3381 - Empty OneToMany when overflowing LazyLoadBatchSize and mixed queries
14.0.0 via #3295 introduced this bug.

 #3295 introduced a behaviour where bulk updates clear the persistence context. Now the lazy loading of a BeanCollection works with an assumption that the "parent bean" is in the persistence context (and this assumption is broken with that change for this test case). That is, the bulk update is clearing the persistence context - removing the parent bean BEFORE the lazy loading is invoked ... and that doesn't then work because the parent bean isn't in the persistence context.

 This fix means that when lazy loading many's, the parent beans are putIfAbsent into the persistence context.
2024-04-04 20:13:20 +13:00
Rob Bygrave f556e04f58 Merge pull request #3380 from ebean-orm/feature/deprecate-update-transaction
Deprecate update(transaction) and delete(transaction) - migrate to usingTransaction(transaction)
2024-04-01 20:57:58 +13:00
Rob Bygrave d9c75a60a5 Deprecate update(transaction) and delete(transaction) - migrate to usingTransaction(transaction)
Migrate to usingTransaction(transaction) to set the transaction explicitly on a delete or update query.
2024-04-01 20:35:59 +13:00
Rob Bygrave 225bec7b28 Merge pull request #3379 from ebean-orm/feature/query-builder-interface
Add QueryBuilder interface as common API for Query and QueryBean
2024-04-01 19:42:34 +13:00
Rob Bygrave da44f7e604 Add QueryBuilder interface as common API for Query and QueryBean 2024-04-01 18:58:57 +13:00
lqbandRob Bygrave e0eda4f8be #3370 LimitOffsetPagedList add getTotalCount cache (#3373)
* #3370 LimitOffsetPagedList add getTotalCount cache

* Move totalRowCount check to inside the lock

* No change, format only on LimitOffsetPagedList

---------

Co-authored-by: Rob Bygrave <robin.bygrave@gmail.com>
2024-04-01 11:08:57 +13:00
Rob Bygrave e4e780588c Merge pull request #3378 from ebean-orm/feature/QueryBean-interface
Add QueryBean interface for query beans to implement
2024-04-01 10:59:38 +13:00
Rob Bygrave 7c8f235550 Add QueryBean interface for query beans to implement 2024-04-01 10:56:25 +13:00
Rob Bygrave 9a77ec1567 Merge pull request #3377 from ebean-orm/feature/3374
querybeans: #3374 - Add copy() method to TQRootBean
2024-04-01 09:09:24 +13:00
Rob Bygrave ff426319f5 Fix deprecated annotation for kotlin-querybean-generator 2024-04-01 09:02:13 +13:00
Rob Bygrave 7f56aff9dd Bump kotlin-querybean-generator 2024-03-31 11:49:58 +13:00
Rob Bygrave cdbdd98d36 querybeans: #3374 - Add copy() method to TQRootBean 2024-03-31 11:44:32 +13:00
Rob Bygrave 7bf030f59c Merge pull request #3376 from ebean-orm/feature/querybean-txn
Deprecate Query bean constructor that takes transaction - migrate to query.usingTransaction() instead
2024-03-31 11:38:26 +13:00
Rob Bygrave 16dca28831 Deprecate Query bean constructor that takes transaction - migrate to usingTransaction()
Migrate to query.usingTransaction() instead of the constructor that takes a transaction.
2024-03-31 11:37:32 +13:00
Rob Bygrave 678b43693d Merge pull request #3372 from ebean-orm/feature/1857-SlowQueryBindCapture
#1857 Add bind values capture for SlowQueryEvent / SlowQueryListener
2024-03-31 11:34:18 +13:00
Rob Bygrave 7ab93a0cdf Merge pull request #3375 from ebean-orm/feature/tidy-tqrootbean
Update TQRootBean with final methods, remove setRoot()
2024-03-31 11:32:54 +13:00
Rob Bygrave 951f942c73 Tidy TQRootBean constructor for alias.
Note that this requires the updated ebean-agent, where it will not longer use the setRoot() method which has now been removed.
2024-03-31 11:19:39 +13:00
Rob Bygrave cf2ec4b81d Modify TQRootBean making root final
Remove the unused setRoot() method
2024-03-31 10:02:41 +13:00
Rob Bygrave 7d1745ba53 Add final modifier to TQRootBean methods
As these are never expected to be overwritten
2024-03-31 09:57:42 +13:00
Rob Bygrave 7adbdbf532 feat: For SpringJdbcTransactionManager support using CurrentTenantProvider (#3368)
Note that this only works for Multi-Tenant PARTITION mode where the TenantId is a column on the tables (rather than schema or database multi-tenancy mode).
2024-03-26 22:45:39 +13:00
Rob Bygrave 40d9c88e34 #1857 Add bind values capture for SlowQueryEvent / SlowQueryListener
- changes SqlQueryEvent into an interface.
- captures the bind values the slow query used and makes those available via SlowQueryEvent
- also makes the query label and profileLocation available via SlowQueryEvent
2024-03-25 22:52:04 +13:00
1315 changed files with 58330 additions and 12343 deletions
+8 -5
View File
@@ -2,6 +2,7 @@
name: Build
on:
workflow_dispatch:
pull_request:
push:
branches: master
@@ -16,18 +17,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -39,5 +40,7 @@ jobs:
# - name: Maven single test
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
- name: Build with Maven
run: mvn -T 8 clean test
run: mvn -T 1C clean install -Pdefault
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
run: cd tests/test-java16 && mvn test
+5 -5
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: db2
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
+5 -5
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -37,5 +37,5 @@ jobs:
- name: Maven version
run: mvn --version
- name: H2Database
run: mvn -T 8 clean package
run: mvn -T 1C clean package
+3 -3
View File
@@ -20,14 +20,14 @@ jobs:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: oracle-actions/setup-java@v1
with:
website: jdk.java.net
release: ${{ matrix.java_version }}
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -37,5 +37,5 @@ jobs:
- name: Maven version
run: mvn --version
- name: Build with Maven
run: mvn -T 8 test
run: mvn test -Pea
+6 -6
View File
@@ -16,23 +16,23 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: mariadb 10.6
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
- name: mariadb 10.11
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
+4 -4
View File
@@ -13,18 +13,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
+4 -4
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11, 17, 21]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
+5 -5
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: mysql
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
+5 -5
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: oracle
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
+5 -5
View File
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: postgres
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
+4 -4
View File
@@ -13,18 +13,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
+6 -6
View File
@@ -16,23 +16,23 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: sqlserver 2017
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties
- name: sqlserver 2022
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
+41
View File
@@ -0,0 +1,41 @@
name: Valhalla EA
on:
workflow_dispatch:
schedule:
- cron: '39 2 * * 3'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [valhalla]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: oracle-actions/setup-java@v1
with:
website: jdk.java.net
release: ${{ matrix.java_version }}
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: Maven version
run: mvn --version
# - name: Prepare
# run: ./jakarta-to-valhalla.sh
- name: Build with Maven
run: mvn package
+5 -5
View File
@@ -4,7 +4,7 @@ name: Yugabyte
on:
workflow_dispatch:
schedule:
- cron: '10 3 * * *'
- cron: '10 3 * * 3'
jobs:
build:
@@ -16,18 +16,18 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
+3
View File
@@ -13,6 +13,9 @@ ebean-profiling*.xml
profiling/
.DS_Store
# Local Redis integration test credentials
ebean-redis/src/test/resources/redis-local.yml
# Intellij project files
*.iml
*.ipr
+14 -6
View File
@@ -3,6 +3,7 @@
[![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
[![Multi-JDK Build](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
[![GraalVM Native Image](https://img.shields.io/badge/GraalVM-Native%20Image%20Ready-darkgreen?logo=graalvm)](https://www.graalvm.org/)
##### Build with database platforms
[![H2Database](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
@@ -56,12 +57,7 @@ Work at the highest level of abstraction and drop down levels as needed.
<tr>
<td align="center" valign="middle">
<a href="https://www.foconis.de/" target="_blank">
<img width="222px" src="https://www.foconis.de/templates/yootheme/cache/foconis_logo_322-709da1de.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.payintech.com/" target="_blank">
<img width="222px" src="https://ebean.io/images/sponsor_PayinTech-logo-noir.png">
<img width="222px" src="https://group.foconis.com/download/ci/logo/png-72dpi/logo-quer/foconis-analytics-quer.png">
</a>
</td>
<td align="center" valign="middle">
@@ -85,6 +81,18 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
## Documentation
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
## Guides
Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
Available guides:
- [Maven POM setup](docs/guides/add-ebean-postgres-maven-pom.md)
- [Database configuration](docs/guides/add-ebean-postgres-database-config.md)
- [Test container setup](docs/guides/add-ebean-postgres-test-container.md)
- [DB migration generation](docs/guides/add-ebean-db-migration-generation.md)
- [Lombok with Ebean entity beans](docs/guides/lombok-with-ebean-entity-beans.md)
## Maven central
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-net-postgis</name>
<description>ebean-net-postgis composite</description>
<artifactId>ebean-net-postgis</artifactId>
<properties>
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgres.jdbc.version}</version>
<exclusions>
<!-- exclude unnecessary checker framework -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>${postgis.jdbc.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,7 @@
package io.ebean.postgis.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -0,0 +1,10 @@
module io.ebean.postgis {
requires transitive io.ebean.api;
requires transitive io.ebean.core;
requires transitive io.ebean.datasource;
requires transitive io.ebean.querybean;
requires transitive io.ebean.platform.postgres;
// requires transitive io.ebean.postgis.types;
}
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-pgvector</name>
<description>ebean-pgvector composite</description>
<artifactId>ebean-pgvector</artifactId>
<properties>
<pgvector.version>0.1.6</pgvector.version>
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgres.jdbc.version}</version>
<exclusions>
<!-- exclude unnecessary checker framework -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.pgvector</groupId>
<artifactId>pgvector</artifactId>
<version>${pgvector.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,7 @@
package io.ebean.pgvector.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -0,0 +1,9 @@
module io.ebean.pgvector {
requires transitive io.ebean.api;
requires transitive io.ebean.core;
requires transitive io.ebean.datasource;
requires transitive io.ebean.querybean;
requires transitive io.ebean.platform.postgres;
}
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>14.0.2</version>
<version>18.3.0</version>
</dependency>
</dependencies>
+3 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
</parent>
<artifactId>composites</artifactId>
@@ -24,6 +24,8 @@
<module>ebean-oracle</module>
<module>ebean-postgres</module>
<module>ebean-postgis</module>
<module>ebean-net-postgis</module>
<module>ebean-pgvector</module>
<!-- <module>sqlanywhere</module>-->
<module>ebean-sqlite</module>
<module>ebean-sqlserver</module>
+252
View File
@@ -0,0 +1,252 @@
# Ebean ORM Library Definition
Ebean is an ORM library for Java and Kotlin focused on relational data access, type-safe query construction, and production-friendly SQL behavior.
## Identity
- **Name**: Ebean ORM
- **Package**: `io.ebean`
- **Primary Maven Group**: `io.ebean`
- **Category**: ORM / Data Access
- **Repository**: https://github.com/ebean-orm/ebean
- **Issues**: https://github.com/ebean-orm/ebean/issues
- **Discussions**: https://github.com/ebean-orm/ebean/discussions
- **Website**: https://ebean.io/
- **Documentation**: https://ebean.io/docs/
- **License**: Apache 2.0
## Version & Requirements
- **Repository Version (this checkout)**: `16.5.0` (from repository `pom.xml`)
- **Minimum Java Version**: 11+
- **Languages**: Java, Kotlin
- **Build Tooling in this docs set**: Maven-focused examples
## Core Artifacts
| Artifact | Purpose |
|------|------|
| `io.ebean:ebean` | Core ORM runtime and API |
| `io.ebean:ebean-postgres` | PostgreSQL platform bundle used in setup guides |
| `io.ebean:ebean-test` | Test support, including Docker-backed database testing |
| `io.ebean:querybean-generator` | Generates `Q*` type-safe query beans |
| `io.ebean:ebean-maven-plugin` | Bytecode enhancement for entities at build time |
| `io.ebean:ebean-migration` | Runtime migration runner (often transitive via platform artifact) |
## Core APIs & Annotations
### Database and transaction APIs
| API | Purpose | Example |
|------|------|------|
| `DB.getDefault()` | Access default `Database` | `Database db = DB.getDefault();` |
| `DB.byName("...")` | Access named `Database` | `Database reporting = DB.byName("reporting");` |
| `database.find(...)` | Query entities | `Customer c = database.find(Customer.class, id);` |
| `database.insert/save/update/delete` | Persist entity changes | `database.save(customer);` |
| `database.beginTransaction()` | Manual transaction boundary | `try (Transaction txn = database.beginTransaction()) { ... }` |
| `Database.builder()` | Programmatic `Database` setup | `Database.builder().loadFromProperties().build();` |
### Query APIs
| API | Purpose | Example |
|------|------|------|
| `Q*` query beans | Type-safe query construction | `new QCustomer().status.equalTo(ACTIVE).findList();` |
| `exists()` | Efficient existence checks | `new QCustomer().email.equalTo(email).exists();` |
| `findOne()` | Unique/single-row retrieval | `new QCustomer().id.equalTo(id).findOne();` |
| `findList()` | List retrieval | `new QCustomer().findList();` |
| `asDto(...).findList()` | Flat DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
| `mapTo(...).findList()` | Nested DTO graph projection reads | `new QCustomer().mapTo(CustomerDto.class).findList();` |
### Entity mapping and lifecycle annotations
| Annotation | Purpose |
|------|------|
| `@Entity` | Marks class as persistent entity |
| `@Id` | Primary key mapping |
| `@Version` | Optimistic locking |
| `@WhenCreated` | Creation timestamp management |
| `@WhenModified` | Modification timestamp management |
| `@Transactional` | Declarative transaction boundary |
## Capabilities
### ✅ Included
- Relational ORM with automatic dirty checking and lazy loading (via enhancement)
- Multiple query abstraction levels (ORM query, DTO query, SQL/JDBC)
- Type-safe query beans (`Q*`) with IDE autocomplete
- Built-in migration generation and migration running support
- Transaction APIs for implicit, declarative, and explicit transaction control
- Support for test-time Docker database workflows
- Query tuning and caching features for performance-sensitive workloads
### ❌ Not in scope
- HTTP routing, REST controllers, or web server runtime
- Dependency injection container functionality
- JSON serialization framework responsibilities
- Front-end/UI rendering concerns
Ebean is intentionally focused on persistence and data access. Pair it with a web framework and DI library as needed.
## Use Cases
### ✅ Strong fit
- SQL-backed business applications with rich domain models
- Services that need both ORM productivity and SQL-level control
- Projects requiring type-safe query authoring via generated query beans
- Teams that want migration generation integrated with entity model changes
- Integration test suites that need real database behavior (not only in-memory mocks)
### ⚠️ Consider alternatives if
- You need a full web framework (routing/controllers) rather than a persistence layer
- Your project does not use relational databases as a core storage model
- You want a single library to cover persistence, DI, and HTTP all at once
## Quick Start (Maven)
```xml
<properties>
<ebean.version><!-- use latest stable from Maven Central --></ebean.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>${ebean.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
```
## Minimal Example
```java
import io.ebean.DB;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
class Customer {
@Id
private long id;
private String name;
public void setName(String name) {
this.name = name;
}
}
Database database = DB.getDefault(); // or injected
Customer customer = database.find(Customer.class, 42);
customer.setName("Updated");
database.save(customer);
```
## Common Tasks & Guides
| Task | Guide |
|------|------|
| Add Ebean to an existing Maven project | [add-ebean-postgres-maven-pom.md](guides/add-ebean-postgres-maven-pom.md) |
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
| Map nested entity graphs to DTO graphs | [mapping-entity-graphs-to-dtos.md](guides/mapping-entity-graphs-to-dtos.md) |
| Persist changes and manage transactions | [persisting-and-transactions-with-ebean.md](guides/persisting-and-transactions-with-ebean.md) |
| Build test entities quickly | [testing-with-testentitybuilder.md](guides/testing-with-testentitybuilder.md) |
**Guides index**: [guides/README.md](guides/README.md)
## Related Ecosystem Docs
- [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)
- [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)
- [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)
## AI Agent Instructions
### For Claude, GPT, and web-based agents
Use this file as the top-level reference when answering Ebean questions.
1. Check this file first for scope and capability fit.
2. Route implementation tasks to the relevant guide in **Common Tasks & Guides**.
3. Treat Ebean as the persistence layer only; avoid implying it provides HTTP/DI features.
4. Prefer type-safe query bean examples when showing query code.
5. For setup and migration changes, follow the Maven-focused guide steps exactly.
### For IDE-based agents (Copilot, Cursor, etc.)
If `docs/LIBRARY.md` is not in context automatically:
1. Read `README.md` for docs entry points.
2. Open `docs/guides/README.md` for task-specific guides.
3. Follow linked guide files directly for concrete implementation steps.
---
## Notes for Maintainers
### When to update this file
- New release that changes requirements or key APIs
- New guide added to `docs/guides/`
- Capability/scope changes that affect "Included" or "Not in scope"
- Significant migration or setup workflow changes
### Maintenance checklist
- [ ] Keep requirements and version references accurate
- [ ] Keep Common Tasks table aligned with `docs/guides/README.md`
- [ ] Keep artifact names/snippets aligned with setup guides
- [ ] Keep AI instructions aligned with current docs structure
### Link from repository README
In `README.md`, include:
```markdown
## Documentation
- [Ebean docs](https://ebean.io/docs/)
- [Library reference](docs/LIBRARY.md)
- [Step-by-step guides](docs/guides/README.md)
```
+990
View File
@@ -0,0 +1,990 @@
# Nested DTO Mapping — API Design
Design spike for the accepted requirements in [dto-mapping-requirements.md](./dto-mapping-requirements.md),
covering issue #2540. This captures the concrete API shape, annotations, and open-question decisions made
during design review — before implementation begins.
## Two source-vs-target mapping pipelines
Ebean now has (or will have) two distinct DTO pipelines. It's important callers can tell which one they're
using:
1. **`asDto(Dto.class)`** (existing) — a `DtoQuery`, executed directly against a flat SQL `ResultSet`.
One row -> one DTO, via constructor/setter matching. No nested ToOne/ToMany support, no entity graph
involved.
2. **`mapTo(Dto.class)`** (new) — runs the normal ORM entity query (joins/fetches as usual), producing an
*unmodifiable entity graph*, then maps that Java object graph into a DTO graph. Supports nested
ToOne/ToMany, identity-aware de-duplication, and derives its own fetch spec from the DTO shape.
## Proposed API
```java
// Existing flat DtoQuery pipeline — unchanged
new QUser().valid.eq(true)
.select(firstName, lastName)
.asDto(UserInfo.class)
.findList();
```
```java
// NEW: nested DTO graph pipeline
public class CustomerDto {
Long id;
String name;
AddressDto billingAddress; // ToOne -> nested DTO, matched by property name "billingAddress"
List<ContactDto> contacts; // ToMany -> nested DTO list, matched by property name "contacts"
@DtoPath("billingAddress.line1")
String billingLine1; // renamed / flattened path
}
public class ContactDto {
Long id;
String firstName;
String lastName;
@DtoRef
Long customerId; // id-only back-reference, avoids re-embedding CustomerDto (cycle)
}
List<CustomerDto> dtos = new QCustomer()
.status.eq(Status.ACTIVE)
.mapTo(CustomerDto.class)
.findList();
```
Computed values (e.g. `cityOrUnknown` derived from `coalesce(billingAddress.city, 'Unknown')`) are
**not** modeled with a `@Formula2` annotation directly on the DTO - that was explored and rejected
(see "Formula2-on-DTO scope" below). Instead they're modeled as a plain matching field on the DTO,
sourced from an `@Entity @View` entity that itself carries the `@Formula2` - see "Computed/aggregate
properties" below for the worked example.
`mapTo(CustomerDto.class)`:
- Introspects `CustomerDto` (recursively, at codegen time) to derive the `select(...)`/`.fetch(...)` spec
automatically from the DTO's declared shape.
- Forces `setUnmodifiable(true)` under the hood — gives fail-fast + a cheap, non-mutable source graph
(satisfies the fail-fast requirement without a separate flag).
- Runs the query, then runs a mapper over the resulting entity graph, de-duplicating DTO instances by id
for repeated nested references (identity-aware, mirrors the source entity graph's own de-duplication).
## Decisions made
### Fetch spec: auto-derived from DTO shape
The DTO's declared structure (fields, nested DTO types, `@DtoPath` overrides) is the single source of truth
for what gets selected/fetched from the database. Callers do not need to separately maintain a `.fetch(...)`
spec in parallel with the DTO — this directly addresses the original issue's pain point (DTO and query
projection drifting out of sync).
### Entry point naming: `mapTo(Dto.class)`
Chosen over `asGraph(...)` / `into(...)` / overloading `findList(Class)`. Reads clearly as "map the
resulting entity graph to this DTO type" and is unambiguous against the existing `asDto(...)` (flat,
SQL-row-based) mechanism.
### Cycle handling: codegen-time DAG check + `@DtoRef` escape hatch
Because the fetch spec and mapper are both derived from the *static* DTO type graph (not live object
traversal), cycle detection is a compile-time/codegen-time concern, not a runtime one. This is stronger
than the common approach in the ecosystem:
- **MapStruct** does not auto-detect cycles. It offers an opt-in `@Context` "cycle guard" pattern (an
identity map of already-mapped source -> target objects) that the developer must wire up manually to
avoid infinite recursion mapping bidirectional object graphs.
- **Blaze-Persistence / QueryDSL / JOOQ record mapping** avoid the problem architecturally: view/projection
types are required to be a strict tree; a back-reference is modeled as an id or a much shallower type,
never the same full view type again.
Ebean's approach: fail the build at annotation-processing time if a DTO's declared type graph is not a DAG,
with a clear error message. Provide `@DtoRef` as an explicit escape hatch for intentional back-references
(e.g. `Contact.customer`) — it maps only the id, not the full nested DTO, breaking the cycle by design
rather than by runtime guard.
### `@DtoPath` / `@DtoRef`: parallels for readers coming from MapStruct or Blaze-Persistence
Neither annotation is a novel concept - both map onto things MapStruct and Blaze-Persistence users will
already recognise, which is worth spelling out explicitly so it's easy to "grok fast":
- **`@DtoPath("billingAddress.line1")` is Ebean's equivalent of MapStruct's dot-path `source` flattening**
— e.g. `@Mapping(target = "line1", source = "billingAddress.line1")`. MapStruct auto-generates a
null-safe chain of getter calls for a dotted `source`; `@DtoPath` does exactly the same thing, just
declared on the DTO field itself rather than on a mapper method parameter list. It is also close to
Blaze-Persistence's `@Mapping("billingAddress.line1")` on an `@EntityView` attribute, which is a JPQL
path expression evaluated the same way — Blaze's placement (directly on the target view property) is
actually the closer analogue of the two, since Ebean's `@DtoPath` is likewise placed on the DTO field.
The difference from Blaze: `@DtoPath` is restricted to plain getter-chain navigation (no arbitrary JPQL/
SQL expression) - see "Formula2-on-DTO scope" below for the boundary and why full expression support is
deliberately deferred.
- **`@DtoRef` has no dedicated equivalent in either tool** - both MapStruct and Blaze would express the
same "just the id" mapping as a plain dot-path to `.id` (`@Mapping(source = "customer.id")` / Blaze
`@Mapping("customer.id")`), with no special marker for it. What `@DtoRef` adds beyond that shorthand is
*intent*: it tells the codegen this property is a deliberate cycle-breaking reference, so (a) it adds
just the association's own name (not a dotted `.id` path) to the generated fetch spec's root
`select(...)` - reading the FK column directly with no join, and skipped entirely if that same
association is already fully fetched by a `NESTED_ONE`/`NESTED_MANY` property elsewhere on the same DTO
(see `DtoMapperWriter.fetchGroupChainCalls()`'s `case REF` branch) - and (b) it participates in the
codegen-time DAG cycle check above as an explicit "this is fine, don't flag it" signal, rather than
requiring a suppression escape hatch bolted on afterwards.
**Bug found and fixed while building the aggregation worked example below:** the original implementation
excluded `REF` properties from the fetch spec *entirely*, on the assumption the id is "already available
off an unfetched reference without triggering a fetch/lazy load". That assumption is only true when some
*other* property on the same DTO happens to also fetch that association (as was always the case in the
existing hand-built examples). Tested directly against a bare `@ManyToOne` with no other fetch of it:
accessing `.getCustomer().getId()` in that case triggers a full lazy-reload of the owning row (extra SQL,
not free) - and for an aggregation query it's worse, since the property being grouped by must be selected
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
to `InterceptReadWrite` (the mutable/updatable variant), which additionally carries a `ReentrantLock`, four
transient collaborator references (`NodeUsageCollector`, `PersistenceContext`, `BeanLoader`,
`PreGetterCallback`), a `byte[] flags` array (per-property loaded+changed+dirty+orig-value-set state),
`Object[] origValues`, `Exception[] loadErrors`, `MutableValueInfo[]`/`MutableValueNext[]`, and several more
scalar bookkeeping fields. None of that is needed for a bean that will only ever be read, so
`setUnmodifiable(true)` graphs carry meaningfully less per-instance overhead than normal fetched entities -
relevant here because `mapTo(Dto.class)` forces `setUnmodifiable(true)` on its underlying query, making the
*source* graph for a DTO mapping cheaper than the equivalent normal (writable) entity graph would be.
### Ad-hoc computed/formula properties: model as `@Entity @View`/`@Sql`, not ad-hoc SQL-on-DTO
The "fully ad-hoc SQL-on-DTO" stretch goal above (closer to Blaze's arbitrary `@Mapping` expressions) doesn't
need to be built as a bespoke DTO-annotation-processing feature. Ebean already supports modelling read-only,
computed, or view-backed data as ordinary entities via `@Entity` + `@View` (backed by a SQL view, e.g. one
with aggregates/computed columns) or `@Entity` + `@Sql` (backed by arbitrary `RawSql`, no base table). Given
that, the Blaze-Persistence-style "an entity view attribute backed by an arbitrary SQL expression" need can
usually be satisfied by:
1. Modelling the computed/derived shape as its own `@Entity @View` (or `@Sql`) "read entity" - the SQL
expression/aggregation lives in the view definition, not in a new annotation-processed DTO mechanism.
`@View`'s `name()` doesn't have to point at a genuinely separate database view - it can just point at
an *existing* table (e.g. `@View(name = "contact")` on a second entity class reading the same table as
`Contact`) purely to mark the entity as view-like/read-only, in which case Ebean's DDL generator emits
**no new table or view at all** for it - it's just a second lens onto the same physical data.
2. Mapping *that* entity into a plain DTO using the existing, already-implemented `@DtoMapping` machinery -
no ad-hoc-SQL-on-DTO support required, since there's no computed expression left to resolve at the DTO
layer at all; it's just another entity-to-DTO mapping.
3. This read entity benefits from the same `setUnmodifiable(true)`/`InterceptReadOnly` memory efficiency
above when used purely as `mapTo(...)` input, so there's no meaningful cost to preferring this over a
hypothetical native ad-hoc-SQL-on-DTO feature.
This significantly narrows (and may eliminate) the case for a dedicated ad-hoc-SQL-on-DTO mechanism - it
remains listed as an open stretch goal below primarily for the case where a computed value's SQL is genuinely
one-off/DTO-specific and not worth promoting to a standalone `@View`/`@Sql` entity.
**Worked example** (`tests/test-dto-mapping`): `ContactSummary` is `@Entity @View(name = "contact")` (no new
DDL - reads the same table as `Contact`) with `@Formula2("concat(firstName, ' ', lastName)")` computing
`fullName`; `ContactSummaryDto` is a plain two-field DTO; `@DtoMapping(source = ContactSummary.class, target
= ContactSummaryDto.class)` generates `ContactSummaryDtoMapper` exactly like any other entity→DTO pair - the
formula property is just selected like any other field (`select("id,fullName")` in the generated
`fetchGroup()`). See `TestContactSummaryDtoMapping`.
### Aggregate/group-by computed properties: `@Sum`/`@Aggregation` as `@Entity @View`, same pattern
Ebean's `@Sum` (shorthand for `@Aggregation("sum($1)")`) and `@Aggregation("count(...)"/"sum(...)"/"avg(...)"/
"min(...)"/"max(...)")` are the group-by parallel to the formula pattern above - the same `@Entity @View`
approach applies, just with an implicit `GROUP BY` instead of a per-row computed column. Ebean auto-derives
the `GROUP BY` clause from whichever non-aggregate properties end up in the query's `select()`/`fetch()` - so
a second `@Entity @View(name = <same base table>)` entity with one or more `@Sum`/`@Aggregation` properties
plus a `@ManyToOne` grouping key becomes a per-parent rollup, with **no new table/view and no explicit
`.groupBy()` call required**. This is Ebean's parallel to Blaze-Persistence entity view correlated aggregate
mappings, e.g. `@Mapping("SIZE(contacts)")` / `@Mapping("SUM(contacts.engagementScore)")` on an `@EntityView`.
**Nuance found while building the worked example, and since fixed: `@DtoRef` originally didn't fit the
grouping key.** `@DtoRef` was originally excluded from the generated `select()`/`fetch()` spec entirely, on
the premise that the id is already available off an unfetched reference for an ordinary entity graph. That
premise doesn't hold for an aggregation query: the `@ManyToOne` *is* the property being grouped by, so if
it's never selected, the query has nothing to group by. It turned out the premise didn't fully hold for
ordinary entity graphs either - see the `@DtoRef` bug writeup above. Fixed so `@DtoRef` now adds the
association's own name to the root `select(...)` (reading the FK column directly, no join) - which both
supplies the grouping key here and fixes the general-case gap.
**Worked example** (`tests/test-dto-mapping`): `ContactStats` is `@Entity @View(name = "contact")` (no new
DDL - reads the same table as `Contact`/`ContactSummary`) with `@Aggregation("count(id)") contactCount` and
`@Sum Integer engagementScore` (a new nullable field added to `Contact` purely to have something to sum),
grouped by its `@ManyToOne customer`. `ContactStatsDto` is a flat 3-field DTO (`customerId`, `contactCount`,
`engagementScore`), with `customerId` mapped via plain `@DtoRef`. The generated `ContactStatsDtoMapper`:
```java
this.fetchGroup = FetchGroup.of(ContactStats.class)
.select("customer,contactCount,engagementScore")
.build();
...
// skip DtoMapContext, only ever a top-level mapping
return new ContactStatsDto(
(source.getCustomer() == null ? null : source.getCustomer().getId()),
source.getContactCount(),
source.getEngagementScore());
```
confirmed (via `LoggedSql`) to produce `select t0.customer_id, count(t0.id), sum(t0.engagement_score) from
contact t0 ... group by t0.customer_id` - **no join**, one row per customer, correctly summed and counted.
See `TestContactStatsDtoMapping`.
### Formula2-on-DTO scope (v1): existing entity formulas only
`@Formula2` on a DTO property in v1 only pulls in a formula **already declared on the source entity** (or
a reachable associated entity) — it does not support fully ad-hoc SQL declared directly on the DTO with no
matching entity property. Fully ad-hoc SQL-on-DTO (closer to Blaze's arbitrary `@Mapping` expressions) is a
separate, larger stretch goal to revisit once the core graph-mapping mechanism is proven.
**Attempted and rejected for v1.** A narrower version was implemented (`@Formula2(value)` resolved exactly
like `@DtoPath` - a dot-path getter chain - plus a codegen-time validation that the resolved entity property
is itself `@Formula2`/`@Formula`-annotated) but was rejected: for the common case (a DTO field with the same
name as the entity's formula property) it generated **identical code to a plain unannotated field** - the
only difference was the validation, which wasn't judged enough distinct value to justify a new annotation
surface. Not implemented. The only way `@Formula2`-on-DTO would add real value is the full ad-hoc-SQL
capability described above, which remains an open stretch goal.
### Mapper implementation strategy: codegen, not reflection (native-image constraint)
Native-image support is a core Ebean requirement, so the entity-graph -> DTO-graph mapper must not rely on
runtime reflection or `MethodHandles`. This ruled out an initial reflection-based spike:
- Ebean's existing flat `DtoQuery` (`DtoMetaConstructor`) already uses `MethodHandles` via
`Lookups.getLookup()`, but there is no `reflect-config.json` / native-image reachability metadata shipped
for it anywhere in the repo. That existing approach is not a clean precedent to copy for a bigger,
native-image-first feature.
- Instead, the approach mirrors `querybean-generator`, which already generates real `.java` source for
`Q*` query bean types (not reflection) — consistent with the wider avaje-ecosystem convention
(avaje-inject / avaje-jsonb are explicitly reflection-free via compile-time codegen).
**Implementation sequencing:** hand-write the mapper in the exact shape the annotation processor will
eventually generate (plain Java, direct getter/constructor/setter calls, zero reflection) for one concrete
example first, to validate the mapping algorithm and API shape quickly without ever introducing throwaway
reflective code. That hand-written mapper then becomes the target/acceptance-test shape for the
`querybean-generator` annotation processor that automates producing it.
### Codegen target: Java first
The mapper generation (requirement r2) targets `querybean-generator` (the existing APT module that already
generates `Q*` query beans, reusing its `PropertyMeta` / `ProcessingContext` machinery). Kotlin parity via
`kotlin-querybean-generator` is deferred to a later phase — not blocking initial delivery.
### Mapper composition: one mapper per entity/DTO pair, generic `DtoMapper<SOURCE, TARGET>` interface
Rather than one large mapper inlining every nested DTO type, each entity/DTO pair gets its own small
mapper class - mirroring MapStruct's per-type mapper generation. All mappers implement a shared generic
interface (prototyped as `org.tests.dtomapping.DtoMapper<SOURCE, TARGET>` in the spike, expected to move to
`io.ebean` as a public type once solidified):
```java
public interface DtoMapper<SOURCE, TARGET> {
TARGET map(SOURCE source);
default List<TARGET> mapList(List<SOURCE> source) { ... }
}
```
A parent mapper composes nested mappers via **constructor injection**, not a static singleton:
```java
public final class CustomerDtoMapper implements DtoMapper<Customer, CustomerDto> {
private final DtoMapper<Address, AddressDto> addressMapper;
public CustomerDtoMapper() {
this(new AddressDtoMapper());
}
public CustomerDtoMapper(DtoMapper<Address, AddressDto> addressMapper) {
this.addressMapper = addressMapper;
}
@Override
public CustomerDto map(Customer source) {
if (source == null) return null;
return new CustomerDto(source.getId(), source.getName(), addressMapper.map(source.getBillingAddress()));
}
}
```
Rationale:
- **Composability & reuse** - the same nested DTO type (e.g. `AddressDto`) used from multiple parent DTOs
reuses one generated mapper class rather than duplicating inline mapping logic.
- **Constructor injection over static state** - avoids a global mutable singleton; a no-arg constructor
gives the common case (default nested mapper), while an overload accepting the nested mapper explicitly
allows substitution (tests, customization) without touching global state.
- **Codegen-friendly** - this shape generates naturally: one top-level mapper class per DTO type, each
constructor-injecting the mappers for any nested DTO types it references.
## ToMany collections and identity de-duplication (dto-spike-tomany-identity)
Extending the spike (`ebean-test/src/test/java/org/tests/dtomapping/`) to a `Customer` with a
`List<Contact> contacts` ToMany, where each `Contact` has a `customer` back-reference, surfaced
two things worth recording.
### The `DtoMapper` interface threads a shared context
`DtoMapper<SOURCE, TARGET>` was extended so that mapping is always done against a `DtoMapContext`:
```java
public interface DtoMapper<SOURCE, TARGET> {
TARGET map(SOURCE source, DtoMapContext context);
default TARGET map(SOURCE source) {
return map(source, new DtoMapContext());
}
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) { ... }
default List<TARGET> mapList(List<SOURCE> source) {
return mapList(source, new DtoMapContext());
}
}
```
`DtoMapContext` is an identity-keyed cache of already-mapped source -> target instances, created
once per top-level `mapList(...)`/`map(...)` call and threaded through every nested `map(...)`
call. This lets repeated references to the *same* source entity instance - which Ebean's own
persistence context already de-duplicates within one query (`contact.getCustomer() == customer`
for the enclosing `Customer`, confirmed by an existing test) - map to the *same* target DTO
instance, rather than each producing an equal-but-distinct copy. This is what makes the mapped
DTO output "graph shaped" rather than "tree of copies shaped", and is required for r1/r3.
### Bug found and fixed: the cache must be partitioned by target type, not just source identity
The first cut of `DtoMapContext` was a single `IdentityHashMap<Object, Object>` keyed only by the
source instance. This breaks as soon as the *same* source instance legitimately needs to map to
*two different target types* within one graph - which happens immediately with a back-reference:
- The top-level `CustomerDtoMapper` maps a `Customer` -> full `CustomerDto`.
- The nested `ContactDtoMapper`, mapping `contact.getCustomer()` (the *same* `Customer` instance,
by identity), maps it -> shallow `CustomerRefDto` (the `@DtoRef`-style escape hatch that avoids
the `Customer -> Contact -> Customer` cycle).
With a single un-partitioned identity map, whichever mapper runs first "wins" the cache slot for
that `Customer` instance, and the other mapper incorrectly receives the wrong-typed cached result
(a `ClassCastException` at best, silently wrong data at worst). This was caught by a failing test
during the spike and fixed by partitioning the cache per target type:
```java
public final class DtoMapContext {
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
// ... existing/create/put ...
}
}
```
Each generated mapper passes its own target DTO `Class` as the first argument, so `Customer ->
CustomerDto` and `Customer -> CustomerRefDto` are cached independently even though the key
(`Customer` instance) is identical. **This is an implementation detail the codegen must get
right** - worth flagging explicitly when `dto-codegen-mapper` starts, since it's easy to
regress if the generator is written from scratch without this test coverage in front of it.
### Codegen optimization: skip the `DtoMapContext` cache for types that are never nested elsewhere
`DtoMapContext.computeIfAbsent` only ever produces a cache *hit* when the exact same source
instance is presented to `map()` more than once within one top-level call - which can only happen
when the target type is reachable via more than one path in the graph, i.e. it's used as a
`NESTED_ONE`/`NESTED_MANY` property by some *other* `@DtoMapping` pair (e.g. `CustomerRefDto`
reached from many `Contact`s via a shared `Customer`, or `AddressDto` shared as `billingAddress`
across customers). A type that's only ever a top-level `mapTo(...)`/`mapList(...)` entry point can
never receive the same source instance twice within one call - Ebean's own query engine already
de-duplicates root entity instances - so the cache lookup/insert there is pure overhead with a
guaranteed-never-hit `IdentityHashMap`.
Since all `@DtoMapping` pairs are resolved together at codegen time (`DtoMappingReader.
resolveAndValidate()`), it's straightforward to compute this: after cycle exclusion, walk every
surviving `DtoBeanMeta`'s properties and mark any `nested()` target as `nestedElsewhere()`. The
generated `map()` method then branches per mapper:
```java
// CustomerDto - never nested by another mapper, only a mapTo()/mapList() entry point
public CustomerDto map(Customer source, DtoMapContext context) {
if (source == null) return null;
// DtoMapContext for nested mappers only
return new CustomerDto(source.getId(), source.getName(),
billingAddressMapper.map(source.getBillingAddress(), context),
contactsMapper.mapList(source.getContacts(), context));
}
// AddressDto - nested under CustomerDto.billingAddress, so may be shared across customers
public AddressDto map(Address source, DtoMapContext context) {
if (source == null) return null;
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(
s.getId(), s.getLine1(), s.getCity()));
}
// ContactSummaryDto - flat, top-level only, no nested children at all
public ContactSummaryDto map(ContactSummary source, DtoMapContext context) {
if (source == null) return null;
// skip DtoMapContext, only ever a top-level mapping
return new ContactSummaryDto(source.getId(), source.getFullName());
}
```
Deliberately terse, single-line comments - just enough for a developer skimming generated code (e.g.
per the earlier `@Formula2`-on-DTO worked example) to know at a glance *why* a given mapper does or
doesn't use the cache, without spelling out the full reachability argument inline every time (that
lives here in the design doc instead). Note `CustomerDto`'s own construction skips the cache even
though it *has* nested children - `context` is still threaded down to `billingAddressMapper`/
`contactsMapper` since those target types (`AddressDto`, `ContactDto`) *are* nested elsewhere and
still need the identity cache for themselves - hence the distinct "for nested mappers only" wording
from the "only ever a top-level mapping" case (`ContactSummaryDto`), which has no children to thread
a context to at all.
### Fetching a ToOne back-reference used only for its FK/id needs the FK property fetched too
Confirmed (via a first-cut test failure) that if a ToMany's element type has a ToOne back to its
parent (e.g. `Contact.customer`), that FK property must itself be included in the fetch
(`.fetch("contacts", "id,firstName,lastName,customer")`) even when the mapper only reads the id
off the reference. Omitting it throws `LazyInitialisationException: Property not loaded:
customer` on the `getCustomer()` call itself (not merely on a property access on the returned
reference) - i.e. the earlier "ToOne reference access alone doesn't lazy load" finding
(dto-validate-fetch-pagination) only holds once the ToOne/FK property is itself part of the
fetch/select spec. This reinforces r6 (auto-deriving the fetch spec from DTO shape): the codegen
must include a ToOne property in the fetch spec whenever a DTO needing it (even just its id) is
reachable through a ToMany, not just at the top level.
### Test coverage added
- `TestCustomerDtoGraphMapping` extended to cover `contacts` ToMany mapping and to assert that
sibling `ContactDto`s under the same customer share the identical `CustomerRefDto` instance.
- `TestContactDtoGraphMapping` (new) - standalone `ContactDtoMapper` test focused specifically on
the identity de-dup guarantee and null-source handling.
## Codegen foundation: avaje-prisms adopted in querybean-generator (dto-codegen-mapper, step 1)
Before writing the DTO-mapper annotation-processing logic itself, adopted `avaje-prisms`
(`io.avaje:avaje-prisms`) in `querybean-generator` as the mechanism for reading the new
`@DtoPath`/`@DtoRef` annotations at APT time, replacing what would otherwise be more hand-rolled
`AnnotationMirror` walking (the existing pattern in `FindDbName.java`/`ReadModuleInfo.java`,
left as-is/unmigrated - only the *new* annotations use prisms).
This mirrors the proven pattern already used in two sibling projects in the same ecosystem -
`avaje-inject`'s `inject-generator` and `avaje-jsonb`'s `jsonb-generator` - both declare
`@GeneratePrism(SomeAnnotation.class)` once and get a generated `SomeAnnotationPrism` with
`isPresent(element)` / `getInstanceOn(element)` / `getOptionalOn(element)` and typed accessors
for every annotation member (correctly handling `Class`-valued members, avoiding the classic
`MirroredTypeException` dance).
Key property preserved: `querybean-generator` has **zero runtime/compile dependencies today**
(confirmed via `mvn dependency:list` returning "none"), matching annotations by FQN string
constants (`Constants.java`) rather than importing the actual annotation classes - deliberately
keeping the processor free of any dependency footprint for consumers. Adding `avaje-prisms` (to
generate the prism wrapper) and `ebean-annotation` (to reference `@DtoPath`/`@DtoRef` as literal
`Class` values in `@GeneratePrism(...)`) as `optional` dependencies preserves this: `mvn
dependency:list -DincludeScope=runtime` confirms every one of these (plus their own transitive
deps: `avaje-prism-core`, `avaje-spi-service`, `avaje-spi-core`) is marked `(optional)`, so none
of it propagates to a project that depends on `querybean-generator` (whether as a normal
dependency or via `annotationProcessorPaths`).
New annotations were added to the separate `ebean-annotation` repo (`io.ebean.annotation`
package, alongside `@Formula2`), not this repo:
```java
@DtoPath("billingAddress.line1")
String billingLine1; // rename/flatten a DTO property from a nested source path
@DtoRef
Integer customerId; // id-only back-reference, breaks what would otherwise be a graph cycle
```
Both use `@Target({FIELD, METHOD})` and `RetentionPolicy.CLASS` - visible to the annotation
processor (including across module boundaries, since `CLASS` retention survives in the compiled
`.class` file) but absent from runtime reflection, consistent with DTOs remaining plain,
framework-free types with no runtime footprint.
Wiring changes in `querybean-generator`:
- `pom.xml`: added `avaje-prisms` (`optional`, plus `annotationProcessorPaths` entry) and
`ebean-annotation` (`optional`) dependencies; removed the previous `-proc:none` compiler arg
(which would have suppressed `avaje-prisms`' own processor from running to generate the prism
source) - annotation processing is now scoped to exactly `avaje-prisms` via the explicit
`annotationProcessorPaths` list, so no other processor is auto-discovered.
- `module-info.java`: added `requires static io.avaje.prism;` and `requires static
io.ebean.annotation;` (`static` = compile-time only, matching the `optional` Maven scope).
- New `package-info.java` declaring `@GeneratePrism(DtoPath.class)` and
`@GeneratePrism(DtoRef.class)`, generating `DtoPathPrism`/`DtoRefPrism` into
`target/generated-sources/annotations`.
Verified: full `querybean-generator` build + existing test suite pass unchanged, and a downstream
full rebuild (`ebean-test` with `-am`) - which exercises the existing Q-bean codegen - also
passes with no regressions.
### Trigger mechanism: `@DtoMapping(source, target)` on a neutral package-info.java
Considered and rejected: putting a `source`/entity-referencing annotation directly on the DTO
class itself (e.g. `@Dto(Customer.class)` on `CustomerDto`). Rejected because DTO types are
often owned/generated elsewhere (e.g. from an OpenAPI spec) and must not be forced to reference
an internal persistence/entity type - that would leak internal domain types into a
public-facing/generated DTO module.
Instead, adopted the same pattern `avaje-jsonb` uses for external/foreign types it doesn't own
(`@Json.Import`): a repeatable annotation declared on a *neutral* holder - a `package-info.java`
- naming the `source` entity and `target` DTO as a pair:
```java
@DtoMapping(source = Customer.class, target = CustomerDto.class)
@DtoMapping(source = Contact.class, target = ContactDto.class)
package org.example.dto;
```
`@DtoMapping` (new, in `ebean-annotation`) is `@Target({PACKAGE, MODULE})`,
`@Retention(SOURCE)` (pure codegen trigger, never needed at runtime - unlike `@DtoPath`/
`@DtoRef` which need `CLASS` retention to remain visible to the DTO field itself),
`@Repeatable(DtoMapping.List.class)` following Java's own repeatable-annotation idiom. Neither
the entity nor the DTO needs any annotation of its own.
**Generated mapper package placement** - also modeled directly on `avaje-jsonb`'s handling of
`@Json.Import` for external types (`AdapterName`/`ProcessingContext.isImported`): defaults to the
target DTO's own package, *unless* the source or target type belongs to a different Java module
than the one being processed, in which case the generated mapper is placed in a package derived
from the processing module's own name instead - avoiding a JPMS "split package" violation that
would occur from generating source into a package owned by another module. An explicit
`mapperPackage` attribute is available to override this for edge cases. Same-module (or
non-modular/unnamed-module) projects are unaffected and just get the mapper alongside the DTO.
### mapTo(Class) dispatch: Class-token API + generated compile-time-safe registry
The original API sketch above (`mapTo(CustomerDto.class)`) predates the native-image/no-reflection
decision. Rather than switching to an instance-based API (`mapTo(new CustomerDtoMapper())`),
decided to keep the `Class`-token shape and generate a compile-time-safe registry to resolve it -
no reflection, no `Class.forName`, just literal `Class` comparisons generated at build time, e.g.:
```java
<S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> targetType) {
if (sourceType == Customer.class && targetType == CustomerDto.class) {
return (DtoMapper<S, D>) new CustomerDtoMapper();
}
if (sourceType == Contact.class && targetType == ContactDto.class) {
return (DtoMapper<S, D>) new ContactDtoMapper();
}
return null;
}
```
Dispatch is keyed on the **(source, target) pair**, not target alone - this matches how
`@DtoMapping(source, target)` pairs are declared, allows the same DTO type to be mapped from more
than one source entity without ambiguity, and lets `query.mapTo(dtoType)` fail fast with a clear
`PersistenceException` (rather than an incorrect match) when `query.getBeanType()` doesn't pair
with the requested DTO.
This mirrors the existing, already-proven `EbeanEntityRegister`/`EntityClassRegister` mechanism
(`SimpleModuleInfoWriter.java`) that `querybean-generator` already generates per module for entity
classes - a `List<Class<?>>` built from literal `SomeEntity.class` references, registered via
`META-INF/services` (`ServiceLoader`, itself native-image-friendly with no extra reflection
config needed for simple no-arg-constructor implementations). The DTO mapper registry follows the
same per-module aggregation + `META-INF/services` registration shape, giving `mapTo(Class)` a
concrete generated implementation to dispatch through at runtime without reflection anywhere in
the chain.
### mapTo(Dto.class) runtime wiring (implemented)
`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`/
`findStream()`/`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
On first use it resolves the generated `DtoMapper<S, D>` for the query's `(getBeanType(), dtoType)`
pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all generated
`DtoMapperRegister`s, analogous to `DtoBeanManager`), then:
- applies `mapper.fetchGroup()` to the query via `query.select(fetchGroup)` - the fetch/select spec
is entirely derived from the DTO's declared shape, no manual `.select()`/`.fetch()` needed;
- forces `query.setUnmodifiable(true)` - the resulting entity graph is read-only input to the
mapper, and any DTO property whose source wasn't actually fetched fails fast with
`LazyInitialisationException` rather than silently lazy loading or returning `null`;
- executes the query and maps the result(s) via `mapper.map(...)`/`mapper.mapList(...)`.
An unregistered `(source, dtoType)` pair throws a `PersistenceException` with a suggested
`@DtoMapping` fix, at first use (i.e. `findList()`/`findOne()`), not at `mapTo(dtoType)` call time.
`MappedQuery<D>.usingMaster(boolean)`, `.usingTransaction(Transaction)`, and `.usingConnection(Connection)`
all delegate directly to the underlying entity query, mirroring `Query`/`QueryBuilder`. This lets a
caller retry against the master data source after a read-replica failure by calling
`usingMaster(true)` on the *same* `MappedQuery` instance and re-invoking a find method - there's no
need to rebuild the query and call `.mapTo(...)` again.
`MappedQuery<D>.findStream()` mirrors `QueryBuilder#findStream()` - the underlying entity query is
streamed (supporting very large result sets, potentially using multiple persistence contexts
internally) and each entity is mapped to its target DTO lazily as the stream is consumed. One
`DtoMapContext` is shared across the whole stream (not per-element), so identity de-duplication of
nested DTOs (e.g. several `Contact`s sharing the same `Customer`) still holds even when the source
entities are never materialized into one `List` at all. As with the entity-level `findStream()`,
callers must consume it via try-with-resources to ensure the underlying resources are closed.
## Still open / to revisit during implementation
- Whether `.fetch(...)` calls can still be layered on top of a `mapTo(Dto.class)` query for explicit
overrides. Currently the mapper's `fetchGroup()` is the *only* source of the fetch spec - any
`.select()`/`.fetch()` calls made before `.mapTo(...)` are overwritten by it.
- Whether `@DtoPath`/`@DtoRef` need additional attributes beyond a bare path/marker (e.g. an explicit
target type on `@DtoRef` for disambiguation) once real DTOs with more complex shapes are codegen'd.
- Behavior when a DTO property has no matching entity property and no `@DtoPath`/`@Formula2` override
(fail at codegen time, most likely, consistent with the "fail fast" philosophy).
- `@Formula2`-on-DTO mapping to Blaze-Persistence/QueryDSL-style computed properties - not yet
implemented (see requirements doc); a narrower validation-only variant was attempted and rejected
as not distinct enough from `@DtoPath` (see "Formula2-on-DTO scope" above). The broader ad-hoc-SQL
case likely doesn't need a dedicated DTO feature at all - see "Ad-hoc computed/formula properties"
above for the `@Entity @View`/`@Sql` alternative.
- **Fetch-path collision between a `NESTED_ONE`/`NESTED_MANY` property and a `@DtoPath` property -
found and fixed**: `DtoMapperWriter.fetchGroupChainCalls()` builds one `.fetch(path, ...)`
chain-call per distinct fetch path, but the underlying `OrmQueryDetail.fetch(...)` unconditionally
**overwrites** (rather than merges) any existing entry for the same path key. If a DTO declared a
`NESTED_ONE`/`NESTED_MANY` property AND a `@DtoPath` property whose fetch-path prefix is the *exact
same* path (e.g. a nested `AddressDto billingAddress` alongside `@DtoPath("billingAddress.line1")`
on the same DTO - both resolve to fetch path `"billingAddress"`), the generator would emit two
`.fetch("billingAddress", ...)` calls and the second would silently discard the first's selected
properties. Merging wasn't practical - the nested property's `.fetch(path, mapper.fetchGroup())`
call passes another mapper's own pre-built, immutable, shared `FetchGroup`, so there's no clean way
to splice an extra scalar property into it at the call site. Fixed instead with a **fail-fast
compile-time error**: `DtoMapperWriter` now detects the collision and raises a clear
`ctx.logError(...)` (annotation-processor `ERROR` diagnostic, fails the compile) naming the
colliding property and fetch path, and suggesting the two ways out - move the property onto the
nested DTO type instead, or pick a `@DtoPath` that reaches a different, non-colliding path (as
`ContactDto.customerCity` already does deliberately, per its own comment, using a 3-segment path).
Verified empirically by compiling a small reproduction with a colliding `@DtoPath` and confirming
the expected error fires; a permanent regression test
(`DtoMapperFetchPathCollisionTest` in `querybean-generator`) now runs this same repro directly
through `javax.tools.JavaCompiler` with the `Processor` registered, asserting the compile fails with
the expected diagnostic message.
- **Compile-time verification of `select(...).asDto(...)` (r6, aspirational) - explored and closed as
rejected**: raw SQL is an opaque `String` at compile time, and even the typed query-bean
`.select(...)` form only type-checks against the *entity* - the match to the target DTO's constructor
still happens at runtime via reflection (`DtoQueryPlanConstructor`), and the `.asDto(...)` call site
can be arbitrarily distant from the `.select(...)` call, so there's no fixed AST shape an annotation
processor could reliably verify (unlike QueryDSL, whose compile-time safety actually comes from typed
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking a select-list
against a DTO). `mapTo(Dto.class)` already closes the underlying gap in the tractable direction - it
derives the select/fetch spec *from* the DTO's declared shape at APT time, so it is compile-time safe
by construction. Recommend `mapTo()` whenever compile-time-checked DTO projection matters, and treat
`asDto()`/`findDto()` as the flexible, runtime-checked escape hatch for raw/dynamic SQL. See
`dto-mapping-requirements.md` requirement r6.
- **Custom property conversion (`@DtoConvert`/`@DtoMixin`, r13/r14) - implemented**: motivated by a
real hand-written mapper (`DriverMapper`, central-access) needing both a dependency-free scalar
coercion (`short` -> `boolean`) and a dependency-backed conversion (AES decryption via an injected
cipher). Final design (see `dto-mapping-requirements.md` section E), as built:
- `@DtoConvert(value = ConverterType.class, method = "name")` on a DTO property (combinable with
`@DtoPath`); the generator dispatches on whether the referenced method is `static` - static means a
direct inlined static call (no registration, covers common reusable coercions), instance means
dispatch via a new `DtoConverterManager.get(ConverterType.class).method(...)` call, with the
resolved instance wired as a real constructor parameter/field on the generated mapper (same shape
as existing nested-mapper constructor injection). Multiple properties on the same mapper sharing
the same converter type are deduplicated to a single constructor parameter/field
(`DtoBeanMeta.converterDeps()`).
- `DtoConverterManager` (`ebean-api`, `io.ebean` package) is a small, narrowly-scoped static put/get
bridge - the app registers an already-DI-constructed converter singleton (e.g. built by
avaje-inject) *before* building the `Database`. This is a deliberate, narrow exception to the
general no-static-mutable-state convention: `ServiceLoader`-discovered, no-arg-constructed
generated code (`EbeanDtoMapperRegister`) has no other way to reach an already-DI-constructed
singleton. `DtoConverterManager.get(type)` throws immediately if nothing was registered for that
type, so a missing converter fails fast at Database-startup time (an eager field initializer on
`EbeanDtoMapperRegister`, and equally on each mapper's own no-arg constructor, which resolves the
same way via `DtoConverterManager.get(...)` for standalone/test construction), not lazily on first
use - `DtoMapperRegister`'s `mapperFor(...)` signature and `DtoMapperManager` are otherwise
completely unchanged, as originally planned.
- Two alternatives were explored and rejected first: (a) a `DtoMapContext.service(Class)` lookup -
wrong lifetime, `DtoMapContext` is a short-lived per-call identity-cache only; (b) a
`ServiceLoader`-discovered `DtoConverterSource` SPI mirroring `DtoMapperRegister` itself - can't
bridge to an *already* DI-constructed dependency without reconstructing/duplicating it.
- `@DtoMixin(Target.class)` - a companion type overlaying `@DtoPath`/`@DtoConvert`/`@DtoRef`
annotations onto a DTO that can't be annotated directly (e.g. OpenAPI-generated). Discovered via
`roundEnv.getElementsAnnotatedWith(...)` (added to `Processor.getSupportedAnnotationTypes()`,
since - unlike `@DtoPath`/`@DtoRef`/`@DtoConvert` - a mixin doesn't annotate an already-iterated
field of a known `@DtoMapping` target, so it can't be found lazily). `DtoMappingReader` resolves
each target property's annotations from the field itself first, falling back to a same-named
method on the registered mixin (`DtoMappingReader.prismOn(...)`) - directly mirrors avaje-jsonb's
proven `@Json.MixIn` mechanism.
- Implemented in `ebean-annotation` (`DtoConvert`, `DtoMixin`), `ebean-api` (`DtoConverterManager`),
and `querybean-generator` (`DtoConverterMeta`, `DtoBeanMeta.converterDeps()`,
`DtoMappingReader`/`DtoMapperWriter`/`DtoMapperRegisterWriter` changes). Test coverage:
`tests/test-dto-mapping` `TestDtoConvert` (static + instance dispatch, fail-fast unregistered-type
check) and `TestDtoMixin` (mixin overlay, including instance-dispatch conversion resolved purely
from mixin-declared annotations). The instance-dispatch converter is registered via a
`DatabaseConfigProvider` (ServiceLoader hook run before the `Database` is built) rather than a test
`@BeforeAll`, since `EbeanDtoMapperRegister`'s mapper fields (including any needing
`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
- Requirements: [dto-mapping-requirements.md](./dto-mapping-requirements.md)
- Issue: https://github.com/ebean-orm/ebean/issues/2540
- MapStruct cycle mapping: https://mapstruct.org/documentation/stable/reference/html/#mapping-object-cycles
+337
View File
@@ -0,0 +1,337 @@
# Nested DTO Mapping — Requirements
Design requirements distilled from [issue #2540 "Support nested DTO mapping"](https://github.com/ebean-orm/ebean/issues/2540),
reviewed against comparable features in QueryDSL (`@QueryProjection`) and Blaze-Persistence (`@EntityView`).
## Context
Ebean already supports:
- Partial/flat DTO queries via `DB.findDto(...)` and `query.select(...).asDto(Dto.class)`.
- `@Formula` / `@Formula2` — path-based, auto-joined computed SQL expressions, but only on managed entities.
- `query.setUnmodifiable(true)` — builds a read-only, non-lazy-loading entity graph (`InterceptReadOnly`,
see PR #2626). Accessing an unloaded property throws `LazyInitializationException`; mutating throws
`UnmodifiableEntityException`.
Unlike Hibernate, Ebean does dirty-detection on the bean itself (no dynamic proxies), so there is very little
extra cost to an entity-graph query versus a DTO query. This makes an **unmodifiable entity graph** a cheap,
natural intermediate representation to map *from* when producing a DTO graph — we don't need Blaze/Hibernate's
proxy-based `EntityView` mechanism to get the performance benefit they are chasing.
The goal is nested DTO graph support (DTOs containing ToOne/ToMany child DTOs), not just today's flat DTOs,
while keeping DTOs as plain, framework-unattached classes.
## Accepted Requirements
### A. Nested DTO graphs
- **Support nested DTO graphs (ToOne/ToMany)**
Allow mapping a query result into a DTO graph where DTO fields are themselves DTOs (ToOne) or
`List`/`Set<Dto>` (ToMany), not just flat DTOs. Use the existing `setUnmodifiable(true)` entity graph as
the intermediate, de-duplicated, identity-consistent source to map from.
*Inspiration: Blaze `@EntityView` subviews/subview collections; Jimmer fetcher DTOs.*
- **Auto-generated entity → DTO graph mapper**
Given an unmodifiable entity graph plus a target nested DTO type, generate (via annotation processing,
reflection-free) a mapper that walks the graph and populates the DTO graph, matching properties by
name/type with override annotations for renames, computed values, and collection element types.
*Inspiration: Blaze `@EntityView` + subview mapping; conceptually similar to MapStruct but Ebean-generated
and graph/identity aware.*
- **Identity-aware de-duplication in nested collections**
When mapping nested collections referencing the same underlying entity instance multiple times, reuse the
same DTO instance (mirrors Blaze/Jimmer identity semantics) rather than producing independent copies.
*Inspiration: Blaze/Jimmer identity handling.*
### B. Formula-style DTO annotations
- **`@Formula2`-like annotations on DTO fields**
Bring the existing `@Formula` / `@Formula2` concept (auto-joined, path-based computed SQL expressions) to
DTO classes so a DTO field can request a computed/aggregated value with the join auto-derived, instead of
only being available on managed entities.
*Inspiration: User suggestion; Ebean `@Formula2`; Blaze `@Mapping` computed expressions.*
*Status: a narrower version (pulling in an existing entity-level `@Formula2` by path) was implemented and
then rejected - for the common same-name case it generated code identical to a plain unannotated field, so
the annotation added no real value beyond a codegen-time validation. See `docs/dto-mapping-design.md`
("Formula2-on-DTO scope" and "Ad-hoc computed/formula properties" sections). The broader goal - arbitrary
ad-hoc computed SQL on a DTO field - is better served by modelling the computed value as its own
`@Entity @View`/`@Sql` read entity and mapping *that* into a plain DTO, reusing the existing (already
accepted) nested-DTO mapping machinery rather than a new DTO-level annotation.
- **Path-based property mapping annotation on DTO**
Allow a DTO field or constructor param to be annotated with a source path expression (e.g. `parent.name`)
so Ebean can auto-derive the select clause plus joins for nested/renamed properties, reducing manual
constructor wiring for non-trivial mappings.
*Inspiration: Blaze `@Mapping`; QueryDSL constructor expressions.*
### C. Compile-time safety
- **Compile-time verification of `select(...).asDto(...)` mapping** *(explored, rejected as impractical -
`mapTo()` accepted as the alternative)*
Today `select(props).asDto(Dto.class)` is only checked at runtime. Explored an annotation-processor
based mechanism to verify at compile time that selected properties match the DTO constructor or setters,
mirroring QueryDSL's `@QueryProjection` compile-time Q-type generation. Rejected as impractical: raw SQL
is an opaque `String` at compile time, and even the typed query-bean `.select(...)` form only
type-checks against the *entity* - the match to the target DTO still happens at runtime via reflection
(`DtoQueryPlanConstructor`), and the `.asDto(...)` call site can be arbitrarily distant from the
`.select(...)` call, so there's no fixed AST shape an annotation processor could reliably verify.
QueryDSL's actual compile-time safety comes from a different mechanism entirely - typed
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking an
independently-built select-list against a DTO. `mapTo(Dto.class)` (see section A) already closes the
underlying gap in the opposite, tractable direction: it derives the select/fetch spec *from* the DTO's
declared shape at APT time, so it is compile-time safe by construction, with no separate select-list to
drift out of sync. Recommendation: document `mapTo()` as the compile-time-safe answer for DTO
projections, and treat `asDto()`/`findDto()` explicitly as the flexible, runtime-checked escape hatch
for raw/dynamic SQL.
*Inspiration: QueryDSL `@QueryProjection` compile-time Q-type generation.*
- **Fail-fast on unmapped or lazy property access**
Ensure a clear, documented, minimal-ceremony way to fail fast if code touches a property not included in
the query projection, instead of silently lazy loading or returning null. `query.setUnmodifiable(true)`
already satisfies this (throws `LazyInitializationException`) — document/promote it as the answer, and
evaluate whether a lighter-weight flag decoupled from full unmodifiable/read-only semantics is needed.
*Inspiration: Original issue ask; already solved via `setUnmodifiable()` (PR #2626 `InterceptReadOnly`).*
### D. Fetch strategy and performance
- **Fetch strategy control for DTO graph relationships**
Existing entity query fetch hints (join vs. select/subselect secondary query, `+query`/`+lazy`) should
transparently carry over when the target of the query is a DTO graph rather than an entity graph.
`query.mapTo(Dto.class)` applies the DTO-derived `FetchGroup` only when the query has no
`select()`/`fetch()` already set - a manually tuned fetch spec always takes precedence and is never
overridden, allowing manual query optimisation when needed (at the cost of falling back to the
existing fail-fast-on-unmapped-property behaviour if the manual spec doesn't cover what the DTO needs).
*Inspiration: Blaze FETCH/SELECT/SUBSELECT fetch strategies.*
- **Pagination support for DTO graph queries**
Confirm existing pagination works unchanged when projecting into nested DTO graphs.
*Inspiration: Blaze pagination and keyset pagination support.*
### E. Custom property conversion
- **Per-property custom scalar conversion (`@DtoConvert`)**
Motivated by real hand-written mapper code (`DriverMapper`, central-access) doing per-property scalar
coercion (`short` -> `boolean`) and dependency-backed conversion (AES decryption via an injected cipher).
Introduce a `@DtoConvert(value = ConverterType.class, method = "name")` annotation (combinable with
`@DtoPath` for source-getter override) on a DTO property. The generator dispatches based on whether the
referenced method is `static`:
- **Static method** -> a direct static call is inlined (`ConverterType.method(source.getX())`), zero
ceremony, no registration - covers common, reusable, dependency-free scalar coercions (e.g.
`short`/`boolean`, enum <-> `String`) that could apply across many unrelated entity/DTO pairs.
- **Instance method** -> dispatched via `DtoConverterManager.get(ConverterType.class).method(source.getX())`
and wired as a real constructor parameter/field on the generated mapper (same shape as existing
nested-mapper constructor injection) - covers conversions needing a real dependency (e.g. a cipher).
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge: the app registers an
already-DI-constructed converter singleton (e.g. built by avaje-inject) *before* building the `Database`.
This is a narrow, accepted exception to the general no-static-mutable-state convention - it exists solely
to bridge an already-DI-constructed singleton into `ServiceLoader`-discovered, no-arg-constructed generated
code, which cannot otherwise reach a DI container. `DtoConverterManager.get(type)` throws immediately if
nothing was registered, so a missing converter fails fast at Database-startup time (an eager field
initializer on the generated `EbeanDtoMapperRegister`), not lazily on first use.
*Design exploration considered and rejected two alternatives first: (a) a `DtoMapContext.service(Class)`
lookup - rejected because `DtoMapContext` is a short-lived per-call identity-cache only, wrong lifetime for
a real singleton dependency; (b) a `ServiceLoader`-discovered `DtoConverterSource` SPI mirroring
`DtoMapperRegister` itself - rejected because a `ServiceLoader`-instantiated (no-arg) source cannot bridge
to an *already* DI-constructed dependency (e.g. a cipher needing config/secrets) without reconstructing it
itself, duplicating/bypassing the app's own DI-managed instance.*
*Inspiration: `DriverMapper` (central-access) hand-written pattern; MapStruct qualified converter methods.*
*Status: implemented - `@DtoConvert` (ebean-annotation), `io.ebean.DtoConverterManager` (ebean-api), and
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)`
companion interface/type, discovered by scanning the compilation round and overlaying its per-property
annotations onto the real target's properties by name-match - directly mirrors avaje-jsonb's
`@Json.MixIn` mechanism (`KingfisherMixin`/`CrewMateMixIn` pattern), a proven prior-art solution to the
exact same "can't annotate a generated/unowned type" problem.
*Inspiration: avaje-jsonb `@Json.MixIn`.*
*Status: implemented - `@DtoMixin` (ebean-annotation) and querybean-generator round-scanning/overlay
support (matches mixin methods to target properties by name, applying whichever of `@DtoPath`/`@DtoRef`/
`@DtoConvert` is present as if declared on the target field itself). Test coverage: `tests/test-dto-mapping`
`TestDtoMixin`.*
### F. DI-friendly manual mapper usage
- **Public `DtoMapperManager` with `get(Class<T> mapperType)` for DI**
Motivated by `DriverMapper`/`DriverService` (central-access): `DriverMapper` is a hand-written
`@Component` constructor-injected into `DriverService`. Moved `DtoMapperManager` from internal
(`io.ebeaninternal.server.dto`) to public `io.ebean` - unchanged `mapperFor(source, dto)`, plus a new
`get(Class<T> mapperType)` keyed by the generated mapper's own concrete class (e.g.
`manager.get(CustomerDtoMapper.class)`), for direct/concrete-typed DI injection. `DtoMapperRegister`
gained a default `mapperOfType(Class<T>)` method (non-breaking); the generator emits the real if-chain
body (mirrors `mapperFor`'s if-chain). `DtoMapperManager` has zero `Database` dependency (constructor
only does `ServiceLoader.load(DtoMapperRegister.class)`), so it can be constructed standalone,
independent of/before a `Database` - e.g. as an avaje-inject bean.
*Inspiration: `DriverMapper`/`DriverService` (central-access).*
*Status: implemented.*
- **`DtoMapperManager` sharing via `DatabaseBuilder.putServiceObject`**
So `query.mapTo()` and application-injected mappers share the exact same `DtoMapperManager` instance
(and hence the same underlying generated mapper singletons) rather than each independently constructing
its own, `InternalConfiguration` checks `config.getServiceObject(DtoMapperManager.class)` first (mirrors
the existing `AutoMigrationRunner`/`GeoTypeProvider` `putServiceObject`/`getServiceObject` pattern),
falling back to constructing a default `new DtoMapperManager()` if none was supplied.
*Inspiration: user proposal following `DriverMapper`/`DriverService` review.*
*Status: implemented.*
*Rejected: generator-emitted `builder(source)` method* - `DriverMapper` exposes `builder(cDriver)`
returning a partially-populated `DriverBuilder` so callers can add extra caller-supplied fields (e.g.
fleets) before `build()`. Rejected as a generator feature - `Driver`/`DriverSummary` already use
avaje-recordbuilder's `@RecordBuilder`, which generates `Target.builder(existingInstance)`
(seed-from-instance). The same effect is already achievable with zero ebean changes:
`mapper.map(source)` then `Builder.builder(mapped).extraField(x).build()`. Documented as a recipe
instead (see "Recipe: adding extra caller-supplied fields after mapping" in
`docs/guides/mapping-entity-graphs-to-dtos.md`).
### G. Large-target construction and shape variants
- **Builder-based target construction for large DTOs**
Motivated by `UserService`/`User` (central-access): `User` is a 24-field OpenAPI-generated record with
a `@RecordBuilder`-generated `UserBuilder`, hand-mapped via a long fluent builder chain rather than a
positional constructor to stay readable/refactor-safe. The generator auto-detects a RecordBuilder-style
builder on the target (static `Target.builder()` + fluent per-property setters + `build()`) and uses
`Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)` whenever (a) a builder is
detected and (b) the target has more than a threshold number of properties (default 5), falling back to
the positional constructor otherwise. An explicit `@DtoMapping` attribute (`builder = AUTO | ALWAYS |
NEVER`) overrides the heuristic in either direction. Applies regardless of whether the target class is
hand-authored or foreign/generated (e.g. an OpenAPI record) - `@DtoMapping` is already declared
externally via `package-info.java`, not on the target class, so this was already compatible with
foreign target types.
*Inspiration: `UserService`/`User` (central-access).*
*Status: implemented.*
- **Named mapper variants excluding nested paths, sharing one generated class**
Motivated by `UserService`/`User` (central-access): `CUser` -> `User` is mapped in two shapes - with
nested `fleets` (`findUserByGid`) and without (`findAll`, bulk listing) - to avoid an unnecessary
join/fetch on the common bulk-listing path. Keeps the existing "shape always derived from declaration,
fetch spec always wins" philosophy (rejected relaxing that rule / rejected a runtime
is-property-loaded auto-skip check as less deterministic). The same `(source, target)` pair can be
declared more than once in `package-info.java` via a named variant, e.g.
`@DtoMapping(source = CUser.class, target = User.class)` (base/full) plus
`@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")`
(variant). Both variants are generated into the **same** mapper class (one class per target, not one
per variant) and share a single private `build(source, context, boolean includeXxx, ...)` method
containing the common field population written once; each excluded nested path becomes a `boolean
includeXxx` parameter of that shared method rather than a precomputed value, so `build()` still
evaluates every property - included or excluded - inline, at its own declared field position (a
`includeFleets ? fleetsMapper.mapList(...) : List.of()` ternary in place, not hoisted out as a
pre-evaluated call argument). This preserves the DTO's declared property order as the true evaluation
order regardless of which properties a variant happens to exclude. The base `map()` passes `true` for
every flag; each named variant (exposed as a same-named accessor, e.g. `noFleets()`, returning a single
shared/cached instance of its own small `DtoMapper<SOURCE, TARGET>`-implementing inner class - not
reconstructed per call) passes `false` for the paths it excludes and omits that path from its own
`fetchGroup`. Selected via a new `query.mapTo(Class<D> dtoType, DtoMapper<T, D> mapper)` overload
taking an already-resolved mapper instance directly (e.g. `query.mapTo(User.class,
userMapper.noFleets())`) - no string-based variant lookup, and no changes needed to
`DtoMapperRegister`/`DtoMapperManager`.
*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**
Ebean supports entity beans declared as Java `record`s (e.g. `public record CourseRecordEntity(@Id long id,
String name, String notes) {}` - see `test-java16`), whose only accessor shape is the bare component name
(`active()`, `name()`, `id()`) - never `getXxx()`/`isXxx()`. This bare-accessor convention isn't limited to
an actual `record` type though - an ordinary class can just as easily expose bare/fluent-style accessors
with no `get`/`is` prefix at all. The generator resolves the real accessor for each source type (the direct
source, or an intermediate `@DtoPath`/`@DtoRef` association type) by checking which shape actually exists as
a method, in order: (1) `isXxx()` returning `boolean` (JavaBean boolean convention), (2) `getXxx()` (JavaBean
convention), (3) the bare `propertyName()` itself - falling back to a guessed `getXxx()` only if none of the
three are found. Resolution is entirely name/existence-based (no dependency on whether the type is actually
a `record`). The Ebean bean-property name used in generated `FetchGroup.select(...)`/`.fetch(...)` calls is
tracked directly from the original property/segment name (not reverse-parsed from the resolved accessor's
method name), so it's correct regardless of which of the three accessor shapes was used.
*Inspiration: Ebean's own record-entity support (`test-java16`); user-reported gap during review.*
*Status: implemented.*
## Rejected Requirements
These were considered and explicitly rejected as out of scope:
- **DTO as interface / dynamic proxy views** — Blaze `@EntityView` defines views as interfaces backed by
runtime proxies. This conflicts with Ebean's preference for plain, framework-unattached DTO classes.
- **Updatable or creatable entity views (persist through DTO)** — Blaze's `@UpdatableEntityView` /
`@CreatableEntityView` cascade persist/update through the view. This would duplicate Ebean's existing
entity persistence model and introduce a second, ambiguous dirty-checking/cascade model.
- **New predicate/filter DSL for subview collections** — Blaze allows filter expressions directly in
`@Mapping` (e.g. filtering a collection by an attribute value). Ebean already has typed query bean
predicates and `.filterMany()` for filtering child collections in queries; no new embedded filter
expression language is needed on the DTO itself.
## References
- Issue: https://github.com/ebean-orm/ebean/issues/2540
- PR #2626: `InterceptReadOnly` / `InterceptReadWrite` split enabling the unmodifiable entity graph fast path
- Ebean docs: https://ebean.io/docs/query/option#unmodifiable
- QueryDSL: `@QueryProjection` (constructor-based, compile-time-checked projections)
- Blaze-Persistence Entity Views: https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/
+26
View File
@@ -0,0 +1,26 @@
# AGENTS.md — Ebean ORM
This project uses [Ebean ORM](https://ebean.io).
## AI Agent Instructions
Step-by-step guides for common Ebean tasks (Maven setup, database config, query writing, transactions, test containers, DB migrations, Lombok) are at:
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
Key guides (fetch and follow when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.md
- Derived / formula properties (`@Formula`, `@Formula2`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/derived-formula-properties.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
Agents: Before performing any Ebean-related task, fetch and follow the relevant guide above.
+208
View File
@@ -0,0 +1,208 @@
# Guides
See also: [AGENTS.md](AGENTS.md) — a minimal template for AI agent onboarding and automation in Ebean ORM projects.
Step-by-step guides written as instructions for AI agents and developers.
For a high-level capability reference (scope, core APIs, and AI guidance), see
[../LIBRARY.md](../LIBRARY.md).
## Adding Ebean ORM with PostgreSQL to an existing Maven project
A three-part guide covering everything needed to wire Ebean + PostgreSQL into an
existing Maven project. Complete the steps in order.
| Step | Guide | Description |
|------|-------|-------------|
| 1 | [Maven POM setup](add-ebean-postgres-maven-pom.md) | Add Ebean dependencies, the enhancement plugin, and the querybean-generator annotation processor to `pom.xml` |
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
## Migration & upgrades
| Guide | Description |
|-------|-------------|
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
## Observability
| Guide | Description |
|-------|-------------|
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
## Entity beans
| Guide | Description |
|-------|-------------|
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
| [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md) | Read-only computed properties: physical-SQL `@Formula` (with `${ta}` and hand-written joins) versus logical path-based `@Formula2` (auto-resolved joins); use in `select`/`where`/`orderBy`; default inclusion and the `@Transient` opt-out |
## Querying
| Guide | Description |
|-------|-------------|
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
| [Mapping entity graphs to DTOs (`mapTo`)](mapping-entity-graphs-to-dtos.md) | Map a nested entity graph query result to a nested DTO graph via `query.mapTo(Dto.class)`; `@DtoPath`/`@DtoRef` for renamed/flattened/id-only properties; identity-aware de-dup via `DtoMapContext`; computed/aggregate DTO values via `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation`; comparison with the flat `asDto()` pipeline |
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
| [Using `RawSql` with Ebean](using-rawsql-with-ebean.md) | Choose between `RawSqlBuilder.parse()`, `unparsed()`, and `withPlaceholders()`; the `${where}`/`${andWhere}`/`${having}`/`${andHaving}` placeholder reference for CTEs, window functions, and subqueries; column mapping; and using `RawSql` with query beans |
## Persisting & transactions
| Guide | Description |
|-------|-------------|
| [Persisting and transactions with Ebean](persisting-and-transactions-with-ebean.md) | Step-by-step guidance for AI agents to choose `insert` / `save` / `update` / `delete`; inspect cascades; select the right transaction boundary; and use batch or bulk update for large write sets |
## Testing
| Guide | Description |
|-------|-------------|
| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
## Database migrations
| Guide | Description |
|-------|-------------|
| [DB migration generation](add-ebean-db-migration-generation.md) | Add `GenerateDbMigration.java` to generate schema diff migrations offline; configure the migration runner; understand `.sql` and `.model.xml` output files; workflow for pending drops |
## Connection Pooling & DataSource Configuration
The [ebean-datasource](https://github.com/ebean-orm/ebean-datasource) project provides
comprehensive guides on connection pool configuration and best practices. These are particularly
useful for production deployments, especially in Kubernetes or AWS environments:
| Guide | Description |
|-------|-------------|
| [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md) | Step-by-step guide for basic, read-only, Kubernetes, and AWS Lambda datasource configurations |
| [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md) | Setting up dual DataSources with Aurora read and write endpoints for Ebean integration |
| [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md) | Understanding connection heartbeat strategies and why `Connection.isValid()` is the recommended approach |
---
## Helping AI agents find these guides
### Agent Skills (recommended)
The **ebean-orm skill** packages these guides into a structured format that AI agents
load on-demand. See the [ebean-orm/skills](https://github.com/ebean-orm/skills) repo
for installation instructions. Quick start:
```bash
git clone git@github.com:ebean-orm/skills.git ~/.agents/ebean-skills
mkdir -p ~/.agents/skills
ln -sf ~/.agents/ebean-skills/ebean-orm ~/.agents/skills/ebean-orm
```
This works with [pi](https://github.com/mariozechner/pi-coding-agent),
[Claude Code](https://docs.anthropic.com/en/docs/claude-code), and any harness
that supports the [Agent Skills standard](https://agentskills.io/specification).
### Other approaches
AI coding agents can only follow these guides if they know they exist. Below are
copy-paste snippets for the most common AI tooling configurations. Add whichever
applies to your project.
### llms.txt (fastest bootstrap for any agent)
`https://ebean.io/llms.txt` provides a concise Ebean capability reference and links
to all guides in a format optimised for LLMs. Agents that check `llms.txt` before
starting work will find everything they need without requiring project-level
configuration. This is worth knowing if you are an AI agent reading this file.
### Project `README.md` (universal — works with all AI tools)
Add a section near the top of your project README:
```markdown
## AI Agent Instructions
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
Ebean tasks (Maven setup, database configuration, query writing, transactions,
test containers, DB migrations, Lombok) are at:
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
AI agents should fetch and follow the relevant guide before performing Ebean-related
tasks.
```
### Why README is important
Many AI agents (Copilot, Claude, Cursor, Aider) read the project `README.md`
automatically as part of understanding the codebase. A visible link in README is the
most universally effective way to ensure any agent working on your project will
discover the guides without requiring tool-specific configuration files.
------
### GitHub Copilot — `.github/copilot-instructions.md`
```markdown
## Ebean ORM
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.md
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
```
### Claude Code — `CLAUDE.md`
Same content as above — Claude Code reads `CLAUDE.md` at the project root.
### AGENTS.md — OpenAI Codex / GitHub Copilot coding agent
Place an `AGENTS.md` at your repo root:
```markdown
## Ebean ORM
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Entity bean creation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/entity-bean-creation.md
```
### Cursor — `.cursor/rules/ebean.mdc`
```markdown
---
description: Ebean ORM task guidance
globs: ["**/*.java", "**/pom.xml"]
alwaysApply: false
---
## Ebean ORM
This project uses Ebean ORM. Before performing any Ebean-related task, fetch and
follow the relevant step-by-step guide from:
https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
```
@@ -0,0 +1,400 @@
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
## Purpose
This guide provides step-by-step instructions for adding Ebean DB migration generation
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
performing a diff of the current entity model against the previously recorded model state,
producing platform-specific DDL SQL scripts.
These instructions are designed for AI agents and developers to follow precisely.
---
## Prerequisites
- An existing Maven project with Ebean ORM configured (entity beans present)
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
---
## Step 1 — Verify migration dependencies
### Generation tooling (`ebean-ddl-generator`)
`ebean-test` (already present as a test dependency) transitively includes
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
is required for generation.
### Runtime migration runner (`ebean-migration`)
`ebean-migration` is the library that runs migrations on application startup.
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
equivalent platform dependency). Verify it is on the classpath by running:
```bash
mvn dependency:tree | grep ebean-migration
```
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean.version}</version>
</dependency>
```
---
## Step 2 — Create `GenerateDbMigration.java`
Create the following class in `src/test/java/main/`. This `main` method is run manually
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
```java
package main;
import io.ebean.annotation.Platform;
import io.ebean.dbmigration.DbMigration;
import java.io.IOException;
/**
* Generate the next database migration based on a diff of the entity model.
* Run this main method after making entity bean changes to produce the migration SQL.
*/
public class GenerateDbMigration {
public static void main(String[] args) throws IOException {
DbMigration migration = DbMigration.create();
migration.setPlatform(Platform.POSTGRES);
migration.setVersion("1.1"); // set to the next migration version
migration.setName("add-customer"); // short description of the change
migration.generateMigration();
}
}
```
### Version naming convention
Ebean supports two common version formats — choose one and apply it consistently:
| Format | Example | Notes |
|--------|---------|-------|
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
The version controls execution order — Ebean runs migrations in ascending version order.
### Name convention
The `name` should be a short, lowercase, hyphenated description of the change:
- `add-customer-email`
- `rename-machine-type`
- `drop-unused-columns`
---
## Step 3 — Configure the output path (if needed)
By default, migration files are written to `src/main/resources/dbmigration/` relative
to the **current working directory** when `generateMigration()` is called. This is
usually the module root, which is correct for single-module projects.
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
resources directory is at a different relative path, specify it explicitly:
```java
// Relative path from the working directory (project root) to the module's resources
migration.setPathToResources("my-module/src/main/resources");
```
---
## Step 4 — Run `GenerateDbMigration` to produce the first migration
Run the `main` method via the IDE or Maven:
```bash
# Run via Maven exec plugin (or use IDE run configuration)
mvn test-compile exec:java \
-Dexec.mainClass="main.GenerateDbMigration" \
-Dexec.classpathScope="test" \
-pl <your-module>
```
Ebean migration generation runs in **offline mode** — no database connection is required.
### Expected output files
After running, two files are created per migration in `src/main/resources/dbmigration/`:
```
src/main/resources/dbmigration/
1.1__add-customer.sql ← DDL SQL to apply (commit this)
model/
1.1__add-customer.model.xml ← logical model diff XML (commit this)
```
Both files must be committed to source control. The `.model.xml` file records the
logical state of the diff and is used by subsequent migration generations to determine
what has changed.
If **no entity beans have changed** since the last migration, the command outputs:
```
DbMigration - no changes detected - no migration written
```
---
## Step 5 — Enable the migration runner
Configure Ebean to run pending migrations automatically on application startup.
### Preferred approach — programmatic via `DatabaseBuilder`
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
the `Database` bean. This is the preferred approach as it is explicit, co-located with
the database configuration, and does not rely on external property files.
In the `@Factory` class that builds the `Database` bean (see the database configuration
guide), add `.runMigration(true)` to the builder chain:
```java
@Bean
Database database(ConfigWrapper config) {
var dataSource = DataSourceBuilder.create()
.url(config.getDatabaseUrl())
.username(config.getDatabaseUser())
.password(config.getDatabasePassword())
// ... other datasource settings ...
;
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
}
```
If migrations should only run in certain environments (e.g., not in production, or
only when a config flag is set), make it conditional:
```java
.runMigration(config.isRunMigrations()) // driven by config value
```
### Alternative — via application properties
If programmatic configuration is not available or not preferred, set the property
in `src/main/resources/application.properties`:
```properties
ebean.migration.run=true
```
Or in `src/main/resources/application.yaml`:
```yaml
ebean:
migration:
run: true
```
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
name in the property key:
```properties
ebean.mydb.migration.run=true
```
### What the runner does at startup
When migration running is enabled, Ebean will on each application start:
1. Look at the migrations in `src/main/resources/dbmigration/`
2. Compare against the `db_migration` table (created automatically on first run)
3. Apply any migrations that have not yet been executed, in version order
4. Record each successfully applied migration in `db_migration`
---
## Step 6 — Commit the migration files
Add both generated files to source control:
```bash
git add src/main/resources/dbmigration/1.1__add-customer.sql
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
git commit -m "Add db migration 1.1: add-customer"
```
---
## Ongoing workflow — generating subsequent migrations
For each future set of entity bean changes:
1. Make changes to the entity bean classes
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
```java
migration.setVersion("1.2");
migration.setName("add-address-table");
```
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
### Protecting hand-edited and non-versioned migrations across regeneration
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
entity model**. It can therefore overwrite content you did not change in the
entity beans, including:
- **hand-edited DDL** in a generated versioned `.sql` file, and
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must **delete the file first**, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
cannot express — it is isolated and now protected from regeneration.
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
produces, review the diff after **every** regeneration and **restore** any
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
re-check that the new migration is listed and filenames match after renaming a
generated file.
> **Run the generator from the module directory.** The output path set via
> `setPathToResources(...)` is resolved relative to the **working directory**.
> Run `GenerateDbMigration` with the working directory set to the module that owns
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
---
## Understanding the output files
### Apply SQL (`.sql`)
The apply SQL file contains the DDL that will be executed against the database:
```sql
-- apply changes
alter table customer add column email varchar(255);
```
### Model XML (`.model.xml`)
The model XML records the logical diff in a database-agnostic format. Ebean uses
this file on the next generation run to determine what has already been captured.
It is not executed against the database.
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<addColumn tableName="customer">
<column name="email" type="varchar(255)"/>
</addColumn>
</changeSet>
</migration>
```
---
## Optional configurations
### Multiple database platforms
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
instead of `setPlatform()`:
```java
migration.addPlatform(Platform.POSTGRES);
migration.addPlatform(Platform.SQLSERVER17);
migration.addPlatform(Platform.MYSQL);
```
Each platform gets its own subdirectory under `dbmigration/`.
### Include index
When enabled the migration generation also generates a file that contains
all the migrations and their associated hashes. This is a performance
optimisation (that will become the default) and means that the migration
runner just needs to read the one resource and has the pre-computed hash
values (so does not need to read each migration resource and compute the
hash for each of those at runtime).
```java
migration.setIncludeIndex(true);
```
### Strict mode
Strict mode (on by default) errors if there are any pending drops not yet applied.
Set to `false` to allow generation to proceed regardless:
```java
migration.setStrictMode(false);
```
### Applying pending drops
Destructive changes (drop column, drop table) are **not** included in the apply
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
the application to be deployed without immediately dropping columns (important for
rolling deployments).
The migration runner logs a message when pending drops exist:
```
INFO DbMigration - Pending un-applied drops in versions [1.1]
```
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
contains the pending drops:
```java
migration.setVersion("1.3");
migration.setName("drop-pending-from-1.1");
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
migration.generateMigration();
```
### Custom dbSchema
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
`application.properties`), no additional configuration is needed in
`GenerateDbMigration` — Ebean picks up the schema from the application config
automatically when running in offline mode.
```properties
# application.properties
ebean.dbSchema=myschema
```
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
+158
View File
@@ -0,0 +1,158 @@
# Guide: Add Ebean OpenTelemetry tracing
## Purpose
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
most importantly, how to order startup so Ebean sees the intended global
OpenTelemetry instance.
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
or fixing `GlobalOpenTelemetry` double-registration errors.
---
## Overview
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
spans as children of the current active OpenTelemetry span. It does not create
top-level request, job, or Lambda invocation spans by itself.
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
`Database` is configured. For that reason, the application must build and register
the OpenTelemetry SDK before any Ebean `Database` beans are created.
Rules of thumb:
- Register the global OpenTelemetry instance once.
- Register it before building Ebean databases.
- Model that ordering as a real DI dependency.
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
multiple places.
---
## Step 1 - Add the dependency
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-opentelemetry</artifactId>
<version>${ebean.version}</version>
</dependency>
```
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
No manual Ebean plugin registration is normally required.
---
## Step 2 - Build OpenTelemetry before Ebean databases
Create one application-owned OpenTelemetry bean. For example, when using
`avaje-metrics-otel`:
```java
import io.avaje.config.Configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.metrics.otel.MetricsOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import java.time.Duration;
@Factory
class OpenTelemetryConfig {
@Bean
OpenTelemetry openTelemetry(Configuration config) {
return MetricsOpenTelemetry.builder()
.endpoint(config.get("otel.endpoint"))
.serviceName(config.get("otel.serviceName", "orders"))
.deploymentEnvironmentName(config.get("app.env", "local"))
.meterInterval(Duration.ofSeconds(30))
.traceInterval(Duration.ofSeconds(30))
.buildAndRegisterGlobal();
}
}
```
If you build the SDK directly, use the same principle: create the SDK once and
register that instance globally before any Ebean databases are built.
---
## Step 3 - Make database beans depend on OpenTelemetry
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
is intentionally present to make startup order deterministic: OpenTelemetry is
created and registered before Ebean configures the database and profile handler.
```java
import io.avaje.config.Configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;
import io.opentelemetry.api.OpenTelemetry;
@Factory
class DatabaseConfig {
@Bean
Database database(OpenTelemetry openTelemetry, Configuration config) {
var dataSource = DataSourceBuilder.create()
.url(config.get("db.url"))
.username(config.get("db.username"))
.password(config.get("db.password"));
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.build();
}
}
```
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
initialized first.
Do not invert the dependency by making OpenTelemetry depend on the Ebean
`Database`. That creates a startup cycle and can still initialize Ebean before the
global OpenTelemetry instance is ready.
---
## Step 4 - Create a parent span at the application boundary
Ebean transaction spans are child spans. They are only created when a recording
OpenTelemetry span is active on the current thread.
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
root span around the top-level request/job boundary. Ebean will then attach
transaction spans beneath that current span.
---
## Troubleshooting
### `GlobalOpenTelemetry.set has already been called`
This usually means more than one component is trying to register a global SDK, or
some startup path touched the global before the application registered its SDK.
Fixes:
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
call in the application.
2. Build that OpenTelemetry bean before Ebean `Database` beans.
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
### No Ebean spans appear
Check:
1. `ebean-opentelemetry` is on the runtime classpath.
2. OpenTelemetry is registered before Ebean databases are built.
3. There is a current recording parent span when Ebean transactions run.
4. Sampling is not dropping the parent trace.
@@ -0,0 +1,295 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Database Configuration
## Purpose
This guide provides step-by-step instructions for configuring an Ebean `Database` bean
using **Avaje Inject** (`@Factory` / `@Bean`), backed by a PostgreSQL datasource built
with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 3 of 3.
---
## Prerequisites
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
- **Step 2 complete**: Test container setup is working and `mvn verify` passes
(see `add-ebean-postgres-test-container.md`)
- **Avaje Inject** is on the classpath (e.g. `io.avaje:avaje-inject`)
- A configuration source is available at runtime (e.g. `avaje-config` reading
`application.yml` or environment variables)
- The following configuration keys are resolvable at runtime (adapt names to your project):
| Key | Description |
|-----|-------------|
| `db_url` | JDBC URL for the master/write connection |
| `db_user` | Database username |
| `db_pass` | Database password |
| `db_master_min_connections` | Minimum pool size (default: 1) |
| `db_master_initial_connections` | Initial pool size at startup — set high to pre-warm on pod start (see K8s note below) |
| `db_master_max_connections` | Maximum pool size (default: 200) |
---
## Step 1 — Locate or create the `@Factory` class
Look for an existing Avaje Inject `@Factory`-annotated class in the project
(often named `AppConfig`, `DatabaseConfig`, or similar). If one exists, add the new
`@Bean` method to it. If none exists, create one:
```java
package com.example.configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
@Factory
class DatabaseConfig {
// beans will be added in the steps below
}
```
---
## Step 2 — Add the `Database` bean method (minimal — master datasource only)
Add the following `@Bean` method to the `@Factory` class. This creates an Ebean
`Database` backed by a single master (read-write) PostgreSQL datasource.
```java
import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;
@Bean
Database database() {
var dataSource = DataSourceBuilder.create()
.url(/* resolve from config, e.g.: */ Config.get("db_url"))
.username(Config.get("db_user"))
.password(Config.get("db_pass"))
.driver("org.postgresql.Driver")
.schema("myschema") // set to your target schema
.applicationName("my-app") // visible in pg_stat_activity
.minConnections(Config.getInt("db_master_min_connections", 1))
.initialConnections(Config.getInt("db_master_initial_connections", 10))
.maxConnections(Config.getInt("db_master_max_connections", 200));
return Database.builder()
.name("db") // logical name for this Database instance
.dataSourceBuilder(dataSource)
.build();
}
```
### Field guidance
| Field | Notes |
|-------|-------|
| `url` | Full JDBC URL, e.g. `jdbc:postgresql://host:5432/dbname` |
| `schema` | The Postgres schema Ebean should use (omit if using `public`) |
| `applicationName` | Shown in `pg_stat_activity.application_name`; helps with DB-side diagnostics |
| `name("db")` | Logical Ebean database name; relevant if multiple Database instances exist |
| `minConnections` | Connections kept open at all times; pool will not shrink below this |
| `initialConnections` | Connections opened at startup; see K8s warm-up note below |
| `maxConnections` | Hard upper limit on concurrent connections |
### Connection pool sizing for Kubernetes (and similar orchestrated environments)
When a pod starts in Kubernetes it will receive live traffic as soon as it passes
readiness checks — often before the connection pool has had a chance to grow to handle
the load. This can cause latency spikes on the first wave of requests while the pool
expands one connection at a time.
Use `initialConnections` to **pre-warm the pool at startup** so it is already sized
for peak load when the pod goes live:
```
minConnections: 2 ← floor; pool will shrink back here when idle
initialConnections: 20 ← opened at pod start, before first request arrives
maxConnections: 50 ← hard ceiling
```
The lifecycle is:
1. **Pod starts** — pool opens `initialConnections` connections immediately.
2. **Pod receives traffic** — pool is already at capacity; no growth latency.
3. **Traffic drops** — idle connections are closed; pool trims back toward `minConnections`.
4. **Next traffic spike** — pool grows again up to `maxConnections` on demand.
Set `initialConnections` to a value high enough that the pool does not need to grow
during the first minute of live traffic. A common starting point is 5075% of
`maxConnections`.
---
## Step 3 — Inject configuration via a constructor or config helper (recommended)
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
Avaje `Configuration` bean if one is available. This makes the factory testable and
keeps the wiring explicit. For example:
```java
@Bean
Database database(Configuration config) {
String url = config.get("db_url");
String user = config.get("db_user");
String pass = config.get("db_pass");
int min = config.getInt("db_master_min_connections", 1);
int init = config.getInt("db_master_initial_connections", 10);
int max = config.getInt("db_master_max_connections", 200);
var dataSource = DataSourceBuilder.create()
.url(url)
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.minConnections(min)
.initialConnections(init)
.maxConnections(max);
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.build();
}
```
If the project has a dedicated config-wrapper class (a `@Component` that reads config
keys), accept it as a parameter instead of `Configuration`.
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
> into the DI context. If you encounter "No dependency provided for
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
> (as shown in Step 2).
---
## Step 4 (Optional) — Add a read-only datasource
For production services that have a separate read-replica, add a second
`DataSourceBuilder` for read-only queries and wire it via
`readOnlyDataSourceBuilder(...)`. The read-only datasource:
- Uses `readOnly(true)` and `autoCommit(true)` (Ebean routes read queries there automatically)
- Typically has a higher max connection count than the master
- Benefits from a prepared-statement cache (`pstmtCacheSize`)
```java
@Bean
Database database(Configuration config) {
String masterUrl = config.get("db_url");
String readOnlyUrl = config.get("db_url_readonly");
String user = config.get("db_user");
String pass = config.get("db_pass");
var masterDataSource = buildDataSource(user, pass)
.url(masterUrl)
.minConnections(config.getInt("db_master_min_connections", 1))
.initialConnections(config.getInt("db_master_initial_connections", 10))
.maxConnections(config.getInt("db_master_max_connections", 50));
var readOnlyDataSource = buildDataSource(user, pass)
.url(readOnlyUrl)
.readOnly(true)
.autoCommit(true)
.pstmtCacheSize(250) // cache up to 250 prepared statements per connection
.maxInactiveTimeSecs(600) // close idle connections after 10 minutes
.minConnections(config.getInt("db_readonly_min_connections", 2))
.initialConnections(config.getInt("db_readonly_initial_connections", 10))
.maxConnections(config.getInt("db_readonly_max_connections", 200));
return Database.builder()
.name("db")
.dataSourceBuilder(masterDataSource)
.readOnlyDataSourceBuilder(readOnlyDataSource)
.build();
}
private static DataSourceBuilder buildDataSource(String user, String pass) {
return DataSourceBuilder.create()
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.addProperty("prepareThreshold", "2"); // PostgreSQL: server-side prepared statements
}
```
### Additional configuration keys for the read-only datasource
| Key | Description | Default |
|-----|-------------|---------|
| `db_url_readonly` | JDBC URL for the read replica | — |
| `db_master_initial_connections` | Initial master pool size at startup | 10 |
| `db_readonly_min_connections` | Minimum pool size | 2 |
| `db_readonly_initial_connections` | Initial pool size at startup | same as min |
| `db_readonly_max_connections` | Maximum pool size | 20 |
---
## Step 5 (Optional) — Enable the migration runner
If the project uses Ebean's built-in DB migration runner to apply SQL migrations on
startup, enable it on the `DatabaseBuilder`:
```java
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
```
This is equivalent to setting `ebean.migration.run=true` in `application.properties`
but is preferred because it keeps all database configuration in one place. To make it
conditional (e.g. only in non-production environments):
```java
.runMigration(config.getBoolean("db.runMigrations", false))
```
See the DB migration generation guide (`add-ebean-db-migration-generation.md`) for
full details on generating and managing migration files.
---
## See Also
For advanced connection pool configuration, production deployment patterns, and connection
validation best practices, see the [ebean-datasource guides](https://github.com/ebean-orm/ebean-datasource/tree/master/docs/guides/):
- **[Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)** — Covers read-only pools (`readOnly(true)` + `autoCommit(true)`), Kubernetes deployment strategies using `initialConnections`, and AWS Lambda optimization
- **[AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)** — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
- **[Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)** — Why `Connection.isValid()` is the recommended default and when (rarely) explicit `heartbeatSql` is needed
---
## Verification
1. Start the application (or run `mvn test -pl <your-module>`).
2. Look for log output similar to:
```
INFO o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
INFO io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
```
3. If you see `DataSourcePool` and `DatabasePlatform` log lines, Ebean is connected and
the database bean is wired correctly.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `ClassNotFoundException: org.postgresql.Driver` | PostgreSQL JDBC driver missing | Add `org.postgresql:postgresql` dependency (see Step 1 guide) |
| `Cannot connect to database` at startup | DB unreachable but `skipDataSourceCheck` is `false` | Set `.skipDataSourceCheck(true)` |
| Ebean enhancement warnings in logs | `ebean-maven-plugin` not configured | Complete Step 1 guide |
| `NullPointerException` reading config key | Config key not defined | Add the key to `application.yml` or environment |
---
## Related
The test container setup (Step 2) should already be complete and passing
before this step. See `add-ebean-postgres-test-container.md`.
+294
View File
@@ -0,0 +1,294 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 1: POM Setup
## Purpose
This guide provides step-by-step instructions for modifying an existing Maven `pom.xml`
to add Ebean ORM with PostgreSQL support. Follow every step in order. This is Step 1 of 3.
---
## Prerequisites
- An existing Maven project (`pom.xml` already exists)
- Java 11 or higher
- The project does **not** yet include any Ebean dependencies
---
## Step 0 — Gather requirements from the user
Before modifying any files, ask the user the following questions to determine
the correct setup path. Record the answers — they affect dependency choices
in this step and the approach used in Steps 2 and 3.
### Mandatory gate (do not skip)
- Do **not** continue to Step 1+ until the DI path is explicitly recorded.
- Do **not** infer the **None** path by default. Use **None** only when the user explicitly confirms no DI framework.
- If the user asks for a partial action (for example, "do only step 3"), keep the previously selected DI path; do not switch paths implicitly.
### DI path precedence (when user has not answered yet)
Use this precedence order:
1. Existing project context (highest priority): if dependencies/config already show Avaje Inject or Spring, select that path.
2. Explicit user answer in this guide's questions.
3. Recommended default only when context is genuinely unknown: Avaje Inject.
If context remains ambiguous, ask one multiple-choice clarification question and wait for the answer before editing files.
### Question 1: Dependency injection framework
> "Does this project use (or will it use) a DI framework? If so, which one?"
| Answer | Effect |
|--------|--------|
| **Avaje Inject** | Add `avaje-inject` + `avaje-inject-test` dependencies; use `@TestScope @Factory` for test container (Step 2); use `@Factory`/`@Bean` for production database (Step 3) |
| **Spring** | Use Spring `@TestConfiguration` for test container (Step 2); use Spring `@Configuration`/`@Bean` for production database (Step 3) |
| **None** | Use declarative `application-test.yaml` for test container (Step 2); use programmatic `Database.builder()` directly in application code (Step 3) |
### Question 2: PostGIS
> "Do you need PostGIS spatial extensions (geometry types, spatial queries)?"
| Answer | Effect |
|--------|--------|
| **Yes** | Use `PostgisContainer` in test setup (Step 2); may need `net.postgis:postgis-jdbc` dependency |
| **No** | Use `PostgresContainer` in test setup (Step 2) |
### Question 3: Read replica
> "Does your production environment use a separate read-replica (read-only) database?"
| Answer | Effect |
|--------|--------|
| **Yes** | Configure a read-only `DataSourceBuilder` in production database config (Step 3) |
| **No** | Single datasource only (Step 3) |
### Defaults
If the user is unsure or setting up a new project, recommend:
- **Avaje Inject** (lightweight, fast compile-time DI)
- **No PostGIS** (can be added later)
- **No read replica** (can be added later)
---
## Step 1 — Define the Ebean version property
Open the module's `pom.xml` (the one that will use Ebean directly, i.e. the module
containing the database configuration and entity classes).
Inside the `<properties>` block, add the `ebean.version` property if it does not
already exist:
```xml
<properties>
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
<ebean.version>17.5.0</ebean.version>
</properties>
```
> If the project has a parent POM that already defines `ebean.version`, skip this step.
---
## Step 2 — Add the PostgreSQL JDBC driver dependency
Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
```xml
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.11</version>
</dependency>
```
> Check [Maven Central](https://central.sonatype.com/artifact/org.postgresql/postgresql)
> for the latest version. If the parent POM manages the PostgreSQL version, omit the
> `<version>` tag.
---
## Step 3 — Add the Ebean PostgreSQL platform dependency
Inside the `<dependencies>` block, add the Ebean Postgres platform dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>${ebean.version}</version>
</dependency>
```
This single artifact pulls in the Ebean core, the datasource connection pool
(`ebean-datasource`), and all Postgres-specific support.
---
## Step 4 — Add the ebean-test dependency (test scope)
`ebean-test` configures Ebean for tests and enables automatic Docker container management
for Postgres test instances:
```xml
<!-- test dependencies -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>junit</artifactId>
<version>1.8</version>
<scope>test</scope>
</dependency>
```
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
avoiding the need to declare those dependencies separately.
---
## Step 4b — Add DI framework dependencies (if applicable)
If the user chose **Avaje Inject** in Step 0, add the following dependencies and
annotation processor. Skip this step if the user chose Spring or no DI.
### Dependencies
```xml
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
<version>12.5</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-test</artifactId>
<version>12.5</version>
<scope>test</scope>
</dependency>
```
> Check [Maven Central](https://central.sonatype.com/artifact/io.avaje/avaje-inject)
> for the latest version.
### Annotation processor
The `avaje-inject-generator` must be added to the `annotationProcessorPaths` in
`maven-compiler-plugin` (added in Step 6 below). When adding both processors,
the final `<annotationProcessorPaths>` block should include both:
```xml
<annotationProcessorPaths>
<path> <!-- generate ebean query beans -->
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
<path> <!-- generate avaje-inject DI code -->
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>12.5</version>
</path>
</annotationProcessorPaths>
```
---
## Step 5 — Add the ebean-maven-plugin (bytecode enhancement)
Ebean requires bytecode enhancement to provide dirty-checking and lazy-loading.
The `ebean-maven-plugin` performs this enhancement at build time.
Inside the `<build><plugins>` block, add:
```xml
<plugin> <!-- perform ebean enhancement -->
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
```
---
## Step 6 — Add the querybean-generator annotation processor
The `querybean-generator` annotation processor generates type-safe query bean classes
at compile time. It must be registered as an `annotationProcessorPath` inside
`maven-compiler-plugin`.
### Case A — No existing `maven-compiler-plugin` configuration
Add the full plugin entry to `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<annotationProcessorPaths>
<path> <!-- generate ebean query beans -->
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```
### Case B — `maven-compiler-plugin` already exists with `<annotationProcessorPaths>`
Locate the existing `<annotationProcessorPaths>` block inside the existing
`maven-compiler-plugin` entry and add the new `<path>` inside it. Do **not** add a
second `<configuration>` block or a second `<annotationProcessorPaths>` block.
Example — if the existing block already has a path for, say, `avaje-nima-generator`:
```xml
<annotationProcessorPaths>
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-nima-generator</artifactId>
<version>${avaje-nima.version}</version>
</path>
<!-- ADD the new path here, inside the existing block -->
<path>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
```
---
## Verification
Run the following to confirm the POM is valid and both main and test sources compile:
```bash
mvn test-compile
```
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
Using `test-compile` rather than `compile` ensures test dependencies and test
source files are also verified.
---
## Next Step
Proceed to **Step 2: Test container setup**
(`add-ebean-postgres-test-container.md`) to wire an injectable test `Database`
backed by `ebean-test` containers. Verify with `mvn verify` before continuing
to production database configuration.
@@ -0,0 +1,445 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project - Step 2: Test Container Setup
## Purpose
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
container for tests, exposing an `io.ebean.Database` instance for use in test
classes. This is Step 2 of 3.
Complete this step before configuring the production database in Step 3. Getting
the test container working first gives you a fast feedback loop - you can verify
entity changes compile, enhance, and persist correctly with `mvn verify` before
wiring up production datasource configuration.
---
## Prerequisites
- **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`,
`querybean-generator`, and **`ebean-test`** as a test-scoped dependency
(see `add-ebean-postgres-maven-pom.md`)
- **Step 0 answers recorded**: DI framework choice and PostGIS requirement
- **Docker** is installed and running on the developer machine
---
## Overview: Choosing your approach
The approach depends on the DI framework choice made in Step 0:
| DI framework | Approach | How |
|--------------|----------|-----|
| **Avaje Inject** | Programmatic | `@TestScope @Factory` class with injectable `Database` bean |
| **Spring** | Programmatic | `@TestConfiguration` class with `@Bean` methods |
| **None** | Declarative | `application-test.yaml` + plain JUnit test |
Follow the path that matches your choice below.
---
## Path A — Programmatic with Avaje Inject (recommended)
This approach uses `@TestScope @Factory` to expose the container and `Database`
as injectable beans. It offers more control (image mirrors, custom config) and
makes `Database` directly injectable into test classes.
### A.1 — Verify Avaje Inject test dependencies
Confirm the following are present in `pom.xml` (in addition to `ebean-test`):
```xml
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject</artifactId>
<version>${avaje-inject.version}</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-test</artifactId>
<version>${avaje-inject.version}</version>
<scope>test</scope>
</dependency>
```
And the `avaje-inject-generator` annotation processor in `maven-compiler-plugin`:
```xml
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-inject-generator</artifactId>
<version>${avaje-inject.version}</version>
</path>
```
### A.2 — Create a `@TestScope @Factory` class
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`):
```java
package com.example.testconfig;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.Database;
@TestScope
@Factory
class TestConfiguration {
// bean methods added below
}
```
### A.3 — Add a container bean and a Database bean
#### Plain PostgreSQL
```java
import io.ebean.test.containers.PostgresContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
#### PostGIS (PostgreSQL + PostGIS extension)
Use `PostgisContainer` instead. The default image is
`ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`,
and `postgis` are installed automatically.
```java
import io.ebean.test.containers.PostgisContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgisContainer postgres() {
return PostgisContainer.builder("17")
.dbName("my_app")
.build()
.start();
}
@Bean
Database database(PostgisContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
#### Key differences
| | PostgresContainer | PostgisContainer |
|---|---|---|
| Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` |
| Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` |
| Default port | 6432 | 6432 |
| Optional LW mode | — | `.useLW(true)` (see Optional section) |
### A.4 — Write a test
Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`:
```java
package com.example.testconfig;
import io.avaje.inject.test.InjectTest;
import io.ebean.Database;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@InjectTest
class DatabaseTest {
@Inject
Database database;
@Test
void database_isAvailable() {
assertThat(database).isNotNull();
}
}
```
### A.5 — Verify
```bash
mvn verify
```
Expected log output:
```
INFO Container ut_postgres running with port:6432 ...
INFO connectivity confirmed for ut_postgres
INFO DataSourcePool [my_app] autoCommit[false] ...
INFO DatabasePlatform name:my_app platform:postgres
INFO Executing db-create-all.sql - ...
```
**Important:** Verify this step passes with `mvn verify` before proceeding to
Step 3 (production database configuration).
---
## Path B — Programmatic with Spring
Use Springs `@TestConfiguration` to provide the container and `Database` beans.
### B.1 — Create a `@TestConfiguration` class
```java
package com.example.testconfig;
import io.ebean.Database;
import io.ebean.test.containers.PostgresContainer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
@TestConfiguration
class TestDatabaseConfig {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17")
.dbName("my_app")
.build()
.start();
}
@Primary
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
For PostGIS, use `PostgisContainer` instead (same pattern as Path A).
### B.2 — Write a test
```java
package com.example;
import io.ebean.Database;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class DatabaseTest {
@Autowired
Database database;
@Test
void database_isAvailable() {
assertThat(database).isNotNull();
}
}
```
### B.3 — Verify
Run `mvn verify` and confirm the same log output as Path A.
---
## Path C — Declarative (no DI framework)
This is the simplest approach but offers less control. `ebean-test` reads a
YAML config file and automatically manages the Docker container and `Database`
instance. Use this when the project has no DI framework.
### C.1 — Create `application-test.yaml`
Create `src/test/resources/application-test.yaml`:
```yaml
ebean:
test:
platform: postgres
ddlMode: dropCreate
dbName: my_app
```
For PostGIS, use `platform: postgis` instead.
### C.2 — Write a test
Use `DB.getDefault()` to obtain the `Database` instance:
```java
package com.example;
import io.ebean.DB;
import io.ebean.Database;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class DatabaseTest {
@Test
void database_isAvailable() {
Database database = DB.getDefault();
assertThat(database).isNotNull();
}
}
```
### C.3 — Verify
```bash
mvn verify
```
Expected log output:
```
INFO Container ut_postgres running with port:6432 ...
INFO connectivity confirmed for ut_postgres
INFO DataSourcePool [my_app] autoCommit[false] ...
INFO DatabasePlatform name:my_app platform:postgres
```
**Important:** Verify this passes before proceeding to Step 3.
Skip to [Optional configurations](#optional-configurations) or proceed to Step 3.
---
## Optional configurations
### Image mirror (for CI / private registry)
If CI builds pull images from a private registry (e.g., AWS ECR) instead of Docker Hub
or GitHub Container Registry, specify a mirror. The mirror is **only used in CI** -
it is ignored on local developer machines (where Docker Hub / GHCR is used directly).
```java
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("16")
.dbName("my_app")
.mirror("123456789.dkr.ecr.ap-southeast-2.amazonaws.com/mirrored")
.build()
.start();
}
```
Alternatively, set the mirror globally via a system property or
`ebean.test.containers.mirror` in a properties file, avoiding code changes per project.
### Read-only datasource (for tests using read-replica simulation)
Call `.autoReadOnlyDataSource(true)` on the `DatabaseBuilder` to automatically
create a second read-only datasource pointing at the same container:
```java
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.autoReadOnlyDataSource(true) // test read-only queries against same container
.build();
}
```
### Dump metrics on shutdown
Useful for performance analysis during test runs:
```java
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.dumpMetricsOnShutdown(true)
.dumpMetricsOptions("loc,sql,hash")
.build();
}
```
### PostGIS: LW mode (HexWKB)
For PostGIS with DriverWrapperLW (HexWKB binary geometry encoding), set `.useLW(true)`.
This switches the JDBC URL prefix to `jdbc:postgresql_lwgis://` and requires the
`net.postgis:postgis-jdbc` dependency on the test classpath:
```xml
<!-- add to pom.xml test dependencies when using useLW(true) -->
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2024.1.0</version>
<scope>test</scope>
</dependency>
```
```java
@Bean
PostgisContainer postgres() {
return PostgisContainer.builder("16")
.dbName("my_app")
.useLW(true) // use HexWKB + DriverWrapperLW
.build()
.start();
}
```
> **Note**: LW mode is not required for most PostGIS use cases. Only enable it if
> your entities use binary geometry types (e.g., `net.postgis.jdbc.geometry.Geometry`)
> that require the `DriverWrapperLW` driver.
---
## Keeping the container running (local development)
By default, `ebean-test` stops the Docker container when tests finish. To keep it
running between test runs (much faster for local development), create a marker file:
```bash
mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
```
On CI servers, omit this file so containers are cleaned up after each build.
---
## Next Steps
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
with auto-populated random values. See `testing-with-testentitybuilder.md`.
- **Proceed to Step 3** — production database configuration
(`add-ebean-postgres-database-config.md`). Verify this step passes with
`mvn verify` before continuing.
+116
View File
@@ -0,0 +1,116 @@
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
## Purpose
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
for everything the built-in support does not handle.
This guide lists exactly which property types are handled built-in and which require
`ebean-jackson-mapper`.
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
> classpath, Ebean fails fast at startup:
>
> ```text
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
> Jackson ObjectMapper not present for <property>
> ```
---
## Quick reference
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|---|:---:|:---:|
| `String` | ✅ | |
| `List<String>`, `List<Long>` | ✅ | |
| `Set<String>`, `Set<Long>` | ✅ | |
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
| `Map<String, String>` | ✅ | |
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
| POJOs, records, or any other type | | ✅ |
---
## Built-in support (no Jackson required)
The built-in path materialises JSON into the *natural* JSON value types
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
for the following declared property types:
- **`String`** — stored as raw JSON text.
- **`List<String>`** and **`List<Long>`**.
- **`Set<String>`** and **`Set<Long>`**.
- **`Map<K, V>`** where:
- the key `K` is `String` or an **enum**, and
- the value `V` is `Object`, `String`, or a wildcard `?`.
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
are all built-in.
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
---
## Everything else → Jackson `ObjectMapper`
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
requires `ebean-jackson-mapper`:
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
- **POJOs, records, and any other custom type.**
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
---
## Adding `ebean-jackson-mapper`
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>${ebean.version}</version>
</dependency>
```
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
registers the mapper-based JSON support automatically.
---
## Notes
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
JSON keys.
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
(with a JSON fallback on platforms without array support) and supports more element types
than built-in `@DbJson` collections.
- The reason typed value/element collections need a real mapper is that the built-in path
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
type-aware mapper.
---
## Choosing
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
object/string maps) to avoid pulling in Jackson.
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
typed-value maps.
+164
View File
@@ -0,0 +1,164 @@
# Guide: Derived / formula properties — `@Formula` and `@Formula2`
## Purpose
A *formula property* is a read-only entity property whose value is computed by a SQL
expression at query time rather than stored in its own column. Ebean has two
annotations for this:
- **`@Formula`** — you write the **physical SQL** for the `select` (and any `join`),
using the `${ta}` placeholder for the base table alias. Maximum control; verbose.
- **`@Formula2`** — you write a **logical expression** using dot-notation property
paths (e.g. `parent.familyName`). Ebean translates the paths to the correct table
aliases and **adds the required JOINs automatically**.
`@Formula2` is intended as the easier, path-based replacement for `@Formula`. Both
produce read-only properties and behave the same way with respect to default
inclusion (see [Default inclusion](#default-inclusion-and-transient)).
---
## Quick comparison
| | `@Formula` | `@Formula2` |
|---|---|---|
| Expression | Physical SQL columns + aliases | Logical property paths |
| Table alias | `${ta}` placeholder you write | Resolved automatically |
| Joins | You write the `join` clause | Added automatically from the paths |
| Read only | ✅ | ✅ |
| Included by default | ✅ (use `@Transient` to opt out) | ✅ (use `@Transient` to opt out) |
| Usable in `select` / `where` / `orderBy` / `having` | ✅ | ✅ |
| Creates a DB column (DDL) | ❌ | ❌ |
---
## `@Formula` — physical SQL
You supply the SQL `select` fragment, and an optional `join`. Use `${ta}` wherever you
need the base table alias of the entity.
```java
@Entity
public class ParentPerson {
// aggregation via a derived join; ${ta} is the base table alias
@Formula(select = "coalesce(f2.child_count, 0)",
join = "left join (select parent_id, count(*) as child_count"
+ " from child group by parent_id) f2 on f2.parent_id = ${ta}.id")
Integer childCount;
// coalesce across a joined table using an explicit join alias (j1)
@Formula(select = "coalesce(${ta}.family_name, j1.family_name)",
join = "join parent_person j1 on j1.id = ${ta}.parent_id")
String effectiveFamilyName;
}
```
Notes:
- The `join` string must start with `join` or `left join`.
- You manage the join aliases (`j1`, `f2`, …) yourself and reference them in `select`.
- `@Formula` is `@Repeatable` and supports a `platforms()` restriction.
---
## `@Formula2` — logical property paths
Write the expression using property paths. Ebean resolves each path to the right table
alias and adds the joins it needs.
```java
@Entity
public class ParentPerson {
@ManyToOne
GrandParentPerson parent;
String familyName;
// Ebean automatically left joins 'parent' and resolves the aliases
@Formula2("coalesce(familyName, parent.familyName)")
String derivedFamilyName;
}
```
A query selecting `derivedFamilyName` produces (roughly):
```sql
select t0.id, coalesce(t0.family_name, t1.family_name)
from parent_person t0
left join grand_parent_person t1 on t1.id = t0.parent_id
```
Multi-level paths join through each step:
```java
// joins parent and parent.parent automatically
@Formula2("coalesce(familyName, parent.familyName, parent.parent.familyName)")
String deepFamilyName;
```
`@Formula2` works wherever a normal property does — the required joins are added
automatically in each case:
```java
// selected explicitly
DB.find(ParentPerson.class).select("derivedFamilyName").findList();
// used in where (auto-joins even when not selected)
DB.find(ParentPerson.class).where().eq("derivedFamilyName", "Smith").findList();
// used in order by
DB.find(ParentPerson.class).orderBy("derivedFamilyName").findList();
// referenced via a path from another bean
DB.find(ChildPerson.class).where().eq("parent.derivedFamilyName", "Smith").findList();
```
It also resolves correctly inside nested `fetch()` joins, so a `@Formula2` on a fetched
association is computed with its own joins relative to that association.
Notes:
- The expression supports any SQL function whose arguments are logical property paths.
- `@Formula2` supports a `platforms()` restriction.
- No `${ta}` and no hand-written join — that is the point of `@Formula2`.
---
## Default inclusion and `@Transient`
Both annotations are **included in queries by default** (just like a normal mapped
property). When no explicit `select()`/`fetch()` is given, the formula — and for
`@Formula2` the joins it requires — are added to the query.
Add `@Transient` to make the formula **opt-in**: it is then **not** selected by default
and must be requested explicitly via `select()` or `fetch()`. Do this when the formula
(or the joins it needs) is relatively expensive.
```java
// not selected by default; must be requested explicitly
@Transient
@Formula2("coalesce(familyName, parent.familyName)")
String lazyDerivedFamilyName;
```
```java
DB.find(ParentPerson.class)
.select("lazyDerivedFamilyName") // explicitly included, join auto-added
.findList();
```
This is the same `@Transient` opt-out mechanism used by `@Formula`.
---
## Which should I use?
- Prefer **`@Formula2`** for expressions over property paths (coalesce/case/functions
across associations). It is shorter, refactor-friendly, and the joins stay correct as
the model changes.
- Use **`@Formula`** when you need raw SQL that does not map cleanly to property paths —
for example a derived aggregate sub-select / dynamic view, or vendor-specific SQL.
For read models that exist only to carry computed values, also consider projecting to a
DTO instead of mapping the formula onto the entity — see
[writing-ebean-query-beans.md](writing-ebean-query-beans.md).
+262
View File
@@ -0,0 +1,262 @@
# Guide: Ebean query metrics and naming
## Purpose
This guide explains the metrics Ebean captures, how the metric **name** for a query
is derived, and how you influence that name with `setLabel(..)` and **profile
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
Use this guide when you want to identify a query in metrics/telemetry, when a query
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
---
## Overview
Ebean records timing and counter metrics for the work it does. Every metric has a
**name** whose leading segment identifies the kind of work:
| Prefix | What it measures | Example name |
|---|---|---|
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
| `orm.update.` | ORM update statement | `orm.update.<label>` |
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
| `l2n.` | L2 cache region | `l2n.customer.hit` |
The rest of this guide focuses on **`orm.` query names**, which is where labels and
profile locations apply.
---
## How an ORM query name is derived
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
of three sources, in priority order:
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
3. **Neither** — the bean type plus the query type (e.g. `findList`).
| Root query source | Resulting name |
|---|---|
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
could be used for any bean), so the bean type is prefixed. A profile location is already
unique and type-independent, so it is used as-is.
### Step 1 - Label a query explicitly
```java
List<Customer> customers = DB.find(Customer.class)
.setLabel("custMain")
.findList();
// metric name: orm.Customer.custMain
```
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
used as-is, and an unlabelled DTO query uses just the DTO type:
```java
DB.findDto(CustomerDto.class, sql)
.setLabel("byEmail")
.findList();
// metric name: dto.CustomerDto.byEmail
// profile location only -> dto.<location> (no type prefix)
// unlabelled -> dto.CustomerDto
```
### Step 2 - Use a profile location (preferred for finders / query beans)
A profile location identifies a query by its **call site** (`Class.method`) instead of a
hand-written label.
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
setup when using query beans / finders), Ebean assigns each query a profile location
derived from its call site — no code is required:
```java
List<Customer> customers = new QCustomer()
.status.eq(Status.ACTIVE)
.findList();
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
```
The enhancer derives the location from the calling code (the method that runs the query),
and for many call sites it includes the **source line number** (e.g.
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
names automatically.
**Setting one explicitly.** You can also set a profile location yourself, which is useful
without enhancement or to control the identity:
```java
ProfileLocation LOC = ProfileLocation.create();
List<Customer> customers = DB.find(Customer.class)
.setProfileLocation(LOC)
.where().eq("status", Status.ACTIVE)
.findList();
// metric name: orm.<DeclaringClass>.<method>
```
Factory choices:
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
- `ProfileLocation.createWithLine()` — includes the source line number
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
**distinct** names.
- `ProfileLocation.create("label")` — a named location (used for named transactions).
> Note: a location with no line number (`create()`, or a call site the enhancer emits
> without a line) means two different queries in the same method share one name. The
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
> same-method call sites in the name itself.
---
## Secondary (lazy / query) load naming
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
**secondary** query. Its name **extends the parent query's full name** with the relative
path and the load mode (`lazy` or `query`), joined with `.`:
```
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
```
So a secondary load is always an exact extension of its parent metric name, which makes
the relationship obvious in dashboards.
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
Lazy loading:
```
orm.Customer.custMain
orm.Customer.custMain.orders.lazy
orm.Customer.custMain.orders.lazy.details.lazy
```
Secondary eager `fetchQuery()` loading:
```
orm.Customer.custMain
orm.Customer.custMain.orders.query
orm.Customer.custMain.orders.query.details.query
```
The same applies with a **profile-location** root (no explicit `setLabel`):
```
orm.CustomerFinder.byName
orm.CustomerFinder.byName.contacts.lazy
```
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
type — it inherits the parent's name so it relates back to where the load originated.
---
## Inline SQL comment
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
profile-location label) as an inline SQL comment, which is useful for matching slow
queries in database logs back to application code:
```sql
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
```
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
label. Secondary queries use their full extended name
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
commented.
Disable it via the builder:
```java
Database.builder()
.includeLabelInSql(false)
.build();
```
---
## Collecting metrics at runtime
Read collected metrics through `Database.metaInfo()`:
```java
import io.ebean.meta.MetaQueryMetric;
import io.ebean.meta.ServerMetrics;
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
for (MetaQueryMetric q : metrics.queryMetrics()) {
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
q.name(), // e.g. orm.Customer.custMain
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
q.count(), q.total(), q.mean());
}
```
Key API:
- `database.metaInfo()``MetaInfoManager`.
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
reset; `visitMetrics(visitor)` for streaming.
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
---
## Mapping to avaje-metrics tags
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
form, with the bean type carried as a `type` tag:
```
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
```
Because the entity is available as the `type` tag, two different-entity queries that
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
Prometheus, StatsD) without needing the bean type in the name.
For the integration setup, see the avaje-metrics guide
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
---
## Troubleshooting
### A query shows up as `orm.<Bean>.findList` (no useful identity)
It has neither a label nor a profile location. Add `setLabel(..)` or a
`ProfileLocation`, or apply a profile location on the finder / query bean.
### Two queries in one method share a metric name
This happens when the profile location for those call sites has no line number. With
enhancement, many call sites already include a line number; for those that don't, use
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
different entity types.
### A secondary (lazy / query) load isn't grouped under its parent
Secondary names extend the parent's full name. If the parent has no label or profile
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
that. Give the root query a label or profile location for a stable parent name.
+242
View File
@@ -0,0 +1,242 @@
# Guide: Ebean query plan capture
## Purpose
This guide explains how to enable and configure **query plan capture** in Ebean — the
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
queries, so you can diagnose missing indexes and poor plans in production.
Use this guide when you want Ebean to record real query plans, when tuning the capture
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
---
## Overview
Query plan capture is a **two-phase** mechanism:
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
slower than a threshold, captures the actual **bind values** that were used. This is
cheap: it just remembers the parameters of a slow execution.
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
against the database to obtain the execution plan, producing `MetaQueryPlan` results
that are handed to a `QueryPlanListener`.
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
happens periodically or on demand, against representative bind values, rather than on
every slow query.
Two ways to trigger phase 2:
- **Automatic periodic capture** — a background timer collects plans on a schedule.
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
(this is what remote tooling such as ebean-insight uses).
Plan capable queries are:
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
does not double-count in `queryPlanInit`.
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
Specifically **excluded** are:
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
never captures bind values and cannot be `EXPLAIN`'d.
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
> incur no extra cost beyond their existing timing metric.
---
## Step 1 - Enable bind capture
Bind capture is the master switch; nothing is captured until it is on.
> **Security — bind values may contain PII.** Bind capture records the **actual
> parameter values** used by slow query executions, and those values are stored
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
> plan). They can therefore contain personal or otherwise sensitive data. Capture
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
> that data exposure is acceptable, restrict who can read captured plans, and
> prefer arming specific query hashes (Step 3) over a low global threshold so you
> capture the minimum needed.
```java
Database database = Database.builder()
.queryPlanEnable(true) // turn on bind capture
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
.build();
```
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
Equivalent `application.properties` (avaje-config / properties):
```properties
queryPlan.enable=true
queryPlan.thresholdMicros=100000
```
---
## Step 2 - Enable automatic periodic capture (optional)
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
```java
Database database = Database.builder()
.queryPlanEnable(true)
.queryPlanThresholdMicros(100_000)
.queryPlanCapture(true) // turn on the periodic capture timer
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
.queryPlanListener(capture -> {
for (var plan : capture.plans()) {
System.out.println(plan.label() + "\n" + plan.plan());
}
})
.build();
```
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
exceeded, bounding the database load. Default **10000** (10s).
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
Properties form:
```properties
queryPlan.enable=true
queryPlan.thresholdMicros=100000
queryPlan.capture=true
queryPlan.capturePeriodSecs=600
queryPlan.captureMaxTimeMillis=10000
queryPlan.captureMaxCount=10
```
---
## Step 3 - Capture on demand (foreground)
Instead of (or in addition to) the periodic timer, drive capture through
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
arms specific slow queries by their plan hash.
```java
import io.ebean.meta.MetaInfoManager;
import io.ebean.meta.MetaQueryPlan;
import io.ebean.meta.QueryPlanInit;
import io.ebean.meta.QueryPlanRequest;
MetaInfoManager meta = database.metaInfo();
// Phase 1: arm bind capture - either all plans or specific hashes
QueryPlanInit init = new QueryPlanInit();
init.setAll(true); // or init.add("<planHash>", 50_000);
init.thresholdMicros(100_000);
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
// ... let the application run so slow executions capture their bind values ...
// Phase 2: collect plans now (runs EXPLAIN)
QueryPlanRequest request = new QueryPlanRequest();
request.maxCount(10);
request.maxTimeMillis(10_000);
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
```
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
arms a specific plan (a hash of `"all"` is treated as all).
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
bound the work, mirroring the periodic settings.
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
`captureMicros()`, and `whenCaptured()`.
---
## Step 4 - EXPLAIN dialect
Ebean chooses the `EXPLAIN` statement per database platform:
| Platform | EXPLAIN used |
|---|---|
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
| Oracle | `EXPLAIN PLAN FOR <sql>` |
| SQL Server | platform-specific logger |
| H2 / MySQL / other | `explain <sql>` |
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
```java
Database.builder()
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
.build();
```
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
> executes** the query to produce real timings. For non-idempotent or expensive queries,
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
---
## Related setting: internal plan TTL
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
the same builder.
---
## Troubleshooting
### No plans are captured
1. `queryPlanEnable(true)` must be set — it is the master switch.
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
3. For periodic capture, also set `queryPlanCapture(true)`.
4. Queries must actually run slower than the threshold to have their binds captured.
### Plans appear but nothing is reported anywhere
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
### Capture adds noticeable database load
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
`queryPlanExplain` to a non-ANALYZE form.
### An unlabelled SqlQuery or update metric never offers plan capture
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
capture and are intentionally excluded.
+797
View File
@@ -0,0 +1,797 @@
# Entity Bean Creation Guide for AI Agents
**Target Audience:** AI systems (Claude, Copilot, ChatGPT, etc.)
**Purpose:** Learn how to generate clean, idiomatic Ebean entity beans
**Key Insight:** Ebean entity fields must be non-public (no public fields). Accessors don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed
**Language:** Java
**Framework:** Ebean ORM
---
## Quick Rules
Before writing entity code, remember:
| Requirement | Needed? | Notes |
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------|
| `@Entity` annotation | ✅ **YES** | Marks class as persistent entity |
| `@Id` annotation | ✅ **YES** | Marks primary key field |
| Getters/setters (or other accessors) | ✅ **YES** | Needed for application code to access fields. Naming can be JavaBeans, fluent, or custom — no specific convention required. |
| Default constructor | ❌ **NO** | Not required. Ebean can instantiate without it. |
| equals/hashCode | ❌ **NO** | Ebean auto-enhances these at compile time. |
| toString() | ❌ **NO** | Ebean auto-enhances this. Don't implement with getters. |
| `@Version` | ⚠️ **OPTIONAL** | Use for optimistic locking. Highly recommended. |
| `@WhenCreated` | ⚠️ **OPTIONAL** | Auto-timestamp creation time. Highly recommended. Use for audit trail. |
| `@WhenModified` | ⚠️ **OPTIONAL** | Auto-timestamp modification time. Highly recommended. Use for audit trail. |
**Critical:**
- Prefer primitive `long` for `@Id` and `@Version`, NOT `Long` object.
- Fields should be non-public: **private**, **protected**, or package-private.
- If you add accessors, they do NOT need to follow Java bean conventions.
---
## Naming Conventions: The D* (Domain) Prefix Pattern
Entity beans represent internal domain/persistence model details. It's a common best practice in Ebean projects to use the **D* prefix** (D for Domain) for entity class names.
**Why use D* prefix?**
1. **Avoid name clashes with DTOs** - Your public API may have `Customer` (DTO), but your entity is `DCustomer` (Domain). They're clearly different.
2. **Signal intent clearly** - The D prefix immediately tells developers "this is an internal domain class, not part of the public API"
3. **Clarify conversions** - When converting `DCustomer``Customer` (DTO), the direction is obvious
4. **Separate concerns** - API classes in one package (no prefix), domain classes in another (with D prefix)
**Example naming pattern:**
- Entity: `DCustomer`, `DOrder`, `DProduct`, `DInvoice`
- DTO: `Customer`, `Order`, `Product`, `Invoice`
- Converter: `DCustomerMapper.toDTO(DCustomer)``Customer`
**Where to place entities:**
- Entities: `com.example.domain.entity` (or `persistence`)
- DTOs: `com.example.api.model` or `com.example.dto`
**When to use D* prefix:**
-**DO** use for entity beans (internal domain model)
-**DO** use when you have parallel DTO classes with similar names
-**DON'T** use for DTOs or public API classes
-**DON'T** use if you have no DTOs and entities are your public API
Example with and without prefix:
```java
// With D* prefix (recommended - allows both entity and DTO to exist)
@Entity
public class DCustomer {
@Id private long id;
private String name;
// ... entity-specific fields and methods
}
// Public API DTO (no D prefix)
public record Customer(long id, String name) {
// ... conversion method
}
// Conversion
public static Customer toDTO(DCustomer entity) {
return new Customer(entity.getId(), entity.getName());
}
```
This naming convention is optional but highly recommended for projects with separate domain and API layers.
---
## Minimal Entity (No Boilerplate)
This is a complete, valid Ebean entity:
```java
@Entity
public class Customer {
@Id
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
**Why this works:**
-`@Entity` marks it as persistent
-`@Id private long id` is the primary key
- ✅ Private fields (Ebean does NOT support public fields without expert flags enabled)
- ✅ Accessors can follow any naming convention, and can be omitted when field access is preferred
- ✅ No default constructor needed
- ✅ No equals/hashCode needed (Ebean enhances these)
**What Ebean does at compile time:**
- Enhances equals/hashCode based on @Id
- Adds field change tracking
- Enables lazy loading
- Enhances toString()
**Result:** Your entity is now fully functional with zero boilerplate.
---
## Pattern 1: Basic Entity
**Use this when:** You need a simple persistent object.
```java
@Entity
public class Product {
@Id
private long id;
private String name;
private String description;
private BigDecimal price;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
}
```
**What you get:**
- Primary key: `id` (private field, accessed via getter)
- Three properties: `name`, `description`, `price` (private fields, accessed via getters/setters)
- Automatic equals/hashCode based on id
- Full ORM functionality
---
## Pattern 2: Entity with Audit Trail
**Use this when:** You need to track who/when created/modified data.
```java
@Entity
public class Order {
@Id
private long id;
@Version
private long version;
@WhenCreated
private Instant createdAt;
@WhenModified
private Instant modifiedAt;
private String orderNumber;
private BigDecimal totalAmount;
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public Instant getCreatedAt() {
return createdAt;
}
public Instant getModifiedAt() {
return modifiedAt;
}
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}
}
```
**What you get:**
- `version`: Optimistic locking (prevents concurrent update conflicts)
- `createdAt`: Automatically set when inserted (Ebean manages this)
- `modifiedAt`: Automatically updated on every modification (Ebean manages this)
**Example usage:**
```java
// Create
Order order = new Order();
order.setOrderNumber("ORD-001");
order.setTotalAmount(new BigDecimal("99.99"));
database.save(order); // createdAt is automatically set by Ebean
// Modify
order.setTotalAmount(new BigDecimal("109.99"));
database.update(order); // version incremented, modifiedAt updated automatically
// Check when modified
System.out.println(order.getModifiedAt()); // Current timestamp
```
---
## Pattern 3: Entity with Constructor
**Use this when:** Domain logic requires initialization or validation.
```java
@Entity
public class Invoice {
@Id
private long id;
@Version
private long version;
private String invoiceNumber;
private String customerName;
private BigDecimal amount;
public Invoice(String invoiceNumber, String customerName, BigDecimal amount) {
this.invoiceNumber = invoiceNumber;
this.customerName = customerName;
this.amount = amount;
}
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public String getInvoiceNumber() {
return invoiceNumber;
}
public String getCustomerName() {
return customerName;
}
public BigDecimal getAmount() {
return amount;
}
}
```
**When to add a constructor:**
- ✅ Required fields must be set during creation
- ✅ Validation needs to happen on initialization
- ✅ Domain logic needs setup
**When NOT to add:**
- ❌ If users will just set fields afterwards anyway
- ❌ If there are many optional fields
---
## Pattern 4: Entity with Relationships
**Use this when:** You need associations to other entities.
```java
@Entity
public class Customer {
@Id
private long id;
@Version
private long version;
@WhenCreated
private Instant createdAt;
private String name;
private String email;
@OneToMany(mappedBy = "customer")
private List<Order> orders; // Use List, not Set
@ManyToOne
private Address billingAddress;
public long getId() {
return id;
}
public long getVersion() {
return version;
}
public Instant getCreatedAt() {
return createdAt;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<Order> getOrders() {
return orders;
}
public Address getBillingAddress() {
return billingAddress;
}
public void setBillingAddress(Address billingAddress) {
this.billingAddress = billingAddress;
}
}
```
**Important:**
- Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs)
- `mappedBy` means Order.customer is the owner
- Relationships are lazy-loaded by default
---
## What NOT to Do (Anti-Patterns)
### ❌ Anti-Pattern 1: Public Fields
**DON'T:**
```java
@Entity
public class Customer {
@Id public long id; // ❌ Public field - not supported
public String name; // ❌ Public field - not supported
}
```
**DO:**
```java
@Entity
public class Customer {
@Id
private long id; // ✅ Private field with getter
private String name; // ✅ Private field with accessors
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
**Why:** Ebean does NOT support public fields. Fields must be private and accessed via getters/setters or other accessor methods. Public fields bypass Ebean's tracking mechanisms and will cause data consistency issues.
---
### ❌ Anti-Pattern 2: Use Long Object Instead of Primitive
**DON'T:**
```java
@Entity
public class Customer {
@Id
private Long id; // ❌ Object type
private String name;
}
```
**DO:**
```java
@Entity
public class Customer {
@Id
private long id; // ✅ Primitive type
private String name;
}
```
**Why:** Performance, nullability semantics, Ebean optimization.
---
### ❌ Anti-Pattern 3: Implement equals/hashCode
**DON'T:**
```java
@Entity
public class Customer {
@Id
private long id;
private String name;
@Override
public boolean equals(Object o) { // ❌ Unnecessary
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id == customer.id;
}
@Override
public int hashCode() { // ❌ Unnecessary
return Objects.hash(id);
}
}
```
**DO:**
```java
@Entity
public class Customer {
@Id
private long id;
private String name;
public long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// Ebean enhances equals/hashCode automatically
}
```
**Why:** Ebean's enhancement is optimized for ORM operations. Your implementation might conflict with Ebean's tracking.
---
### ❌ Anti-Pattern 4: Use Set for Collections
**DON'T:**
```java
@Entity
public class Customer {
@Id
private long id;
@OneToMany(mappedBy = "customer")
private Set<Order> orders; // ❌ Set calls equals/hashCode before IDs assigned
}
```
**DO:**
```java
@Entity
public class Customer {
@Id
private long id;
@OneToMany(mappedBy = "customer")
private List<Order> orders; // ✅ List doesn't require equals/hashCode on unsaved beans
}
```
**Why:** Set calls equals/hashCode immediately. New beans don't have IDs yet, causing issues.
---
### ❌ Anti-Pattern 5: toString() with Getters
**DON'T:**
```java
@Entity
public class Customer {
@Id
private long id;
private String name;
@Override
public String toString() { // ❌ Uses getters
return "Customer{" +
"id=" + getId() +
", name='" + getName() + '\'' +
'}';
}
public long getId() { return id; }
public String getName() { return name; }
}
```
**Why:** In a debugger, toString() is called automatically. Getters can trigger lazy loading, changing debug behavior.
**DO:** Either don't implement toString(), or access fields directly:
```java
@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
```
---
### ❌ Anti-Pattern 6: @Column(name=...) for Naming Convention
**DON'T:**
```java
@Entity
public class Customer {
@Id
private long id;
@Column(name = "first_name") // ❌ Unnecessary
private String firstName;
}
```
**DO:**
```java
@Entity
public class Customer {
@Id
private long id;
private String firstName; // ✅ Ebean uses naming convention: first_name
}
```
**Why:** Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
---
## What Ebean Enhancement Provides
At compile time, Ebean enhances your entity classes:
1. **equals/hashCode** - Based on @Id, optimal for ORM
2. **Field change tracking** - Knows which fields were modified
3. **Lazy loading** - Collections and relationships load on demand
4. **Persistence context** - Manages identity and state
5. **toString()** - Auto-implemented (don't override with getters)
**Result:** Your entity bean is minimal, but fully featured.
---
## Field Types
**Recommended for ID/Version:**
- `long` (primitive) ✅ Use this
- `int` (primitive) ✅ Use this
- `UUID` ✅ Use this
**Not recommended:**
- `Long` object ⚠️ Avoid (use primitive long)
- `Integer` object ⚠️ Avoid (use primitive int)
**For other fields:**
- Use standard Java types: `String`, `BigDecimal`, `Instant`, `LocalDate`, etc.
- Use primitives where nullable semantics don't apply: `int`, `long`, `boolean`
- Use objects where null has meaning: `String`, `BigDecimal`, `LocalDate`
---
## Example: Building an Entity Step by Step
Start minimal, add what you need:
**Step 1: Minimal**
```java
@Entity
public class BlogPost {
@Id long id;
String title;
String content;
public long getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
```
**Step 2: Add audit trail**
```java
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
}
```
**Step 3: Add author relationship**
```java
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
@ManyToOne
Author author;
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Author getAuthor() { return author; }
public void setAuthor(Author author) { this.author = author; }
}
```
**Step 4: Add constructor if needed**
```java
@Entity
public class BlogPost {
@Id long id;
@Version long version;
@WhenCreated Instant createdAt;
@WhenModified Instant modifiedAt;
String title;
String content;
@ManyToOne
Author author;
public BlogPost(String title, String content, Author author) {
this.title = title;
this.content = content;
this.author = author;
}
public long getId() { return id; }
public long getVersion() { return version; }
public Instant getCreatedAt() { return createdAt; }
public Instant getModifiedAt() { return modifiedAt; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public Author getAuthor() { return author; }
public void setAuthor(Author author) { this.author = author; }
}
```
Each step adds only what's necessary for the entity structure. Accessors are always included so application code can use the entity.
---
## Usage Examples
### Creating and saving
```java
Customer customer = new Customer();
customer.setName("Alice");
database.save(customer); // id auto-generated
```
### Finding
```java
Customer found = database.find(Customer.class, 1);
System.out.println(found.getName());
```
### Updating
```java
found.setName("Bob");
database.update(found); // version auto-incremented
```
### Collections (relationships)
```java
Customer customer = database.find(Customer.class, 1);
List<Order> orders = customer.getOrders(); // Lazy loads automatically
```
---
## Summary for AI Agents
When generating Ebean entity beans:
**DO:**
- Use primitive `long` for @Id and @Version
- Keep entities minimal (just fields + @Entity + @Id)
- Use @Version for concurrency control
- Use @WhenCreated/@WhenModified for audit trail
- Use List for collections, not Set
- Add constructors only if domain logic requires it
- Add getters/setters for all fields that application code needs to read or write
**DON'T:**
- Use Long object for @Id/@Version
- Implement equals/hashCode
- Implement toString() with getters
- Use Set for @OneToMany/@ManyToMany
- Add unnecessary @Column annotations
- Add default constructors "just in case"
**Result:** Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
---
## Related Documentation
- Entity Bean Best Practices: `/docs/best-practice/`
- JPA Mapping Reference: `/docs/mapping/jpa/`
- Ebean Extensions: `/docs/mapping/extensions/`
- First Entity Guide: `/docs/intro/first-entity/`
+136
View File
@@ -0,0 +1,136 @@
# Immutable bean cache for read-only references
This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
references (for example `Label` references reused across many entities).
Use this when you want:
- fewer lazy-load SQL calls for assoc-one references via caching
- reusable fetch-group-based loading for cache misses
---
## Step 1 - Build an immutable cache (typical via builder)
```java
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();
ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
.loading(database, fetchGroup)
.maxSize(10_000)
.maxIdleSeconds(300)
.maxSecondsToLive(6_000)
.build();
```
`loading(...)` uses the query shape:
- `select(fetchGroup)`
- `setUnmodifiable(true)`
- `where().idIn(ids)`
- `findMap()`
Alternative domain example:
```java
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
.select("name,version")
.fetch("billingAddress", "line1,city")
.fetch("shippingAddress", "line1,city")
.build();
ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
.loading(database, customerGroup)
.build();
```
---
## Step 2 - Attach cache to the root query
```java
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
.setId(id)
.setUnmodifiable(true)
.using(labelCache)
.findOne();
```
`using(...)` is on the root query. Ebean will use this cache for matching
bean types when resolving references.
---
## Use loading helper for simple memoization
If you don't need policy controls, use the shorthand helper:
```java
ImmutableBeanCache<Label> labelCache =
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
```
With `ebean-core` on the classpath, builder policy settings are backed by core
cache implementation (including periodic trim / eviction).
---
## Unmodifiable vs mutable query behavior
### Unmodifiable query path
`setUnmodifiable(true)` disables lazy loading. If you need association content
in cached beans make sure that is included in the fetch group.
```java
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();
```
### Mutable query path
On a mutable query (no `setUnmodifiable(true)`), references populated from the
immutable cache are still mutable beans in that object graph. Additional
unloaded properties can still lazy load as normal.
Typical pattern:
1. cache serves already-loaded reference properties (for example `version`)
2. later access to unloaded properties (for example `labelTexts`) triggers
normal lazy loading
---
## Understand secondary query behavior (`+query`, `+lazy`)
When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
the immutable caches configured on the root query are propagated to those
secondary queries.
That means assoc-one references resolved in secondary query paths can still hit
the immutable cache.
---
## Operational note (TTL / max size)
Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
loader-based memoization.
---
## Testing checklist
1. Hit / partial hit / miss behavior for `getAll(ids)`
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
3. Mutable path: additional unloaded properties can still lazy load
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
5. If needed associations are in fetch group, assert no extra SQL for those
accesses
@@ -0,0 +1,206 @@
# Guide: Using Lombok with Ebean Entity Beans
## Purpose
This guide explains which Lombok annotations are safe and recommended for Ebean
entity beans, which ones to avoid, and why. It is written as prescriptive instructions
for AI agents and developers.
---
## The Core Rule
> **Do NOT use `@Data` on Ebean entity beans.**
Use `@Getter` + `@Setter` instead, with the optional `@Accessors(chain = true)` for
a fluent setter style.
---
## Why `@Data` is Incompatible with Ebean
`@Data` is a convenience annotation that is equivalent to applying `@Getter`,
`@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode` together.
Three of those are problematic for Ebean entity beans:
### 1. `@EqualsAndHashCode` (included in `@Data`) — breaks entity identity
`@Data` generates `hashCode()` and `equals()` based on all non-static, non-transient
fields. Ebean entity beans have identity semantics — two references to the same database
row should be considered equal based on their `@Id` value, not field-by-field comparison.
Problems caused:
- Inconsistent `hashCode` before and after persist (the `@Id` field is `0` on a new
entity, then changes after insert — violating the `hashCode` contract for collections)
- Entities placed in a `Set` or `HashMap` before saving will be unfindable after saving
- Ebean's internal identity map and dirty checking can be confused
### 2. `@ToString` (included in `@Data`) — triggers unexpected lazy loading
`@Data` generates a `toString()` that accesses **all** fields, including
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
outside of a transaction triggers a `LazyInitialisationException` or fires an unexpected
SQL query, which can:
- Cause subtle bugs in logging statements
- Trigger N+1 queries in test output or debug logging
- Fail with an exception if no active transaction exists
### 3. `@RequiredArgsConstructor` (included in `@Data`) — unnecessary for Ebean
Ebean does not require a default constructor — it can construct entity instances without
one. `@RequiredArgsConstructor` therefore adds nothing useful to entity beans.
---
## Recommended Annotation Set
Use exactly these three Lombok annotations on every Ebean entity bean:
```java
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "my_table")
public class MyEntity {
// ...
}
```
| Annotation | Purpose |
|---|---|
| `@Getter` | Generates `getFoo()` / `isFoo()` accessor methods |
| `@Setter` | Generates `setFoo(value)` mutator methods; Ebean enhancement intercepts these for dirty tracking |
| `@Accessors(chain = true)` | Makes setters return `this`, enabling fluent/builder-style property setting |
---
## `@Accessors(chain = true)` — Fluent Setter Style
With `chain = true`, setters return `this` instead of `void`, allowing method chaining:
```java
// without chain = true (void setters)
CMachine machine = new CMachine();
machine.setMake("Toyota");
machine.setModel("Hilux");
machine.setStatus("active");
// with @Accessors(chain = true)
CMachine machine = new CMachine()
.setMake("Toyota")
.setModel("Hilux")
.setStatus("active");
```
This is particularly useful when building test data:
```java
CMachine machine = new CMachine()
.setGid(UUID.randomUUID())
.setMachineType("HV")
.setStatus("active")
.setMake("Komatsu")
.setModel("PC200");
database.save(machine);
```
Ebean's bytecode enhancement is fully compatible with chained setters — the
enhancement intercepts each `setFoo()` call to record which fields have been modified
(dirty checking), regardless of whether the setter returns `void` or `this`.
---
## `@Accessors(fluent = true)` — also compatible
`@Accessors(fluent = true)` removes the `get`/`set`/`is` prefix, generating `name()`
(getter) and `name(value)` (setter) instead of `getName()` and `setName(value)`.
Ebean does **not** require JavaBeans naming conventions — it can work with any accessor
method style, including fluent accessors with no prefix. `@Accessors(fluent = true)` is
therefore compatible with Ebean.
`@Accessors(chain = true)` is the more common choice in practice (it keeps the familiar
`get`/`set` prefix while adding method chaining), but `fluent = true` is a valid
alternative if that style is preferred consistently across the codebase.
---
## Full Entity Bean Example
```java
package com.example.repository.data;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "machine")
public class CMachine {
@Id
private long id;
@Version
private int version;
@Column(nullable = false, unique = true)
private UUID gid;
@Column(nullable = false, length = 10)
private String machineType;
@Column(length = 200)
private String make;
@Column(length = 200)
private String model;
@WhenCreated
private Instant created;
@WhenModified
private Instant lastModified;
}
```
---
## Summary: Lombok Annotations and Ebean Compatibility
| Lombok Annotation | Compatible? | Notes |
|---|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `@Getter` | ✅ Safe | Use on every entity bean |
| `@Setter` | ✅ Safe | Use on every entity bean; enhancement intercepts these |
| `@Accessors(chain = true)` | ✅ Safe | Recommended for fluent construction style |
| `@ToString` | ❌ Avoid | Ebean does a better job and handles recursion |
| `@EqualsAndHashCode` | ❌ Avoid | Breaks entity identity and `@Id`-based equality |
| `@Data` | ❌ Avoid | Includes `@EqualsAndHashCode` and `@ToString` — both problematic |
| `@Value` | ❌ Avoid | Makes fields final — incompatible with Ebean's field-level bytecode enhancement |
| `@Accessors(fluent = true)` | ✅ Safe | Removes `get`/`set` prefix — Ebean does not require JavaBeans naming conventions and works with any accessor style |
| `@Builder` | ⚠️ Careful | Usable on non-entity helper/factory classes; on entity beans it requires a no-arg constructor alongside it and offers no advantage over `@Accessors(chain = true)` |
---
## Relationship with Ebean Bytecode Enhancement
Ebean's bytecode enhancement (applied by `ebean-maven-plugin` at build time) modifies
the `setXxx()` methods of entity beans to:
1. Mark the field as dirty (changed) so only modified fields are included in UPDATE statements
2. Support lazy loading of associations when a getter is called on an unloaded field
For this to work correctly, Ebean needs:
- Accessor methods for each persistent field (any naming style is fine — `getFoo()`, `foo()`, or no accessors at all; Ebean can also access fields directly)
- No override of `hashCode()` / `equals()` that would interfere with the identity map — which means **no `@Data` or `@EqualsAndHashCode`**
@@ -0,0 +1,785 @@
# Guide: Mapping entity graphs to DTOs — `query.mapTo(Dto.class)`
## Purpose
`query.mapTo(SomeDto.class)` maps an entity query result to a **nested DTO graph**
DTO fields can themselves be DTOs (`ToOne`) or `List<Dto>`/`Set<Dto>` (`ToMany`), not
just flat scalar columns. Ebean generates the mapper (reflection-free), automatically
derives the query's `select()`/`fetch()` spec from the target DTO's declared shape, and
forces `setUnmodifiable(true)` so any property the mapper needs but wasn't fetched fails
fast with `LazyInitialisationException` instead of silently lazy loading.
This is distinct from the existing flat `asDto(Dto.class)` — see
[Quick comparison](#quick-comparison-mapto-vs-asdto-vs-plain-entity-query) below.
```java
Optional<CustomerDto> dto = new QCustomer()
.id.eq(customerId)
.mapTo(CustomerDto.class)
.findOneOrEmpty();
```
```java
List<CustomerDto> dtos = DB.find(Customer.class)
.where().eq("status", Status.ACTIVE)
.mapTo(CustomerDto.class) // no .select()/.fetch() needed - derived from CustomerDto's shape
.findList();
```
---
## Quick comparison: `mapTo()` vs `asDto()` vs plain entity query
| | `mapTo(Dto.class)` | `asDto(Dto.class)` | Plain entity query |
|---|---|---|---|
| Shape | Nested DTO **graph** (ToOne/ToMany) | Flat, single-row DTO | Entity graph |
| Fetch spec | Auto-derived from the DTO's declared shape | Whatever `select()`/SQL you write | Whatever `select()`/`fetch()` you write |
| Mismatch caught | At compile time (unregistered pair fails fast at first use; codegen fails fast on structural problems) | At runtime (reflection-based constructor/setter matching) | N/A (real entity properties) |
| Identity/de-dup | Yes - repeated source instances map to the same DTO instance (`DtoMapContext`) | N/A (one row in, one DTO out) | Yes (entity/persistence-context identity) |
| Backing pipeline | Executes the entity ORM query, `setUnmodifiable(true)`, maps the resulting graph | Executes SQL directly against a flat `ResultSet` | Executes the entity ORM query |
| Best for | API/read-model responses that mirror a **nested** entity shape | Flat summary rows, reports, native/vendor SQL | Data you intend to mutate and save back |
See also [writing-ebean-query-beans.md](writing-ebean-query-beans.md) (Step 8/9) for
`asDto()` and the general query-shape decision guide.
---
## Basic usage
### 1. Declare a plain DTO
DTOs are plain classes with **no framework attachment** — no annotations required for
the common case (properties matched to the source entity by name):
```java
public class CustomerDto {
private final Long id;
private final String name;
private final AddressDto billingAddress; // nested ToOne
private final List<ContactDto> contacts; // nested ToMany
public CustomerDto(Long id, String name, AddressDto billingAddress, List<ContactDto> contacts) {
this.id = id;
this.name = name;
this.billingAddress = billingAddress;
this.contacts = contacts;
}
public Long getId() { return id; }
public String getName() { return name; }
public AddressDto getBillingAddress() { return billingAddress; }
public List<ContactDto> getContacts() { return contacts; }
}
```
A constructor whose parameters match (by name) a source entity/DTO property is used
for mapping — same shape convention as the existing `DtoQuery`. Getters are used to
read the source's properties — a bare/fluent accessor like `active()` is resolved
automatically too, not just `getActive()`/`isActive()` (useful both for Ebean's own
record entity beans and for ordinary classes that just expose bare-name accessors).
### 2. Register the (source, target) pair
Declare each entity → DTO pair with `@DtoMapping` on a `package-info.java` (a neutral
holder — see [Why `package-info.java`?](#why-package-infojava)):
```java
@DtoMapping(source = Customer.class, target = CustomerDto.class)
@DtoMapping(source = Address.class, target = AddressDto.class)
@DtoMapping(source = Contact.class, target = ContactDto.class)
package org.example.dto;
import io.ebean.annotation.DtoMapping;
```
This triggers `querybean-generator` (the existing annotation processor) to generate a
`CustomerDtoMapper implements DtoMapper<Customer, CustomerDto>` for each pair — no new
Maven/Gradle setup beyond what query beans already require.
### 3. Query with `mapTo(...)`
```java
List<CustomerDto> dtos = DB.find(Customer.class)
.where().eq("status", Status.ACTIVE)
.mapTo(CustomerDto.class)
.findList();
CustomerDto one = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOne();
Optional<CustomerDto> maybe = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOneOrEmpty();
```
`mapTo(...)` works the same from a query bean (`QCustomer`) or a plain `DB.find(...)`/
`ExpressionList` query.
### Paging - `findPagedList()`
`findPagedList()` mirrors `Query#findPagedList()` — the underlying entity query is paged
as normal and each page's result is mapped to the target DTO list:
```java
PagedList<CustomerDto> paged = DB.find(Customer.class)
.where().eq("status", Status.ACTIVE)
.orderBy().asc("name")
.setFirstRow(0)
.setMaxRows(50)
.mapTo(CustomerDto.class)
.findPagedList();
int totalRowCount = paged.getTotalCount(); // page metadata - unaffected by DTO mapping
List<CustomerDto> page1 = paged.getList(); // mapped DTOs for this page
```
Page metadata (`getTotalCount()`, `getTotalPageCount()`, `hasNext()`, `hasPrev()`,
`loadCount()`, ...) reflects the underlying entity query directly; only `getList()`
is mapped (once, cached) to the DTO type.
### An unregistered pair fails fast
If `(Customer.class, SomeDto.class)` was never declared via `@DtoMapping`, the first
`mapTo(SomeDto.class)` call throws immediately:
```
PersistenceException: No DtoMapper registered mapping Customer -> SomeDto
- check @DtoMapping(source = Customer.class, target = SomeDto.class) is declared
on a package-info.java processed by querybean-generator
```
---
## Auto-derived fetch spec
You never write `.select()`/`.fetch()` for a `mapTo(...)` query — the generated mapper
exposes a `fetchGroup()` built directly from the DTO's declared shape, and `mapTo(...)`
applies it automatically:
```java
public CustomerDtoMapper() {
this(new AddressDtoMapper(), new ContactDtoMapper());
}
public CustomerDtoMapper(DtoMapper<Address, AddressDto> billingAddressMapper,
DtoMapper<Contact, ContactDto> contactsMapper) {
this.fetchGroup = FetchGroup.of(Customer.class)
.select("id,name")
.fetch("billingAddress", billingAddressMapper.fetchGroup())
.fetch("contacts", contactsMapper.fetchGroup())
.build();
}
```
Each nested DTO gets its own generated mapper (mirroring MapStruct's per-type mapper
generation), wired together via constructor injection — mappers are stateless and
substitutable, not static singletons. Mapper instances are constructed once, in
dependency order, and reused — see [DtoMapperManager](#one-mapper-instance-per-pair)
below.
---
## Nested collections and identity-aware de-duplication
When the same source entity instance is reachable via more than one path in the graph
(e.g. two `Contact`s sharing the same `Customer`, or the same `Address` referenced from
two paths), the mapper reuses the **same** target DTO instance rather than creating
duplicate-but-equal copies — mirroring the identity semantics the entity graph already
has:
```java
List<CustomerDto> dtos = DB.find(Customer.class).mapTo(CustomerDto.class).findList();
CustomerDto customer = dtos.get(0);
// both contacts share the exact same customer.billingAddress AddressDto instance
assertThat(customer.getContacts().get(0).getCustomer())
.isSameAs(customer.getContacts().get(1).getCustomer());
```
This is done via a `DtoMapContext` threaded through every nested `map(...)` call within
one top-level `mapList(...)`/`findList()` invocation. The generated code only pays for
this when it can actually matter — a DTO that's never nested under another DTO skips
`DtoMapContext` entirely (there's nothing else in scope to de-duplicate against):
```java
// AddressDto is nested under CustomerDto (reachable via multiple contacts) - dedup needed
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(...));
// ContactSummaryDto is only ever mapped as a top-level query result - no dedup possible
// skip DtoMapContext, only ever a top-level mapping
return new ContactSummaryDto(source.getId(), source.getFullName());
// CustomerDto has nested mappers (billingAddress, contacts) but is never itself nested
// DtoMapContext for nested mappers only
return new CustomerDto(source.getId(), source.getName(), ...);
```
The generated comment tells you at a glance which of the three cases applies — useful
when debugging why a `DtoMapContext` is (or isn't) in the generated code for a
particular mapper.
---
## Using generated mappers directly (outside `query.mapTo()`)
Every generated `XxxDtoMapper` is a plain public class — you don't need `ServiceLoader`,
a registry, or a `Database` just to construct or call one directly (though
`DtoMapperManager`, below, is available if you want a shared, DI-friendly lookup). It
always has a public no-arg constructor (delegating to defaults for any nested mappers/
`@DtoConvert` converters) plus an explicit constructor taking those dependencies directly,
and implements `DtoMapper<SOURCE, TARGET>`'s `map(...)`/`mapList(...)`:
```java
CustomerDtoMapper mapper = new CustomerDtoMapper();
CustomerDto dto = mapper.map(customer); // any Customer you already have on hand
List<CustomerDto> dtos = mapper.mapList(customers);
```
This works on **any** entity graph, not just one that just came out of a `mapTo(...)`
query — e.g. entities you loaded with a plain `.fetch(...)` query, entities you just
`.save()`d, or entities built by hand in a test. The only requirement is that whatever the
mapper reads (via plain getters) is actually populated — there's no lazy-loading fallback.
### Testing the mapping in isolation
Because mappers are plain, constructor-injected classes, you can unit test the mapping
logic itself — independent of `query.mapTo()`, the DTO-pair registry, and (for
`@DtoConvert` instance-dispatch converters) `DtoConverterManager` — by passing a test
double straight into the explicit constructor:
```java
SecretCipher upperCasingTestCipher = String::toUpperCase;
ContactConversionDto dto = new ContactConversionDtoMapper(upperCasingTestCipher).map(contact);
assertThat(dto.getSecretCode()).isEqualTo("SHH");
```
No `DtoConverterManager.put(...)` registration needed for this kind of test — the real
production wiring (`DtoConverterManager.get(SecretCipher.class)`) only happens in the
generated no-arg constructor, which the explicit-constructor call above bypasses entirely.
See `TestCustomerDtoGraphMapping` (mapper called directly against a manually queried
graph) and `TestMapperManualUsage` (mapper called directly against hand-built/just-saved
entities, plus the converter test-double case above) in `tests/test-dto-mapping`.
### `DtoMapperManager` — resolving a generated mapper for dependency injection
`new CustomerDtoMapper()` is enough for a single mapper, but if your application wants a
single shared instance of *every* generated mapper (mirroring how `query.mapTo()` resolves
them internally) - e.g. to wire one up for constructor injection into a service, replacing
a hand-written mapper class - use `io.ebean.DtoMapperManager`:
```java
DtoMapperManager manager = new DtoMapperManager(); // ServiceLoader discovery only - no Database needed
CustomerDtoMapper mapper = manager.get(CustomerDtoMapper.class);
```
`DtoMapperManager` has no dependency on `Database` at all - its constructor only does
`ServiceLoader.load(DtoMapperRegister.class)` - so it can be constructed independently,
before (or entirely without) a `Database`, e.g. as a bean in an avaje-inject (or any DI
framework's) dependency graph:
```java
@Factory
class DtoMapperFactory {
@Bean
DtoMapperManager dtoMapperManager() {
return new DtoMapperManager();
}
@Bean
CustomerDtoMapper customerDtoMapper(DtoMapperManager manager) {
return manager.get(CustomerDtoMapper.class);
}
}
```
If you also want `query.mapTo(...)` to use that *exact same* manager instance (so there's
only ever one instance of each generated mapper, whichever path resolves it), register it
via `DatabaseBuilder.putServiceObject` before building the `Database` - this is the same
`putServiceObject`/`getServiceObject` mechanism already used for things like
`AutoMigrationRunner`:
```java
DtoMapperManager sharedManager = new DtoMapperManager();
Database db = Database.builder()
.putServiceObject(DtoMapperManager.class, sharedManager)
.build();
// query.mapTo(...) against `db` now resolves mappers via `sharedManager`
```
If nothing is registered via `putServiceObject`, the `Database` builds its own default
`DtoMapperManager` instance instead - registering one is entirely optional. A standalone
`DtoMapperManager()` construction bypasses the `DatabaseConfigProvider` hook (that hook is
specifically about `Database` startup ordering), so if any of your mappers need a
`@DtoConvert` instance-dispatch converter, register it via `DtoConverterManager.put(...)`
yourself first, exactly as you would before building a `Database`. See
`TestDtoMapperManager` and `TestDtoMapperManagerSharing` in `tests/test-dto-mapping`.
### Recipe: adding extra caller-supplied fields after mapping
Sometimes a target DTO needs a field that isn't sourced from the entity graph at all - e.g.
populated from a separate query or business rule, only when a caller-supplied flag is set.
Rather than the generator supporting partial/builder-based mapping directly, if your DTO is
a record with a "seed from instance" builder (e.g. via `avaje-recordbuilder`'s
`@RecordBuilder`, which generates `Target.builder(existingInstance)`), just map the
graph-sourced fields as usual and layer the extra field on afterwards:
```java
Driver base = mapper.map(cDriver);
Driver full = DriverBuilder.builder(base).fleets(fleets).build();
```
No generator changes needed - the mapped instance is simply the seed for the builder.
---
## Large targets: builder-based construction and named variants
Two features aimed at large, builder-shaped target DTOs (typically OpenAPI-generated records
with a generated builder), where a positional constructor call is unwieldy and a single query
needs to populate the target in more than one shape.
### Builder-based construction (`builder = AUTO | ALWAYS | NEVER`)
If the target has a static no-arg `Target.builder()` factory returning a type with a fluent
(returns-itself) setter per property plus a `build()` method - the shape
`avaje-recordbuilder`'s `@RecordBuilder` generates - the generated mapper can construct the
target via `Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)`:
```java
public record User(Long id, String name, String email, /* ... 21 more fields */) {
public static UserBuilder builder() {
return UserBuilder.builder();
}
}
```
```java
@DtoMapping(source = CUser.class, target = User.class)
package org.example.dto;
```
By default (`builder = AUTO`), the generator auto-detects a matching builder and uses it only
once the target has more than 5 properties, falling back to a positional constructor for
smaller DTOs. Override explicitly either direction:
```java
@DtoMapping(source = CUser.class, target = User.class, builder = DtoMapping.Builder.ALWAYS)
```
`builder = ALWAYS` is a codegen-time error if no matching builder shape is found; `builder =
NEVER` always uses a positional constructor even if a builder is detected. This applies
regardless of whether the target is hand-authored or foreign/generated - `@DtoMapping` is
already declared externally via `package-info.java`, so no annotation on the target itself is
needed either way.
### Named variants excluding nested paths (`name=`, `exclude=`)
The same `(source, target)` pair can be registered more than once - one base mapping (leaving
`name()` empty) plus any number of named variants, each excluding one or more nested
ToOne/ToMany properties:
```java
@DtoMapping(source = CUser.class, target = User.class)
@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")
package org.example.dto;
```
Both variants are generated into the **same** mapper class (one class per target, not one per
variant) - the generated `noFleets()` accessor returns a single shared/cached `DtoMapper<CUser,
User>` view (not reconstructed per call), omitting `fleets` from both its mapped output (`null`
for a ToOne, `List.of()` for a ToMany) and its own `fetchGroup()`. Each excluded property is still
evaluated inline at its own declared field position internally (guarded by a boolean flag) - a
variant's exclusions never change the evaluation order of the DTO's other properties. Select it
with the `query.mapTo(Class, DtoMapper)` overload, which takes an already-resolved mapper instance
directly - no string-based lookup:
```java
UserMapper userMapper = new UserMapper();
// full shape, with fleets fetched/mapped
List<User> withFleets = DB.find(CUser.class)
.mapTo(User.class, userMapper) // or plain .mapTo(User.class)
.findList();
// bulk listing shape - fleets excluded from both the fetch spec and the output
List<User> noFleets = DB.find(CUser.class)
.mapTo(User.class, userMapper.noFleets())
.findList();
```
Only nested ToOne/ToMany properties can be excluded - a scalar or `@DtoRef` property can't be,
since there's no type-safe "absent" value for an arbitrary scalar type. Named variants are
scoped to independent, top-level query results only - unlike the base mapping, they don't
participate in `DtoMapContext` identity de-duplication when nested elsewhere in a graph, since a
variant is never intended to be nested inside another DTO's mapping.
---
## `@DtoPath` — renamed or flattened properties
By default a DTO property is matched to the source entity property (or nested DTO
mapper) of the **same name**. `@DtoPath` overrides that, allowing a DTO property to be
renamed and/or flattened from a nested path using dot-notation:
```java
public class ContactDto {
private final long id;
private final String firstName;
private final String lastName;
@DtoPath("customer.billingAddress.city")
private final String customerCity; // flattened, 2 hops through customer
// constructor / getters ...
}
```
The generated mapper reads the path with a null-guard at each hop and adds the
necessary joins to the fetch spec automatically:
```java
(s.getCustomer() == null ? null
: (s.getCustomer().getBillingAddress() == null ? null
: s.getCustomer().getBillingAddress().getCity()))
```
`@DtoPath` is purely a compile-time/codegen-time hint — the DTO class itself carries no
runtime dependency on the annotation.
### Fetch-path collisions are a compile-time error
A `@DtoPath` whose fetch path is identical to a nested `ToOne`/`ToMany` property's own
fetch path on the *same* DTO (e.g. a nested `customer` field alongside
`@DtoPath("customer.name")` — both resolve to fetch path `"customer"`) fails the build
with a clear error, rather than silently discarding one side's fetched properties:
```
error: @DtoPath property 'customerName' on FooDto resolves to fetch path 'customer',
which collides with the nested mapping already using that same fetch path - Ebean's
fetch spec can only carry one set of properties per path, so one silently discards
the other. Move 'customerName' onto the nested DTO type instead, or choose a
@DtoPath that reaches into a different, non-colliding path.
```
Fix it either way it suggests: move the property onto the nested DTO type, or choose a
`@DtoPath` that reaches a different path (as `customerCity` above does deliberately,
using a 3-segment path through `customer.billingAddress` rather than colliding with a
plain `customer` nested field).
---
## `@DtoRef` — id-only back-references (breaking cycles)
The DTO graph derived from a set of DTO types must form a DAG — codegen fails if it
doesn't. `@DtoRef` is the explicit escape hatch for an intentional back-reference, e.g.
a `Contact` DTO referencing its parent `Customer` by id only, rather than re-embedding
a full `CustomerDto` (which would recreate the `Customer → Contact → Customer` cycle):
```java
public class ContactDto {
private final long id;
@DtoRef
private final Long customerId; // id-only, no nested CustomerDto re-embedded
// constructor / getters ...
}
```
The generated fetch spec adds the association to the **root** `select(...)` rather
than a nested `.fetch(...)` — this reads the foreign-key column directly off the base
table (no SQL join):
```java
this.fetchGroup = FetchGroup.of(ContactStats.class)
.select("customer,contactCount,engagementScore") // "customer" -> FK column, no join
.build();
```
```java
(source.getCustomer() == null ? null : source.getCustomer().getId())
```
If the same association is *also* independently nested-fetched elsewhere on the DTO
(e.g. `ContactDto` has both a nested `customer` field **and** `@DtoRef Long
customerId`), the generator recognizes the association is already covered and doesn't
add a redundant/duplicate select — no join is added twice.
---
## `@DtoConvert` — custom property conversion
Some properties need more than a plain getter copy — a scalar coercion (`short` to
`boolean`), an enum-to-`String` mapping, or a conversion needing a real dependency (e.g.
decrypting a value with a cipher). `@DtoConvert(value = ConverterType.class, method =
"name")` covers both, combinable with `@DtoPath` when the source value also needs a
path/rename override:
```java
public class ContactDto {
@DtoPath("status")
@DtoConvert(value = ContactConversions.class, method = "toActive")
private final boolean active; // Contact.status (Short) -> boolean
@DtoConvert(value = SecretCipher.class, method = "decode")
private final String secretCode; // decrypted via a registered SecretCipher
// constructor / getters ...
}
```
The generator resolves the referenced method at codegen time and dispatches one of two
ways, purely based on whether it's `static`:
- **Static method** — inlined as a direct static call
(`ContactConversions.toActive(source.getStatus())`). No registration needed at all —
use this for common, reusable, dependency-free coercions.
- **Instance method** — the generated mapper resolves one shared instance via
`DtoConverterManager.get(SecretCipher.class)`, wired as a constructor
parameter/field (the same shape as nested-mapper constructor injection), then calls
`secretCipher.decode(source.getSecretCode())`. Use this when the conversion needs a
real dependency.
### Registering an instance-dispatch converter
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge — register
an already-constructed converter instance (e.g. built by your DI container) **before**
building the `Database`:
```java
AES256Cipher cipher = ...; // already DI-constructed
DtoConverterManager.put(SecretCipher.class, cipher::decrypt); // or a small adapter class
Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
```
If nothing is registered for a required type, `DtoConverterManager.get(...)` throws a
`PersistenceException` immediately — this happens as an eager field initializer on the
generated `EbeanDtoMapperRegister`, so a missing registration fails fast at `Database`
build time, not lazily on first `mapTo(...)` call.
> **Testing tip:** since `EbeanDtoMapperRegister`'s mapper fields are all constructed
> together when the `Database` starts, register converters via a `DatabaseConfigProvider`
> (a `ServiceLoader` hook that runs before the `Database` is built) rather than a test
> `@BeforeAll`, so registration always happens before *any* test triggers startup —
> regardless of which test class runs first.
## `@DtoMixin` — overlaying annotations onto a DTO you can't edit
Some DTOs are generated elsewhere (e.g. from an OpenAPI spec, regenerated on every
build) and can't be annotated directly. `@DtoMixin(Target.class)` overlays
`@DtoPath`/`@DtoRef`/`@DtoConvert` from a separate companion type instead — directly
mirroring avaje-jsonb's `@Json.MixIn` mechanism. Declare a companion interface (or
class) whose method names match the target DTO's property names:
```java
// ContactMixinDto itself carries no Ebean annotations at all
public class ContactMixinDto {
public ContactMixinDto(long id, String firstName, boolean active, String secretCode) { ... }
// getters ...
}
@DtoMixin(ContactMixinDto.class)
interface ContactMixinDtoMixin {
@DtoPath("status")
@DtoConvert(value = ContactConversions.class, method = "toActive")
boolean active();
@DtoConvert(value = SecretCipher.class, method = "decode")
String secretCode();
}
```
The processor matches each mixin method to the target's property by name and applies
whichever annotations are present as if they were declared on the target field itself.
The mixin type is never instantiated and carries no runtime footprint — it's purely a
compile-time/codegen-time hint.
---
## Computed / aggregate properties via `@Entity @View`
There's no dedicated "formula on DTO" annotation (a narrower `@Formula2`-on-DTO
variant was explored and rejected — see
[dto-mapping-design.md](../dto-mapping-design.md) for the reasoning). Instead, model
the computed value as its own read-only entity using `@View`, then map that entity to a
plain DTO with the same `@DtoMapping` machinery described above. `@View(name = "...")`
here just points a second entity at an **existing** table — it does not create a new
database view or table.
### Worked example — computed column (`@Formula2`)
```java
@Entity
@View(name = "contact") // reads the existing 'contact' table, no new DDL
public class ContactSummary {
@Id
private Long id;
private String firstName;
private String lastName;
@Formula2("concat(firstName, ' ', lastName)")
private String fullName;
// getters ...
}
```
```java
public class ContactSummaryDto {
private final Long id;
private final String fullName;
// constructor / getters ...
}
```
```java
@DtoMapping(source = ContactSummary.class, target = ContactSummaryDto.class)
```
```java
List<ContactSummaryDto> summaries = DB.find(ContactSummary.class)
.mapTo(ContactSummaryDto.class)
.findList();
```
### Worked example — group-by aggregation (`@Sum`/`@Aggregation`)
The same `@View`-on-base-table pattern applies to Ebean's `@Sum`/`@Aggregation`
group-by formulas — the Blaze-Persistence parallel is an `@EntityView` with
`@Mapping("SIZE(...)")`/`@Mapping("SUM(...)")` correlated mappings:
```java
@Entity
@View(name = "contact")
public class ContactStats {
@Id
private Long id; // required so @Aggregation("count(id)") has something to
// count; deliberately never selected/mapped - selecting it
// would defeat the aggregation (one row per contact
// instead of one row per customer)
@ManyToOne
private Customer customer;
@Aggregation("count(id)")
private Long contactCount;
@Sum
private Integer engagementScore;
// getters ...
}
```
```java
public class ContactStatsDto {
@DtoRef
private final Long customerId; // also the implicit GROUP BY key
private final Long contactCount;
private final Integer engagementScore;
// constructor / getters ...
}
```
Because `customerId` uses `@DtoRef`, the generated fetch spec is
`select("customer,contactCount,engagementScore")` with **no join** — the query groups
by the FK column directly:
```sql
select t0.customer_id, count(t0.id), sum(t0.engagement_score)
from contact t0
group by t0.customer_id
```
---
## Performance notes
### Fail-fast, no accidental lazy loading
`mapTo(...)` forces `query.setUnmodifiable(true)` under the hood. If the mapper ever
needs a property that wasn't fetched, it throws `LazyInitialisationException`
immediately rather than silently issuing an extra query per row or returning `null`.
`InterceptReadOnly` (the unmodifiable-graph bean state) is also cheap — a `boolean[]
loaded` flag array plus a `frozen` flag, not a full second copy of bean state.
### One mapper instance per pair
Generated mappers are constructed once (in dependency order — a mapper with nested
mappers takes them as constructor params) and reused across every `mapTo(...)` call for
that pair, resolved and cached by `DtoMapperManager` keyed on `(sourceType, dtoType)`.
### `DtoMapContext` overhead only where it earns its keep
As shown above, the generator only involves `DtoMapContext` for mappers that can
actually be reached via more than one path in some graph (dedup) or that have nested
mappers of their own (need to thread the context down); a DTO that's only ever a
top-level query result skips it entirely.
### Fetch strategy and pagination carry over unchanged
Existing fetch-strategy control (`+query`/`+lazy`, `fetchQuery()`) and pagination
(including keyset pagination and `findPagedList()`) work the same whether the query
target is an entity graph or a `mapTo(...)` DTO graph — no special-casing needed.
---
## Which should I use?
- **`mapTo(Dto.class)`** — the target is a **nested** shape (has its own ToOne/ToMany
DTO fields) that should mirror part of the entity graph; you want the fetch spec
derived automatically and verified to match the DTO's declared shape.
- **`asDto(Dto.class)`** / `DB.findDto(...)` — the target is a **flat** row (report,
summary, native/vendor SQL); you're comfortable with runtime-checked column-to-bean
matching, or the SQL doesn't map cleanly to entity property paths at all.
- **Plain entity query** — the caller needs a real, persistable, mutable entity — not a
read-only projection.
---
## Reference
### Why `package-info.java`?
`@DtoMapping` is declared on a package (`ElementType.PACKAGE`), not the DTO or the
entity, because:
- the DTO type is often owned/generated elsewhere (e.g. from an OpenAPI spec) and
shouldn't need to be annotated with an internal persistence/entity type;
- one entity may be the source for several different DTOs (e.g. a summary vs. a detail
view), and the same entity/DTO pair may need registering from multiple consuming
modules.
### Annotations at a glance
| Annotation | Target | Purpose |
|---|---|---|
| `@DtoMapping(source=, target=)` | `package-info.java` | Registers an entity → DTO pair, triggers mapper generation |
| `@DtoMapping(..., builder=)` | `package-info.java` | `AUTO` (default, threshold-based) / `ALWAYS` / `NEVER` - builder-chain vs positional constructor |
| `@DtoMapping(..., name=, exclude=)` | `package-info.java` | Registers a named variant sharing the base mapping's generated class, excluding nested paths |
| `@DtoPath("a.b.c")` | DTO field/getter | Renamed and/or flattened multi-hop property mapping |
| `@DtoRef` | DTO field/getter | Id-only back-reference; breaks a cycle; root-selects the FK (no join) |
| `@DtoConvert(value=, method=)` | DTO field/getter | Custom scalar conversion - static (no registration) or instance (via `DtoConverterManager`) dispatch |
| `@DtoMixin(Target.class)` | Companion interface/class | Overlays `@DtoPath`/`@DtoRef`/`@DtoConvert` onto a DTO that can't be annotated directly |
### Parallels with other tools
If you're coming from another mapping library, here's the rough correspondence:
| Ebean | MapStruct | Blaze-Persistence |
|---|---|---|
| Generated `DtoMapper` per (source, DTO) pair | Generated `@Mapper` implementation | `@EntityView` (interface + runtime proxy) |
| `@DtoPath("a.b.c")` | `@Mapping(target = "x", source = "a.b.c")` | `@Mapping("a.b.c")` |
| `@DtoRef` | `@Context`/manual cycle-breaking (no dedicated annotation) | Sub-view referencing an id-only projection |
| `@DtoConvert(value=, method=)` | `@Mapping(qualifiedByName = "...")` / custom mapper methods | Custom converter/`@Mapping` expression |
| `@DtoMixin(Target.class)` | N/A (annotate the `@Mapper` interface's abstract methods instead) | N/A |
| `DtoMapContext` identity de-dup | Not built in (opt-in `@MappingTarget`/manual caching) | Built in (entity-view identity) |
| `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation` for computed DTO values | N/A (MapStruct doesn't touch SQL) | `@Mapping("SIZE(...)")` / `@Mapping("SUM(...)")` correlated mappings |
See [dto-mapping-design.md](../dto-mapping-design.md) for the full design rationale and
[dto-mapping-requirements.md](../dto-mapping-requirements.md) for the accepted/rejected
requirements this feature was scoped against (issue
[#2540](https://github.com/ebean-orm/ebean/issues/2540)).
@@ -0,0 +1,91 @@
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
## Purpose
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
avaje-json-core APIs.
Use this when upgrading code that references:
- `com.fasterxml.jackson.core.JsonParser`
- `com.fasterxml.jackson.core.JsonGenerator`
- `com.fasterxml.jackson.core.JsonFactory`
The replacement types are:
- `io.avaje.json.JsonReader`
- `io.avaje.json.JsonWriter`
- `io.avaje.json.stream.JsonStream`
---
## Breaking API changes
| Previous API | New API |
|---|---|
| `JsonParser` | `JsonReader` |
| `JsonGenerator` | `JsonWriter` |
| `JsonFactory` | `JsonStream` |
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
---
## Typical migration rewrites
### Parser and generator signatures
```java
// before
void read(JsonParser parser)
void write(JsonGenerator generator)
// after
void read(JsonReader parser)
void write(JsonWriter generator)
```
### Database configuration
```java
// before
Database.builder().jsonFactory(factory)
// after
Database.builder().jsonStream(stream)
```
### JSON utility calls
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
If your code was calling those APIs with Jackson core types, switch to avaje types.
---
## Dependency and module notes
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
parsing/writing.
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
integrations.
---
## Behavior notes to verify during upgrade
1. Parser token handling is now based on avaje `JsonReader.Token`.
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
be validated in your tests if you previously depended on Jackson token quirks.
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
keep ObjectMapper wiring enabled.
---
## Validation checklist
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
@@ -0,0 +1,242 @@
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
## Purpose
This guide shows how to migrate legacy programmatic database creation code from:
- `new DatabaseConfig()`
- `DatabaseFactory.create(...)`
- old `setXxx(...)` builder-style configuration methods
…to the preferred builder-based style using:
- `Database.builder()`
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
- `DatabaseBuilder.build()`
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
---
## Preferred pattern
Prefer code shaped like this:
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.dataSourceBuilder(dataSource)
.register(true)
.defaultDatabase(true)
.build();
```
The important points are:
1. Start with `Database.builder()`
2. Configure via `DatabaseBuilder`
3. Finish with `.build()`
---
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
```
### Notes
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
- If existing code only uses standard builder methods, this change is usually mechanical.
- If existing code later reads configuration back, use `config.settings()`.
---
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
Database database = DatabaseFactory.create(config);
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
Database database = config.build();
```
### Short form
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.build();
```
---
## Step 3 — Replace `DatabaseFactory.create("name")`
### Before
```java
Database database = DatabaseFactory.create("other");
```
### After
```java
Database database = Database.builder()
.name("other")
.loadFromProperties()
.build();
```
### Important
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
---
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
Use those names when migrating older setup code.
| Legacy call | Preferred call |
|---|---|
| `setName("db")` | `name("db")` |
| `setRegister(false)` | `register(false)` |
| `setDefaultServer(false)` | `defaultDatabase(false)` |
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
| `setDbSchema("app")` | `dbSchema("app")` |
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
| `setRunMigration(true)` | `runMigration(true)` |
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
| `setPersistBatch(batch)` | `persistBatch(batch)` |
### Full example
#### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.setRegister(false);
config.setDefaultServer(false);
config.setDataSourceConfig(dataSource);
Database database = DatabaseFactory.create(config);
```
#### After
```java
Database database = Database.builder()
.name("db")
.register(false)
.defaultDatabase(false)
.dataSourceBuilder(dataSource)
.build();
```
---
## Step 5 — Verify semantics after migration
The migration should preserve behavior, but verify these points:
- `register(true)` is still the default
- `defaultDatabase(true)` is still the default
- call `loadFromProperties()` if the old code loaded configuration from properties
- for named databases, set the name before loading properties
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
---
## Manual-review cases
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
### `DatabaseFactory.createWithContextClassLoader(...)`
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
### `DatabaseFactory.initialiseContainer(...)`
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
### `DatabaseFactory.shutdown()`
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
### Variables or method signatures typed as `DatabaseConfig`
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
### Code that needs read access to builder settings
Use:
```java
DatabaseBuilder builder = Database.builder();
DatabaseBuilder.Settings settings = builder.settings();
```
rather than relying on the concrete `DatabaseConfig` type only to read getters.
---
## Automation notes for AI agents and bulk refactors
This migration is a good candidate for semi-automated upgrading.
### Safe mechanical rewrites
These are usually safe to rewrite automatically:
- `new DatabaseConfig()``Database.builder()`
- `DatabaseFactory.create(builder)``builder.build()`
- `DatabaseFactory.create("name")``Database.builder().name("name").loadFromProperties().build()`
- legacy `setXxx(...)` calls → preferred fluent builder methods
### Flag for manual review
Automatically flag, but do not blindly rewrite:
- `DatabaseFactory.createWithContextClassLoader(...)`
- `DatabaseFactory.initialiseContainer(...)`
- `DatabaseFactory.shutdown()`
- parameters, fields, or return types declared as `DatabaseConfig`
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
---
## Related guides
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
- [Guide index](README.md) — full list of Ebean setup and migration guides
@@ -0,0 +1,447 @@
# Guide: Persist Changes and Manage Transactions with Ebean
## Purpose
This guide gives step-by-step instructions for AI agents and developers to save,
update, delete, and batch changes with Ebean while choosing the correct
transaction boundary.
Use this guide when you need to:
- create a new entity row
- update one or more existing rows
- delete rows safely
- decide between implicit transactions, `@Transactional`, and explicit
transactions
- batch or bulk-write many rows efficiently
The default recommendation is:
1. Choose the correct persistence operation first
2. Use implicit transactions for a single isolated write
3. Use `@Transactional` for multi-step application workflows
4. Use explicit transactions only when you need explicit control
5. Use bulk update or batching for large write sets
---
## Prerequisites
- The project already uses Ebean ORM
- Entity beans and database configuration already exist
- You know which `Database` is being used (`DB.getDefault()` or a named database)
If the project is not yet configured, first follow:
- [`add-ebean-postgres-database-config.md`](add-ebean-postgres-database-config.md)
- [`entity-bean-creation.md`](entity-bean-creation.md)
---
## Step 1 - Choose the correct persistence operation before editing code
Do not start with `database.save(...)` by habit. First decide what kind of change the
caller is making.
| Need | Preferred API | Use when |
|------|---------------|----------|
| Insert a bean that is definitely new | `database.insert(bean)` | New-create flow, seed data, fixture setup |
| Save a bean that may be new or existing | `database.save(bean)` | Common default when bean state determines insert vs update |
| Update a bean that is definitely existing | `database.update(bean)` | Existing row should be updated only |
| Delete one bean | `database.delete(bean)` | Remove a loaded entity bean |
| Update many rows without loading beans | `database.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
| Delete many rows without loading beans | bulk update/delete API or `database.sqlUpdate(...)` | Set-based deletion |
### Agent rule
Choose the operation that matches intent:
- known new row -> `insert`
- known existing row -> `update`
- uncertain/new-or-existing -> `save`
- many rows -> bulk update/delete, not a loop of individual saves
### Style note
Use a `Database` instance for all persistence operations: `database.save(bean)`,
`database.insert(bean)`, `database.update(bean)`, `database.delete(bean)`.
Inject the `Database` bean or obtain it via `DB.getDefault()`. Avoid using the
static `DB.*` convenience methods.
---
## Step 2 - Persist single-bean changes with the correct API
### Example - insert a known new bean
```java
Customer customer = new Customer();
customer.setName("Rob");
customer.setEmail("rob@example.com");
database.insert(customer);
```
### Example - update an existing bean
```java
Customer customer = new QCustomer()
.id.equalTo(customerId)
.findOne();
customer.setStatus(Customer.Status.ACTIVE);
database.update(customer);
```
### When to prefer `insert()` over `save()`
Use `insert()` when the code is creating a brand new row and should fail if the
operation does not behave like an insert.
### When to prefer `update()` over `save()`
Use `update()` when the bean is definitely existing and the method should not
silently behave like an insert.
---
## Step 3 - Check cascade mappings before assuming related beans will persist or delete
Ebean follows cascade rules defined on mapping annotations such as
`@OneToMany`, `@OneToOne`, `@ManyToOne`, and `@ManyToMany`.
The default is **no cascade**.
### Example
```java
@Entity
public class Order {
@ManyToOne
private Customer customer; // no cascade by default
@OneToMany(cascade = CascadeType.ALL)
private List<OrderDetail> details; // save + delete cascade
}
```
```java
database.save(order);
```
With the mapping above:
- `details` are cascaded
- `customer` is **not** cascaded
### Agent rules for cascades
1. Inspect the mapping before writing save/delete logic
2. Do not assume `@ManyToOne` cascades
3. Avoid adding cascade to shared parent references unless ownership is truly
intended
4. If a relationship should not cascade, save/delete related beans explicitly
---
## Step 4 - Let Ebean use an implicit transaction for a single isolated write
If the method performs one isolated persistence operation, Ebean can manage the
transaction implicitly.
### Good fit for implicit transaction
```java
Customer customer = new QCustomer()
.id.equalTo(customerId)
.findOne();
customer.setStatus(Customer.Status.INACTIVE);
database.save(customer);
```
### Good fit
- one save
- one update
- one delete
- small helper method with a single write
### Poor fit
- multiple writes that must commit or roll back together
- query + save + save workflow
- any method where later failure must roll back earlier writes
### Important
Queries also use implicit transactions when needed. You generally do **not**
need to wrap ordinary read queries in an explicit transaction "just in case".
---
## Step 5 - Use `@Transactional` for multi-step service workflows
When multiple Ebean operations belong to one unit of work, use
`@Transactional`.
### Example - service method
```java
import io.ebean.annotation.Transactional;
@Transactional
public void shipOrder(long orderId) {
Order order = new QOrder()
.id.equalTo(orderId)
.findOne();
order.setStatus(Order.Status.SHIPPED);
database.save(order);
Shipment shipment = new Shipment(order, Instant.now());
database.insert(shipment);
}
```
All database work inside the method runs in one transaction and commits only if
the method completes successfully.
### Use `Transaction.current()` only when needed
If the method needs access to the current transaction itself:
```java
Transaction txn = Transaction.current();
```
Do this only for transaction-specific behavior such as comments, savepoints, or
other advanced control. Do not fetch the current transaction if the method does
not need it.
### Agent rules for `@Transactional`
1. Put it on application/service workflow methods, not everywhere by default
2. Keep the transaction focused on database work
3. Avoid remote HTTP calls, message publishing, or long-running CPU work inside
the transaction if those can be moved outside
### Named database note
If the method uses a non-default database, obtain that `Database` instance via
`DB.byName("...")` and consistently use that database for both queries and
writes.
---
## Step 6 - Use `beginTransaction()` when you need explicit control
Use an explicit transaction when you need manual `commit()`, batching, explicit
flush, savepoints, or other low-level transaction control.
### Example - explicit transaction with try-with-resources
```java
try (Transaction txn = database.beginTransaction()) {
Order order = new QOrder()
.id.equalTo(orderId)
.findOne();
order.cancel();
database.save(order);
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
database.insert(auditLog);
txn.commit();
}
```
If `commit()` is not reached, closing the transaction rolls it back.
### Useful explicit controls
- `txn.commit()` - commit current work
- `txn.setRollbackOnly()` - force rollback-only behavior
- `txn.flush()` - push batched statements to the database now
### Agent rule
Prefer `@Transactional` unless explicit transaction control is actually needed.
Do not use `beginTransaction()` only because it feels "safer".
---
## Step 7 - Use `createTransaction()` only for non-thread-local transaction handling
`createTransaction()` creates a transaction that is **not** placed into the
thread-local scope. This is a specialized tool.
Use it when:
- the transaction will be passed explicitly
- you need more than one transaction in the same thread
- you are coordinating work across threads or lower-level APIs
### Example - explicit transaction passed to query and save
```java
Database database = DB.getDefault();
try (Transaction txn = database.createTransaction()) {
Customer customer = new QCustomer(txn)
.email.equalTo(email)
.findOne();
customer.setInactive(true);
database.save(customer, txn);
txn.commit();
}
```
### Agent rule
If you are not deliberately bypassing thread-local transaction scope, do **not**
use `createTransaction()`. Most service code should use `@Transactional` or
`beginTransaction()`.
---
## Step 8 - Use bulk update/delete or JDBC batch for many-row writes
Loops of `database.save(...)` are often the wrong tool for large write sets.
### Prefer bulk update for set-based changes
If the update can be expressed as "change all rows matching this predicate",
perform one bulk update instead of loading and saving each bean.
### Example - bulk update with query beans
```java
var cust = QCustomer.alias();
int rows = new QCustomer()
.status.equalTo(Customer.Status.NEW)
.asUpdate()
.set(cust.status, Customer.Status.ACTIVE)
.update();
```
### Example - bulk update with `database.update(...)`
```java
int rows = database.update(Customer.class)
.set("status", Customer.Status.ACTIVE)
.where()
.eq("status", Customer.Status.NEW)
.update();
```
### Prefer JDBC batch for many individual inserts/updates
If each row has different values and must still go through per-bean persistence,
use batching.
```java
Database database = DB.getDefault();
try (Transaction txn = database.beginTransaction()) {
txn.setBatchMode(true);
txn.setBatchSize(100);
txn.setGetGeneratedKeys(false);
for (Customer customer : customersToInsert) {
database.insert(customer, txn);
}
txn.commit();
}
```
### Alternative - annotation-driven batching
```java
@Transactional(batchSize = 50)
public void importCustomers(List<Customer> customers) {
for (Customer customer : customers) {
database.insert(customer);
}
}
```
### Batch caveats
- Executing a query inside a batched transaction can flush the batch
- Mixing bean persistence and `SqlUpdate` can also flush the batch
- Accessing generated/unloaded properties on batched beans can flush the batch
If the workflow depends on delayed flushing, review the batch-flush rules before
adding more queries inside the same transaction.
---
## Common anti-patterns
### Anti-pattern 1 - Saving many rows one by one without batch or bulk update
If you are changing hundreds or thousands of rows, first ask whether it should
be a bulk update or a batched transaction.
### Anti-pattern 2 - Assuming child beans cascade automatically
Cascade is not automatic. Inspect the mapping first.
### Anti-pattern 3 - Wrapping external calls inside the database transaction
Do not keep transactions open while waiting on HTTP calls, queues, or other
slow external systems unless the design genuinely requires it.
### Anti-pattern 4 - Using `createTransaction()` for ordinary service code
Most service code should not bypass thread-local transaction handling.
### Anti-pattern 5 - Using `save()` when you really need `insert()` or `update()`
If operation intent matters, choose the more specific API.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Child beans were not saved or deleted | Missing cascade mapping | Inspect annotations and add explicit save/delete or the correct cascade |
| Earlier writes committed even though later work failed | The whole workflow was not inside one transaction | Wrap the unit of work in `@Transactional` or an explicit transaction |
| `OptimisticLockException` on update/delete | Concurrent modification or stale version | Re-fetch, merge, or handle concurrency explicitly |
| Batch writes flush earlier than expected | Query, mixed SQL, or property access triggered flush | Review batch flush rules and transaction flow |
| Explicit transaction example does not affect the expected database | Mixed default DB and named DB usage | Use the same `Database` instance consistently for query and write |
---
## Summary workflow for AI agents
When asked to add persistence logic:
1. Choose `insert`, `save`, `update`, `delete`, or bulk update based on intent
2. Inspect cascade mappings before assuming related beans will persist/delete
3. Use implicit transactions for one isolated write
4. Use `@Transactional` for multi-step units of work
5. Use `beginTransaction()` only when explicit transaction control is needed
6. Use `createTransaction()` only for explicit, non-thread-local handling
7. Use bulk update or batching for large write sets
---
## Related documentation
- [Entity Bean Creation](entity-bean-creation.md)
- [Testing with TestEntityBuilder](testing-with-testentitybuilder.md)
- [Ebean persist docs](https://ebean.io/docs/persist)
- [Ebean transaction docs](https://ebean.io/docs/transactions)
@@ -0,0 +1,817 @@
# Guide: Testing with TestEntityBuilder
## Purpose
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
- **Integration tests** that need representative data without caring about specific values
- **Persistence layer tests** that verify save/update/delete operations work correctly
- **Query and filter tests** where you need multiple entities with varied data
- **Rapid test setup** that reduces test code verbosity and improves readability
---
## Setup & Dependencies
### Add ebean-test to Your Project
The `TestEntityBuilder` class is provided by the `ebean-test` module.
**Maven:**
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
**Gradle:**
```gradle
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
```
Use a version that matches your Ebean runtime (`ebean.version` /
`ebeanVersion`), or replace with an explicit fixed version if your build does
not centralize dependency versions.
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
> runtime and test versions are not supported.
### Import the Class
```java
import io.ebean.test.TestEntityBuilder;
```
---
## Basic Usage
### Create a Builder Instance
`TestEntityBuilder` uses a builder pattern for configuration:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
```
The `Database` parameter specifies which Ebean database instance to use for entity type
lookups and persistence operations. Pass the injected `Database` bean (see
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
use the injected `database` bean for **all** persistence operations in your tests
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
### Build an Entity (In-Memory)
The `build()` method creates an instance with populated fields **without persisting to the database:**
```java
Product product = builder.build(Product.class);
// Fields are populated:
// - id: unset (typically 0 for primitive long, null for boxed Long)
// - name: random UUID-based string
// - price: random BigDecimal
// - inStock: true
// - createdAt: current instant
// - etc.
// Not persisted yet (`@Id` is still unset until the entity is persisted).
```
### Build and Save (Persist to Database)
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
```java
Product product = builder.save(Product.class);
// Entity is now in the database:
assert database.find(Product.class, product.getId()) != null;
```
### Save Multiple Entities
The `saveAll()` method persists multiple pre-built entities in a single call:
```java
Product p1 = builder.build(Product.class);
Product p2 = builder.build(Product.class);
builder.saveAll(p1, p2);
// Both are now in the database with assigned IDs:
assert p1.getId() != null;
assert p2.getId() != null;
```
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
`Database` reference in tests that already hold a `TestEntityBuilder`.
### Access the Underlying Database
The `database()` method returns the `Database` instance used internally by the builder.
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
still need to perform `find()`, `delete()`, or other database operations:
```java
Product saved = builder.save(Product.class);
// Use builder.database() instead of injecting a separate Database bean:
Product found = builder.database().find(Product.class, saved.getId());
assert found != null;
```
---
## Using with Dependency Injection
Most applications using Ebean also use a DI framework. The recommended pattern is to
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
### Avaje Inject — `@TestScope @Factory`
Add a `@Bean` method to your test-scoped `@Factory` class:
```java
import io.ebean.Database;
import io.ebean.test.ContainerDatabase;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.test.TestEntityBuilder;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes using `@InjectTest`:
```java
@InjectTest
class OrderControllerTest {
@Inject Database database;
@Inject TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
Both patterns produce a single shared `TestEntityBuilder` instance, wired
from the managed `Database` bean — no `@BeforeEach` required.
### Spring Boot — `@TestConfiguration`
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
```java
@TestConfiguration
class TestConfig {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
// use @Primary if your main application context also wires a Database bean
// or conditionally wire the main Database bean to exclude it from tests
@Primary
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes:
```java
@SpringBootTest
class OrderControllerTest {
@Autowired Database database;
@Autowired TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
---
## Type-Specific Value Generation
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
| Type | Generated Value | Notes |
|------|-----------------|-------|
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
| `Integer` / `int` | Random in `[1, 1_000)` | |
| `Long` / `long` | Random in `[1, 100_000)` | |
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
| `Double` / `double` | Random in `[1, 100)` | |
| `Float` / `float` | Random in `[1, 100)` | |
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
| `LocalDate` | Today's date | Via `LocalDate.now()` |
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
| `Instant` | Current instant | Via `Instant.now()` |
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
| `Enum` | First constant | Override in custom generator if needed |
| Other types | `null` | Set these fields manually in tests |
### String Length Constraints
`TestEntityBuilder` respects column length constraints defined in the entity:
```java
@Entity
public class User {
@Column(length = 50)
private String username;
}
User user = builder.build(User.class);
assert user.getUsername().length() <= 50; // ✅ Constraint respected
```
### BigDecimal Precision and Scale
For `BigDecimal` fields, the builder respects the database column precision and scale:
```java
@Entity
public class LineItem {
@Column(precision = 10, scale = 2) // max 99_999_999.99
private BigDecimal amount;
}
LineItem item = builder.build(LineItem.class);
assert item.getAmount().scale() == 2;
```
### Short Fields Used as Boolean Flags
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
code checks `entity.getActive() == 1` specifically, override the field after building:
```java
Organisation org = builder.build(Organisation.class)
.setActive((short) 1); // explicit override — random short won't do
```
---
## Entity Relationships
### Cascade-Persist Relationships: Recursively Built
Relationships marked with `cascade = PERSIST` are recursively populated:
```java
@Entity
public class Order {
@ManyToOne(cascade = CascadeType.PERSIST)
private Customer customer;
}
Order order = builder.build(Order.class);
// Both order and customer are built:
assert order != null;
assert order.getCustomer() != null;
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
// When saved, cascade handles both:
Order saved = builder.save(Order.class);
assert saved.getId() != null;
assert saved.getCustomer().getId() != null; // parent also saved
```
### Non-Cascade Relationships: Left Null
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
Create and save the related entity first (the builder works well here), then assign it manually
before saving the parent:
```java
@Entity
public class BlogPost {
@ManyToOne
private Author author; // No cascade = left null by builder
}
BlogPost post = builder.build(BlogPost.class);
assert post.getAuthor() == null;
// Use the builder to create the related entity, then set it manually:
Author author = builder.save(Author.class);
post.setAuthor(author);
database.save(post);
```
### Collection Relationships: Left Empty
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
`.add()` or `.addAll()` directly is safe:
```java
@Entity
public class Author {
@OneToMany(mappedBy = "author")
private List<BlogPost> posts; // Left empty
}
Author author = builder.build(Author.class);
assert author.getPosts().isEmpty();
// Populate if needed for testing:
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
```
### Cycle Detection: Prevents Infinite Recursion
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
```java
@Entity
public class Person {
@ManyToOne(cascade = CascadeType.PERSIST)
private Organization org;
}
@Entity
public class Organization {
@ManyToOne(cascade = CascadeType.PERSIST)
private Person founder;
}
Person person = builder.build(Person.class);
// One reference will be null to break the cycle:
// either person.org or person.org.founder is null
```
---
## Custom Value Generators
### Why Customize?
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
- Email addresses with your company domain
- Realistic phone numbers
- Product SKUs following a pattern
- Addresses in specific regions
- Monetary amounts within realistic ranges
### Creating a Custom Generator
Subclass `RandomValueGenerator` and override individual `random*()` methods:
```java
class CompanyTestDataGenerator extends RandomValueGenerator {
@Override
protected String randomString(String propName, int maxLength) {
if (propName != null && propName.toLowerCase().contains("email")) {
// Use company domain instead of generic @domain.com
String localPart = UUID.randomUUID().toString().substring(0, 8);
String email = localPart + "@mycompany.com";
if (maxLength > 0 && email.length() > maxLength) {
return email.substring(0, maxLength);
}
return email;
}
return super.randomString(propName, maxLength);
}
// Override other methods as needed:
@Override
protected Object randomEnum(Class<?> type) {
if (type == OrderStatus.class) {
// Bias towards common statuses for realistic test data
return ThreadLocalRandom.current().nextDouble() < 0.8
? OrderStatus.PENDING
: OrderStatus.COMPLETED;
}
return super.randomEnum(type);
}
}
```
### Using a Custom Generator
Pass the custom generator when building:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
User user = builder.build(User.class);
assert user.getEmail().endsWith("@mycompany.com");
```
In a DI context, register this as the bean:
```java
// Spring Boot
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
}
```
### Example: Money Type
```java
public class MoneyValueGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Generate prices in a realistic range: $5.00 to $999.99
BigDecimal price = BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
);
return price.setScale(2, RoundingMode.HALF_UP);
}
}
```
---
## Best Practices
### 1. Use for Integration Tests, Not Unit Tests
**Good:** Integration test with database
```java
@Test
void whenSaving_thenCanRetrieve() {
Product product = builder.save(Product.class);
Product found = database.find(Product.class, product.getId());
assertThat(found).isNotNull();
}
```
**Poor:** Validation test requiring specific values
```java
@Test
void whenNameIsBlank_thenThrowException() {
Product product = builder.build(Product.class); // name is random!
product.setName(""); // have to override anyway
// ... test proceeds
}
```
### 2. Override Values for Specific Test Scenarios
When test requirements demand specific field values, manually override after building:
```java
@Test
void whenStockIsLow_thenShowWarning() {
Product product = builder.build(Product.class);
product.setQuantity(2); // Specific value for this test
boolean shouldWarn = product.shouldShowLowStockWarning();
assertThat(shouldWarn).isTrue();
}
```
### 3. Create Fixture Factories for Common Patterns
For shared domain-specific setup, encapsulate build patterns in an instance helper class
rather than a static factory. In a DI context, this class can be registered as a bean
alongside `TestEntityBuilder`:
```java
// Spring Boot
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
@Bean
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
return new OrderTestFactory(builder, database);
}
}
public class OrderTestFactory {
private final TestEntityBuilder builder;
private final Database database;
public OrderTestFactory(TestEntityBuilder builder, Database database) {
this.builder = builder;
this.database = database;
}
public Order savePendingOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.PENDING);
database.save(order);
return order;
}
public Order saveShippedOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.SHIPPED);
order.setShippedAt(Instant.now());
database.save(order);
return order;
}
}
// Usage in tests:
@SpringBootTest
class OrderControllerTest {
@Autowired OrderTestFactory orderFactory;
@Test
void whenOrderPending_thenCanUpdate() {
Order order = orderFactory.savePendingOrder();
// ... test logic
}
}
```
### 4. Build Multiple Distinct Instances
Each call to `build()` or `save()` produces a new instance with fresh random values:
```java
@Test
void whenFetchingMultipleOrders_thenAllUnique() {
Order order1 = builder.save(Order.class);
Order order2 = builder.save(Order.class);
Order order3 = builder.save(Order.class);
assertThat(order1.getId()).isNotEqualTo(order2.getId());
assertThat(order2.getId()).isNotEqualTo(order3.getId());
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
}
```
---
## Complete Examples
### Example 1: Integration Test with Spring Boot
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
the repository under test:
```java
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@SpringBootTest
class OrderRepositoryTest {
@Autowired OrderRepository orderRepository;
@Autowired TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
assertThat(pending).hasSize(2);
}
}
```
### Example 2: Integration Test with Avaje Inject
```java
@TestScope
@Factory
class TestConfiguration {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@InjectTest
class OrderControllerTest {
@Inject TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
// ... test assertions
}
}
```
### Example 3: Recursive Relationship Building
```java
@Test
void whenBuildingOrderWithCustomer_thenBothPopulated() {
Order order = builder.build(Order.class);
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
assertThat(order.getCustomer()).isNotNull();
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
assertThat(order.getCustomer().getName()).isNotNull();
// Saving cascades to customer:
Order saved = builder.save(Order.class);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCustomer().getId()).isNotNull();
}
```
### Example 4: Custom Generator for Domain Values
```java
// Custom generator for your domain
class ECommerceTestDataGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Product prices typically range $10-$500
return BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
).setScale(2, RoundingMode.HALF_UP);
}
}
@Test
void usingCustomGenerator() {
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new ECommerceTestDataGenerator())
.build();
Product product = builder.build(Product.class);
assertThat(product.getPrice())
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
}
```
---
## Troubleshooting
### "No BeanDescriptor found for [Class] — is it an @Entity?"
**Cause:** The class you're trying to build is not registered as an Ebean entity.
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
```java
@Entity
@Table(name = "products")
public class Product {
// ...
}
```
### Fields are unset even though I expected them to be populated
**Cause:** `TestEntityBuilder` does **not** populate:
- `@Id` fields (identity/primary key; left unset until persist)
- `@Version` fields (optimistic locking; left unset until persist)
- `@Transient` fields
- `@OneToMany` collections
- Non-cascade `@ManyToOne` relationships
**Solution:** Set only the fields your test scenario cares about, then persist.
`@Id` and `@Version` are usually database-managed and should typically be left
unset before save:
```java
Product product = builder.build(Product.class);
product.setName("specific-name"); // test-specific override
database.save(product); // database assigns @Id/@Version
```
### Building recursive relationships causes StackOverflowError
**Cause:** Two or more entities mutually reference each other without cycle detection.
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
```java
Person person = builder.build(Person.class);
person.getOrganization().setFounder(null); // Break cycle
```
### Values generated are "too random" for my test
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
**Solution:** Create a custom generator that produces deterministic values:
```java
class DeterministicTestDataGenerator extends RandomValueGenerator {
private int counter = 0;
@Override
protected String randomString(String propName, int maxLength) {
return "test_" + (counter++);
}
}
```
---
## Summary
`TestEntityBuilder` accelerates test development by:
1. **Reducing boilerplate** — No need to manually set every field
2. **Improving readability** — Tests focus on what matters, not setup
3. **Enabling variety** — Each build produces distinct random values
4. **Respecting constraints** — Column lengths and decimal scales are enforced
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
+464
View File
@@ -0,0 +1,464 @@
# Guide: Using `RawSql` with Ebean
## Purpose
`RawSql` lets you back an Ebean bean with a **hand-written SQL query** instead of
Ebean generating the SQL from the entity mapping. Ebean still handles object
mapping (result set columns → bean properties), lazy loading of associated beans,
and - depending on how the `RawSql` is built - dynamic `WHERE`/`HAVING` predicates
added through the normal query API.
Use this guide when you need to:
- run vendor-specific SQL, complex aggregation, or reporting queries that don't
map cleanly to an ORM query
- reuse a hand-tuned query but still want typed/dynamic predicates, paging, or
`ORDER BY` added by the caller
- back a query bean (`Q*`) or DTO-like bean with SQL containing a CTE, window
function, or subquery in the `FROM` clause
Prefer ordinary query bean queries first - see
[Write Ebean queries with query beans](writing-ebean-query-beans.md), Step 9,
for the full decision order (query bean → `asDto()` → DTO query → raw SQL).
This guide covers raw SQL once you've decided it's the right tool.
---
## The bean behind a `RawSql` query
A bean queried with `RawSql` is not necessarily backed by a physical table. Annotate
it `@Entity @Sql` to tell Ebean it is mapped via `RawSql` rather than table DDL:
```java
@Entity
@Sql
public class OrderAggregate {
@OneToOne
Order order;
Double totalAmount;
Long totalItems;
// getters/setters
}
```
`@Sql` beans still get a generated query bean (`QOrderAggregate`) if the
querybean-generator annotation processor is configured - see
[Using `RawSql` with query beans](#using-rawsql-with-query-beans) below.
You can also query an ordinary table-backed `@Entity` with `RawSql` - the column
mapping just needs to line up with that entity's properties.
---
## Building a `RawSql` - three factory methods
`RawSqlBuilder` has three ways to construct a `RawSql`, depending on how much of
the SQL Ebean needs to understand:
| Method | SELECT columns parsed? | Dynamic WHERE/HAVING/ORDER BY? | Use for |
|--------|------------------------|------------------------|---------|
| `RawSqlBuilder.parse(sql)` | Yes | Yes | Ordinary `SELECT ... FROM ... WHERE ...` statements |
| `RawSqlBuilder.unparsed(sql)` | No | No | Fixed SQL that never needs additional predicates |
| `RawSqlBuilder.withPlaceholders(sql)` | No (explicit `columnMapping()` required) | Yes, via `${where}` / `${andWhere}` / `${having}` / `${andHaving}` / `${orderBy}` / `${andOrderBy}` | CTEs, window functions, subqueries - SQL that keyword-based parsing can't handle |
### `parse(sql)` - the common case
`parse(sql)` scans the SQL text for the `select` / `from` / `where` / `group by`
/ `having` / `order by` keywords to work out the SELECT column list (so it can
validate your column mappings) and the injection points for dynamic `WHERE`/
`HAVING` expressions.
```java
RawSql rawSql = RawSqlBuilder.parse(
"select c.id, c.name, c.status from customer c")
.columnMapping("c.id", "id")
.columnMapping("c.name", "name")
.columnMapping("c.status", "status")
.create();
List<Customer> customers = DB.find(Customer.class)
.setRawSql(rawSql)
.where().eq("status", Customer.Status.ACTIVE)
.orderBy("name")
.findList();
```
Because the SQL is parsed, mistakes in `columnMapping()` (unknown column, wrong
order for `unparsed`-style mappings) are caught early. **This fails on SQL the
keyword parser can't make sense of** - a `WITH` CTE, a window function, a
subquery in `FROM`, etc. - because the keyword positions found don't correspond
to the outer query's real structure. Use `withPlaceholders(sql)` for that SQL
instead (see below).
### `unparsed(sql)` - fixed queries
`unparsed(sql)` skips all parsing. The SQL is used exactly as written, and **no
further `WHERE`/`HAVING`/`ORDER BY` can be added** by the caller - useful for a
completely fixed reporting query with no caller-supplied filtering.
```java
RawSql rawSql = RawSqlBuilder.unparsed(
"select id, name, status from customer where status = 'ACTIVE'")
.columnMapping("id", "id")
.columnMapping("name", "name")
.columnMapping("status", "status")
.create();
List<Customer> customers = DB.find(Customer.class)
.setRawSql(rawSql)
.findList();
```
Column mappings for `unparsed(sql)` must be supplied **in the same order** as
the columns appear in the SQL, since there's no parsing to match them by name.
### `withPlaceholders(sql)` - complex SQL (CTEs, window functions, subqueries)
`withPlaceholders(sql)` avoids keyword scanning entirely. You mark exactly where
a dynamic `WHERE`/`HAVING`/`ORDER BY` expression should be injected using
placeholder tokens, and column mappings are always explicit (as with `unparsed`).
#### Placeholder reference
| Placeholder | Meaning | Use when |
|-------------|---------|----------|
| `${where}` | Insert a new `WHERE <expr>` clause here | No static `WHERE` clause exists yet at this point in the SQL |
| `${andWhere}` | Insert `AND <expr>` here | A static `WHERE ...` clause already exists in the SQL and you want to append to it |
| `${having}` | Insert a new `HAVING <expr>` clause here | No static `HAVING` clause exists yet at this point in the SQL |
| `${andHaving}` | Insert `AND <expr>` here | A static `HAVING ...` clause already exists in the SQL and you want to append to it |
| `${orderBy}` | Insert a new `ORDER BY <expr>` clause here | No static `ORDER BY` clause exists yet at this point in the SQL, and callers may supply `.orderBy(...)` |
| `${andOrderBy}` | Insert `, <expr>` here | A static `ORDER BY ...` clause already exists in the SQL and you want callers to be able to append extra sort columns to it |
Rules:
- At least one placeholder is required - `withPlaceholders(sql)` throws
`IllegalArgumentException` if none of the six tokens are present.
- Use only the placeholders you need. Omit `${where}`/`${andWhere}` entirely if
the query never needs a dynamic `WHERE` (e.g. only a dynamic `HAVING` on an
aggregate). Omit `${having}`/`${andHaving}` if there's no dynamic `HAVING`.
Omit `${orderBy}`/`${andOrderBy}` if the ordering is always fixed.
- Explicit `columnMapping()` is required for every returned column - there is no
column-list parsing to infer names from.
- **A caller-supplied `.orderBy(...)`/`.order(...)` is only applied if the SQL
contains an `${orderBy}` or `${andOrderBy}` placeholder.** Without one of
those placeholders there is no defined injection point for dynamic ordering,
so any `.orderBy(...)` call on the query is safely ignored rather than risk
producing invalid SQL - even if the template has a static trailing
`ORDER BY ...` of its own. If you need callers to be able to influence
ordering, add `${orderBy}` (no existing static order by) or `${andOrderBy}`
(append after an existing static order by).
- Any other static SQL that follows a `${where}`/`${having}` placeholder (e.g.
a trailing `GROUP BY`) is preserved and correctly positioned **after**
whatever dynamic expression gets injected at that placeholder.
#### Example - CTE with `${where}`
```java
String sql = """
with order_totals as (
select o.id as order_id, sum(d.qty * d.unit_price) as total_amount
from o_order o
join o_order_detail d on d.order_id = o.id
group by o.id
)
select order_id, total_amount
from order_totals
${where}
order by order_id
""";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.where().gt("totalAmount", 100)
.findList();
```
`total_amount` is a genuine column of the `order_totals` CTE here, so it's valid
to filter on it in the outer `WHERE` - this only works because the aggregate is
computed inside the CTE rather than as a same-level `SELECT` alias.
#### Example - static `WHERE` already present, append with `${andWhere}`
```java
String sql = "... from order_totals where total_amount > 0 ${andWhere} order by order_id";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
// executed SQL: ... where total_amount > 0 and total_amount > ? order by order_id
DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.where().gt("totalAmount", 100)
.findList();
```
#### Example - `${having}` only, filtering on an aggregate directly
No `WHERE` placeholder is needed if you only ever filter on the aggregate value:
```java
String sql =
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
" from o_order o join o_order_detail d on d.order_id = o.id" +
" group by o.id" +
" ${having}" +
" order by order_id";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.having().gt("totalAmount", 100)
.findList();
```
The dynamic `HAVING` clause is injected before the static trailing `ORDER BY`,
even though `${having}` is the only placeholder present. Because there's no
`${orderBy}`/`${andOrderBy}` placeholder here, a caller-supplied `.orderBy(...)`
would be ignored - the ordering stays fixed as `order by order_id`.
#### Example - both `${where}` and `${having}`
```java
String sql =
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
" from o_order o join o_order_detail d on d.order_id = o.id" +
" ${where}" +
" group by o.id" +
" ${having}" +
" order by order_id";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.where().gt("order.id", 0)
.having().gt("totalAmount", 50)
.findList();
```
Both the dynamic `WHERE` and dynamic `HAVING` are injected at their respective
placeholders, and the trailing `order by order_id` is preserved after the
`HAVING` clause.
#### Example - `${orderBy}`, fully dynamic ordering
Use `${orderBy}` when there's no static default ordering and you want the
caller's `.orderBy(...)` to control it entirely:
```java
String sql =
"with order_totals as (" +
" select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
" from o_order o join o_order_detail d on d.order_id = o.id" +
" group by o.id" +
")" +
" select order_id, total_amount from order_totals" +
" ${where}" +
" ${orderBy}";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
// executed SQL: ... where total_amount > ? order by total_amount desc
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.where().gt("totalAmount", 0)
.orderBy("totalAmount desc")
.findList();
```
If the caller doesn't call `.orderBy(...)`, nothing is injected at `${orderBy}`
and no `ORDER BY` clause is emitted at all.
#### Example - `${andOrderBy}`, appending to a static default ordering
Use `${andOrderBy}` when there's a sensible static default ordering but you
want callers to be able to add extra tie-breaker sort columns:
```java
String sql =
"... from order_totals" +
" ${where}" +
" order by total_amount desc ${andOrderBy}";
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
.columnMapping("order_id", "order.id")
.columnMapping("total_amount", "totalAmount")
.create();
// executed SQL: ... order by total_amount desc , order_id
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql)
.where().gt("totalAmount", 0)
.orderBy("order.id")
.findList();
```
---
## Using `fetchQuery()` to build out more of the graph
A `RawSql` query can be the **root query** and still use `fetchQuery(path)` the
same way an ordinary ORM query does - Ebean runs the raw SQL for the root rows,
then runs additional secondary ORM queries to populate the requested paths. This
lets you hand-write only the part of the query that needs raw SQL (e.g. an
aggregate/CTE) and let the ORM build out the rest of the object graph normally.
```java
List<OrderAggregate> list = DB.find(OrderAggregate.class)
.setRawSql(rawSql) // root query - runs the CTE/aggregate SQL
.fetchQuery("order") // secondary query - loads the full Order
.fetchQuery("order.details") // secondary query - loads Order.details
.where().gt("totalAmount", 50)
.findList();
```
This executes **three** queries: the raw SQL root query, then one secondary
query per `fetchQuery(path)` call.
**Important**: if the raw SQL's column mapping only populates part of an
association (e.g. only `order.id`, as in the examples above), that association
is a *partial reference*. To load a nested to-many under it (e.g.
`order.details`), you must add an explicit `fetchQuery(...)` (or `fetch(...)`)
for the **intermediate path** (`order`) as well as the nested path
(`order.details`) - `fetchQuery("order.details")` alone will leave `details` as
a deferred/lazy collection, because Ebean doesn't otherwise have a fetch node
for `order` to hang the secondary query off. If the raw SQL already selects the
full set of columns for an association directly (no partial reference), this
extra step isn't needed.
This is the same `fetchQuery()` mechanism used for ordinary query bean queries -
see [Use `fetchQuery()` for to-many paths](writing-ebean-query-beans.md#step-7---use-fetchquery-for-to-many-paths-and-fetchgroup-for-reusable-query-shapes)
for background on why to-many paths are loaded via secondary queries rather than
a single joined query.
---
## Column mapping
Every `RawSqlBuilder` (except a bare `unparsed(sql)` with implicit positional
mapping) uses `columnMapping(dbColumn, propertyName)` to map SQL result columns
to bean properties:
```java
.columnMapping("order_id", "order.id") // maps to the "order" association's "id" property
.columnMapping("total_amount", "totalAmount")
```
- Dotted property paths (e.g. `"order.id"`) map a column into a nested/associated
bean property.
- `columnMappingIgnore(dbColumn)` marks a selected column as intentionally unmapped
(present in the SQL but not needed on the bean).
- `tableAliasMapping(tableAlias, path)` bulk-renames every mapping using a given
SQL table alias to be prefixed with a bean property path - handy when a `parse()`
query selects many columns from a joined table (e.g. alias `c` → path `customer`)
and you don't want to repeat the prefix in every `columnMapping()` call.
---
## Using `RawSql` with query beans
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
generated query bean, giving type-safe `where()`/`having()`-equivalent
expressions (as bean properties) over hand-written SQL. Every generated query
bean exposes `setRawSql(...)`:
```java
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
.columnMapping("id", "id")
.columnMapping("name", "name")
.columnMapping("status", "status")
.create();
List<Customer> customers = new QCustomer()
.setRawSql(rawSql)
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
.findList();
```
This also works with `withPlaceholders(sql)` and an `@Sql` query bean:
```java
List<OrderAggregate> list = new QOrderAggregate()
.setRawSql(rawSql) // built with withPlaceholders() as shown above
.totalAmount.gt(100)
.findList();
```
The typed property expression (`.totalAmount.gt(100)`) is translated to a bound
predicate and injected at the `${where}`/`${having}` placeholder position, exactly
as `.where().gt("totalAmount", 100)` would be on the plain `Query<T>` API.
---
## Common anti-patterns
### Anti-pattern 1 - reaching for raw SQL before trying a query bean
Complex-looking joins are often just ordinary association traversal in a query
bean. Don't use raw SQL just because a query touches several tables - see
[Write Ebean queries with query beans](writing-ebean-query-beans.md).
### Anti-pattern 2 - using `parse(sql)` on a CTE or window-function query
`parse(sql)` will throw a parsing exception (or silently mis-locate the WHERE
injection point) on SQL it can't understand structurally. If your SQL starts
with `WITH ...` or has a subquery in `FROM`, use `withPlaceholders(sql)` instead.
### Anti-pattern 3 - filtering on a same-level SELECT alias
You cannot add a dynamic `WHERE` predicate on a `SELECT`-clause alias in the
same query level (e.g. `select sum(x) as total ... ${where}` - `total` isn't a
real column yet at the `WHERE` stage of that query level). Either:
- move the aggregation into a CTE and filter on the CTE's output column in the
outer query (`WHERE` case), or
- use `${having}`/`${andHaving}` to filter on the aggregate at the `HAVING` stage
of the same query level, where the aggregate expression is valid.
### Anti-pattern 4 - forgetting `columnMapping()` with `unparsed()`/`withPlaceholders()`
Both `unparsed(sql)` and `withPlaceholders(sql)` require **every** returned
column to be explicitly mapped (or explicitly ignored via
`columnMappingIgnore(...)`) - there's no column-list parsing to infer them.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `RuntimeException: Error parsing sql, can not find ... keyword` | `parse(sql)` used on SQL with a CTE, window function, or subquery in `FROM` | Use `RawSqlBuilder.withPlaceholders(sql)` instead |
| `IllegalArgumentException: withPlaceholders() requires at least one of ${where}, ${andWhere}, ${having}, ${andHaving}, ${orderBy}, ${andOrderBy}...` | None of the six placeholder tokens were found in the SQL | Add the appropriate placeholder token at the injection point |
| Dynamic `WHERE`/`HAVING` predicate silently has no effect, or query throws | Used `unparsed(sql)` and then tried to add a predicate | `unparsed(sql)` queries cannot be modified - switch to `parse(sql)` or `withPlaceholders(sql)` |
| Generated SQL is invalid / clauses appear in the wrong order | Predicates added via `.where()`/`.having()` don't match the placeholders actually present in the SQL | Make sure `${where}`/`${having}` (or the `and` variants) exist at the point you expect predicates to be injected |
| `.orderBy(...)`/`.order(...)` on the query silently has no effect | The SQL has no `${orderBy}`/`${andOrderBy}` placeholder | This is by design - without one of those placeholders there's no defined injection point, so the ordering is ignored rather than corrupting the SQL. Add `${orderBy}` or `${andOrderBy}` if you need caller-controlled ordering |
| `Unknown column` / unmapped property error | Missing `columnMapping()` for a selected column | Add a `columnMapping(...)` or `columnMappingIgnore(...)` for every SQL column |
| `fetchQuery("a.b")` collection stays deferred/lazy | `a` is a partial reference from the raw SQL column mapping (e.g. only `a.id` mapped), and there's no fetch node for `a` itself | Add `fetchQuery("a")` (or `fetch("a")`) alongside `fetchQuery("a.b")` |
---
## Related documentation
- [Write Ebean queries with query beans](writing-ebean-query-beans.md)
- [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md)
- [Ebean query docs](https://ebean.io/docs/query/)
+603
View File
@@ -0,0 +1,603 @@
# Guide: Write Ebean Queries with Query Beans
## Purpose
This guide gives step-by-step instructions for AI agents and developers to write
application queries using Ebean query beans.
Use this guide when the project already has Ebean configured and you need to:
- add a repository/service query
- replace string-based ORM queries with type-safe query beans
- tune what data is fetched to avoid over-fetching or N+1 issues
- return DTO projections for list screens or API responses
The default recommendation is:
1. Prefer query beans first
2. Prefer entity queries for domain logic
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
4. Prefer DTO projection for summary/read-model use cases
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
---
## Prerequisites
- The project already uses Ebean ORM
- Query bean generation is configured (for Maven this usually means
`querybean-generator` is registered as an annotation processor)
- Entity beans already exist
- A compile/build has run successfully since the last entity model change
If query beans are not yet configured, first follow:
[`add-ebean-postgres-maven-pom.md`](add-ebean-postgres-maven-pom.md)
---
## Step 1 - Verify the generated `Q*` query bean exists
For each entity bean, Ebean generates a query bean with the same name prefixed
with `Q`.
Examples:
- `Customer` -> `QCustomer`
- `Order` -> `QOrder`
- `Contact` -> `QContact`
Import the generated type from the query bean package:
```java
import org.example.domain.query.QCustomer;
```
If the `Q*` type does not exist or the IDE cannot resolve it:
1. Confirm the entity compiled successfully
2. Run a normal project compile/build
3. If the entity was renamed or moved, run a full rebuild rather than relying on
incremental compilation
### Important caveat - entity rename
After refactoring an entity name, old generated query beans can remain on disk
until the next full build. If both old and new `Q*` types appear to exist, do a
clean rebuild before editing application queries.
---
## Step 2 - Choose the terminal query method before writing predicates
Decide what the caller actually needs. This determines the terminal method and
often the right query shape.
| Need | Preferred method | Notes |
|------|------------------|-------|
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
### Example - existence check
```java
boolean alreadyUsed = new QCustomer()
.email.equalTo(email)
.exists();
```
### Example - unique lookup
```java
Customer customer = new QCustomer()
.email.equalTo(email)
.findOne();
```
Do **not** use `findOne()` for predicates that can match multiple rows.
### Example - stream and map to another type
Choose based on result size and how you consume it:
- **`findList().stream()`** — executes the query, materialises the rows,
**releases the connection**, then streams over an in-memory list. No open
database resources and no try-with-resources needed. Prefer this for small or
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
connection (and an implicit transaction) open for the **whole lifetime of the
stream pipeline**. It must be closed with try-with-resources. Prefer it when
the result may be large, when you want constant memory, or when you want to
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
```java
// small, bounded result fully collected -> findList().stream()
List<PendingPlan> pending = new QCaptureRequest()
.collectedAt.isNull()
.orderBy().requestedAt.asc()
.findList()
.stream()
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
.toList();
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
try (Stream<Customer> stream = new QCustomer()
.status.equalTo(Status.NEW)
.findStream()) {
stream
.map(...)
.forEach(...);
}
```
For processing large results one bean at a time, `findEach()` is often the
simplest choice because it closes the underlying resources automatically.
---
## Step 3 - Build predicates by traversing properties and associations
With query beans, write predicates directly against properties. When you
traverse an association, Ebean adds the necessary joins automatically.
### Example - root property predicates
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.name.istartsWith("rob")
.findList();
```
### Example - association traversal
```java
List<Customer> customers = new QCustomer()
.billingAddress.city.equalTo("Auckland")
.findList();
```
### Example - collection predicate
```java
List<Customer> customers = new QCustomer()
.contacts.isEmpty()
.findList();
```
### Optional predicates - prefer conditional helpers over `if` blocks
When a filter is driven by a nullable/optional parameter, use the built-in
conditional helpers instead of wrapping predicates in `if` blocks. The query
stays fluent and reads top-to-bottom, and no predicate is added when the value
is absent.
| Helper | Adds predicate when | Resulting SQL |
|--------|---------------------|---------------|
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
```java
// Instead of building the query with if blocks:
QCustomer q = new QCustomer();
if (name != null && !name.isBlank()) {
q.name.eq(name.trim());
}
if (status != null) {
q.status.eq(status);
}
List<Customer> customers = q.findList();
// Prefer the conditional helpers:
List<Customer> customers = new QCustomer()
.name.eqIfNotBlank(name)
.status.eqIfPresent(status)
.findList();
```
Use `eqOrNull(v)` when a null column value should also match - for example an
"any environment" row stored with `env_id is null` should surface under any env
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
```java
List<CaptureRequest> rows = new QCaptureRequest()
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
.findList();
```
### Agent rule
When adding a new query:
1. Start from the root entity that the caller wants back
2. Add predicates with query bean properties
3. Traverse relationships instead of writing manual join SQL
4. Keep property references type-safe; avoid string property names unless the API
specifically requires them
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
null column"
---
## Step 4 - Add ordering, limits, and pagination deliberately
Do not leave list queries unordered unless the call site truly does not care.
For UI lists, APIs, and background jobs, explicit ordering is usually better.
### Example - ordered list with limit
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().name.asc()
.setMaxRows(50)
.findList();
```
### Example - offset/limit pagination
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().id.asc()
.setFirstRow(offset)
.setMaxRows(pageSize)
.findList();
```
### Example - paged list with total count
```java
PagedList<Customer> page = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().id.asc()
.setFirstRow(offset)
.setMaxRows(pageSize)
.findPagedList();
page.loadRowCount();
List<Customer> customers = page.getList();
int totalRowCount = page.getTotalRowCount();
```
### Agent rule
- Use `findList()` when the caller only needs rows
- Use `findPagedList()` when the caller also needs page metadata or total counts
- Pair pagination with a stable `orderBy()` so page boundaries stay predictable
---
## Step 5 - Control fetched data with `select()` and `fetch()`
By default, entity queries can load more of the object graph than the caller
actually needs. Use `select()` and `fetch()` to control the root and association
properties that are loaded.
### Root properties with `select()`
Use `select()` to define which properties should be fetched on the root entity.
### Associated bean properties with `fetch()`
Use `fetch()` to define what should be fetched on associated paths.
### Example - partial entity query
```java
private static final QCustomer CUST = QCustomer.alias();
private static final QContact CONT = QContact.alias();
List<Customer> customers = new QCustomer()
.select(CUST.name, CUST.status, CUST.whenCreated)
.contacts.fetch(CONT.email)
.name.istartsWith("rob")
.findList();
```
In this example:
- `select(...)` tunes the root `Customer` properties
- `contacts.fetch(...)` tunes the associated `Contact` properties
- the query still returns `Customer` entity beans
### Agent rules for partial entity queries
1. Only use `select()`/`fetch()` when you know what the caller will read next
2. Do not treat partially loaded entities like fully populated API DTOs
3. If the caller only needs summary fields, prefer a DTO projection instead
---
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
read-only graph.
This means:
- setters cannot mutate returned beans
- associated collections are unmodifiable
- lazy loading is disabled
- accessing an unloaded property throws `LazyInitialisationException`
- the query uses `PersistenceContextScope.QUERY`
### Example - read-only entity graph
```java
private static final QCustomer CUST = QCustomer.alias();
private static final QContact CONT = QContact.alias();
List<Customer> customers = new QCustomer()
.select(CUST.name, CUST.status, CUST.whenCreated)
.contacts.fetch(CONT.email)
.status.equalTo(Customer.Status.ACTIVE)
.setUnmodifiable(true)
.findList();
```
### When to prefer `setUnmodifiable(true)`
Use it when the result is meant to be read-only, such as:
- service/query methods returning entity graphs for display or serialization
- query results you want the application to treat as immutable
- cached query results or other shared read models backed by entity graphs
- partial entity graphs where you want accidental lazy loading to fail fast
### When **not** to use it
Do **not** use `setUnmodifiable(true)` when the caller will:
- modify the beans and save them later
- rely on lazy loading of associations or unloaded scalar properties
- treat the result as a working persistence model rather than a read-only view
### Agent rule
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
should be the default recommendation. If the caller needs a mutable model or a
serialized summary shape, choose mutable entities or DTO projection instead.
If you need cached assoc-one references for unmodifiable graphs, see
[Immutable bean cache for read-only references](immutable-bean-cache.md).
---
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
Ebean applies important SQL rules when translating ORM queries:
1. It does not generate SQL cartesian products
2. It honors `maxRows` in SQL
This means to-many paths often need special handling.
### Use `fetchQuery()` when:
- the query includes a `OneToMany` or `ManyToMany` path
- the query includes `setMaxRows(...)`
- the query loads multiple to-many paths
- you want the query shape to make the secondary-query behavior explicit
### Example - explicit secondary queries for to-many paths
```java
private static final QCustomer CUST = QCustomer.alias();
List<Order> orders = new QOrder()
.customer.fetch(CUST.name)
.lines.fetchQuery()
.shipments.fetchQuery()
.status.equalTo(Order.Status.NEW)
.setMaxRows(100)
.findList();
```
### Use `FetchGroup` when:
- the same fetch shape is reused in multiple places
- you want to separate predicate logic from fetch-shape tuning
- you want an immutable, static query-shape definition
### Example - reusable fetch group
```java
private static final QCustomer CUST = QCustomer.alias();
private static final FetchGroup<Customer> CUSTOMER_SUMMARY =
QCustomer.forFetchGroup()
.select(CUST.name, CUST.status, CUST.whenCreated)
.billingAddress.fetch()
.buildFetchGroup();
List<Customer> customers = new QCustomer()
.select(CUSTOMER_SUMMARY)
.status.equalTo(Customer.Status.ACTIVE)
.findList();
```
### Agent rule
If the caller needs multiple to-many paths or a paged query, be suspicious of a
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
---
## Step 8 - Use DTO projection when the caller does not need entity beans
For list screens, API summaries, exports, or read-model views, the caller often
does **not** need managed entity beans. In those cases, project directly to a
DTO using `asDto(...)`.
### Example - DTO projection with query beans
```java
import static org.example.domain.query.QCustomer.Alias.id;
import static org.example.domain.query.QCustomer.Alias.name;
public record CustomerSummary(long id, String name) {}
List<CustomerSummary> summaries = new QCustomer()
.select(id, name)
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().name.asc()
.asDto(CustomerSummary.class)
.findList();
```
### Prefer DTO projection when:
- the caller will serialize the result directly
- only a subset of fields is needed
- the result is not going to be updated and saved back as an entity
- the query contains formulas or aggregation intended for a read model
`asDto(...)` maps a **flat**, single-row result. If the target DTO itself needs nested
DTO fields (ToOne/ToMany) mirroring part of the entity graph, use
`mapTo(Dto.class)` instead — see
[Mapping entity graphs to DTOs](mapping-entity-graphs-to-dtos.md).
---
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
Prefer the following order:
1. Query bean query
2. Query bean query + `asDto(...)`
3. `database.findDto(...)` or DTO query
4. Native SQL / `SqlQuery` / `RawSql`
### Typical reasons to use raw SQL
- vendor-specific SQL that query beans do not express well
- advanced aggregation or database functions
- hand-tuned reporting queries
- stored procedures or raw JDBC workflows
Do **not** jump to raw SQL just because the query joins multiple tables. Query
beans already handle ordinary relationship traversal well.
### Using `RawSql` with query beans
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
generated query bean, giving type-safe `where()`/`having()` expressions over
hand-written SQL. Every generated query bean exposes `setRawSql(...)`:
```java
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
.columnMapping("id", "id")
.columnMapping("name", "name")
.columnMapping("status", "status")
.create();
List<Customer> customers = new QCustomer()
.setRawSql(rawSql)
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
.findList();
```
For the full guide to building `RawSql` - including `unparsed()`,
`withPlaceholders()` for CTEs/window functions, the `${where}` / `${andWhere}`
/ `${having}` / `${andHaving}` placeholder reference, and column mapping - see
[Using `RawSql` with Ebean](using-rawsql-with-ebean.md).
---
## Common anti-patterns
### Anti-pattern 1 - Using raw SQL first
**Avoid:**
```java
List<Customer> customers = database.findNative(Customer.class,
"select c.* from customer c join address a on a.id = c.billing_address_id where a.city = ?")
.setParameter(1, city)
.findList();
```
**Prefer:**
```java
List<Customer> customers = new QCustomer()
.billingAddress.city.equalTo(city)
.findList();
```
### Anti-pattern 2 - Using `findOne()` on a non-unique predicate
**Avoid:**
```java
Customer customer = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.findOne();
```
**Why:** Many rows can match; this is not a unique lookup.
### Anti-pattern 3 - Returning partially loaded entities as API models
If the caller only needs summary fields, return a DTO instead of partially
loaded entities that might later trigger more loading or confuse serializers.
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
so accidental setter calls, collection mutation, and lazy loading fail fast.
### Anti-pattern 5 - Fetching every relationship "just in case"
Do not eagerly fetch large object graphs unless the immediate caller will use
them. Query tuning is part of the job.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `Cannot resolve symbol QCustomer` | Query bean generation not configured or build not run | Check the annotation processor and run a build |
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
| Setter calls or collection mutation fail on query results | `setUnmodifiable(true)` returned a read-only graph | Remove `setUnmodifiable(true)` or treat the result as read-only |
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
| Ebean executes secondary queries for a to-many path | ORM rules avoided cartesian product or honored `maxRows` | This is expected; use `fetchQuery()` explicitly when appropriate |
---
## Summary workflow for AI agents
When asked to add or modify an Ebean query:
1. Verify the relevant `Q*` type exists
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
3. Add predicates with query bean properties and association traversal
4. Add explicit ordering and pagination if relevant
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
7. Prefer DTO projection for read models and serialized responses
8. Only use raw SQL if the ORM query is genuinely the wrong tool
---
## Related documentation
- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
- [Entity Bean Creation](entity-bean-creation.md)
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
- [Using `RawSql` with Ebean](using-rawsql-with-ebean.md)
- [Ebean query docs](https://ebean.io/docs/query/)
+333
View File
@@ -0,0 +1,333 @@
# Immutable Bean Cache — notes on multi-level / remote caching
These notes capture design thoughts for a possible future multi-level immutable bean cache,
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
in addition to an in-JVM cache.
## Current important constraint
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
That means:
- `getIfPresent(id)` is on the **row read hot path**
- it must remain **cheap and local**
- it should **not** perform network I/O
- it should **not** deserialize remote payloads
- it should **not** trigger loading or record misses
## Strong recommendation
For any multi-level cache design:
- **L1 cache** = in-JVM cache of already materialized immutable beans
- **L2 cache** = remote/shared cache of serialized immutable snapshots
- **Loader** = Ebean query using the configured fetch group
With that split:
- `getIfPresent(id)` => **L1 only**
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
This preserves the `AssocOneHelp` fast path.
---
## Snapshot mindset
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
A cached value is specific to:
- bean type
- bean id
- tenant (if multi-tenant)
- fetch-group / cache identity
- serializer/schema version
This matters because a `Customer` cached with:
- `select("name,version")`
is not equivalent to a `Customer` cached with:
- `select("name,version").fetch("billingAddress", "line1,city")`
## Key design recommendation
Remote keys should include at least:
- bean type
- bean id
- tenant id (if applicable)
- cache/fetch-group identity
- optionally serializer/schema version
Example shape:
- `immutable:Customer:basic:42`
- `immutable:Customer:withAddresses:42`
---
## Recommended multi-level flow
### L1
Store actual read-only `EntityBean` instances.
Responsibilities:
- support `getIfPresent(id)`
- avoid repeated deserialize cost
- avoid network calls on row read path
### L2
Store serialized immutable snapshots.
Responsibilities:
- batch lookup only
- support cross-JVM sharing
- feed L1 with materialized immutable beans
### Loader
Use the existing query/fetch-group-based loader for misses.
### Suggested `getAll(ids)` flow
1. Check L1
2. Batch remaining ids to L2
3. Deserialize L2 hits into read-only beans
4. Put those beans into L1
5. Batch remaining misses to DB loader
6. Freeze / ensure read-only beans
7. Write through to L2
8. Put into L1
9. Negative-cache true misses if desired
---
## Invalidation is more important than serialization
Things to think about:
- update/delete invalidation across JVMs
- local L1 invalidation when L2 entry is removed
- ordering relative to DB commit
- multiple cache instances for the same bean type but different fetch groups
- tenant-scoped invalidation
Recommended direction:
- keep current immutable-cache invalidation semantics
- add a remote invalidation/event mechanism for L2-backed caches
- each JVM should evict affected L1 entries when notified
Examples:
- Redis: pub/sub or streams
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
---
## Serialization format considerations
## JSON
### Pros
- human readable / debuggable
- easier rolling upgrades
- field-name based, so generally more tolerant of schema evolution
- good fit for Redis strings or Postgres JSONB
- easier operational debugging
### Cons
- larger payloads
- more CPU to serialize/deserialize
- nested graphs / enums / dates / inheritance need disciplined handling
## Kryo / generic binary serialization
### Pros
- smaller payloads
- often faster than JSON
- can preserve object graphs efficiently
### Cons
- more fragile across versions and rolling deploys
- class registration / compatibility pain
- harder to inspect/debug
- tighter coupling to JVM/class layout
- riskier for long-lived shared cache entries
## Recommendation
For a first remote/shared implementation:
- prefer **JSON** or another self-describing structured format
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
---
## What to serialize
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
A cleaner model is:
- serialize a **snapshot representation**
- deserialize into a fresh entity bean
- mark loaded properties appropriately
- freeze / ensure read-only state
- store the resulting materialized bean in L1
This gives more control over:
- loaded-property semantics
- read-only state
- subtype handling
- schema/version evolution
## Practical recommendation
Remote cache entries should represent exactly the configured fetch-group snapshot.
That means:
- cache what the fetch group loaded
- include nested associations loaded by that fetch group
- treat it as a self-contained immutable snapshot
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
---
## Redis vs Postgres cache table
## Redis
### Good for
- low latency
- batch lookup via MGET / pipelining
- TTL/eviction support
- natural shared-cache use case
### Tradeoffs
- extra infrastructure
- memory cost
- invalidation/event coordination still required
## Postgres cache table (including unlogged-style approach)
### Good for
- simpler ops if Postgres is already present
- easy batch lookup with `IN (...)`
- fewer moving parts than introducing Redis
### Tradeoffs
- slower than Redis for hot shared-cache usage
- adds pressure to Postgres
- TTL/cleanup becomes application responsibility
- still network/database I/O, so should remain off the `getIfPresent()` hot path
## Recommendation
- if the goal is a serious shared L2 cache, Redis is the more natural fit
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
---
## Versioning / evolution
Whatever serializer is used, include versioning information.
Useful dimensions:
- serializer/schema version
- cache implementation version
- fetch-group/cache identity version
This helps when:
- fields are added/removed
- graph shape changes
- fetch-group definitions evolve
---
## Compression
If remote snapshots become large:
- compress only above a size threshold
- avoid compressing tiny payloads
This is especially relevant for JSON in Redis or Postgres L2.
---
## Observability
A multi-level cache should expose at least:
- L1 hit rate
- L2 hit rate
- DB loader rate
- deserialize failures
- invalidation counts
- average payload size
- cold-start amplification
Without this, it will be hard to judge whether the remote cache is helping.
---
## Overall recommended architecture
### Recommended model
- **L1**: actual read-only `EntityBean` instances
- **L2**: serialized immutable snapshots
- **Loader**: fetch-group-based DB query
### Method responsibilities
- `getIfPresent(id)` => **L1 only**
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
---
## Bottom line
If/when multi-level immutable caching is explored, the main points to preserve are:
1. keep `getIfPresent()` local-only
2. do remote work only in batched `getAll()`
3. key by type + id + tenant + fetch-group/cache identity
4. treat remote values as immutable snapshots
5. prefer JSON/self-describing format first
6. be cautious with generic binary serializers like Kryo
---
## Possible follow-up
If this becomes active design work later, consider promoting these notes into one of:
- a dedicated design note under `docs/notes/`
- a GitHub issue / discussion for design iteration
- a lightweight ADR if this becomes a committed architectural direction
+56 -32
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>14.0.2</version>
<version>18.3.0</version>
</parent>
<name>ebean api</name>
@@ -26,20 +26,16 @@
<version>1.0</version>
</dependency>
<!--
Class retention Nonnull and Nullable annotations
to assist with IDE auto-completion with Ebean API
-->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-lang</artifactId>
<version>1.1</version>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-config</artifactId>
<version>3.12</version>
<version>4.2</version>
</dependency>
<dependency>
@@ -74,15 +70,13 @@
<optional>true</optional>
</dependency>
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- provided scope for JsonNode support -->
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -90,23 +84,6 @@
<optional>true</optional>
</dependency>
<!-- JAVAX-DEPENDENCY-START ___
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<optional>true</optional>
</dependency>
____ JAVAX-DEPENDENCY-END -->
<!-- JAKARTA-DEPENDENCY-START -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<optional>true</optional>
</dependency>
<!-- JAKARTA-DEPENDENCY-END -->
</dependencies>
<build>
@@ -126,6 +103,53 @@
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>11</release>
</configuration>
</execution>
<execution>
<id>compile-21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>21</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
<!-- <manifest>-->
<!-- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>-->
<!-- </manifest>-->
</plugin>
</plugins>
</build>
</project>
@@ -1,10 +1,9 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -20,7 +19,7 @@ import java.util.concurrent.TimeUnit;
* This also propagates MDC context from the current thread to the
* background task if defined.
*/
@NonNullApi
@NullMarked
public interface BackgroundExecutor {
/**
@@ -0,0 +1,25 @@
package io.ebean;
/**
* Unsupported access of a property on an entity bean.
* <p>
* Attempted a lazy load operation on a bean that has disabled lazy loading
* or attempt to mutate an unmodifiable bean.
*/
public class BeanAccessException extends UnsupportedOperationException {
private static final long serialVersionUID = 1;
/**
* Create with no message.
*/
public BeanAccessException() {
super();
}
/**
* Create with message.
*/
public BeanAccessException(String message) {
super(message);
}
}
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
import java.util.Optional;
@@ -30,7 +30,7 @@ import java.util.Optional;
*
* @see BeanRepository
*/
@NonNullApi
@NullMarked
public abstract class BeanFinder<I,T> {
protected final Database database;
@@ -1,6 +1,6 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;
import io.ebean.bean.EntityBean;
import java.util.Collection;
@@ -36,7 +36,7 @@ import java.util.Collection;
* @param <I> The ID type
* @param <T> The Bean type
*/
@NonNullApi
@NullMarked
public abstract class BeanRepository<I, T> extends BeanFinder<I, T> {
/**
@@ -89,12 +89,7 @@ public interface BeanState {
* <p>
* If a setter is called on a readOnly bean it will throw an exception.
*/
boolean isReadOnly();
/**
* Set the readOnly status for the bean.
*/
void setReadOnly(boolean readOnly);
boolean isUnmodifiable();
/**
* Advanced - Used to programmatically build a partially or fully loaded
+8 -4
View File
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import io.ebean.annotation.TxIsolation;
import io.ebean.cache.ServerCacheManager;
import io.ebean.plugin.Property;
@@ -57,7 +57,7 @@ import java.util.concurrent.Callable;
*
* }</pre>
*/
@NonNullApi
@NullMarked
public final class DB {
private static final DbContext context = DbContext.getInstance();
@@ -457,6 +457,10 @@ public final class DB {
/**
* Same as {@link #checkUniqueness(Object)} but with given transaction.
* <p>
* For control over query cache use and whether to skip the check when the bean's unique
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
* via {@link #getDefault()} instead.
*/
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
return getDefault().checkUniqueness(bean, transaction);
@@ -605,7 +609,7 @@ public final class DB {
* // find orders and their customers
* List<Order> list = DB.find(Order.class)
* .fetch("customer")
* .order("id")
* .orderBy("id")
* .findList();
*
* // sort by customer name ascending, then by order shipDate
@@ -0,0 +1,50 @@
package io.ebean;
final class DPaging implements Paging {
static final Paging NONE = new DPaging(0, 0, null);
static Paging build(int pgIndex, int pgSize, OrderBy<?> orderBy) {
return new DPaging(pgIndex, pgSize, orderBy);
}
static Paging build(int pgIndex, int pgSize) {
return new DPaging(pgIndex, pgSize, null);
}
private final int pageNumber;
private final int pageSize;
private final OrderBy<?> orderBy;
DPaging(int pageNumber, int pageSize, OrderBy<?> orderBy) {
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.orderBy = orderBy;
}
@Override
public int pageIndex() {
return pageNumber;
}
@Override
public int pageSize() {
return pageSize;
}
@Override
public OrderBy<?> orderBy() {
return orderBy;
}
@Override
public Paging withPage(int pageNumber) {
return new DPaging(pageNumber, pageSize, orderBy);
}
@Override
public Paging withOrderBy(String orderByClause) {
return new DPaging(pageNumber, pageSize, OrderBy.of(orderByClause));
}
}
+30 -28
View File
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import io.ebean.annotation.Platform;
import io.ebean.annotation.TxIsolation;
import io.ebean.cache.ServerCacheManager;
@@ -23,11 +23,18 @@ import java.util.concurrent.Callable;
/**
* Provides the API for fetching and saving beans to a particular database.
*
* <h5>Constructing a Database</h5>
* <p>
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
* They can also be automatically constructed on demand using configuration information in
* the application.properties file. The underlying implementation is provided by
* {@link DatabaseFactory}.
*
* <h5>Registration with the DB singleton</h5>
* <p>
* When a Database instance is created it can be registered with the DB
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
* singleton is essentially a map of Database's that have been registered
* When a Database instance is created it can be registered with the {@link DB}
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
* singleton is essentially a map of {@link Database}'s that have been registered
* with it.
* <p>
* The Database can then be retrieved later via {@link DB#byName(String)}.
@@ -35,16 +42,10 @@ import java.util.concurrent.Callable;
* <h5>The 'default' Database</h5>
* <p>
* One Database can be designated as the 'default' or 'primary' Database
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
* such as {@link DB#find(Class)} etc are actually just a convenient way to
* call methods on the 'default/primary' Database.
*
* <h5>Constructing a Database</h5>
* <p>
* Databases are constructed by the DatabaseFactory. They can be created
* programmatically via {@link DatabaseFactory#create(DatabaseBuilder)} or they
* can be automatically constructed on demand using configuration information in
* the application.properties file.
*
* <h5>Example: Get a Database</h5>
* <pre>{@code
@@ -80,10 +81,11 @@ import java.util.concurrent.Callable;
* method. Example: a single thread requires more than one transaction.
*
* @see DB
* @see DatabaseBuilder
* @see DatabaseFactory
* @see DatabaseConfig
*/
@NonNullApi
@NullMarked
public interface Database {
/**
@@ -94,11 +96,13 @@ public interface Database {
* // from application.properties / application.yaml
*
* Database db = Database.builder()
* .name("db")
* .loadFromProperties()
* .build();
*
* }</pre>
*/
@SuppressWarnings("removal")
static DatabaseBuilder builder() {
return new DatabaseConfig();
}
@@ -137,6 +141,7 @@ public interface Database {
/**
* Return the associated read only DataSource for this Database instance (can be null).
*/
@Nullable
DataSource readOnlyDataSource();
/**
@@ -400,7 +405,7 @@ public interface Database {
* // find orders and their customers
* List<Order> list = database.find(Order.class)
* .fetch("customer")
* .order("id")
* .orderBy("id")
* .findList();
*
* // sort by customer name ascending, then by order shipDate
@@ -772,18 +777,6 @@ public interface Database {
*/
<T> T reference(Class<T> beanType, Object id);
/**
* Return the extended API for Database.
* <p>
* The extended API has the options for executing queries that take an explicit
* transaction as an argument.
* <p>
* Typically, we only need to use the extended API when we do NOT want to use the
* usual ThreadLocal based mechanism to obtain the current transaction but instead
* supply the transaction explicitly.
*/
ExtendedServer extended();
/**
* Either Insert or Update the bean depending on its state.
* <p>
@@ -1085,12 +1078,21 @@ public interface Database {
* @param bean The entity bean to check uniqueness on
* @return a set of Properties if constraint validation was detected or empty list.
*/
Set<Property> checkUniqueness(Object bean);
default Set<Property> checkUniqueness(Object bean) {
return checkUniqueness(bean, null, false, true);
}
/**
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
*/
Set<Property> checkUniqueness(Object bean, Transaction transaction);
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
return checkUniqueness(bean, transaction, false, true);
}
/**
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
*/
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
/**
* Marks the entity bean as dirty.
@@ -1,6 +1,6 @@
package io.ebean;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.json.stream.JsonStream;
import io.ebean.annotation.*;
import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.*;
@@ -61,6 +61,11 @@ public interface DatabaseBuilder {
/**
* Build and return the Database instance.
* <p>
* When {@link #setRegister(boolean)} is set to true (the default), and a database
* with the same name is already registered, this throws an {@link IllegalStateException}.
* Use a unique name, or use {@link #setRegister(boolean)} with {@code false} if the
* Database instance is not intended to be registered/looked up by name.
*/
Database build();
@@ -360,18 +365,18 @@ public interface DatabaseBuilder {
DatabaseBuilder putServiceObject(Object configObject);
/**
* Set the Jackson JsonFactory to use.
* Set the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
return setJsonFactory(jsonFactory);
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
return setJsonStream(jsonStream);
}
/**
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
*/
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
DatabaseBuilder setJsonStream(JsonStream jsonStream);
/**
* Set the JSON format to use for DateTime types.
@@ -723,19 +728,6 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
/**
* Set the configuration for profiling.
*/
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
return setProfilingConfig(profilingConfig);
}
/**
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
*/
@Deprecated
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
/**
* Set the suffix appended to the base table to derive the view that contains the union
* of the base table and the history table in order to support asOf queries.
@@ -896,6 +888,13 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper);
/**
* Enable tenant-partitioned caches. When enabled each tenant gets its own cache namespace,
* improving cache-hit ratio by preventing cross-tenant key collisions.
* Use {@link SpiCacheManager#clearTenant(Object)} when a tenant is deactivated.
*/
DatabaseBuilder tenantPartitionedCache(boolean tenantPartitionedCache);
/**
* Set the L2 cache default max size.
*/
@@ -989,6 +988,14 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setNamingConvention(NamingConvention namingConvention);
/**
* Set the AggregateFormulaContext which is used to determine if a database function
* is an aggregate function (like sum, min, max, avg etc).
* <p>
* Use this to override the default known aggregation functions.
*/
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
/**
* Set to true if all DB column and table names should use quoted identifiers.
* <p>
@@ -1062,6 +1069,11 @@ public interface DatabaseBuilder {
*/
DatabaseBuilder readOnlyDatabase(boolean readOnlyDatabase);
/**
* Set to false such that the instance does not register a JVM shutdown hook.
*/
DatabaseBuilder shutdownHook(boolean shutdownHook);
/**
* Set a DataSource.
*/
@@ -1968,8 +1980,8 @@ public interface DatabaseBuilder {
DatabaseBuilder setLocalOnlyL2Cache(boolean localOnlyL2Cache);
/**
* Controls if Ebean should ignore <code>&x64;javax.validation.contstraints.NotNull</code> or
* <code>&x64;jakarta.validation.contstraints.NotNull</code>
* Controls if Ebean should ignore <code>&#64;javax.validation.contstraints.NotNull</code> or
* <code>&#64;jakarta.validation.contstraints.NotNull</code>
* with respect to generating a <code>NOT NULL</code> column.
* <p>
* Normally when Ebean sees javax NotNull annotation it means that column is defined as NOT NULL.
@@ -2019,6 +2031,11 @@ public interface DatabaseBuilder {
@Deprecated
DatabaseBuilder setQueryPlanTTLSeconds(int queryPlanTTLSeconds);
/**
* Set the EXPLAIN (with options) to use for query plan capture.
*/
DatabaseBuilder queryPlanExplain(String queryPlanExplain);
/**
* Create a new PlatformConfig based of the one held but with overridden properties by reading
* properties with the given path and prefix.
@@ -2216,7 +2233,7 @@ public interface DatabaseBuilder {
*
* @param includeLabelInSql When true include a SQL inline comment in generated SELECT queries.
*/
DatabaseConfig includeLabelInSql(boolean includeLabelInSql);
DatabaseBuilder includeLabelInSql(boolean includeLabelInSql);
/**
* Set the naming convention to apply to metrics names.
@@ -2234,7 +2251,7 @@ public interface DatabaseBuilder {
/**
* Sets the length check mode.
*/
DatabaseConfig lengthCheck(LengthCheck lengthCheck);
DatabaseBuilder lengthCheck(LengthCheck lengthCheck);
/**
* Provides read access (getters) for the DatabaseBuilder configuration
@@ -2249,11 +2266,11 @@ public interface DatabaseBuilder {
boolean isAutoLoadModuleInfo();
/**
* Return the Jackson JsonFactory to use.
* Return the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
JsonFactory getJsonFactory();
JsonStream getJsonStream();
/**
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
@@ -2472,11 +2489,6 @@ public interface DatabaseBuilder {
*/
TenantCatalogProvider getTenantCatalogProvider();
/**
* Return the configuration for profiling.
*/
ProfilingConfig getProfilingConfig();
/**
* Return the DB schema to use.
*/
@@ -2563,6 +2575,11 @@ public interface DatabaseBuilder {
*/
boolean isAutoPersistUpdates();
/**
* Return true if caches are partitioned by tenant.
*/
boolean isTenantPartitionedCache();
/**
* Return the L2 cache default max size.
*/
@@ -2600,6 +2617,11 @@ public interface DatabaseBuilder {
*/
NamingConvention getNamingConvention();
/**
* Return the AggregateFormulaContext.
*/
AggregateFormulaContext aggregateFormulaContext();
/**
* Return true if all DB column and table names should use quoted identifiers.
*/
@@ -2638,6 +2660,11 @@ public interface DatabaseBuilder {
*/
boolean readOnlyDatabase();
/**
* Return if a JVM shutdown hook should be registered.
*/
boolean shutdownHook();
/**
* Return the DataSource.
*/
@@ -3044,6 +3071,11 @@ public interface DatabaseBuilder {
*/
int getQueryPlanTTLSeconds();
/**
* Return the EXPLAIN (with options) to use for capturing query plans.
*/
String getQueryPlanExplain();
/**
* Return mapping locations to search for xml mapping via class path search.
*/
@@ -5,23 +5,21 @@ import io.ebean.service.SpiContainer;
import io.ebean.service.SpiContainerFactory;
import jakarta.persistence.PersistenceException;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.concurrent.locks.ReentrantLock;
/**
* Creates Database instances.
* Low-level factory for creating {@link Database} instances.
* <p>
* This uses either DatabaseConfig or properties in the application.properties file to
* configure and create a Database instance.
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
* This factory remains for legacy creation entry points plus container lifecycle methods.
* <p>
* The Database instance can either be registered with the DB singleton or
* not. The DB singleton effectively holds a map of Database by a name.
* If the Database is registered with the DB singleton you can retrieve it
* The Database instance can either be registered with the {@link DB} singleton or
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
* If the Database is registered with the {@link DB} singleton you can retrieve it
* later via {@link DB#byName(String)}.
* <p>
* One Database can be nominated as the 'default/primary' Database. Many
* methods on the DB singleton such as {@link DB#find(Class)} are just a
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
* convenient way of using the 'default/primary' Database.
*/
public final class DatabaseFactory {
@@ -38,7 +36,8 @@ public final class DatabaseFactory {
* Initialise the container with clustering configuration.
* <p>
* Call this prior to creating any Database instances or alternatively set the
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
*/
public static void initialiseContainer(ContainerConfig containerConfig) {
lock.lock();
@@ -50,8 +49,11 @@ public final class DatabaseFactory {
}
/**
* Create using properties to configure the database.
* Create using configuration loaded from properties for the given database name.
*
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
*/
@Deprecated
public static Database create(String name) {
lock.lock();
try {
@@ -62,34 +64,35 @@ public final class DatabaseFactory {
}
/**
* Create using the DatabaseConfig object to configure the database.
*
* <pre>{@code
*
* DatabaseConfig config = new DatabaseConfig();
* config.setName("db");
* config.loadProperties();
*
* Database database = DatabaseFactory.create(config);
*
* }</pre>
* @deprecated migrate to {@link DatabaseBuilder#build()}.
*/
@Deprecated(forRemoval = true)
public static Database create(DatabaseBuilder builder) {
lock.lock();
try {
var config = builder.settings();
if (config.getName() == null) {
var name = config.getName();
if (name == null) {
throw new PersistenceException("The name is null (it is required)");
}
if (config.isRegister()) {
// We're explicitly creating a database to be registered, so avoid
// triggering DbContext static initialisation to auto-create a default one.
DbPrimary.setSkip(true);
if (DbContext.getInstance().contains(name)) {
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
+ " if this Database instance is not intended to be registered/looked up by name.");
}
}
Database server = createInternal(config);
if (config.isRegister()) {
if (config.isDefaultServer()) {
if (defaultServerName != null && !defaultServerName.equals(config.getName())) {
throw new IllegalStateException("Registering [" + config.getName() + "] as the default server but [" + defaultServerName + "] is already registered as the default");
if (defaultServerName != null && !defaultServerName.equals(name)) {
throw new IllegalStateException("Registering [" + name + "] as the default server but [" + defaultServerName + "] is already registered as the default");
}
defaultServerName = config.getName();
defaultServerName = name;
}
DbPrimary.setSkip(true);
DbContext.getInstance().register(server, config.isDefaultServer());
}
return server;
@@ -99,7 +102,8 @@ public final class DatabaseFactory {
}
/**
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
* context class loader.
*/
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
lock.lock();
@@ -117,6 +121,24 @@ public final class DatabaseFactory {
}
}
/**
* Remove the registration of this Database.
* <p>
* This is invoked when a Database is shutdown so that its registered name
* becomes available again for a subsequently created Database with the same name.
*/
public static void unregister(Database server) {
lock.lock();
try {
DbContext.getInstance().deregister(server);
if (server.name().equals(defaultServerName)) {
defaultServerName = null;
}
} finally {
lock.unlock();
}
}
/**
* Shutdown gracefully all Database instances cleaning up any resources as required.
* <p>
@@ -157,10 +179,10 @@ public final class DatabaseFactory {
* Create the container instance using the configuration.
*/
private static SpiContainer createContainer(ContainerConfig containerConfig) {
Iterator<SpiContainerFactory> factories = ServiceLoader.load(SpiContainerFactory.class).iterator();
if (factories.hasNext()) {
return factories.next().create(containerConfig);
SpiContainerFactory factory = XBootstrapService.containerFactory();
if (factory == null) {
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
}
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
return factory.create(containerConfig);
}
}
@@ -4,6 +4,7 @@ import io.ebean.config.BeanNotEnhancedException;
import io.ebean.datasource.DataSourceConfigurationException;
import jakarta.persistence.PersistenceException;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@@ -75,6 +76,10 @@ final class DbContext {
return defaultDatabase;
}
boolean contains(String name) {
return concMap.containsKey(name);
}
/**
* Return the database by name.
*/
@@ -92,6 +97,7 @@ final class DbContext {
/**
* Read, create and put of Databases.
*/
@SuppressWarnings("deprecation")
private Database getWithCreate(String name) {
lock.lock();
try {
@@ -114,6 +120,27 @@ final class DbContext {
registerWithName(server.name(), server, isDefault);
}
/**
* Remove the registration for this Database (typically on shutdown) so that
* its name becomes available again for a subsequently created Database.
* <p>
* Only removes the registration if it currently maps to this exact instance
* (avoids removing a different Database subsequently registered with the same name).
*/
void deregister(Database server) {
lock.lock();
try {
String name = server.name();
concMap.remove(name, server);
syncMap.remove(name, server);
if (defaultDatabase == server) {
defaultDatabase = null;
}
} finally {
lock.unlock();
}
}
private void registerWithName(String name, Database server, boolean isDefault) {
lock.lock();
try {
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import io.ebean.docstore.DocQueryContext;
import io.ebean.docstore.RawDoc;
@@ -14,7 +14,7 @@ import java.util.function.Predicate;
/**
* Document storage operations.
*/
@NonNullApi
@NullMarked
public interface DocumentStore {
/**
@@ -0,0 +1,65 @@
package io.ebean;
import jakarta.persistence.PersistenceException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Static bridge registering custom {@code @DtoConvert} converter instances so generated DTO
* mappers can reach them.
* <p>
* Generated mappers (see {@code query.mapTo(SomeDto.class)}) are wired via {@code ServiceLoader}
* as plain, no-arg-constructed, compile-time singletons (mirroring how entity/query-bean
* registration already works) - they have no way to reach a dependency-injection container, or
* any particular {@code Database} instance, at construction time. When a
* {@code @DtoConvert(value = ConverterType.class, method = "...")} property's converter is an
* <b>instance</b> method (as opposed to a {@code static} one, which is called directly with no
* registration needed at all), the generated mapper resolves it via {@link #get(Class)} - so the
* application must register an instance here, typically one already built by its own DI
* container, <b>before</b> building the {@code Database}:
* <pre>{@code
* AES256Cipher cipher = ...; // already DI-constructed
* DtoConverterManager.put(DriverConversions.class, new DriverConversionsImpl(cipher));
*
* Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
* }</pre>
* <p>
* This is a deliberate, narrowly-scoped exception to preferring dependency injection over static
* mutable state - it exists solely to bridge an already-DI-constructed singleton into
* {@code ServiceLoader}-discovered, no-arg-constructed generated code, which cannot otherwise
* reach a DI container or a specific {@code Database} instance. {@link #get(Class)} throws
* immediately if nothing was registered for the given type, so a missing/late registration fails
* fast at {@code Database} build time (a generated mapper's eager field initializer) rather than
* lazily on first use.
*/
public final class DtoConverterManager {
private static final Map<Class<?>, Object> converters = new ConcurrentHashMap<>();
private DtoConverterManager() {
}
/**
* Register a converter instance for the given type - must be called before the
* {@code Database} using it is built.
*/
public static <T> void put(Class<T> type, T instance) {
converters.put(type, instance);
}
/**
* Return the registered converter instance for the given type.
*
* @throws PersistenceException if no instance was registered for {@code type}.
*/
@SuppressWarnings("unchecked")
public static <T> T get(Class<T> type) {
T instance = (T) converters.get(type);
if (instance == null) {
throw new PersistenceException("No " + type.getName() + " registered - call "
+ "DtoConverterManager.put(" + type.getSimpleName() + ".class, ...) before starting the Database");
}
return instance;
}
}
@@ -0,0 +1,51 @@
package io.ebean;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.function.Function;
/**
* Identity-keyed cache of already-mapped source -&gt; target instances, shared across one
* top-level {@link DtoMapper#mapList(java.util.List)} call (or an explicitly shared context).
* <p>
* Keyed by source object <b>identity</b> (an {@link IdentityHashMap}, not {@code equals()}/
* {@code hashCode()}) because the source is an Ebean entity graph, where repeated references to
* the same row within one query already resolve to the same Java object instance.
* <p>
* The identity map is partitioned <b>per target DTO type</b>. This matters because the same
* source instance can legitimately need to be mapped to more than one target type within a
* single graph - e.g. a top-level {@code CustomerDtoMapper} maps a {@code Customer} to a full
* {@code CustomerDto}, while a nested {@code ContactDtoMapper} maps the very same {@code Customer}
* instance (accessed via {@code contact.getCustomer()}) to a shallow {@code CustomerRefDto} to
* avoid a cycle. A single un-partitioned {@code IdentityHashMap<Object,Object>} would have the
* two mappers collide on the same source key and incorrectly hand back the other mapper's
* (wrong-typed) cached result. Partitioning by target type keeps each mapper's cache isolated
* while still sharing one context/instance per top-level mapping call.
* <p>
* Not thread-safe - a context is expected to be created per top-level mapping call and not
* shared across threads.
*/
public final class DtoMapContext {
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
/**
* Return the already-mapped target for the given source instance if present, otherwise map it
* via {@code mappingFunction}, register it, and return it.
*
* @param targetType the DTO type being produced - used to partition the identity cache so that
* mapping the same source to different target types never collides.
*/
@SuppressWarnings("unchecked")
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
T existing = (T) mapped.get(source);
if (existing != null) {
return existing;
}
T created = mappingFunction.apply(source);
mapped.put(source, created);
return created;
}
}
@@ -0,0 +1,76 @@
package io.ebean;
import java.util.ArrayList;
import java.util.List;
/**
* Mapper interface implemented by generated (or hand-written) entity -&gt; DTO graph mappers.
* <p>
* Used with nested entity-to-DTO graph mapping (see {@code query.mapTo(SomeDto.class)}) as
* distinct from the existing flat, single-row {@link DtoQuery} pipeline. Each entity/DTO type
* pair gets its own small, composable mapper implementation (mirroring MapStruct's per-type
* mapper generation) rather than one large mapper inlining every nested type. Nested mappers are
* wired together via constructor injection, not static singletons - this keeps mappers stateless,
* substitutable (e.g. for tests) and avoids global mutable state.
* <p>
* A {@link DtoMapContext} is threaded through every nested {@code map(...)} call within one
* top-level {@link #mapList(List)} invocation, so that repeated references to the same source
* entity instance (e.g. several {@code Contact}s sharing the same {@code Customer}) map to the
* <b>same</b> target DTO instance rather than creating duplicate-but-equal copies. This mirrors
* the identity semantics Ebean's own entity graph already has, and is what makes the resulting
* DTO graph "graph shaped" rather than "tree of copies shaped".
* <p>
* Implementations contain no reflection or {@code MethodHandles} - only direct getter calls and
* constructor invocation - so generated mappers are safe under GraalVM native-image with zero
* additional reachability metadata.
*
* @param <SOURCE> the source entity (or embeddable) type
* @param <TARGET> the target DTO type
*/
public interface DtoMapper<SOURCE, TARGET> {
/**
* Return the {@link FetchGroup} of exactly the source properties (and nested paths) needed to
* populate the target DTO graph - the select()/fetch() spec is derived from the DTO's declared
* shape rather than maintained separately by hand. Used by {@code query.mapTo(TARGET.class)}
* to automatically apply the correct fetch spec before the query is executed.
*/
FetchGroup<SOURCE> fetchGroup();
/**
* Map a single source instance to its target DTO, reusing/registering the mapping in the
* given context so that repeated references to the same source instance de-duplicate to the
* same target instance. Must return {@code null} when given {@code null}.
*/
TARGET map(SOURCE source, DtoMapContext context);
/**
* Map a single source instance using a fresh, one-off context. Convenience for mapping a
* single object in isolation (no de-duplication opportunity since there's nothing else in
* scope to de-duplicate against).
*/
default TARGET map(SOURCE source) {
return map(source, new DtoMapContext());
}
/**
* Map a list of source instances to a list of target DTOs sharing the given context,
* preserving order.
*/
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) {
List<TARGET> result = new ArrayList<>(source.size());
for (SOURCE s : source) {
result.add(map(s, context));
}
return result;
}
/**
* Map a list of source instances to a list of target DTOs using a fresh context shared across
* the whole list - this is the usual top-level entry point, e.g. mapping the result of a
* {@code query.findList()} call.
*/
default List<TARGET> mapList(List<SOURCE> source) {
return mapList(source, new DtoMapContext());
}
}
@@ -0,0 +1,125 @@
package io.ebean;
import io.ebean.config.DtoMapperRegister;
import jakarta.persistence.PersistenceException;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
* Loads all generated {@link DtoMapperRegister} implementations (via {@code ServiceLoader},
* mirroring how {@code EntityClassRegister} is discovered) once, and resolves the {@link
* DtoMapper} for a given (source, dto) pair, or by the generated mapper's own concrete type, on
* request.
* <p>
* Has no dependency on {@link Database} - it can be constructed independently, before (or
* without) a {@code Database} existing at all, e.g. as a DI-managed singleton constructed
* alongside the rest of an application's dependency graph. If you want the exact same instance
* (and hence the exact same underlying mapper instances) shared between {@code query.mapTo(...)}
* and your own application code, construct it yourself and register it via {@code
* DatabaseBuilder.putServiceObject(DtoMapperManager.class, myManager)} before building the {@code
* Database} - it is then used instead of a Database-internal default instance.
* <p>
* Resolved mappers are cached so that repeated lookups only ever pay the cost of iterating the
* generated registers and constructing the mapper (and its nested mapper/{@code FetchGroup}
* graph) once - after that, every lookup is a single hash-map hit regardless of how many entity/
* DTO pairs are registered.
*/
public final class DtoMapperManager {
private final List<DtoMapperRegister> registers;
private final ConcurrentHashMap<MapperKey, DtoMapper<?, ?>> pairCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Class<?>, Object> typeCache = new ConcurrentHashMap<>();
public DtoMapperManager() {
this.registers = load();
}
private static List<DtoMapperRegister> load() {
List<DtoMapperRegister> result = new ArrayList<>();
for (DtoMapperRegister register : ServiceLoader.load(DtoMapperRegister.class)) {
result.add(register);
}
return result;
}
/**
* Return the {@link DtoMapper} for the given (source, dto) pair.
*
* @throws PersistenceException if no generated mapper is registered for that pair.
*/
@SuppressWarnings("unchecked")
public <S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> dtoType) {
return (DtoMapper<S, D>) pairCache.computeIfAbsent(new MapperKey(sourceType, dtoType), this::resolve);
}
/**
* Return the generated mapper instance of the given concrete mapper type - e.g. {@code
* manager.get(CustomerDtoMapper.class)} - typically used to resolve a mapper instance for
* dependency injection into application code (e.g. an avaje-inject {@code @Factory} bean
* method).
*
* @throws PersistenceException if no generated mapper of that type is registered.
*/
@SuppressWarnings("unchecked")
public <T> T get(Class<T> mapperType) {
return (T) typeCache.computeIfAbsent(mapperType, this::resolveByType);
}
private DtoMapper<?, ?> resolve(MapperKey key) {
for (DtoMapperRegister register : registers) {
DtoMapper<?, ?> mapper = register.mapperFor(key.sourceType, key.dtoType);
if (mapper != null) {
return mapper;
}
}
throw new PersistenceException("No DtoMapper registered mapping " + key.sourceType + " -> " + key.dtoType
+ " - check @DtoMapping(source = " + key.sourceType.getSimpleName() + ".class, target = "
+ key.dtoType.getSimpleName() + ".class) is declared on a package-info.java processed by querybean-generator");
}
private Object resolveByType(Class<?> mapperType) {
for (DtoMapperRegister register : registers) {
Object mapper = register.mapperOfType(mapperType);
if (mapper != null) {
return mapper;
}
}
throw new PersistenceException("No DtoMapper of type " + mapperType.getName() + " registered"
+ " - check a @DtoMapping(...) pair generating " + mapperType.getSimpleName()
+ " is declared on a package-info.java processed by querybean-generator");
}
/**
* Cache key pairing the source entity type and target DTO type.
*/
private static final class MapperKey {
private final Class<?> sourceType;
private final Class<?> dtoType;
MapperKey(Class<?> sourceType, Class<?> dtoType) {
this.sourceType = sourceType;
this.dtoType = dtoType;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MapperKey)) {
return false;
}
MapperKey other = (MapperKey) o;
return sourceType == other.sourceType && dtoType == other.dtoType;
}
@Override
public int hashCode() {
return 31 * sourceType.hashCode() + dtoType.hashCode();
}
}
}
@@ -0,0 +1,137 @@
package io.ebean;
/**
* Runtime helpers used by generated {@link DtoMapper} implementations to safely resolve a
* primitive-typed DTO field whose value is derived from a multi-hop {@code @DtoPath} that
* traverses a nullable intermediate relation.
* <p>
* A {@code null}-guarded getter-chain (e.g. {@code source.getOrganisation() == null ? null :
* source.getOrganisation().getId()}) always types as the boxed wrapper (since one branch is the
* {@code null} literal). When the DTO's target field is a primitive (e.g. {@code long
* organisationId}), passing that boxed expression to the constructor auto-unboxes it - which
* throws a raw, unhelpful {@link NullPointerException} if the relation really is {@code null}.
* <p>
* These methods give the generated mapper a choice, controlled by {@code @DtoPath#failOnNull()}:
* default to the primitive's zero-equivalent value ({@code orZero} methods, the default), or
* throw a clear, descriptive exception naming the offending property path ({@code require}
* methods, opted into via {@code failOnNull = true}).
*
* @see io.ebean.annotation.DtoPath
*/
public final class DtoMapperSupport {
private DtoMapperSupport() {
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static long orZero(Long value) {
return value == null ? 0L : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static int orZero(Integer value) {
return value == null ? 0 : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static short orZero(Short value) {
return value == null ? 0 : value;
}
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static byte orZero(Byte value) {
return value == null ? 0 : value;
}
/** Return {@code 0.0} if {@code value} is {@code null}, otherwise its unboxed value. */
public static double orZero(Double value) {
return value == null ? 0.0 : value;
}
/** Return {@code 0.0f} if {@code value} is {@code null}, otherwise its unboxed value. */
public static float orZero(Float value) {
return value == null ? 0.0f : value;
}
/** Return {@code false} if {@code value} is {@code null}, otherwise its unboxed value. */
public static boolean orZero(Boolean value) {
return value != null && value;
}
/** Return {@code '\u0000'} if {@code value} is {@code null}, otherwise its unboxed value. */
public static char orZero(Character value) {
return value == null ? '\u0000' : value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static long require(Long value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static int require(Integer value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static short require(Short value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static byte require(Byte value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static double require(Double value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static float require(Float value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static boolean require(Boolean value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
public static char require(Character value, String path) {
if (value == null) {
throw failure(path);
}
return value;
}
private static IllegalStateException failure(String path) {
return new IllegalStateException(
"@DtoPath(\"" + path + "\") resolved to null via a nullable intermediate relation, but the"
+ " target DTO field is primitive and failOnNull=true - either handle the null case in"
+ " source data, use a boxed wrapper type for the DTO field, or remove failOnNull to"
+ " default to the primitive's zero-equivalent value instead.");
}
}
+81 -4
View File
@@ -1,8 +1,9 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
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.Collection;
@@ -10,6 +11,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
@@ -42,7 +44,7 @@ import java.util.stream.Stream;
*
* }</pre>
*/
@NonNullApi
@NullMarked
public interface DtoQuery<T> extends CancelableQuery {
/**
@@ -109,6 +111,22 @@ public interface DtoQuery<T> extends CancelableQuery {
*/
Optional<T> findOneOrEmpty();
/**
* Execute the query returning a single bean 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 bean 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);
}
/**
* Bind all the parameters using index positions.
* <p>
@@ -140,6 +158,17 @@ public interface DtoQuery<T> extends CancelableQuery {
* Bind the named multi-value array parameter which we would use with Postgres ANY.
* <p>
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
* <pre>{@code
*
* String sql = "select id, name from o_customer where id = any(:idList)";
*
* var ids = List.of(1, 2, 3);
*
* List<CustomerDto> list2 = DB.findDto(CustomerDto.class, sql)
* .setArrayParameter("idList", ids)
* .findList();
*
* }</pre>
*/
DtoQuery<T> setArrayParameter(String name, Collection<?> values);
@@ -223,5 +252,53 @@ public interface DtoQuery<T> extends CancelableQuery {
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
DtoQuery<T> usingMaster();
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>
* 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>
* This is only supported for a DtoQuery that is derived from an ORM query via
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
* there is no query structure available from which to derive a matching row count query -
* a PersistenceException is thrown in that case.
* <pre>{@code
*
* PagedList<OrderDto> pagedList =
* DB.find(Order.class)
* .where().eq("status", Order.Status.NEW)
* .orderBy().asc("id")
* .setFirstRow(50)
* .setMaxRows(20)
* .asDto(OrderDto.class)
* .findPagedList();
*
* // fetch the total row count in the background
* pagedList.loadCount();
*
* List<OrderDto> orders = pagedList.getList();
* int totalRowCount = pagedList.getTotalCount();
*
* }</pre>
*
* @return The PagedList
*/
PagedList<T> findPagedList();
}
@@ -5,6 +5,8 @@ import java.util.List;
import java.util.concurrent.Future;
/**
* @deprecated migrate to using {@link PagedList#emptyList()} only.
* <p>
* An empty PagedList.
* <p>
* For use in application code when we need to return a PagedList but don't want to
@@ -17,7 +19,8 @@ import java.util.concurrent.Future;
*
* }</pre>
*/
public class EmptyPagedList<T> implements PagedList<T> {
@Deprecated(forRemoval = true)
public final class EmptyPagedList<T> implements PagedList<T> {
@Override
public void loadCount() {
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import io.ebean.search.*;
import jakarta.persistence.NonUniqueResultException;
@@ -10,6 +10,7 @@ import java.sql.Timestamp;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* List of Expressions that make up a where or having clause.
@@ -31,7 +32,7 @@ import java.util.function.Predicate;
*
* @see Query#where()
*/
@NonNullApi
@NullMarked
public interface ExpressionList<T> {
/**
@@ -53,14 +54,6 @@ public interface ExpressionList<T> {
*/
Query<T> orderById(boolean orderById);
/**
* @deprecated migrate to {@link #orderBy(String)}
*/
@Deprecated(since = "13.19", forRemoval = true)
default ExpressionList<T> order(String orderByClause) {
return orderBy(orderByClause);
}
/**
* Set the order by clause replacing the existing order by clause if there is
* one.
@@ -71,14 +64,6 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> orderBy(String orderBy);
/**
* @deprecated migrate to {@link #orderBy()}.
*/
@Deprecated(forRemoval = true)
default OrderBy<T> order() {
return orderBy();
}
/**
* Return the OrderBy so that you can append an ascending or descending
* property to the order by clause.
@@ -119,6 +104,29 @@ public interface ExpressionList<T> {
*/
<D> DtoQuery<D> asDto(Class<D> dtoClass);
/**
* Map the query result to a nested DTO graph, automatically deriving the select()/fetch() spec
* from the target DTO's declared shape and forcing {@code setUnmodifiable(true)}.
* <p>
* Distinct from {@link #asDto(Class)} (the flat, single-row SQL pipeline) - this supports
* nested ToOne/ToMany DTO graphs, mapped from the normal ORM entity query result.
*
* @throws jakarta.persistence.PersistenceException if no generated {@link DtoMapper} is
* registered for this (entity, dto) pair.
*/
<D> MappedQuery<D> mapTo(Class<D> dtoType);
/**
* Map the query result to a nested DTO graph using an already-resolved {@link DtoMapper}
* instance, rather than looking one up by (entity, dtoType) - e.g. to select a named variant
* mapper (see {@code @DtoMapping(name = "...", exclude = "...")}), such as
* {@code query.mapTo(User.class, userMapper.noFleets())}.
*
* @param dtoType the DTO type mapped to (must match {@code mapper}'s target type)
* @param mapper the mapper instance to use, e.g. a named variant accessor on a generated mapper
*/
<D> MappedQuery<D> mapTo(Class<D> dtoType, DtoMapper<T, D> mapper);
/**
* Return the underlying query as an UpdateQuery.
* <p>
@@ -222,16 +230,20 @@ public interface ExpressionList<T> {
int delete();
/**
* Execute as a delete query deleting the 'root level' beans that match the predicates
* in the query.
* Execute as a delete query permanently deleting the 'root level' beans that match the
* predicates in the query without soft delete.
* <p>
* This is the same as {@link #delete()} except that when the bean type uses soft delete
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
* being marked as deleted.
* <p>
* Note that if the query includes joins then the generated delete statement may not be
* optimal depending on the database platform.
* </p>
*
* @return the number of rows that were deleted.
* @return the number of rows that were permanently deleted.
*/
int delete(Transaction transaction);
int deletePermanent();
/**
* Execute as a update query.
@@ -241,14 +253,6 @@ public interface ExpressionList<T> {
*/
int update();
/**
* Execute as a update query with the given transaction.
*
* @return the number of rows that were updated.
* @see UpdateQuery
*/
int update(Transaction transaction);
/**
* Execute the query returning true if a row is found.
* <p>
@@ -349,7 +353,7 @@ public interface ExpressionList<T> {
* List<String> names =
* DB.find(Customer.class)
* .select("name")
* .order().asc("name")
* .orderBy().asc("name")
* .findSingleAttributeList();
*
* }</pre>
@@ -362,7 +366,7 @@ public interface ExpressionList<T> {
* .setDistinct(true)
* .select("name")
* .where().eq("status", Customer.Status.NEW)
* .order().asc("name")
* .orderBy().asc("name")
* .setMaxRows(100)
* .findSingleAttributeList();
*
@@ -418,6 +422,26 @@ public interface ExpressionList<T> {
*/
Optional<T> findOneOrEmpty();
/**
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
* if there is no matching bean.
*
* @see Query#findOneOrThrow()
*/
default T findOneOrThrow() {
return query().findOneOrThrow();
}
/**
* Execute the query returning a single bean or throwing the exception produced by the
* given supplier if there is no matching bean.
*
* @see Query#findOneOrThrow(Supplier)
*/
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
return query().findOneOrThrow(exceptionSupplier);
}
/**
* Execute find row count query in a background thread.
* <p>
@@ -1109,6 +1133,14 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> like(String propertyName, String value);
/**
* Is LIKE if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>likeIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value);
/**
* Case insensitive Like - property like value where the value contains the
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
@@ -1116,17 +1148,41 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> ilike(String propertyName, String value);
/**
* Is case insensitive LIKE if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>ilikeIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value);
/**
* Starts With - property like value%.
*/
ExpressionList<T> startsWith(String propertyName, String value);
/**
* Is STARTS WITH if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>startsWithIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value);
/**
* Case insensitive Starts With - property like value%. Typically uses a
* lower() function to make the expression case insensitive.
*/
ExpressionList<T> istartsWith(String propertyName, String value);
/**
* Is case insensitive STARTS WITH if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>istartsWithIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value);
/**
* Ends With - property like %value.
*/
@@ -1143,12 +1199,28 @@ public interface ExpressionList<T> {
*/
ExpressionList<T> contains(String propertyName, String value);
/**
* Is CONTAINS if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>containsIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value);
/**
* Case insensitive Contains - property like %value%. Typically uses a lower()
* function to make the expression case insensitive.
*/
ExpressionList<T> icontains(String propertyName, String value);
/**
* Is case insensitive CONTAINS if value is non-null and otherwise no expression is added to the query.
* <p>
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
* effectively optional. We can use <code>icontainsIfPresent()</code> rather than having a separate if block.
*/
ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value);
/**
* In expression using pairs of value objects.
*/
@@ -1707,7 +1779,7 @@ public interface ExpressionList<T> {
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endAnd()
* .order().asc("name")
* .orderBy().asc("name")
* .findList();
* }</pre>
*/
@@ -1728,7 +1800,7 @@ public interface ExpressionList<T> {
* .or()
* .eq("status", Customer.Status.ACTIVE)
* .isNull("anniversary")
* .order().asc("name")
* .orderBy().asc("name")
* .findList();
*
* }</pre>
@@ -1748,7 +1820,7 @@ public interface ExpressionList<T> {
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endAnd()
* .order().asc("name")
* .orderBy().asc("name")
* .findList();
*
* }</pre>
@@ -1782,7 +1854,7 @@ public interface ExpressionList<T> {
* .gt("id", 1)
* .eq("anniversary", onAfter)
* .endNot()
* .order()
* .orderBy()
* .asc("name")
* .findList();
*
@@ -1,21 +0,0 @@
package io.ebean;
import java.time.Clock;
/**
* The extended API for Database.
*/
public interface ExtendedServer {
/**
* Deprecated but no yet determined suitable replacement (to support testing only change of clock).
* <p>
* Set the Clock to use for <code>@WhenCreated</code> and <code>@WhenModified</code>.
* <p>
* Note that we only expect to change the Clock for testing purposes.
* </p>
*/
@Deprecated
void setClock(Clock clock);
}
@@ -1,6 +1,6 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;
import io.ebean.service.SpiFetchGroupQuery;
/**
@@ -61,7 +61,7 @@ import io.ebean.service.SpiFetchGroupQuery;
*
* @param <T> The bean type the Fetch group can be applied to
*/
@NonNullApi
@NullMarked
public interface FetchGroup<T> {
/**
@@ -84,7 +84,7 @@ public interface FetchGroup<T> {
* @return The FetchGroup with the given select clause
*/
static <T> FetchGroup<T> of(Class<T> cls, String select) {
return XServiceProvider.fetchGroupOf(cls, select);
return XBootstrapService.fetchGroupOf(cls, select);
}
/**
@@ -108,14 +108,14 @@ public interface FetchGroup<T> {
* @return The FetchGroupBuilder with the given select clause which we will add fetch clauses to
*/
static <T> FetchGroupBuilder<T> of(Class<T> cls) {
return XServiceProvider.fetchGroupOf(cls);
return XBootstrapService.fetchGroupOf(cls);
}
/**
* Return a query to be used by query beans for constructing FetchGroup.
*/
static <T> SpiFetchGroupQuery<T> queryFor(Class<T> beanType) {
return XServiceProvider.fetchGroupQueryFor(beanType);
return XBootstrapService.fetchGroupQueryFor(beanType);
}
}
@@ -1,6 +1,6 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;
/**
* Builds a FetchGroup by adding fetch clauses.
@@ -23,7 +23,7 @@ import io.avaje.lang.NonNullApi;
*
* }</pre>
*/
@NonNullApi
@NullMarked
public interface FetchGroupBuilder<T> {
/**
+2 -2
View File
@@ -1,6 +1,6 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import org.jspecify.annotations.NullMarked;
import java.util.List;
import java.util.Set;
@@ -79,7 +79,7 @@ import java.util.Set;
*
* @param <T> the entity bean type
*/
@NonNullApi
@NullMarked
public interface Filter<T> {
/**
+4 -4
View File
@@ -1,7 +1,7 @@
package io.ebean;
import io.avaje.lang.NonNullApi;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
@@ -35,7 +35,7 @@ import java.util.List;
* public List<Customer> findNew() {
* return query().where()
* .eq("status", Customer.Status.NEW)
* .order("name")
* .orderBy("name")
* .findList()
* }
* }
@@ -60,7 +60,7 @@ import java.util.List;
* @see BeanRepository
* @see BeanFinder
*/
@NonNullApi
@NullMarked
public class Finder<I, T> {
/**
@@ -0,0 +1,74 @@
package io.ebean;
import jakarta.persistence.PersistenceException;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* FutureMap represents the result of a background query execution that will
* return a map of entities.
* <p>
* It extends the java.util.concurrent.Future with the ability to cancel the
* query, check if it is finished and get the resulting list waiting for the
* query to finish (ie. the standard features of java.util.concurrent.Future).
* </p>
* <p>
* A simple example:
* </p>
* <pre>{@code
*
* // create a query to find all orders
* Query<Long,Order> query = DB.find(Order.class)
* .setMapKey("id");
*
* // execute the query in a background thread
* // immediately returning the futureMap
* FutureMap<Long,Order> futureMap = query.findFutureMap();
*
* // do something else ...
*
* if (!futureMap.isDone()){
* // we can cancel the query execution. This will cancel
* // the underlying query if that is supported by the JDBC
* // driver and database
* futureMap.cancel(true);
* }
*
* if (!futureMap.isCancelled()){
* // wait for the query to finish and return the map
* Map<Long,Order> map = futureMap.get();
* ...
* }
*
* }</pre>
*/
public interface FutureMap<K, T> extends Future<Map<K, T>> {
/**
* Return the query that is being executed by a background thread.
*/
Query<T> getQuery();
/**
* Same as {@link #get()} but wraps InterruptedException and ExecutionException in the
* unchecked PersistenceException.
*
* @return The query list result
* @throws PersistenceException when a InterruptedException or ExecutionException occurs.
*/
Map<K, T> getUnchecked();
/**
* Same as {@link #get(long, TimeUnit)} but wraps InterruptedException
* and ExecutionException in the unchecked PersistenceException.
*
* @return The query list result
* @throws TimeoutException if the wait timed out
* @throws PersistenceException if a InterruptedException or ExecutionException occurs.
*/
Map<K, T> getUnchecked(long timeout, TimeUnit unit) throws TimeoutException;
}
@@ -0,0 +1,58 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.Map;
import java.util.Set;
/**
* Query-scoped immutable bean cache.
*
* <p>Typical use is to attach an immutable cache to a query and let Ebean use it when
* resolving assoc-one references.
*
* <pre>{@code
* FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
* .select("name,version")
* .fetch("billingAddress", "line1,city")
* .fetch("shippingAddress", "line1,city")
* .build();
*
* ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
* .loading(database, customerGroup)
* .build();
*
* Order order = database.find(Order.class)
* .setId(id)
* .setUnmodifiable(true)
* .using(customerCache)
* .findOne();
* }</pre>
*
* @param <T> The bean type.
*
* @see ImmutableBeanCaches#builder(Class)
*/
@NullMarked
public interface ImmutableBeanCache<T> {
/**
* Return the bean type this cache provides values for.
*/
Class<T> type();
/**
* Return immutable cached beans by id (loading and populating misses as needed).
*/
Map<Object, T> getAll(Set<Object> ids);
/**
* Return a cached bean for the given id if it is already present.
* <p>
* This does not trigger loading or record a miss.
*/
default @Nullable T getIfPresent(Object id) {
return null;
}
}
@@ -0,0 +1,246 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import io.ebean.service.SpiImmutableCacheFactory;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
/**
* Utility factory methods for {@link ImmutableBeanCache}.
*
* <p>Use {@link #builder(Class)} when you want explicit cache policy controls (for example
* max size or TTL). Use {@link #loading(Class, Database, FetchGroup)} as a shorthand for
* query-loader-backed memoization.
*
* <pre>{@code
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
* .select("version")
* .build();
*
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
* .loading(database, fetchGroup)
* .maxSize(10_000)
* .maxIdleSeconds(300)
* .maxSecondsToLive(1_800)
* .build();
* }</pre>
*/
@NullMarked
public final class ImmutableBeanCaches {
private ImmutableBeanCaches() {
}
/**
* Return a builder for immutable bean caches.
*
* <pre>{@code
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
* .loading(database, FetchGroup.of(MyRef.class, "version"))
* .build();
* }</pre>
*/
public static <T> ImmutableCacheBuilder<T> builder(Class<T> type) {
SpiImmutableCacheFactory factory = XBootstrapService.immutableCacheFactory();
if (factory != null) {
return factory.builder(type);
}
return new LoadingBuilder<>(type);
}
/**
* Return a loader-backed immutable bean cache that memoizes both hits and misses.
*
* <pre>{@code
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(MyRef.class, ids ->
* database.find(MyRef.class)
* .setUnmodifiable(true)
* .where().idIn(ids)
* .findMap()
* );
* }</pre>
*
* @param type The bean type.
* @param loader Batch loader for unresolved ids.
*/
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
return builder(type).loader(loader).build();
}
/**
* Return a query-loader-backed immutable bean cache.
*
* <pre>{@code
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(
* MyRef.class,
* database,
* FetchGroup.of(MyRef.class, "version")
* );
* }</pre>
*/
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
return builder(type).loading(db, fetchGroup).build();
}
/**
* Return a batch loader backed by an unmodifiable query using the given fetch group.
*/
public static <T> Function<Set<Object>, Map<Object, T>> queryLoader(Database db, Class<T> type, FetchGroup<T> fetchGroup) {
return new QueryLoader<>(type, db, fetchGroup);
}
private static final class QueryLoader<T> implements Function<Set<Object>, Map<Object, T>> {
private final Class<T> type;
private final Database db;
private final FetchGroup<T> fetchGroup;
QueryLoader(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
this.type = requireNonNull(type);
this.db = requireNonNull(db);
this.fetchGroup = requireNonNull(fetchGroup);
}
@Override
public Map<Object, T> apply(Set<Object> ids) {
if (ids.isEmpty()) {
return Collections.emptyMap();
}
return db.find(type)
.select(fetchGroup)
.setUnmodifiable(true)
.where().idIn(ids)
.findMap();
}
}
private static final class LoadingBuilder<T> implements ImmutableCacheBuilder<T> {
private final Class<T> type;
private Function<Set<Object>, Map<Object, T>> loader;
private int maxSize;
private int maxIdleSeconds;
private int maxSecondsToLive;
private LoadingBuilder(Class<T> type) {
this.type = requireNonNull(type);
}
@Override
public ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader) {
this.loader = requireNonNull(loader);
return this;
}
@Override
public ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup) {
this.loader = new QueryLoader<>(type, db, fetchGroup);
return this;
}
@Override
public ImmutableCacheBuilder<T> maxSize(int maxSize) {
this.maxSize = maxSize;
return this;
}
@Override
public ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds) {
this.maxIdleSeconds = maxIdleSeconds;
return this;
}
@Override
public ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive) {
this.maxSecondsToLive = maxSecondsToLive;
return this;
}
@Override
public ImmutableBeanCache<T> build() {
if (loader == null) {
throw new IllegalStateException("No loader defined. Call loader(...) or loading(...) before build().");
}
if (maxSize > 0 || maxIdleSeconds > 0 || maxSecondsToLive > 0) {
throw new IllegalStateException("Cache policy options require SpiImmutableCacheFactory (ebean-core).");
}
return new LoadingCache<>(type, loader);
}
}
private static final class LoadingCache<T> implements ImmutableBeanCache<T> {
private final Class<T> type;
private final Function<Set<Object>, Map<Object, T>> loader;
private final ConcurrentHashMap<Object, T> cache = new ConcurrentHashMap<>();
private final Set<Object> misses = ConcurrentHashMap.newKeySet();
private LoadingCache(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
this.type = requireNonNull(type);
this.loader = requireNonNull(loader);
}
@Override
public Class<T> type() {
return type;
}
@Override
public @Nullable T getIfPresent(Object id) {
return cache.get(id);
}
@Override
public Map<Object, T> getAll(Set<Object> ids) {
if (ids.isEmpty()) {
return Collections.emptyMap();
}
Set<Object> loadIds = null;
for (Object id : ids) {
if (!cache.containsKey(id) && !misses.contains(id)) {
if (loadIds == null) {
loadIds = new LinkedHashSet<>();
}
loadIds.add(id);
}
}
if (loadIds != null && !loadIds.isEmpty()) {
Map<Object, T> loaded = loader.apply(loadIds);
if (loaded == null) {
loaded = Collections.emptyMap();
}
for (Map.Entry<Object, T> entry : loaded.entrySet()) {
if (entry.getValue() != null) {
cache.put(entry.getKey(), entry.getValue());
}
}
for (Object id : loadIds) {
if (!cache.containsKey(id)) {
misses.add(id);
}
}
}
Map<Object, T> result = new LinkedHashMap<>();
for (Object id : ids) {
T bean = cache.get(id);
if (bean != null) {
result.put(id, bean);
}
}
return result;
}
}
}
@@ -0,0 +1,60 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* Builder for creating {@link ImmutableBeanCache} instances.
*
* <pre>{@code
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
* .select("version")
* .fetch("names", "locale,text")
* .build();
*
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
* .loading(database, fetchGroup)
* .maxSize(10_000)
* .maxIdleSeconds(300)
* .maxSecondsToLive(1_800)
* .build();
* }</pre>
*
* @see ImmutableBeanCaches#builder(Class)
*/
@NullMarked
public interface ImmutableCacheBuilder<T> {
/**
* Set the batch loader used for unresolved ids.
*/
ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader);
/**
* Configure a query-based loader using the given database and fetch group.
*/
ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup);
/**
* Configure max cache size (0 means unbounded).
*/
ImmutableCacheBuilder<T> maxSize(int maxSize);
/**
* Configure max idle time in seconds (0 means disabled).
*/
ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds);
/**
* Configure max time-to-live in seconds (0 means disabled).
*/
ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive);
/**
* Build the immutable bean cache.
*/
ImmutableBeanCache<T> build();
}
@@ -1,6 +1,6 @@
package io.ebean;
import io.avaje.lang.Nullable;
import org.jspecify.annotations.Nullable;
/**
* Options to be used with insert such as ON CONFLICT DO UPDATE | NOTHING.
@@ -61,7 +61,7 @@ package io.ebean;
* .eq("status", Customer.Status.ACTIVE)
* .gt("id", 0)
* .endAnd()
* .order().asc("name");
* .orderBy().asc("name");
*
* q.findList();
* String s = q.getGeneratedSql();
@@ -0,0 +1,19 @@
package io.ebean;
/**
* Thrown when trying to access a property that isn't loaded on an entity
* that is unmodifiable or has disabled lazy loading.
* <p>
* On a normal mutable entity accessing the property would invoke lazy loading. On
* a unmodifiable entity with lazy loading disabled, accessing an unloaded property
* throws this LazyInitialisationException instead.
*/
public class LazyInitialisationException extends BeanAccessException {
/**
* Create specifying the property that was being accessed.
*/
public LazyInitialisationException(String message) {
super(message);
}
}
@@ -0,0 +1,114 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import jakarta.persistence.EntityNotFoundException;
import java.sql.Connection;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Stream;
/**
* Query that maps an entity graph query result to a nested DTO graph, produced by
* {@code query.mapTo(SomeDto.class)}.
* <p>
* Distinct from the existing flat, single-row {@link DtoQuery} pipeline (see {@link
* QueryBuilder#asDto(Class)}) - this executes the underlying entity ORM query (with the
* select()/fetch() spec automatically derived from the target DTO's declared shape, see
* {@link DtoMapper#fetchGroup()}), forces {@code setUnmodifiable(true)}, and then maps the
* resulting (unmodifiable) entity graph into a DTO graph via the generated {@link DtoMapper},
* supporting nested ToOne/ToMany and identity-aware de-duplication.
*
* @param <D> the target DTO type
*/
@NullMarked
public interface MappedQuery<D> {
/**
* Execute the query returning the mapped DTO list.
*/
List<D> findList();
/**
* Execute the query returning a paged list of mapped DTOs.
* <p>
* Mirrors {@code Query#findPagedList()} - the underlying entity graph query is paged (via
* {@code setFirstRow(int)}/{@code setMaxRows(int)}) and executed as normal, then each page's
* result is mapped to the target DTO graph. Row-count/page-index metadata
* ({@link PagedList#getTotalCount()}, {@link PagedList#hasNext()}, etc.) reflects the
* underlying entity query and is unaffected by the DTO mapping.
*/
PagedList<D> findPagedList();
/**
* Execute the query returning the result as a Stream of mapped DTOs.
* <p>
* Mirrors {@link QueryBuilder#findStream()} - the underlying entity graph query is streamed
* (supporting very large queries iterating any number of results, potentially using multiple
* persistence contexts internally) and each entity is mapped to its target DTO lazily as the
* stream is consumed, sharing one {@link DtoMapContext} across the whole stream so that
* repeated references to the same source entity still de-duplicate to the same DTO instance.
* <pre>{@code
*
* // use try with resources to ensure Stream is closed
*
* try (Stream<CustomerDto> stream = query.mapTo(CustomerDto.class).findStream()) {
* stream
* .map(...)
* .collect(...);
* }
*
* }</pre>
*/
Stream<D> findStream();
/**
* Execute the query returning a single mapped DTO, or {@code null} if there is no matching row.
*/
@Nullable
D findOne();
/**
* Execute the query returning an optional mapped DTO.
*/
Optional<D> findOneOrEmpty();
/**
* Execute the query returning a single mapped DTO or throwing a
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
* <p>
* The exception message reflects the underlying entity type and its id or single
* equality predicate (a likely natural/unique key) when the query is that simple,
* otherwise a generic "not found" message.
*/
default D findOneOrThrow() {
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
}
/**
* Execute the query returning a single mapped DTO or throwing the exception produced
* by the given supplier if there is no matching row.
*/
default D findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
return findOneOrEmpty().orElseThrow(exceptionSupplier);
}
/**
* 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);
}
@@ -16,4 +16,7 @@ public interface ModifyAwareType {
*/
void setMarkedDirty(boolean markedDirty);
default Object freeze() {
return this; // throw new UnsupportedOperationException();
}
}
+90 -40
View File
@@ -8,13 +8,11 @@ import java.util.Objects;
/**
* Represents an Order By for a Query.
* <p>
* Is a ordered list of OrderBy.Property objects each specifying a property and
* Is an ordered list of OrderBy.Property objects each specifying a property and
* whether it is ascending or descending order.
* </p>
* <p>
* Typically you will not construct an OrderBy yourself but use one that exists
* Typically, you will not construct an OrderBy yourself but use one that exists
* on the Query object.
* </p>
*/
public class OrderBy<T> implements Serializable {
@@ -25,8 +23,22 @@ public class OrderBy<T> implements Serializable {
private final List<Property> list;
/**
* Create an OrderBy parsing the given order by clause.
* <p>
* The order by clause follows SQL order by clause with comma's between each
* property and optionally "asc" or "desc" to represent ascending or
* descending order respectively.
*/
public static <P> OrderBy<P> of(String orderByClause) {
return new OrderBy<>(orderByClause);
}
/**
* @deprecated This method will be removed from public API.
* <p>
* Create an empty OrderBy with no associated query.
*/
@Deprecated(forRemoval = true)
public OrderBy() {
this.list = new ArrayList<>(3);
}
@@ -36,20 +48,17 @@ public class OrderBy<T> implements Serializable {
}
/**
* Create an orderBy parsing the order by clause.
* <p>
* The order by clause follows SQL order by clause with comma's between each
* property and optionally "asc" or "desc" to represent ascending or
* descending order respectively.
* </p>
* @deprecated migrate to {@link OrderBy#of(String)}.
*/
@Deprecated(forRemoval = true)
public OrderBy(String orderByClause) {
this(null, orderByClause);
}
/**
* Construct with a given query and order by clause.
* @deprecated This method will be removed from public API.
*/
@Deprecated(forRemoval = true)
public OrderBy(Query<T> query, String orderByClause) {
this.query = query;
this.list = new ArrayList<>(3);
@@ -110,8 +119,11 @@ public class OrderBy<T> implements Serializable {
}
/**
* @deprecated This method will become internal only API.
* <p>
* Return a copy of this OrderBy with the path trimmed.
*/
@Deprecated(forRemoval = true)
public OrderBy<T> copyWithTrim(String path) {
List<Property> newList = new ArrayList<>(list.size());
for (Property aList : list) {
@@ -186,15 +198,15 @@ public class OrderBy<T> implements Serializable {
if (list.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
var append = new StringAppend();
for (int i = 0; i < list.size(); i++) {
Property property = list.get(i);
if (i > 0) {
sb.append(", ");
append.append(", ");
}
sb.append(property.toStringFormat());
property.toStringFormat(append);
}
return sb.toString();
return append.toString();
}
@Override
@@ -231,6 +243,55 @@ public class OrderBy<T> implements Serializable {
return this;
}
/**
* Append the order by clause.
*/
public interface Append {
/**
* Append a property expression.
*/
Append property(String property);
/**
* Append a literal.
*/
Append append(String literal);
/**
* Parse and append an expression.
*/
Append parse(String expression);
}
private static final class StringAppend implements Append {
private final StringBuilder builder = new StringBuilder();
@Override
public String toString() {
return builder.toString();
}
@Override
public Append property(String property) {
builder.append(property);
return this;
}
@Override
public Append append(String literal) {
builder.append(literal);
return this;
}
@Override
public Append parse(String raw) {
builder.append(raw);
return this;
}
}
/**
* A property and its ascending descending order.
*/
@@ -309,36 +370,25 @@ public class OrderBy<T> implements Serializable {
@Override
public String toString() {
return toStringFormat();
return property;
}
public String toStringFormat() {
if (nulls == null && collation == null) {
if (ascending) {
return property;
public void toStringFormat(Append append) {
if (collation != null) {
if (collation.contains("${}")) {
// this is a complex collation, e.g. DB2 - we must replace the property
append.parse(collation.replace("${}", property));
} else {
return property + " desc";
append.property(property).append(" collate ").append(collation);
}
} else {
StringBuilder sb = new StringBuilder();
if (collation != null) {
if (collation.contains("${}")) {
// this is a complex collation, e.g. DB2 - we must replace the property
sb.append(collation.replace("${}", property));
} else {
sb.append(property);
sb.append(" collate ").append(collation);
}
} else {
sb.append(property);
}
if (!ascending) {
sb.append(' ').append("desc");
}
if (nulls != null) {
sb.append(' ').append(nulls).append(' ').append(highLow);
}
return sb.toString();
append.property(property);
}
if (!ascending) {
append.append(" desc");
}
if (nulls != null) {
append.append(" ").append(nulls).append(" ").append(highLow);
}
}
@@ -25,7 +25,7 @@ import java.util.concurrent.Future;
*
* PagedList<Order> pagedList = DB.find(Order.class)
* .where().eq("status", Order.Status.NEW)
* .order().asc("id")
* .orderBy().asc("id")
* .setFirstRow(0)
* .setMaxRows(50)
* .findPagedList();

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