Compare commits

...
29 Commits
Author SHA1 Message Date
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
183 changed files with 4620 additions and 656 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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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
+1
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
+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).
+48 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.0.0</version>
<version>18.1.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();
@@ -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.
*/
@@ -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>
@@ -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;
}
@@ -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;
}
@@ -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.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.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.1.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.1.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.1.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.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>18.0.0</version>
<version>18.1.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.1.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.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.0.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
@@ -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>
@@ -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.
*/
@@ -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:
@@ -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) {
@@ -667,7 +666,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 +740,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.
*/
@@ -262,13 +262,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 +280,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);
}
}
}
@@ -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.
@@ -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)) {
@@ -633,14 +633,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);
@@ -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;
}
@@ -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;
@@ -439,27 +440,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 +596,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 +626,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);
@@ -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) {
@@ -127,7 +127,7 @@ final class FormulaPropertyPath {
private DynamicPropertyAggregationFormula create(ScalarType<?> scalarType) {
String logicalName = logicalName();
return new DynamicPropertyAggregationFormula(logicalName, scalarType, parsedAggregation, isAggregate(), target(logicalName), alias);
return new DynamicPropertyAggregationFormula(logicalName, scalarType, parsedAggregation, isAggregate(), target(logicalName), alias, includes);
}
@SuppressWarnings("rawtypes")
@@ -26,7 +26,7 @@ final class CounterFactory {
* Create the GeneratedProperty based on the property type.
*/
private GeneratedProperty createCounter(DeployBeanProperty property) {
Class<?> propType = property.getPropertyType();
Class<?> propType = property.propertyType();
if (propType.equals(Integer.class) || propType.equals(int.class)) {
return integerCounter;
}
@@ -68,7 +68,7 @@ public final class GeneratedPropertyFactory {
}
public void setVersion(DeployBeanProperty property) {
if (isNumberType(property.getPropertyType().getName())) {
if (isNumberType(property.propertyType().getName())) {
setCounter(property);
} else {
setUpdateTimestamp(property);
@@ -43,7 +43,7 @@ final class InsertTimestampFactory {
* Create the insert GeneratedProperty depending on the property type.
*/
GeneratedProperty createInsertTimestamp(DeployBeanProperty property) {
Class<?> propType = property.getPropertyType();
Class<?> propType = property.propertyType();
GeneratedProperty generatedProperty = map.get(propType);
if (generatedProperty != null) {
return generatedProperty;
@@ -43,7 +43,7 @@ final class UpdateTimestampFactory {
* Create the update GeneratedProperty depending on the property type.
*/
GeneratedProperty createUpdateTimestamp(DeployBeanProperty property) {
Class<?> propType = property.getPropertyType();
Class<?> propType = property.propertyType();
GeneratedProperty generatedProperty = map.get(propType);
if (generatedProperty != null) {
return generatedProperty;
@@ -667,7 +667,7 @@ public class DeployBeanDescriptor<T> {
* Add a bean property.
*/
public DeployBeanProperty addBeanProperty(DeployBeanProperty prop) {
return propMap.put(prop.getName(), prop);
return propMap.put(prop.name(), prop);
}
public Collection<DeployBeanProperty> properties() {
@@ -810,7 +810,7 @@ public class DeployBeanDescriptor<T> {
for (DeployBeanProperty prop : propMap.values()) {
if (!prop.isTransient() && !(prop instanceof DeployBeanPropertyAssocMany<?>)) {
if (prop.isFetchEager()) {
sb.append(prop.getName()).append(',');
sb.append(prop.name()).append(',');
} else {
hasLazyFetch = true;
}
@@ -38,7 +38,7 @@ import java.util.Set;
* Description of a property of a bean. Includes its deployment information such
* as database column mapping information.
*/
public class DeployBeanProperty {
public class DeployBeanProperty implements DeployProperty {
private static final int ID_ORDER = 1000000;
private static final int UNIDIRECTIONAL_ORDER = 100000;
@@ -144,6 +144,7 @@ public class DeployBeanProperty {
private String aggregationParsed;
private String sqlFormulaSelect;
private String sqlFormulaJoin;
private String formula2Expression;
/**
* The jdbc data type this maps to.
*/
@@ -229,6 +230,11 @@ public class DeployBeanProperty {
return desc;
}
@Override
public Class<?> ownerType() {
return desc.getBeanType();
}
/**
* Return the DB column length for character columns.
* <p>
@@ -261,10 +267,12 @@ public class DeployBeanProperty {
this.jsonDeserialize = jsonDeserialize;
}
public MutationDetection getMutationDetection() {
@Override
public MutationDetection mutationDetection() {
return mutationDetection;
}
@Override
public void setMutationDetection(MutationDetection dirtyDetection) {
this.mutationDetection = dirtyDetection;
}
@@ -428,10 +436,8 @@ public class DeployBeanProperty {
this.setter = setter;
}
/**
* Return the name of the property.
*/
public String getName() {
@Override
public String name() {
return name;
}
@@ -478,9 +484,7 @@ public class DeployBeanProperty {
this.generatedProperty = generatedValue;
}
/**
* Return true if this property is mandatory.
*/
@Override
public boolean isNullable() {
return nullable;
}
@@ -556,6 +560,23 @@ public class DeployBeanProperty {
this.dbUpdateable = false;
}
/**
* Return the raw logical expression set by {@code @Formula2}.
*/
public String getFormula2Expression() {
return formula2Expression;
}
/**
* Set the raw logical expression for a {@code @Formula2} property.
*/
public void setFormula2Expression(String formula2Expression) {
this.formula2Expression = formula2Expression;
this.dbRead = true;
this.dbInsertable = false;
this.dbUpdateable = false;
}
public void setImportedPrimaryKey() {
this.importedPrimaryKey = true;
}
@@ -843,17 +864,13 @@ public class DeployBeanProperty {
this.isTransient = true;
}
/**
* Return the property type.
*/
public Class<?> getPropertyType() {
@Override
public Class<?> propertyType() {
return propertyType;
}
/**
* Return the generic type for this property.
*/
public Type getGenericType() {
@Override
public Type genericType() {
return genericType;
}
@@ -1057,8 +1074,9 @@ public class DeployBeanProperty {
return null;
}
@Override
@SuppressWarnings("unchecked")
public <A extends Annotation> List<A> getMetaAnnotations(Class<A> annotationType) {
public <A extends Annotation> List<A> metaAnnotations(Class<A> annotationType) {
List<A> result = new ArrayList<>();
for (Annotation ann : metaAnnotations) {
if (ann.annotationType() == annotationType) {
@@ -1095,6 +1113,22 @@ public class DeployBeanProperty {
return fallback;
}
public Formula2 getMetaAnnotationFormula2(Platform platform) {
Formula2 fallback = null;
for (Annotation ann : metaAnnotations) {
if (ann.annotationType() == Formula2.class) {
Formula2 formula2 = (Formula2) ann;
final Platform[] platforms = formula2.platforms();
if (platforms.length == 0) {
fallback = formula2;
} else if (matchPlatform(platforms, platform)) {
return formula2;
}
}
}
return fallback;
}
public Where getMetaAnnotationWhere(Platform platform) {
Where fallback = null;
for (Annotation ann : metaAnnotations) {
@@ -136,7 +136,7 @@ public final class DeployBeanPropertyLists {
private DeployBeanProperty findImported(DeployBeanDescriptor<?> deploy, DeployBeanProperty embeddedScalar) {
// the logical name and db column we are looking for a match on
String name = embeddedScalar.getName();
String name = embeddedScalar.name();
String dbColumn = embeddedScalar.getDbColumn();
DeployBeanProperty match = deploy.getBeanProperty(name);
@@ -146,7 +146,7 @@ public final class DeployBeanPropertyLists {
// could look to match more by dbColumn
for (DeployBeanPropertyAssocOne<?> assocOne : deploy.propertiesAssocOne()) {
if (name.equals(assocOne.getName()) || (dbColumn != null && dbColumn.equals(assocOne.getDbColumn()))) {
if (name.equals(assocOne.name()) || (dbColumn != null && dbColumn.equals(assocOne.getDbColumn()))) {
return assocOne;
}
}
@@ -0,0 +1,53 @@
package io.ebeaninternal.server.deploy.meta;
import io.ebean.annotation.MutationDetection;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
/**
* Property, with basic type information (BeanProperty and DtoProperty).
*/
public interface DeployProperty {
/**
* Return the name of the property.
*/
String name();
/**
* Return the generic type for this property.
*/
Type genericType();
/**
* Return the property type.
*/
Class<?> propertyType();
/**
* Returns the owner class of this property.
*/
Class<?> ownerType();
/**
* Returns the annotations on this property.
*/
<A extends Annotation> List<A> metaAnnotations(Class<A> annotationType);
/**
* Returns the mutation detection setting of this property.
*/
MutationDetection mutationDetection();
/**
* Sets the mutation detection setting of this property.
*/
void setMutationDetection(MutationDetection mutationDetection);
/**
* Return true if this property is not mandatory.
*/
boolean isNullable();
}
@@ -187,7 +187,7 @@ final class AnnotationAssocManys extends AnnotationAssoc {
CollectionTable collectionTable = get(prop, CollectionTable.class);
String fullTableName = getFullTableName(collectionTable);
if (fullTableName == null) {
fullTableName = descriptor.getBaseTable()+"_"+ CamelCaseHelper.toUnderscoreFromCamel(prop.getName());
fullTableName = descriptor.getBaseTable()+"_"+ CamelCaseHelper.toUnderscoreFromCamel(prop.name());
}
BeanTable localTable = factory.beanTable(descriptor.getBeanType());
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy.parse;
import io.ebean.annotation.DbForeignKey;
import io.ebean.annotation.FetchPreference;
import io.ebean.annotation.Formula2;
import io.ebean.annotation.TenantId;
import io.ebean.annotation.Where;
import io.ebean.config.NamingConvention;
@@ -88,6 +89,11 @@ final class AnnotationAssocOnes extends AnnotationAssoc {
prop.setExtraWhere(processFormula(where.clause()));
}
Formula2 formula2 = prop.getMetaAnnotationFormula2(platform);
if (formula2 != null) {
prop.setFormula2Expression(formula2.value());
}
PrimaryKeyJoinColumn primaryKeyJoin = get(prop, PrimaryKeyJoinColumn.class);
if (primaryKeyJoin != null) {
readPrimaryKeyJoin(primaryKeyJoin, prop);
@@ -138,10 +144,11 @@ final class AnnotationAssocOnes extends AnnotationAssoc {
String fkeyPrefix = null;
if (nc.isUseForeignKeyPrefix()) {
fkeyPrefix = nc.getColumnFromProperty(beanType, prop.getName());
fkeyPrefix = nc.getColumnFromProperty(beanType, prop.name());
}
beanTable.createJoinColumn(fkeyPrefix, prop.getTableJoin(), true, prop.getSqlFormulaSelect());
String formulaSelect = prop.getSqlFormulaSelect() != null ? prop.getSqlFormulaSelect() : prop.getFormula2Expression();
beanTable.createJoinColumn(fkeyPrefix, prop.getTableJoin(), true, formulaSelect);
}
}
}
@@ -118,7 +118,7 @@ final class AnnotationFields extends AnnotationParser {
private void readField(DeployBeanProperty prop) {
// all Enums will have a ScalarType assigned...
boolean isEnum = prop.getPropertyType().isEnum();
boolean isEnum = prop.propertyType().isEnum();
Enumerated enumerated = get(prop, Enumerated.class);
if (isEnum || enumerated != null) {
util.setEnumScalarType(enumerated, prop);
@@ -135,7 +135,7 @@ final class AnnotationFields extends AnnotationParser {
readJsonAnnotations(prop);
if (prop.getDbColumn() == null) {
// No @Column or @Column.name() so use NamingConvention
prop.setDbColumn(namingConvention.getColumnFromProperty(beanType, prop.getName()));
prop.setDbColumn(namingConvention.getColumnFromProperty(beanType, prop.name()));
}
initIdentity(prop);
initTenantId(prop);
@@ -270,9 +270,13 @@ final class AnnotationFields extends AnnotationParser {
if (formula != null) {
prop.setSqlFormula(processFormula(formula.select()), processFormula(formula.join()));
}
Formula2 formula2 = prop.getMetaAnnotationFormula2(platform);
if (formula2 != null) {
prop.setFormula2Expression(formula2.value());
}
final Aggregation aggregation = prop.getMetaAnnotation(Aggregation.class);
if (aggregation != null) {
prop.setAggregation(aggregation.value().replace("$1", prop.getName()));
prop.setAggregation(aggregation.value().replace("$1", prop.name()));
}
}
@@ -442,7 +446,7 @@ final class AnnotationFields extends AnnotationParser {
private void readGenValue(GeneratedValue gen, Id id, DeployBeanProperty prop) {
if (id == null) {
if (UUID.class.equals(prop.getPropertyType())) {
if (UUID.class.equals(prop.propertyType())) {
generatedPropFactory.setUuid(prop);
return;
}
@@ -472,7 +476,7 @@ final class AnnotationFields extends AnnotationParser {
if (idGenerator != null) {
descriptor.setCustomIdGenerator(idGenerator);
}
} else if (prop.getPropertyType().equals(UUID.class)) {
} else if (prop.propertyType().equals(UUID.class)) {
descriptor.setUuidGenerator();
}
}
@@ -61,7 +61,7 @@ public abstract class AnnotationParser extends AnnotationBase {
prop.setImportedPrimaryKey();
} else {
prop.setId();
if (prop.getPropertyType().equals(UUID.class) && readConfig.isIdGeneratorAutomatic()) {
if (prop.propertyType().equals(UUID.class) && readConfig.isIdGeneratorAutomatic()) {
descriptor.setUuidGenerator();
}
}
@@ -8,10 +8,12 @@ import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.server.deploy.ManyType;
import io.ebeaninternal.server.deploy.meta.*;
import io.ebeaninternal.server.type.TypeManager;
import io.ebeaninternal.server.type.TypeReflectHelper;
import jakarta.persistence.Convert;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.Transient;
import jakarta.persistence.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
import static java.lang.System.Logger.Level.*;
@@ -37,7 +39,11 @@ public final class DeployCreateProperties {
* Create the appropriate properties for a bean.
*/
public void createProperties(DeployBeanDescriptor<?> desc) {
createProperties(desc, desc.getBeanType(), 0, new HashMap<>());
// Build the full type-variable map once for the entire hierarchy. TypeReflectHelper
// (via TypeResolver) walks superclasses and interfaces, composing mappings so that
// multi-level generic hierarchies (A extends B<T>, B<T> extends C<T>) resolve correctly.
Map<TypeVariable<?>, Type> typeMap = TypeReflectHelper.typeVariableMap(desc.getBeanType());
createProperties(desc, desc.getBeanType(), 0, typeMap);
desc.sortProperties();
}
@@ -63,10 +69,9 @@ public final class DeployCreateProperties {
}
/**
* properties the bean properties from Class. Some of these properties may not map to database
* columns.
* Create the bean properties from Class. Some of these properties may not map to database columns.
*/
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Type> typeMap) {
if (beanType.equals(Model.class)) {
// ignore all fields on model (_$dbName)
return;
@@ -76,7 +81,7 @@ public final class DeployCreateProperties {
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (!ignoreField(field)) {
DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap);
DeployBeanProperty prop = createProp(desc, field, beanType, typeMap);
if (prop != null) {
// set a order that gives priority to inherited properties
// push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down
@@ -96,9 +101,10 @@ public final class DeployCreateProperties {
Class<?> superClass = beanType.getSuperclass();
if (!superClass.equals(Object.class)) {
// recursively add any properties in the inheritance hierarchy
// up to the Object.class level...
createProperties(desc, superClass, level + 1, mapGenerics(beanType));
// up to the Object.class level - the same typeMap covers the full hierarchy
createProperties(desc, superClass, level + 1, typeMap);
}
} catch (PersistenceException ex) {
throw ex;
} catch (Exception ex) {
@@ -118,18 +124,24 @@ public final class DeployCreateProperties {
return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType);
}
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
Class<?> propertyType = field.getGenericType() instanceof TypeVariable<?>
? genericTypeMap.get(field.getGenericType())
: field.getType();
if (isSpecialScalarType(field)) {
return new DeployBeanProperty(desc, propertyType, field.getGenericType());
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Type> typeMap) {
// Resolve the field's generic type through the accumulated type-variable map.
// This handles TypeVariables from generic superclasses at any depth in the hierarchy.
Type resolvedGenericType = TypeReflectHelper.resolveType(field.getGenericType(), typeMap);
Class<?> propertyType = TypeReflectHelper.resolveToClass(resolvedGenericType);
if (propertyType == null) {
propertyType = field.getType();
}
if (isSpecialScalarType(field)) {
return new DeployBeanProperty(desc, propertyType, resolvedGenericType);
}
// check for Collection type (list, set or map)
ManyType manyType = determineManyType.getManyType(propertyType);
ManyType manyType = determineManyType.manyType(propertyType);
if (manyType != null) {
// List, Set or Map based object
Class<?> targetType = determineTargetType(field);
Class<?> targetType = determineTargetType(resolvedGenericType);
if (targetType == null) {
if (AnnotationUtil.has(field, Transient.class)) {
// not supporting this field (generic type used)
@@ -139,20 +151,25 @@ public final class DeployCreateProperties {
}
return createManyType(desc, targetType, manyType);
}
if (propertyType.isEnum() || propertyType.isPrimitive()) {
return new DeployBeanProperty(desc, propertyType, null, null);
}
ScalarType<?> scalarType = typeManager.type(propertyType);
if (scalarType != null) {
return new DeployBeanProperty(desc, propertyType, scalarType, null);
}
if (isTransientField(field)) {
// return with no ScalarType (still support JSON features)
return new DeployBeanProperty(desc, propertyType, null, null);
}
if (AnnotationUtil.has(field, Convert.class)) {
throw new IllegalStateException("No AttributeConverter registered for type " + propertyType + " at " + desc.getFullName() + "." + field.getName());
}
try {
return new DeployBeanPropertyAssocOne<>(desc, propertyType);
} catch (Exception e) {
@@ -176,8 +193,8 @@ public final class DeployCreateProperties {
return AnnotationUtil.has(field, Transient.class);
}
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
DeployBeanProperty prop = createProp(desc, field, genericTypeMap);
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Type> typeMap) {
DeployBeanProperty prop = createProp(desc, field, typeMap);
if (prop == null) {
// transient annotation on unsupported type
return null;
@@ -193,75 +210,21 @@ public final class DeployCreateProperties {
* Determine the type of the List,Set or Map. Not been set explicitly so determine this from
* ParameterizedType.
*/
private Class<?> determineTargetType(Field field) {
Type genType = field.getGenericType();
private Class<?> determineTargetType(Type genType) {
if (genType instanceof ParameterizedType) {
ParameterizedType ptype = (ParameterizedType) genType;
Type[] typeArgs = ptype.getActualTypeArguments();
if (typeArgs.length == 1) {
// expecting set or list
if (typeArgs[0] instanceof Class<?>) {
return (Class<?>) typeArgs[0];
}
if (typeArgs[0] instanceof WildcardType) {
final Type[] upperBounds = ((WildcardType) typeArgs[0]).getUpperBounds();
if (upperBounds.length == 1 && upperBounds[0] instanceof Class<?>) {
// kotlin generated wildcard type
return (Class<?>) upperBounds[0];
}
}
// throw new RuntimeException("Unexpected Parameterised Type? "+typeArgs[0]);
return null;
return TypeReflectHelper.resolveCollectionTarget(typeArgs[0]);
}
if (typeArgs.length == 2) {
// this is probably a Map
if (typeArgs[1] instanceof ParameterizedType) {
// not supporting ParameterizedType on Map.
return null;
}
if (typeArgs[1] instanceof WildcardType) {
return Object.class;
}
return (Class<?>) typeArgs[1];
return TypeReflectHelper.resolveCollectionTarget(typeArgs[1]);
}
}
// if targetType is null, then must be set in annotations
return null;
}
private Map<TypeVariable<?>, Class<?>> mapGenerics(Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
if (!(genericSuperclass instanceof ParameterizedType)) {
return new HashMap<>();
}
ParameterizedType parameterized = (ParameterizedType) genericSuperclass;
TypeVariable<?>[] typeVars = ((Class<?>) parameterized.getRawType()).getTypeParameters();
Type[] actualTypes = parameterized.getActualTypeArguments();
Map<TypeVariable<?>, Class<?>> typeMap = new HashMap<>();
for (int i = 0; i < typeVars.length; i++) {
Type actual = actualTypes[i];
Class<?> resolvedClass = resolveToClass(actual);
if (resolvedClass != null) {
typeMap.put(typeVars[i], resolvedClass);
} else {
// ignore
}
}
return typeMap;
}
private static Class<?> resolveToClass(Type type) {
if (type instanceof Class<?>) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Type raw = pType.getRawType();
if (raw instanceof Class<?>) {
return (Class<?>) raw;
}
}
return null;
}
}
@@ -90,7 +90,7 @@ public final class DeployUtil {
@SuppressWarnings("unchecked")
void setEnumScalarType(Enumerated enumerated, DeployBeanProperty prop) {
Class<?> enumType = prop.getPropertyType();
Class<?> enumType = prop.propertyType();
if (!enumType.isEnum()) {
throw new IllegalArgumentException("Class [" + enumType + "] is Not a Enum?");
}
@@ -130,7 +130,7 @@ public final class DeployUtil {
private ScalarType<?> scalarType(DeployBeanProperty property) {
// Note that Temporal types already have dbType
// set via annotations
Class<?> propType = property.getPropertyType();
Class<?> propType = property.propertyType();
try {
ScalarType<?> scalarType = typeManager.type(propType, property.getDbType());
if (scalarType != null || property.isTransient()) {
@@ -171,8 +171,8 @@ public final class DeployUtil {
// set nullable(false) before the ScalarTypeArray is determined and assigned
prop.setNullable(false);
}
Class<?> type = prop.getPropertyType();
ScalarType<?> scalarType = typeManager.dbArrayType(type, prop.getGenericType(), prop.isNullable());
Class<?> type = prop.propertyType();
ScalarType<?> scalarType = typeManager.dbArrayType(type, prop.genericType(), prop.isNullable());
if (scalarType == null) {
throw new RuntimeException("No ScalarType for @DbArray type for " + prop);
}
@@ -219,7 +219,7 @@ public final class DeployUtil {
/**
* Return the JDBC type for the JSON storage type.
*/
private int dbJsonStorage(DbJsonType dbJsonType) {
public static int dbJsonStorage(DbJsonType dbJsonType) {
switch (dbJsonType) {
case JSONB:
return DbPlatformType.JSONB;
@@ -244,7 +244,7 @@ public final class DeployUtil {
prop.setDbType(lobType);
} else {
// is String or byte[] ? used to determine if its a CLOB or BLOB
Class<?> type = prop.getPropertyType();
Class<?> type = prop.propertyType();
// this also sets the lob flag on DeployBeanProperty
int lobType = isClobType(type) ? dbCLOBType : dbBLOBType;
@@ -11,14 +11,14 @@ import java.util.Set;
*/
final class DetermineManyType {
ManyType getManyType(Class<?> type) {
ManyType manyType(Class<?> type) {
if (type.equals(List.class)) {
return ManyType.LIST;
}
if (type.equals(Set.class)) {
if (type.equals(Set.class) || type.getCanonicalName().equals("java.util.SequencedSet")) {
return ManyType.SET;
}
if (type.equals(Map.class)) {
if (type.equals(Map.class) || type.getCanonicalName().equals("java.util.SequencedMap")) {
return ManyType.MAP;
}
return null;
@@ -2,6 +2,7 @@ package io.ebeaninternal.server.deploy.parse;
import io.ebean.annotation.Aggregation;
import io.ebean.annotation.Formula;
import io.ebean.annotation.Formula2;
import io.ebean.annotation.Where;
import io.ebean.config.ClassLoadConfig;
import io.ebean.DatabaseBuilder;
@@ -46,6 +47,7 @@ final class ReadAnnotationConfig {
this.metaAnnotations.add(Column.class);
this.metaAnnotations.add(Formula.class);
this.metaAnnotations.add(Formula.List.class);
this.metaAnnotations.add(Formula2.class);
this.metaAnnotations.add(Where.class);
this.metaAnnotations.add(Where.List.class);
this.metaAnnotations.add(Aggregation.class);
@@ -41,8 +41,8 @@ final class ReadValidationAnnotationsJakarta implements ReadValidationAnnotation
}
private List<Size> getMetaAnnotationJavaxSize(DeployBeanProperty prop) {
final List<Size> size = prop.getMetaAnnotations(Size.class);
final List<Size.List> lists = prop.getMetaAnnotations(Size.List.class);
final List<Size> size = prop.metaAnnotations(Size.class);
final List<Size.List> lists = prop.metaAnnotations(Size.List.class);
for (Size.List list : lists) {
Collections.addAll(size, list.value());
}
@@ -41,8 +41,8 @@ final class ReadValidationAnnotationsJavax implements ReadValidationAnnotations
}
private List<Size> getMetaAnnotationJavaxSize(DeployBeanProperty prop) {
final List<Size> size = prop.getMetaAnnotations(Size.class);
final List<Size.List> lists = prop.getMetaAnnotations(Size.List.class);
final List<Size> size = prop.metaAnnotations(Size.class);
final List<Size.List> lists = prop.metaAnnotations(Size.List.class);
for (Size.List list : lists) {
Collections.addAll(size, list.value());
}
@@ -1,5 +1,7 @@
package io.ebeaninternal.server.dto;
import io.ebean.annotation.DbJson;
import io.ebean.annotation.DbJsonB;
import io.ebeaninternal.api.CoreLog;
import io.ebeaninternal.server.type.TypeManager;
@@ -21,10 +23,16 @@ final class DtoMetaBuilder {
private final Class<?> dtoType;
private final List<DtoMetaProperty> properties = new ArrayList<>();
private final Map<Integer, DtoMetaConstructor> constructorMap = new HashMap<>();
private final Set<Class<?>> annotationFilter = new HashSet<>();
DtoMetaBuilder(Class<?> dtoType, TypeManager typeManager) {
this.dtoType = dtoType;
this.typeManager = typeManager;
annotationFilter.add(DbJson.class);
annotationFilter.add(DbJsonB.class);
if (typeManager.jsonMarkerAnnotation() != null) {
annotationFilter.add(typeManager.jsonMarkerAnnotation());
}
}
DtoMeta build() {
@@ -38,7 +46,7 @@ final class DtoMetaBuilder {
if (includeMethod(method)) {
try {
final String name = propertyName(method.getName());
properties.add(new DtoMetaProperty(typeManager, dtoType, method, name));
properties.add(new DtoMetaProperty(typeManager, dtoType, method, name, annotationFilter));
} catch (Exception e) {
CoreLog.log.log(DEBUG, "exclude on " + dtoType + " method " + method, e);
}
@@ -0,0 +1,81 @@
package io.ebeaninternal.server.dto;
import io.ebean.annotation.MutationDetection;
import io.ebeaninternal.server.deploy.meta.DeployProperty;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* DeployProperty for Dto-Properties.
*
* @author Roland Praml, FOCONIS AG
*/
final class DtoMetaDeployProperty implements DeployProperty {
private final String name;
private final Class<?> ownerType;
private final Type genericType;
private final Class<?> propertyType;
private final Set<Annotation> metaAnnotations;
private final boolean nullable;
private MutationDetection mutationDetection = MutationDetection.DEFAULT;
DtoMetaDeployProperty(String name, Class<?> ownerType, Type genericType, Class<?> propertyType, Set<Annotation> metaAnnotations) {
this.name = name;
this.ownerType = ownerType;
this.genericType = genericType;
this.nullable = !propertyType.isPrimitive();
this.propertyType = propertyType;
this.metaAnnotations = metaAnnotations;
}
@Override
public String name() {
return name;
}
@Override
public Type genericType() {
return genericType;
}
@Override
public Class<?> propertyType() {
return propertyType;
}
@Override
public Class<?> ownerType() {
return ownerType;
}
@Override
public <A extends Annotation> List<A> metaAnnotations(Class<A> annotationType) {
List<A> result = new ArrayList<>();
for (Annotation ann : metaAnnotations) {
if (ann.annotationType() == annotationType) {
result.add((A) ann);
}
}
return result;
}
@Override
public MutationDetection mutationDetection() {
return mutationDetection;
}
@Override
public void setMutationDetection(MutationDetection mutationDetection) {
this.mutationDetection = mutationDetection;
}
@Override
public boolean isNullable() {
return nullable;
}
}
@@ -1,16 +1,26 @@
package io.ebeaninternal.server.dto;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.sql.SQLException;
import io.ebean.annotation.DbJson;
import io.ebean.annotation.DbJsonB;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.core.type.DataReader;
import io.ebean.core.type.ScalarType;
import io.ebean.plugin.Lookups;
import io.ebean.util.AnnotationUtil;
import io.ebeaninternal.server.deploy.meta.DeployProperty;
import io.ebeaninternal.server.deploy.parse.DeployUtil;
import io.ebeaninternal.server.type.TypeManager;
import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
final class DtoMetaProperty implements DtoReadSet {
private final Class<?> dtoType;
@@ -18,18 +28,68 @@ final class DtoMetaProperty implements DtoReadSet {
private final MethodHandle setter;
private final ScalarType<?> scalarType;
DtoMetaProperty(TypeManager typeManager, Class<?> dtoType, Method writeMethod, String name) throws IllegalAccessException, NoSuchMethodException {
DtoMetaProperty(TypeManager typeManager, Class<?> dtoType, Method writeMethod, String name, Set<Class<?>> annotationFilter)
throws IllegalAccessException, NoSuchMethodException {
this.dtoType = dtoType;
this.name = name;
if (writeMethod != null) {
this.setter = lookupMethodHandle(dtoType, writeMethod);
this.scalarType = typeManager.type(propertyType(writeMethod), propertyClass(writeMethod));
var deployProp = new DtoMetaDeployProperty(name,
dtoType,
propertyType(writeMethod),
propertyClass(writeMethod),
findMetaAnnotations(dtoType, writeMethod, name, annotationFilter));
scalarType = scalarType(typeManager, deployProp);
} else {
this.scalarType = null;
this.setter = null;
}
}
private ScalarType<?> scalarType(TypeManager typeManager, DeployProperty deployProp) {
List<DbJson> json = deployProp.metaAnnotations(DbJson.class);
if (!json.isEmpty()) {
return typeManager.dbJsonType(deployProp, DeployUtil.dbJsonStorage(json.get(0).storage()), json.get(0).length());
}
List<DbJsonB> jsonB = deployProp.metaAnnotations(DbJsonB.class);
if (!jsonB.isEmpty()) {
return typeManager.dbJsonType(deployProp, DbPlatformType.JSONB, jsonB.get(0).length());
}
if (typeManager.jsonMarkerAnnotation() != null
&& !deployProp.metaAnnotations(typeManager.jsonMarkerAnnotation()).isEmpty()) {
return typeManager.dbJsonType(deployProp, DbPlatformType.JSON, 0);
}
return typeManager.type(deployProp);
}
/**
* Find all annotations on fields and methods.
*/
private Set<Annotation> findMetaAnnotations(Class<?> dtoType, Method writeMethod, String name, Set<Class<?>> annotationFilter) {
Field field = findField(dtoType, name);
if (field != null) {
Set<Annotation> metaAnnotations = AnnotationUtil.metaFindAllFor(field, annotationFilter);
metaAnnotations.addAll(AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter));
return metaAnnotations;
} else {
return AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter);
}
}
/**
* Find field in class with same name
*/
private Field findField(Class<?> type, String name) {
while (type != Object.class && type != null) {
try {
return type.getDeclaredField(name);
} catch (NoSuchFieldException e) {
type = type.getSuperclass();
}
}
return null;
}
private static MethodHandle lookupMethodHandle(Class<?> dtoType, Method method) throws NoSuchMethodException, IllegalAccessException {
return Lookups.getLookup(dtoType).findVirtual(dtoType, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes()));
}
@@ -9,6 +9,8 @@ import io.ebeaninternal.api.SpiExpressionRequest;
import io.ebeaninternal.server.deploy.BeanProperty;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
@@ -24,6 +26,9 @@ import java.util.Arrays;
*/
public final class ElPropertyChain implements ElPropertyValue {
/** Matches placeholders of the form ${} or ${path} (e.g. ${parent}) used by @Formula2. */
private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{([^}]*)}");
private final String prefix;
private final String placeHolder;
private final String placeHolderEncrypted;
@@ -94,9 +99,18 @@ public final class ElPropertyChain implements ElPropertyValue {
if (!el.contains("${}")) {
// typically a secondary table property
return el.replace("${", "${" + prefix + ".");
} else {
return el.replace(ROOT_ELPREFIX, "${" + prefix + "}");
}
// prefix the root placeholder ${} as well as any path placeholders ${path}
// (e.g. ${parent} used by a @Formula2 property referenced via a path)
Matcher matcher = PLACEHOLDER.matcher(el);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
String path = matcher.group(1);
String replacement = path.isEmpty() ? "${" + prefix + "}" : "${" + prefix + "." + path + "}";
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(sb);
return sb.toString();
}
/**
@@ -345,6 +345,10 @@ final class SaveManyBeans extends SaveManyBase {
if (insertedParent) {
// after insert set the modify listening mode for private owned etc
c.setModifyListening(many.modifyListenMode());
} else {
// a lazily initialized collection (e.g. first save had null value) has no
// listen mode yet - set it now so that subsequent modifications are tracked
setListenMode(c, many);
}
// We must not reset when we still have to update other entities in the collection and set their new orderColumn value
if (!hasOrderColumn) {

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