Compare commits

..
73 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
Rob Bygraveandrobin.bygrave a317d669f0 Deps: Bump ebean-datasource to 10.10 (exclude isValid from metrics) (#3808)
Co-authored-by: robin.bygrave <robin.bygrave@eroad.com>
2026-07-01 12:57:23 +12:00
Rob Bygrave 291966cea9 Test and Fix for #3806 - @OneToMany query that populates from the immutable cache gets NPE (#3807)
## Root cause

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

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

  ## Fix (2 files)

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

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

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

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

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

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

## Fix:

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

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

## Performance (single-tenant unaffected)

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

## Compatibility

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

## Tests

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

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

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

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

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

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

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

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

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

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

* Restore prior format only on DeployCreateProperties

---------

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

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

Motivation

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

Usage

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

Behaviour

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

Files changed

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

Supersedes

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

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

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

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

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

  What changed

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

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

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

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

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

  Fixes

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

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

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

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

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

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

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

This happens, when

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

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

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

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

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

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

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

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

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

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

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

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

 ### Tests

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

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

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

Replace with collectionAdd() and refresh() methods.

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

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

* Update SequencedSet etc from recent changes

* Update build, needs package to use MR-JAR

---------

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

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

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

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

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

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

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

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

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

Changes

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

Why

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

This change makes the I__ scripts a "add if not already exists".
Note that R__ repeatable scripts are "always overwrite"
2026-06-10 17:02:58 +12:00
robin.bygrave a1ee75ffec docs: update docs / guides for query bean optional predicates 2026-06-10 16:59:24 +12:00
robin.bygrave da3dd8b215 docs: findStream() preferred over findList().stream() 2026-06-10 16:31:36 +12:00
robin.bygrave 6d30e6ff82 docs: Improve docs / guides / writing query beans, section on Optional predicates 2026-06-10 16:29:17 +12:00
lance 453a320210 Add Redis Sentinel support and local integration tests for ebean-redis
Signed-off-by: lance <leehaut@gmail.com>
2026-06-09 22:17:25 +08:00
320 changed files with 9130 additions and 3159 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 8 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
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: db2
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -37,5 +37,5 @@ jobs:
- name: Maven version
run: mvn --version
- name: H2Database
run: mvn -T 8 clean package
run: mvn -T 1C clean package
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: mariadb 10.11
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
+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:
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: mysql
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: oracle
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: postgres
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
@@ -35,4 +35,4 @@ jobs:
~/.m2
key: build-${{ env.cache-name }}
- name: sqlserver 2022
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver.properties
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
java_version: [11]
java_version: [21]
os: [ubuntu-latest]
steps:
+3
View File
@@ -13,6 +13,9 @@ ebean-profiling*.xml
profiling/
.DS_Store
# Local Redis integration test credentials
ebean-redis/src/test/resources/redis-local.yml
# Intellij project files
*.iml
*.ipr
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
</parent>
<artifactId>composites</artifactId>
+2
View File
@@ -183,6 +183,8 @@ database.save(customer);
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
+2
View File
@@ -12,7 +12,9 @@ Key guides (fetch and follow when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- 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
+3
View File
@@ -23,6 +23,7 @@ existing Maven project. Complete the steps in order.
| Guide | Description |
|-------|-------------|
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
## Observability
@@ -38,6 +39,8 @@ 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
@@ -250,6 +250,40 @@ For each future set of entity bean changes:
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
### Protecting hand-edited and non-versioned migrations across regeneration
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
entity model**. It can therefore overwrite content you did not change in the
entity beans, including:
- **hand-edited DDL** in a generated versioned `.sql` file, and
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must **delete the file first**, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
cannot express — it is isolated and now protected from regeneration.
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
produces, review the diff after **every** regeneration and **restore** any
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
re-check that the new migration is listed and filenames match after renaming a
generated file.
> **Run the generator from the module directory.** The output path set via
> `setPathToResources(...)` is resolved relative to the **working directory**.
> Run `GenerateDbMigration` with the working directory set to the module that owns
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
---
## Understanding the output files
+116
View File
@@ -0,0 +1,116 @@
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
## Purpose
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
for everything the built-in support does not handle.
This guide lists exactly which property types are handled built-in and which require
`ebean-jackson-mapper`.
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
> classpath, Ebean fails fast at startup:
>
> ```text
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
> Jackson ObjectMapper not present for <property>
> ```
---
## Quick reference
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|---|:---:|:---:|
| `String` | ✅ | |
| `List<String>`, `List<Long>` | ✅ | |
| `Set<String>`, `Set<Long>` | ✅ | |
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
| `Map<String, String>` | ✅ | |
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
| POJOs, records, or any other type | | ✅ |
---
## Built-in support (no Jackson required)
The built-in path materialises JSON into the *natural* JSON value types
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
for the following declared property types:
- **`String`** — stored as raw JSON text.
- **`List<String>`** and **`List<Long>`**.
- **`Set<String>`** and **`Set<Long>`**.
- **`Map<K, V>`** where:
- the key `K` is `String` or an **enum**, and
- the value `V` is `Object`, `String`, or a wildcard `?`.
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
are all built-in.
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
---
## Everything else → Jackson `ObjectMapper`
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
requires `ebean-jackson-mapper`:
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
- **POJOs, records, and any other custom type.**
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
---
## Adding `ebean-jackson-mapper`
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>${ebean.version}</version>
</dependency>
```
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
registers the mapper-based JSON support automatically.
---
## Notes
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
JSON keys.
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
(with a JSON fallback on platforms without array support) and supports more element types
than built-in `@DbJson` collections.
- The reason typed value/element collections need a real mapper is that the built-in path
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
type-aware mapper.
---
## Choosing
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
object/string maps) to avoid pulling in Jackson.
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
typed-value maps.
+164
View File
@@ -0,0 +1,164 @@
# Guide: Derived / formula properties — `@Formula` and `@Formula2`
## Purpose
A *formula property* is a read-only entity property whose value is computed by a SQL
expression at query time rather than stored in its own column. Ebean has two
annotations for this:
- **`@Formula`** — you write the **physical SQL** for the `select` (and any `join`),
using the `${ta}` placeholder for the base table alias. Maximum control; verbose.
- **`@Formula2`** — you write a **logical expression** using dot-notation property
paths (e.g. `parent.familyName`). Ebean translates the paths to the correct table
aliases and **adds the required JOINs automatically**.
`@Formula2` is intended as the easier, path-based replacement for `@Formula`. Both
produce read-only properties and behave the same way with respect to default
inclusion (see [Default inclusion](#default-inclusion-and-transient)).
---
## Quick comparison
| | `@Formula` | `@Formula2` |
|---|---|---|
| Expression | Physical SQL columns + aliases | Logical property paths |
| Table alias | `${ta}` placeholder you write | Resolved automatically |
| Joins | You write the `join` clause | Added automatically from the paths |
| Read only | ✅ | ✅ |
| Included by default | ✅ (use `@Transient` to opt out) | ✅ (use `@Transient` to opt out) |
| Usable in `select` / `where` / `orderBy` / `having` | ✅ | ✅ |
| Creates a DB column (DDL) | ❌ | ❌ |
---
## `@Formula` — physical SQL
You supply the SQL `select` fragment, and an optional `join`. Use `${ta}` wherever you
need the base table alias of the entity.
```java
@Entity
public class ParentPerson {
// aggregation via a derived join; ${ta} is the base table alias
@Formula(select = "coalesce(f2.child_count, 0)",
join = "left join (select parent_id, count(*) as child_count"
+ " from child group by parent_id) f2 on f2.parent_id = ${ta}.id")
Integer childCount;
// coalesce across a joined table using an explicit join alias (j1)
@Formula(select = "coalesce(${ta}.family_name, j1.family_name)",
join = "join parent_person j1 on j1.id = ${ta}.parent_id")
String effectiveFamilyName;
}
```
Notes:
- The `join` string must start with `join` or `left join`.
- You manage the join aliases (`j1`, `f2`, …) yourself and reference them in `select`.
- `@Formula` is `@Repeatable` and supports a `platforms()` restriction.
---
## `@Formula2` — logical property paths
Write the expression using property paths. Ebean resolves each path to the right table
alias and adds the joins it needs.
```java
@Entity
public class ParentPerson {
@ManyToOne
GrandParentPerson parent;
String familyName;
// Ebean automatically left joins 'parent' and resolves the aliases
@Formula2("coalesce(familyName, parent.familyName)")
String derivedFamilyName;
}
```
A query selecting `derivedFamilyName` produces (roughly):
```sql
select t0.id, coalesce(t0.family_name, t1.family_name)
from parent_person t0
left join grand_parent_person t1 on t1.id = t0.parent_id
```
Multi-level paths join through each step:
```java
// joins parent and parent.parent automatically
@Formula2("coalesce(familyName, parent.familyName, parent.parent.familyName)")
String deepFamilyName;
```
`@Formula2` works wherever a normal property does — the required joins are added
automatically in each case:
```java
// selected explicitly
DB.find(ParentPerson.class).select("derivedFamilyName").findList();
// used in where (auto-joins even when not selected)
DB.find(ParentPerson.class).where().eq("derivedFamilyName", "Smith").findList();
// used in order by
DB.find(ParentPerson.class).orderBy("derivedFamilyName").findList();
// referenced via a path from another bean
DB.find(ChildPerson.class).where().eq("parent.derivedFamilyName", "Smith").findList();
```
It also resolves correctly inside nested `fetch()` joins, so a `@Formula2` on a fetched
association is computed with its own joins relative to that association.
Notes:
- The expression supports any SQL function whose arguments are logical property paths.
- `@Formula2` supports a `platforms()` restriction.
- No `${ta}` and no hand-written join — that is the point of `@Formula2`.
---
## Default inclusion and `@Transient`
Both annotations are **included in queries by default** (just like a normal mapped
property). When no explicit `select()`/`fetch()` is given, the formula — and for
`@Formula2` the joins it requires — are added to the query.
Add `@Transient` to make the formula **opt-in**: it is then **not** selected by default
and must be requested explicitly via `select()` or `fetch()`. Do this when the formula
(or the joins it needs) is relatively expensive.
```java
// not selected by default; must be requested explicitly
@Transient
@Formula2("coalesce(familyName, parent.familyName)")
String lazyDerivedFamilyName;
```
```java
DB.find(ParentPerson.class)
.select("lazyDerivedFamilyName") // explicitly included, join auto-added
.findList();
```
This is the same `@Transient` opt-out mechanism used by `@Formula`.
---
## Which should I use?
- Prefer **`@Formula2`** for expressions over property paths (coalesce/case/functions
across associations). It is shorter, refactor-friendly, and the joins stay correct as
the model changes.
- Use **`@Formula`** when you need raw SQL that does not map cleanly to property paths —
for example a derived aggregate sub-select / dynamic view, or vendor-specific SQL.
For read models that exist only to carry computed values, also consider projecting to a
DTO instead of mapping the formula onto the entity — see
[writing-ebean-query-beans.md](writing-ebean-query-beans.md).
+9
View File
@@ -63,6 +63,15 @@ never captures bind values and cannot be `EXPLAIN`'d.
Bind capture is the master switch; nothing is captured until it is on.
> **Security — bind values may contain PII.** Bind capture records the **actual
> parameter values** used by slow query executions, and those values are stored
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
> plan). They can therefore contain personal or otherwise sensitive data. Capture
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
> that data exposure is acceptable, restrict who can read captured plans, and
> prefer arming specific query hashes (Step 3) over a low global threshold so you
> capture the minimum needed.
```java
Database database = Database.builder()
.queryPlanEnable(true) // turn on bind capture
@@ -0,0 +1,91 @@
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
## Purpose
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
avaje-json-core APIs.
Use this when upgrading code that references:
- `com.fasterxml.jackson.core.JsonParser`
- `com.fasterxml.jackson.core.JsonGenerator`
- `com.fasterxml.jackson.core.JsonFactory`
The replacement types are:
- `io.avaje.json.JsonReader`
- `io.avaje.json.JsonWriter`
- `io.avaje.json.stream.JsonStream`
---
## Breaking API changes
| Previous API | New API |
|---|---|
| `JsonParser` | `JsonReader` |
| `JsonGenerator` | `JsonWriter` |
| `JsonFactory` | `JsonStream` |
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
---
## Typical migration rewrites
### Parser and generator signatures
```java
// before
void read(JsonParser parser)
void write(JsonGenerator generator)
// after
void read(JsonReader parser)
void write(JsonWriter generator)
```
### Database configuration
```java
// before
Database.builder().jsonFactory(factory)
// after
Database.builder().jsonStream(stream)
```
### JSON utility calls
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
If your code was calling those APIs with Jackson core types, switch to avaje types.
---
## Dependency and module notes
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
parsing/writing.
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
integrations.
---
## Behavior notes to verify during upgrade
1. Parser token handling is now based on avaje `JsonReader.Token`.
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
be validated in your tests if you previously depended on Jackson token quirks.
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
keep ObjectMapper wiring enabled.
---
## Validation checklist
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
+85
View File
@@ -77,6 +77,7 @@ often the right query shape.
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
@@ -99,6 +100,43 @@ Customer customer = new QCustomer()
Do **not** use `findOne()` for predicates that can match multiple rows.
### Example - stream and map to another type
Choose based on result size and how you consume it:
- **`findList().stream()`** — executes the query, materialises the rows,
**releases the connection**, then streams over an in-memory list. No open
database resources and no try-with-resources needed. Prefer this for small or
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
connection (and an implicit transaction) open for the **whole lifetime of the
stream pipeline**. It must be closed with try-with-resources. Prefer it when
the result may be large, when you want constant memory, or when you want to
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
```java
// small, bounded result fully collected -> findList().stream()
List<PendingPlan> pending = new QCaptureRequest()
.collectedAt.isNull()
.orderBy().requestedAt.asc()
.findList()
.stream()
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
.toList();
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
try (Stream<Customer> stream = new QCustomer()
.status.equalTo(Status.NEW)
.findStream()) {
stream
.map(...)
.forEach(...);
}
```
For processing large results one bean at a time, `findEach()` is often the
simplest choice because it closes the underlying resources automatically.
---
## Step 3 - Build predicates by traversing properties and associations
@@ -131,6 +169,49 @@ List<Customer> customers = new QCustomer()
.findList();
```
### Optional predicates - prefer conditional helpers over `if` blocks
When a filter is driven by a nullable/optional parameter, use the built-in
conditional helpers instead of wrapping predicates in `if` blocks. The query
stays fluent and reads top-to-bottom, and no predicate is added when the value
is absent.
| Helper | Adds predicate when | Resulting SQL |
|--------|---------------------|---------------|
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
```java
// Instead of building the query with if blocks:
QCustomer q = new QCustomer();
if (name != null && !name.isBlank()) {
q.name.eq(name.trim());
}
if (status != null) {
q.status.eq(status);
}
List<Customer> customers = q.findList();
// Prefer the conditional helpers:
List<Customer> customers = new QCustomer()
.name.eqIfNotBlank(name)
.status.eqIfPresent(status)
.findList();
```
Use `eqOrNull(v)` when a null column value should also match - for example an
"any environment" row stored with `env_id is null` should surface under any env
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
```java
List<CaptureRequest> rows = new QCaptureRequest()
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
.findList();
```
### Agent rule
When adding a new query:
@@ -140,6 +221,10 @@ When adding a new query:
3. Traverse relationships instead of writing manual join SQL
4. Keep property references type-safe; avoid string property names unless the API
specifically requires them
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
null column"
---
+52 -7
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.10.0</version>
<version>18.1.0</version>
</parent>
<name>ebean api</name>
@@ -70,15 +70,13 @@
<optional>true</optional>
</dependency>
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- provided scope for JsonNode support -->
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -105,6 +103,53 @@
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>11</release>
</configuration>
</execution>
<execution>
<id>compile-21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>21</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
<!-- <manifest>-->
<!-- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>-->
<!-- </manifest>-->
</plugin>
</plugins>
</build>
</project>
@@ -1,6 +1,6 @@
package io.ebean;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.json.stream.JsonStream;
import io.ebean.annotation.*;
import io.ebean.cache.ServerCachePlugin;
import io.ebean.config.*;
@@ -61,6 +61,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();
@@ -360,18 +364,18 @@ public interface DatabaseBuilder {
DatabaseBuilder putServiceObject(Object configObject);
/**
* Set the Jackson JsonFactory to use.
* Set the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
return setJsonFactory(jsonFactory);
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
return setJsonStream(jsonStream);
}
/**
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
*/
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
DatabaseBuilder setJsonStream(JsonStream jsonStream);
/**
* Set the JSON format to use for DateTime types.
@@ -2254,11 +2258,11 @@ public interface DatabaseBuilder {
boolean isAutoLoadModuleInfo();
/**
* Return the Jackson JsonFactory to use.
* Return the JsonStream to use.
* <p>
* If not set a default implementation will be used.
*/
JsonFactory getJsonFactory();
JsonStream getJsonStream();
/**
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
@@ -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;
}
@@ -60,7 +60,8 @@ public class ClassLoadConfig {
}
public boolean isJacksonCorePresent() {
return isPresent("com.fasterxml.jackson.core.JsonParser");
// Legacy method name retained for compatibility; now checks avaje JSON core.
return isPresent("io.avaje.json.JsonReader");
}
/**
@@ -158,4 +159,3 @@ public class ClassLoadConfig {
}
}
}
@@ -1,7 +1,7 @@
package io.ebean.config;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.config.Config;
import io.avaje.json.stream.JsonStream;
import io.ebean.*;
import io.ebean.annotation.MutationDetection;
import io.ebean.annotation.PersistBatch;
@@ -420,7 +420,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
* The default PersistenceContextScope used if one is not explicitly set on a query.
*/
private PersistenceContextScope persistenceContextScope = PersistenceContextScope.TRANSACTION;
private JsonFactory jsonFactory;
private JsonStream jsonStream;
private boolean localTimeWithNanos;
private boolean durationWithNanos;
private int maxCallStack = 5;
@@ -631,13 +631,13 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
}
@Override
public JsonFactory getJsonFactory() {
return jsonFactory;
public JsonStream getJsonStream() {
return jsonStream;
}
@Override
public DatabaseConfig setJsonFactory(JsonFactory jsonFactory) {
this.jsonFactory = jsonFactory;
public DatabaseConfig setJsonStream(JsonStream jsonStream) {
this.jsonStream = jsonStream;
return this;
}
@@ -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,135 @@
package io.ebean.meta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Canonical "v2" mapping of Ebean's internal flat metric names (e.g.
* {@code orm.Customer.findList}, {@code iud.User.save}, {@code txn.named.X},
* {@code l2.<region>.<op>}) into a metric family name plus a tag string following
* the label-tag convention.
*
* <p>This is the source-of-truth mapping for the v2 metrics JSON form
* ({@link ServerMetricsAsJson#writeV2(Appendable)}). The tag string is a canonical,
* sorted, comma separated list of {@code key:value} pairs, e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}.
*
* <table>
* <caption>Ebean prefix → family name + tags</caption>
* <tr><th>Ebean prefix</th><th>name</th><th>tags</th></tr>
* <tr><td>{@code iud.X}</td><td>{@code ebean.dml}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code orm.X}</td><td>{@code ebean.query}</td><td>{@code kind=orm, type=<bean>, label=X}</td></tr>
* <tr><td>{@code dto.X}</td><td>{@code ebean.query}</td><td>{@code kind=dto, type=<bean>, label=X}</td></tr>
* <tr><td>{@code sql.X}</td><td>{@code ebean.query}</td><td>{@code kind=sql, type=<bean>, label=X}</td></tr>
* <tr><td>{@code txn.named.X} / {@code txn.X}</td><td>{@code ebean.txn}</td><td>{@code label=X}</td></tr>
* <tr><td>{@code l2.<region>.<op>}</td><td>{@code ebean.l2}</td><td>{@code op=<op>, region=<region>}</td></tr>
* <tr><td>(unrecognised)</td><td>{@code ebean.other}</td><td>{@code label=<original name>}</td></tr>
* </table>
*
* <p>The {@code kind} tag is the query category (orm/dto/sql) while the {@code type}
* tag is the queried bean/entity simple name. The {@code type} tag is omitted when
* the bean type is unknown.
*/
final class MetricNamingV2 {
/** Result of a name mapping: family name plus canonical tag string. */
static final class Mapped {
private final String name;
private final String tags;
Mapped(String name, String tags) {
this.name = name;
this.tags = tags;
}
String name() {
return name;
}
String tags() {
return tags;
}
}
private MetricNamingV2() {
}
/**
* Map an Ebean flat metric name (and optional bean type for query metrics) into
* the canonical family name plus tag string.
*/
static Mapped map(String ebeanName, String beanType) {
if (ebeanName == null || ebeanName.isEmpty()) {
return new Mapped("ebean.other", "");
}
int firstDot = ebeanName.indexOf('.');
if (firstDot <= 0) {
return new Mapped("ebean.other", tags("label", ebeanName));
}
String prefix = ebeanName.substring(0, firstDot);
String rest = ebeanName.substring(firstDot + 1);
switch (prefix) {
case "iud":
return new Mapped("ebean.dml", tags("label", rest));
case "orm":
return query("orm", rest, beanType);
case "dto":
return query("dto", rest, beanType);
case "sql":
return query("sql", rest, beanType);
case "txn":
String txnLabel = rest.startsWith("named.") ? rest.substring("named.".length()) : rest;
return new Mapped("ebean.txn", tags("label", txnLabel));
case "l2":
return l2(rest);
default:
return new Mapped("ebean.other", tags("label", ebeanName));
}
}
private static Mapped query(String kind, String label, String beanType) {
if (beanType == null || beanType.isEmpty()) {
return new Mapped("ebean.query", tags("kind", kind, "label", label));
}
return new Mapped("ebean.query", tags("kind", kind, "type", beanType, "label", label));
}
private static Mapped l2(String rest) {
int dot = rest.indexOf('.');
if (dot <= 0) {
return new Mapped("ebean.l2", tags("op", rest));
}
String region = rest.substring(0, dot);
String op = rest.substring(dot + 1);
return new Mapped("ebean.l2", tags("op", op, "region", region));
}
/**
* Build a canonical (sorted) {@code key:value,key2:value2} tag string from the given
* key/value pairs, skipping null/empty values and sanitising the reserved
* delimiter characters from values.
*/
private static String tags(String... keyValues) {
List<String> pairs = new ArrayList<>(keyValues.length / 2);
for (int i = 0; i + 1 < keyValues.length; i += 2) {
String value = keyValues[i + 1];
if (value != null && !value.isEmpty()) {
pairs.add(keyValues[i] + ':' + sanitize(value));
}
}
Collections.sort(pairs);
return String.join(",", pairs);
}
/**
* Replace the reserved tag delimiter characters ({@code ,} and {@code :}) so they
* cannot break the {@code key:value,key2:value2} encoding.
*/
private static String sanitize(String value) {
if (value.indexOf(',') < 0 && value.indexOf(':') < 0) {
return value;
}
return value.replace(',', '_').replace(':', '_');
}
}
@@ -19,6 +19,7 @@ final class MetricsAsJson implements ServerMetricsAsJson {
private Comparator<MetaTimedMetric> sortBy = SortMetric.NAME;
private int listCounter;
private int objKeyCounter;
private boolean v2;
MetricsAsJson(ServerMetrics metrics) {
this.metrics = metrics;
@@ -67,6 +68,13 @@ final class MetricsAsJson implements ServerMetricsAsJson {
collect();
}
@Override
public void writeV2(Appendable buffer) {
this.v2 = true;
this.writer = buffer;
collect();
}
private void collect() {
try {
start();
@@ -151,12 +159,26 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void metricStart(MetaMetric metric) throws IOException {
metricStart(metric, null);
}
private void metricStart(MetaMetric metric, String beanType) throws IOException {
if (listCounter++ > 0) {
writer.append(',').append(newLine);
}
objStart();
key("name");
val(metric.name());
if (v2) {
MetricNamingV2.Mapped mapped = MetricNamingV2.map(metric.name(), beanType);
key("name");
val(mapped.name());
if (!mapped.tags().isEmpty()) {
key("tags");
val(mapped.tags());
}
} else {
key("name");
val(metric.name());
}
}
private void metricEnd() throws IOException {
@@ -180,7 +202,8 @@ final class MetricsAsJson implements ServerMetricsAsJson {
}
private void logQuery(MetaQueryMetric metric) throws IOException {
metricStart(metric);
Class<?> beanType = metric.type();
metricStart(metric, beanType == null ? null : beanType.getSimpleName());
appendTiming(metric);
if (withHash) {
append("hash", metric.hash());
@@ -41,6 +41,17 @@ public interface ServerMetricsAsJson {
*/
void write(Appendable buffer);
/**
* Collect and write metrics as "v2" JSON to the given buffer.
* <p>
* The v2 form uses the canonical label-tag convention: each metric is written with
* a family {@code name} (e.g. {@code ebean.query}, {@code ebean.dml}) plus a
* {@code tags} string of sorted {@code key:value} pairs (e.g.
* {@code "kind:orm,label:Customer.findList,type:Customer"}) rather than the flat
* prefixed name. Timing, hash, location and sql attributes are unchanged.
*/
void writeV2(Appendable buffer);
/**
* Return the metrics in raw JSON.
*/
@@ -1,8 +1,8 @@
package io.ebean.service;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import java.io.IOException;
import java.io.Reader;
@@ -32,12 +32,12 @@ public interface SpiJsonService extends BootstrapService {
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
void write(Object object, JsonGenerator jsonGenerator) throws IOException;
void write(Object object, JsonWriter jsonGenerator) throws IOException;
/**
* Write the collection as json array to the jsonGenerator.
*/
void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException;
void writeCollection(Collection<Object> collection, JsonWriter jsonGenerator) throws IOException;
/**
* Parse the json and return as a Map additionally specifying if the returned map should
@@ -61,17 +61,17 @@ public interface SpiJsonService extends BootstrapService {
Map<String, Object> parseObject(Reader reader) throws IOException;
/**
* Parse the json and return as a Map taking a JsonParser.
* Parse the json and return as a Map taking a JsonReader.
*/
Map<String, Object> parseObject(JsonParser parser) throws IOException;
Map<String, Object> parseObject(JsonReader parser) throws IOException;
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
* Parse the json and return as a Map taking a JsonReader and a starting token.
* <p>
* Used when the first token is checked to see if the value is null prior to calling this.
* </p>
*/
Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException;
Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException;
/**
* Parse the json and return as a modify aware List.
@@ -89,14 +89,14 @@ public interface SpiJsonService extends BootstrapService {
List<Object> parseList(Reader reader) throws IOException;
/**
* Parse the json and return as a List taking a JsonParser.
* Parse the json and return as a List taking a JsonReader.
*/
List<Object> parseList(JsonParser parser) throws IOException;
List<Object> parseList(JsonReader parser) throws IOException;
/**
* Parse the json returning as a List taking into account the current token.
*/
<T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException;
<T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException;
/**
* Parse the json and return as a List or Map.
@@ -111,7 +111,7 @@ public interface SpiJsonService extends BootstrapService {
/**
* Parse the json and return as a List or Map.
*/
Object parse(JsonParser parser) throws IOException;
Object parse(JsonReader parser) throws IOException;
/**
* Parse the json returning a Set that might be modify aware.
@@ -121,5 +121,5 @@ public interface SpiJsonService extends BootstrapService {
/**
* Parse the json returning as a Set taking into account the current token.
*/
<T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException;
<T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException;
}
@@ -1,8 +1,7 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.XBootstrapService;
import io.ebean.service.SpiJsonService;
@@ -38,14 +37,14 @@ public class EJson {
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
public static void write(Object object, JsonGenerator jsonGenerator) throws IOException {
public static void write(Object object, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
plugin.write(object, jsonGenerator);
}
/**
* Write the collection as json array to the jsonGenerator.
*/
public static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
public static void writeCollection(Collection<Object> collection, io.avaje.json.JsonWriter jsonGenerator) throws IOException {
plugin.writeCollection(collection, jsonGenerator);
}
@@ -79,19 +78,19 @@ public class EJson {
}
/**
* Parse the json and return as a Map taking a JsonParser.
* Parse the json and return as a Map taking a JsonReader.
*/
public static Map<String, Object> parseObject(JsonParser parser) throws IOException {
public static Map<String, Object> parseObject(JsonReader parser) throws IOException {
return plugin.parseObject(parser);
}
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
* Parse the json and return as a Map taking a JsonReader and a starting token.
* <p>
* Used when the first token is checked to see if the value is null prior to calling this.
* </p>
*/
public static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
public static Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return plugin.parseObject(parser, token);
}
@@ -117,16 +116,16 @@ public class EJson {
}
/**
* Parse the json and return as a List taking a JsonParser.
* Parse the json and return as a List taking a JsonReader.
*/
public static List<Object> parseList(JsonParser parser) throws IOException {
public static List<Object> parseList(JsonReader parser) throws IOException {
return plugin.parseList(parser);
}
/**
* Parse the json returning as a List taking into account the current token.
*/
public static <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
public static <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
return plugin.parseList(parser, currentToken);
}
@@ -147,7 +146,7 @@ public class EJson {
/**
* Parse the json and return as a List or Map.
*/
public static Object parse(JsonParser parser) throws IOException {
public static Object parse(JsonReader parser) throws IOException {
return plugin.parse(parser);
}
@@ -161,7 +160,7 @@ public class EJson {
/**
* Parse the json returning as a Set taking into account the current token.
*/
public static <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
public static <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
return plugin.parseSet(parser, currentToken);
}
}
@@ -1,6 +1,6 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.PersistenceContext;
/**
@@ -25,9 +25,9 @@ public interface JsonBeanReader<T> {
}
/**
* Create a new reader taking the context from the existing one but using a new JsonParser.
* Create a new reader taking the context from the existing one but using a new JsonReader.
*/
JsonBeanReader<T> forJson(JsonParser moreJson);
JsonBeanReader<T> forJson(JsonReader moreJson);
/**
* Add a bean explicitly to the persistence context.
@@ -1,7 +1,6 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.FetchPath;
import io.ebean.plugin.BeanType;
@@ -49,14 +48,14 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> T toBean(Class<T> cls, JsonParser parser) throws JsonIOException;
<T> T toBean(Class<T> cls, JsonReader parser) throws JsonIOException;
/**
* Convert json parser input into a Bean of a specific type additionally using JsonReadOptions..
*
* @throws JsonIOException When IOException occurs
*/
<T> T toBean(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> T toBean(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Read json parser input into a given Bean. <br>
@@ -65,19 +64,19 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> void toBean(T target, JsonParser parser) throws JsonIOException;
<T> void toBean(T target, JsonReader parser) throws JsonIOException;
/**
* Read json parser input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details modified.
* See {@link #toBean(Class, JsonReader)} for details modified.
*
* @throws JsonIOException When IOException occurs
*/
<T> void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> void toBean(T target, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Read json reader input into a given Bean.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -85,7 +84,7 @@ public interface JsonContext {
/**
* Read json reader input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details modified.
* See {@link #toBean(Class, JsonReader)} for details modified.
*
* @throws JsonIOException When IOException occurs
*/
@@ -93,7 +92,7 @@ public interface JsonContext {
/**
* Read json string input into a given Bean.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -101,7 +100,7 @@ public interface JsonContext {
/**
* Read json string input into a given Bean additionally using JsonReadOptions.<br>
* See {@link #toBean(Class, JsonParser)} for details
* See {@link #toBean(Class, JsonReader)} for details
*
* @throws JsonIOException When IOException occurs
*/
@@ -113,7 +112,7 @@ public interface JsonContext {
* Note that JsonOption provides an option for setting a persistence context and also enabling further lazy loading. Further lazy
* loading requires a persistence context so if that is set on then a persistence context is created if there is not one set.
*/
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> JsonBeanReader<T> createBeanReader(Class<T> cls, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Create and return a new bean reading for the bean type given the JSON options and source.
@@ -122,7 +121,7 @@ public interface JsonContext {
* further lazy loading. Further lazy loading requires a persistence context so if that is set
* on then a persistence context is created if there is not one set.
*/
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException;
<T> JsonBeanReader<T> createBeanReader(BeanType<T> beanType, JsonReader parser, JsonReadOptions options) throws JsonIOException;
/**
* Convert json string input into a list of beans of a specific type.
@@ -157,14 +156,14 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
<T> List<T> toList(Class<T> cls, JsonParser json) throws JsonIOException;
<T> List<T> toList(Class<T> cls, JsonReader json) throws JsonIOException;
/**
* Convert json parser input into a list of beans of a specific type additionally using JsonReadOptions.
*
* @throws JsonIOException When IOException occurs
*/
<T> List<T> toList(Class<T> cls, JsonParser json, JsonReadOptions options) throws JsonIOException;
<T> List<T> toList(Class<T> cls, JsonReader json, JsonReadOptions options) throws JsonIOException;
/**
* Use the genericType to determine if this should be converted into a List or
@@ -188,7 +187,7 @@ public interface JsonContext {
*
* @throws JsonIOException When IOException occurs
*/
Object toObject(Type genericType, JsonParser jsonParser) throws JsonIOException;
Object toObject(Type genericType, JsonReader jsonParser) throws JsonIOException;
/**
* Return the bean or collection as JSON string.
@@ -212,11 +211,11 @@ public interface JsonContext {
void toJson(Object value, Writer writer) throws JsonIOException;
/**
* Write the bean or collection to the JsonGenerator.
* Write the bean or collection to the JsonWriter.
*
* @throws JsonIOException When IOException occurs
*/
void toJson(Object value, JsonGenerator generator) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator) throws JsonIOException;
/**
* Return the bean or collection as JSON string using FetchPath.
@@ -231,15 +230,15 @@ public interface JsonContext {
void toJson(Object value, Writer writer, FetchPath fetchPath) throws JsonIOException;
/**
* Write the bean or collection to the JsonGenerator using the FetchPath.
* Write the bean or collection to the JsonWriter using the FetchPath.
*/
void toJson(Object value, JsonGenerator generator, FetchPath fetchPath) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator, FetchPath fetchPath) throws JsonIOException;
/**
* Deprecated in favour of using PathProperties by itself.
* Write json to the JsonGenerator using the JsonWriteOptions.
* Write json to the JsonWriter using the JsonWriteOptions.
*/
void toJson(Object value, JsonGenerator generator, JsonWriteOptions options) throws JsonIOException;
void toJson(Object value, io.avaje.json.JsonWriter generator, JsonWriteOptions options) throws JsonIOException;
/**
* Deprecated in favour of using PathProperties by itself.
@@ -264,27 +263,27 @@ public interface JsonContext {
boolean isSupportedType(Type genericType);
/**
* Create and return a new JsonGenerator for the given writer.
* Create and return a new JsonWriter for the given writer.
*
* @throws JsonIOException When IOException occurs
*/
JsonGenerator createGenerator(Writer writer) throws JsonIOException;
io.avaje.json.JsonWriter createGenerator(Writer writer) throws JsonIOException;
/**
* Create and return a new JsonParser for the given reader.
* Create and return a new JsonReader for the given reader.
*
* @throws JsonIOException When IOException occurs
*/
JsonParser createParser(Reader reader) throws JsonIOException;
JsonReader createParser(Reader reader) throws JsonIOException;
/**
* Write a scalar types known to Ebean to Jackson.
* Write scalar types known to Ebean to JsonWriter.
* <p>
* Ebean has built in support for java8 and Joda types as well as the other
* standard JDK types like URI, URL, UUID etc. This is a fast simple way to
* write any of those types to Jackson.
* write any of those types.
* </p>
*/
void writeScalar(JsonGenerator generator, Object scalarValue) throws IOException;
void writeScalar(io.avaje.json.JsonWriter generator, Object scalarValue) throws IOException;
}
@@ -1,19 +1,17 @@
package io.ebean.text.json;
import com.fasterxml.jackson.core.JsonGenerator;
import java.io.InputStream;
import java.math.BigDecimal;
/**
* Wraps an underlying JsonGenerator taking into account null suppression and exposing isIncludeEmpty() etc.
* Wraps an underlying JsonWriter taking into account null suppression and exposing isIncludeEmpty() etc.
*/
public interface JsonWriter {
/**
* Return the Jackson core JsonGenerator.
* Return the underlying JsonWriter.
*/
JsonGenerator gen();
io.avaje.json.JsonWriter gen();
/**
* Return true if null values should be included in JSON output.
+1 -1
View File
@@ -8,6 +8,7 @@ module io.ebean.api {
requires transitive java.sql;
requires transitive io.avaje.config;
requires transitive io.avaje.json;
requires transitive org.jspecify;
requires transitive jakarta.persistence.api;
requires transitive io.ebean.annotation;
@@ -16,7 +17,6 @@ module io.ebean.api {
requires static org.slf4j;
requires static io.ebean.types;
requires static com.fasterxml.jackson.core;
requires static com.fasterxml.jackson.databind;
exports io.ebean;
@@ -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);
@@ -0,0 +1,93 @@
package io.ebean.meta;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MetricNamingV2Test {
private MetricNamingV2.Mapped map(String name, String beanType) {
return MetricNamingV2.map(name, beanType);
}
@Test
void orm_withBeanType() {
MetricNamingV2.Mapped m = map("orm.Customer.findList", "Customer");
assertThat(m.name()).isEqualTo("ebean.query");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList,type:Customer");
}
@Test
void orm_withoutBeanType() {
MetricNamingV2.Mapped m = map("orm.Customer.findList", null);
assertThat(m.name()).isEqualTo("ebean.query");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.findList");
}
@Test
void dto_andSql() {
assertThat(map("dto.CustomerDto.findRecent", "CustomerDto").tags())
.isEqualTo("kind:dto,label:CustomerDto.findRecent,type:CustomerDto");
assertThat(map("sql.query.fooBar", "Customer").tags())
.isEqualTo("kind:sql,label:query.fooBar,type:Customer");
}
@Test
void iud() {
MetricNamingV2.Mapped m = map("iud.User.save", null);
assertThat(m.name()).isEqualTo("ebean.dml");
assertThat(m.tags()).isEqualTo("label:User.save");
}
@Test
void txn_named_and_plain() {
assertThat(map("txn.named.ProcessJob", null).name()).isEqualTo("ebean.txn");
assertThat(map("txn.named.ProcessJob", null).tags()).isEqualTo("label:ProcessJob");
assertThat(map("txn.main", null).tags()).isEqualTo("label:main");
}
@Test
void l2_regionAndOp() {
MetricNamingV2.Mapped m = map("l2.customer.hit", null);
assertThat(m.name()).isEqualTo("ebean.l2");
assertThat(m.tags()).isEqualTo("op:hit,region:customer");
}
@Test
void l2_opOnly() {
assertThat(map("l2.hit", null).tags()).isEqualTo("op:hit");
}
@Test
void unrecognisedPrefix_isOther() {
MetricNamingV2.Mapped m = map("l2n.Customer.hit", null);
assertThat(m.name()).isEqualTo("ebean.other");
assertThat(m.tags()).isEqualTo("label:l2n.Customer.hit");
}
@Test
void noDot_isOther() {
assertThat(map("jvm", null).name()).isEqualTo("ebean.other");
assertThat(map("jvm", null).tags()).isEqualTo("label:jvm");
}
@Test
void nullOrEmpty() {
assertThat(map(null, null).name()).isEqualTo("ebean.other");
assertThat(map(null, null).tags()).isEmpty();
assertThat(map("", null).tags()).isEmpty();
}
@Test
void sanitisesReservedChars() {
MetricNamingV2.Mapped m = map("orm.Customer.weird", "Cust:om,er");
assertThat(m.tags()).isEqualTo("kind:orm,label:Customer.weird,type:Cust_om_er");
}
@Test
void tagsAreSortedByKey() {
// kind < label < type alphabetically regardless of build order
assertThat(map("orm.X.find", "Bean").tags())
.isEqualTo("kind:orm,label:X.find,type:Bean");
}
}
@@ -0,0 +1,166 @@
package io.ebean.meta;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class MetricsAsJsonV2Test {
@Test
void writeV2_usesFamilyNamesAndTags() {
ServerMetrics metrics = new FakeServerMetrics();
StringBuilder sb = new StringBuilder();
new MetricsAsJson(metrics).writeV2(sb);
String json = sb.toString();
assertThat(json).contains("\"db\":\"db1\"");
// query metric -> ebean.query with kind/type/label tags
assertThat(json).contains("\"name\":\"ebean.query\"");
assertThat(json).contains("\"tags\":\"kind:orm,label:Customer.findList,type:Customer\"");
// timed iud metric -> ebean.dml
assertThat(json).contains("\"name\":\"ebean.dml\"");
assertThat(json).contains("\"tags\":\"label:User.save\"");
// count metric (l2n not specially mapped) -> ebean.other
assertThat(json).contains("\"name\":\"ebean.other\"");
assertThat(json).contains("\"tags\":\"label:l2n.Customer.hit\"");
}
@Test
void write_v1_unchanged_usesFlatNames() {
ServerMetrics metrics = new FakeServerMetrics();
StringBuilder sb = new StringBuilder();
new MetricsAsJson(metrics).write(sb);
String json = sb.toString();
assertThat(json).contains("\"name\":\"orm.Customer.findList\"");
assertThat(json).contains("\"name\":\"iud.User.save\"");
assertThat(json).doesNotContain("\"tags\"");
}
static final class FakeServerMetrics implements ServerMetrics {
@Override
public String name() {
return "db1";
}
@Override
public ServerMetricsAsJson asJson() {
return new MetricsAsJson(this);
}
@Override
public List<MetricData> asData() {
return new java.util.ArrayList<>();
}
@Override
public List<MetaTimedMetric> timedMetrics() {
return new java.util.ArrayList<>(List.of(new FakeTimed("iud.User.save")));
}
@Override
public List<MetaQueryMetric> queryMetrics() {
return new java.util.ArrayList<>(List.of(new FakeQuery("orm.Customer.findList", Customer.class)));
}
@Override
public List<MetaCountMetric> countMetrics() {
return new java.util.ArrayList<>(List.of(new FakeCount("l2n.Customer.hit")));
}
}
static class Customer {
}
static class FakeTimed implements MetaTimedMetric {
private final String name;
FakeTimed(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public String location() {
return null;
}
@Override
public long count() {
return 3;
}
@Override
public long total() {
return 30;
}
@Override
public long max() {
return 20;
}
@Override
public long mean() {
return 10;
}
@Override
public boolean initialCollection() {
return false;
}
}
static final class FakeQuery extends FakeTimed implements MetaQueryMetric {
private final Class<?> type;
FakeQuery(String name, Class<?> type) {
super(name);
this.type = type;
}
@Override
public Class<?> type() {
return type;
}
@Override
public String label() {
return null;
}
@Override
public String sql() {
return null;
}
@Override
public String hash() {
return "h1";
}
}
static final class FakeCount implements MetaCountMetric {
private final String name;
FakeCount(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public long count() {
return 5;
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.10.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>16.10.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>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -125,13 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
</dependencies>
+5 -7
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</parent>
<artifactId>ebean-core-json</artifactId>
<name>ebean-core-json</name>
@@ -16,15 +16,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
</dependencies>
@@ -1,190 +1,170 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.mapper.JsonMapper;
import io.avaje.json.stream.JsonStream;
import io.ebean.service.SpiJsonService;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.*;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Utility that converts between JSON content and simple java Maps/Lists.
* <p>
* Backed by avaje {@link JsonMapper} using {@link EbeanJsonAdapter} which
* preserves Ebean's modify-aware collection and number semantics.
*/
public final class DJsonService implements SpiJsonService {
/**
* Write the nested Map/List as json.
*/
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private static final JsonMapper MAPPER = JsonMapper.builder().jsonStream(JSON_STREAM).build();
private static final JsonMapper.Type<Object> PLAIN = MAPPER.type(EbeanJsonAdapter.PLAIN);
private static final JsonMapper.Type<Object> MODIFY_AWARE = MAPPER.type(EbeanJsonAdapter.MODIFY_AWARE);
private static JsonMapper.Type<Object> type(boolean modifyAware) {
return modifyAware ? MODIFY_AWARE : PLAIN;
}
private static boolean blank(String content) {
return content == null || content.trim().isEmpty();
}
private static String readAll(Reader reader) throws IOException {
StringBuilder builder = new StringBuilder();
char[] buffer = new char[2048];
int len;
while ((len = reader.read(buffer)) != -1) {
builder.append(buffer, 0, len);
}
return builder.toString();
}
@Override
public String write(Object object) throws IOException {
return EJsonWriter.write(object);
StringWriter writer = new StringWriter();
write(object, writer);
return writer.toString();
}
/**
* Write the nested Map/List as json to the writer.
*/
@Override
public void write(Object object, Writer writer) throws IOException {
EJsonWriter.write(object, writer);
JsonWriter jsonWriter = JSON_STREAM.writer(writer);
jsonWriter.serializeNulls(true);
PLAIN.toJson(object, jsonWriter);
jsonWriter.flush();
}
/**
* Write the nested Map/List as json to the jsonGenerator.
*/
@Override
public void write(Object object, JsonGenerator jsonGenerator) throws IOException {
EJsonWriter.write(object, jsonGenerator);
public void write(Object object, JsonWriter jsonWriter) throws IOException {
PLAIN.toJson(object, jsonWriter);
}
/**
* Write the collection as json array to the jsonGenerator.
*/
@Override
public void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
EJsonWriter.writeCollection(collection, jsonGenerator);
public void writeCollection(Collection<Object> collection, JsonWriter jsonWriter) throws IOException {
EbeanJsonAdapter.writeCollection(jsonWriter, collection);
}
/**
* Parse the json and return as a Map additionally specifying if the returned map should be modify
* aware meaning that it can detect when it has been modified.
*/
@Override
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(json, modifyAware);
}
/**
* Parse the json and return as a Map.
*/
@Override
public Map<String, Object> parseObject(String json) throws IOException {
return EJsonReader.parseObject(json);
}
/**
* Parse the json and return as a Map taking a reader.
*/
@Override
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return EJsonReader.parseObject(reader, modifyAware);
}
/**
* Parse the json and return as a Map taking a reader.
*/
@Override
public Map<String, Object> parseObject(Reader reader) throws IOException {
return EJsonReader.parseObject(reader);
}
/**
* Parse the json and return as a Map taking a JsonParser.
*/
@Override
public Map<String, Object> parseObject(JsonParser parser) throws IOException {
return EJsonReader.parseObject(parser);
}
/**
* Parse the json and return as a Map taking a JsonParser and a starting token.
*
* <p>Used when the first token is checked to see if the value is null prior to calling this.
*/
@Override
public Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
return EJsonReader.parseObject(parser, token);
}
/**
* Parse the json and return as a modify aware List.
*/
@Override
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return EJsonReader.parseList(json, modifyAware);
}
/**
* Parse the json and return as a List.
*/
@Override
public List<Object> parseList(String json) throws IOException {
return EJsonReader.parseList(json);
}
/**
* Parse the json and return as a List taking a Reader.
*/
@Override
public List<Object> parseList(Reader reader) throws IOException {
return EJsonReader.parseList(reader);
}
/**
* Parse the json and return as a List taking a JsonParser.
*/
@Override
public List<Object> parseList(JsonParser parser) throws IOException {
return EJsonReader.parseList(parser, false);
}
/**
* Parse the json returning as a List taking into account the current token.
*/
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(JsonParser parser, JsonToken currentToken) throws IOException {
return (List<T>) EJsonReader.parse(parser, currentToken, false);
public Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return blank(json) ? null : (Map<String, Object>) type(modifyAware).fromJson(json);
}
@Override
public Map<String, Object> parseObject(String json) throws IOException {
return parseObject(json, false);
}
@Override
public Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return parseObject(readAll(reader), modifyAware);
}
@Override
public Map<String, Object> parseObject(Reader reader) throws IOException {
return parseObject(reader, false);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser) throws IOException {
return (Map<String, Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> parseObject(JsonReader parser, Token token) throws IOException {
return (Map<String, Object>) EbeanJsonAdapter.read(parser, token, false);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return blank(json) ? null : (List<T>) type(modifyAware).fromJson(json);
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(String json) throws IOException {
return (List<Object>) parseList(json, false);
}
@Override
public List<Object> parseList(Reader reader) throws IOException {
return parseList(readAll(reader));
}
@Override
@SuppressWarnings("unchecked")
public List<Object> parseList(JsonReader parser) throws IOException {
return (List<Object>) PLAIN.fromJson(parser);
}
@Override
@SuppressWarnings("unchecked")
public <T> List<T> parseList(JsonReader parser, Token currentToken) throws IOException {
return (List<T>) EbeanJsonAdapter.read(parser, currentToken, false);
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(String json) throws IOException {
return EJsonReader.parse(json);
return blank(json) ? null : PLAIN.fromJson(json);
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(Reader reader) throws IOException {
return EJsonReader.parse(reader);
return parse(readAll(reader));
}
/**
* Parse the json and return as a List or Map.
*/
@Override
public Object parse(JsonParser parser) throws IOException {
return EJsonReader.parse(parser);
public Object parse(JsonReader parser) throws IOException {
return PLAIN.fromJson(parser);
}
/**
* Parse the json returning a Set that might be modify aware.
*/
@Override
public <T> Set<T> parseSet(String json, boolean modifyAware) throws IOException {
List<T> list = parseList(json, modifyAware);
if (list == null) {
return null;
}
if (modifyAware) {
return ((ModifyAwareList<T>) list).asSet();
} else {
return new LinkedHashSet<>(list);
}
return new LinkedHashSet<>(list);
}
/**
* Parse the json returning as a Set taking into account the current token.
*/
@Override
public <T> Set<T> parseSet(JsonParser parser, JsonToken currentToken) throws IOException {
public <T> Set<T> parseSet(JsonReader parser, Token currentToken) throws IOException {
return new LinkedHashSet<>(parseList(parser, currentToken));
}
}
@@ -1,356 +0,0 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.ebean.ModifyAwareType;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.*;
final class EJsonReader {
static final JsonFactory json = new JsonFactory();
private final JsonParser parser;
private final boolean modifyAware;
private final ModifyAwareFlag modifyAwareOwner;
private int depth;
private Stack stack;
private Context currentContext;
EJsonReader(JsonParser parser, boolean modifyAware) {
this.parser = parser;
this.modifyAware = modifyAware;
this.modifyAwareOwner = modifyAware ? new ModifyAwareFlag() : null;
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(String json) throws IOException {
return (Map<String, Object>) parse(json);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader) throws IOException {
return (Map<String, Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(Reader reader, boolean modifyAware) throws IOException {
return (Map<String, Object>) parse(reader, modifyAware);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser) throws IOException {
return (Map<String, Object>) parse(parser);
}
@SuppressWarnings("unchecked")
static Map<String, Object> parseObject(JsonParser parser, JsonToken token) throws IOException {
return (Map<String, Object>) parse(parser, token, false);
}
@SuppressWarnings("unchecked")
static <T> List<T> parseList(String json, boolean modifyAware) throws IOException {
return (List<T>) parse(json, modifyAware);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(String json) throws IOException {
return (List<Object>) parse(json);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(Reader reader) throws IOException {
return (List<Object>) parse(reader);
}
@SuppressWarnings("unchecked")
static List<Object> parseList(JsonParser parser, boolean modifyAware) throws IOException {
return (List<Object>) parse(parser, modifyAware);
}
static Object parse(String json) throws IOException {
if (json == null) {
return null;
}
return parse(new StringReader(json));
}
static Object parse(String json, boolean modifyAware) throws IOException {
if (json == null) {
return null;
}
return parse(new StringReader(json), modifyAware);
}
static Object parse(Reader reader) throws IOException {
return parse(json.createParser(reader));
}
static Object parse(Reader reader, boolean modifyAware) throws IOException {
return parse(json.createParser(reader), modifyAware);
}
static Object parse(JsonParser parser) throws IOException {
return parse(parser, null, false);
}
static Object parse(JsonParser parser, boolean modifyAware) throws IOException {
return parse(parser, null, modifyAware);
}
static Object parse(JsonParser parser, JsonToken token, boolean modifyAware) throws IOException {
return new EJsonReader(parser, modifyAware).parseJson(token);
}
private void startArray() {
depth++;
stack.push(currentContext);
currentContext = modifyAware ? new ArrayContext(modifyAwareOwner) : new ArrayContext();
}
private void startObject() {
depth++;
stack.push(currentContext);
currentContext = modifyAware ? new ObjectContext(modifyAwareOwner) : new ObjectContext();
}
private void endArray() {
end();
}
private void endObject() {
end();
}
private void end() {
depth--;
if (!stack.isEmpty()) {
currentContext = stack.pop(currentContext);
}
if (modifyAwareOwner != null) {
modifyAwareOwner.setMarkedDirty(false);
}
}
private void setValue(Object value) {
currentContext.setValue(value);
}
private void setValueNull() {
currentContext.setValueNull();
}
private Object parseJson(JsonToken token) throws IOException {
if (token == null) {
token = parser.nextToken();
// if it is a simple value just return it
switch (token) {
case VALUE_NULL:
return null;
case VALUE_FALSE:
return Boolean.FALSE;
case VALUE_TRUE:
return Boolean.TRUE;
case VALUE_STRING:
return parser.getText();
case VALUE_NUMBER_INT:
return parser.getLongValue();
case VALUE_NUMBER_FLOAT:
return parser.getDecimalValue();
}
}
// it is a object or array, process the first JsonToken
stack = new Stack();
processJsonToken(token);
// process the rest of the object or array
while (depth > 0) {
token = parser.nextToken();
processJsonToken(token);
}
return currentContext.getValue();
}
/**
* Process the JsonToken for objects and arrays.
*/
private void processJsonToken(JsonToken token) throws IOException {
switch (token) {
case START_ARRAY:
startArray();
break;
case START_OBJECT:
startObject();
break;
case FIELD_NAME:
currentContext.setKey(parser.getCurrentName());
break;
case VALUE_STRING:
setValue(parser.getValueAsString());
break;
case VALUE_NUMBER_INT:
setValue(parser.getLongValue());
break;
case VALUE_NUMBER_FLOAT:
setValue(parser.getDecimalValue());
break;
case VALUE_TRUE:
setValue(Boolean.TRUE);
break;
case VALUE_FALSE:
setValue(Boolean.FALSE);
break;
case VALUE_NULL:
setValueNull();
break;
case END_OBJECT:
endObject();
break;
case END_ARRAY:
endArray();
break;
default:
break;
}
}
private static final class Stack {
private Context head;
private void push(Context context) {
if (context != null) {
context.next = head;
head = context;
}
}
private Context pop(Context endingContext) {
if (head == null) {
throw new NoSuchElementException();
}
Context temp = head;
head = head.next;
temp.popContext(endingContext);
return temp;
}
private boolean isEmpty() {
return head == null;
}
}
private abstract static class Context {
Context next;
abstract void popContext(Context temp);
abstract Object getValue();
abstract void setValue(Object value);
abstract void setKey(String key);
abstract void setValueNull();
}
private static class ObjectContext extends Context {
private final Map<String, Object> map;
private String key;
ObjectContext() {
map = new LinkedHashMap<>();
}
ObjectContext(ModifyAwareType owner) {
map = new ModifyAwareMap<>(owner, new LinkedHashMap<>());
}
@Override
public void popContext(Context temp) {
setValue(temp.getValue());
}
@Override
Object getValue() {
return map;
}
@Override
void setValue(Object value) {
map.put(key, value);
}
@Override
void setKey(String key) {
this.key = key;
}
@Override
void setValueNull() {
map.put(key, null);
}
}
private static class ArrayContext extends Context {
private final List<Object> values;
ArrayContext() {
values = new ArrayList<>();
}
ArrayContext(ModifyAwareType owner) {
values = new ModifyAwareList<>(owner, new ArrayList<>());
}
@Override
public void popContext(Context temp) {
values.add(temp.getValue());
}
@Override
Object getValue() {
return values;
}
@Override
void setValue(Object value) {
values.add(value);
}
@Override
void setValueNull() {
// ignore
}
@Override
void setKey(String key) {
// not expected
}
}
}
@@ -1,212 +0,0 @@
package io.ebeaninternal.json;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
final class EJsonWriter {
/**
* Base jsonFactory implementation used when it is not passed in.
*/
static final JsonFactory jsonFactory = new JsonFactory();
private final JsonGenerator jsonGenerator;
private EJsonWriter(JsonGenerator jsonGenerator) {
this.jsonGenerator = jsonGenerator;
}
static String write(Object object) throws IOException {
StringWriter writer = new StringWriter(200);
write(object, writer).close();
return writer.toString();
}
static JsonGenerator write(Object object, Writer writer) throws IOException {
JsonGenerator generator = jsonFactory.createGenerator(writer);
write(object, generator);
generator.flush();
return generator;
}
static void write(Object object, JsonGenerator jsonGenerator) {
new EJsonWriter(jsonGenerator).writeJson(object);
}
static void writeCollection(Collection<Object> collection, JsonGenerator jsonGenerator) throws IOException {
new EJsonWriter(jsonGenerator).writeCollection(null, collection);
}
private void writeJson(Object object) {
writeJson(null, object);
}
@SuppressWarnings("unchecked")
private void writeJson(String name, Object object) {
try {
if (object == null) {
writeNull(name);
} else if (object instanceof Number) {
writeNumber(name, (Number) object);
} else if (object instanceof String) {
writeString(name, (String) object);
} else if (object instanceof Map) {
writeMap(name, (Map<Object, Object>) object);
} else if (object instanceof Collection) {
writeCollection(name, (Collection<Object>) object);
} else if (object instanceof Boolean) {
writeBoolean(name, (Boolean) object);
} else if (object instanceof Date) {
writeDate(name, (Date) object);
} else if (object instanceof Map.Entry<?, ?>) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
writeJson(entry.getKey().toString(), entry.getValue());
} else {
writeString(name, object.toString());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeBoolean(String name, Boolean object) throws IOException {
if (name == null) {
jsonGenerator.writeBoolean(object);
} else {
jsonGenerator.writeBooleanField(name, object);
}
}
private void writeDate(String name, Date object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber(object.getTime());
} else {
jsonGenerator.writeNumberField(name, object.getTime());
}
}
private void writeNumber(String name, Number object) throws IOException {
if (object instanceof Long) {
writeLong(name, object);
} else if (object instanceof Integer) {
writeInteger(name, object);
} else if (object instanceof Double) {
writeDouble(name, object);
} else if (object instanceof BigDecimal) {
writeBigDecimal(name, object);
} else if (object instanceof BigInteger) {
writeBigInteger(name, object);
} else {
writeGeneralNumber(name, object);
}
}
private void writeGeneralNumber(String name, Number object) throws IOException {
writeBigDecimal(name, new BigDecimal(object.toString()));
}
private void writeBigDecimal(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((BigDecimal) object);
} else {
jsonGenerator.writeNumberField(name, (BigDecimal) object);
}
}
private void writeBigInteger(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((BigInteger) object);
} else {
jsonGenerator.writeNumberField(name, object.longValue());
}
}
private void writeDouble(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Double) object);
} else {
jsonGenerator.writeNumberField(name, (Double) object);
}
}
private void writeLong(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Long) object);
} else {
jsonGenerator.writeNumberField(name, (Long) object);
}
}
private void writeInteger(String name, Number object) throws IOException {
if (name == null) {
jsonGenerator.writeNumber((Integer) object);
} else {
jsonGenerator.writeNumberField(name, (Integer) object);
}
}
private void writeNull(String name) throws IOException {
if (name == null) {
jsonGenerator.writeNull();
} else {
jsonGenerator.writeNullField(name);
}
}
private void writeString(String name, String object) throws IOException {
if (name == null) {
jsonGenerator.writeString(object);
} else {
jsonGenerator.writeStringField(name, object);
}
}
private void writeCollection(String name, Collection<Object> collection) throws IOException {
if (name != null) {
jsonGenerator.writeFieldName(name);
}
jsonGenerator.writeStartArray();
for (Object object : collection) {
writeJson(null, object);
}
jsonGenerator.writeEndArray();
}
private void writeMap(String name, Map<Object, Object> map) throws IOException {
if (name != null) {
jsonGenerator.writeFieldName(name);
}
jsonGenerator.writeStartObject();
Set<Entry<Object, Object>> entrySet = map.entrySet();
for (Entry<Object, Object> entry : entrySet) {
writeJson(entry.getKey().toString(), entry.getValue());
}
jsonGenerator.writeEndObject();
}
}
@@ -0,0 +1,181 @@
package io.ebeaninternal.json;
import io.avaje.json.JsonAdapter;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.ModifyAwareType;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Ebean specific {@link JsonAdapter} that materializes JSON into plain Java
* Map/List/scalar values - optionally wrapped in modify-aware collections so
* that mutations after load are tracked as dirty.
* <p>
* This consolidates the prior EJsonReader/EJsonWriter behavior into a single
* adapter that plugs into avaje {@code JsonMapper}.
*/
final class EbeanJsonAdapter implements JsonAdapter<Object> {
static final EbeanJsonAdapter PLAIN = new EbeanJsonAdapter(false);
static final EbeanJsonAdapter MODIFY_AWARE = new EbeanJsonAdapter(true);
private static final JsonStream JSON_STREAM = JsonStream.builder().build();
private final boolean modifyAware;
private EbeanJsonAdapter(boolean modifyAware) {
this.modifyAware = modifyAware;
}
@Override
public Object fromJson(JsonReader reader) {
return read(reader, null, modifyAware);
}
@Override
public void toJson(JsonWriter writer, Object value) {
write(writer, value);
}
/**
* Read a value honoring an explicitly supplied current token (or current token when null).
*/
static Object read(JsonReader parser, Token token, boolean modifyAware) {
ModifyAwareType owner = modifyAware ? new ModifyAwareFlag() : null;
Token effectiveToken = token == null ? parser.currentToken() : token;
Object value;
if (effectiveToken == null) {
value = parseRawJson(parser.readRaw(), owner);
} else {
value = parseValue(parser, effectiveToken, owner);
}
if (owner != null) {
owner.setMarkedDirty(false);
}
return value;
}
private static Object parseValue(JsonReader parser, Token token, ModifyAwareType owner) {
if (token == null) {
token = parser.currentToken();
if (token == null) {
if (parser.isNullValue()) {
return null;
}
return parseRawJson(parser.readRaw(), owner);
}
}
switch (token) {
case BEGIN_OBJECT:
return parseObjectValue(parser, owner);
case BEGIN_ARRAY:
return parseArrayValue(parser, owner);
case NUMBER:
BigDecimal value = parser.readDecimal();
return value.scale() <= 0 ? value.longValue() : value;
case STRING:
return parser.readString();
case BOOLEAN:
return parser.readBoolean();
case NULL:
parser.isNullValue();
return null;
default:
return parseRawJson(parser.readRaw(), owner);
}
}
private static Object parseRawJson(String json, ModifyAwareType owner) {
if (json == null) {
return null;
}
String content = json.trim();
if (content.isEmpty()) {
return null;
}
try (JsonReader parser = JSON_STREAM.reader(content)) {
return parseValue(parser, parser.currentToken(), owner);
}
}
private static Map<String, Object> parseObjectValue(JsonReader parser, ModifyAwareType owner) {
Map<String, Object> map = owner == null
? new LinkedHashMap<>()
: new ModifyAwareMap<>(owner, new LinkedHashMap<>());
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
map.put(fieldName, parseValue(parser, parser.currentToken(), owner));
}
parser.endObject();
return map;
}
private static List<Object> parseArrayValue(JsonReader parser, ModifyAwareType owner) {
List<Object> list = owner == null
? new ArrayList<>()
: new ModifyAwareList<>(owner, new ArrayList<>());
parser.beginArray();
while (parser.hasNextElement()) {
list.add(parseValue(parser, parser.currentToken(), owner));
}
parser.endArray();
return list;
}
/**
* Write the value to an existing JsonWriter (used for the raw stream paths).
*/
static void write(JsonWriter jsonWriter, Object object) {
if (object == null) {
jsonWriter.nullValue();
} else if (object instanceof String) {
jsonWriter.value((String) object);
} else if (object instanceof Integer) {
jsonWriter.value((Integer) object);
} else if (object instanceof Long) {
jsonWriter.value((Long) object);
} else if (object instanceof Double) {
jsonWriter.value((Double) object);
} else if (object instanceof Float) {
jsonWriter.value((Float) object);
} else if (object instanceof BigDecimal) {
jsonWriter.value((BigDecimal) object);
} else if (object instanceof Boolean) {
jsonWriter.value((Boolean) object);
} else if (object instanceof Map<?, ?>) {
writeMap(jsonWriter, (Map<?, ?>) object);
} else if (object instanceof Collection<?>) {
writeCollection(jsonWriter, (Collection<?>) object);
} else {
jsonWriter.value(object.toString());
}
}
private static void writeMap(JsonWriter jsonWriter, Map<?, ?> map) {
jsonWriter.beginObject();
for (Map.Entry<?, ?> entry : map.entrySet()) {
jsonWriter.name((String) entry.getKey());
write(jsonWriter, entry.getValue());
}
jsonWriter.endObject();
}
static void writeCollection(JsonWriter jsonWriter, Collection<?> collection) {
jsonWriter.beginArray();
for (Object element : collection) {
write(jsonWriter, element);
}
jsonWriter.endArray();
}
}
@@ -1,8 +1,7 @@
module io.ebean.core.json {
requires io.ebean.api;
requires transitive com.fasterxml.jackson.core;
requires transitive io.avaje.json;
exports io.ebeaninternal.json to io.ebean.test, io.ebean.core;
provides io.ebean.service.BootstrapService with io.ebeaninternal.json.DJsonService;
+5 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.10.0</version>
<version>18.1.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,14 +16,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- Provided scope for Postgres JSON/JSONB support -->
@@ -1,7 +1,7 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import io.ebean.text.StringFormatter;
import io.ebean.text.StringParser;
@@ -177,13 +177,44 @@ public interface ScalarType<T> extends StringParser, StringFormatter, ScalarData
void writeData(DataOutput dataOutput, T value) throws IOException;
/**
* Read the value from JsonParser.
* Read the value from JsonReader.
*/
T jsonRead(JsonParser parser) throws IOException;
default T jsonRead(JsonReader parser) throws IOException {
JsonReader.Token token = parser.currentToken();
if (token == JsonReader.Token.NULL) {
parser.isNullValue();
return null;
}
if (token == JsonReader.Token.STRING) {
return parse(parser.readString());
}
return parse(parser.readRaw());
}
/**
* Write the value to the JsonGenerator.
* Write the value to the JsonWriter.
*/
void jsonWrite(JsonGenerator writer, T value) throws IOException;
default void jsonWrite(JsonWriter writer, T value) throws IOException {
if (value == null) {
writer.nullValue();
return;
}
String formatted = formatValue(value);
if (formatted == null) {
writer.nullValue();
return;
}
DocPropertyType docType = docType();
if (docType == DocPropertyType.OBJECT || docType == DocPropertyType.LIST || docType == DocPropertyType.ROOT || likelyRawJson(formatted)) {
writer.rawValue(formatted);
} else {
writer.value(formatted);
}
}
private static boolean likelyRawJson(String formatted) {
String trimmed = formatted.trim();
return !trimmed.isEmpty() && (trimmed.charAt(0) == '{' || trimmed.charAt(0) == '[');
}
}
@@ -1,8 +1,8 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.ebean.config.JsonConfig;
import io.ebean.core.type.DataBinder;
import io.ebean.core.type.DataReader;
@@ -83,20 +83,31 @@ public abstract class ScalarTypeBaseDate<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
if (JsonToken.VALUE_NUMBER_INT == parser.getCurrentToken()) {
return convertFromMillis(parser.getLongValue());
} else {
return convertFromDate(Date.valueOf(parser.getText()));
public T jsonRead(JsonReader parser) throws IOException {
Token token = parser.currentToken();
if (Token.NUMBER == token) {
return convertFromMillis(parser.readLong());
}
if (Token.STRING == token) {
return convertFromDate(Date.valueOf(parser.readString()));
}
String raw = parser.readRaw();
if (raw == null || "null".equals(raw)) {
return null;
}
if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
return convertFromDate(Date.valueOf(raw.substring(1, raw.length() - 1)));
}
return convertFromMillis(Long.parseLong(raw));
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
public void jsonWrite(JsonWriter writer, T value) throws IOException {
if (mode == JsonConfig.Date.ISO8601) {
writer.writeString(toIsoFormat(value));
writer.value(toIsoFormat(value));
} else {
writer.writeNumber(convertToMillis(value));
writer.value(convertToMillis(value));
}
}
@@ -1,7 +1,8 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.avaje.json.JsonWriter;
import io.ebean.config.JsonConfig;
import java.io.DataInput;
@@ -99,35 +100,53 @@ public abstract class ScalarTypeBaseDateTime<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
switch (parser.getCurrentToken()) {
case VALUE_NUMBER_INT: {
return convertFromMillis(parser.getLongValue());
}
case VALUE_NUMBER_FLOAT: {
BigDecimal value = parser.getDecimalValue();
Timestamp timestamp = ScalarTypeUtils.toTimestamp(value);
return convertFromTimestamp(timestamp);
}
default: {
return fromJsonISO8601(parser.getText());
}
public T jsonRead(JsonReader parser) throws IOException {
Token token = parser.currentToken();
if (token == Token.NUMBER) {
return readNumber(parser.readDecimal());
}
if (token == Token.STRING) {
return fromStringValue(parser.readString());
}
String raw = parser.readRaw();
if (raw == null || "null".equals(raw)) {
return null;
}
if (raw.length() > 1 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
return fromStringValue(raw.substring(1, raw.length() - 1));
}
return readNumber(new BigDecimal(raw));
}
private T fromStringValue(String value) {
if (value.indexOf('-') == -1 && Character.isDigit(value.charAt(0))) {
return readNumber(new BigDecimal(value));
}
return fromJsonISO8601(value);
}
private T readNumber(BigDecimal value) {
if (value.scale() <= 0) {
return convertFromMillis(value.longValue());
}
Timestamp timestamp = ScalarTypeUtils.toTimestamp(value);
return convertFromTimestamp(timestamp);
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
public void jsonWrite(JsonWriter writer, T value) throws IOException {
switch (mode) {
case ISO8601: {
writer.writeString(toJsonISO8601(value));
writer.value(toJsonISO8601(value));
break;
}
case NANOS: {
writer.writeNumber(toJsonNanos(value));
writer.value(toJsonNanos(value));
break;
}
default: {
writer.writeNumber(convertToMillis(value));
writer.value(convertToMillis(value));
}
}
}
@@ -1,7 +1,7 @@
package io.ebean.core.type;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonWriter;
import java.io.DataInput;
import java.io.DataOutput;
@@ -104,13 +104,16 @@ public abstract class ScalarTypeBaseVarchar<T> extends ScalarTypeBase<T> {
}
@Override
public T jsonRead(JsonParser parser) throws IOException {
return parse(parser.getValueAsString());
public T jsonRead(JsonReader parser) throws IOException {
if (parser.isNullValue()) {
return null;
}
return parse(parser.readString());
}
@Override
public void jsonWrite(JsonGenerator writer, T value) throws IOException {
writer.writeString(format(value));
public void jsonWrite(JsonWriter writer, T value) throws IOException {
writer.value(format(value));
}
@Override
@@ -4,8 +4,7 @@ module io.ebean.core.type {
requires transitive java.sql;
requires transitive io.ebean.api;
requires transitive io.avaje.json;
requires static org.postgresql.jdbc;
requires static com.fasterxml.jackson.core;
}
+7 -15
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.10.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>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
</dependency>
<dependency>
@@ -138,14 +138,6 @@
<!-- <optional>true</optional>-->
<!-- </dependency>-->
<!-- Jackson core used internally by Ebean -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -165,21 +157,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.10.0</version>
<version>18.1.0</version>
<scope>test</scope>
</dependency>
@@ -1,6 +1,6 @@
package io.ebeaninternal.api;
import com.fasterxml.jackson.core.JsonGenerator;
import io.avaje.json.JsonWriter;
import io.ebean.plugin.BeanType;
import io.ebean.text.json.JsonContext;
import io.ebean.text.json.JsonWriteOptions;
@@ -17,7 +17,7 @@ public interface SpiJsonContext extends JsonContext {
/**
* Create a Json Writer for writing beans as JSON.
*/
SpiJsonWriter createJsonWriter(JsonGenerator gen, JsonWriteOptions options);
SpiJsonWriter createJsonWriter(JsonWriter gen, JsonWriteOptions options);
/**
* Create a Json Writer for writing beans as JSON supplying a writer.
@@ -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);
}
}
}
}
@@ -1,8 +1,8 @@
package io.ebeaninternal.api.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.avaje.json.JsonReader;
import io.avaje.json.JsonReader.Token;
import io.ebean.bean.EntityBean;
import io.ebean.bean.PersistenceContext;
import io.ebeaninternal.server.deploy.BeanDescriptor;
@@ -14,7 +14,9 @@ public interface SpiJsonReader {
PersistenceContext persistenceContext();
SpiJsonReader forJson(JsonParser moreJson);
SpiJsonReader forJson(JsonReader moreJson);
SpiJsonReader forJson(String moreJson);
<T> void persistenceContextPut(Object beanId, T currentBean);
@@ -22,9 +24,9 @@ public interface SpiJsonReader {
ObjectMapper mapper();
JsonParser parser();
JsonReader parser();
JsonToken nextToken() throws IOException;
Token nextToken() throws IOException;
void pushPath(String path);
@@ -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.
*/
@@ -1,7 +1,7 @@
package io.ebeaninternal.server.changelog;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import io.avaje.json.JsonWriter;
import io.avaje.json.stream.JsonStream;
import io.ebean.event.changelog.BeanChange;
import io.ebean.event.changelog.ChangeSet;
import io.ebean.event.changelog.ChangeType;
@@ -15,13 +15,13 @@ import java.util.Map;
*/
final class ChangeJsonBuilder {
private final JsonFactory jsonFactory = new JsonFactory();
private final JsonStream jsonStream = JsonStream.builder().build();
/**
* Write the bean change as JSON.
*/
void writeBeanJson(Writer writer, BeanChange bean, ChangeSet changeSet) throws IOException {
try (JsonGenerator generator = jsonFactory.createGenerator(writer)) {
try (JsonWriter generator = jsonStream.writer(writer)) {
writeBeanChange(generator, bean, changeSet);
generator.flush();
}
@@ -30,58 +30,67 @@ final class ChangeJsonBuilder {
/**
* Write the bean change as JSON document containing the transaction header details.
*/
private void writeBeanChange(JsonGenerator gen, BeanChange bean, ChangeSet changeSet) throws IOException {
gen.writeStartObject();
gen.writeNumberField("ts", bean.getEventTime());
gen.writeStringField("change", bean.getEvent().getCode());
gen.writeStringField("type", bean.getType());
gen.writeStringField("id", bean.getId().toString());
private void writeBeanChange(JsonWriter gen, BeanChange bean, ChangeSet changeSet) {
gen.beginObject();
gen.name("ts");
gen.value(bean.getEventTime());
gen.name("change");
gen.value(bean.getEvent().getCode());
gen.name("type");
gen.value(bean.getType());
gen.name("id");
gen.value(bean.getId().toString());
if (bean.getTenantId() != null) {
gen.writeStringField("tenantId", bean.getTenantId().toString());
gen.name("tenantId");
gen.value(bean.getTenantId().toString());
}
writeBeanTransactionDetails(gen, changeSet);
writeBeanValues(gen, bean);
gen.writeEndObject();
gen.endObject();
}
/**
* Denormalise by writing the transaction header details.
*/
private void writeBeanTransactionDetails(JsonGenerator gen, ChangeSet changeSet) throws IOException {
private void writeBeanTransactionDetails(JsonWriter gen, ChangeSet changeSet) {
String source = changeSet.getSource();
if (source != null) {
gen.writeStringField("source", source);
gen.name("source");
gen.value(source);
}
String userId = changeSet.getUserId();
if (userId != null) {
gen.writeStringField("userId", userId);
gen.name("userId");
gen.value(userId);
}
String userIpAddress = changeSet.getUserIpAddress();
if (userIpAddress != null) {
gen.writeStringField("userIpAddress", userIpAddress);
gen.name("userIpAddress");
gen.value(userIpAddress);
}
Map<String, String> userContext = changeSet.getUserContext();
if (userContext != null && !userContext.isEmpty()) {
gen.writeObjectFieldStart("userContext");
gen.name("userContext");
gen.beginObject();
for (Map.Entry<String, String> entry : userContext.entrySet()) {
gen.writeStringField(entry.getKey(), entry.getValue());
gen.name(entry.getKey());
gen.value(entry.getValue());
}
gen.writeEndObject();
gen.endObject();
}
}
/**
* For insert and update write the new/old values.
*/
private void writeBeanValues(JsonGenerator gen, BeanChange bean) throws IOException {
private void writeBeanValues(JsonWriter gen, BeanChange bean) {
if (bean.getEvent() != ChangeType.DELETE) {
gen.writeFieldName("data");
gen.writeRaw(":");
gen.writeRaw(bean.getData());
gen.name("data");
gen.rawValue(bean.getData());
String oldData = bean.getOldData();
if (oldData != null) {
gen.writeRaw(",\"oldData\":");
gen.writeRaw(oldData);
gen.name("oldData");
gen.rawValue(oldData);
}
}
}
@@ -188,7 +188,10 @@ final class DefaultBeanLoader {
query.setLazyLoadProperty(ebi.lazyLoadProperty());
if (draft) {
query.asDraft();
} else if (mode == SpiQuery.Mode.LAZYLOAD_BEAN && desc.isSoftDelete()) {
} else if (desc.isSoftDelete()
&& (mode == SpiQuery.Mode.LAZYLOAD_BEAN || mode == SpiQuery.Mode.REFRESH_BEAN)) {
// include soft-deleted rows when lazy loading or refreshing so a
// refresh() on a soft-deleted bean can reload it (issue #3641)
query.setIncludeSoftDeletes();
}
if (embeddedOwnerIndex > -1) {
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.core;
import com.fasterxml.jackson.core.JsonFactory;
import io.avaje.json.stream.JsonStream;
import io.ebean.DatabaseBuilder;
import io.ebean.ExpressionFactory;
import io.ebean.annotation.Platform;
@@ -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;
@@ -89,7 +90,7 @@ public final class InternalConfiguration {
private final boolean jacksonCorePresent;
private final ExpressionFactory expressionFactory;
private final SpiBackgroundExecutor backgroundExecutor;
private final JsonFactory jsonFactory;
private final JsonStream jsonStream;
private final DocStoreFactory docStoreFactory;
private final List<Plugin> plugins = new ArrayList<>();
private final MultiValueBind multiValueBind;
@@ -108,7 +109,7 @@ public final class InternalConfiguration {
this.tableModState = new TableModState();
this.logManager = initLogManager();
this.docStoreFactory = initDocStoreFactory(service(DocStoreFactory.class));
this.jsonFactory = config.getJsonFactory();
this.jsonStream = config.getJsonStream();
this.clusterManager = clusterManager;
this.backgroundExecutor = backgroundExecutor;
this.bootupClasses = bootupClasses;
@@ -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();
@@ -292,7 +294,7 @@ public final class InternalConfiguration {
}
SpiJsonContext createJsonContext(SpiEbeanServer server) {
return jacksonCorePresent ? new DJsonContext(server, jsonFactory, typeManager) : null;
return jacksonCorePresent ? new DJsonContext(server, jsonStream, typeManager) : null;
}
AutoTuneService createAutoTuneService(SpiEbeanServer server) {
@@ -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);
}
}
}
@@ -2,8 +2,11 @@ package io.ebeaninternal.server.deploy;
import io.ebean.PersistenceIOException;
import io.ebean.bean.BeanDiffVisitor;
import io.ebean.config.JsonConfig;
import io.ebeaninternal.api.json.SpiJsonWriter;
import io.ebeaninternal.server.json.WriteJson;
import io.ebeaninternal.server.util.ArrayStack;
import io.avaje.json.stream.JsonStream;
import java.io.IOException;
import java.io.StringWriter;
@@ -13,26 +16,18 @@ import java.io.StringWriter;
*/
final class BeanChangeJson implements BeanDiffVisitor {
private final StringWriter newData;
private final StringWriter oldData;
private final SpiJsonWriter newJson;
private final SpiJsonWriter oldJson;
private final StringWriter data;
private final SpiJsonWriter json;
private final boolean writeNew;
private final ArrayStack<BeanDescriptor<?>> stack = new ArrayStack<>();
private BeanDescriptor<?> descriptor;
BeanChangeJson(BeanDescriptor<?> descriptor, boolean statelessUpdate) {
BeanChangeJson(BeanDescriptor<?> descriptor, boolean writeNew) {
this.descriptor = descriptor;
this.newData = new StringWriter(200);
this.newJson = descriptor.createJsonWriter(newData);
newJson.writeStartObject();
if (statelessUpdate) {
this.oldJson = null;
this.oldData = null;
} else {
this.oldData = new StringWriter(200);
this.oldJson = descriptor.createJsonWriter(oldData);
oldJson.writeStartObject();
}
this.writeNew = writeNew;
this.data = new StringWriter(200);
this.json = new WriteJson(JsonStream.builder().build().writer(data), JsonConfig.Include.ALL);
json.writeStartObject();
}
@Override
@@ -40,10 +35,7 @@ final class BeanChangeJson implements BeanDiffVisitor {
try {
BeanProperty prop = descriptor.propertiesIndex[position];
if (prop.isDbUpdatable()) {
prop.jsonWriteValue(newJson, newVal);
if (oldJson != null) {
prop.jsonWriteValue(oldJson, oldVal);
}
prop.jsonWriteValue(json, writeNew ? newVal : oldVal);
}
} catch (IOException e) {
throw new PersistenceIOException(e);
@@ -55,18 +47,12 @@ final class BeanChangeJson implements BeanDiffVisitor {
stack.push(descriptor);
BeanPropertyAssocOne<?> embedded = (BeanPropertyAssocOne<?>)descriptor.propertiesIndex[position];
descriptor = embedded.targetDescriptor();
newJson.writeStartObject(embedded.name());
if (oldJson != null) {
oldJson.writeStartObject(embedded.name());
}
json.writeStartObject(embedded.name());
}
@Override
public void visitPop() {
newJson.writeEndObject();
if (oldJson != null) {
oldJson.writeEndObject();
}
json.writeEndObject();
descriptor = stack.pop();
}
@@ -75,28 +61,14 @@ final class BeanChangeJson implements BeanDiffVisitor {
*/
void flush() {
try {
newJson.writeEndObject();
newJson.flush();
if (oldJson != null) {
oldJson.writeEndObject();
oldJson.flush();
}
json.writeEndObject();
json.flush();
} catch (IOException e) {
throw new PersistenceIOException(e);
}
}
/**
* Return the new values JSON.
*/
String newJson() {
return newData.toString();
}
/**
* Return the old values JSON.
*/
String oldJson() {
return oldData == null ? null : oldData.toString();
String json() {
return data.toString();
}
}
@@ -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)) {
@@ -792,10 +809,17 @@ public class BeanDescriptor<T> implements BeanType<T>, STreeType, SpiBeanType {
*/
private BeanChange updateBeanChange(PersistRequestBean<T> request) {
try {
BeanChangeJson changeJson = new BeanChangeJson(this, request.isStatelessUpdate());
request.intercept().addDirtyPropertyValues(changeJson);
changeJson.flush();
return beanChange(ChangeType.UPDATE, request.beanId(), changeJson.newJson(), changeJson.oldJson());
BeanChangeJson newValues = new BeanChangeJson(this, true);
request.intercept().addDirtyPropertyValues(newValues);
newValues.flush();
String oldData = null;
if (!request.isStatelessUpdate()) {
BeanChangeJson oldValues = new BeanChangeJson(this, false);
request.intercept().addDirtyPropertyValues(oldValues);
oldValues.flush();
oldData = oldValues.json();
}
return beanChange(ChangeType.UPDATE, request.beanId(), newValues.json(), oldData);
} catch (RuntimeException e) {
log.log(ERROR, "Failed to write ChangeLog entry for update", e);
return null;
@@ -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);
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.EntityBean;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -49,25 +49,25 @@ class BeanDescriptorElementEmbeddedMap<T> extends BeanDescriptorElementEmbedded<
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
String fieldName = parser.nextFieldName();
if (fieldName == null) {
break;
}
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
if (stringKey) {
parser.nextToken();
Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here?
add.addKeyValue(fieldName, val);
} else {
parser.nextFieldName();
parser.beginObject();
parser.nextField();
Object key = scalarTypeKey.jsonRead(parser);
parser.nextFieldName();
parser.nextField();
Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here?
parser.endObject();
add.addKeyValue(key, val);
}
} while (true);
}
parser.endObject();
return add.collection();
}
@@ -1,7 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.avaje.json.JsonReader;
import io.ebean.PersistenceIOException;
import io.ebean.SqlUpdate;
import io.ebean.bean.EntityBean;
@@ -40,15 +39,13 @@ class BeanDescriptorElementScalar<T> extends BeanDescriptorElement<T> {
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
JsonToken token = parser.nextToken();
if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) {
break;
}
parser.beginArray();
while (parser.hasNextElement()) {
add.addElement(scalarType.jsonRead(parser));
} while (true);
}
parser.endArray();
return add.collection();
}
@@ -1,6 +1,6 @@
package io.ebeaninternal.server.deploy;
import com.fasterxml.jackson.core.JsonParser;
import io.avaje.json.JsonReader;
import io.ebean.bean.EntityBean;
import io.ebean.core.type.ScalarType;
import io.ebeaninternal.api.json.SpiJsonReader;
@@ -49,25 +49,25 @@ class BeanDescriptorElementScalarMap<T> extends BeanDescriptorElement<T> {
@Override
public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException {
JsonParser parser = readJson.parser();
JsonReader parser = readJson.parser();
ElementCollector add = elementHelp.createCollector();
do {
String fieldName = parser.nextFieldName();
if (fieldName == null) {
break;
}
parser.beginObject();
while (parser.hasNextField()) {
String fieldName = parser.nextField();
if (stringKey) {
parser.nextToken();
Object val = scalarTypeVal.jsonRead(parser);
add.addKeyValue(fieldName, val);
} else {
parser.nextFieldName();
parser.beginObject();
parser.nextField();
Object key = scalarTypeKey.jsonRead(parser);
parser.nextFieldName();
parser.nextField();
Object val = scalarTypeVal.jsonRead(parser);
parser.endObject();
add.addKeyValue(key, val);
}
} while (true);
}
parser.endObject();
return add.collection();
}

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