Compare commits

..
46 Commits
Author SHA1 Message Date
Rob Bygrave ebb0305cf3 Version 16.5.0 2026-04-12 22:31:24 +12:00
Rob Bygrave d03d36bba4 Update ebean-agent 2026-04-12 22:27:52 +12:00
Rob BygraveandGitHub 88408edd24 Bump ebean-datasource dependency to 10.5 with increase default prepared statement cache (#3749)
Default prepared statement cache increased from 100 to 300. This could have the
effect of increased memory consumption for applications traded off with potentially
improved performance due to me cached prepared statements for larger applications.

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

* Fix query generation for extra ManyToMany joins

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

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

* Put the contextClear() calls into a finally

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

---------

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

* Changes DeployCreateProperties to allow processing of inheritance hierarchies with generics

* adds tests for ebean-querybean

* bumps querybean-generator version

* fixes tests by changing table names for ProductWithGenericLong and ProductWithGenericString

* Restore format, this reduces the diff

* Restore format, this reduces the diff

---------

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

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

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

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

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

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

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

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

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

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

* Simplify NaturalKeyEntryBasic.addInPairs()

---------

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

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

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

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

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

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

* For build bump kotlin-maven-plugin version to 2.3.10
2026-02-15 09:59:16 +13:00
Rob Bygrave 7addba7c93 Version 16.2.1 2026-02-02 23:22:22 +13:00
Rob BygraveandGitHub 00b45c1642 For query usingMaster support passing a boolean (#3721)
This is to support the use case where other logic is used to determine if a query
should be "forced" to use the master data source or not, and so it's easier to
pass that as a boolean to the usingMaster() method.
2026-02-02 23:17:37 +13:00
120 changed files with 3907 additions and 374 deletions
+11
View File
@@ -3,6 +3,7 @@
[![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
[![Multi-JDK Build](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
[![GraalVM Native Image](https://img.shields.io/badge/GraalVM-Native%20Image%20Ready-darkgreen?logo=graalvm)](https://www.graalvm.org/)
##### Build with database platforms
[![H2Database](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
@@ -80,6 +81,16 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
## Documentation
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
## Guides
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
Available guides:
- [Maven POM setup](docs/guides/add-ebean-postgres-maven-pom.md)
- [Database configuration](docs/guides/add-ebean-postgres-database-config.md)
- [Test container setup](docs/guides/add-ebean-postgres-test-container.md)
- [DB migration generation](docs/guides/add-ebean-db-migration-generation.md)
- [Lombok with Ebean entity beans](docs/guides/lombok-with-ebean-entity-beans.md)
## Maven central
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -47,19 +47,19 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -42,13 +42,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -17,13 +17,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -60,13 +60,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>composites</artifactId>
+109
View File
@@ -0,0 +1,109 @@
# Guides
Step-by-step guides written as instructions for AI agents and developers.
## Adding Ebean ORM with PostgreSQL to an existing Maven project
A three-part guide covering everything needed to wire Ebean + PostgreSQL into an
existing Maven project. Complete the steps in order.
| Step | Guide | Description |
|------|-------|-------------|
| 1 | [Maven POM setup](add-ebean-postgres-maven-pom.md) | Add Ebean dependencies, the enhancement plugin, and the querybean-generator annotation processor to `pom.xml` |
| 2 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
| 3 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; covers image mirror, read-only datasource, and PostGIS variant |
## Entity beans
| Guide | Description |
|-------|-------------|
| [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)` |
## Database migrations
| Guide | Description |
|-------|-------------|
| [DB migration generation](add-ebean-db-migration-generation.md) | Add `GenerateDbMigration.java` to generate schema diff migrations offline; configure the migration runner; understand `.sql` and `.model.xml` output files; workflow for pending drops |
## Connection Pooling & DataSource Configuration
The [ebean-datasource](https://github.com/ebean-orm/ebean-datasource) project provides
comprehensive guides on connection pool configuration and best practices. These are particularly
useful for production deployments, especially in Kubernetes or AWS environments:
| Guide | Description |
|-------|-------------|
| [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md) | Step-by-step guide for basic, read-only, Kubernetes, and AWS Lambda datasource configurations |
| [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md) | Setting up dual DataSources with Aurora read and write endpoints for Ebean integration |
| [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md) | Understanding connection heartbeat strategies and why `Connection.isValid()` is the recommended approach |
---
## Helping AI agents find these guides
AI coding agents can only follow these guides if they know they exist. Below are
copy-paste snippets for the most common AI tooling configurations. Add whichever
applies to your project.
### Project `README.md` (universal — works with all AI tools)
Add a section near the top of your project README:
```markdown
## AI Agent Instructions
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
Ebean tasks (Maven setup, database configuration, test containers, DB migrations,
Lombok) are at:
**https://github.com/ebean-orm/ebean/tree/main/docs/guides/**
AI agents should fetch and follow the relevant guide before performing Ebean-related
tasks.
```
### Why README is important
Many AI agents (Copilot, Claude, Cursor, Aider) read the project `README.md`
automatically as part of understanding the codebase. A visible link in README is the
most universally effective way to ensure any agent working on your project will
discover the guides without requiring tool-specific configuration files.
------
### GitHub Copilot — `.github/copilot-instructions.md`
```markdown
## Ebean ORM
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
tasks are at: https://github.com/ebean-orm/ebean/tree/main/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/main/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/main/docs/guides/add-ebean-postgres-database-config.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/main/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/main/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/main/docs/guides/lombok-with-ebean-entity-beans.md
```
### Claude Code — `CLAUDE.md`
Same content as above — Claude Code reads `CLAUDE.md` at the project root.
### Cursor — `.cursor/rules/ebean.mdc`
```markdown
---
description: Ebean ORM task guidance
globs: ["**/*.java", "**/pom.xml"]
alwaysApply: false
---
## Ebean ORM
This project uses Ebean ORM. Before performing any Ebean-related task, fetch and
follow the relevant step-by-step guide from:
https://github.com/ebean-orm/ebean/tree/main/docs/guides/
```
@@ -0,0 +1,366 @@
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
## Purpose
This guide provides step-by-step instructions for adding Ebean DB migration generation
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
performing a diff of the current entity model against the previously recorded model state,
producing platform-specific DDL SQL scripts.
These instructions are designed for AI agents and developers to follow precisely.
---
## Prerequisites
- An existing Maven project with Ebean ORM configured (entity beans present)
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
---
## Step 1 — Verify migration dependencies
### Generation tooling (`ebean-ddl-generator`)
`ebean-test` (already present as a test dependency) transitively includes
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
is required for generation.
### Runtime migration runner (`ebean-migration`)
`ebean-migration` is the library that runs migrations on application startup.
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
equivalent platform dependency). Verify it is on the classpath by running:
```bash
mvn dependency:tree | grep ebean-migration
```
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean.version}</version>
</dependency>
```
---
## Step 2 — Create `GenerateDbMigration.java`
Create the following class in `src/test/java/main/`. This `main` method is run manually
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
```java
package main;
import io.ebean.annotation.Platform;
import io.ebean.dbmigration.DbMigration;
import java.io.IOException;
/**
* Generate the next database migration based on a diff of the entity model.
* Run this main method after making entity bean changes to produce the migration SQL.
*/
public class GenerateDbMigration {
public static void main(String[] args) throws IOException {
DbMigration migration = DbMigration.create();
migration.setPlatform(Platform.POSTGRES);
migration.setVersion("1.1"); // set to the next migration version
migration.setName("add-customer"); // short description of the change
migration.generateMigration();
}
}
```
### Version naming convention
Ebean supports two common version formats — choose one and apply it consistently:
| Format | Example | Notes |
|--------|---------|-------|
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
The version controls execution order — Ebean runs migrations in ascending version order.
### Name convention
The `name` should be a short, lowercase, hyphenated description of the change:
- `add-customer-email`
- `rename-machine-type`
- `drop-unused-columns`
---
## Step 3 — Configure the output path (if needed)
By default, migration files are written to `src/main/resources/dbmigration/` relative
to the **current working directory** when `generateMigration()` is called. This is
usually the module root, which is correct for single-module projects.
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
resources directory is at a different relative path, specify it explicitly:
```java
// Relative path from the working directory (project root) to the module's resources
migration.setPathToResources("my-module/src/main/resources");
```
---
## Step 4 — Run `GenerateDbMigration` to produce the first migration
Run the `main` method via the IDE or Maven:
```bash
# Run via Maven exec plugin (or use IDE run configuration)
mvn test-compile exec:java \
-Dexec.mainClass="main.GenerateDbMigration" \
-Dexec.classpathScope="test" \
-pl <your-module>
```
Ebean migration generation runs in **offline mode** — no database connection is required.
### Expected output files
After running, two files are created per migration in `src/main/resources/dbmigration/`:
```
src/main/resources/dbmigration/
1.1__add-customer.sql ← DDL SQL to apply (commit this)
model/
1.1__add-customer.model.xml ← logical model diff XML (commit this)
```
Both files must be committed to source control. The `.model.xml` file records the
logical state of the diff and is used by subsequent migration generations to determine
what has changed.
If **no entity beans have changed** since the last migration, the command outputs:
```
DbMigration - no changes detected - no migration written
```
---
## Step 5 — Enable the migration runner
Configure Ebean to run pending migrations automatically on application startup.
### Preferred approach — programmatic via `DatabaseBuilder`
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
the `Database` bean. This is the preferred approach as it is explicit, co-located with
the database configuration, and does not rely on external property files.
In the `@Factory` class that builds the `Database` bean (see the database configuration
guide), add `.runMigration(true)` to the builder chain:
```java
@Bean
Database database(ConfigWrapper config) {
var dataSource = DataSourceBuilder.create()
.url(config.getDatabaseUrl())
.username(config.getDatabaseUser())
.password(config.getDatabasePassword())
// ... other datasource settings ...
;
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
}
```
If migrations should only run in certain environments (e.g., not in production, or
only when a config flag is set), make it conditional:
```java
.runMigration(config.isRunMigrations()) // driven by config value
```
### Alternative — via application properties
If programmatic configuration is not available or not preferred, set the property
in `src/main/resources/application.properties`:
```properties
ebean.migration.run=true
```
Or in `src/main/resources/application.yaml`:
```yaml
ebean:
migration:
run: true
```
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
name in the property key:
```properties
ebean.mydb.migration.run=true
```
### What the runner does at startup
When migration running is enabled, Ebean will on each application start:
1. Look at the migrations in `src/main/resources/dbmigration/`
2. Compare against the `db_migration` table (created automatically on first run)
3. Apply any migrations that have not yet been executed, in version order
4. Record each successfully applied migration in `db_migration`
---
## Step 6 — Commit the migration files
Add both generated files to source control:
```bash
git add src/main/resources/dbmigration/1.1__add-customer.sql
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
git commit -m "Add db migration 1.1: add-customer"
```
---
## Ongoing workflow — generating subsequent migrations
For each future set of entity bean changes:
1. Make changes to the entity bean classes
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
```java
migration.setVersion("1.2");
migration.setName("add-address-table");
```
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
---
## Understanding the output files
### Apply SQL (`.sql`)
The apply SQL file contains the DDL that will be executed against the database:
```sql
-- apply changes
alter table customer add column email varchar(255);
```
### Model XML (`.model.xml`)
The model XML records the logical diff in a database-agnostic format. Ebean uses
this file on the next generation run to determine what has already been captured.
It is not executed against the database.
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<addColumn tableName="customer">
<column name="email" type="varchar(255)"/>
</addColumn>
</changeSet>
</migration>
```
---
## Optional configurations
### Multiple database platforms
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
instead of `setPlatform()`:
```java
migration.addPlatform(Platform.POSTGRES);
migration.addPlatform(Platform.SQLSERVER17);
migration.addPlatform(Platform.MYSQL);
```
Each platform gets its own subdirectory under `dbmigration/`.
### Include index
When enabled the migration generation also generates a file that contains
all the migrations and their associated hashes. This is a performance
optimisation (that will become the default) and means that the migration
runner just needs to read the one resource and has the pre-computed hash
values (so does not need to read each migration resource and compute the
hash for each of those at runtime).
```java
migration.setIncludeIndex(true);
```
### Strict mode
Strict mode (on by default) errors if there are any pending drops not yet applied.
Set to `false` to allow generation to proceed regardless:
```java
migration.setStrictMode(false);
```
### Applying pending drops
Destructive changes (drop column, drop table) are **not** included in the apply
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
the application to be deployed without immediately dropping columns (important for
rolling deployments).
The migration runner logs a message when pending drops exist:
```
INFO DbMigration - Pending un-applied drops in versions [1.1]
```
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
contains the pending drops:
```java
migration.setVersion("1.3");
migration.setName("drop-pending-from-1.1");
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
migration.generateMigration();
```
### Custom dbSchema
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
`application.properties`), no additional configuration is needed in
`GenerateDbMigration` — Ebean picks up the schema from the application config
automatically when running in offline mode.
```properties
# application.properties
ebean.dbSchema=myschema
```
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
@@ -0,0 +1,282 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 2: Database Configuration
## Purpose
This guide provides step-by-step instructions for configuring an Ebean `Database` bean
using **Avaje Inject** (`@Factory` / `@Bean`), backed by a PostgreSQL datasource built
with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 2 of 2.
---
## Prerequisites
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
- **Avaje Inject** is on the classpath (e.g. `io.avaje:avaje-inject`)
- A configuration source is available at runtime (e.g. `avaje-config` reading
`application.yml` or environment variables)
- The following configuration keys are resolvable at runtime (adapt names to your project):
| Key | Description |
|-----|-------------|
| `db_url` | JDBC URL for the master/write connection |
| `db_user` | Database username |
| `db_pass` | Database password |
| `db_master_min_connections` | Minimum pool size (default: 1) |
| `db_master_initial_connections` | Initial pool size at startup — set high to pre-warm on pod start (see K8s note below) |
| `db_master_max_connections` | Maximum pool size (default: 200) |
---
## Step 1 — Locate or create the `@Factory` class
Look for an existing Avaje Inject `@Factory`-annotated class in the project
(often named `AppConfig`, `DatabaseConfig`, or similar). If one exists, add the new
`@Bean` method to it. If none exists, create one:
```java
package com.example.configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
@Factory
class DatabaseConfig {
// beans will be added in the steps below
}
```
---
## Step 2 — Add the `Database` bean method (minimal — master datasource only)
Add the following `@Bean` method to the `@Factory` class. This creates an Ebean
`Database` backed by a single master (read-write) PostgreSQL datasource.
```java
import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;
@Bean
Database database() {
var dataSource = DataSourceBuilder.create()
.url(/* resolve from config, e.g.: */ Config.get("db_url"))
.username(Config.get("db_user"))
.password(Config.get("db_pass"))
.driver("org.postgresql.Driver")
.schema("myschema") // set to your target schema
.applicationName("my-app") // visible in pg_stat_activity
.minConnections(Config.getInt("db_master_min_connections", 1))
.initialConnections(Config.getInt("db_master_initial_connections", 10))
.maxConnections(Config.getInt("db_master_max_connections", 200));
return Database.builder()
.name("db") // logical name for this Database instance
.dataSourceBuilder(dataSource)
.build();
}
```
### Field guidance
| Field | Notes |
|-------|-------|
| `url` | Full JDBC URL, e.g. `jdbc:postgresql://host:5432/dbname` |
| `schema` | The Postgres schema Ebean should use (omit if using `public`) |
| `applicationName` | Shown in `pg_stat_activity.application_name`; helps with DB-side diagnostics |
| `name("db")` | Logical Ebean database name; relevant if multiple Database instances exist |
| `minConnections` | Connections kept open at all times; pool will not shrink below this |
| `initialConnections` | Connections opened at startup; see K8s warm-up note below |
| `maxConnections` | Hard upper limit on concurrent connections |
### Connection pool sizing for Kubernetes (and similar orchestrated environments)
When a pod starts in Kubernetes it will receive live traffic as soon as it passes
readiness checks — often before the connection pool has had a chance to grow to handle
the load. This can cause latency spikes on the first wave of requests while the pool
expands one connection at a time.
Use `initialConnections` to **pre-warm the pool at startup** so it is already sized
for peak load when the pod goes live:
```
minConnections: 2 ← floor; pool will shrink back here when idle
initialConnections: 20 ← opened at pod start, before first request arrives
maxConnections: 50 ← hard ceiling
```
The lifecycle is:
1. **Pod starts** — pool opens `initialConnections` connections immediately.
2. **Pod receives traffic** — pool is already at capacity; no growth latency.
3. **Traffic drops** — idle connections are closed; pool trims back toward `minConnections`.
4. **Next traffic spike** — pool grows again up to `maxConnections` on demand.
Set `initialConnections` to a value high enough that the pool does not need to grow
during the first minute of live traffic. A common starting point is 5075% of
`maxConnections`.
---
## Step 3 — Inject configuration via a constructor or config helper (recommended)
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
Avaje `Configuration` bean if one is available. This makes the factory testable and
keeps the wiring explicit. For example:
```java
@Bean
Database database(Configuration config) {
String url = config.get("db_url");
String user = config.get("db_user");
String pass = config.get("db_pass");
int min = config.getInt("db_master_min_connections", 1);
int init = config.getInt("db_master_initial_connections", 10);
int max = config.getInt("db_master_max_connections", 200);
var dataSource = DataSourceBuilder.create()
.url(url)
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.minConnections(min)
.initialConnections(init)
.maxConnections(max);
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.skipDataSourceCheck(true)
.build();
}
```
If the project has a dedicated config-wrapper class (a `@Component` that reads config
keys), accept it as a parameter instead of `Configuration`.
---
## Step 4 (Optional) — Add a read-only datasource
For production services that have a separate read-replica, add a second
`DataSourceBuilder` for read-only queries and wire it via
`readOnlyDataSourceBuilder(...)`. The read-only datasource:
- Uses `readOnly(true)` and `autoCommit(true)` (Ebean routes read queries there automatically)
- Typically has a higher max connection count than the master
- Benefits from a prepared-statement cache (`pstmtCacheSize`)
```java
@Bean
Database database(Configuration config) {
String masterUrl = config.get("db_url");
String readOnlyUrl = config.get("db_url_readonly");
String user = config.get("db_user");
String pass = config.get("db_pass");
var masterDataSource = buildDataSource(user, pass)
.url(masterUrl)
.minConnections(config.getInt("db_master_min_connections", 1))
.initialConnections(config.getInt("db_master_initial_connections", 10))
.maxConnections(config.getInt("db_master_max_connections", 50));
var readOnlyDataSource = buildDataSource(user, pass)
.url(readOnlyUrl)
.readOnly(true)
.autoCommit(true)
.pstmtCacheSize(250) // cache up to 250 prepared statements per connection
.maxInactiveTimeSecs(600) // close idle connections after 10 minutes
.minConnections(config.getInt("db_readonly_min_connections", 2))
.initialConnections(config.getInt("db_readonly_initial_connections", 10))
.maxConnections(config.getInt("db_readonly_max_connections", 200));
return Database.builder()
.name("db")
.dataSourceBuilder(masterDataSource)
.readOnlyDataSourceBuilder(readOnlyDataSource)
.build();
}
private static DataSourceBuilder buildDataSource(String user, String pass) {
return DataSourceBuilder.create()
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.addProperty("prepareThreshold", "2"); // PostgreSQL: server-side prepared statements
}
```
### Additional configuration keys for the read-only datasource
| Key | Description | Default |
|-----|-------------|---------|
| `db_url_readonly` | JDBC URL for the read replica | — |
| `db_master_initial_connections` | Initial master pool size at startup | 10 |
| `db_readonly_min_connections` | Minimum pool size | 2 |
| `db_readonly_initial_connections` | Initial pool size at startup | same as min |
| `db_readonly_max_connections` | Maximum pool size | 20 |
---
## Step 5 (Optional) — Enable the migration runner
If the project uses Ebean's built-in DB migration runner to apply SQL migrations on
startup, enable it on the `DatabaseBuilder`:
```java
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
```
This is equivalent to setting `ebean.migration.run=true` in `application.properties`
but is preferred because it keeps all database configuration in one place. To make it
conditional (e.g. only in non-production environments):
```java
.runMigration(config.getBoolean("db.runMigrations", false))
```
See the DB migration generation guide (`add-ebean-db-migration-generation.md`) for
full details on generating and managing migration files.
---
## See Also
For advanced connection pool configuration, production deployment patterns, and connection
validation best practices, see the [ebean-datasource guides](https://github.com/ebean-orm/ebean-datasource/tree/master/docs/guides/):
- **[Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)** — Covers read-only pools (`readOnly(true)` + `autoCommit(true)`), Kubernetes deployment strategies using `initialConnections`, and AWS Lambda optimization
- **[AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)** — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
- **[Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)** — Why `Connection.isValid()` is the recommended default and when (rarely) explicit `heartbeatSql` is needed
---
## Verification
1. Start the application (or run `mvn test -pl <your-module>`).
2. Look for log output similar to:
```
INFO o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
INFO io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
```
3. If you see `DataSourcePool` and `DatabasePlatform` log lines, Ebean is connected and
the database bean is wired correctly.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `ClassNotFoundException: org.postgresql.Driver` | PostgreSQL JDBC driver missing | Add `org.postgresql:postgresql` dependency (see Step 1 guide) |
| `Cannot connect to database` at startup | DB unreachable but `skipDataSourceCheck` is `false` | Set `.skipDataSourceCheck(true)` |
| Ebean enhancement warnings in logs | `ebean-maven-plugin` not configured | Complete Step 1 guide |
| `NullPointerException` reading config key | Config key not defined | Add the key to `application.yml` or environment |
+175
View File
@@ -0,0 +1,175 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 1: POM Setup
## Purpose
This guide provides step-by-step instructions for modifying an existing Maven `pom.xml`
to add Ebean ORM with PostgreSQL support. Follow every step in order. This is Step 1 of 2.
---
## Prerequisites
- An existing Maven project (`pom.xml` already exists)
- Java 11 or higher
- The project does **not** yet include any Ebean dependencies
---
## Step 1 — Define the Ebean version property
Open the module's `pom.xml` (the one that will use Ebean directly, i.e. the module
containing the database configuration and entity classes).
Inside the `<properties>` block, add the `ebean.version` property if it does not
already exist:
```xml
<properties>
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
<ebean.version>17.2.0</ebean.version>
</properties>
```
> If the project has a parent POM that already defines `ebean.version`, skip this step.
---
## Step 2 — Add the PostgreSQL JDBC driver dependency
Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
```xml
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.8</version>
</dependency>
```
> Check [Maven Central](https://central.sonatype.com/artifact/org.postgresql/postgresql)
> for the latest version. If the parent POM manages the PostgreSQL version, omit the
> `<version>` tag.
---
## Step 3 — Add the Ebean PostgreSQL platform dependency
Inside the `<dependencies>` block, add the Ebean Postgres platform dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>${ebean.version}</version>
</dependency>
```
This single artifact pulls in the Ebean core, the datasource connection pool
(`ebean-datasource`), and all Postgres-specific support.
---
## Step 4 — Add the ebean-test dependency (test scope)
`ebean-test` configures Ebean for tests and enables automatic Docker container management
for Postgres test instances:
```xml
<!-- test dependencies -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
---
## Step 5 — Add the ebean-maven-plugin (bytecode enhancement)
Ebean requires bytecode enhancement to provide dirty-checking and lazy-loading.
The `ebean-maven-plugin` performs this enhancement at build time.
Inside the `<build><plugins>` block, add:
```xml
<plugin> <!-- perform ebean enhancement -->
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
```
---
## Step 6 — Add the querybean-generator annotation processor
The `querybean-generator` annotation processor generates type-safe query bean classes
at compile time. It must be registered as an `annotationProcessorPath` inside
`maven-compiler-plugin`.
### Case A — No existing `maven-compiler-plugin` configuration
Add the full plugin entry to `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<annotationProcessorPaths>
<path> <!-- generate ebean query beans -->
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```
### Case B — `maven-compiler-plugin` already exists with `<annotationProcessorPaths>`
Locate the existing `<annotationProcessorPaths>` block inside the existing
`maven-compiler-plugin` entry and add the new `<path>` inside it. Do **not** add a
second `<configuration>` block or a second `<annotationProcessorPaths>` block.
Example — if the existing block already has a path for, say, `avaje-nima-generator`:
```xml
<annotationProcessorPaths>
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-nima-generator</artifactId>
<version>${avaje-nima.version}</version>
</path>
<!-- ADD the new path here, inside the existing block -->
<path>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
```
---
## Verification
Run the following to confirm the POM is valid and the project compiles:
```bash
mvn compile -pl <your-module-name>
```
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
---
## Next Step
Proceed to **Step 2: Configure the Datasource and Ebean Database bean**
(`add-ebean-postgres-database-config.md`).
@@ -0,0 +1,293 @@
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Test Container Setup
## Purpose
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
container for tests using `ebean-test-containers`, exposing an `io.ebean.Database`
bean via an Avaje Inject `@TestScope @Factory` class. This is Step 3 of 3.
Two variants are covered:
- **Variant A** — plain PostgreSQL
- **Variant B** — PostgreSQL with PostGIS extension
---
## Prerequisites
- **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`,
`querybean-generator`, and **`ebean-test`** as a test-scoped dependency
(see `add-ebean-postgres-maven-pom.md`)
- **Step 2 complete**: A production `Database` bean exists (see `add-ebean-postgres-database-config.md`)
- **Avaje Inject** is on the classpath with test support (`io.avaje:avaje-inject-test`)
- **Docker** is installed and running on the developer machine
---
## Overview: Declarative vs Programmatic approach
`ebean-test` supports two ways to configure the test database:
| Approach | How | Best for |
|----------|-----|---------|
| **Declarative** | `src/test/resources/application-test.yaml` | Simple projects with no DI, no image mirrors |
| **Programmatic** | `@TestScope @Factory` class | Avaje Inject tests, private image mirrors (ECR), more control |
This guide uses the **programmatic approach** because it integrates naturally with
Avaje Inject, allows a private mirror to be specified (useful in CI with ECR or similar),
and makes the `Database` injectable into tests.
---
## Step 1 — Verify ebean-test is a test dependency
Confirm the following is present in `pom.xml` (added in Step 1):
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
`ebean-test` transitively brings in `ebean-test-containers` which provides
`PostgresContainer` and `PostgisContainer`.
---
## Step 2 — Create a `@TestScope @Factory` class
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`).
Annotate it with `@TestScope` and `@Factory` so Avaje Inject uses it only in tests.
```java
package com.example.testconfig;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.Database;
@TestScope
@Factory
class TestConfiguration {
// bean methods added in the steps below
}
```
---
## Step 3 — Add a container bean and a Database bean
### Variant A — Plain PostgreSQL
```java
import io.ebean.test.containers.PostgresContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
### Variant B — PostGIS (PostgreSQL + PostGIS extension)
Use `PostgisContainer` instead of `PostgresContainer`. The default image is
`ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`,
and `postgis` are installed automatically.
```java
import io.ebean.test.containers.PostgisContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgisContainer postgres() {
return PostgisContainer.builder("17") // PostGIS image version (Postgres 17)
.dbName("my_app")
.build()
.start();
}
@Bean
Database database(PostgisContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
### Key differences from Variant A
| | PostgresContainer | PostgisContainer |
|---|---|---|
| Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` |
| Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` |
| Default port | 6432 | 6432 |
| Optional LW mode | — | `.useLW(true)` (see Optional section) |
---
## Step 4 — Write a test
Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`:
```java
package com.example.testconfig;
import io.avaje.inject.test.InjectTest;
import io.ebean.Database;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@InjectTest
class DatabaseTest {
@Inject
Database database;
@Test
void database_isAvailable() {
assertThat(database).isNotNull();
}
}
```
---
## Verification
Run the tests:
```bash
mvn test -pl <your-module>
```
Expected log output confirming the container started and Ebean connected:
```
INFO Container ut_postgres running with port:6432 ...
INFO connectivity confirmed for ut_postgres
INFO DataSourcePool [my_app] autoCommit[false] ...
INFO DatabasePlatform name:my_app platform:postgres
INFO Executing db-create-all.sql - ...
```
---
## Optional configurations
### Image mirror (for CI / private registry)
If CI builds pull images from a private registry (e.g., AWS ECR) instead of Docker Hub
or GitHub Container Registry, specify a mirror. The mirror is **only used in CI**
it is ignored on local developer machines (where Docker Hub / GHCR is used directly).
```java
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("16")
.dbName("my_app")
.mirror("123456789.dkr.ecr.ap-southeast-2.amazonaws.com/mirrored")
.build()
.start();
}
```
Alternatively, set the mirror globally via a system property or
`ebean.test.containers.mirror` in a properties file, avoiding code changes per project.
### Read-only datasource (for tests using read-replica simulation)
Call `.autoReadOnlyDataSource(true)` on the `DatabaseBuilder` to automatically
create a second read-only datasource pointing at the same container:
```java
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.autoReadOnlyDataSource(true) // test read-only queries against same container
.build();
}
```
### Dump metrics on shutdown
Useful for performance analysis during test runs:
```java
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.dumpMetricsOnShutdown(true)
.dumpMetricsOptions("loc,sql,hash")
.build();
}
```
### PostGIS: LW mode (HexWKB)
For PostGIS with DriverWrapperLW (HexWKB binary geometry encoding), set `.useLW(true)`.
This switches the JDBC URL prefix to `jdbc:postgresql_lwgis://` and requires the
`net.postgis:postgis-jdbc` dependency on the test classpath:
```xml
<!-- add to pom.xml test dependencies when using useLW(true) -->
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>2024.1.0</version>
<scope>test</scope>
</dependency>
```
```java
@Bean
PostgisContainer postgres() {
return PostgisContainer.builder("16")
.dbName("my_app")
.useLW(true) // use HexWKB + DriverWrapperLW
.build()
.start();
}
```
> **Note**: LW mode is not required for most PostGIS use cases. Only enable it if
> your entities use binary geometry types (e.g., `net.postgis.jdbc.geometry.Geometry`)
> that require the `DriverWrapperLW` driver.
---
## Keeping the container running (local development)
By default, `ebean-test` stops the Docker container when tests finish. To keep it
running between test runs (much faster for local development), create a marker file:
```bash
mkdir -p ~/.ebean && touch ~/.ebean/ignore-docker-shutdown
```
On CI servers, omit this file so containers are cleaned up after each build.
@@ -0,0 +1,206 @@
# Guide: Using Lombok with Ebean Entity Beans
## Purpose
This guide explains which Lombok annotations are safe and recommended for Ebean
entity beans, which ones to avoid, and why. It is written as prescriptive instructions
for AI agents and developers.
---
## The Core Rule
> **Do NOT use `@Data` on Ebean entity beans.**
Use `@Getter` + `@Setter` instead, with the optional `@Accessors(chain = true)` for
a fluent setter style.
---
## Why `@Data` is Incompatible with Ebean
`@Data` is a convenience annotation that is equivalent to applying `@Getter`,
`@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode` together.
Three of those are problematic for Ebean entity beans:
### 1. `@EqualsAndHashCode` (included in `@Data`) — breaks entity identity
`@Data` generates `hashCode()` and `equals()` based on all non-static, non-transient
fields. Ebean entity beans have identity semantics — two references to the same database
row should be considered equal based on their `@Id` value, not field-by-field comparison.
Problems caused:
- Inconsistent `hashCode` before and after persist (the `@Id` field is `0` on a new
entity, then changes after insert — violating the `hashCode` contract for collections)
- Entities placed in a `Set` or `HashMap` before saving will be unfindable after saving
- Ebean's internal identity map and dirty checking can be confused
### 2. `@ToString` (included in `@Data`) — triggers unexpected lazy loading
`@Data` generates a `toString()` that accesses **all** fields, including
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
outside of a transaction triggers a `LazyInitializationException` or fires an unexpected
SQL query, which can:
- Cause subtle bugs in logging statements
- Trigger N+1 queries in test output or debug logging
- Fail with an exception if no active transaction exists
### 3. `@RequiredArgsConstructor` (included in `@Data`) — unnecessary for Ebean
Ebean does not require a default constructor — it can construct entity instances without
one. `@RequiredArgsConstructor` therefore adds nothing useful to entity beans.
---
## Recommended Annotation Set
Use exactly these three Lombok annotations on every Ebean entity bean:
```java
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "my_table")
public class MyEntity {
// ...
}
```
| Annotation | Purpose |
|---|---|
| `@Getter` | Generates `getFoo()` / `isFoo()` accessor methods |
| `@Setter` | Generates `setFoo(value)` mutator methods; Ebean enhancement intercepts these for dirty tracking |
| `@Accessors(chain = true)` | Makes setters return `this`, enabling fluent/builder-style property setting |
---
## `@Accessors(chain = true)` — Fluent Setter Style
With `chain = true`, setters return `this` instead of `void`, allowing method chaining:
```java
// without chain = true (void setters)
CMachine machine = new CMachine();
machine.setMake("Toyota");
machine.setModel("Hilux");
machine.setStatus("active");
// with @Accessors(chain = true)
CMachine machine = new CMachine()
.setMake("Toyota")
.setModel("Hilux")
.setStatus("active");
```
This is particularly useful when building test data:
```java
CMachine machine = new CMachine()
.setGid(UUID.randomUUID())
.setMachineType("HV")
.setStatus("active")
.setMake("Komatsu")
.setModel("PC200");
database.save(machine);
```
Ebean's bytecode enhancement is fully compatible with chained setters — the
enhancement intercepts each `setFoo()` call to record which fields have been modified
(dirty checking), regardless of whether the setter returns `void` or `this`.
---
## `@Accessors(fluent = true)` — also compatible
`@Accessors(fluent = true)` removes the `get`/`set`/`is` prefix, generating `name()`
(getter) and `name(value)` (setter) instead of `getName()` and `setName(value)`.
Ebean does **not** require JavaBeans naming conventions — it can work with any accessor
method style, including fluent accessors with no prefix. `@Accessors(fluent = true)` is
therefore compatible with Ebean.
`@Accessors(chain = true)` is the more common choice in practice (it keeps the familiar
`get`/`set` prefix while adding method chaining), but `fluent = true` is a valid
alternative if that style is preferred consistently across the codebase.
---
## Full Entity Bean Example
```java
package com.example.repository.data;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "machine")
public class CMachine {
@Id
private long id;
@Version
private int version;
@Column(nullable = false, unique = true)
private UUID gid;
@Column(nullable = false, length = 10)
private String machineType;
@Column(length = 200)
private String make;
@Column(length = 200)
private String model;
@WhenCreated
private Instant created;
@WhenModified
private Instant lastModified;
}
```
---
## Summary: Lombok Annotations and Ebean Compatibility
| Lombok Annotation | Compatible? | Notes |
|---|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `@Getter` | ✅ Safe | Use on every entity bean |
| `@Setter` | ✅ Safe | Use on every entity bean; enhancement intercepts these |
| `@Accessors(chain = true)` | ✅ Safe | Recommended for fluent construction style |
| `@ToString` | ❌ Avoid | Ebean does a better job and handles recursion |
| `@EqualsAndHashCode` | ❌ Avoid | Breaks entity identity and `@Id`-based equality |
| `@Data` | ❌ Avoid | Includes `@EqualsAndHashCode` and `@ToString` — both problematic |
| `@Value` | ❌ Avoid | Makes fields final — incompatible with Ebean's field-level bytecode enhancement |
| `@Accessors(fluent = true)` | ✅ Safe | Removes `get`/`set` prefix — Ebean does not require JavaBeans naming conventions and works with any accessor style |
| `@Builder` | ⚠️ Careful | Usable on non-entity helper/factory classes; on entity beans it requires a no-arg constructor alongside it and offers no advantage over `@Accessors(chain = true)` |
---
## Relationship with Ebean Bytecode Enhancement
Ebean's bytecode enhancement (applied by `ebean-maven-plugin` at build time) modifies
the `setXxx()` methods of entity beans to:
1. Mark the field as dirty (changed) so only modified fields are included in UPDATE statements
2. Support lazy loading of associations when a getter is called on an unloaded field
For this to work correctly, Ebean needs:
- Accessor methods for each persistent field (any naming style is fine — `getFoo()`, `foo()`, or no accessors at all; Ebean can also access fields directly)
- No override of `hashCode()` / `equals()` that would interfere with the identity map — which means **no `@Data` or `@EqualsAndHashCode`**
+1 -1
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean api</name>
+12 -1
View File
@@ -234,5 +234,16 @@ public interface DtoQuery<T> extends CancelableQuery {
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
DtoQuery<T> usingMaster();
default DtoQuery<T> usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
DtoQuery<T> usingMaster(boolean useMaster);
}
@@ -135,7 +135,17 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
SELF usingMaster();
default SELF usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
SELF usingMaster(boolean useMaster);
/**
* Set the base table to use for this query.
+19 -1
View File
@@ -63,7 +63,17 @@ public interface SqlQuery extends Serializable, CancelableQuery {
* source. We use {@code usingMaster()} to instead ensure that the query is executed
* against the master data source.
*/
SqlQuery usingMaster();
default SqlQuery usingMaster() {
return usingMaster(true);
}
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see #usingMaster()
*/
SqlQuery usingMaster(boolean useMaster);
/**
* Execute the query returning a list.
@@ -357,6 +367,14 @@ public interface SqlQuery extends Serializable, CancelableQuery {
*/
interface TypeQuery<T> {
/**
* Ensure the master DataSource is used when useMaster is true. Otherwise, the read only
* data source can be used if defined.
*
* @see SqlQuery#usingMaster(boolean)
*/
TypeQuery<T> usingMaster(boolean useMaster);
/**
* Execute the query using the given transaction.
*/
@@ -51,6 +51,13 @@ public class DbPlatformTypeMapping {
private static final DbPlatformType VECTOR_BIT = new DbPlatformType("bit", 64000, null);
private static final DbPlatformType VECTOR_SPARSE = new DbPlatformType("sparsevec", 1000, null);
/**
* Timestamp with max precision of 15, and fallback to plain timestamp without precision defined.
*/
private static final DbPlatformType TIMESTAMP =
new DbPlatformType("timestamp", 0, 15,
new DbPlatformType("timestamp", false));
private final Map<DbType, DbPlatformType> typeMap = new EnumMap<>(DbType.class);
/**
@@ -87,7 +94,8 @@ public class DbPlatformTypeMapping {
put(DbType.ARRAY);
put(DbType.DATE);
put(DbType.TIME);
put(DbType.TIMESTAMP);
put(DbType.TIMESTAMP, TIMESTAMP);
put(DbType.LONGVARBINARY);
put(DbType.LONGVARCHAR);
// most commonly real maps to db float
+28 -28
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean bom</name>
@@ -89,25 +89,25 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -125,13 +125,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -155,37 +155,37 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>kotlin-querybean-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-redis</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-spring-txn</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<!-- platforms -->
@@ -193,91 +193,91 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-clickhouse</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-db2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-hana</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mariadb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-mysql</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-nuodb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-oracle</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlite</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-sqlserver</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<groupId>io.ebean</groupId>
<artifactId>ebean-parent</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>ebean-core-json</artifactId>
<name>ebean-core-json</name>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<!-- Jackson core used internally by Ebean -->
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>ebean-core-type</artifactId>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
+7 -7
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>ebean-core</artifactId>
@@ -22,13 +22,13 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-json</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -52,7 +52,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -165,21 +165,21 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -41,7 +41,7 @@ final class NaturalKeyEntryBasic implements NaturalKeyEntry {
* Create when query uses an IN PAIRS clause.
*/
NaturalKeyEntryBasic(BeanNaturalKey naturalKey, List<NaturalKeyEq> eqList,
String inMapProperty0, String inMapProperty1, Pairs.Entry pair) {
String inMapProperty0, String inMapProperty1, Pairs.Entry pair) {
load(eqList);
map.put(inMapProperty0, pair.getA());
map.put(inMapProperty1, pair.getB());
@@ -49,6 +49,14 @@ final class NaturalKeyEntryBasic implements NaturalKeyEntry {
this.key = calculateKey(naturalKey);
}
NaturalKeyEntryBasic(BeanNaturalKey naturalKey, List<NaturalKeyEq> eqList,
Map<String, Object> properties, Object[] naturalKeyValue) {
load(eqList);
map.putAll(properties);
this.inValue = naturalKeyValue;
this.key = calculateKey(naturalKey);
}
private void load(List<NaturalKeyEq> eqList) {
if (eqList != null) {
for (NaturalKeyEq eq : eqList) {
@@ -3,10 +3,7 @@ package io.ebeaninternal.api;
import io.ebean.Pairs;
import io.ebeaninternal.server.deploy.BeanNaturalKey;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
* Collects the data for processing the natural key cache processing.
@@ -19,8 +16,9 @@ public final class NaturalKeyQueryData<T> {
*/
private boolean hasIn;
// IN Pairs clause - only one allowed
private String inProperty0, inProperty1;
private List<Pairs.Entry> inPairs;
private String[] properties;
private List<Object[]> inTuples;
// IN clause - only one allowed
private List<Object> inValues;
private String inProperty;
@@ -47,14 +45,39 @@ public final class NaturalKeyQueryData<T> {
}
if (matchProperty(property0) && matchProperty(property1)) {
this.hasIn = true;
this.inProperty0 = property0;
this.inProperty1 = property1;
this.properties = new String[]{property0, property1};
this.inPairs = new ArrayList<>(inPairs); // will be modified
return this.inPairs;
}
return null;
}
/**
* Match for In Tuples expression. We only allow one IN clause.
*/
public List<Object[]> matchInTuples(String[] properties, List<Object[]> inTuples) {
if (hasIn) {
// only 1 IN allowed (to project naturalIds)
return null;
}
boolean matchAll = true;
for (String property : properties) {
if (!matchProperty(property)) {
matchAll = false;
break;
}
}
if (matchAll) {
this.hasIn = true;
this.properties = Arrays.copyOf(properties, properties.length);
this.inTuples = new ArrayList<>(inTuples);
return this.inTuples;
}
return null;
}
/**
* Match for IN expression. We only allow one IN clause.
*/
@@ -100,6 +123,8 @@ public final class NaturalKeyQueryData<T> {
addInValues();
} else if (inPairs != null) {
addInPairs();
} else if (inTuples != null) {
addInTuples();
} else {
addEqualsKey();
}
@@ -110,7 +135,17 @@ public final class NaturalKeyQueryData<T> {
// a findList() with an IN Map clause so we project
// for every IN value a natural key combination
for (Pairs.Entry entry : inPairs) {
set.add(new NaturalKeyEntryBasic(naturalKey, eqList, inProperty0, inProperty1, entry));
set.add(new NaturalKeyEntryBasic(naturalKey, eqList, properties[0], properties[1], entry));
}
}
private void addInTuples() {
for (Object[] inTuple : inTuples) {
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < inTuple.length; i++) {
map.put(properties[i], inTuple[i]);
}
set.add(new NaturalKeyEntryBasic(naturalKey, eqList, map, inTuple));
}
}
@@ -152,11 +187,8 @@ public final class NaturalKeyQueryData<T> {
if (inProperty != null) {
exprProps.add(inProperty);
}
if (inProperty0 != null) {
exprProps.add(inProperty0);
}
if (inProperty1 != null) {
exprProps.add(inProperty1);
if (properties != null) {
exprProps.addAll(Arrays.asList(properties));
}
if (eqList != null) {
for (NaturalKeyEq eq : eqList) {
@@ -173,6 +205,7 @@ public final class NaturalKeyQueryData<T> {
int defined = (inValues == null) ? 0 : 1;
defined += (inPairs == null) ? 0 : 2;
defined += (eqList == null) ? 0 : eqList.size();
defined += (inTuples == null) ? 0 : properties.length;
return defined == naturalKey.length();
}
@@ -206,6 +239,9 @@ public final class NaturalKeyQueryData<T> {
} else if (inPairs != null) {
//noinspection SuspiciousMethodCalls
inPairs.remove(inValue);
} else if (inTuples != null) {
//noinspection SuspiciousMethodCalls
inTuples.remove(inValue);
}
}
}
@@ -338,11 +338,6 @@ public interface SpiTransaction extends Transaction {
*/
boolean isNestedUseSavepoint();
/**
* Return true if explicitly set to skip cache (ignores skipOnWrite).
*/
boolean isSkipCacheExplicit();
/**
* Fire pre commit processing/listeners.
*/
@@ -169,11 +169,6 @@ public abstract class SpiTransactionProxy implements SpiTransaction {
transaction.setSkipCache(skipCache);
}
@Override
public boolean isSkipCacheExplicit() {
return transaction.isSkipCacheExplicit();
}
@Override
public boolean isSkipCache() {
return transaction.isSkipCache();
@@ -497,10 +497,6 @@ public final class OrmQueryRequest<T> extends BeanRequest implements SpiOrmQuery
return cacheKey != null && query.queryCacheMode().isPut();
}
public boolean isBeanCachePutMany() {
return !transaction.isSkipCacheExplicit() && query.isBeanCachePut();
}
public boolean isBeanCachePut() {
return !transaction.isSkipCache() && query.isBeanCachePut();
}
@@ -10,7 +10,7 @@ import io.ebean.core.type.DataReader;
import io.ebean.core.type.ScalarType;
import io.ebean.text.TextException;
import io.ebeaninternal.server.deploy.meta.DeployBeanProperty;
import io.ebeaninternal.server.util.Checksum;
import io.ebeaninternal.server.util.JsonContentHash;
import jakarta.persistence.PersistenceException;
import java.sql.SQLException;
@@ -141,7 +141,10 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
}
/**
* Hold checksum of json source content to use for dirty detection.
* Hold canonical hash of json content to use for dirty detection.
* <p>
* Uses an order-independent hash so that databases which reorder JSON object
* keys (e.g. PostgreSQL JSONB) do not cause false dirty detection.
* <p>
* Does not support rebuilding 'oldValue' as no original json content.
*/
@@ -152,7 +155,7 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
ChecksumMutableValue(ScalarType<?> parent, String json) {
this.parent = parent;
this.checksum = Checksum.checksum(json);
this.checksum = JsonContentHash.hash(json);
}
/**
@@ -165,13 +168,13 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
@Override
public MutableValueNext nextDirty(String json) {
final long nextChecksum = Checksum.checksum(json);
final long nextChecksum = JsonContentHash.hash(json);
return nextChecksum == checksum ? null : new NextPair(json, new ChecksumMutableValue(parent, nextChecksum));
}
@Override
public boolean isEqualToObject(Object obj) {
return Checksum.checksum(parent.format(obj)) == checksum;
return JsonContentHash.hash(parent.format(obj)) == checksum;
}
@Override
@@ -182,6 +185,10 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
/**
* Hold json source content. This supports rebuilding the 'oldValue'.
* <p>
* Uses fast string equality as primary check, with an order-independent
* canonical hash as fallback to handle databases that reorder JSON object
* keys (e.g. PostgreSQL JSONB).
*/
private static final class SourceMutableValue implements MutableValueInfo, MutableValueNext {
@@ -195,12 +202,15 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
@Override
public MutableValueNext nextDirty(String json) {
return Objects.equals(originalJson, json) ? null : new SourceMutableValue(parent, json);
if (jsonContentEqual(originalJson, json)) {
return null;
}
return new SourceMutableValue(parent, json);
}
@Override
public boolean isEqualToObject(Object obj) {
return Objects.equals(originalJson, parent.format(obj));
return jsonContentEqual(originalJson, parent.format(obj));
}
@Override
@@ -219,4 +229,13 @@ public final class BeanPropertyJsonMapper extends BeanPropertyJsonBasic {
return this;
}
}
/**
* Compare two JSON strings for content equality, ignoring key ordering.
* Uses fast string equality first, falls back to order-independent hash comparison.
*/
private static boolean jsonContentEqual(String json1, String json2) {
return Objects.equals(json1, json2)
|| JsonContentHash.hash(json1) == JsonContentHash.hash(json2);
}
}
@@ -11,6 +11,8 @@ import io.ebeaninternal.server.type.TypeManager;
import jakarta.persistence.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
import static java.lang.System.Logger.Level.*;
@@ -35,7 +37,7 @@ public final class DeployCreateProperties {
* Create the appropriate properties for a bean.
*/
public void createProperties(DeployBeanDescriptor<?> desc) {
createProperties(desc, desc.getBeanType(), 0);
createProperties(desc, desc.getBeanType(), 0, new HashMap<>());
desc.sortProperties();
}
@@ -64,7 +66,7 @@ public final class DeployCreateProperties {
* properties the bean properties from Class. Some of these properties may not map to database
* columns.
*/
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level) {
private void createProperties(DeployBeanDescriptor<?> desc, Class<?> beanType, int level, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
if (beanType.equals(Model.class)) {
// ignore all fields on model (_$dbName)
return;
@@ -74,7 +76,7 @@ public final class DeployCreateProperties {
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
if (!ignoreField(field)) {
DeployBeanProperty prop = createProp(desc, field, beanType);
DeployBeanProperty prop = createProp(desc, field, beanType, genericTypeMap);
if (prop != null) {
// set a order that gives priority to inherited properties
// push Id/EmbeddedId up and CreatedTimestamp/UpdatedTimestamp down
@@ -95,7 +97,7 @@ public final class DeployCreateProperties {
if (!superClass.equals(Object.class)) {
// recursively add any properties in the inheritance hierarchy
// up to the Object.class level...
createProperties(desc, superClass, level + 1);
createProperties(desc, superClass, level + 1, mapGenerics(beanType));
}
} catch (PersistenceException ex) {
throw ex;
@@ -116,8 +118,10 @@ public final class DeployCreateProperties {
return new DeployBeanPropertyAssocMany<>(desc, targetType, manyType);
}
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field) {
Class<?> propertyType = field.getType();
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
Class<?> propertyType = field.getGenericType() instanceof TypeVariable<?>
? genericTypeMap.get(field.getGenericType())
: field.getType();
if (isSpecialScalarType(field)) {
return new DeployBeanProperty(desc, propertyType, field.getGenericType());
}
@@ -172,8 +176,8 @@ public final class DeployCreateProperties {
return AnnotationUtil.has(field, Transient.class);
}
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType) {
DeployBeanProperty prop = createProp(desc, field);
private DeployBeanProperty createProp(DeployBeanDescriptor<?> desc, Field field, Class<?> beanType, Map<TypeVariable<?>, Class<?>> genericTypeMap) {
DeployBeanProperty prop = createProp(desc, field, genericTypeMap);
if (prop == null) {
// transient annotation on unsupported type
return null;
@@ -224,4 +228,40 @@ public final class DeployCreateProperties {
// if targetType is null, then must be set in annotations
return null;
}
private Map<TypeVariable<?>, Class<?>> mapGenerics(Class<?> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
if (!(genericSuperclass instanceof ParameterizedType)) {
return new HashMap<>();
}
ParameterizedType parameterized = (ParameterizedType) genericSuperclass;
TypeVariable<?>[] typeVars = ((Class<?>) parameterized.getRawType()).getTypeParameters();
Type[] actualTypes = parameterized.getActualTypeArguments();
Map<TypeVariable<?>, Class<?>> typeMap = new HashMap<>();
for (int i = 0; i < typeVars.length; i++) {
Type actual = actualTypes[i];
Class<?> resolvedClass = resolveToClass(actual);
if (resolvedClass != null) {
typeMap.put(typeVars[i], resolvedClass);
} else {
// ignore
}
}
return typeMap;
}
private static Class<?> resolveToClass(Type type) {
if (type instanceof Class<?>) {
return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) type;
Type raw = pType.getRawType();
if (raw instanceof Class<?>) {
return (Class<?>) raw;
}
}
return null;
}
}
@@ -12,7 +12,7 @@ final class InTuplesExpression extends AbstractExpression {
private final boolean not;
private final String[] properties;
private final List<Object[]> entries;
private List<Object[]> entries;
InTuplesExpression(InTuples pairs, boolean not) {
super("");
@@ -25,7 +25,15 @@ final class InTuplesExpression extends AbstractExpression {
@Override
public boolean naturalKey(NaturalKeyQueryData<?> data) {
return false;
if (not) {
return false;
}
List<Object[]> copy = data.matchInTuples(properties, entries);
if (copy == null) {
return false;
}
entries = copy;
return true;
}
@Override
@@ -227,7 +227,7 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext
// re-add to the batch and lazy load from DB skipping l2 cache
if (loadingStarted.get()) {
if (CoreLog.markedAsDeleted.isLoggable(DEBUG)) {
CoreLog.markedAsDeleted.log(DEBUG, "Adding " + ebi + "to batch " + this + "after loadingStarted(2) ", new RuntimeException("Adding to batch after load(2"));
CoreLog.markedAsDeleted.log(DEBUG, "Adding " + ebi + "to batch " + this + "after loadingStarted(2) ", new RuntimeException("Adding to batch after load(2)"));
}
}
batch.add(ebi);
@@ -239,9 +239,36 @@ final class DLoadBeanContext extends DLoadBaseContext implements LoadBeanContext
return;
}
}
// ensure, that every bean in the batch is in the persistence context.
// this may happen, when bean was previously deleted, but the result is not yet committed.
List<Object> reincarnatedIds = ensureBatchInContext(ebi);
try {
context.desc.ebeanServer().loadBean(new LoadBeanRequest(this, ebi, context.hitCache));
batch.clear();
} finally {
if (reincarnatedIds != null) {
for (Object id : reincarnatedIds) {
context.desc.contextClear(persistenceContext, id);
}
}
}
}
context.desc.ebeanServer().loadBean(new LoadBeanRequest(this, ebi, context.hitCache));
batch.clear();
private List<Object> ensureBatchInContext(EntityBeanIntercept ebi) {
List<Object> reincarnatedIds = null;
for (EntityBeanIntercept batchEbi : batch) {
Object id = context.desc.getId(batchEbi.owner());
if (id != null && context.desc.contextPutIfAbsent(persistenceContext, id, batchEbi.owner()) == null) {
if (reincarnatedIds == null) {
reincarnatedIds = new ArrayList<>();
}
reincarnatedIds.add(id);
if (CoreLog.markedAsDeleted.isLoggable(DEBUG)) {
CoreLog.markedAsDeleted.log(DEBUG, "Temporary adding " + ebi + "to persistence context", new RuntimeException("Temporary adding bean to persistence context"));
}
}
}
return reincarnatedIds;
}
}
@@ -280,7 +280,7 @@ final class CQueryBuilder {
sql = wrapSelectCount(sql);
} else if (wrap || query.isRawSql()) {
// remove order by - mssql does not accept order by in subqueries
int pos = sql.lastIndexOf(" order by ");
int pos = lastTopLevelOrderBy(sql);
if (pos != -1) {
sql = sql.substring(0, pos);
}
@@ -300,6 +300,27 @@ final class CQueryBuilder {
return request.descriptor().includesAggregation(query.detail());
}
/**
* Find the last " order by " that is not inside parentheses (i.e. not inside a subquery).
* Returns the position or -1 if not found.
*/
static int lastTopLevelOrderBy(String sql) {
String target = " order by ";
int depth = 0;
int lastFound = -1;
for (int i = 0; i < sql.length(); i++) {
char c = sql.charAt(i);
if (c == '(') {
depth++;
} else if (c == ')') {
depth--;
} else if (depth == 0 && c == ' ' && sql.regionMatches(true, i, target, 0, target.length())) {
lastFound = i;
}
}
return lastFound;
}
private String wrapSelectCount(String sql) {
sql = "select count(*) from ( " + sql + ")";
if (selectCountWithAlias) {
@@ -235,7 +235,7 @@ final class DefaultFetchGroupQuery<T> implements SpiFetchGroupQuery<T>, SpiQuery
}
@Override
public Query<T> usingMaster() {
public Query<T> usingMaster(boolean useMaster) {
throw new RuntimeException("EB102: Only select() and fetch() clause is allowed on FetchGroup");
}
@@ -129,7 +129,7 @@ public final class DefaultOrmQueryEngine implements OrmQueryEngine {
result = finder.postProcessMany(request, result);
}
if (result != null && request.isBeanCachePutMany()) {
if (result != null && request.isBeanCachePut()) {
// load the individual beans into the bean cache
request.descriptor().cacheBeanPutAll(result.actualDetails());
}
@@ -371,10 +371,11 @@ public final class SqlTreeBuilder {
IncludesDistiller extraJoinDistill = new IncludesDistiller(desc, selectIncludes, predicateIncludes, manyWhereJoins, temporalMode);
Collection<SqlTreeNodeExtraJoin> extraJoins = extraJoinDistill.getExtraJoinRootNodes();
if (!extraJoins.isEmpty()) {
// add extra joins required to support predicates
// and/or order by clause
// add extra joins required to support predicates and/or order by clause
for (SqlTreeNodeExtraJoin extraJoin : extraJoins) {
myList.add(extraJoin);
if (!addToParent(extraJoin, myList)) {
myList.add(extraJoin);
}
if (extraJoin.isManyJoin()) {
// as we are now going to join to the many then we need
// to add the distinct to the sql query to stop duplicate
@@ -385,6 +386,20 @@ public final class SqlTreeBuilder {
}
}
/**
* Return true if the extra join was added as a child to one of the nodes.
*/
private boolean addToParent(SqlTreeNodeExtraJoin extraJoin, List<SqlTreeNode> myList) {
String parentPath = SplitName.split(extraJoin.prefix())[0];
for (SqlTreeNode maybeParent : myList) {
if (maybeParent.prefix().equals(parentPath)) {
maybeParent.addChild(extraJoin);
return true;
}
}
return false;
}
/**
* A subQuery has slightly different rules in that it just generates SQL (into
* the where clause) and its properties are not required to read the resultSet
@@ -641,7 +656,7 @@ public final class SqlTreeBuilder {
SqlTreeNodeExtraJoin root = findExtraJoinRoot(includeProp, extraJoin);
// register the root because these are the only ones we
// return back.
rootRegister.put(root.name(), root);
rootRegister.put(root.prefix(), root);
}
}
@@ -84,4 +84,12 @@ interface SqlTreeNode {
*/
default void unselectLobsForPlatform() {
}
default String prefix() {
return ""; // not matched
}
default void addChild(SqlTreeNode extraJoin) {
throw new UnsupportedOperationException();
}
}
@@ -99,6 +99,16 @@ class SqlTreeNodeBean implements SqlTreeNode {
this.pathMap = createPathMap(prefix, desc);
}
@Override
public String prefix() {
return prefix;
}
@Override
public void addChild(SqlTreeNode extraJoin) {
children.add(extraJoin);
}
@Override
public SqlTreeLoad createLoad() {
return new SqlTreeLoadBean(this);
@@ -25,7 +25,7 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode {
private final STreePropertyAssoc assocBeanProperty;
private final SpiQuery.TemporalMode temporalMode;
private final String prefix;
private final boolean manyJoin;
private boolean manyJoin;
private final boolean pathContainsMany;
private List<SqlTreeNode> children;
@@ -87,7 +87,8 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode {
return manyJoin;
}
public String name() {
@Override
public String prefix() {
return prefix;
}
@@ -95,6 +96,9 @@ final class SqlTreeNodeExtraJoin implements SqlTreeNode {
if (children == null) {
children = new ArrayList<>();
}
if (child.hasMany()) {
manyJoin = true;
}
children.add(child);
}
@@ -95,8 +95,8 @@ public final class DefaultDtoQuery<T> extends AbstractQuery implements SpiDtoQue
}
@Override
public DtoQuery<T> usingMaster() {
this.useMaster = true;
public DtoQuery<T> usingMaster(boolean useMaster) {
this.useMaster = useMaster;
return this;
}
@@ -1475,8 +1475,8 @@ public class DefaultOrmQuery<T> extends AbstractQuery implements SpiQuery<T> {
}
@Override
public Query<T> usingMaster() {
this.useMaster = true;
public Query<T> usingMaster(boolean useMaster) {
this.useMaster = useMaster;
return this;
}
@@ -60,8 +60,8 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
}
@Override
public SqlQuery usingMaster() {
this.useMaster = true;
public SqlQuery usingMaster(boolean useMaster) {
this.useMaster = useMaster;
return this;
}
@@ -269,6 +269,12 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
this.type = type;
}
@Override
public TypeQuery<T> usingMaster(boolean useMaster) {
DefaultRelationalQuery.this.usingMaster(useMaster);
return this;
}
@Override
public TypeQuery<T> usingTransaction(Transaction transaction) {
transaction(transaction);
@@ -308,6 +314,12 @@ public final class DefaultRelationalQuery extends AbstractQuery implements SpiSq
this.mapper = mapper;
}
@Override
public TypeQuery<T> usingMaster(boolean useMaster) {
DefaultRelationalQuery.this.usingMaster(useMaster);
return this;
}
@Override
public TypeQuery<T> usingTransaction(Transaction transaction) {
transaction(transaction);
@@ -150,11 +150,6 @@ final class ImplicitReadOnlyTransaction implements SpiTransaction, TxnProfileEve
return false;
}
@Override
public boolean isSkipCacheExplicit() {
return false;
}
@Override
public void setSkipCache(boolean skipCache) {
}
@@ -209,11 +209,6 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes {
return autoPersistUpdates;
}
@Override
public final boolean isSkipCacheExplicit() {
return (skipCache != null && !skipCache);
}
@Override
public final boolean isSkipCache() {
if (skipCache != null) return skipCache;
@@ -235,11 +235,6 @@ final class NoTransaction implements SpiTransaction {
public void setSkipCache(boolean skipCache) {
}
@Override
public boolean isSkipCacheExplicit() {
return false;
}
@Override
public boolean isSkipCache() {
return false;
@@ -0,0 +1,130 @@
package io.ebeaninternal.server.util;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.IOException;
/**
* Compute an order-independent structural hash of JSON content using Jackson's streaming parser.
* <p>
* Object key ordering does NOT affect the hash value (handles PostgreSQL JSONB key reordering),
* while array element ordering DOES affect it (array position is semantically significant).
* <p>
* This is significantly faster than a full parse/format roundtrip because it performs
* zero object allocation beyond the parser itself — no tree building, no reflection,
* no type conversion. Single-pass O(n) time with O(depth) stack space.
*/
public final class JsonContentHash {
private static final JsonFactory FACTORY = new JsonFactory();
/**
* Compute an order-independent hash of JSON content.
* Two JSON strings with identical content but different key ordering
* will produce the same hash value.
*/
public static long hash(String json) {
if (json == null || json.isEmpty()) {
return 0L;
}
try (JsonParser parser = FACTORY.createParser(json)) {
parser.nextToken();
return computeHash(parser);
} catch (IOException e) {
// Fallback to regular string hash if JSON is malformed.
// This is safe: two identical malformed strings produce the same hash,
// and a malformed string won't falsely match a valid one.
return stringHash(json);
}
}
private static long computeHash(JsonParser parser) throws IOException {
JsonToken token = parser.currentToken();
if (token == null) {
return 0L;
}
switch (token) {
case START_OBJECT:
return hashObject(parser);
case START_ARRAY:
return hashArray(parser);
case VALUE_STRING:
return mix(stringHash(parser.getText()));
case VALUE_NUMBER_INT:
case VALUE_NUMBER_FLOAT:
// Use text representation for numeric consistency across int/long/double
return mix(stringHash(parser.getText()));
case VALUE_TRUE:
return 0x9E3779B97F4A7C15L;
case VALUE_FALSE:
return 0x517CC1B727220A95L;
case VALUE_NULL:
return 0x6C62272E07BB0142L;
default:
return 0L;
}
}
// Type markers to distinguish empty object {}, empty array [], and null
private static final long OBJECT_SEED = 0x7A5662B4E8B10FA3L;
private static final long ARRAY_SEED = 0x3C6EF372FE94F82BL;
/**
* Hash an object using commutative addition of entry hashes.
* Addition is commutative (a + b == b + a), so the result is
* independent of the order in which keys appear in the JSON.
*/
private static long hashObject(JsonParser parser) throws IOException {
long hash = OBJECT_SEED;
while (parser.nextToken() != JsonToken.END_OBJECT) {
long keyHash = stringHash(parser.currentName());
parser.nextToken();
long valueHash = computeHash(parser);
// Mix key+value into a single entry hash, then add (commutative)
hash += mix(keyHash * 0x9E3779B97F4A7C15L + valueHash);
}
return hash;
}
/**
* Hash an array using position-dependent combination.
* Array element order IS semantically significant in JSON.
*/
private static long hashArray(JsonParser parser) throws IOException {
long hash = ARRAY_SEED;
while (parser.nextToken() != JsonToken.END_ARRAY) {
hash = hash * 31 + computeHash(parser);
}
return mix(hash);
}
/**
* 64-bit FNV-1a inspired string hash for better distribution than String.hashCode().
*/
private static long stringHash(String s) {
long h = 0xcbf29ce484222325L;
for (int i = 0; i < s.length(); i++) {
h ^= s.charAt(i);
h *= 0x100000001b3L;
}
return h;
}
/**
* Mixing/finalizer function to improve hash distribution and break
* additive symmetry (prevents collisions when values are swapped between keys).
* <p>
* This is fmix64 from MurmurHash3 by Austin Appleby (public domain).
* See: https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp
*/
private static long mix(long h) {
h ^= (h >>> 33);
h *= 0xff51afd7ed558ccdL;
h ^= (h >>> 33);
h *= 0xc4ceb9fe1a85ec53L;
h ^= (h >>> 33);
return h;
}
}
@@ -55,7 +55,7 @@ class BasicProfileLocationTest {
DProfileLocation loc = new DTimedProfileLocation("foo", MetricFactory.get().createTimedMetric("junk"));
assertThat(loc.obtain()).isTrue();
assertThat(loc.fullLocation()).endsWith("org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)");
assertThat(loc.fullLocation()).endsWith("org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:786)");
assertThat(loc.location()).isEqualTo("org.junit.platform.commons.util.ReflectionUtils.invokeMethod");
assertThat(loc.label()).isEqualTo("ReflectionUtils.invokeMethod");
}
@@ -0,0 +1,118 @@
package io.ebeaninternal.server.query;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class CQueryBuilderTest {
/**
* Simulates what buildRowCountQuery does: strip the top-level order by, then wrap with count.
* This is the logic that must use lastTopLevelOrderBy instead of lastIndexOf.
*/
private static String simulateCountWrap(String sql) {
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
if (pos != -1) {
sql = sql.substring(0, pos);
}
return "select count(*) from ( " + sql + ") as c";
}
private static int countChar(String s, char c) {
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == c) count++;
}
return count;
}
@Test
void lastTopLevelOrderBy_simple() {
String sql = "select t0.id from ad t0 order by t0.id";
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
assertThat(pos).isEqualTo(sql.indexOf(" order by "));
}
@Test
void lastTopLevelOrderBy_noOrderBy() {
String sql = "select t0.id from ad t0";
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
assertThat(pos).isEqualTo(-1);
}
@Test
void lastTopLevelOrderBy_insideSubquery() {
// order by is only inside a subquery - should not be found at top level
String sql = "select t0.id from ad t0 where t0.id in (select t0.id from ad t0 order by t0.rebate)";
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
assertThat(pos).isEqualTo(-1);
}
@Test
void lastTopLevelOrderBy_bothLevels() {
// order by inside subquery AND at top level - should find only the top-level one
String sql = "select t0.id from ad t0 where t0.id in (select t0.id from ad t0 order by t0.rebate) order by t0.id";
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
assertThat(sql.substring(pos)).isEqualTo(" order by t0.id");
}
@Test
void lastTopLevelOrderBy_nestedSubqueries() {
// deeply nested order by should not be found
String sql = "select t0.id from ad t0 where t0.id in (select t0.id from ad t0 where t0.x in (select id from foo order by bar))";
int pos = CQueryBuilder.lastTopLevelOrderBy(sql);
assertThat(pos).isEqualTo(-1);
}
/**
* Reproduces the exact scenario from https://github.com/ebean-orm/ebean/issues/3686
*
* With lastIndexOf(" order by "), the inner subquery's order by is matched,
* stripping its closing parenthesis and producing unbalanced SQL.
*/
@Test
void countWrap_formulaJoinWithSubqueryOrderBy_issue3686() {
// This is the SQL that buildRowCountQuery would produce before wrapping,
// matching the bug report: @Formula with JOIN + IN subquery with distinctOn + orderBy
String innerSql = "select t0.id from ad t0"
+ " LEFT JOIN price_range ON price_range.ad_id = t0.id"
+ " where t0.id in (select distinct on (t0.rebate) t0.id from ad t0 order by t0.rebate)";
String countSql = simulateCountWrap(innerSql);
// The subquery's closing ) must be preserved
assertThat(countSql).contains("order by t0.rebate)");
// Parentheses must be balanced
assertThat(countChar(countSql, '(')).isEqualTo(countChar(countSql, ')'));
// Should end with ") as c" - the outer count wrapper's closing paren
assertThat(countSql).endsWith(") as c");
}
@Test
void countWrap_topLevelOrderByIsStripped() {
// When there IS a top-level order by, it should be stripped
String innerSql = "select t0.id from ad t0"
+ " LEFT JOIN price_range ON price_range.ad_id = t0.id"
+ " where t0.id in (select distinct on (t0.rebate) t0.id from ad t0 order by t0.rebate)"
+ " order by price_range.discounted_price";
String countSql = simulateCountWrap(innerSql);
// Top-level order by should be removed
assertThat(countSql).doesNotContain("discounted_price");
// But inner subquery order by must remain intact
assertThat(countSql).contains("order by t0.rebate)");
// Parentheses must be balanced
assertThat(countChar(countSql, '(')).isEqualTo(countChar(countSql, ')'));
}
@Test
void countWrap_simpleOrderByIsStripped() {
// Simple case: top-level order by with no subquery
String innerSql = "select t0.id from ad t0 order by t0.id";
String countSql = simulateCountWrap(innerSql);
assertThat(countSql).isEqualTo("select count(*) from ( select t0.id from ad t0) as c");
}
}
@@ -0,0 +1,162 @@
package io.ebeaninternal.server.util;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class JsonContentHashTest {
@Test
void sameContent_sameHash() {
String json = "{\"name\":\"Alice\",\"age\":30}";
assertThat(JsonContentHash.hash(json)).isEqualTo(JsonContentHash.hash(json));
}
@Test
void reorderedKeys_sameHash() {
// The core scenario: PostgreSQL JSONB reorders keys
String jackson = "{\"status\":\"ACTIVE\",\"type\":\"ADMIN\"}";
String postgres = "{\"type\":\"ADMIN\",\"status\":\"ACTIVE\"}";
assertThat(JsonContentHash.hash(jackson)).isEqualTo(JsonContentHash.hash(postgres));
}
@Test
void reorderedKeys_multipleFields() {
String a = "{\"zebra\":1,\"apple\":2,\"mango\":3}";
String b = "{\"apple\":2,\"mango\":3,\"zebra\":1}";
String c = "{\"mango\":3,\"zebra\":1,\"apple\":2}";
long hashA = JsonContentHash.hash(a);
long hashB = JsonContentHash.hash(b);
long hashC = JsonContentHash.hash(c);
assertThat(hashA).isEqualTo(hashB);
assertThat(hashA).isEqualTo(hashC);
}
@Test
void differentValues_differentHash() {
String a = "{\"status\":\"ACTIVE\",\"type\":\"ADMIN\"}";
String b = "{\"status\":\"INACTIVE\",\"type\":\"ADMIN\"}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void differentKeys_differentHash() {
String a = "{\"name\":\"Alice\"}";
String b = "{\"nome\":\"Alice\"}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void nestedObjects_reorderedKeys() {
String a = "{\"user\":{\"first\":\"Alice\",\"last\":\"Smith\"},\"active\":true}";
String b = "{\"active\":true,\"user\":{\"last\":\"Smith\",\"first\":\"Alice\"}}";
assertThat(JsonContentHash.hash(a)).isEqualTo(JsonContentHash.hash(b));
}
@Test
void nestedObjects_differentValues() {
String a = "{\"user\":{\"first\":\"Alice\",\"last\":\"Smith\"}}";
String b = "{\"user\":{\"first\":\"Bob\",\"last\":\"Smith\"}}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void arrayOrder_matters() {
// Array element order IS semantically significant
String a = "[1,2,3]";
String b = "[3,2,1]";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void arrayOrder_sameOrder_sameHash() {
String a = "[1,2,3]";
String b = "[1,2,3]";
assertThat(JsonContentHash.hash(a)).isEqualTo(JsonContentHash.hash(b));
}
@Test
void enumValues_reorderedKeys() {
// The exact scenario from issue #3129: POJO with multiple enum fields
String jackson = "{\"status\":\"ACTIVE\",\"role\":\"ADMIN\",\"priority\":\"HIGH\"}";
String postgres = "{\"role\":\"ADMIN\",\"priority\":\"HIGH\",\"status\":\"ACTIVE\"}";
assertThat(JsonContentHash.hash(jackson)).isEqualTo(JsonContentHash.hash(postgres));
}
@Test
void swappedValues_differentHash() {
// Swapping values between keys must produce different hashes
String a = "{\"a\":1,\"b\":2}";
String b = "{\"a\":2,\"b\":1}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void emptyObject() {
assertThat(JsonContentHash.hash("{}")).isNotEqualTo(0L);
}
@Test
void emptyArray() {
assertThat(JsonContentHash.hash("[]")).isNotEqualTo(0L);
}
@Test
void emptyObject_vs_emptyArray() {
assertThat(JsonContentHash.hash("{}")).isNotEqualTo(JsonContentHash.hash("[]"));
}
@Test
void nullInput() {
assertThat(JsonContentHash.hash(null)).isEqualTo(0L);
}
@Test
void emptyString() {
assertThat(JsonContentHash.hash("")).isEqualTo(0L);
}
@Test
void booleanValues() {
String a = "{\"flag\":true}";
String b = "{\"flag\":false}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void nullValues() {
String a = "{\"value\":null}";
String b = "{\"value\":\"text\"}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void numericTypes() {
String a = "{\"count\":42}";
String b = "{\"count\":43}";
assertThat(JsonContentHash.hash(a)).isNotEqualTo(JsonContentHash.hash(b));
}
@Test
void whitespaceVariations() {
// Whitespace in JSON structure (not in values) should not matter
String compact = "{\"a\":1,\"b\":2}";
String spaced = "{ \"a\" : 1 , \"b\" : 2 }";
assertThat(JsonContentHash.hash(compact)).isEqualTo(JsonContentHash.hash(spaced));
}
@Test
void complexNestedStructure() {
String a = "{\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}],\"count\":2,\"active\":true}";
String b = "{\"active\":true,\"count\":2,\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]}";
assertThat(JsonContentHash.hash(a)).isEqualTo(JsonContentHash.hash(b));
}
@Test
void postgresJsonbKeyReordering_realistic() {
// Simulates PostgreSQL JSONB storage which reorders by key length, then alphabetically
String javaOrder = "{\"status\":\"ACTIVE\",\"type\":\"STANDARD\",\"createdAt\":\"2024-01-01\",\"id\":123}";
String pgOrder = "{\"id\":123,\"type\":\"STANDARD\",\"status\":\"ACTIVE\",\"createdAt\":\"2024-01-01\"}";
assertThat(JsonContentHash.hash(javaOrder)).isEqualTo(JsonContentHash.hash(pgOrder));
}
}
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean ddl generation</name>
@@ -28,14 +28,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -65,7 +65,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
+2 -2
View File
@@ -3,7 +3,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -15,7 +15,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core-type</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean net postgis types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean pgvector types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -54,7 +54,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
+4 -4
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean postgis types</name>
@@ -19,14 +19,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<!-- provided scope -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -62,7 +62,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
+5 -5
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean querybean</name>
@@ -17,7 +17,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -59,14 +59,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -80,7 +80,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -739,8 +739,8 @@ public abstract class QueryBean<T, R extends QueryBean<T, R>> implements IQueryB
}
@Override
public final R usingMaster() {
query.usingMaster();
public R usingMaster(boolean useMaster) {
query.usingMaster(useMaster);
return root;
}
@@ -0,0 +1,69 @@
package org.example.domain;
import io.ebean.Model;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import java.sql.Timestamp;
/**
* Base domain object with Id, version, whenCreated and whenUpdated.
*
* <p>
* Extending Model to enable the 'active record' style.
*
* <p>
* whenCreated and whenUpdated are generally useful for maintaining external search services (like
* elasticsearch) and audit.
*/
@MappedSuperclass
public abstract class GenericBaseModel<T> extends Model {
@Id
T id;
@Version
Long version;
@WhenCreated
Timestamp whenCreated;
@WhenModified
Timestamp whenUpdated;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Timestamp getWhenCreated() {
return whenCreated;
}
public void setWhenCreated(Timestamp whenCreated) {
this.whenCreated = whenCreated;
}
public Timestamp getWhenUpdated() {
return whenUpdated;
}
public void setWhenUpdated(Timestamp whenUpdated) {
this.whenUpdated = whenUpdated;
}
}
@@ -0,0 +1,48 @@
package org.example.domain;
import org.example.domain.ProductWithGenericLong;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.validation.constraints.Size;
/**
* Product entity bean.
*/
@Entity
@Table(name = "long_product", schema = "foo")
public class ProductWithGenericLong extends GenericBaseModel<Long> {
@Size(max = 20)
String sku;
String name;
/**
* Return sku.
*/
public String getSku() {
return sku;
}
/**
* Set sku.
*/
public void setSku(String sku) {
this.sku = sku;
}
/**
* Return name.
*/
public String getName() {
return name;
}
/**
* Set name.
*/
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,48 @@
package org.example.domain;
import org.example.domain.ProductWithGenericLong;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import javax.validation.constraints.Size;
/**
* Product entity bean.
*/
@Entity
@Table(name = "string_product", schema = "foo")
public class ProductWithGenericString extends GenericBaseModel<String> {
@Size(max = 20)
String sku;
String name;
/**
* Return sku.
*/
public String getSku() {
return sku;
}
/**
* Set sku.
*/
public void setSku(String sku) {
this.sku = sku;
}
/**
* Return name.
*/
public String getName() {
return name;
}
/**
* Set name.
*/
public void setName(String name) {
this.name = name;
}
}
@@ -446,6 +446,22 @@ public class QCustomerTest {
.findList();
}
@Test
public void usingMaster_true() {
new QCustomer()
.registered.isNull()
.usingMaster(true)
.findList();
}
@Test
public void usingMaster_false() {
new QCustomer()
.registered.isNull()
.usingMaster(false)
.findList();
}
@Test
public void usingTransaction() {
try (Transaction transaction = DB.getDefault().createTransaction()) {
@@ -0,0 +1,49 @@
package org.querytest;
import io.ebean.InTuples;
import org.example.domain.ProductWithGenericLong;
import org.example.domain.ProductWithGenericString;
import org.example.domain.query.QContact;
import org.example.domain.query.QProductWithGenericLong;
import org.example.domain.query.QProductWithGenericString;
import org.junit.jupiter.api.Test;
import java.time.ZonedDateTime;
import static org.assertj.core.api.Assertions.assertThat;
public class QProductWithGenericTest {
@Test
void findByLongId() {
var entity = new ProductWithGenericLong();
entity.setId(42L);
entity.setName("Gadget");
entity.save();
var result = new QProductWithGenericLong()
.id.eq(42L)
.findOne();
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("Gadget");
}
@Test
void findByStringId() {
var entity = new ProductWithGenericString();
entity.setId("1234");
entity.setName("Gadget");
entity.save();
var result = new QProductWithGenericString()
.id.eq("1234")
.findOne();
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("Gadget");
}
}
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>ebean-redis</artifactId>
@@ -29,35 +29,35 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -1,6 +1,8 @@
package org.integration;
import io.ebean.DB;
import io.ebean.InTuples;
import io.ebean.Pairs;
import io.ebean.cache.ServerCache;
import io.ebean.cache.ServerCacheStatistics;
import org.domain.*;
@@ -177,6 +179,76 @@ class IntegrationTest {
.findOne();
}
@Test
void naturalKey_inPairs() throws InterruptedException {
DB.save(new OtherOne("ip_A", "ip_1", "ip_A1"));
DB.save(new OtherOne("ip_A", "ip_2", "ip_A2"));
DB.save(new OtherOne("ip_B", "ip_1", "ip_B1"));
ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(OtherOne.class);
nkeyCache.clear();
Pairs pairs = new Pairs("one", "two")
.add("ip_A", "ip_1")
.add("ip_A", "ip_2")
.add("ip_B", "ip_1");
// first fetch — miss, populates natural key + bean cache
List<OtherOne> list0 = DB.find(OtherOne.class)
.where()
.inPairs(pairs)
.setUseCache(true)
.findList();
assertThat(list0).hasSize(3);
nkeyCache.statistics(true); // reset stats
Thread.sleep(5);
// second fetch — all three should hit the natural key cache
List<OtherOne> list1 = DB.find(OtherOne.class)
.where()
.inPairs(pairs)
.setUseCache(true)
.findList();
assertThat(list1).hasSize(3);
assertThat(nkeyCache.statistics(true).getHitCount()).isEqualTo(3);
}
@Test
void naturalKey_inTuples() throws InterruptedException {
DB.save(new OtherOne("it_A", "it_1", "it_A1"));
DB.save(new OtherOne("it_A", "it_2", "it_A2"));
DB.save(new OtherOne("it_B", "it_1", "it_B1"));
ServerCache nkeyCache = DB.cacheManager().naturalKeyCache(OtherOne.class);
nkeyCache.clear();
InTuples tuples = InTuples.of("one", "two")
.add("it_A", "it_1")
.add("it_A", "it_2")
.add("it_B", "it_1");
// first fetch — miss, populates natural key + bean cache
List<OtherOne> list0 = DB.find(OtherOne.class)
.where()
.inTuples(tuples)
.setUseCache(true)
.findList();
assertThat(list0).hasSize(3);
nkeyCache.statistics(true); // reset stats
Thread.sleep(5);
// second fetch — all three should hit the natural key cache
List<OtherOne> list1 = DB.find(OtherOne.class)
.where()
.inTuples(tuples)
.setUseCache(true)
.findList();
assertThat(list1).hasSize(3);
assertThat(nkeyCache.statistics(true).getHitCount()).isEqualTo(3);
}
@Test
void test() throws InterruptedException {
+3 -3
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<artifactId>ebean-spring-txn</artifactId>
@@ -28,7 +28,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
@@ -77,7 +77,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
+7 -7
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>ebean test</name>
@@ -13,7 +13,7 @@
<properties>
<bytebuddy.version>1.18.1</bytebuddy.version>
<assertj.version>3.27.6</assertj.version>
<assertj.version>3.27.7</assertj.version>
</properties>
<dependencies>
@@ -33,20 +33,20 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
@@ -149,14 +149,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -0,0 +1,202 @@
package io.ebean.test;
import io.ebeaninternal.server.deploy.BeanProperty;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* Generates random values for entity bean properties in tests.
* <p>
* The primary entry point is {@link #generate(BeanProperty)}, which is
* property-aware (e.g. caps String values at the column's max length).
* The secondary entry point {@link #generate(Class)} works on the Java type
* alone and is useful when no property metadata is available.
* </p>
* <p>
* All per-type factory methods ({@link #randomString(String, int)},
* {@link #randomBigDecimal(int, int)}, {@link #randomLong()}, etc.) are
* {@code protected} so that subclasses can override individual types without
* replacing the full dispatch logic.
* </p>
* <p>
* Returns {@code null} for types that are not mapped (exotic / unknown types) —
* the caller is expected to set those fields manually.
* </p>
*/
public class RandomValueGenerator {
/**
* Generate a random value for the given bean property.
* <p>
* For {@code String} properties, the value is capped at the column's
* {@link BeanProperty#dbLength()} when that length is positive.
* For {@code BigDecimal} properties, precision and scale from the column
* definition are used.
* </p>
*/
public Object generate(BeanProperty prop) {
Class<?> type = prop.type();
if (type == String.class) {
return randomString(prop.name(), prop.dbLength());
}
if (type == BigDecimal.class) {
return randomBigDecimal(prop.dbLength(), prop.dbScale());
}
return generate(type);
}
/**
* Generate a random value for the given Java type, without property metadata.
* <p>
* String values produced here use a fixed 8-character length. Use
* {@link #generate(BeanProperty)} when column-length constraints matter.
* </p>
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Object generate(Class<?> type) {
if (type == null) return null;
if (type == String.class) return randomString(null, 0);
if (type == Boolean.class || type == boolean.class) return randomBoolean();
if (type == UUID.class) return randomUUID();
if (type == Instant.class) return randomInstant();
if (type == OffsetDateTime.class) return randomOffsetDateTime();
if (type == ZonedDateTime.class) return randomZonedDateTime();
if (type == LocalDate.class) return randomLocalDate();
if (type == LocalDateTime.class) return randomLocalDateTime();
if (type == Long.class || type == long.class) return randomLong();
if (type == Integer.class || type == int.class) return randomInt();
if (type == Short.class || type == short.class) return randomShort();
if (type == BigDecimal.class) return randomBigDecimal(0, -1);
if (type == Double.class || type == double.class) return randomDouble();
if (type == Float.class || type == float.class) return randomFloat();
if (type.isEnum()) return randomEnum(type);
return null; // exotic/unknown type — caller must set this field manually
}
/** Return a random {@code long} in [1, 100_000). */
protected long randomLong() {
return ThreadLocalRandom.current().nextLong(1, 100_000L);
}
/** Return a random {@code int} in [1, 1_000). */
protected int randomInt() {
return ThreadLocalRandom.current().nextInt(1, 1_000);
}
/** Return a random {@code short} in [1, 100). */
protected short randomShort() {
return (short) ThreadLocalRandom.current().nextInt(1, 100);
}
/** Return a random {@code boolean} — defaults to {@code true}. */
protected boolean randomBoolean() {
return true;
}
/** Return a random {@link UUID}. */
protected UUID randomUUID() {
return UUID.randomUUID();
}
/** Return a random {@link Instant} — defaults to now. */
protected Instant randomInstant() {
return Instant.now();
}
/** Return a random {@link OffsetDateTime} — defaults to now. */
protected OffsetDateTime randomOffsetDateTime() {
return OffsetDateTime.now();
}
/** Return a random {@link ZonedDateTime} — defaults to now. */
protected ZonedDateTime randomZonedDateTime() {
return ZonedDateTime.now();
}
/** Return a random {@link LocalDate} — defaults to today. */
protected LocalDate randomLocalDate() {
return LocalDate.now();
}
/** Return a random {@link LocalDateTime} — defaults to now. */
protected LocalDateTime randomLocalDateTime() {
return LocalDateTime.now();
}
/** Return a random {@code double} in [1, 100). */
protected double randomDouble() {
return ThreadLocalRandom.current().nextDouble(1, 100);
}
/** Return a random {@code float} in [1, 100). */
protected float randomFloat() {
return (float) ThreadLocalRandom.current().nextDouble(1, 100);
}
/**
* Return a value for the given enum type — defaults to the first declared constant.
* Returns {@code null} if the enum has no constants.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
protected Object randomEnum(Class<?> type) {
Object[] constants = type.getEnumConstants();
return constants.length > 0 ? constants[0] : null;
}
/**
* Generate a random string, optionally capped at {@code maxLength}.
* <p>
* When the property name contains "email" (case-insensitive), a value of the
* form {@code <prefix>@domain.com} is returned, truncated to fit {@code maxLength}.
* Otherwise a UUID-derived string is returned, truncated to {@code maxLength}
* (defaulting to 8 characters when {@code maxLength} is 0 or negative).
* </p>
*/
protected String randomString(String propName, int maxLength) {
String base = UUID.randomUUID().toString().replace("-", ""); // 32 chars
if (propName != null && propName.toLowerCase().contains("email")) {
String email = base.substring(0, 8) + "@domain.com";
if (maxLength > 0 && email.length() > maxLength) {
int localLen = maxLength - "@domain.com".length();
email = (localLen > 0 ? base.substring(0, localLen) : base.substring(0, 1)) + "@domain.com";
}
return email;
}
if (maxLength <= 0) {
return base.substring(0, 8);
}
int len = Math.min(maxLength, base.length());
return base.substring(0, len);
}
/**
* Generate a random {@link BigDecimal} that fits within the given precision and scale.
* <p>
* {@code precision} is the total number of significant digits ({@code dbLength});
* {@code scale} is the number of decimal places ({@code dbScale}).
* When precision is 0 or negative (unknown), a default of 6 integer digits is used.
* When scale is negative (unknown), a default scale of 2 is used.
* </p>
*/
protected BigDecimal randomBigDecimal(int precision, int scale) {
int actualScale = scale >= 0 ? scale : 2;
int intDigits = precision > 0 ? Math.max(1, precision - actualScale) : 6;
long maxInt = (long) Math.pow(10, intDigits) - 1;
long intPart = ThreadLocalRandom.current().nextLong(1, maxInt + 1);
if (actualScale == 0) {
return BigDecimal.valueOf(intPart);
}
long scaleFactor = (long) Math.pow(10, actualScale);
long fracPart = ThreadLocalRandom.current().nextLong(0, scaleFactor);
double value = intPart + (double) fracPart / scaleFactor;
return BigDecimal.valueOf(value).setScale(actualScale, RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,153 @@
package io.ebean.test;
import io.ebean.Database;
import io.ebean.bean.EntityBean;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import io.ebeaninternal.server.deploy.BeanPropertyAssocMany;
import io.ebeaninternal.server.deploy.BeanPropertyAssocOne;
import java.util.HashSet;
import java.util.Set;
import static java.util.Objects.requireNonNull;
/**
* Builds entity bean instances with randomly populated scalar fields for use in tests.
* <p>
* {@code @Id} and {@code @Version} properties are left at their defaults (zero/null).
* {@code @ManyToOne} and {@code @OneToOne} relationships that have cascade persist are
* recursively built and set. Collection relationships ({@code @OneToMany},
* {@code @ManyToMany}) are left empty — the caller can populate them if needed.
* </p>
* <pre>{@code
* TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault()).build();
*
* // build in-memory (not saved to database)
* MyEntity entity = builder.build(MyEntity.class);
*
* // build and insert to the database
* MyEntity entity = builder.save(MyEntity.class);
*
* // supply a custom value generator for domain-specific values
* TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault())
* .valueGenerator(myGenerator)
* .build();
* }</pre>
*/
public class TestEntityBuilder {
private final Database database;
private final RandomValueGenerator valueGenerator;
private TestEntityBuilder(Database database, RandomValueGenerator valueGenerator) {
this.database = database;
this.valueGenerator = valueGenerator;
}
/** Returns a new {@link Builder} for the given database. */
public static Builder builder(Database database) {
return new Builder(database);
}
/** Builder for {@link TestEntityBuilder}. */
public static final class Builder {
private final Database database;
private RandomValueGenerator valueGenerator;
private Builder(Database database) {
this.database = requireNonNull(database);
}
/** Override the default {@link RandomValueGenerator}, e.g. for domain-specific value generation. */
public Builder valueGenerator(RandomValueGenerator valueGenerator) {
this.valueGenerator = valueGenerator;
return this;
}
/** Build and return a {@link TestEntityBuilder}. */
public TestEntityBuilder build() {
if (valueGenerator == null) {
valueGenerator = new RandomValueGenerator();
}
return new TestEntityBuilder(database, valueGenerator);
}
}
/**
* Build and return an instance of the entity class with scalar fields populated
* with random values. The entity is not saved to the database.
*
* @param beanClass the entity class to build
* @throws IllegalArgumentException if the class is not a known Ebean entity
*/
public <T> T build(Class<T> beanClass) {
return build(beanClass, new HashSet<>());
}
/**
* Build an instance of the entity class, insert it to the database, and return it.
*
* @param beanClass the entity class to build and save
* @throws IllegalArgumentException if the class is not a known Ebean entity
*/
public <T> T save(Class<T> beanClass) {
T bean = build(beanClass);
database.save(bean);
return bean;
}
private <T> T build(Class<T> beanClass, Set<Class<?>> buildStack) {
BeanDescriptor<T> descriptor = (BeanDescriptor<T>) database.pluginApi().beanType(beanClass);
if (descriptor == null) {
throw new IllegalArgumentException("No BeanDescriptor found for " + beanClass.getName()
+ " — is it an @Entity registered with this Database?");
}
Set<String> importedSaveNames = importedSavePropertyNames(descriptor);
T bean = descriptor.createBean();
buildStack.add(beanClass);
try {
for (BeanProperty prop : descriptor.propertiesAll()) {
if (prop.isId() || prop.isVersion() || prop.isGenerated() || prop.isTransient()) {
continue;
}
if (prop instanceof BeanPropertyAssocMany) {
// leave collections empty — caller populates if needed
continue;
}
if (prop instanceof BeanPropertyAssocOne) {
if (importedSaveNames.contains(prop.name())) {
BeanPropertyAssocOne<?> assocOne = (BeanPropertyAssocOne<?>) prop;
Class<?> targetType = assocOne.targetType();
if (!buildStack.contains(targetType)) {
Object related = build(targetType, buildStack);
prop.setValue((EntityBean) bean, related);
}
// else: cycle detected — leave the reference null
}
// non-cascade-save association — leave null
continue;
}
// scalar property
Object value = valueGenerator.generate(prop);
if (value != null) {
prop.setValue((EntityBean) bean, value);
}
}
} finally {
buildStack.remove(beanClass);
}
return bean;
}
private Set<String> importedSavePropertyNames(BeanDescriptor<?> descriptor) {
Set<String> names = new HashSet<>();
for (BeanPropertyAssocOne<?> p : descriptor.propertiesOneImportedSave()) {
names.add(p.name());
}
return names;
}
}
@@ -0,0 +1,242 @@
package io.ebean.test;
import io.ebean.DB;
import io.ebean.xtest.BaseTestCase;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanProperty;
import org.junit.jupiter.api.Test;
import org.tests.cache.personinfo.PersonOther;
import org.tests.model.basic.EBasic;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class RandomValueGeneratorTest extends BaseTestCase {
private final RandomValueGenerator generator = new RandomValueGenerator();
private <T> BeanDescriptor<T> descriptor(Class<T> cls) {
return (BeanDescriptor<T>) DB.getDefault().pluginApi().beanType(cls);
}
@Test
void generate_stringType_returnsEightCharString() {
Object value = generator.generate(String.class);
assertThat(value).isInstanceOf(String.class);
assertThat((String) value).hasSize(8);
}
@Test
void generate_stringPropWithLength_cappedAtDbLength() {
BeanDescriptor<EBasic> descriptor = descriptor(EBasic.class);
BeanProperty nameProp = descriptor.findProperty("name");
assertThat(nameProp.dbLength()).isEqualTo(127);
Object value = generator.generate(nameProp);
assertThat(value).isInstanceOf(String.class);
assertThat((String) value).hasSizeLessThanOrEqualTo(127);
assertThat((String) value).isNotEmpty();
}
@Test
void generate_stringPropWithNoLength_returnsEightCharString() {
BeanDescriptor<EBasic> descriptor = descriptor(EBasic.class);
BeanProperty descProp = descriptor.findProperty("description");
// description has no @Size annotation — dbLength is 0 (unlimited)
Object value = generator.generate(descProp);
assertThat(value).isInstanceOf(String.class);
assertThat((String) value).hasSize(8);
}
@Test
void generate_variousScalarTypes_returnsExpectedTypes() {
assertThat(generator.generate(Long.class)).isInstanceOf(Long.class);
assertThat(generator.generate(long.class)).isInstanceOf(Long.class);
assertThat(generator.generate(Integer.class)).isInstanceOf(Integer.class);
assertThat(generator.generate(int.class)).isInstanceOf(Integer.class);
assertThat(generator.generate(Short.class)).isInstanceOf(Short.class);
assertThat(generator.generate(short.class)).isInstanceOf(Short.class);
assertThat(generator.generate(Boolean.class)).isEqualTo(Boolean.TRUE);
assertThat(generator.generate(boolean.class)).isEqualTo(Boolean.TRUE);
assertThat(generator.generate(UUID.class)).isInstanceOf(UUID.class);
assertThat(generator.generate(Instant.class)).isInstanceOf(Instant.class);
assertThat(generator.generate(OffsetDateTime.class)).isInstanceOf(OffsetDateTime.class);
assertThat(generator.generate(ZonedDateTime.class)).isInstanceOf(ZonedDateTime.class);
assertThat(generator.generate(LocalDate.class)).isInstanceOf(LocalDate.class);
assertThat(generator.generate(LocalDateTime.class)).isInstanceOf(LocalDateTime.class);
assertThat(generator.generate(BigDecimal.class)).isInstanceOf(BigDecimal.class);
assertThat(generator.generate(Double.class)).isInstanceOf(Double.class);
assertThat(generator.generate(Float.class)).isInstanceOf(Float.class);
}
@Test
void generate_enumType_returnsFirstConstant() {
Object value = generator.generate(EBasic.Status.class);
assertThat(value).isEqualTo(EBasic.Status.NEW);
}
@Test
void generate_unknownType_returnsNull() {
assertThat(generator.generate(Object.class)).isNull();
assertThat(generator.generate((Class<?>) null)).isNull();
}
@Test
void generate_emailPropName_returnsEmailAddress() {
BeanDescriptor<PersonOther> descriptor = descriptor(PersonOther.class);
BeanProperty emailProp = descriptor.findProperty("email");
assertThat(emailProp.dbLength()).isEqualTo(60);
Object value = generator.generate(emailProp);
assertThat(value).isInstanceOf(String.class);
assertThat((String) value).contains("@domain.com");
assertThat((String) value).hasSizeLessThanOrEqualTo(60);
}
@Test
void generate_bigDecimalType_returnsScaledValue() {
Object value = generator.generate(BigDecimal.class);
assertThat(value).isInstanceOf(BigDecimal.class);
BigDecimal decimal = (BigDecimal) value;
assertThat(decimal.scale()).isEqualTo(2);
assertThat(decimal).isGreaterThan(BigDecimal.ZERO);
}
@Test
void generate_stringPropWithLengthShorterThan32_truncatesCorrectly() {
BeanDescriptor<EBasic> descriptor = descriptor(EBasic.class);
BeanProperty nameProp = descriptor.findProperty("name");
// Run many times to verify no value ever exceeds the limit
for (int i = 0; i < 20; i++) {
Object value = generator.generate(nameProp);
assertThat((String) value).hasSizeLessThanOrEqualTo(127);
}
}
// --- direct protected-method tests (no database needed) ---
@Test
void randomLong_isPositiveAndInRange() {
long v = generator.randomLong();
assertThat(v).isBetween(1L, 100_000L);
}
@Test
void randomInt_isPositiveAndInRange() {
int v = generator.randomInt();
assertThat(v).isBetween(1, 999);
}
@Test
void randomShort_isPositiveAndInRange() {
short v = generator.randomShort();
assertThat((int) v).isBetween(1, 99);
}
@Test
void randomBoolean_returnsTrue() {
assertThat(generator.randomBoolean()).isTrue();
}
@Test
void randomUUID_returnsValidUUID() {
UUID v = generator.randomUUID();
assertThat(v).isNotNull();
assertThat(v.toString()).hasSize(36);
}
@Test
void randomInstant_returnsInstant() {
assertThat(generator.randomInstant()).isInstanceOf(Instant.class);
}
@Test
void randomOffsetDateTime_returnsOffsetDateTime() {
assertThat(generator.randomOffsetDateTime()).isInstanceOf(OffsetDateTime.class);
}
@Test
void randomZonedDateTime_returnsZonedDateTime() {
assertThat(generator.randomZonedDateTime()).isInstanceOf(ZonedDateTime.class);
}
@Test
void randomLocalDate_returnsLocalDate() {
assertThat(generator.randomLocalDate()).isInstanceOf(LocalDate.class);
}
@Test
void randomLocalDateTime_returnsLocalDateTime() {
assertThat(generator.randomLocalDateTime()).isInstanceOf(LocalDateTime.class);
}
@Test
void randomDouble_isPositiveAndInRange() {
double v = generator.randomDouble();
assertThat(v).isBetween(1.0, 100.0);
}
@Test
void randomFloat_isPositiveAndInRange() {
float v = generator.randomFloat();
assertThat((double) v).isBetween(1.0, 100.0);
}
@Test
void randomBigDecimal_defaultPrecisionAndScale_scaleIsTwo() {
BigDecimal v = generator.randomBigDecimal(0, -1);
assertThat(v.scale()).isEqualTo(2);
assertThat(v).isGreaterThan(BigDecimal.ZERO);
}
@Test
void randomBigDecimal_explicitPrecisionAndScale_fitsWithinBounds() {
// DECIMAL(8,3) → max integer part 99999, scale 3
BigDecimal v = generator.randomBigDecimal(8, 3);
assertThat(v.scale()).isEqualTo(3);
assertThat(v.precision()).isLessThanOrEqualTo(8);
assertThat(v).isGreaterThan(BigDecimal.ZERO);
}
@Test
void randomString_emailPropName_containsDomainSuffix() {
String v = generator.randomString("emailAddress", 0);
assertThat(v).endsWith("@domain.com");
}
@Test
void randomString_regularPropName_returnsEightChars() {
String v = generator.randomString("name", 0);
assertThat(v).hasSize(8);
}
@Test
void randomString_withMaxLength_isTruncated() {
String v = generator.randomString("description", 5);
assertThat(v).hasSize(5);
}
@Test
void subclassCanOverrideRandomLong() {
RandomValueGenerator fixed = new RandomValueGenerator() {
@Override
protected long randomLong() { return 42L; }
};
assertThat(fixed.generate(Long.class)).isEqualTo(42L);
}
}
@@ -0,0 +1,86 @@
package io.ebean.test;
import io.ebean.DB;
import io.ebean.xtest.BaseTestCase;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.EBasic;
import org.tests.model.basic.UUOne;
import org.tests.model.basic.UUTwo;
import static org.assertj.core.api.Assertions.assertThat;
class TestEntityBuilderTest extends BaseTestCase {
private final TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault()).build();
@Test
void build_simpleEntity_populatesScalarFields() {
EBasic bean = builder.build(EBasic.class);
assertThat(bean).isNotNull();
assertThat(bean.getId()).isNull(); // @Id — not populated
assertThat(bean.getName()).isNotNull(); // String scalar — populated
assertThat(bean.getName()).hasSizeLessThanOrEqualTo(127); // @Size(max=127) respected
assertThat(bean.getDescription()).isNotNull();
assertThat(bean.getStatus()).isNotNull(); // Enum — populated with first constant
assertThat(bean.getStatus()).isEqualTo(EBasic.Status.NEW);
}
@Test
void build_entityWithCascadeManyToOne_populatesRelationship() {
UUTwo bean = builder.build(UUTwo.class);
assertThat(bean).isNotNull();
assertThat(bean.getId()).isNull(); // @Id — not populated
assertThat(bean.getVersion()).isZero(); // @Version — not populated
assertThat(bean.getName()).isNotNull();
assertThat(bean.getMaster()).isNotNull(); // @ManyToOne(cascade=PERSIST) — recursively built
assertThat(bean.getMaster().getName()).isNotNull();
assertThat(bean.getMaster().getId()).isNull(); // @Id on UUOne — not populated
assertThat(bean.getMaster().getVersion()).isZero(); // @Version on UUOne — not populated
}
@Test
void build_calledTwice_producesDistinctInstances() {
EBasic first = builder.build(EBasic.class);
EBasic second = builder.build(EBasic.class);
assertThat(first).isNotSameAs(second);
// String values should be different random values
assertThat(first.getName()).isNotEqualTo(second.getName());
}
@Test
void save_insertsEntityAndReturnsWithId() {
EBasic saved = builder.save(EBasic.class);
assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull(); // @Id assigned after insert
assertThat(saved.getName()).isNotNull();
// verify it's actually in the database
EBasic found = DB.find(EBasic.class, saved.getId());
assertThat(found).isNotNull();
assertThat(found.getName()).isEqualTo(saved.getName());
}
@Test
void save_entityWithCascadeManyToOne_savesCascades() {
UUTwo saved = builder.save(UUTwo.class);
assertThat(saved).isNotNull();
assertThat(saved.getId()).isNotNull();
assertThat(saved.getMaster()).isNotNull();
assertThat(saved.getMaster().getId()).isNotNull(); // parent also saved via cascade
UUOne foundMaster = DB.find(UUOne.class, saved.getMaster().getId());
assertThat(foundMaster).isNotNull();
}
@Test
void build_unknownClass_throwsIllegalArgumentException() {
org.assertj.core.api.Assertions.assertThatThrownBy(() -> builder.build(String.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("No BeanDescriptor found");
}
}
@@ -155,6 +155,34 @@ public class DtoQueryFromOrmTest extends BaseTestCase {
}
}
@Test
public void asDto_usingMaster_true() {
asDtoUsingMaster(true);
}
@Test
public void asDto_usingMaster_false() {
asDtoUsingMaster(false);
}
private void asDtoUsingMaster(boolean useMaster) {
ResetBasicData.reset();
LoggedSql.start();
DtoQuery<ContactDto> query = DB.find(Contact.class)
.select("id, email")
.where().isNotNull("email")
.asDto(ContactDto.class)
.usingMaster(useMaster);
List<ContactDto> dtos = query.findList();
assertThat(dtos).isNotEmpty();
for (ContactDto dto : dtos) {
assertThat(dto.getEmail()).isNotNull();
}
}
@Test
public void asDto_withExplicitId() {
@@ -2,16 +2,17 @@ package io.ebean.xtest.config.dbplatform;
import io.ebean.config.dbplatform.DbPlatformType;
import io.ebean.config.dbplatform.DbPlatformTypeMapping;
import io.ebean.config.dbplatform.DbType;
import org.junit.jupiter.api.Test;
import java.sql.Types;
import static org.assertj.core.api.Assertions.assertThat;
public class DbPlatformTypeMappingTest {
class DbPlatformTypeMappingTest {
@Test
public void logicalBoolean_renderType_expect_noLength() {
void logicalBoolean_renderType_expect_noLength() {
DbPlatformTypeMapping logicalMapping = DbPlatformTypeMapping.logicalTypes();
@@ -19,4 +20,11 @@ public class DbPlatformTypeMappingTest {
String colDefinition = type.renderType(1, 1, false);
assertThat(colDefinition).isEqualTo("boolean");
}
@Test
void timestamp_with_255_expectNoPrecision() {
DbPlatformType timestampType = new DbPlatformTypeMapping().get(DbType.TIMESTAMP);
String colDefinition = timestampType.renderType(255, 0, true);
assertThat(colDefinition).isEqualTo("timestamp");
}
}
@@ -30,13 +30,13 @@ public class BeanFindControllerTest extends BaseTestCase {
var config = new DatabaseConfig();
config.setName("h2otherfind");
config.setRegister(false);
config.setDefaultServer(false);
config.loadFromProperties();
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
config.setRegister(false);
config.setDefaultServer(false);
config.add(new ModUuidGenerator());
config.addClass(EBasic.class);
config.addClass(ECustomId.class);
@@ -173,13 +173,13 @@ public class BeanFindControllerTest extends BaseTestCase {
DatabaseConfig config = new DatabaseConfig();
config.setName("h2otherfind");
config.setRegister(false);
config.setDefaultServer(false);
config.loadFromProperties();
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
config.setRegister(false);
config.setDefaultServer(false);
config.add(new ModUuidGenerator());
config.addClass(FindControllerMain.class);
config.addClass(SoftRefA.class);
@@ -2,14 +2,18 @@ package io.ebean.xtest.event;
import io.ebean.Database;
import io.ebean.DatabaseBuilder;
import io.ebean.DatabaseFactory;
import io.ebean.Transaction;
import io.ebean.DatabaseBuilder;
import io.ebean.config.DatabaseConfig;
import io.ebean.event.BeanDeleteIdRequest;
import io.ebean.event.BeanPersistAdapter;
import io.ebean.event.BeanPersistController;
import io.ebean.event.BeanPersistRequest;
import io.ebean.test.LoggedSql;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tests.model.basic.EBasicVer;
import org.tests.model.basic.UTDetail;
import org.tests.model.basic.UTMaster;
@@ -22,13 +26,15 @@ import static org.assertj.core.api.Assertions.assertThat;
public class BeanPersistControllerTest {
private static final Logger log = LoggerFactory.getLogger(BeanPersistControllerTest.class);
private final PersistAdapter continuePersistingAdapter = new PersistAdapter(true);
private final PersistAdapter stopPersistingAdapter = new PersistAdapter(false);
@Test
public void issued1() {
Database db = getDatabase(continuePersistingAdapter);
Database db = createDatabase(continuePersistingAdapter);
UTMaster bean0 = new UTMaster("m0");
bean0.setJournal(new UTMaster.Journal());
@@ -47,11 +53,12 @@ public class BeanPersistControllerTest {
assertThat(journal.getEntries()).hasSize(2);
db.shutdown();
log.info("done issued1");
}
@Test
public void issue_1341() {
Database db = getDatabase(continuePersistingAdapter);
Database db = createDatabase(continuePersistingAdapter);
UTMaster bean0 = new UTMaster("one0");
UTDetail detail0 = new UTDetail("detail0", 12, 23D);
@@ -77,12 +84,13 @@ public class BeanPersistControllerTest {
}
db.shutdown();
log.info("done issue_1341");
}
@Test
public void testInsertUpdateDelete_given_continuePersistingAdapter() {
Database db = getDatabase(continuePersistingAdapter);
Database db = createDatabase(continuePersistingAdapter);
EBasicVer bean = new EBasicVer("testController");
@@ -102,12 +110,13 @@ public class BeanPersistControllerTest {
assertThat(continuePersistingAdapter.methodsCalled).containsExactly("preDelete", "postDelete");
db.shutdown();
log.info("done testInsertUpdateDelete_given_continuePersistingAdapter");
}
@Test
public void testInsertUpdateDelete_given_stopPersistingAdapter() {
Database db = getDatabase(stopPersistingAdapter);
Database db = createDatabase(stopPersistingAdapter);
EBasicVer bean = new EBasicVer("testController");
@@ -132,24 +141,25 @@ public class BeanPersistControllerTest {
assertThat(stopPersistingAdapter.methodsCalled).containsExactly("preDeleteById");
stopPersistingAdapter.methodsCalled.clear();
db.deleteAll(EBasicVer.class, Arrays.asList(22,23,24));
db.deleteAll(EBasicVer.class, Arrays.asList(22, 23, 24));
assertThat(stopPersistingAdapter.methodsCalled).hasSize(3);
assertThat(stopPersistingAdapter.methodsCalled).containsExactly("preDeleteById", "preDeleteById", "preDeleteById");
stopPersistingAdapter.methodsCalled.clear();
db.shutdown();
log.info("done testInsertUpdateDelete_given_stopPersistingAdapter");
}
private Database getDatabase(PersistAdapter persistAdapter) {
private Database createDatabase(PersistAdapter persistAdapter) {
DatabaseBuilder config = new DatabaseConfig();
config.setName("h2ebasicver");
config.setRegister(false);
config.setDefaultServer(false);
config.loadFromProperties();
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
config.setRegister(false);
config.setDefaultServer(false);
config.addClass(EBasicVer.class);
config.addClass(UTMaster.class);
config.addClass(UTDetail.class);
@@ -194,12 +204,12 @@ public class BeanPersistControllerTest {
Object bean = request.bean();
if (bean instanceof UTDetail) {
UTDetail detail = (UTDetail)bean;
UTDetail detail = (UTDetail) bean;
// invoke lazy loading ... which invoke the flush of the jdbc batch
detail.setQty(42);
}
if (bean instanceof UTMaster) {
UTMaster master = (UTMaster)bean;
UTMaster master = (UTMaster) bean;
UTMaster.Journal journal = master.getJournal();
if (journal == null) {
journal = new UTMaster.Journal();
@@ -52,13 +52,13 @@ public class BeanPostLoadTest extends BaseTestCase {
DatabaseBuilder config = new DatabaseConfig();
config.setName("h2ebasicver");
config.setRegister(false);
config.setDefaultServer(false);
config.loadFromProperties();
config.setDdlGenerate(true);
config.setDdlRun(true);
config.setDdlExtra(false);
config.setRegister(false);
config.setDefaultServer(false);
config.addClass(EBasicVer.class);
config.add(postLoad);
@@ -7,23 +7,20 @@ import org.tests.model.basic.Order;
import jakarta.persistence.PersistenceException;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
public class TestErrorBindLog extends BaseTestCase {
class TestErrorBindLog extends BaseTestCase {
@Test
public void test() {
void test() {
try {
DB.find(Order.class).where().gt("id", "JUNK").findList();
} catch (PersistenceException e) {
String msg = e.getMessage();
if (isHana()) {
assertTrue(msg.contains("Error with property[1] dt[12]data[JUNK]"));
}
else {
assertTrue(msg.contains("Bind values:"));
if (isHana() || isDb2()) {
assertThat(msg).contains("Error with property");
} else {
assertThat(msg).contains("Bind values:");
}
}
}
@@ -0,0 +1,55 @@
package org.tests.basic;
import io.ebean.DB;
import io.ebean.Query;
import io.ebean.xtest.BaseTestCase;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Country;
import org.tests.model.basic.EBasic;
import org.tests.model.basic.OBeanChild;
import org.tests.model.basic.OCachedBean;
import org.tests.model.basic.ResetBasicData;
public class TestManyOnChildOfExtraJoin extends BaseTestCase {
@Test
public void test() {
ResetBasicData.reset();
OCachedBean bean = new OCachedBean();
bean.setName("m2m-with-sq");
bean.getCountries().add(DB.reference(Country.class, "NZ"));
bean.getCountries().add(DB.reference(Country.class, "AU"));
DB.save(bean);
OBeanChild child = new OBeanChild();
child.setCachedBean(bean);
DB.save(child);
EBasic b1 = new EBasic();
b1.setName("Australia");
b1.setStatus(EBasic.Status.ACTIVE);
DB.save(b1);
EBasic b2 = new EBasic();
b2.setName("New Zealand");
b2.setStatus(EBasic.Status.ACTIVE);
DB.save(b2);
Query<OBeanChild> query = DB.find(OBeanChild.class).where()
.eq("cachedBean.name", "m2m-with-sq")
.exists(DB.find(EBasic.class)
.alias("sq1")
.where()
.raw("cachedBean.countries.name = sq1.name")
.eq("status", EBasic.Status.ACTIVE)
.query())
.query();
SoftAssertions softly = new SoftAssertions();
softly.assertThat(query.findList()).hasSize(1);
softly.assertThat(query.getGeneratedSql()).startsWith("select distinct");
softly.assertAll();
}
}
@@ -68,11 +68,15 @@ public class TestQueryUsingConnection extends BaseTestCase {
final int otherCount = DB.find(Country.class).findCount();
final int masterCount = DB.find(Country.class).usingMaster().findCount();
final int masterCount2 = DB.find(Country.class).usingMaster(true).findCount();
final int masterCount3 = DB.find(Country.class).usingMaster(false).findCount();
transaction.rollback();
assertThat(count).isEqualTo(otherCount + 1);
assertThat(otherCount).isEqualTo(masterCount);
assertThat(otherCount).isEqualTo(masterCount2);
assertThat(otherCount).isEqualTo(masterCount3);
}
}
@@ -124,7 +124,7 @@ public class TestBeanCache extends BaseTestCase {
ServerCacheStatistics statistics = beanCache.statistics(true);
assertThat(statistics.getHitCount()).isEqualTo(0);
assertThat(statistics.getMissCount()).isEqualTo(0);
assertThat(statistics.getPutCount()).isEqualTo(2);
assertThat(statistics.getPutCount()).isEqualTo(0);
}
@Test
@@ -3,6 +3,7 @@ package org.tests.cache.personinfo;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
@@ -16,6 +17,7 @@ public class PersonOther {
@Size(max=128)
private String id;
@Column(length = 60)
private String email;
@WhenCreated
@@ -445,4 +445,21 @@ public class TestQueryFilterMany extends BaseTestCase {
assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id and (t1.first_name is not null and lower(t1.email) like ? escape'|') order by t0.id; --bind(rob%)");
}
}
@Test
void testFilterManyWithNestedPathExpression() {
ResetBasicData.reset();
LoggedSql.start();
DB.find(Customer.class)
.select("id, name")
.fetch("contacts", "firstName, email")
.filterMany("contacts").eq("group.name", "DoesNotExist").isNotNull("cretime")
.findList();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertSql(sql.get(0)).contains(" from o_customer t0 left join contact t1 on t1.customer_id = t0.id left join contact_group t2 on t2.id = t1.group_id and t2.name = ? and t1.cretime is not null order by t0.id");
}
}
@@ -43,4 +43,19 @@ public class TestQueryFilterManySimple extends BaseTestCase {
assertThat(sql.get(1)).contains("from contact t0 where (t0.customer_id) in (?) and t0.first_name is not null;");
}
}
@Test
void testNestedFilterMany() {
ResetBasicData.reset();
LoggedSql.start();
DB.find(Customer.class)
.fetch("orders")
.filterMany("orders").eq("customer.status", Customer.Status.NEW)
.findList();
List<String> sql = LoggedSql.stop();
assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("from o_customer t0 left join o_order t1 on t1.kcustomer_id = t0.id and t1.order_date is not null left join o_customer t2 on t2.id = t1.kcustomer_id and t2.status = ? order by t0.id");
}
}
@@ -4,6 +4,7 @@ import io.ebean.DB;
import io.ebean.Query;
import io.ebean.test.LoggedSql;
import io.ebean.xtest.BaseTestCase;
import io.ebean.xtest.ForPlatform;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.tests.model.basic.Order;
@@ -16,6 +17,8 @@ import org.tests.model.family.ParentPerson;
import java.sql.Date;
import java.util.List;
import static io.ebean.annotation.Platform.H2;
import static io.ebean.annotation.Platform.POSTGRES;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -82,7 +85,7 @@ public class TestQueryJoinOnFormula extends BaseTestCase {
.orderBy().asc("order.totalAmount");
shipQuery.findList();
assertSql(shipQuery.getGeneratedSql()).isEqualTo("select t0.id "
assertSql(shipQuery.getGeneratedSql()).isEqualTo("select distinct t0.id, z_bt1.total_amount "
+ "from or_order_ship t0 "
+ "left join o_order t1 on t1.id = t0.order_id "
+ "left join (select order_id, count(*) as total_items, sum(order_qty*unit_price) as total_amount from o_order_detail group by order_id) z_bt1 on z_bt1.order_id = t1.id "
@@ -99,7 +102,7 @@ public class TestQueryJoinOnFormula extends BaseTestCase {
.where().isNotNull("order.totalAmount").query();
shipQuery.findList();
assertSql(shipQuery.getGeneratedSql()).isEqualTo("select t0.id "
assertSql(shipQuery.getGeneratedSql()).isEqualTo("select distinct t0.id "
+ "from or_order_ship t0 "
+ "left join o_order t1 on t1.id = t0.order_id "
+ "left join (select order_id, count(*) as total_items, sum(order_qty*unit_price) as total_amount from o_order_detail group by order_id) z_bt1 on z_bt1.order_id = t1.id "
@@ -313,7 +316,7 @@ public class TestQueryJoinOnFormula extends BaseTestCase {
List<String> loggedSql = LoggedSql.stop();
assertEquals(1, loggedSql.size());
assertThat(loggedSql.get(0))
.contains("select t0.identifier from child_person t0")
.contains("select distinct t0.identifier from child_person t0")
.contains("left join (select i2.parent_identifier")
.contains("where coalesce(f2.child_age, 0) = ?");
}
@@ -329,8 +332,54 @@ public class TestQueryJoinOnFormula extends BaseTestCase {
List<String> loggedSql = LoggedSql.stop();
assertEquals(1, loggedSql.size());
assertThat(loggedSql.get(0)).contains("select count(*) from child_person t0 left join parent_person t1 on t1.identifier = t0.parent_identifier");
assertThat(loggedSql.get(0)).contains("where coalesce(f2.child_age, 0) = ?");
assertThat(loggedSql.get(0)).contains("select count(*) from ( select distinct t0.identifier from child_person t0 left join parent_person t1 on t1.identifier = t0.parent_identifier");
assertThat(loggedSql.get(0)).contains("where coalesce(f2.child_age, 0) = ?)");
}
/**
* Test for https://github.com/ebean-orm/ebean/issues/3686
* findCount with Formula join + subquery containing ORDER BY
* should produce valid SQL with properly balanced parentheses.
*
* Before the fix, lastIndexOf(" order by ") matched the ORDER BY inside the
* IN subquery, stripping its closing parenthesis and producing invalid SQL.
*/
@ForPlatform({H2, POSTGRES})
@Test
public void test_findCount_formulaJoin_subqueryWithOrderBy_issue3686() {
LoggedSql.start();
// Subquery with orderBy — the order by inside the subquery triggers the bug.
Query<Order> subQuery = DB.find(Order.class)
.select("id")
.orderBy("id");
// Outer query: formula join (totalItems) via where clause + findCount
int count = DB.find(Order.class)
.where()
.in("id", subQuery)
.eq("totalItems", 3)
.findCount();
assertThat(count).isEqualTo(2);
List<String> sql = LoggedSql.stop();
assertEquals(1, sql.size());
String countSql = sql.get(0);
// The count query must wrap with select count(*) from ( ... )
assertThat(countSql).contains("select count(*) from (");
// The subquery's ORDER BY and closing ) must be preserved
assertThat(countSql).contains("order by t0.id)");
// Parentheses must be balanced in the generated SQL
int open = 0, close = 0;
for (char c : countSql.toCharArray()) {
if (c == '(') open++;
if (c == ')') close++;
}
assertThat(open).as("parentheses must be balanced in: " + countSql).isEqualTo(close);
}
@Test
@@ -75,16 +75,18 @@ public class TestOrderByWithDistinct extends BaseTestCase {
assertSql(query).startsWith("select distinct on (t0.id, t2.name, t1.id) t0.id, t0.parent_id, t0.ref_id, t1.id, t1.parent_id, t1.ref_id, t2.name "
+ "from e_basic_tree t0 "
+ "left join e_basic_tree t1 on t1.parent_id = t0.id "
+ "left join e_basic t2 on t2.id = t1.ref_id "
+ "join e_basic_tree u1 on u1.parent_id = t0.id "
+ "join e_basic u2 on u2.id = u1.ref_id left "
+ "join e_basic t2 on t2.id = t1.ref_id where u2.status = ? order by t0.id, t2.name");
+ "join e_basic u2 on u2.id = u1.ref_id "
+ "where u2.status = ? order by t0.id, t2.name");
} else {
assertSql(query).startsWith("select distinct t0.id, t0.parent_id, t0.ref_id, t1.id, t1.parent_id, t1.ref_id, t2.name "
+ "from e_basic_tree t0 "
+ "left join e_basic_tree t1 on t1.parent_id = t0.id "
+ "left join e_basic t2 on t2.id = t1.ref_id "
+ "join e_basic_tree u1 on u1.parent_id = t0.id "
+ "join e_basic u2 on u2.id = u1.ref_id left "
+ "join e_basic t2 on t2.id = t1.ref_id where u2.status = ? order by t0.id, t2.name");
+ "join e_basic u2 on u2.id = u1.ref_id "
+ "where u2.status = ? order by t0.id, t2.name");
}
}
@@ -280,6 +280,28 @@ class SqlQueryTests extends BaseTestCase {
assertThat(custDtos).isNotEmpty();
}
@Test
void queryUsingMaster_true() {
queryUsingMasterAsParameter(true);
}
@Test
void queryUsingMaster_false() {
queryUsingMasterAsParameter(false);
}
void queryUsingMasterAsParameter(boolean useMaster) {
ResetBasicData.reset();
String sql = "select id, name, status from o_customer where name is not null";
List<CustDto> custDtos = DB.sqlQuery(sql)
.usingMaster(useMaster)
.mapTo(CUST_MAPPER)
.findList();
assertThat(custDtos).isNotEmpty();
}
@Test
void queryUsingConnection() throws SQLException {
ResetBasicData.reset();
+66
View File
@@ -0,0 +1,66 @@
# GraalVM Native Image
Ebean ORM supports GraalVM native image compilation. No additional dependencies or
native-image configuration files are required beyond what Ebean already provides.
## How it works
Ebean uses **bytecode enhancement at compile time** (via the ebean-maven-plugin or
ebean-gradle-plugin). The enhanced entity classes are compiled directly into the native
image. Because the enhancement happens ahead-of-time, Ebean does not rely on runtime
reflection for normal ORM operations — queries, inserts, updates, deletes, and
associations all work without additional configuration.
Entity classes do require reflection registration for native image, but this is handled
**automatically by the querybean-generator** annotation processor at compile time.
Projects using the querybean-generator (which is the standard and expected setup) have
nothing extra to do for `@Entity` classes.
## Setup
Ensure the ebean enhancement plugin runs during your build, as it normally would for
a standard JVM application. No extra steps are required for native image.
**Maven:**
```xml
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
```
**Gradle:**
```kotlin
plugins {
id("io.ebean") version "${ebeanVersion}"
}
```
## DtoQuery and reflection
[`DtoQuery`](https://ebean.io/docs/query/dto) maps SQL results onto plain classes using
their public constructors and setter methods. This mapping uses reflection at runtime, so
DTO classes must be explicitly registered for reflection in your native-image configuration.
Create `src/main/resources/META-INF/native-image/<group-id>/<artifact-id>/reflect-config.json`
and add an entry for each DTO class:
```json
[
{
"name": "com.example.CustomerDto",
"allDeclaredConstructors": true,
"allPublicMethods": true
},
{
"name": "com.example.OrderSummaryDto",
"allDeclaredConstructors": true,
"allPublicMethods": true
}
]
```
> **Note:** `@Entity` classes are registered automatically by the querybean-generator —
> no manual entries are needed for them.
+2 -2
View File
@@ -15,8 +15,8 @@ mvn -T 4 clean package
mvn -T 4 deploy -pl '!composites,!platforms' -Pcentral -DskipTests
## git commit, git tag, git push --tags
git commit -am 'Version 16.2.0'
git tag 16.2.0
git commit -am 'Version 16.5.0'
git tag 16.5.0
git push --tags
## convert to javax
+6 -6
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
</parent>
<name>kotlin querybean generator</name>
@@ -21,7 +21,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -35,7 +35,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -56,14 +56,14 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-ddl-generator</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
<scope>test</scope>
</dependency>
@@ -83,7 +83,7 @@
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.2.10</version>
<version>2.3.10</version>
<executions>
<execution>
<id>test-compile</id>
+14 -14
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,67 +16,67 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hsqldb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlanywhere</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>
+2 -2
View File
@@ -4,7 +4,7 @@
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>16.2.0</version>
<version>16.5.0</version>
<relativePath>../..</relativePath>
</parent>
@@ -16,7 +16,7 @@
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>16.2.0</version>
<version>16.5.0</version>
</dependency>
</dependencies>

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