Compare commits

..
56 Commits
Author SHA1 Message Date
Rob Bygrave e525d4e159 Version 18.2.0 2026-07-03 19:38:05 +12:00
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
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
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
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
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
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
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
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
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
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
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 GlushkovandGitHub 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 BygraveandGitHub 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 BygraveandGitHub 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 BygraveandGitHub 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 BygraveandGitHub 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
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
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 BygraveandGitHub 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
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 GlushkovandGitHub 98f0d42b7e Fix: defer savepoint cache changes to parent transaction commit (#3804) 2026-06-30 18:47:24 +12:00
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 BygraveandGitHub 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
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
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
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
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
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 BygraveandGitHub 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 Pramlandrobin.bygrave dd1acf58a0 DbJson Support for Dto-Queries 2026-06-24 18:14:45 +12:00
Rob BygraveandGitHub 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
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 BygraveandGitHub 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
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 BygraveandGitHub 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
Roland Praml 1c0a811c01 Tenant support for query plans 2024-10-07 15:07:38 +02:00
263 changed files with 8461 additions and 984 deletions
+4 -2
View File
@@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -40,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 1C clean test -Pdefault
run: mvn -T 1C clean install -Pdefault
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
run: cd tests/test-java16 && mvn test
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11, 17, 21]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<artifactId>composites</artifactId>
+1
View File
@@ -14,6 +14,7 @@ Key guides (fetch and follow when performing the relevant task):
- 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
- 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
+2
View File
@@ -40,6 +40,7 @@ existing Maven project. Complete the steps in order.
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
| [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
@@ -47,6 +48,7 @@ existing Maven project. Complete the steps in order.
|-------|-------------|
| [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 |
| [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
+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).
+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/)
+25
View File
@@ -483,6 +483,30 @@ Prefer the following order:
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
@@ -570,4 +594,5 @@ When asked to add or modify an Ebean query:
- [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/)
+48 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<name>ebean api</name>
@@ -103,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>
@@ -61,6 +61,10 @@ public interface DatabaseBuilder {
/**
* Build and return the Database instance.
* <p>
* When {@link #setRegister(boolean)} is set to true, and a database with the same
* name is already registered, this may return the existing registered database
* rather than creating a new one.
*/
Database build();
@@ -883,6 +887,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.
*/
@@ -2563,6 +2574,11 @@ public interface DatabaseBuilder {
*/
boolean isAutoPersistUpdates();
/**
* Return true if caches are partitioned by tenant.
*/
boolean isTenantPartitionedCache();
/**
* Return the L2 cache default max size.
*/
@@ -7,6 +7,8 @@ import jakarta.persistence.PersistenceException;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.System.Logger.Level.WARNING;
/**
* Low-level factory for creating {@link Database} instances.
* <p>
@@ -71,18 +73,28 @@ public final class DatabaseFactory {
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);
Database existing = DbContext.getInstance().getRegistered(name);
if (existing != null) {
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
return existing;
}
}
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;
@@ -4,6 +4,8 @@ import io.ebean.config.BeanNotEnhancedException;
import io.ebean.datasource.DataSourceConfigurationException;
import jakarta.persistence.PersistenceException;
import org.jspecify.annotations.Nullable;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@@ -75,6 +77,11 @@ final class DbContext {
return defaultDatabase;
}
@Nullable
Database getRegistered(String name) {
return concMap.get(name);
}
/**
* Return the database by name.
*/
@@ -205,6 +205,22 @@ public interface ExpressionList<T> {
*/
int delete();
/**
* 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 permanently deleted.
*/
int deletePermanent();
/**
* Execute as a update query.
*
@@ -607,6 +607,21 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
*/
int delete();
/**
* 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.
*
* @return the number of beans/rows that were permanently deleted.
*/
int deletePermanent();
/**
* Execute the query returning true if a row is found.
* <p>
@@ -41,6 +41,49 @@ public interface RawSqlBuilder {
return XBootstrapService.rawSql().unparsed(sql);
}
/**
* Return a RawSqlBuilder for SQL containing {@code ${where}}, {@code ${having}} and/or
* {@code ${orderBy}} placeholder(s). Unlike {@link #parse(String)} this does NOT attempt to parse
* the SELECT columns, so it supports complex SQL such as CTEs, subqueries, and window functions.
* <p>
* Explicit column mappings must be provided (as with {@link #unparsed(String)}), but
* WHERE, HAVING and ORDER BY expressions can be added dynamically via the query API - provided
* the corresponding placeholder is present in the SQL. If a query calls {@code .orderBy(...)}
* on a template with no {@code ${orderBy}}/{@code ${andOrderBy}} placeholder, that ordering is
* ignored (there is no injection point for it) rather than producing invalid SQL.
* </p>
* <p>
* Available placeholders:
* </p>
* <ul>
* <li>{@code ${where}} / {@code ${andWhere}} - inject "where &lt;expr&gt;" / "and &lt;expr&gt;"</li>
* <li>{@code ${having}} / {@code ${andHaving}} - inject "having &lt;expr&gt;" / "and &lt;expr&gt;"</li>
* <li>{@code ${orderBy}} / {@code ${andOrderBy}} - inject "order by &lt;expr&gt;" / ", &lt;expr&gt;"</li>
* </ul>
* <h3>Example:</h3>
* <pre>{@code
*
* String sql = """
* with agg as (
* select company_id, sum(amount) as total
* from orders
* ${where}
* group by company_id
* )
* select company_id, total from agg ${orderBy}
* """;
*
* RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
* .columnMapping("company_id", "companyId")
* .columnMapping("total", "total")
* .create();
*
* }</pre>
*/
static RawSqlBuilder withPlaceholders(String sql) {
return XBootstrapService.rawSql().withPlaceholders(sql);
}
/**
* Return a RawSqlBuilder parsing the sql.
* <p>
@@ -260,6 +260,27 @@ public interface Transaction extends AutoCloseable {
*/
void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties);
/**
* Set to false to disable auto-generation of {@code @WhenCreated}, {@code @WhenModified},
* {@code @WhoCreated} and {@code @WhoModified} values for this transaction.
* <p>
* When disabled, Ebean will only set a generated property value if the property currently
* has a null value (for inserts) or is a {@code @Version} property. Any value already set
* on the bean is preserved.
* <p>
* This is useful in backup and restore scenarios where you need to retain the original
* audit timestamps and user values rather than have them overwritten.
* <pre>{@code
* try (Transaction txn = DB.beginTransaction()) {
* txn.setGeneratedPropertiesEnabled(false);
* bean.setWhenCreated(originalTimestamp);
* DB.save(bean);
* txn.commit();
* }
* }</pre>
*/
void setGeneratedPropertiesEnabled(boolean enable);
/**
* Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries.
* <p>
@@ -175,7 +175,7 @@ public final class InterceptReadOnly extends InterceptBase {
@Override
public boolean isUpdate() {
return false;
return true;
}
@Override
@@ -151,14 +151,16 @@ public final class BeanList<E> extends AbstractBeanCollection<E> implements List
}
}
/**
* Set the actual underlying list.
* <p>
* This is primarily for the deferred fetching function.
*/
@SuppressWarnings("unchecked")
public void setActualList(List<?> list) {
this.list = (List<E>) list;
public BeanCollectionAdd collectionAdd() {
if (list == null) {
list = new ArrayList<>();
}
return this;
}
public void refresh(ModifyListenMode modifyListenMode, BeanList<E> newList) {
setModifyListening(modifyListenMode);
this.list = newList.actualList();
}
/**
@@ -1,9 +1,6 @@
package io.ebean.common;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollectionLoader;
import io.ebean.bean.EntityBean;
import io.ebean.bean.ToStringBuilder;
import io.ebean.bean.*;
import java.util.*;
@@ -17,12 +14,12 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
/**
* The underlying map implementation.
*/
private Map<K, E> map;
private LinkedHashMap<K, E> map;
/**
* Create with a given Map.
*/
public BeanMap(Map<K, E> map) {
public BeanMap(LinkedHashMap<K, E> map) {
this.map = map;
}
@@ -165,18 +162,23 @@ public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements Ma
}
}
/**
* Set the actual underlying map. Used for performing lazy fetch.
*/
public LinkedHashMap<K, E> collectionAdd() {
if (map == null) {
map = new LinkedHashMap<>();
}
return map;
}
@SuppressWarnings("unchecked")
public void setActualMap(Map<?, ?> map) {
this.map = (Map<K, E>) map;
public void refresh(ModifyListenMode modifyListenMode, BeanMap<?, ?> newMap) {
setModifyListening(modifyListenMode);
this.map = (LinkedHashMap<K, E>) newMap.actualMap();
}
/**
* Return the actual underlying map.
*/
public Map<K, E> actualMap() {
public LinkedHashMap<K, E> actualMap() {
return map;
}
@@ -15,12 +15,12 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
/**
* The underlying Set implementation.
*/
private Set<E> set;
private LinkedHashSet<E> set;
/**
* Create with a specific Set implementation.
*/
public BeanSet(Set<E> set) {
public BeanSet(LinkedHashSet<E> set) {
this.set = set;
}
@@ -146,18 +146,22 @@ public final class BeanSet<E> extends AbstractBeanCollection<E> implements Set<E
}
}
/**
* Set the underlying set (used for lazy fetch).
*/
@SuppressWarnings("unchecked")
public void setActualSet(Set<?> set) {
this.set = (Set<E>) set;
public BeanCollectionAdd collectionAdd() {
if (set == null) {
set = new LinkedHashSet<>();
}
return this;
}
public void refresh(ModifyListenMode modifyListenMode, BeanSet<E> newSet) {
setModifyListening(modifyListenMode);
this.set = newSet.actualSet();
}
/**
* Return the actual underlying set.
*/
public Set<E> actualSet() {
public LinkedHashSet<E> actualSet() {
return set;
}
@@ -432,6 +432,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
private int backgroundExecutorShutdownSecs = 30;
private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper();
private boolean tenantPartitionedCache;
// defaults for the L2 bean caching
private int cacheMaxSize = 10000;
@@ -1175,6 +1177,17 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
return cacheMaxSize;
}
@Override
public boolean isTenantPartitionedCache() {
return tenantPartitionedCache;
}
@Override
public DatabaseConfig tenantPartitionedCache(boolean tenantPartitionedCache) {
this.tenantPartitionedCache = tenantPartitionedCache;
return this;
}
@Override
public DatabaseConfig setCacheMaxSize(int cacheMaxSize) {
this.cacheMaxSize = cacheMaxSize;
@@ -2228,6 +2241,15 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders);
ddlHeader = p.get("ddl.header", ddlHeader);
tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache);
cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize);
cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime);
cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive);
queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize);
queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime);
queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive);
// read tenant-configuration from config:
// tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION
String mode = p.get("tenant.mode");
@@ -11,10 +11,12 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
@@ -23,18 +25,36 @@ import static java.lang.System.Logger.Level.ERROR;
/**
* Database sequence based IdGenerator.
* <p>
* Maintains a separate buffer of pre-fetched id values per tenant when the supplied
* DataSource implements {@link TenantConnectionSource}. For the common single-tenant
* case a single buffer is used (keyed by {@link #SINGLE}).
*/
public abstract class SequenceIdGenerator implements PlatformIdGenerator {
protected static final System.Logger log = AppLog.getLogger("io.ebean.SEQ");
private final ReentrantLock lock = new ReentrantLock();
/**
* Buffer key used when there is no current tenant (single-tenant or no tenant in scope).
*/
private static final Object SINGLE = new Object();
protected final String seqName;
protected final DataSource dataSource;
protected final BackgroundExecutor backgroundExecutor;
protected final NavigableSet<Long> idList = new TreeSet<>();
protected final int allocationSize;
protected AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
private final TenantConnectionSource tenantSource;
private final TenantBuffer single = new TenantBuffer();
private final ConcurrentMap<Object, TenantBuffer> buffers = new ConcurrentHashMap<>();
/**
* Per-tenant pre-fetched id buffer with its own lock and background-loading flag.
*/
private static final class TenantBuffer {
final ReentrantLock lock = new ReentrantLock();
final Deque<Long> idList = new ArrayDeque<>();
final AtomicBoolean currentlyBackgroundLoading = new AtomicBoolean(false);
}
/**
* Construct given a dataSource and sql to return the next sequence value.
@@ -44,6 +64,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
this.dataSource = ds;
this.seqName = seqName;
this.allocationSize = allocationSize;
this.tenantSource = (ds instanceof TenantConnectionSource) ? (TenantConnectionSource) ds : null;
}
public abstract String getSql(int batchSize);
@@ -64,6 +85,24 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
return true;
}
private Object currentTenantKey() {
if (tenantSource != null) {
Object tenantId = tenantSource.currentTenantId();
if (tenantId != null) {
return tenantId;
}
}
return SINGLE;
}
private TenantBuffer buffer(Object tenantKey) {
if (tenantKey == SINGLE) {
// common single-tenant path - avoid the concurrent map lookup
return single;
}
return buffers.computeIfAbsent(tenantKey, k -> new TenantBuffer());
}
/**
* If allocateSize is large load some sequences in a background thread.
* <p>
@@ -78,23 +117,22 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
/**
* Return the next Id.
* <p>
* If a Transaction has been passed in use the Connection from it.
* </p>
*/
@Override
public Object nextId(Transaction t) {
lock.lock();
Object tenantKey = currentTenantKey();
TenantBuffer buffer = buffer(tenantKey);
buffer.lock.lock();
try {
int size = idList.size();
int size = buffer.idList.size();
if (size > 0) {
maybeLoadMoreInBackground(size);
} else {
loadMore(allocationSize);
loadMore(tenantKey, buffer, allocationSize);
}
return idList.pollFirst();
return buffer.idList.poll();
} finally {
lock.unlock();
buffer.lock.unlock();
}
}
@@ -106,29 +144,36 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
}
}
private void loadMore(int requestSize) {
List<Long> newIds = getMoreIds(requestSize);
lock.lock();
private void loadMore(Object tenantKey, TenantBuffer buffer, int requestSize) {
List<Long> newIds = getMoreIds(tenantKey, requestSize);
buffer.lock.lock();
try {
idList.addAll(newIds);
buffer.idList.addAll(newIds);
} finally {
lock.unlock();
buffer.lock.unlock();
}
}
/**
* Load another batch of Id's using a background thread.
* <p>
* The tenant is captured here (submit time) as the current tenant is not in scope
* on the background executor thread.
*/
protected void loadInBackground(final int requestSize) {
if (currentlyBackgroundLoading.get()) {
final Object tenantKey = currentTenantKey();
final TenantBuffer buffer = buffer(tenantKey);
if (!buffer.currentlyBackgroundLoading.compareAndSet(false, true)) {
// skip as already background loading
log.log(DEBUG, "... skip background sequence load (another load in progress)");
return;
}
currentlyBackgroundLoading.set(true);
backgroundExecutor.execute(() -> {
loadMore(requestSize);
currentlyBackgroundLoading.set(false);
try {
loadMore(tenantKey, buffer, requestSize);
} finally {
buffer.currentlyBackgroundLoading.set(false);
}
});
}
@@ -140,7 +185,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
/**
* Get more Id's by executing a query and reading the Id's returned.
*/
protected List<Long> getMoreIds(int requestSize) {
protected List<Long> getMoreIds(Object tenantKey, int requestSize) {
String sql = getSql(requestSize);
@@ -148,7 +193,7 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
PreparedStatement statement = null;
ResultSet resultSet = null;
try {
connection = dataSource.getConnection();
connection = connectionFor(tenantKey);
statement = connection.prepareStatement(sql);
resultSet = statement.executeQuery();
@@ -174,6 +219,17 @@ public abstract class SequenceIdGenerator implements PlatformIdGenerator {
}
}
/**
* Return a connection for the given tenant. For multi-tenant this is routed to the
* tenant database/schema/catalog; otherwise the plain DataSource connection is used.
*/
private Connection connectionFor(Object tenantKey) throws SQLException {
if (tenantSource != null && tenantKey != SINGLE) {
return tenantSource.connectionForTenant(tenantKey);
}
return dataSource.getConnection();
}
/**
* Close the JDBC resources.
*/
@@ -0,0 +1,29 @@
package io.ebean.config.dbplatform;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Optionally implemented by the DataSource passed to a {@link SequenceIdGenerator}
* to make sequence id allocation multi-tenant aware.
* <p>
* When the DataSource implements this interface the sequence generator maintains
* a separate id buffer per tenant and obtains connections that are routed to the
* correct tenant database (TenantMode.DB) or schema/catalog (TenantMode.SCHEMA / CATALOG).
* <p>
* The {@link #connectionForTenant(Object)} method takes an explicit tenantId so that
* background pre-fetch (which runs on a separate thread without the current tenant
* in scope) can fetch sequence values for the tenant captured at submit time.
*/
public interface TenantConnectionSource {
/**
* Return the current tenant id, or null when there is no current tenant scope.
*/
Object currentTenantId();
/**
* Return a connection routed to the given tenant (its database, schema or catalog).
*/
Connection connectionForTenant(Object tenantId) throws SQLException;
}
@@ -44,6 +44,11 @@ public interface MetaQueryPlan {
*/
String plan();
/**
* The tenant ID of the plan.
*/
Object tenantId();
/**
* Return the query execution time associated with the bind values capture.
*/
@@ -27,6 +27,13 @@ public interface SpiRawSqlService extends BootstrapService {
*/
RawSqlBuilder unparsed(String sql);
/**
* SQL with ${where}/${having} placeholder(s) but no SELECT column parsing.
* Supports complex SQL (CTEs, window functions) where keyword parsing would fail.
* Explicit column mapping is required (as with unparsed).
*/
RawSqlBuilder withPlaceholders(String sql);
/**
* Create based on a JDBC ResultSet.
*
@@ -0,0 +1,432 @@
package io.ebean.common;
import io.ebean.bean.*;
import java.util.*;
/**
* Map capable of lazy loading and modification aware.
*/
public final class BeanMap<K, E> extends AbstractBeanCollection<E> implements SequencedMap<K, E> {
private static final long serialVersionUID = 1L;
/**
* The underlying map implementation.
*/
private LinkedHashMap<K, E> map;
/**
* Create with a given Map.
*/
public BeanMap(LinkedHashMap<K, E> map) {
this.map = map;
}
/**
* Create using a underlying LinkedHashMap.
*/
public BeanMap() {
this(new LinkedHashMap<>());
}
public BeanMap(BeanCollectionLoader ebeanServer, EntityBean ownerBean, String propertyName) {
super(ebeanServer, ownerBean, propertyName);
}
@Override
public Map<K, E> freeze() {
return map == null ? null : Collections.unmodifiableMap(map);
}
@Override
public void toString(ToStringBuilder builder) {
if (map == null || map.isEmpty()) {
builder.addRaw("{}");
} else {
builder.addRaw("{");
for (Entry<K, E> entry : map.entrySet()) {
builder.add(String.valueOf(entry.getKey()), entry.getValue());
}
builder.addRaw("}");
}
}
@Override
public void reset(EntityBean ownerBean, String propertyName) {
this.ownerBean = ownerBean;
this.propertyName = propertyName;
this.map = null;
}
@Override
public boolean isSkipSave() {
return map == null || (map.isEmpty() && !holdsModifications());
}
@Override
@SuppressWarnings("unchecked")
public void loadFrom(BeanCollection<?> other) {
BeanMap<K, E> otherMap = (BeanMap<K, E>) other;
internalPutNull();
map.putAll(otherMap.actualMap());
}
public void internalPutNull() {
if (map == null) {
map = new LinkedHashMap<>();
}
}
@SuppressWarnings("unchecked")
public void internalPut(Object key, Object bean) {
if (map == null) {
map = new LinkedHashMap<>();
}
if (key != null) {
map.put((K) key, (E) bean);
}
}
public void internalPutWithCheck(Object key, Object bean) {
if (map == null || key == null || !map.containsKey(key)) {
internalPut(key, bean);
}
}
@Override
public void internalAddWithCheck(Object bean) {
throw new RuntimeException("Not allowed for map");
}
@Override
public void internalAdd(Object bean) {
throw new RuntimeException("Not allowed for map");
}
/**
* Return true if the underlying map has been populated. Returns false if it
* has a deferred fetch pending.
*/
@Override
public boolean isPopulated() {
return map != null;
}
/**
* Return true if this is a reference (lazy loading) bean collection. This is
* the same as !isPopulated();
*/
@Override
public boolean isReference() {
return map == null;
}
@Override
public boolean checkEmptyLazyLoad() {
if (map == null) {
map = new LinkedHashMap<>();
return true;
} else {
return false;
}
}
private void initClear() {
lock.lock();
try {
if (map == null) {
if (!disableLazyLoad && modifyListening) {
lazyLoadCollection(true);
} else {
map = new LinkedHashMap<>();
}
}
} finally {
lock.unlock();
}
}
private void init() {
lock.lock();
try {
if (map == null) {
if (disableLazyLoad) {
map = new LinkedHashMap<>();
} else {
lazyLoadCollection(false);
}
}
} finally {
lock.unlock();
}
}
public LinkedHashMap<K, E> collectionAdd() {
if (map == null) {
map = new LinkedHashMap<>();
}
return map;
}
@SuppressWarnings("unchecked")
public void refresh(ModifyListenMode modifyListenMode, BeanMap<?, ?> newMap) {
setModifyListening(modifyListenMode);
this.map = (LinkedHashMap<K, E>) newMap.actualMap();
}
/**
* Return the actual underlying map.
*/
public LinkedHashMap<K, E> actualMap() {
return map;
}
/**
* Returns the collection of beans (map values).
*/
@Override
public Collection<E> actualDetails() {
return map.values();
}
/**
* Returns the map entrySet.
*/
@Override
public Collection<?> actualEntries() {
return map.entrySet();
}
@Override
public String toString() {
if (map == null) {
return "BeanMap<deferred>";
} else {
return map.toString();
}
}
/**
* Equal if object is a Map and equal in a Map sense.
*/
@Override
public boolean equals(Object object) {
init();
return map.equals(object);
}
@Override
public int hashCode() {
init();
return map.hashCode();
}
@Override
public void clear() {
initClear();
if (modifyListening) {
// add all beans to the removal list
for (E bean : map.values()) {
modifyRemoval(bean);
}
}
map.clear();
}
@Override
public boolean containsKey(Object key) {
init();
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
init();
return map.containsValue(value);
}
@Override
public Set<Entry<K, E>> entrySet() {
init();
return modifyListening ? new ModifyEntrySet<>(this, map.entrySet()) : map.entrySet();
}
@Override
public E get(Object key) {
init();
return map.get(key);
}
@Override
public boolean isEmpty() {
init();
return map.isEmpty();
}
@Override
public Set<K> keySet() {
init();
return modifyListening ? new ModifyKeySet<>(this, map.keySet()) : map.keySet();
}
@Override
public E put(K key, E value) {
init();
if (modifyListening) {
E oldBean = map.put(key, value);
if (value != oldBean) {
// register the add of the new and the removal of the old
modifyAddition(value);
modifyRemoval(oldBean);
}
return oldBean;
} else {
return map.put(key, value);
}
}
@Override
public void putAll(Map<? extends K, ? extends E> puts) {
init();
if (modifyListening) {
for (Entry<? extends K, ? extends E> entry : puts.entrySet()) {
Object oldBean = map.put(entry.getKey(), entry.getValue());
if (entry.getValue() != oldBean) {
modifyAddition(entry.getValue());
modifyRemoval(oldBean);
}
}
} else {
map.putAll(puts);
}
}
@Override
public void addBean(E bean) {
throw new UnsupportedOperationException("Method not allowed on Map. Please use List instead.");
}
@Override
public void removeBean(E bean) {
throw new UnsupportedOperationException("Method not allowed on Map. Please use List instead.");
}
@Override
public E remove(Object key) {
init();
if (modifyListening) {
E o = map.remove(key);
modifyRemoval(o);
return o;
}
return map.remove(key);
}
@Override
public int size() {
init();
return map.size();
}
@Override
public Collection<E> values() {
init();
return modifyListening ? new ModifyCollection<>(this, map.values()) : map.values();
}
// -----------------------------------------------------//
// SequencedMap (Java 21+)
// -----------------------------------------------------//
@Override
public SequencedMap<K, E> reversed() {
init();
if (modifyListening) {
throw new UnsupportedOperationException("Not supported on modify listening map");
}
return map.reversed();
}
@Override
public Entry<K, E> firstEntry() {
init();
return map.firstEntry();
}
@Override
public Entry<K, E> lastEntry() {
init();
return map.lastEntry();
}
@Override
public Entry<K, E> pollFirstEntry() {
init();
Entry<K, E> entry = map.pollFirstEntry();
if (modifyListening && entry != null) {
modifyRemoval(entry.getValue());
}
return entry;
}
@Override
public Entry<K, E> pollLastEntry() {
init();
Entry<K, E> entry = map.pollLastEntry();
if (modifyListening && entry != null) {
modifyRemoval(entry.getValue());
}
return entry;
}
@Override
public E putFirst(K key, E value) {
init();
if (modifyListening) {
E oldBean = map.putFirst(key, value);
if (value != oldBean) {
// register the add of the new and the removal of the old
modifyAddition(value);
modifyRemoval(oldBean);
}
return oldBean;
} else {
return map.putFirst(key, value);
}
}
@Override
public E putLast(K key, E value) {
init();
if (modifyListening) {
E oldBean = map.putLast(key, value);
if (value != oldBean) {
// register the add of the new and the removal of the old
modifyAddition(value);
modifyRemoval(oldBean);
}
return oldBean;
} else {
return map.putLast(key, value);
}
}
@Override
public SequencedSet<K> sequencedKeySet() {
init();
return map.sequencedKeySet();
}
@Override
public SequencedCollection<E> sequencedValues() {
init();
return map.sequencedValues();
}
@Override
public SequencedSet<Entry<K, E>> sequencedEntrySet() {
init();
return map.sequencedEntrySet();
}
}
@@ -0,0 +1,419 @@
package io.ebean.common;
import io.ebean.bean.*;
import java.util.*;
/**
* Set capable of lazy loading and modification aware.
*/
public final class BeanSet<E> extends AbstractBeanCollection<E> implements SequencedSet<E>, BeanCollectionAdd {
private static final long serialVersionUID = 1L;
/**
* The underlying Set implementation.
*/
private LinkedHashSet<E> set;
/**
* Create with a specific Set implementation.
*/
public BeanSet(LinkedHashSet<E> set) {
this.set = set;
}
/**
* Create using an underlying LinkedHashSet.
*/
public BeanSet() {
this(new LinkedHashSet<>());
}
public BeanSet(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) {
super(loader, ownerBean, propertyName);
}
@Override
public Set<E> freeze() {
return set == null ? null : Collections.unmodifiableSet(set);
}
@Override
public void toString(ToStringBuilder builder) {
builder.addCollection(set);
}
@Override
public void reset(EntityBean ownerBean, String propertyName) {
this.ownerBean = ownerBean;
this.propertyName = propertyName;
this.set = null;
}
@Override
public boolean isSkipSave() {
return set == null || (set.isEmpty() && !holdsModifications());
}
@Override
@SuppressWarnings("unchecked")
public void addEntityBean(EntityBean bean) {
set.add((E) bean);
}
@Override
@SuppressWarnings("unchecked")
public void loadFrom(BeanCollection<?> other) {
if (set == null) {
set = new LinkedHashSet<>();
}
set.addAll((Collection<? extends E>) other.actualDetails());
}
@Override
public void internalAddWithCheck(Object bean) {
// set add() already de-dups so just add it
internalAdd(bean);
}
@Override
@SuppressWarnings("unchecked")
public void internalAdd(Object bean) {
if (set == null) {
set = new LinkedHashSet<>();
}
if (bean != null) {
set.add((E) bean);
}
}
/**
* Returns true if the underlying set has its data.
*/
@Override
public boolean isPopulated() {
return set != null;
}
/**
* Return true if this is a reference (lazy loading) bean collection. This is
* the same as !isPopulated();
*/
@Override
public boolean isReference() {
return set == null;
}
@Override
public boolean checkEmptyLazyLoad() {
if (set == null) {
set = new LinkedHashSet<>();
return true;
} else {
return false;
}
}
private void initClear() {
lock.lock();
try {
if (set == null) {
if (!disableLazyLoad && modifyListening) {
lazyLoadCollection(false);
} else {
set = new LinkedHashSet<>();
}
}
} finally {
lock.unlock();
}
}
private void init() {
lock.lock();
try {
if (set == null) {
if (disableLazyLoad) {
set = new LinkedHashSet<>();
} else {
lazyLoadCollection(false);
}
}
} finally {
lock.unlock();
}
}
public BeanCollectionAdd collectionAdd() {
if (set == null) {
set = new LinkedHashSet<>();
}
return this;
}
public void refresh(ModifyListenMode modifyListenMode, BeanSet<E> newSet) {
setModifyListening(modifyListenMode);
this.set = newSet.actualSet();
}
/**
* Return the actual underlying set.
*/
public LinkedHashSet<E> actualSet() {
return set;
}
@Override
public Collection<E> actualDetails() {
return set;
}
@Override
public Collection<?> actualEntries() {
return set;
}
@Override
public String toString() {
if (set == null) {
return "BeanSet<deferred>";
} else {
return set.toString();
}
}
/**
* Equal if obj is a Set and equal in a Set sense.
*/
@Override
public boolean equals(Object obj) {
init();
return set.equals(obj);
}
@Override
public int hashCode() {
init();
return set.hashCode();
}
@Override
public void addBean(E bean) {
add(bean);
}
@Override
public void removeBean(E bean) {
if (set.remove(bean)) {
getModifyHolder().modifyRemoval(bean);
}
}
// -----------------------------------------------------//
// proxy method for map
// -----------------------------------------------------//
@Override
public boolean add(E bean) {
init();
if (modifyListening) {
if (set.add(bean)) {
modifyAddition(bean);
return true;
} else {
return false;
}
}
return set.add(bean);
}
@Override
public boolean addAll(Collection<? extends E> beans) {
init();
if (modifyListening) {
boolean changed = false;
for (E bean : beans) {
if (set.add(bean)) {
// register the addition of the bean
modifyAddition(bean);
changed = true;
}
}
return changed;
}
return set.addAll(beans);
}
@Override
public void clear() {
initClear();
if (modifyListening) {
for (E bean : set) {
modifyRemoval(bean);
}
}
set.clear();
}
@Override
public boolean contains(Object bean) {
init();
return set.contains(bean);
}
@Override
public boolean containsAll(Collection<?> beans) {
init();
return set.containsAll(beans);
}
@Override
public boolean isEmpty() {
init();
return set.isEmpty();
}
@Override
public Iterator<E> iterator() {
init();
if (modifyListening) {
return new ModifyIterator<>(this, set.iterator());
}
return set.iterator();
}
@Override
public boolean remove(Object bean) {
init();
if (modifyListening) {
if (set.remove(bean)) {
modifyRemoval(bean);
return true;
}
return false;
}
return set.remove(bean);
}
@Override
public boolean removeAll(Collection<?> beans) {
init();
if (modifyListening) {
boolean changed = false;
for (Object bean : beans) {
if (set.remove(bean)) {
modifyRemoval(bean);
changed = true;
}
}
return changed;
}
return set.removeAll(beans);
}
@Override
public boolean retainAll(Collection<?> beans) {
init();
if (modifyListening) {
boolean changed = false;
Iterator<?> it = set.iterator();
while (it.hasNext()) {
Object bean = it.next();
if (!beans.contains(bean)) {
// not retaining this bean so add it to the removal list
it.remove();
modifyRemoval(bean);
changed = true;
}
}
return changed;
}
return set.retainAll(beans);
}
@Override
public int size() {
init();
return set.size();
}
@Override
public Object[] toArray() {
init();
return set.toArray();
}
@Override
public <T> T[] toArray(T[] array) {
init();
//noinspection SuspiciousToArrayCall
return set.toArray(array);
}
// -----------------------------------------------------//
// SequencedSet (Java 21+)
// -----------------------------------------------------//
@Override
public SequencedSet<E> reversed() {
init();
if (modifyListening) {
throw new UnsupportedOperationException("Not supported on modify listening set");
}
return set.reversed();
}
@Override
public void addFirst(E bean) {
init();
if (modifyListening) {
modifyAddition(bean);
}
set.addFirst(bean);
}
@Override
public void addLast(E bean) {
init();
if (modifyListening) {
modifyAddition(bean);
}
set.addLast(bean);
}
@Override
public E getFirst() {
init();
return set.getFirst();
}
@Override
public E getLast() {
init();
return set.getLast();
}
@Override
public E removeFirst() {
init();
if (modifyListening) {
E bean = set.removeFirst();
modifyRemoval(bean);
return bean;
}
return set.removeFirst();
}
@Override
public E removeLast() {
init();
if (modifyListening) {
E bean = set.removeLast();
modifyRemoval(bean);
return bean;
}
return set.removeLast();
}
}
@@ -136,13 +136,13 @@ class ToStringBuilderTest {
@Test
void beanSet_null_empty() {
assertThat(toStringFor(new BeanSet<String>(null))).isEqualTo("[]");
assertThat(toStringFor(new BeanSet<String>(Collections.emptySet()))).isEqualTo("[]");
assertThat(toStringFor(new BeanSet<String>(new LinkedHashSet<>()))).isEqualTo("[]");
}
@Test
void beanMap_null_empty() {
assertThat(toStringFor(new BeanMap<String, String>(null))).isEqualTo("{}");
assertThat(toStringFor(new BeanMap<String, String>(Collections.emptyMap()))).isEqualTo("{}");
assertThat(toStringFor(new BeanMap<String, String>(new LinkedHashMap<>()))).isEqualTo("{}");
}
@Test
@@ -159,7 +159,7 @@ class ToStringBuilderTest {
@Test
void beanMap_some() {
Map<String, Recurse> under = new LinkedHashMap<>();
var under = new LinkedHashMap<String, Recurse>();
under.put("a", new Recurse(1, "a"));
under.put("b", new Recurse(2, "b"));
BeanMap<String, Recurse> list = new BeanMap<>(under);
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-bench</artifactId>
+28 -28
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<name>ebean bom</name>
@@ -89,25 +89,25 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -125,13 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-core-json</artifactId>
<name>ebean-core-json</name>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.2.0</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
</dependency>
<dependency>
@@ -157,21 +157,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.2.0</version>
<scope>test</scope>
</dependency>
@@ -100,7 +100,9 @@ public final class LoadBeanRequest extends LoadRequest {
query.setLoadDescription(mode(), description());
if (lazy) {
query.setLazyLoadBatchSize(loadBuffer.batchSize());
if (alreadyLoaded) {
if (alreadyLoaded || !loadCache) {
// alreadyLoaded: bean is being re-loaded, skip the cache to avoid a stale hit
// !loadCache: parent context disabled cache (e.g. CacheMode.OFF or asOf query),
query.setBeanCacheMode(CacheMode.OFF);
}
} else {
@@ -12,6 +12,6 @@ public interface SpiDbQueryPlan extends MetaQueryPlan {
/**
* Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured.
*/
SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured);
SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId);
}
@@ -374,6 +374,8 @@ public interface SpiEbeanServer extends SpiServer, BeanCollectionLoader {
<T> int delete(SpiQuery<T> query);
<T> int deletePermanent(SpiQuery<T> query);
<T> int update(SpiQuery<T> query);
List<SqlRow> findList(SpiSqlQuery query);
@@ -104,6 +104,11 @@ public interface SpiTransaction extends Transaction {
*/
Boolean isUpdateAllLoadedProperties();
/**
* Return true if generated properties ({@code @WhenCreated} etc.) are enabled for this transaction.
*/
boolean isGeneratedPropertiesEnabled();
/**
* Return the batchSize specifically set for this transaction or 0.
* <p>
@@ -60,6 +60,6 @@ public interface SpiTransactionManager {
/**
* Return a connection used for query plan collection.
*/
Connection queryPlanConnection() throws SQLException;
Connection queryPlanConnection(Object tenantId) throws SQLException;
}
@@ -244,6 +244,16 @@ public abstract class SpiTransactionProxy implements SpiTransaction {
transaction.setUpdateAllLoadedProperties(updateAllLoaded);
}
@Override
public void setGeneratedPropertiesEnabled(boolean enable) {
transaction.setGeneratedPropertiesEnabled(enable);
}
@Override
public boolean isGeneratedPropertiesEnabled() {
return transaction.isGeneratedPropertiesEnabled();
}
@Override
public Boolean isUpdateAllLoadedProperties() {
return transaction.isUpdateAllLoadedProperties();
@@ -150,4 +150,45 @@ public final class TransactionEvent implements Serializable {
return changeSet;
}
public void merge(TransactionEvent other) {
if (other == null) {
return;
}
// Merge table events
if (other.eventTables != null) {
if (this.eventTables == null) {
this.eventTables = other.eventTables;
} else {
this.eventTables.add(other.eventTables);
}
}
// Merge listeners
if (other.listenerNotify != null) {
if (this.listenerNotify == null) {
this.listenerNotify = other.listenerNotify;
} else {
this.listenerNotify.addAll(other.listenerNotify);
}
}
// Merge delete-by-id
if (other.deleteByIdMap != null) {
if (this.deleteByIdMap == null) {
this.deleteByIdMap = other.deleteByIdMap;
} else {
this.deleteByIdMap.merge(other.deleteByIdMap);
}
}
// Merge cache changes
if (other.changeSet != null) {
if (this.changeSet == null) {
this.changeSet = other.changeSet;
} else {
this.changeSet.merge(other.changeSet);
}
}
}
}
@@ -42,4 +42,8 @@ final class CacheChangeBeanRemove implements CacheChange {
public void addId(Object id) {
ids.add(id);
}
void merge(CacheChangeBeanRemove other) {
this.ids.addAll(other.ids);
}
}
@@ -184,6 +184,32 @@ public final class CacheChangeSet {
return manyChangeMap.computeIfAbsent(key, ManyChange::new);
}
public void merge(CacheChangeSet other) {
if (other == null) {
return;
}
this.entries.addAll(other.entries);
this.touchedTables.addAll(other.touchedTables);
this.queryCaches.addAll(other.queryCaches);
this.beanCaches.addAll(other.beanCaches);
other.beanRemoveMap.forEach((desc, remove) ->
this.beanRemoveMap.merge(desc, remove, (a, b) -> {
a.merge(b);
return a;
})
);
other.manyChangeMap.forEach((key, change) ->
this.manyChangeMap.merge(key, change, (a, b) -> {
a.merge(b);
return a;
})
);
}
/**
* Changes for a specific many property.
*/
@@ -215,6 +241,32 @@ public final class CacheChangeSet {
}
}
void merge(ManyChange other) {
// clear dominates everything
if (other.clear) {
this.clear = true;
this.removes.clear();
this.puts.clear();
return;
}
if (this.clear) {
// already clearing, ignore finer changes
return;
}
// merge puts (put overrides remove)
this.puts.putAll(other.puts);
// merge removes, but do not remove something we just put
for (String key : other.removes) {
if (!this.puts.containsKey(key)) {
this.removes.add(key);
}
}
}
/**
* Put entry for the given parentId.
*/
@@ -13,8 +13,9 @@ import io.ebeaninternal.server.cluster.ClusterManager;
public final class CacheManagerOptions {
private final ClusterManager clusterManager;
private final DatabaseBuilder.Settings databaseBuilder;
private final String serverName;
private final boolean localL2Caching;
private final boolean tenantPartitionedCache;
private CurrentTenantProvider currentTenantProvider;
private QueryCacheEntryValidate queryCacheEntryValidate;
private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory();
@@ -24,7 +25,8 @@ public final class CacheManagerOptions {
CacheManagerOptions() {
this.localL2Caching = true;
this.clusterManager = null;
this.databaseBuilder = null;
this.serverName = "db";
this.tenantPartitionedCache = false;
this.cacheFactory = new DefaultServerCacheFactory();
this.beanDefault = new ServerCacheOptions();
this.queryDefault = new ServerCacheOptions();
@@ -32,9 +34,10 @@ public final class CacheManagerOptions {
public CacheManagerOptions(ClusterManager clusterManager, DatabaseBuilder.Settings config, boolean localL2Caching) {
this.clusterManager = clusterManager;
this.databaseBuilder = config;
this.serverName = config.getName();
this.localL2Caching = localL2Caching;
this.currentTenantProvider = config.getCurrentTenantProvider();
this.tenantPartitionedCache = config.isTenantPartitionedCache();
}
public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) {
@@ -55,7 +58,7 @@ public final class CacheManagerOptions {
}
public String getServerName() {
return (databaseBuilder == null) ? "db" : databaseBuilder.getName();
return serverName;
}
public boolean isLocalL2Caching() {
@@ -85,4 +88,8 @@ public final class CacheManagerOptions {
public QueryCacheEntryValidate getQueryCacheEntryValidate() {
return queryCacheEntryValidate;
}
public boolean isTenantPartitionedCache() {
return tenantPartitionedCache;
}
}
@@ -9,6 +9,7 @@ import io.ebean.config.CurrentTenantProvider;
import io.ebean.meta.MetricVisitor;
import io.ebean.util.AnnotationUtil;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
@@ -32,6 +33,7 @@ final class DefaultCacheHolder {
private final ServerCacheOptions queryDefault;
private final CurrentTenantProvider tenantProvider;
private final QueryCacheEntryValidate queryCacheEntryValidate;
private final boolean tenantPartitionedCache;
DefaultCacheHolder(CacheManagerOptions builder) {
this.cacheFactory = builder.getCacheFactory();
@@ -39,6 +41,7 @@ final class DefaultCacheHolder {
this.queryDefault = builder.getQueryDefault();
this.tenantProvider = builder.getCurrentTenantProvider();
this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate();
this.tenantPartitionedCache = builder.isTenantPartitionedCache();
}
void visitMetrics(MetricVisitor visitor) {
@@ -56,16 +59,31 @@ final class DefaultCacheHolder {
return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty);
}
private String tenantKey(String beanName) {
if (tenantPartitionedCache) {
return beanName + '.' + tenantProvider.currentId();
}
return beanName;
}
private String key(String beanName, ServerCacheType type) {
if (tenantPartitionedCache) {
return beanName + '.' + tenantProvider.currentId() + type.code();
}
return beanName + type.code();
}
private String key(String beanName, String collectionProperty, ServerCacheType type) {
if (collectionProperty != null) {
return beanName + "." + collectionProperty + type.code();
} else {
return beanName + type.code();
StringBuilder sb = new StringBuilder(beanName.length() + 64);
sb.append(beanName);
if (tenantPartitionedCache) {
sb.append('.').append(tenantProvider.currentId());
}
if (collectionProperty != null) {
sb.append('.').append(collectionProperty);
}
sb.append(type.code());
return sb.toString();
}
/**
@@ -82,12 +100,15 @@ final class DefaultCacheHolder {
if (type == ServerCacheType.COLLECTION_IDS) {
lock.lock();
try {
collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key);
collectIdCaches.computeIfAbsent(tenantKey(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key);
} finally {
lock.unlock();
}
}
return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate));
// in partitioned mode each ServerCache instance is already tenant-scoped via its key,
// so the tenantProvider is not needed inside the cache itself
CurrentTenantProvider cacheProvider = tenantPartitionedCache ? null : tenantProvider;
return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, cacheProvider, queryCacheEntryValidate));
}
void clearAll() {
@@ -100,17 +121,75 @@ final class DefaultCacheHolder {
public void clear(String name) {
log.log(DEBUG, "clear {0}", name);
clearIfExists(key(name, ServerCacheType.QUERY));
clearIfExists(key(name, ServerCacheType.BEAN));
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
Set<String> keys = collectIdCaches.get(name);
if (keys != null) {
for (String collectionIdKey : keys) {
clearIfExists(collectionIdKey);
if (tenantPartitionedCache) {
// In partitioned mode, tenantProvider.currentId() may be null/wrong on a background
// invalidation thread. Scan all cache entries for this entity type across all tenants.
clearAllTenantsFor(name);
} else {
clearIfExists(key(name, ServerCacheType.QUERY));
clearIfExists(key(name, ServerCacheType.BEAN));
clearIfExists(key(name, ServerCacheType.NATURAL_KEY));
Set<String> keys = collectIdCaches.get(name);
if (keys != null) {
for (String collectionIdKey : keys) {
clearIfExists(collectionIdKey);
}
}
}
}
private void clearAllTenantsFor(String name) {
// Keys in partitioned mode: beanName.tenantId_X or beanName.tenantId.prop_X
// collectIdCaches keys: beanName.tenantId
// Both start with beanName + '.' so we can use prefix scan.
String prefix = name + '.';
for (Map.Entry<String, ServerCache> entry : allCaches.entrySet()) {
if (entry.getKey().startsWith(prefix)) {
log.log(TRACE, "clear cache {0}", entry.getKey());
entry.getValue().clear();
}
}
lock.lock();
try {
for (Map.Entry<String, Set<String>> entry : collectIdCaches.entrySet()) {
if (entry.getKey().startsWith(prefix)) {
for (String collectionIdKey : entry.getValue()) {
clearIfExists(collectionIdKey);
}
}
}
} finally {
lock.unlock();
}
}
/**
* Remove all cache entries belonging to the given tenant.
* Call this when a tenant is deactivated to prevent unbounded memory growth.
*/
void clearTenant(Object tenantId) {
String tenantIdStr = String.valueOf(tenantId);
// Keys look like: beanName.tenantId_X or beanName.tenantId.prop_X
String segmentBeforeTypeCode = '.' + tenantIdStr + '_';
String segmentBeforeProperty = '.' + tenantIdStr + '.';
allCaches.entrySet().removeIf(e -> {
String k = e.getKey();
return k.contains(segmentBeforeTypeCode) || k.contains(segmentBeforeProperty);
});
// collectIdCaches keys look like: beanName.tenantId
String collectKeySuffix = '.' + tenantIdStr;
lock.lock();
try {
collectIdCaches.entrySet().removeIf(e -> e.getKey().endsWith(collectKeySuffix));
} finally {
lock.unlock();
}
}
boolean isTenantPartitionedCache() {
return tenantPartitionedCache;
}
private void clearIfExists(String fullKey) {
ServerCache cache = allCaches.get(fullKey);
if (cache != null) {
@@ -154,4 +154,14 @@ public final class DefaultServerCacheManager implements SpiCacheManager {
return cacheHolder.getCache(beanType, ServerCacheType.BEAN);
}
@Override
public boolean isTenantPartitionedCache() {
return cacheHolder.isTenantPartitionedCache();
}
@Override
public void clearTenant(Object tenantId) {
cacheHolder.clearTenant(tenantId);
}
}
@@ -88,4 +88,17 @@ public interface SpiCacheManager {
*/
void clearLocal(Class<?> beanType);
/**
* Returns true if this cache manager runs in tenant-partitioned mode.
* In this mode caches are namespaced per tenant to improve cache-hit ratio.
*/
boolean isTenantPartitionedCache();
/**
* Remove all cache entries belonging to the given tenant.
* Call this when a tenant is deactivated to prevent unbounded memory growth
* in tenant-partitioned mode.
*/
void clearTenant(Object tenantId);
}
@@ -1197,15 +1197,14 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
@Override
public <T> boolean exists(SpiQuery<T> ormQuery) {
SpiQuery<T> ormQueryCopy = ormQuery.copy();
ormQueryCopy.setMaxRows(1);
SpiOrmQueryRequest<?> request = createQueryRequest(Type.EXISTS, ormQueryCopy);
List<Object> ids = request.getFromQueryCache();
if (ids != null) {
return !ids.isEmpty();
Object cached = request.getFromQueryCache();
if (cached != null) {
return (Boolean) cached;
}
try {
request.initTransIfRequired();
return !request.findIds().isEmpty();
return request.findExists();
} finally {
request.endTransIfRequired();
}
@@ -1234,6 +1233,15 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
@Override
public <T> int delete(SpiQuery<T> query) {
return delete(query, false);
}
@Override
public <T> int deletePermanent(SpiQuery<T> query) {
return delete(query, true);
}
private <T> int delete(SpiQuery<T> query, boolean permanent) {
SpiOrmQueryRequest<T> request = createQueryRequest(Type.DELETE, query);
try {
request.initTransIfRequired();
@@ -1247,7 +1255,7 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer {
if (ids.isEmpty()) {
return 0;
} else {
return persister.deleteByIds(request.descriptor(), ids, request.transaction(), false);
return persister.deleteByIds(request.descriptor(), ids, request.transaction(), permanent);
}
}
} finally {
@@ -81,6 +81,7 @@ public final class InternalConfiguration {
private final Binder binder;
private final DeployCreateProperties deployCreateProperties;
private final DeployUtil deployUtil;
private final DataSourceSupplier dataSourceSupplier;
private final BeanDescriptorManager beanDescriptorManager;
private final CQueryEngine cQueryEngine;
private final ClusterManager clusterManager;
@@ -124,6 +125,7 @@ public final class InternalConfiguration {
final InternalConfigXmlMap xmlMap = initExternalMapping();
this.dtoBeanManager = new DtoBeanManager(typeManager, xmlMap.readDtoMapping());
this.dataSourceSupplier = createDataSourceSupplier();
this.beanDescriptorManager = new BeanDescriptorManager(this);
Map<String, String> asOfTableMapping = beanDescriptorManager.deploy(xmlMap.xmlDeployment());
Map<String, String> draftTableMap = beanDescriptorManager.draftTableMap();
@@ -391,7 +393,7 @@ public final class InternalConfiguration {
TransactionManagerOptions options =
new TransactionManagerOptions(server, notifyL2CacheInForeground, config, scopeManager, clusterManager, backgroundExecutor,
indexUpdateProcessor, beanDescriptorManager, dataSource(), profileHandler(), logManager,
indexUpdateProcessor, beanDescriptorManager, dataSourceSupplier, profileHandler(), logManager,
tableModState, cacheNotify);
if (config.isDocStoreOnly()) {
@@ -409,9 +411,16 @@ public final class InternalConfiguration {
}
/**
* Return the DataSource supplier based on the tenancy mode.
* Return the DataSource supplier (multi-tenant aware) based on the tenancy mode.
*/
private DataSourceSupplier dataSource() {
public DataSourceSupplier getDataSourceSupplier() {
return dataSourceSupplier;
}
/**
* Create the DataSource supplier based on the tenancy mode.
*/
private DataSourceSupplier createDataSourceSupplier() {
switch (config.getTenantMode()) {
case DB:
case DB_WITH_MASTER:
@@ -580,7 +589,8 @@ public final class InternalConfiguration {
return QueryPlanManager.NOOP;
}
long threshold = config.getQueryPlanThresholdMicros();
return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform(), config), extraMetrics);
return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(),
threshold, queryPlanLogger(databasePlatform.platform(), config), extraMetrics);
}
/**
@@ -49,6 +49,11 @@ public interface OrmQueryEngine {
*/
<T> int findCount(OrmQueryRequest<T> request);
/**
* Execute the exists query using SELECT EXISTS(...).
*/
<T> boolean findExists(OrmQueryRequest<T> request);
/**
* Execute the find id's query.
*/
@@ -42,7 +42,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
private SpiQuerySecondary secondaryQueries;
private List<T> cacheBeans;
private boolean inlineCountDistinct;
private Set<String> dependentTables;
private SpiQueryManyJoin manyJoin;
public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery<T> query, SpiTransaction t) {
@@ -128,6 +127,13 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
}
}
/**
* Return the secondary queries (fetchQuery and lazy joins) extracted from the query detail.
*/
public SpiQuerySecondary secondaryQueries() {
return secondaryQueries;
}
/**
* For use with QueryIterator and secondary queries this returns the minimum
* batch size that should be loaded before executing the secondary queries.
@@ -356,6 +362,11 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
return queryEngine.findCount(this);
}
@Override
public boolean findExists() {
return queryEngine.findExists(this);
}
@Override
public <A> List<A> findIds() {
return queryEngine.findIds(this);
@@ -667,7 +678,11 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
}
public void putToQueryCache(Object result) {
beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.startTime()));
CQueryPlan plan = queryPlan();
if (plan != null) {
// only cache when we have the plan's dependent tables
beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, plan.dependentTables(), transaction.startTime()));
}
}
/**
@@ -737,15 +752,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
return inlineCountDistinct;
}
public void addDependentTables(Set<String> tables) {
if (tables != null && !tables.isEmpty()) {
if (dependentTables == null) {
dependentTables = new LinkedHashSet<>();
}
dependentTables.addAll(tables);
}
}
/**
* Return true if no MaxRows or use LIMIT in SQL update.
*/
@@ -56,6 +56,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private Object idValue;
private boolean statelessUpdate;
private boolean notifyCache;
/**
* Snapshot of origValues taken before intercept.setLoaded() clears them.
* Used so that cache update code can access old FK values after setLoaded().
*/
private Object[] capturedOrigValues;
/**
* Flag used to detect when only many properties where updated via a cascade. Used to ensure
* appropriate caches are updated in that case.
@@ -122,6 +127,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
*/
private List<SaveMany> saveMany;
private InsertOptions insertOptions;
/**
* Set true when an ON CONFLICT NOTHING insert is skipped (0 rows affected).
* Cascade to children must be suppressed to avoid FK violations.
*/
private boolean insertConflictSkipped;
public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager<T> mgr, SpiTransaction t,
PersistExecute persistExecute, PersistRequest.Type type, int flags) {
@@ -262,13 +272,13 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
GeneratedProperty generatedProperty = prop.generatedProperty();
if (prop.isVersion()) {
if (isLoadedProperty(prop)) {
// @Version property must be loaded to be involved
// @Version property must be loaded to be involved — always auto-incremented
Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
Object oldVal = prop.getValue(entityBean);
setVersionValue(value);
intercept.setOldValue(prop.propertyIndex(), oldVal);
}
} else {
} else if (transaction == null || transaction.isGeneratedPropertiesEnabled()) {
// @WhenModified set without invoking interception
Object oldVal = prop.getValue(entityBean);
Object value = generatedProperty.getUpdateValue(prop, entityBean, now());
@@ -280,17 +290,22 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
private void onFailedUpdateUndoGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) {
Object oldVal = intercept.origValue(prop.propertyIndex());
if (oldVal != null) {
prop.setValue(entityBean, oldVal);
if (prop.isVersion() || transaction == null || transaction.isGeneratedPropertiesEnabled()) {
// undo version always (it was always set); undo others only if they were set
Object oldVal = intercept.origValue(prop.propertyIndex());
if (oldVal != null) {
prop.setValue(entityBean, oldVal);
}
}
}
}
private void onInsertGeneratedProperties() {
for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) {
Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
prop.setValueChanged(entityBean, value);
if (prop.isVersion() || transaction == null || transaction.isGeneratedPropertiesEnabled() || prop.getValue(entityBean) == null) {
Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now());
prop.setValueChanged(entityBean, value);
}
}
}
@@ -705,7 +720,11 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
* Return the original / old value for the given property.
*/
public Object origValue(BeanProperty prop) {
return intercept.origValue(prop.propertyIndex());
int idx = prop.propertyIndex();
if (capturedOrigValues != null && idx < capturedOrigValues.length) {
return capturedOrigValues[idx];
}
return intercept.origValue(idx);
}
@Override
@@ -796,6 +815,9 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
throw new OptimisticLockException("Data has changed. updated row count " + rowCount, null, bean);
} else if (rowCount == 0 && type == Type.UPDATE) {
throw new EntityNotFoundException("No rows updated");
} else if (rowCount == 0 && type == Type.INSERT && isConflictNothingInsert()) {
insertConflictSkipped = true;
return;
}
}
switch (type) {
@@ -810,6 +832,14 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
}
private boolean isConflictNothingInsert() {
return insertOptions != null && insertOptions.key().charAt(0) == 'N';
}
public boolean isInsertConflictSkipped() {
return insertConflictSkipped;
}
/**
* Clear the bean from the PersistenceContext (L1 cache) for stateless updates.
*/
@@ -862,6 +892,15 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
if (type == Type.UPDATE) {
// get the dirty properties for notify cache & orphanRemoval of vanilla collection detection
dirtyProperties = intercept.dirtyProperties();
// snapshot orig values before setLoaded() clears them - needed by cache FK-eviction logic
if (dirtyProperties != null && notifyCache) {
capturedOrigValues = new Object[dirtyProperties.length];
for (int i = 0; i < dirtyProperties.length; i++) {
if (dirtyProperties[i]) {
capturedOrigValues[i] = intercept.origValue(i);
}
}
}
}
if (isChangeLog) {
changeLog();
@@ -898,6 +937,20 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
}
/**
* Ensure the preUpdate event fires (for case where only a ManyToMany collection has changed).
*/
public void preManyToManyUpdate() {
if (controller != null && !dirty) {
// fire preUpdate notification when only ManyToMany intersection updated
controller.preUpdate(this);
pendingPostUpdateNotify = true;
}
if (!dirty) {
setNotifyCache();
}
}
public boolean isNotifyCache() {
return notifyCache;
}
@@ -1410,7 +1463,12 @@ public final class PersistRequestBean<T> extends PersistRequest implements BeanP
}
public void setInsertOptions(InsertOptions insertOptions) {
this.insertOptions = insertOptions;
this.insertOptions = insertOptions;
if (insertOptions != null && insertOptions.key().charAt(0) == 'N' && beanDescriptor.hasCascadeChildren()) {
// force immediate (non-batch) execution of this insert so we know whether it
// was actually inserted before attempting cascade saves on children.
skipBatchForTopLevel = true;
}
}
public InsertOptions insertOptions() {
@@ -75,6 +75,11 @@ public interface SpiOrmQueryRequest<T> extends BeanQueryRequest<T>, DocQueryRequ
*/
int findCount();
/**
* Execute the find exists query using select exists(...).
*/
boolean findExists();
/**
* Execute the find ids query.
*/
@@ -10,8 +10,11 @@ import java.sql.SQLException;
*/
final class AssocOneHelpEmbedded extends AssocOneHelp {
AssocOneHelpEmbedded(BeanPropertyAssocOne<?> property) {
private final boolean allowEmpty;
AssocOneHelpEmbedded(BeanPropertyAssocOne<?> property, boolean allowEmpty) {
super(property);
this.allowEmpty = allowEmpty;
}
@Override
@@ -40,7 +43,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp {
notNull = true;
}
}
if (notNull) {
if (notNull || allowEmpty) {
return embeddedBean;
} else {
return null;
@@ -69,7 +72,7 @@ final class AssocOneHelpEmbedded extends AssocOneHelp {
notNull = true;
}
}
return notNull ? embeddedBean : null;
return (notNull || allowEmpty) ? embeddedBean : null;
}
@Override
@@ -1,12 +1,9 @@
package io.ebeaninternal.server.deploy;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.BeanCollectionLoader;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.json.SpiJsonWriter;
import io.ebeaninternal.server.query.CQueryCollectionAdd;
@@ -34,7 +31,7 @@ public interface BeanCollectionHelp<T> extends CQueryCollectionAdd<T> {
* For Map's this needs to take the mapKey.
* </p>
*/
BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey);
BeanCollectionAdd collectionAdd(Object bc, String mapKey);
/**
* Create an empty collection of the correct type without a parent bean.
@@ -320,7 +320,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
this.idOnlyReference = isIdOnlyReference(propertiesBaseScalar);
boolean noRelationships = propertiesOne.length + propertiesMany.length == 0;
this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly();
this.cacheHelp = new BeanDescriptorCacheHelp<>(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
this.cacheHelp = BeanDescriptorCacheHelp.create(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported);
this.jsonHelp = initJsonHelp();
this.draftHelp = new BeanDescriptorDraftHelp<>(this);
this.docStoreAdapter = owner.createDocStoreBeanAdapter(this, deploy);
@@ -556,6 +556,23 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
}
}
/**
* Parse @Formula2 logical expressions into placeholder-form SQL after all
* relationships have been wired up and elPropertyDeploy() is fully functional.
* Called from BeanDescriptorManager in a dedicated pass after all descriptors
* have been fully initialised, so cross-descriptor paths are safe to navigate.
*/
void initFormula2Properties() {
for (BeanProperty prop : propertiesAll()) {
String rawExpr = prop.formula2RawExpression();
if (rawExpr != null) {
DeployPropertyParser parser = parser().setCatchFirst(true);
String parsed = parser.parse(rawExpr);
prop.initFormula2(parsed, parser.includes());
}
}
}
private boolean hasCircularImportedId() {
for (BeanPropertyAssocOne<?> assocOne : propertiesOneImportedSave) {
if (assocOne.hasCircularImportedId(this)) {
@@ -2385,6 +2402,12 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
if (assocProp == null) {
return null;
}
// this method is an entry-point, although it introduces recursive calls via
// buildElPropertyValue -> createElPropertyValue -> buildElGetValue (back to here)
// it seems we can initialize ElPropertyChainBuilder at this point and skip further checks.
if (chain == null) {
chain = new ElPropertyChainBuilder(propName);
}
String remainder = propName.substring(basePos + 1);
return assocProp.buildElPropertyValue(propName, remainder, chain, propertyDeploy);
}
@@ -2396,9 +2419,7 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
if (property == null) {
throw new PersistenceException("No property found for [" + propName + "] in expression " + chain.expression());
}
if (property.containsMany()) {
chain.setContainsMany();
}
return chain.add(property).build();
}
@@ -3320,6 +3341,15 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
return propertiesManySave;
}
/**
* Return true if this bean has cascade-save children (OneToMany or exported OneToOne)
* that hold a FK back to this bean. Used to decide whether an ON CONFLICT NOTHING
* insert must be executed immediately so the row-count is known before cascading.
*/
public boolean hasCascadeChildren() {
return propertiesManySave.length > 0 || propertiesOneExportedSave.length > 0;
}
/**
* Assoc Many's with delete cascade.
*/
@@ -27,7 +27,7 @@ import static java.lang.System.Logger.Level.*;
*
* @param <T> The entity bean type
*/
final class BeanDescriptorCacheHelp<T> {
abstract class BeanDescriptorCacheHelp<T> {
private static final System.Logger log = CoreLog.internal;
@@ -36,7 +36,7 @@ final class BeanDescriptorCacheHelp<T> {
private static final System.Logger manyLog = AppLog.getLogger("io.ebean.cache.COLL");
private static final System.Logger natLog = AppLog.getLogger("io.ebean.cache.NATKEY");
private final BeanDescriptor<T> desc;
final BeanDescriptor<T> desc;
private final SpiCacheManager cacheManager;
private final CacheOptions cacheOptions;
/**
@@ -44,13 +44,10 @@ final class BeanDescriptorCacheHelp<T> {
*/
private final boolean cacheSharableBeans;
private final boolean invalidateQueryCache;
private final Class<?> beanType;
final Class<?> beanType;
private final String cacheName;
private final BeanPropertyAssocOne<?>[] propertiesOneImported;
private final String[] naturalKey;
private final ServerCache beanCache;
private final ServerCache naturalKeyCache;
private final ServerCache queryCache;
private final boolean noCaching;
private final SpiCacheControl cacheControl;
private final SpiCacheRegion cacheRegion;
@@ -63,6 +60,21 @@ final class BeanDescriptorCacheHelp<T> {
* Set to true if delete changes need to notify cache.
*/
private boolean cacheNotifyOnDelete;
/**
* Set to true if this bean type has owning OneToOne properties pointing to cached targets.
* When true, all persist events must evict the target bean caches regardless of whether
* this bean type itself has a bean cache (bypasses the cacheRegion.isEnabled() gate).
*/
private boolean cacheNotifyOneToOneOwner;
static <T> BeanDescriptorCacheHelp<T> create(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
if ((cacheOptions.isEnableQueryCache() || cacheOptions.isEnableBeanCache()) && cacheManager.isTenantPartitionedCache()) {
return new BeanDescriptorCacheHelpPartitioned<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
} else {
return new BeanDescriptorCacheHelpFixed<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
}
}
BeanDescriptorCacheHelp(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
@@ -75,48 +87,54 @@ final class BeanDescriptorCacheHelp<T> {
this.cacheSharableBeans = cacheSharableBeans;
this.propertiesOneImported = propertiesOneImported;
this.naturalKey = cacheOptions.getNaturalKey();
if (!cacheOptions.isEnableQueryCache()) {
this.queryCache = null;
} else {
this.queryCache = cacheManager.getQueryCache(beanType);
}
if (cacheOptions.isEnableBeanCache()) {
this.beanCache = cacheManager.getBeanCache(beanType);
if (cacheOptions.getNaturalKey() != null) {
this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType);
} else {
this.naturalKeyCache = null;
}
} else {
this.beanCache = null;
this.naturalKeyCache = null;
}
this.noCaching = (beanCache == null && queryCache == null);
this.noCaching = !cacheOptions.isEnableQueryCache() && !cacheOptions.isEnableBeanCache();
if (noCaching) {
this.cacheControl = DCacheControlNone.INSTANCE;
this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE;
} else {
this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion());
this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null));
this.cacheControl = new DCacheControl(cacheRegion,
cacheOptions.isEnableBeanCache(),
cacheOptions.isEnableBeanCache() && cacheOptions.getNaturalKey() != null,
cacheOptions.isEnableQueryCache());
}
}
abstract boolean hasBeanCache();
abstract boolean hasQueryCache();
abstract ServerCache queryCache();
abstract ServerCache naturalKeyCache();
abstract ServerCache beanCache();
/**
* Derive the cache notify flags.
*/
void deriveNotifyFlags() {
cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null);
cacheNotifyOnAll = (invalidateQueryCache || hasBeanCache() || hasQueryCache());
cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes();
cacheNotifyOneToOneOwner = hasOwningOneToOneWithCachedTarget();
if (log.isLoggable(DEBUG)) {
if (cacheNotifyOnAll || cacheNotifyOnDelete) {
String notifyMode = cacheNotifyOnAll ? "All" : "Delete";
if (cacheNotifyOnAll || cacheNotifyOnDelete || cacheNotifyOneToOneOwner) {
String notifyMode = cacheNotifyOnAll ? "All" : (cacheNotifyOnDelete ? "Delete" : "OneToOneOwner");
log.log(DEBUG, "l2 caching on {0} - beanCaching:{1} queryCaching:{2} notifyMode:{3} ",
desc.fullName(), isBeanCaching(), isQueryCaching(), notifyMode);
}
}
}
private boolean hasOwningOneToOneWithCachedTarget() {
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
if (imported.isCacheNotifyOwningOneToOne()) {
return true;
}
}
return false;
}
/**
* Return true if there is an imported bi-directional relationship to a bea
* that does have bean caching enabled.
@@ -137,6 +155,9 @@ final class BeanDescriptorCacheHelp<T> {
if (hasImmutableCaches()) {
return true;
}
if (cacheNotifyOneToOneOwner) {
return true;
}
return cacheRegion.isEnabled()
&& (cacheNotifyOnAll || cacheNotifyOnDelete && (type == PersistRequest.Type.DELETE || type == PersistRequest.Type.DELETE_PERMANENT));
}
@@ -184,11 +205,11 @@ final class BeanDescriptorCacheHelp<T> {
* Clear the query cache.
*/
void queryCacheClear() {
if (queryCache != null) {
if (hasQueryCache()) {
if (queryLog.isLoggable(DEBUG)) {
queryLog.log(DEBUG, " CLEAR {0}", cacheName);
}
queryCache.clear();
queryCache().clear();
}
}
@@ -196,7 +217,7 @@ final class BeanDescriptorCacheHelp<T> {
* Add query cache clear to the changeSet.
*/
private void queryCacheClear(CacheChangeSet changeSet) {
if (queryCache != null) {
if (hasQueryCache()) {
changeSet.addClearQuery(desc);
}
}
@@ -205,10 +226,10 @@ final class BeanDescriptorCacheHelp<T> {
* Get a query result from the query cache.
*/
Object queryCacheGet(Object id) {
if (queryCache == null) {
if (!hasQueryCache()) {
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
}
Object queryResult = queryCache.get(id);
Object queryResult = queryCache().get(id);
if (queryLog.isLoggable(DEBUG)) {
if (queryResult == null) {
queryLog.log(DEBUG, " GET {0}({1}) - cache miss", cacheName, id);
@@ -223,13 +244,13 @@ final class BeanDescriptorCacheHelp<T> {
* Put a query result into the query cache.
*/
void queryCachePut(Object id, QueryCacheEntry entry) {
if (queryCache == null) {
if (!hasQueryCache()) {
throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)");
}
if (queryLog.isLoggable(DEBUG)) {
queryLog.log(DEBUG, " PUT {0}({1})", cacheName, id);
}
queryCache.put(id, entry);
queryCache().put(id, entry);
}
void manyPropRemove(String propertyName, String parentKey) {
@@ -300,7 +321,7 @@ final class BeanDescriptorCacheHelp<T> {
*/
void manyPropPut(BeanPropertyAssocMany<?> many, Object details, String parentKey) {
if (many.isElementCollection()) {
CachedBeanData data = (CachedBeanData) beanCache.get(parentKey);
CachedBeanData data = (CachedBeanData) beanCache().get(parentKey);
if (data != null) {
try {
// add as JSON to bean cache
@@ -312,7 +333,7 @@ final class BeanDescriptorCacheHelp<T> {
if (beanLog.isLoggable(DEBUG)) {
beanLog.log(DEBUG, " UPDATE {0}({1}) changes:{2}", cacheName, parentKey, changes);
}
beanCache.put(parentKey, newData);
beanCache().put(parentKey, newData);
} catch (IOException e) {
log.log(ERROR, "Error updating L2 cache", e);
}
@@ -358,7 +379,7 @@ final class BeanDescriptorCacheHelp<T> {
if (ids.isEmpty()) {
return new BeanCacheResult<>();
}
Map<Object, Object> beanDataMap = beanCache.getAll(keys);
Map<Object, Object> beanDataMap = beanCache().getAll(keys);
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
}
@@ -380,7 +401,7 @@ final class BeanDescriptorCacheHelp<T> {
}
// naturalKey -> Id map
Map<Object, Object> naturalKeyMap = naturalKeyCache.getAll(keys);
Map<Object, Object> naturalKeyMap = naturalKeyCache().getAll(keys);
if (natLog.isLoggable(TRACE)) {
natLog.log(TRACE, " MLOOKUP {0}({1}) - hits:{2}", cacheName, keys, naturalKeyMap);
}
@@ -397,7 +418,7 @@ final class BeanDescriptorCacheHelp<T> {
}
Set<Object> ids = new HashSet<>(naturalKeyMap.values());
Map<Object, Object> beanDataMap = beanCache.getAll(ids);
Map<Object, Object> beanDataMap = beanCache().getAll(ids);
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet());
}
@@ -428,25 +449,15 @@ final class BeanDescriptorCacheHelp<T> {
desc.contextPut(context, id, bean);
}
/**
* Return the beanCache creating it if necessary.
*/
private ServerCache getBeanCache() {
if (beanCache == null) {
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
}
return beanCache;
}
/**
* Clear the bean cache.
*/
void beanCacheClear() {
if (beanCache != null) {
if (hasBeanCache()) {
if (beanLog.isLoggable(DEBUG)) {
beanLog.log(DEBUG, " CLEAR {0}", cacheName);
}
beanCache.clear();
beanCache().clear();
}
}
@@ -516,13 +527,13 @@ final class BeanDescriptorCacheHelp<T> {
if (beanLog.isLoggable(DEBUG)) {
beanLog.log(DEBUG, " MPUT {0}({1})", cacheName, map.keySet());
}
getBeanCache().putAll(map);
beanCache().putAll(map);
if (natKeys != null && !natKeys.isEmpty()) {
if (natLog.isLoggable(DEBUG)) {
natLog.log(DEBUG, " MPUT {0}({1}, {2})", cacheName, Arrays.toString(naturalKey), natKeys.keySet());
}
naturalKeyCache.putAll(natKeys);
naturalKeyCache().putAll(natKeys);
}
}
@@ -535,14 +546,14 @@ final class BeanDescriptorCacheHelp<T> {
if (beanLog.isLoggable(DEBUG)) {
beanLog.log(DEBUG, " PUT {0}({1}) data:{2}", cacheName, key, beanData);
}
getBeanCache().put(key, beanData);
beanCache().put(key, beanData);
if (naturalKey != null) {
String naturalKey = calculateNaturalKey(beanData);
if (naturalKey != null) {
if (natLog.isLoggable(DEBUG)) {
natLog.log(DEBUG, " PUT {0}({1}, {2})", cacheName, naturalKey, key);
}
naturalKeyCache.put(naturalKey, key);
naturalKeyCache().put(naturalKey, key);
}
}
}
@@ -564,7 +575,7 @@ final class BeanDescriptorCacheHelp<T> {
}
CachedBeanData beanCacheGetData(String key) {
return (CachedBeanData) getBeanCache().get(key);
return (CachedBeanData) beanCache().get(key);
}
T beanCacheGet(String key, boolean unmodifiable, PersistenceContext context) {
@@ -579,7 +590,7 @@ final class BeanDescriptorCacheHelp<T> {
* Return a bean from the bean cache.
*/
private T beanCacheGetInternal(String key, boolean unmodifiable, PersistenceContext context) {
CachedBeanData data = (CachedBeanData) getBeanCache().get(key);
CachedBeanData data = (CachedBeanData) beanCache().get(key);
if (data == null) {
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " GET {0}({1}) - cache miss", cacheName, key);
@@ -633,14 +644,17 @@ final class BeanDescriptorCacheHelp<T> {
*/
EntityBean loadBeanDirect(Object id, boolean unmodifiable, CachedBeanData data, PersistenceContext context) {
id = desc.convertId(id);
EntityBean bean = context == null ? null : (EntityBean) desc.contextGet(context, id);;
EntityBean bean = context == null ? null : (EntityBean) desc.contextGet(context, id);
if (bean == null) {
bean = desc.createEntityBean2(unmodifiable);
desc.setId(id, bean);
if (context == null) {
// a context is required to resolve @ManyToOne references when converting
// the cached data to the bean - even for unmodifiable beans (which are
// not themselves registered in the persistence context)
context = new DefaultPersistenceContext();
}
if (!unmodifiable) {
if (context == null) {
context = new DefaultPersistenceContext();
}
desc.contextPut(context, id, bean);
EntityBeanIntercept ebi = bean._ebean_getIntercept();
ebi.setPersistenceContext(context);
@@ -681,11 +695,11 @@ final class BeanDescriptorCacheHelp<T> {
* Remove a bean from the cache given its Id.
*/
void beanCacheApplyInvalidate(Collection<String> keys) {
if (beanCache != null) {
if (hasBeanCache()) {
if (beanLog.isLoggable(DEBUG)) {
beanLog.log(DEBUG, " MREMOVE {0}({1})", cacheName, keys);
}
beanCache.removeAll(new HashSet<>(keys));
beanCache().removeAll(new HashSet<>(keys));
}
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
imported.cacheClear();
@@ -701,7 +715,7 @@ final class BeanDescriptorCacheHelp<T> {
ebis.put(desc.cacheKeyForBean(ebi.owner()), ebi);
}
Map<Object, Object> hits = getBeanCache().getAll(ebis.keySet());
Map<Object, Object> hits = beanCache().getAll(ebis.keySet());
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " MLOAD {0}({1}) - got hits ({2})", cacheName, ebis.keySet(), hits.size());
}
@@ -734,7 +748,7 @@ final class BeanDescriptorCacheHelp<T> {
* Returns true if it managed to populate/load the single bean from the cache.
*/
boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, String key, PersistenceContext context) {
CachedBeanData cacheData = (CachedBeanData) getBeanCache().get(key);
CachedBeanData cacheData = (CachedBeanData) beanCache().get(key);
if (cacheData == null) {
if (beanLog.isLoggable(TRACE)) {
beanLog.log(TRACE, " LOAD {0}({1}) - cache miss", cacheName, key);
@@ -756,7 +770,7 @@ final class BeanDescriptorCacheHelp<T> {
}
void cacheUpdateQuery(boolean update, SpiTransaction transaction) {
if (invalidateQueryCache || cacheNotifyOnAll || (!update && cacheNotifyOnDelete)) {
if (invalidateQueryCache || cacheNotifyOnAll || cacheNotifyOneToOneOwner || (!update && cacheNotifyOnDelete)) {
transaction.event().add(desc.baseTable(), false, update, !update);
}
}
@@ -772,10 +786,11 @@ final class BeanDescriptorCacheHelp<T> {
changeSet.addInvalidate(desc);
} else {
queryCacheClear(changeSet);
if (beanCache != null) {
if (hasBeanCache()) {
changeSet.addBeanRemoveMany(desc, ids);
}
cacheDeleteImported(true, null, changeSet);
cacheClearImportedOneToOne(changeSet);
}
}
@@ -790,10 +805,11 @@ final class BeanDescriptorCacheHelp<T> {
changeSet.addInvalidate(desc);
} else {
queryCacheClear(changeSet);
if (beanCache != null) {
if (hasBeanCache()) {
changeSet.addBeanRemove(desc, id);
}
cacheDeleteImported(true, deleteRequest.entityBean(), changeSet);
cacheDeleteImportedOneToOne(deleteRequest.entityBean(), changeSet);
}
}
@@ -806,6 +822,7 @@ final class BeanDescriptorCacheHelp<T> {
} else {
queryCacheClear(changeSet);
cacheDeleteImported(false, insertRequest.entityBean(), changeSet);
cacheDeleteImportedOneToOne(insertRequest.entityBean(), changeSet);
changeSet.addBeanInsert(desc.baseTable());
}
}
@@ -816,6 +833,42 @@ final class BeanDescriptorCacheHelp<T> {
}
}
/**
* Evict the target bean cache for each owning OneToOne property on insert or delete.
*/
private void cacheDeleteImportedOneToOne(EntityBean entityBean, CacheChangeSet changeSet) {
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
imported.cacheDeleteOneToOneOwned(entityBean, changeSet);
}
}
/**
* Clear the target bean cache for each owning OneToOne property on bulk delete-by-ids.
*/
private void cacheClearImportedOneToOne(CacheChangeSet changeSet) {
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
imported.cacheClearOneToOneOwned(changeSet);
}
}
/**
* Evict inverse caches for any imported FK properties whose value changed in this update.
* Handles collection-ids caches (ManyToOne FK reassignment) and bean caches (owning OneToOne FK change).
*/
private void cacheUpdateImportedFKs(PersistRequestBean<T> updateRequest, CacheChangeSet changeSet) {
boolean[] dirty = updateRequest.dirtyProperties();
if (dirty == null) {
return;
}
EntityBean entityBean = updateRequest.entityBean();
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
int propIdx = imported.propertyIndex();
if (propIdx < dirty.length && dirty[propIdx]) {
imported.cacheUpdateFKchange(entityBean, updateRequest.origValue(imported), changeSet);
}
}
}
/**
* Add appropriate changes to support update.
*/
@@ -828,7 +881,8 @@ final class BeanDescriptorCacheHelp<T> {
} else {
queryCacheClear(changeSet);
if (beanCache == null) {
cacheUpdateImportedFKs(updateRequest, changeSet);
if (!hasBeanCache()) {
// query caching only
return;
}
@@ -858,15 +912,17 @@ final class BeanDescriptorCacheHelp<T> {
changeSet.addInvalidate(desc);
return;
}
if (noCaching) {
if (noCaching && !cacheNotifyOneToOneOwner) {
return;
}
changeSet.addClearQuery(desc);
// inserts don't invalidate the bean cache
if (tableIUD.isUpdateOrDelete()) {
changeSet.addClearBean(desc);
if (!noCaching) {
changeSet.addClearQuery(desc);
// inserts don't invalidate the bean cache
if (tableIUD.isUpdateOrDelete()) {
changeSet.addClearBean(desc);
}
}
// any change invalidates the collection IDs cache
// any change invalidates the collection IDs cache and owning OneToOne target bean caches
for (BeanPropertyAssocOne<?> imported : propertiesOneImported) {
imported.cacheClear(changeSet);
}
@@ -874,7 +930,7 @@ final class BeanDescriptorCacheHelp<T> {
void cacheNaturalKeyPut(String key, String newKey) {
if (newKey != null) {
naturalKeyCache.put(newKey, key);
naturalKeyCache().put(newKey, key);
}
}
@@ -882,7 +938,7 @@ final class BeanDescriptorCacheHelp<T> {
* Apply changes to the bean cache entry.
*/
void cacheBeanUpdate(String key, Map<String, Object> changes, boolean updateNaturalKey, long version) {
ServerCache cache = getBeanCache();
ServerCache cache = beanCache();
CachedBeanData existingData = (CachedBeanData) cache.get(key);
if (existingData != null) {
long currentVersion = existingData.getVersion();
@@ -907,7 +963,7 @@ final class BeanDescriptorCacheHelp<T> {
if (natLog.isLoggable(DEBUG)) {
natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key);
}
naturalKeyCache.remove(oldKey);
naturalKeyCache().remove(oldKey);
}
}
}
@@ -0,0 +1,63 @@
package io.ebeaninternal.server.deploy;
import io.ebean.cache.ServerCache;
import io.ebeaninternal.server.cache.SpiCacheManager;
import io.ebeaninternal.server.core.CacheOptions;
/**
* BeanDescriptorCacheHelp implementation for non-tenant-partitioned (fixed) caches.
* Cache instances are obtained once at construction time and reused for all requests.
*
* @param <T> The entity bean type
*/
final class BeanDescriptorCacheHelpFixed<T> extends BeanDescriptorCacheHelp<T> {
private final ServerCache beanCache;
private final ServerCache naturalKeyCache;
private final ServerCache queryCache;
BeanDescriptorCacheHelpFixed(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
if (!cacheOptions.isEnableQueryCache()) {
this.queryCache = null;
} else {
this.queryCache = cacheManager.getQueryCache(beanType);
}
if (cacheOptions.isEnableBeanCache()) {
this.beanCache = cacheManager.getBeanCache(beanType);
this.naturalKeyCache = (cacheOptions.getNaturalKey() != null) ? cacheManager.getNaturalKeyCache(beanType) : null;
} else {
this.beanCache = null;
this.naturalKeyCache = null;
}
}
@Override
boolean hasBeanCache() {
return beanCache != null;
}
@Override
boolean hasQueryCache() {
return queryCache != null;
}
@Override
ServerCache queryCache() {
return queryCache;
}
@Override
ServerCache naturalKeyCache() {
return naturalKeyCache;
}
@Override
ServerCache beanCache() {
if (beanCache == null) {
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
}
return beanCache;
}
}
@@ -0,0 +1,62 @@
package io.ebeaninternal.server.deploy;
import io.ebean.cache.ServerCache;
import io.ebeaninternal.server.cache.SpiCacheManager;
import io.ebeaninternal.server.core.CacheOptions;
import java.util.function.Supplier;
/**
* BeanDescriptorCacheHelp implementation for tenant-partitioned caches.
* Cache instances are looked up on every access via the cacheManager (which resolves the
* correct tenant-namespaced cache using the current tenant id from the tenant provider).
*
* @param <T> The entity bean type
*/
final class BeanDescriptorCacheHelpPartitioned<T> extends BeanDescriptorCacheHelp<T> {
private final Supplier<ServerCache> beanCacheSupplier;
private final Supplier<ServerCache> naturalKeyCacheSupplier;
private final Supplier<ServerCache> queryCacheSupplier;
BeanDescriptorCacheHelpPartitioned(BeanDescriptor<T> desc, SpiCacheManager cacheManager, CacheOptions cacheOptions,
boolean cacheSharableBeans, BeanPropertyAssocOne<?>[] propertiesOneImported) {
super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported);
this.queryCacheSupplier = cacheOptions.isEnableQueryCache() ? () -> cacheManager.getQueryCache(beanType) : null;
if (cacheOptions.isEnableBeanCache()) {
this.beanCacheSupplier = () -> cacheManager.getBeanCache(beanType);
this.naturalKeyCacheSupplier = (cacheOptions.getNaturalKey() != null) ? () -> cacheManager.getNaturalKeyCache(beanType) : null;
} else {
this.beanCacheSupplier = null;
this.naturalKeyCacheSupplier = null;
}
}
@Override
boolean hasBeanCache() {
return beanCacheSupplier != null;
}
@Override
boolean hasQueryCache() {
return queryCacheSupplier != null;
}
@Override
ServerCache queryCache() {
return queryCacheSupplier.get();
}
@Override
ServerCache naturalKeyCache() {
return naturalKeyCacheSupplier.get();
}
@Override
ServerCache beanCache() {
if (beanCacheSupplier == null) {
throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation.");
}
return beanCacheSupplier.get();
}
}
@@ -46,6 +46,8 @@ import io.ebeanservice.docstore.api.DocStoreFactory;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.Transient;
import io.ebeaninternal.server.transaction.DataSourceSupplier;
import io.ebeaninternal.server.transaction.SequenceDataSource;
import javax.sql.DataSource;
import java.io.Serializable;
import java.lang.reflect.Field;
@@ -96,7 +98,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
private final Map<String, List<BeanDescriptor<?>>> tableToDescMap = new HashMap<>();
private final Map<String, List<BeanDescriptor<?>>> tableToViewDescMap = new HashMap<>();
private final DbIdentity dbIdentity;
private final DataSource dataSource;
private final DataSourceSupplier dataSourceSupplier;
private final DatabasePlatform databasePlatform;
private final SpiCacheManager cacheManager;
private final BackgroundExecutor backgroundExecutor;
@@ -132,7 +134,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
this.cacheManager = config.getCacheManager();
this.docStoreFactory = config.getDocStoreFactory();
this.backgroundExecutor = config.getBackgroundExecutor();
this.dataSource = this.config.getDataSource();
this.dataSourceSupplier = config.getDataSourceSupplier();
this.encryptKeyManager = this.config.getEncryptKeyManager();
this.databasePlatform = this.config.getDatabasePlatform();
this.multiValueBind = config.getMultiValueBind();
@@ -519,6 +521,14 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
d.initialiseDocMapping();
}
// PASS 5:
// parse @Formula2 expressions — runs after all descriptors are fully
// initialised so cross-descriptor property paths (e.g. parent.parent.someBean.id)
// can be resolved safely without hitting null targetDescriptors
for (BeanDescriptor<?> d : descMap.values()) {
d.initFormula2Properties();
}
// create BeanManager for each non-embedded entity bean
for (BeanDescriptor<?> d : descMap.values()) {
d.initLast();
@@ -788,7 +798,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
throw new RuntimeException(msg);
}
DeployTableJoin tableJoin = assocOne.getTableJoin();
prop.setSecondaryTableJoin(tableJoin, assocOne.getName());
prop.setSecondaryTableJoin(tableJoin, assocOne.name());
}
}
}
@@ -846,8 +856,8 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
for (DeployBeanPropertyAssocOne<?> possibleMappedBy : ones) {
Class<?> possibleMappedByType = possibleMappedBy.getTargetType();
if (possibleMappedByType.equals(owningType)) {
prop.setMappedBy(possibleMappedBy.getName());
matchSet.add(possibleMappedBy.getName());
prop.setMappedBy(possibleMappedBy.name());
matchSet.add(possibleMappedBy.name());
}
}
@@ -864,7 +874,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
if (matchSet.size() == 2) {
// try to find a match implicitly using a common naming convention
// e.g. List<Bug> loggedBugs; ... search for "logged" in matchSet
String name = prop.getName();
String name = prop.name();
// get the target type short name
String targetType = prop.getTargetType().getName();
@@ -1264,7 +1274,10 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
}
private PlatformIdGenerator createSequenceIdGenerator(String seqName, int stepSize) {
return databasePlatform.createSequenceIdGenerator(backgroundExecutor, dataSource, stepSize, seqName);
DataSource ds = config.getTenantMode().isDynamicDataSource()
? new SequenceDataSource(dataSourceSupplier)
: dataSourceSupplier.dataSource();
return databasePlatform.createSequenceIdGenerator(backgroundExecutor, ds, stepSize, seqName);
}
private void setAccessors(DeployBeanDescriptor<?> deploy) {
@@ -1306,7 +1319,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy
// abstract classes as well.
BeanPropertiesReader reflectProps = new BeanPropertiesReader(desc.propertyNames());
for (DeployBeanProperty prop : desc.propertiesAll()) {
String propName = prop.getName();
String propName = prop.name();
Integer pos = reflectProps.propertyIndex(propName);
if (pos == null) {
if (isPersistentField(prop)) {
@@ -1,16 +1,12 @@
package io.ebeaninternal.server.deploy;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.EntityBean;
import io.ebean.common.BeanList;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.json.SpiJsonWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -23,19 +19,11 @@ public class BeanListHelp<T> extends BaseCollectionHelp<T> {
super(many);
}
BeanListHelp() {
super();
}
@Override
public final BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) {
public final BeanCollectionAdd collectionAdd(Object bc, String mapKey) {
if (bc instanceof BeanList<?>) {
BeanList<?> bl = (BeanList<?>) bc;
if (bl.actualList() == null) {
bl.setActualList(new ArrayList<>());
}
return bl;
return bl.collectionAdd();
} else {
throw new RuntimeException("Unhandled type " + bc);
}
@@ -67,20 +55,20 @@ public class BeanListHelp<T> extends BaseCollectionHelp<T> {
return beanList;
}
@SuppressWarnings("unchecked")
@Override
public final void refresh(BeanCollection<?> bc, EntityBean parentBean) {
BeanList<?> newBeanList = (BeanList<?>) bc;
BeanList<T> newBeanList = (BeanList<T>) bc;
List<?> currentList = (List<?>) many.getValue(parentBean);
newBeanList.setModifyListening(many.modifyListenMode());
if (currentList == null) {
// the currentList is null? Not really expecting this...
many.setValue(parentBean, newBeanList);
} else if (currentList instanceof BeanList<?>) {
} else if (currentList instanceof BeanList) {
// normally this case, replace just the underlying list
BeanList<?> currentBeanList = (BeanList<?>) currentList;
currentBeanList.setActualList(newBeanList.actualList());
currentBeanList.setModifyListening(many.modifyListenMode());
BeanList<T> currentBeanList = (BeanList<T>) currentList;
currentBeanList.refresh(many.modifyListenMode(), newBeanList);
} else {
// replace the entire list with the BeanList
@@ -1,17 +1,13 @@
package io.ebeaninternal.server.deploy;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.EntityBean;
import io.ebean.common.BeanMap;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.json.SpiJsonWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
@@ -37,20 +33,14 @@ public class BeanMapHelp<T> extends BaseCollectionHelp<T> {
@Override
@SuppressWarnings("unchecked")
public final BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) {
public final BeanCollectionAdd collectionAdd(Object bc, String mapKey) {
if (mapKey == null) {
mapKey = many.mapKey();
}
BeanProperty beanProp = targetDescriptor.beanProperty(mapKey);
if (bc instanceof BeanMap<?, ?>) {
BeanMap<Object, Object> bm = (BeanMap<Object, Object>) bc;
Map<Object, Object> actualMap = bm.actualMap();
if (actualMap == null) {
actualMap = new LinkedHashMap<>();
bm.setActualMap(actualMap);
}
return new Adder(beanProp, actualMap);
return new Adder(beanProp, bm.collectionAdd());
} else {
throw new RuntimeException("Unhandled type " + bc);
}
@@ -126,8 +116,7 @@ public class BeanMapHelp<T> extends BaseCollectionHelp<T> {
} else if (current instanceof BeanMap<?, ?>) {
// normally this case, replace just the underlying list
BeanMap<?, ?> currentBeanMap = (BeanMap<?, ?>) current;
currentBeanMap.setActualMap(newBeanMap.actualMap());
currentBeanMap.setModifyListening(many.modifyListenMode());
currentBeanMap.refresh(many.modifyListenMode(), newBeanMap);
} else {
// replace the entire set
@@ -1,7 +1,6 @@
package io.ebeaninternal.server.deploy;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.DataIntegrityException;
import io.ebean.ModifyAwareType;
import io.ebean.ValuePair;
@@ -125,6 +124,9 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
final String elPlaceHolderEncrypted;
private final String sqlFormulaSelect;
final String sqlFormulaJoin;
protected String formula2Select;
private Set<String> formula2Includes;
private final String formula2RawExpression;
private final String aggregation;
private final boolean formula;
private final boolean dbEncrypted;
@@ -189,7 +191,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
public BeanProperty(BeanDescriptor<?> descriptor, DeployBeanProperty deploy) {
this.descriptor = descriptor;
this.name = InternString.intern(deploy.getName());
this.name = InternString.intern(deploy.name());
this.propertyIndex = deploy.getPropertyIndex();
this.unidirectionalShadow = deploy.isUndirectionalShadow();
this.importedPrimaryKey = deploy.isImportedPrimaryKey();
@@ -242,10 +244,11 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
this.sqlFormulaJoin = InternString.intern(deploy.getSqlFormulaJoin());
this.sqlFormulaSelect = InternString.intern(deploy.getSqlFormulaSelect());
this.formula = sqlFormulaSelect != null;
this.formula2RawExpression = deploy.getFormula2Expression();
this.dbType = deploy.getDbType();
this.scalarType = deploy.getScalarType();
this.lob = isLobType(dbType);
this.propertyType = deploy.getPropertyType();
this.propertyType = deploy.propertyType();
this.field = deploy.getField();
this.docOptions = deploy.getDocPropertyOptions();
this.elPlaceHolder = tableAliasIntern(descriptor, deploy.getElPlaceHolder(), false, null);
@@ -298,6 +301,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
this.sqlFormulaJoin = null;
this.sqlFormulaSelect = null;
this.formula = false;
this.formula2RawExpression = null;
this.aggregation = null;
this.excludedFromHistory = source.excludedFromHistory;
this.tenantId = source.tenantId;
@@ -401,7 +405,7 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
* Return true if this property should have a DB Column created in DDL.
*/
public boolean isDDLColumn() {
return !formula && !secondaryTable && (aggregation == null);
return !formula && formula2RawExpression == null && !secondaryTable && (aggregation == null);
}
/**
@@ -487,6 +491,8 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
if (aggregation != null) {
ctx.appendFormulaSelect(aggregation);
} else if (formula2Select != null) {
ctx.appendFormula2Select(formula2Select);
} else if (formula) {
ctx.appendFormulaSelect(sqlFormulaSelect);
} else if (!isTransient && !ignoreDraftOnlyProperty(ctx.isDraftQuery())) {
@@ -855,6 +861,30 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
return formula && sqlFormulaJoin != null;
}
/**
* Initialise this property's formula2 parsed expression and required join paths.
* Called by BeanDescriptor.initialise() after all relationships are wired.
*/
public void initFormula2(String parsedSelect, Set<String> includes) {
this.formula2Select = parsedSelect;
this.formula2Includes = includes.isEmpty() ? null : java.util.Collections.unmodifiableSet(includes);
}
/**
* Return the raw @Formula2 expression (before parsing), or null.
*/
public String formula2RawExpression() {
return formula2RawExpression;
}
/**
* Return the join paths required by this @Formula2 property, or null if not a formula2.
*/
@Override
public Set<String> formula2Joins() {
return formula2Includes;
}
@Override
public boolean containsManySince(String sinceProperty) {
return containsMany();
@@ -922,6 +952,10 @@ public class BeanProperty implements ElPropertyValue, Property, STreeProperty {
@Override
public String elPlaceholder(boolean encrypted) {
if (formula2Select != null) {
// resolve to the parsed @Formula2 expression (with ${} / ${path} placeholders)
return formula2Select;
}
return encrypted ? elPlaceHolderEncrypted : elPlaceHolder;
}
@@ -161,13 +161,7 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
ElPropertyValue createElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
// associated or embedded bean
BeanDescriptor<?> embDesc = targetDescriptor();
if (chain == null) {
chain = new ElPropertyChainBuilder(isEmbedded(), propName);
}
chain.add(this);
if (containsMany()) {
chain.setContainsMany();
}
return embDesc.buildElGetValue(remainder, chain, propertyDeploy);
}
@@ -217,6 +211,10 @@ public abstract class BeanPropertyAssoc<T> extends BeanProperty implements STree
* Return the BeanDescriptor of the target.
*/
public BeanDescriptor<T> targetDescriptor() {
if (targetDescriptor == null) {
// lazily resolve for association properties inside an @Embeddable used as override copy
targetDescriptor = descriptor.descriptor(targetType);
}
return targetDescriptor;
}
@@ -681,7 +681,7 @@ public class BeanPropertyAssocMany<T> extends BeanPropertyAssoc<T> implements ST
}
private BeanCollectionAdd beanCollectionAdd(Object bc) {
return help.getBeanCollectionAdd(bc, null);
return help.collectionAdd(bc, null);
}
public Object parentId(EntityBean parentBean) {
@@ -5,6 +5,7 @@ import io.ebean.Transaction;
import io.ebean.ValuePair;
import io.ebean.bean.EntityBean;
import io.ebean.bean.EntityBeanIntercept;
import io.ebean.bean.InterceptReadWrite;
import io.ebean.bean.PersistenceContext;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.ScalarDataReader;
@@ -44,6 +45,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
private final boolean orphanRemoval;
private final boolean primaryKeyExport;
private final boolean primaryKeyJoin;
private final boolean embeddedAllowEmpty;
private AssocOneHelp localHelp;
final BeanProperty[] embeddedProps;
@@ -53,6 +55,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
private String deleteByParentIdInSql;
private BeanPropertyAssocMany<?> relationshipProperty;
private boolean cacheNotifyRelationship;
private boolean cacheNotifyOwningOneToOne;
/**
* Create based on deploy information of an EmbeddedId.
@@ -72,6 +75,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
oneToOne = deploy.isOneToOne();
oneToOneExported = deploy.isOneToOneExported();
orphanRemoval = deploy.isOrphanRemoval();
embeddedAllowEmpty = deploy.isEmbeddedAllowEmpty();
if (embedded) {
// Overriding of the columns and use table alias of owning BeanDescriptor
BeanEmbeddedMeta overrideMeta = BeanEmbeddedMetaFactory.create(owner, deploy);
@@ -98,6 +102,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
oneToOne = source.oneToOne;
oneToOneExported = source.oneToOneExported;
orphanRemoval = source.orphanRemoval;
embeddedAllowEmpty = source.embeddedAllowEmpty;
embeddedProps = null;
embeddedPropsMap = null;
}
@@ -142,6 +147,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
*/
void initialisePostTarget() {
this.cacheNotifyRelationship = isCacheNotifyRelationship();
this.cacheNotifyOwningOneToOne = oneToOne && !oneToOneExported && targetDescriptor.isBeanCaching();
}
/**
@@ -164,18 +170,31 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
}
/**
* Clear the L2 relationship cache for this property.
* Return true if this owning OneToOne property requires target bean cache eviction on change.
*/
boolean isCacheNotifyOwningOneToOne() {
return cacheNotifyOwningOneToOne;
}
/**
* Clear the L2 relationship cache for this property (used from beanCacheApplyInvalidate).
*/
void cacheClear() {
if (cacheNotifyRelationship) {
targetDescriptor.cacheManyPropClear(relationshipProperty.name());
}
if (cacheNotifyOwningOneToOne) {
targetDescriptor.clearBeanCache();
}
}
void cacheClear(CacheChangeSet changeSet) {
if (cacheNotifyRelationship) {
changeSet.addManyClear(targetDescriptor, relationshipProperty.name());
}
if (cacheNotifyOwningOneToOne) {
changeSet.addClearBean(targetDescriptor);
}
}
/**
@@ -198,6 +217,66 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
}
}
/**
* Evict the target's bean cache entry when this owning OneToOne bean is inserted or deleted.
*/
void cacheDeleteOneToOneOwned(EntityBean bean, CacheChangeSet changeSet) {
if (cacheNotifyOwningOneToOne) {
Object assocBean = getValue(bean);
if (assocBean != null) {
Object targetId = targetDescriptor.id(assocBean);
if (targetId != null) {
changeSet.addBeanRemove(targetDescriptor, targetId);
}
}
}
}
/**
* Clear the target's entire bean cache when owning OneToOne beans are deleted by IDs.
*/
void cacheClearOneToOneOwned(CacheChangeSet changeSet) {
if (cacheNotifyOwningOneToOne) {
changeSet.addClearBean(targetDescriptor);
}
}
/**
* Evict the inverse caches for BOTH old and new targets when the FK changes on update.
* Handles collection-ids caches (ManyToOne) and bean caches (owning OneToOne).
*/
void cacheUpdateFKchange(EntityBean bean, Object origAssocBean, CacheChangeSet changeSet) {
Object newAssocBean = getValue(bean);
if (cacheNotifyRelationship) {
if (origAssocBean != null) {
Object oldId = targetDescriptor.id(origAssocBean);
if (oldId != null) {
changeSet.addManyRemove(targetDescriptor, relationshipProperty.name(), targetDescriptor.cacheKey(oldId));
}
}
if (newAssocBean != null) {
Object newId = targetDescriptor.id(newAssocBean);
if (newId != null) {
changeSet.addManyRemove(targetDescriptor, relationshipProperty.name(), targetDescriptor.cacheKey(newId));
}
}
}
if (cacheNotifyOwningOneToOne) {
if (origAssocBean != null) {
Object oldId = targetDescriptor.id(origAssocBean);
if (oldId != null) {
changeSet.addBeanRemove(targetDescriptor, oldId);
}
}
if (newAssocBean != null) {
Object newId = targetDescriptor.id(newAssocBean);
if (newId != null) {
changeSet.addBeanRemove(targetDescriptor, newId);
}
}
}
}
@Override
Object naturalKeyVal(Map<String, Object> values) {
EntityBean bean = (EntityBean) values.get(name);
@@ -210,15 +289,31 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
@Override
public ElPropertyValue buildElPropertyValue(String propName, String remainder, ElPropertyChainBuilder chain, boolean propertyDeploy) {
if (embedded) {
BeanProperty embProp = embeddedPropsMap.get(remainder);
String embName = remainder;
String embRemainder = null;
int basePos = remainder.indexOf('.');
if (basePos > -1) {
embName = remainder.substring(0, basePos);
embRemainder = remainder.substring(basePos + 1);
}
BeanProperty embProp = embeddedPropsMap.get(embName);
if (embProp == null) {
String msg = "Embedded Property " + remainder + " not found in " + fullName();
String msg = "Embedded Property " + embName + " not found in " + fullName();
throw new PersistenceException(msg);
}
if (chain == null) {
chain = new ElPropertyChainBuilder(true, propName);
}
chain.add(this);
if (embRemainder != null) {
if (embProp instanceof BeanPropertyAssocOne) {
// @ManyToOne using the overridden property, e.g. "ma_country_code" instead of "country_code")
return embProp.buildElPropertyValue(propName, embRemainder, chain, propertyDeploy);
}
// scalar (or non-navigable) leaf with a further path segment - invalid path
String msg = "Embedded Property " + embName + "." + embRemainder + " not found in " + fullName();
throw new PersistenceException(msg);
}
// direct scalar/assoc access within embedded — mark as embedded so the prefix
// strips the embedded segment (keeps parent table alias, not the embedded segment)
chain.setEmbedded(true);
return chain.add(embProp).build();
}
@@ -439,27 +534,29 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
if (embedded) {
setValue(bean, targetDescriptor.cacheEmbeddedBeanLoad((CachedBeanData) cacheData, context));
} else {
// when the owning bean is unmodifiable the reference must also be unmodifiable
final boolean unmodifiable = !(bean._ebean_getIntercept() instanceof InterceptReadWrite);
if (cacheData instanceof CachedBeanId) {
setValue(bean, refInheritBean((CachedBeanId) cacheData, context));
setValue(bean, refInheritBean((CachedBeanId) cacheData, context, unmodifiable));
} else {
setValue(bean, refBean(targetDescriptor, cacheData, context));
setValue(bean, refBean(targetDescriptor, cacheData, context, unmodifiable));
}
}
}
}
private Object refInheritBean(CachedBeanId cacheId, PersistenceContext context) {
private Object refInheritBean(CachedBeanId cacheId, PersistenceContext context, boolean unmodifiable) {
final InheritInfo rowInheritInfo = targetInheritInfo.readType(cacheId.getDiscValue());
return refBean(rowInheritInfo.desc(), cacheId.getId(), context);
return refBean(rowInheritInfo.desc(), cacheId.getId(), context, unmodifiable);
}
private Object refBean(BeanDescriptor<?> desc, Object id, PersistenceContext context) {
private Object refBean(BeanDescriptor<?> desc, Object id, PersistenceContext context, boolean unmodifiable) {
if (id instanceof String) {
id = desc.idProperty().scalarType.parse((String) id);
}
Object bean = desc.contextGet(context, id);
Object bean = context == null ? null : desc.contextGet(context, id);
if (bean == null) {
bean = desc.createRef(id, context);
bean = desc.createReference(unmodifiable, false, id, context);
}
return bean;
}
@@ -593,10 +690,18 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
return findMatch(embeddedProp, prop, prop.dbColumn(), tableJoin);
}
@Override
public boolean isFormula() {
return super.isFormula() || formula2Select != null;
}
@Override
public void appendSelect(DbSqlContext ctx, boolean subQuery) {
if (!isTransient) {
if (primaryKeyExport) {
if (formula2Select != null) {
// @Formula2 on @ManyToOne: use formula2 expression as FK selector
ctx.appendFormula2Select(formula2Select);
} else if (primaryKeyExport) {
descriptor.idProperty().appendSelect(ctx, subQuery);
} else {
localHelp.appendSelect(ctx, subQuery);
@@ -615,9 +720,32 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
return super.addJoin(joinType, a1, a2, ctx);
}
/**
* Add table join using a prefix to resolve table aliases.
*/
@Override
public SqlJoinType addJoin(SqlJoinType joinType, String prefix, DbSqlContext ctx) {
if (formula2Select != null) {
// @Formula2 on @ManyToOne: join target table with formula2 FK expression as ON clause
String parentPrefix = SplitName.split(prefix)[0]; // null for root-level
String a2 = ctx.tableAlias(prefix);
String resolvedFk = ctx.parseFormula2(formula2Select, parentPrefix);
String joinLiteral = joinType.literal(SqlJoinType.OUTER);
String foreignIdCol = tableJoin.columns()[0].getForeignDbColumn();
ctx.addFormula2Join(joinLiteral, tableJoin.getTable(), a2, foreignIdCol, resolvedFk);
return SqlJoinType.OUTER;
}
return super.addJoin(joinType, prefix, ctx);
}
@Override
public void appendFrom(DbSqlContext ctx, SqlJoinType joinType, String manyWhere) {
if (!isTransient && !primaryKeyExport) {
if (formula2Select != null) {
// @Formula2 on @ManyToOne: auto-joins for the formula are handled via formula2JoinIncludes;
// no additional join SQL is emitted here (the FK value comes from the formula2 SELECT expression)
return;
}
localHelp.appendFrom(ctx, joinType);
if (sqlFormulaJoin != null) {
String alias = ctx.tableAliasManyWhere(manyWhere);
@@ -715,7 +843,7 @@ public class BeanPropertyAssocOne<T> extends BeanPropertyAssoc<T> implements STr
private AssocOneHelp createHelp(boolean embedded, boolean oneToOneExported, String embeddedPrefix) {
if (embedded) {
return new AssocOneHelpEmbedded(this);
return new AssocOneHelpEmbedded(this, embeddedAllowEmpty);
} else if (oneToOneExported) {
return new AssocOneHelpRefExported(this);
} else {
@@ -25,7 +25,7 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
public BeanPropertyJsonMapper(BeanDescriptor<?> desc, DeployBeanProperty deployProp) {
super(desc, deployProp);
this.sourceDetection = deployProp.getMutationDetection() == MutationDetection.SOURCE;
this.sourceDetection = deployProp.mutationDetection() == MutationDetection.SOURCE;
}
private BeanPropertyJsonMapper(BeanPropertyJsonMapper source, BeanPropertyOverride override) {
@@ -1,17 +1,13 @@
package io.ebeaninternal.server.deploy;
import io.ebean.Transaction;
import io.ebean.bean.BeanCollection;
import io.ebean.bean.BeanCollectionAdd;
import io.ebean.bean.EntityBean;
import io.ebean.common.BeanSet;
import io.ebeaninternal.api.SpiEbeanServer;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.api.json.SpiJsonWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
@@ -26,21 +22,11 @@ public class BeanSetHelp<T> extends BaseCollectionHelp<T> {
super(many);
}
/**
* For a query that returns a set.
*/
BeanSetHelp() {
super();
}
@Override
public final BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) {
public final BeanCollectionAdd collectionAdd(Object bc, String mapKey) {
if (bc instanceof BeanSet<?>) {
BeanSet<?> beanSet = (BeanSet<?>) bc;
if (beanSet.actualSet() == null) {
beanSet.setActualSet(new LinkedHashSet<>());
}
return beanSet;
return beanSet.collectionAdd();
} else {
throw new RuntimeException("Unhandled type " + bc);
}
@@ -72,9 +58,10 @@ public class BeanSetHelp<T> extends BaseCollectionHelp<T> {
return beanSet;
}
@SuppressWarnings("unchecked")
@Override
public final void refresh(BeanCollection<?> bc, EntityBean parentBean) {
BeanSet<?> newBeanSet = (BeanSet<?>) bc;
BeanSet<T> newBeanSet = (BeanSet<T>) bc;
Set<?> current = (Set<?>) many.getValue(parentBean);
newBeanSet.setModifyListening(many.modifyListenMode());
if (current == null) {
@@ -83,9 +70,8 @@ public class BeanSetHelp<T> extends BaseCollectionHelp<T> {
} else if (current instanceof BeanSet<?>) {
// normally this case, replace just the underlying list
BeanSet<?> currentBeanSet = (BeanSet<?>) current;
currentBeanSet.setActualSet(newBeanSet.actualSet());
currentBeanSet.setModifyListening(many.modifyListenMode());
BeanSet<T> currentBeanSet = (BeanSet<T>) current;
currentBeanSet.refresh(many.modifyListenMode(), newBeanSet);
} else {
// replace the entire set
@@ -63,6 +63,12 @@ public interface DbSqlContext {
*/
void appendParseSelect(String parseSelect, String alias);
/**
* Parse and add a @Formula2 path based formula resolving the path placeholders
* (e.g. ${} or ${parent}) relative to the current node prefix.
*/
void appendFormula2Select(String parseSelect);
/**
* Append a Sql Formula select. This converts the "${ta}" keyword to the
* current table alias.
@@ -75,6 +81,22 @@ public interface DbSqlContext {
*/
void appendFormulaJoin(String sqlFormulaJoin, SqlJoinType joinType, String tableAlias);
/**
* Parse a @Formula2 expression, resolving path placeholders (e.g. ${} or ${parent})
* relative to the given parent prefix.
*/
default String parseFormula2(String formula, String prefix) {
return formula;
}
/**
* Append a join where the ON clause FK side is a pre-resolved @Formula2 expression.
* Used for @Formula2 on @ManyToOne properties where the FK value is a formula.
*/
default void addFormula2Join(String joinLiteral, String table, String a2, String foreignIdCol, String resolvedFkExpr) {
// default no-op for non-default implementations
}
/**
* Return the current content length.
*/
@@ -1,5 +1,6 @@
package io.ebeaninternal.server.deploy;
import io.ebean.util.SplitName;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import java.util.HashSet;
@@ -64,6 +65,7 @@ public final class DeployPropertyParser extends DeployParser {
firstProp = elProp;
}
addIncludes(elProp.elPrefix());
addFormula2Includes(elProp);
return elProp.elPlaceholder(encrypted);
}
}
@@ -79,4 +81,22 @@ public final class DeployPropertyParser extends DeployParser {
includes.add(prefix);
}
}
/**
* Add the join paths required by a @Formula2 property so the joins it
* references (e.g. parent, parent.parent) are included to support the
* formula when used in where / order by / select clauses.
*/
private void addFormula2Includes(ElPropertyDeploy elProp) {
BeanProperty beanProperty = elProp.beanProperty();
if (beanProperty != null) {
Set<String> formula2Joins = beanProperty.formula2Joins();
if (formula2Joins != null) {
String prefix = elProp.elPrefix();
for (String join : formula2Joins) {
includes.add(prefix == null ? join : SplitName.add(prefix, join));
}
}
}
}
}
@@ -6,6 +6,8 @@ import io.ebeaninternal.server.query.SqlBeanLoad;
import jakarta.persistence.PersistenceException;
import java.util.Set;
/**
* Dynamic property based on aggregation (max, min, avg, count).
*/
@@ -15,13 +17,20 @@ class DynamicPropertyAggregationFormula extends DynamicPropertyBase {
private final boolean aggregate;
final BeanProperty asTarget;
private final String alias;
private final Set<String> formulaJoins;
DynamicPropertyAggregationFormula(String name, ScalarType<?> scalarType, String parsedFormula, boolean aggregate, BeanProperty asTarget, String alias) {
DynamicPropertyAggregationFormula(String name, ScalarType<?> scalarType, String parsedFormula, boolean aggregate, BeanProperty asTarget, String alias, Set<String> formulaJoins) {
super(name, name, null, scalarType);
this.parsedFormula = parsedFormula;
this.aggregate = aggregate;
this.asTarget = asTarget;
this.alias = alias;
this.formulaJoins = formulaJoins.isEmpty() ? null : formulaJoins;
}
@Override
public Set<String> formula2Joins() {
return formulaJoins;
}
@Override
@@ -11,7 +11,7 @@ public final class DynamicPropertyAggregationFormulaMTO extends DynamicPropertyA
private final Set<String> includes;
DynamicPropertyAggregationFormulaMTO(BeanPropertyAssocOne prop, String name, String parsedFormula, boolean aggregate, BeanProperty asTarget, String alias, Set<String> includes) {
super(name, prop.idScalarType(), parsedFormula, aggregate, asTarget, alias);
super(name, prop.idScalarType(), parsedFormula, aggregate, asTarget, alias, Set.of());
this.prop = prop;
this.includes = includes;
}
@@ -4,7 +4,6 @@ import io.ebean.bean.BeanCollection;
import io.ebean.common.BeanMap;
import java.util.LinkedHashMap;
import java.util.Map;
final class ElementHelpMap implements ElementHelp {
@@ -15,7 +14,7 @@ final class ElementHelpMap implements ElementHelp {
private static class Collector implements ElementCollector {
private final Map<Object, Object> map = new LinkedHashMap<>();
private final LinkedHashMap<Object, Object> map = new LinkedHashMap<>();
@Override
public void addElement(Object element) {
@@ -4,7 +4,6 @@ import io.ebean.bean.BeanCollection;
import io.ebean.common.BeanSet;
import java.util.LinkedHashSet;
import java.util.Set;
final class ElementHelpSet implements ElementHelp {
@@ -15,7 +14,7 @@ final class ElementHelpSet implements ElementHelp {
private static class Collector implements ElementCollector {
private final Set<Object> set = new LinkedHashSet<>();
private final LinkedHashSet<Object> set = new LinkedHashSet<>();
@Override
public void addElement(Object element) {

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