mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb0305cf3 | ||
|
|
d03d36bba4 | ||
|
|
88408edd24 | ||
|
|
0aad35b840 | ||
|
|
e7979b285d | ||
|
|
91920b00dc | ||
|
|
bdfe016d2d | ||
|
|
b2670f9cc0 | ||
|
|
ebc90e0e82 | ||
|
|
c4230c780f | ||
|
|
794f933310 | ||
|
|
c8a7a263a9 | ||
|
|
aeef6d0ea2 | ||
|
|
a99aef8b00 | ||
|
|
6f17cc6327 | ||
|
|
903947b3cb | ||
|
|
797f75f7b7 | ||
|
|
af443ef2f2 | ||
|
|
ad4f027835 | ||
|
|
367eba8685 | ||
|
|
be542635a5 | ||
|
|
6e9d0a91e0 | ||
|
|
a99ef3ebf2 | ||
|
|
19afa845d1 | ||
|
|
239900cd3b | ||
|
|
73ac39971c | ||
|
|
3a876252ec | ||
|
|
231b52ba88 | ||
|
|
808019cf3d | ||
|
|
364520455f | ||
|
|
e92621a489 | ||
|
|
ccd1b7b8ec | ||
|
|
d144273307 | ||
|
|
41c5ebdcd7 | ||
|
|
9e711efecb | ||
|
|
ef7fd76f14 | ||
|
|
b7e3ddbedd | ||
|
|
69c932816a | ||
|
|
a1facf527b | ||
|
|
ec42ebf66d | ||
|
|
5dede70801 | ||
|
|
b2f02ecc1f | ||
|
|
15fa7cd1c2 | ||
|
|
8dced38cd5 | ||
|
|
7addba7c93 | ||
|
|
00b45c1642 |
@@ -3,6 +3,7 @@
|
||||
[](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
|
||||
[](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
|
||||
[](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
|
||||
[](https://www.graalvm.org/)
|
||||
|
||||
##### Build with database platforms
|
||||
[](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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 50–75% 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 |
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
+26
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+48
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+14
-2
@@ -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);
|
||||
|
||||
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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,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>
|
||||
|
||||
|
||||
@@ -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,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,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,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>
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
|
||||
+10
-2
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user