mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b666013e |
@@ -2,7 +2,6 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: master
|
||||
@@ -17,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -40,7 +39,5 @@ jobs:
|
||||
# - name: Maven single test
|
||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||
- name: Build with Maven
|
||||
run: mvn -T 1C clean install -Pdefault
|
||||
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
|
||||
run: cd tests/test-java16 && mvn test
|
||||
run: mvn -T 8 clean test
|
||||
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: db2
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: H2Database
|
||||
run: mvn -T 1C clean package
|
||||
run: mvn -T 8 clean package
|
||||
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
website: jdk.java.net
|
||||
release: ${{ matrix.java_version }}
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -37,5 +37,5 @@ jobs:
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
- name: Build with Maven
|
||||
run: mvn test -Pea
|
||||
run: mvn -T 8 test
|
||||
|
||||
|
||||
@@ -16,23 +16,23 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mariadb 10.11
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
- name: mariadb 10.6
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||
|
||||
@@ -13,18 +13,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11, 17, 21]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: mysql
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'zulu'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: oracle
|
||||
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties
|
||||
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: postgres
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties
|
||||
|
||||
@@ -13,18 +13,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -16,23 +16,23 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: sqlserver 2022
|
||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
||||
- name: sqlserver 2017
|
||||
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Valhalla EA
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '39 2 * * 3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [valhalla]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: oracle-actions/setup-java@v1
|
||||
with:
|
||||
website: jdk.java.net
|
||||
release: ${{ matrix.java_version }}
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
path:
|
||||
~/.m2
|
||||
key: build-${{ env.cache-name }}
|
||||
- name: Maven version
|
||||
run: mvn --version
|
||||
# - name: Prepare
|
||||
# run: ./jakarta-to-valhalla.sh
|
||||
- name: Build with Maven
|
||||
run: mvn package
|
||||
@@ -4,7 +4,7 @@ name: Yugabyte
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '10 3 * * 3'
|
||||
- cron: '10 3 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -16,18 +16,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
java_version: [21]
|
||||
java_version: [11]
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: ${{ matrix.java_version }}
|
||||
distribution: 'adopt'
|
||||
- name: Maven cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: maven-cache
|
||||
with:
|
||||
|
||||
@@ -13,9 +13,6 @@ ebean-profiling*.xml
|
||||
profiling/
|
||||
.DS_Store
|
||||
|
||||
# Local Redis integration test credentials
|
||||
ebean-redis/src/test/resources/redis-local.yml
|
||||
|
||||
# Intellij project files
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[](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)
|
||||
@@ -57,7 +56,12 @@ Work at the highest level of abstraction and drop down levels as needed.
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.foconis.de/" target="_blank">
|
||||
<img width="222px" src="https://group.foconis.com/download/ci/logo/png-72dpi/logo-quer/foconis-analytics-quer.png">
|
||||
<img width="222px" src="https://www.foconis.de/templates/yootheme/cache/foconis_logo_322-709da1de.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.payintech.com/" target="_blank">
|
||||
<img width="222px" src="https://ebean.io/images/sponsor_PayinTech-logo-noir.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -81,18 +85,6 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
|
||||
## Documentation
|
||||
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
|
||||
|
||||
## Guides
|
||||
Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
|
||||
|
||||
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>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-db2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-h2</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-hana</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mariadb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-mysql</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-net-postgis</name>
|
||||
<description>ebean-net-postgis composite</description>
|
||||
<artifactId>ebean-net-postgis</artifactId>
|
||||
|
||||
<properties>
|
||||
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-datasource</artifactId>
|
||||
<version>${ebean-datasource.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean-migration.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Technically optional but most expected to use query beans -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-net-postgis-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgres.jdbc.version}</version>
|
||||
<exclusions>
|
||||
<!-- exclude unnecessary checker framework -->
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.postgis</groupId>
|
||||
<artifactId>postgis-jdbc</artifactId>
|
||||
<version>${postgis.jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.ebean.postgis.assembly;
|
||||
|
||||
/**
|
||||
* Nothing interesting here - required placeholder for javadoc.
|
||||
*/
|
||||
public class Assembly {
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module io.ebean.postgis {
|
||||
|
||||
requires transitive io.ebean.api;
|
||||
requires transitive io.ebean.core;
|
||||
requires transitive io.ebean.datasource;
|
||||
requires transitive io.ebean.querybean;
|
||||
requires transitive io.ebean.platform.postgres;
|
||||
// requires transitive io.ebean.postgis.types;
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-nuodb</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-oracle</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
<name>ebean-pgvector</name>
|
||||
<description>ebean-pgvector composite</description>
|
||||
<artifactId>ebean-pgvector</artifactId>
|
||||
|
||||
<properties>
|
||||
<pgvector.version>0.1.6</pgvector.version>
|
||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-datasource</artifactId>
|
||||
<version>${ebean-datasource.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-migration</artifactId>
|
||||
<version>${ebean-migration.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Technically optional but most expected to use query beans -->
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-pgvector-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgres.jdbc.version}</version>
|
||||
<exclusions>
|
||||
<!-- exclude unnecessary checker framework -->
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.pgvector</groupId>
|
||||
<artifactId>pgvector</artifactId>
|
||||
<version>${pgvector.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.ebean.pgvector.assembly;
|
||||
|
||||
/**
|
||||
* Nothing interesting here - required placeholder for javadoc.
|
||||
*/
|
||||
public class Assembly {
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
module io.ebean.pgvector {
|
||||
|
||||
requires transitive io.ebean.api;
|
||||
requires transitive io.ebean.core;
|
||||
requires transitive io.ebean.datasource;
|
||||
requires transitive io.ebean.querybean;
|
||||
requires transitive io.ebean.platform.postgres;
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -47,19 +47,19 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgis-types</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlite</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -42,13 +42,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-postgres</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
<relativePath>../..</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-api</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-core</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -60,13 +60,13 @@
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-querybean</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-platform-all</artifactId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
+1
-3
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>composites</artifactId>
|
||||
@@ -24,8 +24,6 @@
|
||||
<module>ebean-oracle</module>
|
||||
<module>ebean-postgres</module>
|
||||
<module>ebean-postgis</module>
|
||||
<module>ebean-net-postgis</module>
|
||||
<module>ebean-pgvector</module>
|
||||
<!-- <module>sqlanywhere</module>-->
|
||||
<module>ebean-sqlite</module>
|
||||
<module>ebean-sqlserver</module>
|
||||
|
||||
-250
@@ -1,250 +0,0 @@
|
||||
# Ebean ORM Library Definition
|
||||
|
||||
Ebean is an ORM library for Java and Kotlin focused on relational data access, type-safe query construction, and production-friendly SQL behavior.
|
||||
|
||||
## Identity
|
||||
|
||||
- **Name**: Ebean ORM
|
||||
- **Package**: `io.ebean`
|
||||
- **Primary Maven Group**: `io.ebean`
|
||||
- **Category**: ORM / Data Access
|
||||
- **Repository**: https://github.com/ebean-orm/ebean
|
||||
- **Issues**: https://github.com/ebean-orm/ebean/issues
|
||||
- **Discussions**: https://github.com/ebean-orm/ebean/discussions
|
||||
- **Website**: https://ebean.io/
|
||||
- **Documentation**: https://ebean.io/docs/
|
||||
- **License**: Apache 2.0
|
||||
|
||||
## Version & Requirements
|
||||
|
||||
- **Repository Version (this checkout)**: `16.5.0` (from repository `pom.xml`)
|
||||
- **Minimum Java Version**: 11+
|
||||
- **Languages**: Java, Kotlin
|
||||
- **Build Tooling in this docs set**: Maven-focused examples
|
||||
|
||||
## Core Artifacts
|
||||
|
||||
| Artifact | Purpose |
|
||||
|------|------|
|
||||
| `io.ebean:ebean` | Core ORM runtime and API |
|
||||
| `io.ebean:ebean-postgres` | PostgreSQL platform bundle used in setup guides |
|
||||
| `io.ebean:ebean-test` | Test support, including Docker-backed database testing |
|
||||
| `io.ebean:querybean-generator` | Generates `Q*` type-safe query beans |
|
||||
| `io.ebean:ebean-maven-plugin` | Bytecode enhancement for entities at build time |
|
||||
| `io.ebean:ebean-migration` | Runtime migration runner (often transitive via platform artifact) |
|
||||
|
||||
## Core APIs & Annotations
|
||||
|
||||
### Database and transaction APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `DB.getDefault()` | Access default `Database` | `Database db = DB.getDefault();` |
|
||||
| `DB.byName("...")` | Access named `Database` | `Database reporting = DB.byName("reporting");` |
|
||||
| `database.find(...)` | Query entities | `Customer c = database.find(Customer.class, id);` |
|
||||
| `database.insert/save/update/delete` | Persist entity changes | `database.save(customer);` |
|
||||
| `database.beginTransaction()` | Manual transaction boundary | `try (Transaction txn = database.beginTransaction()) { ... }` |
|
||||
| `Database.builder()` | Programmatic `Database` setup | `Database.builder().loadFromProperties().build();` |
|
||||
|
||||
### Query APIs
|
||||
|
||||
| API | Purpose | Example |
|
||||
|------|------|------|
|
||||
| `Q*` query beans | Type-safe query construction | `new QCustomer().status.equalTo(ACTIVE).findList();` |
|
||||
| `exists()` | Efficient existence checks | `new QCustomer().email.equalTo(email).exists();` |
|
||||
| `findOne()` | Unique/single-row retrieval | `new QCustomer().id.equalTo(id).findOne();` |
|
||||
| `findList()` | List retrieval | `new QCustomer().findList();` |
|
||||
| `asDto(...).findList()` | DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
||||
|
||||
### Entity mapping and lifecycle annotations
|
||||
|
||||
| Annotation | Purpose |
|
||||
|------|------|
|
||||
| `@Entity` | Marks class as persistent entity |
|
||||
| `@Id` | Primary key mapping |
|
||||
| `@Version` | Optimistic locking |
|
||||
| `@WhenCreated` | Creation timestamp management |
|
||||
| `@WhenModified` | Modification timestamp management |
|
||||
| `@Transactional` | Declarative transaction boundary |
|
||||
|
||||
## Capabilities
|
||||
|
||||
### ✅ Included
|
||||
|
||||
- Relational ORM with automatic dirty checking and lazy loading (via enhancement)
|
||||
- Multiple query abstraction levels (ORM query, DTO query, SQL/JDBC)
|
||||
- Type-safe query beans (`Q*`) with IDE autocomplete
|
||||
- Built-in migration generation and migration running support
|
||||
- Transaction APIs for implicit, declarative, and explicit transaction control
|
||||
- Support for test-time Docker database workflows
|
||||
- Query tuning and caching features for performance-sensitive workloads
|
||||
|
||||
### ❌ Not in scope
|
||||
|
||||
- HTTP routing, REST controllers, or web server runtime
|
||||
- Dependency injection container functionality
|
||||
- JSON serialization framework responsibilities
|
||||
- Front-end/UI rendering concerns
|
||||
|
||||
Ebean is intentionally focused on persistence and data access. Pair it with a web framework and DI library as needed.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### ✅ Strong fit
|
||||
|
||||
- SQL-backed business applications with rich domain models
|
||||
- Services that need both ORM productivity and SQL-level control
|
||||
- Projects requiring type-safe query authoring via generated query beans
|
||||
- Teams that want migration generation integrated with entity model changes
|
||||
- Integration test suites that need real database behavior (not only in-memory mocks)
|
||||
|
||||
### ⚠️ Consider alternatives if
|
||||
|
||||
- You need a full web framework (routing/controllers) rather than a persistence layer
|
||||
- Your project does not use relational databases as a core storage model
|
||||
- You want a single library to cover persistence, DI, and HTTP all at once
|
||||
|
||||
## Quick Start (Maven)
|
||||
|
||||
```xml
|
||||
<properties>
|
||||
<ebean.version><!-- use latest stable from Maven Central --></ebean.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-postgres</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-maven-plugin</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
```
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```java
|
||||
import io.ebean.DB;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
Database database = DB.getDefault(); // or injected
|
||||
|
||||
Customer customer = database.find(Customer.class, 42);
|
||||
customer.setName("Updated");
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
## Common Tasks & Guides
|
||||
|
||||
| Task | Guide |
|
||||
|------|------|
|
||||
| Add Ebean to an existing Maven project | [add-ebean-postgres-maven-pom.md](guides/add-ebean-postgres-maven-pom.md) |
|
||||
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
|
||||
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
|
||||
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
|
||||
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
|
||||
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
|
||||
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
|
||||
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
|
||||
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
|
||||
| Persist changes and manage transactions | [persisting-and-transactions-with-ebean.md](guides/persisting-and-transactions-with-ebean.md) |
|
||||
| Build test entities quickly | [testing-with-testentitybuilder.md](guides/testing-with-testentitybuilder.md) |
|
||||
|
||||
**Guides index**: [guides/README.md](guides/README.md)
|
||||
|
||||
## Related Ecosystem Docs
|
||||
|
||||
- [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)
|
||||
- [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)
|
||||
- [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
### For Claude, GPT, and web-based agents
|
||||
|
||||
Use this file as the top-level reference when answering Ebean questions.
|
||||
|
||||
1. Check this file first for scope and capability fit.
|
||||
2. Route implementation tasks to the relevant guide in **Common Tasks & Guides**.
|
||||
3. Treat Ebean as the persistence layer only; avoid implying it provides HTTP/DI features.
|
||||
4. Prefer type-safe query bean examples when showing query code.
|
||||
5. For setup and migration changes, follow the Maven-focused guide steps exactly.
|
||||
|
||||
### For IDE-based agents (Copilot, Cursor, etc.)
|
||||
|
||||
If `docs/LIBRARY.md` is not in context automatically:
|
||||
|
||||
1. Read `README.md` for docs entry points.
|
||||
2. Open `docs/guides/README.md` for task-specific guides.
|
||||
3. Follow linked guide files directly for concrete implementation steps.
|
||||
|
||||
---
|
||||
|
||||
## Notes for Maintainers
|
||||
|
||||
### When to update this file
|
||||
|
||||
- New release that changes requirements or key APIs
|
||||
- New guide added to `docs/guides/`
|
||||
- Capability/scope changes that affect "Included" or "Not in scope"
|
||||
- Significant migration or setup workflow changes
|
||||
|
||||
### Maintenance checklist
|
||||
|
||||
- [ ] Keep requirements and version references accurate
|
||||
- [ ] Keep Common Tasks table aligned with `docs/guides/README.md`
|
||||
- [ ] Keep artifact names/snippets aligned with setup guides
|
||||
- [ ] Keep AI instructions aligned with current docs structure
|
||||
|
||||
### Link from repository README
|
||||
|
||||
In `README.md`, include:
|
||||
|
||||
```markdown
|
||||
## Documentation
|
||||
|
||||
- [Ebean docs](https://ebean.io/docs/)
|
||||
- [Library reference](docs/LIBRARY.md)
|
||||
- [Step-by-step guides](docs/guides/README.md)
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
# AGENTS.md — Ebean ORM
|
||||
|
||||
This project uses [Ebean ORM](https://ebean.io).
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
Step-by-step guides for common Ebean tasks (Maven setup, database config, query writing, transactions, test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
|
||||
|
||||
Key guides (fetch and follow when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Derived / formula properties (`@Formula`, `@Formula2`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/derived-formula-properties.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
|
||||
|
||||
Agents: Before performing any Ebean-related task, fetch and follow the relevant guide above.
|
||||
@@ -1,205 +0,0 @@
|
||||
# Guides
|
||||
|
||||
See also: [AGENTS.md](AGENTS.md) — a minimal template for AI agent onboarding and automation in Ebean ORM projects.
|
||||
|
||||
Step-by-step guides written as instructions for AI agents and developers.
|
||||
|
||||
For a high-level capability reference (scope, core APIs, and AI guidance), see
|
||||
[../LIBRARY.md](../LIBRARY.md).
|
||||
|
||||
## 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 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
|
||||
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
|
||||
|
||||
## Migration & upgrades
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
|
||||
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
|
||||
|
||||
## Observability
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
|
||||
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
|
||||
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
|
||||
|
||||
## Entity beans
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
|
||||
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
|
||||
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
|
||||
| [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md) | Read-only computed properties: physical-SQL `@Formula` (with `${ta}` and hand-written joins) versus logical path-based `@Formula2` (auto-resolved joins); use in `select`/`where`/`orderBy`; default inclusion and the `@Transient` opt-out |
|
||||
|
||||
## Querying
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
|
||||
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
|
||||
| [Using `RawSql` with Ebean](using-rawsql-with-ebean.md) | Choose between `RawSqlBuilder.parse()`, `unparsed()`, and `withPlaceholders()`; the `${where}`/`${andWhere}`/`${having}`/`${andHaving}` placeholder reference for CTEs, window functions, and subqueries; column mapping; and using `RawSql` with query beans |
|
||||
|
||||
## Persisting & transactions
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Persisting and transactions with Ebean](persisting-and-transactions-with-ebean.md) | Step-by-step guidance for AI agents to choose `insert` / `save` / `update` / `delete`; inspect cascades; select the right transaction boundary; and use batch or bulk update for large write sets |
|
||||
|
||||
## Testing
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
|
||||
|
||||
## 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
|
||||
|
||||
### Agent Skills (recommended)
|
||||
|
||||
The **ebean-orm skill** packages these guides into a structured format that AI agents
|
||||
load on-demand. See the [ebean-orm/skills](https://github.com/ebean-orm/skills) repo
|
||||
for installation instructions. Quick start:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:ebean-orm/skills.git ~/.agents/ebean-skills
|
||||
mkdir -p ~/.agents/skills
|
||||
ln -sf ~/.agents/ebean-skills/ebean-orm ~/.agents/skills/ebean-orm
|
||||
```
|
||||
|
||||
This works with [pi](https://github.com/mariozechner/pi-coding-agent),
|
||||
[Claude Code](https://docs.anthropic.com/en/docs/claude-code), and any harness
|
||||
that supports the [Agent Skills standard](https://agentskills.io/specification).
|
||||
|
||||
### Other approaches
|
||||
|
||||
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.
|
||||
|
||||
### llms.txt (fastest bootstrap for any agent)
|
||||
|
||||
`https://ebean.io/llms.txt` provides a concise Ebean capability reference and links
|
||||
to all guides in a format optimised for LLMs. Agents that check `llms.txt` before
|
||||
starting work will find everything they need without requiring project-level
|
||||
configuration. This is worth knowing if you are an AI agent reading this file.
|
||||
|
||||
|
||||
### 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, query writing, transactions,
|
||||
test containers, DB migrations, Lombok) are at:
|
||||
|
||||
**https://github.com/ebean-orm/ebean/tree/HEAD/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/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
|
||||
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
|
||||
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
|
||||
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/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.
|
||||
|
||||
### AGENTS.md — OpenAI Codex / GitHub Copilot coding agent
|
||||
|
||||
Place an `AGENTS.md` at your repo root:
|
||||
|
||||
```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/HEAD/docs/guides/
|
||||
|
||||
Key guides (fetch and follow these when performing the relevant task):
|
||||
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
|
||||
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
|
||||
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
|
||||
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
|
||||
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
|
||||
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
|
||||
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
|
||||
- Entity bean creation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/entity-bean-creation.md
|
||||
```
|
||||
|
||||
### 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/HEAD/docs/guides/
|
||||
```
|
||||
@@ -1,400 +0,0 @@
|
||||
# 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
|
||||
|
||||
### Protecting hand-edited and non-versioned migrations across regeneration
|
||||
|
||||
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
|
||||
entity model**. It can therefore overwrite content you did not change in the
|
||||
entity beans, including:
|
||||
|
||||
- **hand-edited DDL** in a generated versioned `.sql` file, and
|
||||
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
|
||||
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
|
||||
|
||||
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
|
||||
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
|
||||
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
|
||||
regeneration. The trade-off: to pick up an upstream change to a built-in init
|
||||
script (e.g. the partition helper) you must **delete the file first**, then
|
||||
regenerate. Repeatable scripts are always regenerated.
|
||||
|
||||
To avoid losing manual work:
|
||||
|
||||
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
|
||||
cannot express — it is isolated and now protected from regeneration.
|
||||
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
|
||||
produces, review the diff after **every** regeneration and **restore** any
|
||||
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
|
||||
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
|
||||
re-check that the new migration is listed and filenames match after renaming a
|
||||
generated file.
|
||||
|
||||
> **Run the generator from the module directory.** The output path set via
|
||||
> `setPathToResources(...)` is resolved relative to the **working directory**.
|
||||
> Run `GenerateDbMigration` with the working directory set to the module that owns
|
||||
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
|
||||
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
|
||||
|
||||
---
|
||||
|
||||
## Understanding the output files
|
||||
|
||||
### 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 |
|
||||
@@ -1,158 +0,0 @@
|
||||
# Guide: Add Ebean OpenTelemetry tracing
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable Ebean transaction tracing with OpenTelemetry and,
|
||||
most importantly, how to order startup so Ebean sees the intended global
|
||||
OpenTelemetry instance.
|
||||
|
||||
Use this guide when adding `ebean-opentelemetry`, diagnosing missing Ebean spans,
|
||||
or fixing `GlobalOpenTelemetry` double-registration errors.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
`ebean-opentelemetry` provides an Ebean profiling handler that creates transaction
|
||||
spans as children of the current active OpenTelemetry span. It does not create
|
||||
top-level request, job, or Lambda invocation spans by itself.
|
||||
|
||||
The handler resolves its tracer from `GlobalOpenTelemetry` when the Ebean
|
||||
`Database` is configured. For that reason, the application must build and register
|
||||
the OpenTelemetry SDK before any Ebean `Database` beans are created.
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
- Register the global OpenTelemetry instance once.
|
||||
- Register it before building Ebean databases.
|
||||
- Model that ordering as a real DI dependency.
|
||||
- Do not call `GlobalOpenTelemetry.set(...)` or `buildAndRegisterGlobal()` in
|
||||
multiple places.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Add the dependency
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-opentelemetry</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The module registers the Ebean OpenTelemetry profile handler via `ServiceLoader`.
|
||||
No manual Ebean plugin registration is normally required.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Build OpenTelemetry before Ebean databases
|
||||
|
||||
Create one application-owned OpenTelemetry bean. For example, when using
|
||||
`avaje-metrics-otel`:
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.metrics.otel.MetricsOpenTelemetry;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Factory
|
||||
class OpenTelemetryConfig {
|
||||
|
||||
@Bean
|
||||
OpenTelemetry openTelemetry(Configuration config) {
|
||||
return MetricsOpenTelemetry.builder()
|
||||
.endpoint(config.get("otel.endpoint"))
|
||||
.serviceName(config.get("otel.serviceName", "orders"))
|
||||
.deploymentEnvironmentName(config.get("app.env", "local"))
|
||||
.meterInterval(Duration.ofSeconds(30))
|
||||
.traceInterval(Duration.ofSeconds(30))
|
||||
.buildAndRegisterGlobal();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If you build the SDK directly, use the same principle: create the SDK once and
|
||||
register that instance globally before any Ebean databases are built.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Make database beans depend on OpenTelemetry
|
||||
|
||||
In DI code, make the `Database` bean method accept `OpenTelemetry`. This parameter
|
||||
is intentionally present to make startup order deterministic: OpenTelemetry is
|
||||
created and registered before Ebean configures the database and profile handler.
|
||||
|
||||
```java
|
||||
import io.avaje.config.Configuration;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.ebean.Database;
|
||||
import io.ebean.datasource.DataSourceBuilder;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
|
||||
@Factory
|
||||
class DatabaseConfig {
|
||||
|
||||
@Bean
|
||||
Database database(OpenTelemetry openTelemetry, Configuration config) {
|
||||
var dataSource = DataSourceBuilder.create()
|
||||
.url(config.get("db.url"))
|
||||
.username(config.get("db.username"))
|
||||
.password(config.get("db.password"));
|
||||
|
||||
return Database.builder()
|
||||
.name("db")
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For Spring, use the same dependency shape: either inject `OpenTelemetry` into the
|
||||
database `@Bean` method or use `@DependsOn` to ensure the OpenTelemetry bean is
|
||||
initialized first.
|
||||
|
||||
Do not invert the dependency by making OpenTelemetry depend on the Ebean
|
||||
`Database`. That creates a startup cycle and can still initialize Ebean before the
|
||||
global OpenTelemetry instance is ready.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Create a parent span at the application boundary
|
||||
|
||||
Ebean transaction spans are child spans. They are only created when a recording
|
||||
OpenTelemetry span is active on the current thread.
|
||||
|
||||
Use HTTP server instrumentation, Lambda instrumentation, or an application-level
|
||||
root span around the top-level request/job boundary. Ebean will then attach
|
||||
transaction spans beneath that current span.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `GlobalOpenTelemetry.set has already been called`
|
||||
|
||||
This usually means more than one component is trying to register a global SDK, or
|
||||
some startup path touched the global before the application registered its SDK.
|
||||
|
||||
Fixes:
|
||||
|
||||
1. Keep exactly one `buildAndRegisterGlobal()` / `GlobalOpenTelemetry.set(...)`
|
||||
call in the application.
|
||||
2. Build that OpenTelemetry bean before Ebean `Database` beans.
|
||||
3. Remove duplicate OTEL setup from tests, helper factories, or secondary modules.
|
||||
|
||||
### No Ebean spans appear
|
||||
|
||||
Check:
|
||||
|
||||
1. `ebean-opentelemetry` is on the runtime classpath.
|
||||
2. OpenTelemetry is registered before Ebean databases are built.
|
||||
3. There is a current recording parent span when Ebean transactions run.
|
||||
4. Sampling is not dropping the parent trace.
|
||||
@@ -1,295 +0,0 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: 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 3 of 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
|
||||
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
|
||||
- **Step 2 complete**: Test container setup is working and `mvn verify` passes
|
||||
(see `add-ebean-postgres-test-container.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)
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
If the project has a dedicated config-wrapper class (a `@Component` that reads config
|
||||
keys), accept it as a parameter instead of `Configuration`.
|
||||
|
||||
> **Note:** Injecting `Configuration` requires that `avaje-config` is properly wired
|
||||
> into the DI context. If you encounter "No dependency provided for
|
||||
> io.avaje.config.Configuration" errors, use `Config.get(...)` static access instead
|
||||
> (as shown in Step 2).
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
The test container setup (Step 2) should already be complete and passing
|
||||
before this step. See `add-ebean-postgres-test-container.md`.
|
||||
@@ -1,294 +0,0 @@
|
||||
# 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 3.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- An existing Maven project (`pom.xml` already exists)
|
||||
- Java 11 or higher
|
||||
- The project does **not** yet include any Ebean dependencies
|
||||
|
||||
---
|
||||
|
||||
## Step 0 — Gather requirements from the user
|
||||
|
||||
Before modifying any files, ask the user the following questions to determine
|
||||
the correct setup path. Record the answers — they affect dependency choices
|
||||
in this step and the approach used in Steps 2 and 3.
|
||||
|
||||
### Mandatory gate (do not skip)
|
||||
|
||||
- Do **not** continue to Step 1+ until the DI path is explicitly recorded.
|
||||
- Do **not** infer the **None** path by default. Use **None** only when the user explicitly confirms no DI framework.
|
||||
- If the user asks for a partial action (for example, "do only step 3"), keep the previously selected DI path; do not switch paths implicitly.
|
||||
|
||||
### DI path precedence (when user has not answered yet)
|
||||
|
||||
Use this precedence order:
|
||||
|
||||
1. Existing project context (highest priority): if dependencies/config already show Avaje Inject or Spring, select that path.
|
||||
2. Explicit user answer in this guide's questions.
|
||||
3. Recommended default only when context is genuinely unknown: Avaje Inject.
|
||||
|
||||
If context remains ambiguous, ask one multiple-choice clarification question and wait for the answer before editing files.
|
||||
|
||||
### Question 1: Dependency injection framework
|
||||
|
||||
> "Does this project use (or will it use) a DI framework? If so, which one?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Avaje Inject** | Add `avaje-inject` + `avaje-inject-test` dependencies; use `@TestScope @Factory` for test container (Step 2); use `@Factory`/`@Bean` for production database (Step 3) |
|
||||
| **Spring** | Use Spring `@TestConfiguration` for test container (Step 2); use Spring `@Configuration`/`@Bean` for production database (Step 3) |
|
||||
| **None** | Use declarative `application-test.yaml` for test container (Step 2); use programmatic `Database.builder()` directly in application code (Step 3) |
|
||||
|
||||
### Question 2: PostGIS
|
||||
|
||||
> "Do you need PostGIS spatial extensions (geometry types, spatial queries)?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Use `PostgisContainer` in test setup (Step 2); may need `net.postgis:postgis-jdbc` dependency |
|
||||
| **No** | Use `PostgresContainer` in test setup (Step 2) |
|
||||
|
||||
### Question 3: Read replica
|
||||
|
||||
> "Does your production environment use a separate read-replica (read-only) database?"
|
||||
|
||||
| Answer | Effect |
|
||||
|--------|--------|
|
||||
| **Yes** | Configure a read-only `DataSourceBuilder` in production database config (Step 3) |
|
||||
| **No** | Single datasource only (Step 3) |
|
||||
|
||||
### Defaults
|
||||
|
||||
If the user is unsure or setting up a new project, recommend:
|
||||
- **Avaje Inject** (lightweight, fast compile-time DI)
|
||||
- **No PostGIS** (can be added later)
|
||||
- **No read replica** (can be added later)
|
||||
|
||||
---
|
||||
|
||||
## 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.5.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>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>1.8</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The `io.avaje:junit` bundle includes JUnit Jupiter (API + engine) and AssertJ,
|
||||
avoiding the need to declare those dependencies separately.
|
||||
|
||||
---
|
||||
|
||||
## Step 4b — Add DI framework dependencies (if applicable)
|
||||
|
||||
If the user chose **Avaje Inject** in Step 0, add the following dependencies and
|
||||
annotation processor. Skip this step if the user chose Spring or no DI.
|
||||
|
||||
### Dependencies
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>12.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>12.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
> Check [Maven Central](https://central.sonatype.com/artifact/io.avaje/avaje-inject)
|
||||
> for the latest version.
|
||||
|
||||
### Annotation processor
|
||||
|
||||
The `avaje-inject-generator` must be added to the `annotationProcessorPaths` in
|
||||
`maven-compiler-plugin` (added in Step 6 below). When adding both processors,
|
||||
the final `<annotationProcessorPaths>` block should include both:
|
||||
|
||||
```xml
|
||||
<annotationProcessorPaths>
|
||||
<path> <!-- generate ebean query beans -->
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>querybean-generator</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</path>
|
||||
<path> <!-- generate avaje-inject DI code -->
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>12.5</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 both main and test sources compile:
|
||||
|
||||
```bash
|
||||
mvn test-compile
|
||||
```
|
||||
|
||||
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
|
||||
Using `test-compile` rather than `compile` ensures test dependencies and test
|
||||
source files are also verified.
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Proceed to **Step 2: Test container setup**
|
||||
(`add-ebean-postgres-test-container.md`) to wire an injectable test `Database`
|
||||
backed by `ebean-test` containers. Verify with `mvn verify` before continuing
|
||||
to production database configuration.
|
||||
@@ -1,445 +0,0 @@
|
||||
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project - Step 2: Test Container Setup
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
|
||||
container for tests, exposing an `io.ebean.Database` instance for use in test
|
||||
classes. This is Step 2 of 3.
|
||||
|
||||
Complete this step before configuring the production database in Step 3. Getting
|
||||
the test container working first gives you a fast feedback loop - you can verify
|
||||
entity changes compile, enhance, and persist correctly with `mvn verify` before
|
||||
wiring up production datasource configuration.
|
||||
|
||||
---
|
||||
|
||||
## 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 0 answers recorded**: DI framework choice and PostGIS requirement
|
||||
- **Docker** is installed and running on the developer machine
|
||||
|
||||
---
|
||||
|
||||
## Overview: Choosing your approach
|
||||
|
||||
The approach depends on the DI framework choice made in Step 0:
|
||||
|
||||
| DI framework | Approach | How |
|
||||
|--------------|----------|-----|
|
||||
| **Avaje Inject** | Programmatic | `@TestScope @Factory` class with injectable `Database` bean |
|
||||
| **Spring** | Programmatic | `@TestConfiguration` class with `@Bean` methods |
|
||||
| **None** | Declarative | `application-test.yaml` + plain JUnit test |
|
||||
|
||||
Follow the path that matches your choice below.
|
||||
|
||||
---
|
||||
|
||||
## Path A — Programmatic with Avaje Inject (recommended)
|
||||
|
||||
This approach uses `@TestScope @Factory` to expose the container and `Database`
|
||||
as injectable beans. It offers more control (image mirrors, custom config) and
|
||||
makes `Database` directly injectable into test classes.
|
||||
|
||||
### A.1 — Verify Avaje Inject test dependencies
|
||||
|
||||
Confirm the following are present in `pom.xml` (in addition to `ebean-test`):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-test</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
And the `avaje-inject-generator` annotation processor in `maven-compiler-plugin`:
|
||||
|
||||
```xml
|
||||
<path>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-inject-generator</artifactId>
|
||||
<version>${avaje-inject.version}</version>
|
||||
</path>
|
||||
```
|
||||
|
||||
### A.2 — Create a `@TestScope @Factory` class
|
||||
|
||||
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`):
|
||||
|
||||
```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 below
|
||||
}
|
||||
```
|
||||
|
||||
### A.3 — Add a container bean and a Database bean
|
||||
|
||||
#### 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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### PostGIS (PostgreSQL + PostGIS extension)
|
||||
|
||||
Use `PostgisContainer` instead. 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")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Bean
|
||||
Database database(PostgisContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Key differences
|
||||
|
||||
| | 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) |
|
||||
|
||||
### A.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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### A.5 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
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 - ...
|
||||
```
|
||||
|
||||
**Important:** Verify this step passes with `mvn verify` before proceeding to
|
||||
Step 3 (production database configuration).
|
||||
|
||||
---
|
||||
|
||||
## Path B — Programmatic with Spring
|
||||
|
||||
Use Spring’s `@TestConfiguration` to provide the container and `Database` beans.
|
||||
|
||||
### B.1 — Create a `@TestConfiguration` class
|
||||
|
||||
```java
|
||||
package com.example.testconfig;
|
||||
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.containers.PostgresContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
@TestConfiguration
|
||||
class TestDatabaseConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17")
|
||||
.dbName("my_app")
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For PostGIS, use `PostgisContainer` instead (same pattern as Path A).
|
||||
|
||||
### B.2 — Write a test
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
class DatabaseTest {
|
||||
|
||||
@Autowired
|
||||
Database database;
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### B.3 — Verify
|
||||
|
||||
Run `mvn verify` and confirm the same log output as Path A.
|
||||
|
||||
---
|
||||
|
||||
## Path C — Declarative (no DI framework)
|
||||
|
||||
This is the simplest approach but offers less control. `ebean-test` reads a
|
||||
YAML config file and automatically manages the Docker container and `Database`
|
||||
instance. Use this when the project has no DI framework.
|
||||
|
||||
### C.1 — Create `application-test.yaml`
|
||||
|
||||
Create `src/test/resources/application-test.yaml`:
|
||||
|
||||
```yaml
|
||||
ebean:
|
||||
test:
|
||||
platform: postgres
|
||||
ddlMode: dropCreate
|
||||
dbName: my_app
|
||||
```
|
||||
|
||||
For PostGIS, use `platform: postgis` instead.
|
||||
|
||||
### C.2 — Write a test
|
||||
|
||||
Use `DB.getDefault()` to obtain the `Database` instance:
|
||||
|
||||
```java
|
||||
package com.example;
|
||||
|
||||
import io.ebean.DB;
|
||||
import io.ebean.Database;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DatabaseTest {
|
||||
|
||||
@Test
|
||||
void database_isAvailable() {
|
||||
Database database = DB.getDefault();
|
||||
assertThat(database).isNotNull();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### C.3 — Verify
|
||||
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
Expected log output:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
**Important:** Verify this passes before proceeding to Step 3.
|
||||
|
||||
Skip to [Optional configurations](#optional-configurations) or proceed to Step 3.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Add `TestEntityBuilder`** to your test configuration for rapid test data creation
|
||||
with auto-populated random values. See `testing-with-testentitybuilder.md`.
|
||||
- **Proceed to Step 3** — production database configuration
|
||||
(`add-ebean-postgres-database-config.md`). Verify this step passes with
|
||||
`mvn verify` before continuing.
|
||||
@@ -1,116 +0,0 @@
|
||||
# Guide: `@DbJson` / `@DbJsonB` mapping support — built-in vs Jackson ObjectMapper
|
||||
|
||||
## Purpose
|
||||
|
||||
Ebean can map `@DbJson` and `@DbJsonB` properties in two ways:
|
||||
|
||||
- **Built-in** JSON support, backed by **avaje-json-core** — no extra dependency.
|
||||
- **Jackson `ObjectMapper`**, provided by the **`ebean-jackson-mapper`** module — used
|
||||
for everything the built-in support does not handle.
|
||||
|
||||
This guide lists exactly which property types are handled built-in and which require
|
||||
`ebean-jackson-mapper`.
|
||||
|
||||
> If a property type is **not** handled built-in and `ebean-jackson-mapper` is not on the
|
||||
> classpath, Ebean fails fast at startup:
|
||||
>
|
||||
> ```text
|
||||
> Unsupported @DbJson mapping - Missing dependency ebean-jackson-mapper?
|
||||
> Jackson ObjectMapper not present for <property>
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Property type | Built-in (avaje-json-core) | Needs `ebean-jackson-mapper` |
|
||||
|---|:---:|:---:|
|
||||
| `String` | ✅ | |
|
||||
| `List<String>`, `List<Long>` | ✅ | |
|
||||
| `Set<String>`, `Set<Long>` | ✅ | |
|
||||
| `Map<String, Object>`, `Map<String, ?>` | ✅ | |
|
||||
| `Map<String, String>` | ✅ | |
|
||||
| `Map<Enum, Object>`, `Map<Enum, String>` | ✅ | |
|
||||
| `List`/`Set` of any other element type (`Integer`, `Double`, `UUID`, `LocalDate`, an enum, a POJO, …) | | ✅ |
|
||||
| `Map` with a typed value other than `String`/`Object` (`Map<String,Integer>`, `Map<String,UUID>`, …) | | ✅ |
|
||||
| `Map` with a key other than `String` or an enum (`Map<Integer, …>`, `Map<UUID, …>`) | | ✅ |
|
||||
| POJOs, records, or any other type | | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Built-in support (no Jackson required)
|
||||
|
||||
The built-in path materialises JSON into the *natural* JSON value types
|
||||
(`String`, `Long`, `BigDecimal`, `Boolean`, `Map`, `List`). It is therefore type-safe only
|
||||
for the following declared property types:
|
||||
|
||||
- **`String`** — stored as raw JSON text.
|
||||
- **`List<String>`** and **`List<Long>`**.
|
||||
- **`Set<String>`** and **`Set<Long>`**.
|
||||
- **`Map<K, V>`** where:
|
||||
- the key `K` is `String` or an **enum**, and
|
||||
- the value `V` is `Object`, `String`, or a wildcard `?`.
|
||||
|
||||
So `Map<String,Object>`, `Map<String,String>`, `Map<Enum,Object>` and `Map<Enum,String>`
|
||||
are all built-in.
|
||||
|
||||
These mappings work across all supported storage types — `VARCHAR`, `CLOB`, `BLOB`, and
|
||||
Postgres `json` / `jsonb` — without `ebean-jackson-mapper`.
|
||||
|
||||
---
|
||||
|
||||
## Everything else → Jackson `ObjectMapper`
|
||||
|
||||
Any other `@DbJson` / `@DbJsonB` property routes to the Jackson `ObjectMapper` path, which
|
||||
requires `ebean-jackson-mapper`:
|
||||
|
||||
- **Typed collections** — `List`/`Set` whose element type is not `String` or `Long`
|
||||
(for example `List<Integer>`, `List<UUID>`, `List<LocalDate>`, `List<MyEnum>`, `List<MyPojo>`).
|
||||
- **Typed-value maps** — a `Map` value type other than `String`/`Object`
|
||||
(for example `Map<String,Integer>`, `Map<String,UUID>`, `Map<String,MyPojo>`).
|
||||
- **Non-`String`/non-enum map keys** — for example `Map<Integer,Object>`, `Map<UUID,String>`.
|
||||
- **POJOs, records, and any other custom type.**
|
||||
|
||||
> **Jackson marker annotation override:** if the **field or getter** carries a Jackson annotation
|
||||
> (anything meta-annotated with `com.fasterxml.jackson.annotation.JacksonAnnotation`), Ebean
|
||||
> uses the `ObjectMapper` path even when the type would otherwise be handled built-in.
|
||||
|
||||
---
|
||||
|
||||
## Adding `ebean-jackson-mapper`
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-jackson-mapper</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
A Jackson `ObjectMapper` must be available (via `jackson-databind`). Ebean detects it and
|
||||
registers the mapper-based JSON support automatically.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Enum map keys** are serialised using the enum `name()` (for example `ACTIVE`), not any
|
||||
`@DbEnumValue` mapping. Round-trips are correct; the DB value mapping is not applied to
|
||||
JSON keys.
|
||||
- **`@DbArray` alternative:** for typed *scalar* collections (`List`/`Set` of `Integer`,
|
||||
`Long`, `UUID`, `Double`, an enum, …) consider `@DbArray`, which maps to a native DB array
|
||||
(with a JSON fallback on platforms without array support) and supports more element types
|
||||
than built-in `@DbJson` collections.
|
||||
- The reason typed value/element collections need a real mapper is that the built-in path
|
||||
only produces natural JSON types — for example a JSON number always parses to `Long`, so a
|
||||
declared `List<Integer>` or `Map<String,Integer>` could not be populated safely without a
|
||||
type-aware mapper.
|
||||
|
||||
---
|
||||
|
||||
## Choosing
|
||||
|
||||
- Prefer the **built-in** mappings for the common cases (`String`, string/long lists and sets,
|
||||
object/string maps) to avoid pulling in Jackson.
|
||||
- Add **`ebean-jackson-mapper`** when you need rich POJO JSON columns or typed collections /
|
||||
typed-value maps.
|
||||
@@ -1,164 +0,0 @@
|
||||
# Guide: Derived / formula properties — `@Formula` and `@Formula2`
|
||||
|
||||
## Purpose
|
||||
|
||||
A *formula property* is a read-only entity property whose value is computed by a SQL
|
||||
expression at query time rather than stored in its own column. Ebean has two
|
||||
annotations for this:
|
||||
|
||||
- **`@Formula`** — you write the **physical SQL** for the `select` (and any `join`),
|
||||
using the `${ta}` placeholder for the base table alias. Maximum control; verbose.
|
||||
- **`@Formula2`** — you write a **logical expression** using dot-notation property
|
||||
paths (e.g. `parent.familyName`). Ebean translates the paths to the correct table
|
||||
aliases and **adds the required JOINs automatically**.
|
||||
|
||||
`@Formula2` is intended as the easier, path-based replacement for `@Formula`. Both
|
||||
produce read-only properties and behave the same way with respect to default
|
||||
inclusion (see [Default inclusion](#default-inclusion-and-transient)).
|
||||
|
||||
---
|
||||
|
||||
## Quick comparison
|
||||
|
||||
| | `@Formula` | `@Formula2` |
|
||||
|---|---|---|
|
||||
| Expression | Physical SQL columns + aliases | Logical property paths |
|
||||
| Table alias | `${ta}` placeholder you write | Resolved automatically |
|
||||
| Joins | You write the `join` clause | Added automatically from the paths |
|
||||
| Read only | ✅ | ✅ |
|
||||
| Included by default | ✅ (use `@Transient` to opt out) | ✅ (use `@Transient` to opt out) |
|
||||
| Usable in `select` / `where` / `orderBy` / `having` | ✅ | ✅ |
|
||||
| Creates a DB column (DDL) | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## `@Formula` — physical SQL
|
||||
|
||||
You supply the SQL `select` fragment, and an optional `join`. Use `${ta}` wherever you
|
||||
need the base table alias of the entity.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class ParentPerson {
|
||||
|
||||
// aggregation via a derived join; ${ta} is the base table alias
|
||||
@Formula(select = "coalesce(f2.child_count, 0)",
|
||||
join = "left join (select parent_id, count(*) as child_count"
|
||||
+ " from child group by parent_id) f2 on f2.parent_id = ${ta}.id")
|
||||
Integer childCount;
|
||||
|
||||
// coalesce across a joined table using an explicit join alias (j1)
|
||||
@Formula(select = "coalesce(${ta}.family_name, j1.family_name)",
|
||||
join = "join parent_person j1 on j1.id = ${ta}.parent_id")
|
||||
String effectiveFamilyName;
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The `join` string must start with `join` or `left join`.
|
||||
- You manage the join aliases (`j1`, `f2`, …) yourself and reference them in `select`.
|
||||
- `@Formula` is `@Repeatable` and supports a `platforms()` restriction.
|
||||
|
||||
---
|
||||
|
||||
## `@Formula2` — logical property paths
|
||||
|
||||
Write the expression using property paths. Ebean resolves each path to the right table
|
||||
alias and adds the joins it needs.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class ParentPerson {
|
||||
|
||||
@ManyToOne
|
||||
GrandParentPerson parent;
|
||||
|
||||
String familyName;
|
||||
|
||||
// Ebean automatically left joins 'parent' and resolves the aliases
|
||||
@Formula2("coalesce(familyName, parent.familyName)")
|
||||
String derivedFamilyName;
|
||||
}
|
||||
```
|
||||
|
||||
A query selecting `derivedFamilyName` produces (roughly):
|
||||
|
||||
```sql
|
||||
select t0.id, coalesce(t0.family_name, t1.family_name)
|
||||
from parent_person t0
|
||||
left join grand_parent_person t1 on t1.id = t0.parent_id
|
||||
```
|
||||
|
||||
Multi-level paths join through each step:
|
||||
|
||||
```java
|
||||
// joins parent and parent.parent automatically
|
||||
@Formula2("coalesce(familyName, parent.familyName, parent.parent.familyName)")
|
||||
String deepFamilyName;
|
||||
```
|
||||
|
||||
`@Formula2` works wherever a normal property does — the required joins are added
|
||||
automatically in each case:
|
||||
|
||||
```java
|
||||
// selected explicitly
|
||||
DB.find(ParentPerson.class).select("derivedFamilyName").findList();
|
||||
|
||||
// used in where (auto-joins even when not selected)
|
||||
DB.find(ParentPerson.class).where().eq("derivedFamilyName", "Smith").findList();
|
||||
|
||||
// used in order by
|
||||
DB.find(ParentPerson.class).orderBy("derivedFamilyName").findList();
|
||||
|
||||
// referenced via a path from another bean
|
||||
DB.find(ChildPerson.class).where().eq("parent.derivedFamilyName", "Smith").findList();
|
||||
```
|
||||
|
||||
It also resolves correctly inside nested `fetch()` joins, so a `@Formula2` on a fetched
|
||||
association is computed with its own joins relative to that association.
|
||||
|
||||
Notes:
|
||||
- The expression supports any SQL function whose arguments are logical property paths.
|
||||
- `@Formula2` supports a `platforms()` restriction.
|
||||
- No `${ta}` and no hand-written join — that is the point of `@Formula2`.
|
||||
|
||||
---
|
||||
|
||||
## Default inclusion and `@Transient`
|
||||
|
||||
Both annotations are **included in queries by default** (just like a normal mapped
|
||||
property). When no explicit `select()`/`fetch()` is given, the formula — and for
|
||||
`@Formula2` the joins it requires — are added to the query.
|
||||
|
||||
Add `@Transient` to make the formula **opt-in**: it is then **not** selected by default
|
||||
and must be requested explicitly via `select()` or `fetch()`. Do this when the formula
|
||||
(or the joins it needs) is relatively expensive.
|
||||
|
||||
```java
|
||||
// not selected by default; must be requested explicitly
|
||||
@Transient
|
||||
@Formula2("coalesce(familyName, parent.familyName)")
|
||||
String lazyDerivedFamilyName;
|
||||
```
|
||||
|
||||
```java
|
||||
DB.find(ParentPerson.class)
|
||||
.select("lazyDerivedFamilyName") // explicitly included, join auto-added
|
||||
.findList();
|
||||
```
|
||||
|
||||
This is the same `@Transient` opt-out mechanism used by `@Formula`.
|
||||
|
||||
---
|
||||
|
||||
## Which should I use?
|
||||
|
||||
- Prefer **`@Formula2`** for expressions over property paths (coalesce/case/functions
|
||||
across associations). It is shorter, refactor-friendly, and the joins stay correct as
|
||||
the model changes.
|
||||
- Use **`@Formula`** when you need raw SQL that does not map cleanly to property paths —
|
||||
for example a derived aggregate sub-select / dynamic view, or vendor-specific SQL.
|
||||
|
||||
For read models that exist only to carry computed values, also consider projecting to a
|
||||
DTO instead of mapping the formula onto the entity — see
|
||||
[writing-ebean-query-beans.md](writing-ebean-query-beans.md).
|
||||
@@ -1,262 +0,0 @@
|
||||
# Guide: Ebean query metrics and naming
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains the metrics Ebean captures, how the metric **name** for a query
|
||||
is derived, and how you influence that name with `setLabel(..)` and **profile
|
||||
locations**. It also covers secondary (lazy / query) load naming, the inline SQL
|
||||
comment, collecting metrics at runtime, and how the names map to avaje-metrics tags.
|
||||
|
||||
Use this guide when you want to identify a query in metrics/telemetry, when a query
|
||||
shows up under an unexpected metric name, or when wiring Ebean metrics into a reporter.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Ebean records timing and counter metrics for the work it does. Every metric has a
|
||||
**name** whose leading segment identifies the kind of work:
|
||||
|
||||
| Prefix | What it measures | Example name |
|
||||
|---|---|---|
|
||||
| `orm.` | Entity (ORM) query | `orm.Customer.findList`, `orm.CustomerFinder.byName` |
|
||||
| `dto.` | DTO query | `dto.CustomerDto.byEmail` |
|
||||
| `sql.query.` | Raw SQL query | `sql.query.<label>` |
|
||||
| `sql.update.` / `sql.call.` | Raw SQL update / stored procedure call | `sql.update.<label>` |
|
||||
| `orm.update.` | ORM update statement | `orm.update.<label>` |
|
||||
| `iud.` | Bean insert / update / delete | `iud.Customer.insert` |
|
||||
| `txn.main` / `txn.readonly` / `txn.named.` | Transactions | `txn.main`, `txn.named.processOrders` |
|
||||
| `l2n.` | L2 cache region | `l2n.customer.hit` |
|
||||
|
||||
The rest of this guide focuses on **`orm.` query names**, which is where labels and
|
||||
profile locations apply.
|
||||
|
||||
---
|
||||
|
||||
## How an ORM query name is derived
|
||||
|
||||
An entity query name has the form `orm.<identifier>`. The `<identifier>` comes from one
|
||||
of three sources, in priority order:
|
||||
|
||||
1. **An explicit `setLabel(..)`** — prefixed with the bean type for disambiguation.
|
||||
2. **A profile location** — used as-is (it is already a unique `Class.method` identifier).
|
||||
3. **Neither** — the bean type plus the query type (e.g. `findList`).
|
||||
|
||||
| Root query source | Resulting name |
|
||||
|---|---|
|
||||
| `setLabel("custMain")` on `Customer` | `orm.Customer.custMain` |
|
||||
| Profile location `CustomerFinder.byName` | `orm.CustomerFinder.byName` |
|
||||
| Unlabelled `DB.find(Customer.class).findList()` | `orm.Customer.findList` |
|
||||
|
||||
The asymmetry is intentional: an explicit label is a short, ambiguous token (`custMain`
|
||||
could be used for any bean), so the bean type is prefixed. A profile location is already
|
||||
unique and type-independent, so it is used as-is.
|
||||
|
||||
### Step 1 - Label a query explicitly
|
||||
|
||||
```java
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setLabel("custMain")
|
||||
.findList();
|
||||
// metric name: orm.Customer.custMain
|
||||
```
|
||||
|
||||
DTO queries support `setLabel(..)` too, and follow the **same naming convention** as
|
||||
ORM queries — an explicit label is prefixed with the DTO type, a profile location is
|
||||
used as-is, and an unlabelled DTO query uses just the DTO type:
|
||||
|
||||
```java
|
||||
DB.findDto(CustomerDto.class, sql)
|
||||
.setLabel("byEmail")
|
||||
.findList();
|
||||
// metric name: dto.CustomerDto.byEmail
|
||||
// profile location only -> dto.<location> (no type prefix)
|
||||
// unlabelled -> dto.CustomerDto
|
||||
```
|
||||
|
||||
### Step 2 - Use a profile location (preferred for finders / query beans)
|
||||
|
||||
A profile location identifies a query by its **call site** (`Class.method`) instead of a
|
||||
hand-written label.
|
||||
|
||||
**The common case is automatic.** With Ebean's byte-code enhancement enabled (the normal
|
||||
setup when using query beans / finders), Ebean assigns each query a profile location
|
||||
derived from its call site — no code is required:
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.eq(Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<CallingClass>.<method> (often with a line number, see below)
|
||||
```
|
||||
|
||||
The enhancer derives the location from the calling code (the method that runs the query),
|
||||
and for many call sites it includes the **source line number** (e.g.
|
||||
`CustomerService.find:42`), so distinct call sites — even in the same method — get distinct
|
||||
names automatically.
|
||||
|
||||
**Setting one explicitly.** You can also set a profile location yourself, which is useful
|
||||
without enhancement or to control the identity:
|
||||
|
||||
```java
|
||||
ProfileLocation LOC = ProfileLocation.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setProfileLocation(LOC)
|
||||
.where().eq("status", Status.ACTIVE)
|
||||
.findList();
|
||||
// metric name: orm.<DeclaringClass>.<method>
|
||||
```
|
||||
|
||||
Factory choices:
|
||||
|
||||
- `ProfileLocation.create()` — call site as `Class.method`, **no line number**.
|
||||
- `ProfileLocation.createWithLine()` — includes the source line number
|
||||
(e.g. `CustomerService.find:42`), so two queries in the **same method** get
|
||||
**distinct** names.
|
||||
- `ProfileLocation.create("label")` — a named location (used for named transactions).
|
||||
|
||||
> Note: a location with no line number (`create()`, or a call site the enhancer emits
|
||||
> without a line) means two different queries in the same method share one name. The
|
||||
> queried entity is still distinguishable downstream via the avaje-metrics `type` tag
|
||||
> (see "Mapping to avaje-metrics tags" below). Use `createWithLine()` to separate
|
||||
> same-method call sites in the name itself.
|
||||
|
||||
---
|
||||
|
||||
## Secondary (lazy / query) load naming
|
||||
|
||||
When a query lazy-loads or `fetchQuery()`-loads an association, Ebean issues a
|
||||
**secondary** query. Its name **extends the parent query's full name** with the relative
|
||||
path and the load mode (`lazy` or `query`), joined with `.`:
|
||||
|
||||
```
|
||||
orm.<parent name without the "orm." prefix>.<path>.<loadMode>
|
||||
```
|
||||
|
||||
So a secondary load is always an exact extension of its parent metric name, which makes
|
||||
the relationship obvious in dashboards.
|
||||
|
||||
Example — root labelled `custMain` on `Customer`, chain `Customer -> orders -> details`:
|
||||
|
||||
Lazy loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.lazy
|
||||
orm.Customer.custMain.orders.lazy.details.lazy
|
||||
```
|
||||
|
||||
Secondary eager `fetchQuery()` loading:
|
||||
```
|
||||
orm.Customer.custMain
|
||||
orm.Customer.custMain.orders.query
|
||||
orm.Customer.custMain.orders.query.details.query
|
||||
```
|
||||
|
||||
The same applies with a **profile-location** root (no explicit `setLabel`):
|
||||
```
|
||||
orm.CustomerFinder.byName
|
||||
orm.CustomerFinder.byName.contacts.lazy
|
||||
```
|
||||
|
||||
Unlike the root query, the secondary name is **not** bean-type prefixed by the loaded
|
||||
type — it inherits the parent's name so it relates back to where the load originated.
|
||||
|
||||
---
|
||||
|
||||
## Inline SQL comment
|
||||
|
||||
When `includeLabelInSql` is enabled (the default), Ebean prepends the query's label (or
|
||||
profile-location label) as an inline SQL comment, which is useful for matching slow
|
||||
queries in database logs back to application code:
|
||||
|
||||
```sql
|
||||
select /* CustomerFinder.byName */ t0.id, t0.name from be_customer t0 where ...
|
||||
```
|
||||
|
||||
The comment uses the explicit `setLabel(..)` if present, otherwise the profile-location
|
||||
label. Secondary queries use their full extended name
|
||||
(e.g. `/* Customer.custMain.contacts.query */`). `EXISTS` / subquery forms are not
|
||||
commented.
|
||||
|
||||
Disable it via the builder:
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.includeLabelInSql(false)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Collecting metrics at runtime
|
||||
|
||||
Read collected metrics through `Database.metaInfo()`:
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaQueryMetric;
|
||||
import io.ebean.meta.ServerMetrics;
|
||||
|
||||
ServerMetrics metrics = database.metaInfo().collectMetrics(); // resets counters
|
||||
|
||||
for (MetaQueryMetric q : metrics.queryMetrics()) {
|
||||
System.out.printf("%s type=%s count=%d total=%d mean=%d%n",
|
||||
q.name(), // e.g. orm.Customer.custMain
|
||||
q.type().getSimpleName(), // the queried bean/DTO type, e.g. Customer
|
||||
q.count(), q.total(), q.mean());
|
||||
}
|
||||
```
|
||||
|
||||
Key API:
|
||||
|
||||
- `database.metaInfo()` → `MetaInfoManager`.
|
||||
- `collectMetrics()` collects and **resets**; `collectMetrics(false)` collects without
|
||||
reset; `visitMetrics(visitor)` for streaming.
|
||||
- `ServerMetrics` exposes `queryMetrics()`, `timedMetrics()`, `countMetrics()`.
|
||||
- `MetaQueryMetric` exposes `name()`, `label()`, `type()` (the queried `Class<?>`),
|
||||
`sql()`, `hash()`, plus timing `count()` / `total()` / `max()` / `mean()`.
|
||||
|
||||
---
|
||||
|
||||
## Mapping to avaje-metrics tags
|
||||
|
||||
When integrating with **avaje-metrics** (`avaje-metrics-ebean`
|
||||
`DatabaseMetricSupplier`), the flat `orm.`/`dto.`/`sql.` names are translated to a tagged
|
||||
form, with the bean type carried as a `type` tag:
|
||||
|
||||
```
|
||||
ebean.query{kind=orm|dto|sql, type=<BeanSimpleName>, label=<rest of the name>}
|
||||
```
|
||||
|
||||
Because the entity is available as the `type` tag, two different-entity queries that
|
||||
share a profile-location name remain distinct series on tag-aware backends (OpenTelemetry,
|
||||
Prometheus, StatsD) without needing the bean type in the name.
|
||||
|
||||
For the integration setup, see the avaje-metrics guide
|
||||
[`add-ebean-metrics.md`](https://github.com/avaje/avaje-metrics/blob/master/docs/guides/add-ebean-metrics.md).
|
||||
|
||||
To capture the database execution plan (`EXPLAIN`) for slow queries identified by these
|
||||
metrics, see [Ebean query plan capture](ebean-query-plan-capture.md).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### A query shows up as `orm.<Bean>.findList` (no useful identity)
|
||||
|
||||
It has neither a label nor a profile location. Add `setLabel(..)` or a
|
||||
`ProfileLocation`, or apply a profile location on the finder / query bean.
|
||||
|
||||
### Two queries in one method share a metric name
|
||||
|
||||
This happens when the profile location for those call sites has no line number. With
|
||||
enhancement, many call sites already include a line number; for those that don't, use
|
||||
`ProfileLocation.createWithLine()` to separate them by line, or give each an explicit
|
||||
`setLabel(..)`. On tag-aware backends the avaje-metrics `type` tag already separates
|
||||
different entity types.
|
||||
|
||||
### A secondary (lazy / query) load isn't grouped under its parent
|
||||
|
||||
Secondary names extend the parent's full name. If the parent has no label or profile
|
||||
location, its name falls back to `orm.<Bean>.<queryType>` and the secondary extends
|
||||
that. Give the root query a label or profile location for a stable parent name.
|
||||
@@ -1,242 +0,0 @@
|
||||
# Guide: Ebean query plan capture
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to enable and configure **query plan capture** in Ebean — the
|
||||
mechanism that captures the database's actual execution plan (via `EXPLAIN`) for slow
|
||||
queries, so you can diagnose missing indexes and poor plans in production.
|
||||
|
||||
Use this guide when you want Ebean to record real query plans, when tuning the capture
|
||||
thresholds and load limits, or when wiring a listener to ship captured plans somewhere.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Query plan capture is a **two-phase** mechanism:
|
||||
|
||||
1. **Bind capture** — when enabled, Ebean watches query executions and, for queries
|
||||
slower than a threshold, captures the actual **bind values** that were used. This is
|
||||
cheap: it just remembers the parameters of a slow execution.
|
||||
2. **Plan capture** — using those captured bind values, Ebean runs `EXPLAIN <sql>`
|
||||
against the database to obtain the execution plan, producing `MetaQueryPlan` results
|
||||
that are handed to a `QueryPlanListener`.
|
||||
|
||||
Plan capture is split this way so the expensive `EXPLAIN` work (actual database load)
|
||||
happens periodically or on demand, against representative bind values, rather than on
|
||||
every slow query.
|
||||
|
||||
Two ways to trigger phase 2:
|
||||
|
||||
- **Automatic periodic capture** — a background timer collects plans on a schedule.
|
||||
- **On demand** — call the `MetaInfoManager` API to arm and collect plans yourself
|
||||
(this is what remote tooling such as ebean-insight uses).
|
||||
|
||||
Plan capable queries are:
|
||||
|
||||
- **ORM entity SELECT queries** (`orm.*` metrics) — captured via the per-entity `BeanDescriptor`.
|
||||
- **Native-SQL `DtoQuery`** (`dto.*` metrics) — a `DtoQuery` created from a SQL string
|
||||
(`DB.findDto(MyDto.class, "select ...")`) has its own bind capture and is `EXPLAIN`'d directly.
|
||||
- **ORM-backed `DtoQuery`** (`Query.asDto(...)`) — captured via the *underlying* ORM query plan
|
||||
(`orm.*`), not the `dto.*` plan. The `dto.*` plan itself is **not** armed in this case, so it
|
||||
does not double-count in `queryPlanInit`.
|
||||
- **Native-SQL `SqlQuery`** (`sql.query.*` metrics) — a **labelled** `SqlQuery`
|
||||
(`DB.sqlQuery("select ...").setLabel("myLabel")`) has its own bind capture and is `EXPLAIN`'d
|
||||
directly. A label is required: without `setLabel(...)` the query produces no metric and no plan.
|
||||
|
||||
Specifically **excluded** are:
|
||||
|
||||
- **Update / DML** — `orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`.
|
||||
|
||||
Bind capture is wired into the ORM query path (per-entity `BeanDescriptor`), the native-SQL DTO
|
||||
path (per-DTO `DtoBeanDescriptor`), and the native-SQL `SqlQuery` path (the relational query
|
||||
engine); the init/collect API iterates all three. DML — even though it produces timing metrics —
|
||||
never captures bind values and cannot be `EXPLAIN`'d.
|
||||
|
||||
> **Cost when disabled:** SqlQuery plan capture is fully gated on the `queryPlan.enable` master
|
||||
> switch. When capture is disabled no `SqlQuery` plans are created or cached, so labelled queries
|
||||
> incur no extra cost beyond their existing timing metric.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Enable bind capture
|
||||
|
||||
Bind capture is the master switch; nothing is captured until it is on.
|
||||
|
||||
> **Security — bind values may contain PII.** Bind capture records the **actual
|
||||
> parameter values** used by slow query executions, and those values are stored
|
||||
> and shown verbatim in the captured plan output (alongside the SQL and EXPLAIN
|
||||
> plan). They can therefore contain personal or otherwise sensitive data. Capture
|
||||
> is opt-in and off by default (`queryPlan.enable=false`): only enable it where
|
||||
> that data exposure is acceptable, restrict who can read captured plans, and
|
||||
> prefer arming specific query hashes (Step 3) over a low global threshold so you
|
||||
> capture the minimum needed.
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true) // turn on bind capture
|
||||
.queryPlanThresholdMicros(100_000) // capture binds for queries slower than 100ms
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanEnable(boolean)` — enable bind capture. Default **false**.
|
||||
- `queryPlanThresholdMicros(long)` — global execution-time threshold (microseconds) a
|
||||
query must exceed before its bind values are captured. Default **`Long.MAX_VALUE`**
|
||||
(effectively off), so you must either lower it or arm specific plans by hash (Step 3).
|
||||
|
||||
Equivalent `application.properties` (avaje-config / properties):
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Enable automatic periodic capture (optional)
|
||||
|
||||
To have Ebean periodically run `EXPLAIN` for armed queries and report the plans:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.queryPlanEnable(true)
|
||||
.queryPlanThresholdMicros(100_000)
|
||||
.queryPlanCapture(true) // turn on the periodic capture timer
|
||||
.queryPlanCapturePeriodSecs(600) // every 10 minutes (default)
|
||||
.queryPlanCaptureMaxTimeMillis(10_000) // stop after 10s of capturing per cycle
|
||||
.queryPlanCaptureMaxCount(10) // at most 10 plans per cycle
|
||||
.queryPlanListener(capture -> {
|
||||
for (var plan : capture.plans()) {
|
||||
System.out.println(plan.label() + "\n" + plan.plan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
||||
|
||||
- `queryPlanCapture(boolean)` — enable the background periodic capture. Default **false**.
|
||||
- `queryPlanCapturePeriodSecs(long)` — capture frequency in seconds. Default **600** (10 min).
|
||||
- `queryPlanCaptureMaxTimeMillis(long)` — per-cycle time budget; capture stops once
|
||||
exceeded, bounding the database load. Default **10000** (10s).
|
||||
- `queryPlanCaptureMaxCount(int)` — max plans captured per cycle. Default **10**.
|
||||
- `queryPlanListener(QueryPlanListener)` — receives each `QueryPlanCapture`. If not set,
|
||||
the default listener logs plans to the `io.ebean.QUERYPLAN` logger at `INFO`.
|
||||
|
||||
Properties form:
|
||||
|
||||
```properties
|
||||
queryPlan.enable=true
|
||||
queryPlan.thresholdMicros=100000
|
||||
queryPlan.capture=true
|
||||
queryPlan.capturePeriodSecs=600
|
||||
queryPlan.captureMaxTimeMillis=10000
|
||||
queryPlan.captureMaxCount=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Capture on demand (foreground)
|
||||
|
||||
Instead of (or in addition to) the periodic timer, drive capture through
|
||||
`database.metaInfo()`. This is useful for targeted capture and is how remote tooling
|
||||
arms specific slow queries by their plan hash.
|
||||
|
||||
```java
|
||||
import io.ebean.meta.MetaInfoManager;
|
||||
import io.ebean.meta.MetaQueryPlan;
|
||||
import io.ebean.meta.QueryPlanInit;
|
||||
import io.ebean.meta.QueryPlanRequest;
|
||||
|
||||
MetaInfoManager meta = database.metaInfo();
|
||||
|
||||
// Phase 1: arm bind capture - either all plans or specific hashes
|
||||
QueryPlanInit init = new QueryPlanInit();
|
||||
init.setAll(true); // or init.add("<planHash>", 50_000);
|
||||
init.thresholdMicros(100_000);
|
||||
List<MetaQueryPlan> armed = meta.queryPlanInit(init);
|
||||
|
||||
// ... let the application run so slow executions capture their bind values ...
|
||||
|
||||
// Phase 2: collect plans now (runs EXPLAIN)
|
||||
QueryPlanRequest request = new QueryPlanRequest();
|
||||
request.maxCount(10);
|
||||
request.maxTimeMillis(10_000);
|
||||
request.since(System.currentTimeMillis() - 300_000); // binds at least ~5 min old
|
||||
List<MetaQueryPlan> plans = meta.queryPlanCollectNow(request);
|
||||
```
|
||||
|
||||
- `QueryPlanInit` arms bind capture. `setAll(true)` arms every plan; `add(hash, micros)`
|
||||
arms a specific plan (a hash of `"all"` is treated as all).
|
||||
- `QueryPlanRequest.since(epochMillis)` ensures the captured bind values have existed for
|
||||
a while, so they better represent the slowest executions. `maxCount` / `maxTimeMillis`
|
||||
bound the work, mirroring the periodic settings.
|
||||
|
||||
`MetaQueryPlan` exposes `beanType()`, `label()`, `profileLocation()`, `sql()`, `hash()`,
|
||||
`bind()`, `plan()` (the raw EXPLAIN output), `queryTimeMicros()`, `captureCount()`,
|
||||
`captureMicros()`, and `whenCaptured()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - EXPLAIN dialect
|
||||
|
||||
Ebean chooses the `EXPLAIN` statement per database platform:
|
||||
|
||||
| Platform | EXPLAIN used |
|
||||
|---|---|
|
||||
| PostgreSQL | `explain (analyze, costs, verbose, buffers) <sql>` |
|
||||
| YugabyteDB | `explain (analyze, buffers, dist) <sql>` |
|
||||
| Oracle | `EXPLAIN PLAN FOR <sql>` |
|
||||
| SQL Server | platform-specific logger |
|
||||
| H2 / MySQL / other | `explain <sql>` |
|
||||
|
||||
Override the prefix with `queryPlanExplain(..)` (or `queryPlan.explain`):
|
||||
|
||||
```java
|
||||
Database.builder()
|
||||
.queryPlanExplain("explain (costs, verbose)") // omit ANALYZE on Postgres
|
||||
.build();
|
||||
```
|
||||
|
||||
> **Caution (PostgreSQL / Yugabyte):** the default includes `ANALYZE`, which **actually
|
||||
> executes** the query to produce real timings. For non-idempotent or expensive queries,
|
||||
> override with a non-ANALYZE `explain` to avoid side effects and extra load.
|
||||
|
||||
---
|
||||
|
||||
## Related setting: internal plan TTL
|
||||
|
||||
`queryPlanTTLSeconds(int)` (default **300**) is a **different** concept — it is the time to
|
||||
live for Ebean's *internal* query plan (the object that knows how to execute a query, read
|
||||
the result set and collect metrics). It is not part of EXPLAIN capture, but is set through
|
||||
the same builder.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No plans are captured
|
||||
|
||||
1. `queryPlanEnable(true)` must be set — it is the master switch.
|
||||
2. `queryPlanThresholdMicros` defaults to `Long.MAX_VALUE`. Lower it, or arm specific
|
||||
plans via `QueryPlanInit`, otherwise no execution is ever "slow enough".
|
||||
3. For periodic capture, also set `queryPlanCapture(true)`.
|
||||
4. Queries must actually run slower than the threshold to have their binds captured.
|
||||
|
||||
### Plans appear but nothing is reported anywhere
|
||||
|
||||
No `queryPlanListener` is configured, so plans go to the default `io.ebean.QUERYPLAN`
|
||||
logger. Set a listener, or enable `INFO` logging for `io.ebean.QUERYPLAN`.
|
||||
|
||||
### Capture adds noticeable database load
|
||||
|
||||
`EXPLAIN ANALYZE` executes the query. Reduce `queryPlanCaptureMaxCount`, increase
|
||||
`queryPlanCapturePeriodSecs`, tighten `queryPlanCaptureMaxTimeMillis`, or override
|
||||
`queryPlanExplain` to a non-ANALYZE form.
|
||||
|
||||
### An unlabelled SqlQuery or update metric never offers plan capture
|
||||
|
||||
ORM entity SELECT queries (`orm.*`), native-SQL `DtoQuery` (`dto.*`) and native-SQL
|
||||
**labelled** `SqlQuery` (`sql.query.*`) are plan capable. ORM-backed DTO queries
|
||||
(`Query.asDto(...)`) are captured via their underlying ORM plan (`orm.*`), not the `dto.*` plan.
|
||||
An unlabelled `SqlQuery` produces no metric and no plan — add `setLabel(...)` to make it
|
||||
capturable. Write metrics (`orm.update.*`, `iud.*`, `sql.update.*`, `sql.call.*`) have no bind
|
||||
capture and are intentionally excluded.
|
||||
@@ -1,797 +0,0 @@
|
||||
# Entity Bean Creation Guide for AI Agents
|
||||
|
||||
**Target Audience:** AI systems (Claude, Copilot, ChatGPT, etc.)
|
||||
**Purpose:** Learn how to generate clean, idiomatic Ebean entity beans
|
||||
**Key Insight:** Ebean entity fields must be non-public (no public fields). Accessors don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed
|
||||
**Language:** Java
|
||||
**Framework:** Ebean ORM
|
||||
|
||||
---
|
||||
|
||||
## Quick Rules
|
||||
|
||||
Before writing entity code, remember:
|
||||
|
||||
| Requirement | Needed? | Notes |
|
||||
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------|
|
||||
| `@Entity` annotation | ✅ **YES** | Marks class as persistent entity |
|
||||
| `@Id` annotation | ✅ **YES** | Marks primary key field |
|
||||
| Getters/setters (or other accessors) | ✅ **YES** | Needed for application code to access fields. Naming can be JavaBeans, fluent, or custom — no specific convention required. |
|
||||
| Default constructor | ❌ **NO** | Not required. Ebean can instantiate without it. |
|
||||
| equals/hashCode | ❌ **NO** | Ebean auto-enhances these at compile time. |
|
||||
| toString() | ❌ **NO** | Ebean auto-enhances this. Don't implement with getters. |
|
||||
| `@Version` | ⚠️ **OPTIONAL** | Use for optimistic locking. Highly recommended. |
|
||||
| `@WhenCreated` | ⚠️ **OPTIONAL** | Auto-timestamp creation time. Highly recommended. Use for audit trail. |
|
||||
| `@WhenModified` | ⚠️ **OPTIONAL** | Auto-timestamp modification time. Highly recommended. Use for audit trail. |
|
||||
|
||||
**Critical:**
|
||||
- Prefer primitive `long` for `@Id` and `@Version`, NOT `Long` object.
|
||||
- Fields should be non-public: **private**, **protected**, or package-private.
|
||||
- If you add accessors, they do NOT need to follow Java bean conventions.
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions: The D* (Domain) Prefix Pattern
|
||||
|
||||
Entity beans represent internal domain/persistence model details. It's a common best practice in Ebean projects to use the **D* prefix** (D for Domain) for entity class names.
|
||||
|
||||
**Why use D* prefix?**
|
||||
|
||||
1. **Avoid name clashes with DTOs** - Your public API may have `Customer` (DTO), but your entity is `DCustomer` (Domain). They're clearly different.
|
||||
2. **Signal intent clearly** - The D prefix immediately tells developers "this is an internal domain class, not part of the public API"
|
||||
3. **Clarify conversions** - When converting `DCustomer` → `Customer` (DTO), the direction is obvious
|
||||
4. **Separate concerns** - API classes in one package (no prefix), domain classes in another (with D prefix)
|
||||
|
||||
**Example naming pattern:**
|
||||
- Entity: `DCustomer`, `DOrder`, `DProduct`, `DInvoice`
|
||||
- DTO: `Customer`, `Order`, `Product`, `Invoice`
|
||||
- Converter: `DCustomerMapper.toDTO(DCustomer)` → `Customer`
|
||||
|
||||
**Where to place entities:**
|
||||
- Entities: `com.example.domain.entity` (or `persistence`)
|
||||
- DTOs: `com.example.api.model` or `com.example.dto`
|
||||
|
||||
**When to use D* prefix:**
|
||||
- ✅ **DO** use for entity beans (internal domain model)
|
||||
- ✅ **DO** use when you have parallel DTO classes with similar names
|
||||
- ❌ **DON'T** use for DTOs or public API classes
|
||||
- ❌ **DON'T** use if you have no DTOs and entities are your public API
|
||||
|
||||
Example with and without prefix:
|
||||
|
||||
```java
|
||||
// With D* prefix (recommended - allows both entity and DTO to exist)
|
||||
@Entity
|
||||
public class DCustomer {
|
||||
@Id private long id;
|
||||
private String name;
|
||||
// ... entity-specific fields and methods
|
||||
}
|
||||
|
||||
// Public API DTO (no D prefix)
|
||||
public record Customer(long id, String name) {
|
||||
// ... conversion method
|
||||
}
|
||||
|
||||
// Conversion
|
||||
public static Customer toDTO(DCustomer entity) {
|
||||
return new Customer(entity.getId(), entity.getName());
|
||||
}
|
||||
```
|
||||
|
||||
This naming convention is optional but highly recommended for projects with separate domain and API layers.
|
||||
|
||||
---
|
||||
|
||||
## Minimal Entity (No Boilerplate)
|
||||
|
||||
This is a complete, valid Ebean entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
- ✅ `@Entity` marks it as persistent
|
||||
- ✅ `@Id private long id` is the primary key
|
||||
- ✅ Private fields (Ebean does NOT support public fields without expert flags enabled)
|
||||
- ✅ Accessors can follow any naming convention, and can be omitted when field access is preferred
|
||||
- ✅ No default constructor needed
|
||||
- ✅ No equals/hashCode needed (Ebean enhances these)
|
||||
|
||||
**What Ebean does at compile time:**
|
||||
- Enhances equals/hashCode based on @Id
|
||||
- Adds field change tracking
|
||||
- Enables lazy loading
|
||||
- Enhances toString()
|
||||
|
||||
**Result:** Your entity is now fully functional with zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Basic Entity
|
||||
|
||||
**Use this when:** You need a simple persistent object.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Product {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private BigDecimal price;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(BigDecimal price) {
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- Primary key: `id` (private field, accessed via getter)
|
||||
- Three properties: `name`, `description`, `price` (private fields, accessed via getters/setters)
|
||||
- Automatic equals/hashCode based on id
|
||||
- Full ORM functionality
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: Entity with Audit Trail
|
||||
|
||||
**Use this when:** You need to track who/when created/modified data.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
@WhenModified
|
||||
private Instant modifiedAt;
|
||||
|
||||
private String orderNumber;
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getModifiedAt() {
|
||||
return modifiedAt;
|
||||
}
|
||||
|
||||
public String getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(String orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public void setTotalAmount(BigDecimal totalAmount) {
|
||||
this.totalAmount = totalAmount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- `version`: Optimistic locking (prevents concurrent update conflicts)
|
||||
- `createdAt`: Automatically set when inserted (Ebean manages this)
|
||||
- `modifiedAt`: Automatically updated on every modification (Ebean manages this)
|
||||
|
||||
**Example usage:**
|
||||
```java
|
||||
// Create
|
||||
Order order = new Order();
|
||||
order.setOrderNumber("ORD-001");
|
||||
order.setTotalAmount(new BigDecimal("99.99"));
|
||||
database.save(order); // createdAt is automatically set by Ebean
|
||||
|
||||
// Modify
|
||||
order.setTotalAmount(new BigDecimal("109.99"));
|
||||
database.update(order); // version incremented, modifiedAt updated automatically
|
||||
|
||||
// Check when modified
|
||||
System.out.println(order.getModifiedAt()); // Current timestamp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Entity with Constructor
|
||||
|
||||
**Use this when:** Domain logic requires initialization or validation.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Invoice {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
|
||||
private String invoiceNumber;
|
||||
private String customerName;
|
||||
private BigDecimal amount;
|
||||
|
||||
public Invoice(String invoiceNumber, String customerName, BigDecimal amount) {
|
||||
this.invoiceNumber = invoiceNumber;
|
||||
this.customerName = customerName;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getInvoiceNumber() {
|
||||
return invoiceNumber;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to add a constructor:**
|
||||
- ✅ Required fields must be set during creation
|
||||
- ✅ Validation needs to happen on initialization
|
||||
- ✅ Domain logic needs setup
|
||||
|
||||
**When NOT to add:**
|
||||
- ❌ If users will just set fields afterwards anyway
|
||||
- ❌ If there are many optional fields
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Entity with Relationships
|
||||
|
||||
**Use this when:** You need associations to other entities.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
@Version
|
||||
private long version;
|
||||
@WhenCreated
|
||||
private Instant createdAt;
|
||||
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // Use List, not Set
|
||||
|
||||
@ManyToOne
|
||||
private Address billingAddress;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public List<Order> getOrders() {
|
||||
return orders;
|
||||
}
|
||||
|
||||
public Address getBillingAddress() {
|
||||
return billingAddress;
|
||||
}
|
||||
|
||||
public void setBillingAddress(Address billingAddress) {
|
||||
this.billingAddress = billingAddress;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs)
|
||||
- `mappedBy` means Order.customer is the owner
|
||||
- Relationships are lazy-loaded by default
|
||||
|
||||
---
|
||||
|
||||
## What NOT to Do (Anti-Patterns)
|
||||
|
||||
### ❌ Anti-Pattern 1: Public Fields
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id public long id; // ❌ Public field - not supported
|
||||
public String name; // ❌ Public field - not supported
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Private field with getter
|
||||
private String name; // ✅ Private field with accessors
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean does NOT support public fields. Fields must be private and accessed via getters/setters or other accessor methods. Public fields bypass Ebean's tracking mechanisms and will cause data consistency issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 2: Use Long Object Instead of Primitive
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private Long id; // ❌ Object type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id; // ✅ Primitive type
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Performance, nullability semantics, Ebean optimization.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 3: Implement equals/hashCode
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) { // ❌ Unnecessary
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Customer customer = (Customer) o;
|
||||
return id == customer.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { // ❌ Unnecessary
|
||||
return Objects.hash(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
// Ebean enhances equals/hashCode automatically
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's enhancement is optimized for ORM operations. Your implementation might conflict with Ebean's tracking.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 4: Use Set for Collections
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private Set<Order> orders; // ❌ Set calls equals/hashCode before IDs assigned
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@OneToMany(mappedBy = "customer")
|
||||
private List<Order> orders; // ✅ List doesn't require equals/hashCode on unsaved beans
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Set calls equals/hashCode immediately. New beans don't have IDs yet, causing issues.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 5: toString() with Getters
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
@Override
|
||||
public String toString() { // ❌ Uses getters
|
||||
return "Customer{" +
|
||||
"id=" + getId() +
|
||||
", name='" + getName() + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** In a debugger, toString() is called automatically. Getters can trigger lazy loading, changing debug behavior.
|
||||
|
||||
**DO:** Either don't implement toString(), or access fields directly:
|
||||
```java
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Customer{" +
|
||||
"id=" + id +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Anti-Pattern 6: @Column(name=...) for Naming Convention
|
||||
|
||||
**DON'T:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
@Column(name = "first_name") // ❌ Unnecessary
|
||||
private String firstName;
|
||||
}
|
||||
```
|
||||
|
||||
**DO:**
|
||||
```java
|
||||
@Entity
|
||||
public class Customer {
|
||||
@Id
|
||||
private long id;
|
||||
|
||||
private String firstName; // ✅ Ebean uses naming convention: first_name
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
|
||||
|
||||
---
|
||||
|
||||
## What Ebean Enhancement Provides
|
||||
|
||||
At compile time, Ebean enhances your entity classes:
|
||||
|
||||
1. **equals/hashCode** - Based on @Id, optimal for ORM
|
||||
2. **Field change tracking** - Knows which fields were modified
|
||||
3. **Lazy loading** - Collections and relationships load on demand
|
||||
4. **Persistence context** - Manages identity and state
|
||||
5. **toString()** - Auto-implemented (don't override with getters)
|
||||
|
||||
**Result:** Your entity bean is minimal, but fully featured.
|
||||
|
||||
---
|
||||
|
||||
## Field Types
|
||||
|
||||
**Recommended for ID/Version:**
|
||||
- `long` (primitive) ✅ Use this
|
||||
- `int` (primitive) ✅ Use this
|
||||
- `UUID` ✅ Use this
|
||||
|
||||
**Not recommended:**
|
||||
- `Long` object ⚠️ Avoid (use primitive long)
|
||||
- `Integer` object ⚠️ Avoid (use primitive int)
|
||||
|
||||
**For other fields:**
|
||||
- Use standard Java types: `String`, `BigDecimal`, `Instant`, `LocalDate`, etc.
|
||||
- Use primitives where nullable semantics don't apply: `int`, `long`, `boolean`
|
||||
- Use objects where null has meaning: `String`, `BigDecimal`, `LocalDate`
|
||||
|
||||
---
|
||||
|
||||
## Example: Building an Entity Step by Step
|
||||
|
||||
Start minimal, add what you need:
|
||||
|
||||
**Step 1: Minimal**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Add audit trail**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Add author relationship**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Add constructor if needed**
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@Id long id;
|
||||
@Version long version;
|
||||
@WhenCreated Instant createdAt;
|
||||
@WhenModified Instant modifiedAt;
|
||||
|
||||
String title;
|
||||
String content;
|
||||
|
||||
@ManyToOne
|
||||
Author author;
|
||||
|
||||
public BlogPost(String title, String content, Author author) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVersion() { return version; }
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public Instant getModifiedAt() { return modifiedAt; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public Author getAuthor() { return author; }
|
||||
public void setAuthor(Author author) { this.author = author; }
|
||||
}
|
||||
```
|
||||
|
||||
Each step adds only what's necessary for the entity structure. Accessors are always included so application code can use the entity.
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating and saving
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Alice");
|
||||
database.save(customer); // id auto-generated
|
||||
```
|
||||
|
||||
### Finding
|
||||
```java
|
||||
Customer found = database.find(Customer.class, 1);
|
||||
System.out.println(found.getName());
|
||||
```
|
||||
|
||||
### Updating
|
||||
```java
|
||||
found.setName("Bob");
|
||||
database.update(found); // version auto-incremented
|
||||
```
|
||||
|
||||
### Collections (relationships)
|
||||
```java
|
||||
Customer customer = database.find(Customer.class, 1);
|
||||
List<Order> orders = customer.getOrders(); // Lazy loads automatically
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary for AI Agents
|
||||
|
||||
When generating Ebean entity beans:
|
||||
|
||||
✅ **DO:**
|
||||
- Use primitive `long` for @Id and @Version
|
||||
- Keep entities minimal (just fields + @Entity + @Id)
|
||||
- Use @Version for concurrency control
|
||||
- Use @WhenCreated/@WhenModified for audit trail
|
||||
- Use List for collections, not Set
|
||||
- Add constructors only if domain logic requires it
|
||||
- Add getters/setters for all fields that application code needs to read or write
|
||||
|
||||
❌ **DON'T:**
|
||||
- Use Long object for @Id/@Version
|
||||
- Implement equals/hashCode
|
||||
- Implement toString() with getters
|
||||
- Use Set for @OneToMany/@ManyToMany
|
||||
- Add unnecessary @Column annotations
|
||||
- Add default constructors "just in case"
|
||||
|
||||
**Result:** Clean, readable, maintainable entity beans with full ORM functionality and zero boilerplate.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Entity Bean Best Practices: `/docs/best-practice/`
|
||||
- JPA Mapping Reference: `/docs/mapping/jpa/`
|
||||
- Ebean Extensions: `/docs/mapping/extensions/`
|
||||
- First Entity Guide: `/docs/intro/first-entity/`
|
||||
@@ -1,136 +0,0 @@
|
||||
# Immutable bean cache for read-only references
|
||||
|
||||
This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
|
||||
references (for example `Label` references reused across many entities).
|
||||
|
||||
Use this when you want:
|
||||
|
||||
- fewer lazy-load SQL calls for assoc-one references via caching
|
||||
- reusable fetch-group-based loading for cache misses
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Build an immutable cache (typical via builder)
|
||||
|
||||
```java
|
||||
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
|
||||
.loading(database, fetchGroup)
|
||||
.maxSize(10_000)
|
||||
.maxIdleSeconds(300)
|
||||
.maxSecondsToLive(6_000)
|
||||
.build();
|
||||
```
|
||||
|
||||
`loading(...)` uses the query shape:
|
||||
|
||||
- `select(fetchGroup)`
|
||||
- `setUnmodifiable(true)`
|
||||
- `where().idIn(ids)`
|
||||
- `findMap()`
|
||||
|
||||
Alternative domain example:
|
||||
|
||||
```java
|
||||
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
.select("name,version")
|
||||
.fetch("billingAddress", "line1,city")
|
||||
.fetch("shippingAddress", "line1,city")
|
||||
.build();
|
||||
|
||||
ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
.loading(database, customerGroup)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Attach cache to the root query
|
||||
|
||||
```java
|
||||
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
|
||||
.setId(id)
|
||||
.setUnmodifiable(true)
|
||||
.using(labelCache)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
`using(...)` is on the root query. Ebean will use this cache for matching
|
||||
bean types when resolving references.
|
||||
|
||||
---
|
||||
|
||||
## Use loading helper for simple memoization
|
||||
|
||||
If you don't need policy controls, use the shorthand helper:
|
||||
|
||||
```java
|
||||
ImmutableBeanCache<Label> labelCache =
|
||||
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
|
||||
```
|
||||
|
||||
With `ebean-core` on the classpath, builder policy settings are backed by core
|
||||
cache implementation (including periodic trim / eviction).
|
||||
|
||||
---
|
||||
|
||||
## Unmodifiable vs mutable query behavior
|
||||
|
||||
### Unmodifiable query path
|
||||
|
||||
`setUnmodifiable(true)` disables lazy loading. If you need association content
|
||||
in cached beans make sure that is included in the fetch group.
|
||||
|
||||
```java
|
||||
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
|
||||
.select("version")
|
||||
.fetch("labelTexts", "locale, localeText")
|
||||
.build();
|
||||
```
|
||||
|
||||
### Mutable query path
|
||||
|
||||
On a mutable query (no `setUnmodifiable(true)`), references populated from the
|
||||
immutable cache are still mutable beans in that object graph. Additional
|
||||
unloaded properties can still lazy load as normal.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
1. cache serves already-loaded reference properties (for example `version`)
|
||||
2. later access to unloaded properties (for example `labelTexts`) triggers
|
||||
normal lazy loading
|
||||
|
||||
---
|
||||
|
||||
## Understand secondary query behavior (`+query`, `+lazy`)
|
||||
|
||||
When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
|
||||
the immutable caches configured on the root query are propagated to those
|
||||
secondary queries.
|
||||
|
||||
That means assoc-one references resolved in secondary query paths can still hit
|
||||
the immutable cache.
|
||||
|
||||
---
|
||||
|
||||
## Operational note (TTL / max size)
|
||||
|
||||
Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
|
||||
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
|
||||
loader-based memoization.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
1. Hit / partial hit / miss behavior for `getAll(ids)`
|
||||
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
|
||||
3. Mutable path: additional unloaded properties can still lazy load
|
||||
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
|
||||
5. If needed associations are in fetch group, assert no extra SQL for those
|
||||
accesses
|
||||
@@ -1,206 +0,0 @@
|
||||
# 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 `LazyInitialisationException` 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,91 +0,0 @@
|
||||
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
|
||||
avaje-json-core APIs.
|
||||
|
||||
Use this when upgrading code that references:
|
||||
|
||||
- `com.fasterxml.jackson.core.JsonParser`
|
||||
- `com.fasterxml.jackson.core.JsonGenerator`
|
||||
- `com.fasterxml.jackson.core.JsonFactory`
|
||||
|
||||
The replacement types are:
|
||||
|
||||
- `io.avaje.json.JsonReader`
|
||||
- `io.avaje.json.JsonWriter`
|
||||
- `io.avaje.json.stream.JsonStream`
|
||||
|
||||
---
|
||||
|
||||
## Breaking API changes
|
||||
|
||||
| Previous API | New API |
|
||||
|---|---|
|
||||
| `JsonParser` | `JsonReader` |
|
||||
| `JsonGenerator` | `JsonWriter` |
|
||||
| `JsonFactory` | `JsonStream` |
|
||||
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
|
||||
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
|
||||
|
||||
---
|
||||
|
||||
## Typical migration rewrites
|
||||
|
||||
### Parser and generator signatures
|
||||
|
||||
```java
|
||||
// before
|
||||
void read(JsonParser parser)
|
||||
void write(JsonGenerator generator)
|
||||
|
||||
// after
|
||||
void read(JsonReader parser)
|
||||
void write(JsonWriter generator)
|
||||
```
|
||||
|
||||
### Database configuration
|
||||
|
||||
```java
|
||||
// before
|
||||
Database.builder().jsonFactory(factory)
|
||||
|
||||
// after
|
||||
Database.builder().jsonStream(stream)
|
||||
```
|
||||
|
||||
### JSON utility calls
|
||||
|
||||
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
|
||||
If your code was calling those APIs with Jackson core types, switch to avaje types.
|
||||
|
||||
---
|
||||
|
||||
## Dependency and module notes
|
||||
|
||||
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
|
||||
parsing/writing.
|
||||
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
|
||||
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
|
||||
integrations.
|
||||
|
||||
---
|
||||
|
||||
## Behavior notes to verify during upgrade
|
||||
|
||||
1. Parser token handling is now based on avaje `JsonReader.Token`.
|
||||
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
|
||||
be validated in your tests if you previously depended on Jackson token quirks.
|
||||
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
|
||||
keep ObjectMapper wiring enabled.
|
||||
|
||||
---
|
||||
|
||||
## Validation checklist
|
||||
|
||||
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
|
||||
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
|
||||
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
|
||||
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide shows how to migrate legacy programmatic database creation code from:
|
||||
|
||||
- `new DatabaseConfig()`
|
||||
- `DatabaseFactory.create(...)`
|
||||
- old `setXxx(...)` builder-style configuration methods
|
||||
|
||||
…to the preferred builder-based style using:
|
||||
|
||||
- `Database.builder()`
|
||||
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
|
||||
- `DatabaseBuilder.build()`
|
||||
|
||||
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
|
||||
|
||||
---
|
||||
|
||||
## Preferred pattern
|
||||
|
||||
Prefer code shaped like this:
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.dataSourceBuilder(dataSource)
|
||||
.register(true)
|
||||
.defaultDatabase(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
The important points are:
|
||||
|
||||
1. Start with `Database.builder()`
|
||||
2. Configure via `DatabaseBuilder`
|
||||
3. Finish with `.build()`
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
|
||||
- If existing code only uses standard builder methods, this change is usually mechanical.
|
||||
- If existing code later reads configuration back, use `config.settings()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.loadFromProperties();
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
DatabaseBuilder config = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties();
|
||||
Database database = config.build();
|
||||
```
|
||||
|
||||
### Short form
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Replace `DatabaseFactory.create("name")`
|
||||
|
||||
### Before
|
||||
|
||||
```java
|
||||
Database database = DatabaseFactory.create("other");
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("other")
|
||||
.loadFromProperties()
|
||||
.build();
|
||||
```
|
||||
|
||||
### Important
|
||||
|
||||
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
|
||||
|
||||
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
|
||||
Use those names when migrating older setup code.
|
||||
|
||||
| Legacy call | Preferred call |
|
||||
|---|---|
|
||||
| `setName("db")` | `name("db")` |
|
||||
| `setRegister(false)` | `register(false)` |
|
||||
| `setDefaultServer(false)` | `defaultDatabase(false)` |
|
||||
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
|
||||
| `setDbSchema("app")` | `dbSchema("app")` |
|
||||
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
|
||||
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
|
||||
| `setRunMigration(true)` | `runMigration(true)` |
|
||||
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
|
||||
| `setPersistBatch(batch)` | `persistBatch(batch)` |
|
||||
|
||||
### Full example
|
||||
|
||||
#### Before
|
||||
|
||||
```java
|
||||
DatabaseConfig config = new DatabaseConfig();
|
||||
config.setName("db");
|
||||
config.setRegister(false);
|
||||
config.setDefaultServer(false);
|
||||
config.setDataSourceConfig(dataSource);
|
||||
Database database = DatabaseFactory.create(config);
|
||||
```
|
||||
|
||||
#### After
|
||||
|
||||
```java
|
||||
Database database = Database.builder()
|
||||
.name("db")
|
||||
.register(false)
|
||||
.defaultDatabase(false)
|
||||
.dataSourceBuilder(dataSource)
|
||||
.build();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Verify semantics after migration
|
||||
|
||||
The migration should preserve behavior, but verify these points:
|
||||
|
||||
- `register(true)` is still the default
|
||||
- `defaultDatabase(true)` is still the default
|
||||
- call `loadFromProperties()` if the old code loaded configuration from properties
|
||||
- for named databases, set the name before loading properties
|
||||
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
|
||||
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
|
||||
|
||||
---
|
||||
|
||||
## Manual-review cases
|
||||
|
||||
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
|
||||
|
||||
### `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
|
||||
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
|
||||
|
||||
### `DatabaseFactory.initialiseContainer(...)`
|
||||
|
||||
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
|
||||
|
||||
### `DatabaseFactory.shutdown()`
|
||||
|
||||
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
|
||||
|
||||
### Variables or method signatures typed as `DatabaseConfig`
|
||||
|
||||
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
|
||||
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
|
||||
|
||||
### Code that needs read access to builder settings
|
||||
|
||||
Use:
|
||||
|
||||
```java
|
||||
DatabaseBuilder builder = Database.builder();
|
||||
DatabaseBuilder.Settings settings = builder.settings();
|
||||
```
|
||||
|
||||
rather than relying on the concrete `DatabaseConfig` type only to read getters.
|
||||
|
||||
---
|
||||
|
||||
## Automation notes for AI agents and bulk refactors
|
||||
|
||||
This migration is a good candidate for semi-automated upgrading.
|
||||
|
||||
### Safe mechanical rewrites
|
||||
|
||||
These are usually safe to rewrite automatically:
|
||||
|
||||
- `new DatabaseConfig()` → `Database.builder()`
|
||||
- `DatabaseFactory.create(builder)` → `builder.build()`
|
||||
- `DatabaseFactory.create("name")` → `Database.builder().name("name").loadFromProperties().build()`
|
||||
- legacy `setXxx(...)` calls → preferred fluent builder methods
|
||||
|
||||
### Flag for manual review
|
||||
|
||||
Automatically flag, but do not blindly rewrite:
|
||||
|
||||
- `DatabaseFactory.createWithContextClassLoader(...)`
|
||||
- `DatabaseFactory.initialiseContainer(...)`
|
||||
- `DatabaseFactory.shutdown()`
|
||||
- parameters, fields, or return types declared as `DatabaseConfig`
|
||||
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
|
||||
|
||||
---
|
||||
|
||||
## Related guides
|
||||
|
||||
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
|
||||
- [Guide index](README.md) — full list of Ebean setup and migration guides
|
||||
@@ -1,447 +0,0 @@
|
||||
# Guide: Persist Changes and Manage Transactions with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to save,
|
||||
update, delete, and batch changes with Ebean while choosing the correct
|
||||
transaction boundary.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- create a new entity row
|
||||
- update one or more existing rows
|
||||
- delete rows safely
|
||||
- decide between implicit transactions, `@Transactional`, and explicit
|
||||
transactions
|
||||
- batch or bulk-write many rows efficiently
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Choose the correct persistence operation first
|
||||
2. Use implicit transactions for a single isolated write
|
||||
3. Use `@Transactional` for multi-step application workflows
|
||||
4. Use explicit transactions only when you need explicit control
|
||||
5. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Entity beans and database configuration already exist
|
||||
- You know which `Database` is being used (`DB.getDefault()` or a named database)
|
||||
|
||||
If the project is not yet configured, first follow:
|
||||
|
||||
- [`add-ebean-postgres-database-config.md`](add-ebean-postgres-database-config.md)
|
||||
- [`entity-bean-creation.md`](entity-bean-creation.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Choose the correct persistence operation before editing code
|
||||
|
||||
Do not start with `database.save(...)` by habit. First decide what kind of change the
|
||||
caller is making.
|
||||
|
||||
| Need | Preferred API | Use when |
|
||||
|------|---------------|----------|
|
||||
| Insert a bean that is definitely new | `database.insert(bean)` | New-create flow, seed data, fixture setup |
|
||||
| Save a bean that may be new or existing | `database.save(bean)` | Common default when bean state determines insert vs update |
|
||||
| Update a bean that is definitely existing | `database.update(bean)` | Existing row should be updated only |
|
||||
| Delete one bean | `database.delete(bean)` | Remove a loaded entity bean |
|
||||
| Update many rows without loading beans | `database.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
|
||||
| Delete many rows without loading beans | bulk update/delete API or `database.sqlUpdate(...)` | Set-based deletion |
|
||||
|
||||
### Agent rule
|
||||
|
||||
Choose the operation that matches intent:
|
||||
|
||||
- known new row -> `insert`
|
||||
- known existing row -> `update`
|
||||
- uncertain/new-or-existing -> `save`
|
||||
- many rows -> bulk update/delete, not a loop of individual saves
|
||||
|
||||
### Style note
|
||||
|
||||
Use a `Database` instance for all persistence operations: `database.save(bean)`,
|
||||
`database.insert(bean)`, `database.update(bean)`, `database.delete(bean)`.
|
||||
Inject the `Database` bean or obtain it via `DB.getDefault()`. Avoid using the
|
||||
static `DB.*` convenience methods.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Persist single-bean changes with the correct API
|
||||
|
||||
### Example - insert a known new bean
|
||||
|
||||
```java
|
||||
Customer customer = new Customer();
|
||||
customer.setName("Rob");
|
||||
customer.setEmail("rob@example.com");
|
||||
|
||||
database.insert(customer);
|
||||
```
|
||||
|
||||
### Example - update an existing bean
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.ACTIVE);
|
||||
|
||||
database.update(customer);
|
||||
```
|
||||
|
||||
### When to prefer `insert()` over `save()`
|
||||
|
||||
Use `insert()` when the code is creating a brand new row and should fail if the
|
||||
operation does not behave like an insert.
|
||||
|
||||
### When to prefer `update()` over `save()`
|
||||
|
||||
Use `update()` when the bean is definitely existing and the method should not
|
||||
silently behave like an insert.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Check cascade mappings before assuming related beans will persist or delete
|
||||
|
||||
Ebean follows cascade rules defined on mapping annotations such as
|
||||
`@OneToMany`, `@OneToOne`, `@ManyToOne`, and `@ManyToMany`.
|
||||
|
||||
The default is **no cascade**.
|
||||
|
||||
### Example
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
|
||||
@ManyToOne
|
||||
private Customer customer; // no cascade by default
|
||||
|
||||
@OneToMany(cascade = CascadeType.ALL)
|
||||
private List<OrderDetail> details; // save + delete cascade
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
database.save(order);
|
||||
```
|
||||
|
||||
With the mapping above:
|
||||
|
||||
- `details` are cascaded
|
||||
- `customer` is **not** cascaded
|
||||
|
||||
### Agent rules for cascades
|
||||
|
||||
1. Inspect the mapping before writing save/delete logic
|
||||
2. Do not assume `@ManyToOne` cascades
|
||||
3. Avoid adding cascade to shared parent references unless ownership is truly
|
||||
intended
|
||||
4. If a relationship should not cascade, save/delete related beans explicitly
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Let Ebean use an implicit transaction for a single isolated write
|
||||
|
||||
If the method performs one isolated persistence operation, Ebean can manage the
|
||||
transaction implicitly.
|
||||
|
||||
### Good fit for implicit transaction
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.id.equalTo(customerId)
|
||||
.findOne();
|
||||
|
||||
customer.setStatus(Customer.Status.INACTIVE);
|
||||
database.save(customer);
|
||||
```
|
||||
|
||||
### Good fit
|
||||
|
||||
- one save
|
||||
- one update
|
||||
- one delete
|
||||
- small helper method with a single write
|
||||
|
||||
### Poor fit
|
||||
|
||||
- multiple writes that must commit or roll back together
|
||||
- query + save + save workflow
|
||||
- any method where later failure must roll back earlier writes
|
||||
|
||||
### Important
|
||||
|
||||
Queries also use implicit transactions when needed. You generally do **not**
|
||||
need to wrap ordinary read queries in an explicit transaction "just in case".
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Use `@Transactional` for multi-step service workflows
|
||||
|
||||
When multiple Ebean operations belong to one unit of work, use
|
||||
`@Transactional`.
|
||||
|
||||
### Example - service method
|
||||
|
||||
```java
|
||||
import io.ebean.annotation.Transactional;
|
||||
|
||||
@Transactional
|
||||
public void shipOrder(long orderId) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.setStatus(Order.Status.SHIPPED);
|
||||
database.save(order);
|
||||
|
||||
Shipment shipment = new Shipment(order, Instant.now());
|
||||
database.insert(shipment);
|
||||
}
|
||||
```
|
||||
|
||||
All database work inside the method runs in one transaction and commits only if
|
||||
the method completes successfully.
|
||||
|
||||
### Use `Transaction.current()` only when needed
|
||||
|
||||
If the method needs access to the current transaction itself:
|
||||
|
||||
```java
|
||||
Transaction txn = Transaction.current();
|
||||
```
|
||||
|
||||
Do this only for transaction-specific behavior such as comments, savepoints, or
|
||||
other advanced control. Do not fetch the current transaction if the method does
|
||||
not need it.
|
||||
|
||||
### Agent rules for `@Transactional`
|
||||
|
||||
1. Put it on application/service workflow methods, not everywhere by default
|
||||
2. Keep the transaction focused on database work
|
||||
3. Avoid remote HTTP calls, message publishing, or long-running CPU work inside
|
||||
the transaction if those can be moved outside
|
||||
|
||||
### Named database note
|
||||
|
||||
If the method uses a non-default database, obtain that `Database` instance via
|
||||
`DB.byName("...")` and consistently use that database for both queries and
|
||||
writes.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `beginTransaction()` when you need explicit control
|
||||
|
||||
Use an explicit transaction when you need manual `commit()`, batching, explicit
|
||||
flush, savepoints, or other low-level transaction control.
|
||||
|
||||
### Example - explicit transaction with try-with-resources
|
||||
|
||||
```java
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
|
||||
Order order = new QOrder()
|
||||
.id.equalTo(orderId)
|
||||
.findOne();
|
||||
|
||||
order.cancel();
|
||||
database.save(order);
|
||||
|
||||
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
|
||||
database.insert(auditLog);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
If `commit()` is not reached, closing the transaction rolls it back.
|
||||
|
||||
### Useful explicit controls
|
||||
|
||||
- `txn.commit()` - commit current work
|
||||
- `txn.setRollbackOnly()` - force rollback-only behavior
|
||||
- `txn.flush()` - push batched statements to the database now
|
||||
|
||||
### Agent rule
|
||||
|
||||
Prefer `@Transactional` unless explicit transaction control is actually needed.
|
||||
Do not use `beginTransaction()` only because it feels "safer".
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `createTransaction()` only for non-thread-local transaction handling
|
||||
|
||||
`createTransaction()` creates a transaction that is **not** placed into the
|
||||
thread-local scope. This is a specialized tool.
|
||||
|
||||
Use it when:
|
||||
|
||||
- the transaction will be passed explicitly
|
||||
- you need more than one transaction in the same thread
|
||||
- you are coordinating work across threads or lower-level APIs
|
||||
|
||||
### Example - explicit transaction passed to query and save
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.createTransaction()) {
|
||||
|
||||
Customer customer = new QCustomer(txn)
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
|
||||
customer.setInactive(true);
|
||||
database.save(customer, txn);
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are not deliberately bypassing thread-local transaction scope, do **not**
|
||||
use `createTransaction()`. Most service code should use `@Transactional` or
|
||||
`beginTransaction()`.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use bulk update/delete or JDBC batch for many-row writes
|
||||
|
||||
Loops of `database.save(...)` are often the wrong tool for large write sets.
|
||||
|
||||
### Prefer bulk update for set-based changes
|
||||
|
||||
If the update can be expressed as "change all rows matching this predicate",
|
||||
perform one bulk update instead of loading and saving each bean.
|
||||
|
||||
### Example - bulk update with query beans
|
||||
|
||||
```java
|
||||
var cust = QCustomer.alias();
|
||||
|
||||
int rows = new QCustomer()
|
||||
.status.equalTo(Customer.Status.NEW)
|
||||
.asUpdate()
|
||||
.set(cust.status, Customer.Status.ACTIVE)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Example - bulk update with `database.update(...)`
|
||||
|
||||
```java
|
||||
int rows = database.update(Customer.class)
|
||||
.set("status", Customer.Status.ACTIVE)
|
||||
.where()
|
||||
.eq("status", Customer.Status.NEW)
|
||||
.update();
|
||||
```
|
||||
|
||||
### Prefer JDBC batch for many individual inserts/updates
|
||||
|
||||
If each row has different values and must still go through per-bean persistence,
|
||||
use batching.
|
||||
|
||||
```java
|
||||
Database database = DB.getDefault();
|
||||
|
||||
try (Transaction txn = database.beginTransaction()) {
|
||||
txn.setBatchMode(true);
|
||||
txn.setBatchSize(100);
|
||||
txn.setGetGeneratedKeys(false);
|
||||
|
||||
for (Customer customer : customersToInsert) {
|
||||
database.insert(customer, txn);
|
||||
}
|
||||
|
||||
txn.commit();
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative - annotation-driven batching
|
||||
|
||||
```java
|
||||
@Transactional(batchSize = 50)
|
||||
public void importCustomers(List<Customer> customers) {
|
||||
for (Customer customer : customers) {
|
||||
database.insert(customer);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Batch caveats
|
||||
|
||||
- Executing a query inside a batched transaction can flush the batch
|
||||
- Mixing bean persistence and `SqlUpdate` can also flush the batch
|
||||
- Accessing generated/unloaded properties on batched beans can flush the batch
|
||||
|
||||
If the workflow depends on delayed flushing, review the batch-flush rules before
|
||||
adding more queries inside the same transaction.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Saving many rows one by one without batch or bulk update
|
||||
|
||||
If you are changing hundreds or thousands of rows, first ask whether it should
|
||||
be a bulk update or a batched transaction.
|
||||
|
||||
### Anti-pattern 2 - Assuming child beans cascade automatically
|
||||
|
||||
Cascade is not automatic. Inspect the mapping first.
|
||||
|
||||
### Anti-pattern 3 - Wrapping external calls inside the database transaction
|
||||
|
||||
Do not keep transactions open while waiting on HTTP calls, queues, or other
|
||||
slow external systems unless the design genuinely requires it.
|
||||
|
||||
### Anti-pattern 4 - Using `createTransaction()` for ordinary service code
|
||||
|
||||
Most service code should not bypass thread-local transaction handling.
|
||||
|
||||
### Anti-pattern 5 - Using `save()` when you really need `insert()` or `update()`
|
||||
|
||||
If operation intent matters, choose the more specific API.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| Child beans were not saved or deleted | Missing cascade mapping | Inspect annotations and add explicit save/delete or the correct cascade |
|
||||
| Earlier writes committed even though later work failed | The whole workflow was not inside one transaction | Wrap the unit of work in `@Transactional` or an explicit transaction |
|
||||
| `OptimisticLockException` on update/delete | Concurrent modification or stale version | Re-fetch, merge, or handle concurrency explicitly |
|
||||
| Batch writes flush earlier than expected | Query, mixed SQL, or property access triggered flush | Review batch flush rules and transaction flow |
|
||||
| Explicit transaction example does not affect the expected database | Mixed default DB and named DB usage | Use the same `Database` instance consistently for query and write |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add persistence logic:
|
||||
|
||||
1. Choose `insert`, `save`, `update`, `delete`, or bulk update based on intent
|
||||
2. Inspect cascade mappings before assuming related beans will persist/delete
|
||||
3. Use implicit transactions for one isolated write
|
||||
4. Use `@Transactional` for multi-step units of work
|
||||
5. Use `beginTransaction()` only when explicit transaction control is needed
|
||||
6. Use `createTransaction()` only for explicit, non-thread-local handling
|
||||
7. Use bulk update or batching for large write sets
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Testing with TestEntityBuilder](testing-with-testentitybuilder.md)
|
||||
- [Ebean persist docs](https://ebean.io/docs/persist)
|
||||
- [Ebean transaction docs](https://ebean.io/docs/transactions)
|
||||
@@ -1,817 +0,0 @@
|
||||
# Guide: Testing with TestEntityBuilder
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
|
||||
|
||||
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
|
||||
|
||||
- **Integration tests** that need representative data without caring about specific values
|
||||
- **Persistence layer tests** that verify save/update/delete operations work correctly
|
||||
- **Query and filter tests** where you need multiple entities with varied data
|
||||
- **Rapid test setup** that reduces test code verbosity and improves readability
|
||||
|
||||
---
|
||||
|
||||
## Setup & Dependencies
|
||||
|
||||
### Add ebean-test to Your Project
|
||||
|
||||
The `TestEntityBuilder` class is provided by the `ebean-test` module.
|
||||
|
||||
**Maven:**
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.ebean</groupId>
|
||||
<artifactId>ebean-test</artifactId>
|
||||
<version>${ebean.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle:**
|
||||
```gradle
|
||||
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
|
||||
```
|
||||
|
||||
Use a version that matches your Ebean runtime (`ebean.version` /
|
||||
`ebeanVersion`), or replace with an explicit fixed version if your build does
|
||||
not centralize dependency versions.
|
||||
|
||||
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
|
||||
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
|
||||
> runtime and test versions are not supported.
|
||||
|
||||
### Import the Class
|
||||
|
||||
```java
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Create a Builder Instance
|
||||
|
||||
`TestEntityBuilder` uses a builder pattern for configuration:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
|
||||
```
|
||||
|
||||
The `Database` parameter specifies which Ebean database instance to use for entity type
|
||||
lookups and persistence operations. Pass the injected `Database` bean (see
|
||||
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
|
||||
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
|
||||
use the injected `database` bean for **all** persistence operations in your tests
|
||||
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
|
||||
|
||||
### Build an Entity (In-Memory)
|
||||
|
||||
The `build()` method creates an instance with populated fields **without persisting to the database:**
|
||||
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
|
||||
// Fields are populated:
|
||||
// - id: unset (typically 0 for primitive long, null for boxed Long)
|
||||
// - name: random UUID-based string
|
||||
// - price: random BigDecimal
|
||||
// - inStock: true
|
||||
// - createdAt: current instant
|
||||
// - etc.
|
||||
|
||||
// Not persisted yet (`@Id` is still unset until the entity is persisted).
|
||||
```
|
||||
|
||||
### Build and Save (Persist to Database)
|
||||
|
||||
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
|
||||
|
||||
```java
|
||||
Product product = builder.save(Product.class);
|
||||
|
||||
// Entity is now in the database:
|
||||
assert database.find(Product.class, product.getId()) != null;
|
||||
```
|
||||
|
||||
### Save Multiple Entities
|
||||
|
||||
The `saveAll()` method persists multiple pre-built entities in a single call:
|
||||
|
||||
```java
|
||||
Product p1 = builder.build(Product.class);
|
||||
Product p2 = builder.build(Product.class);
|
||||
builder.saveAll(p1, p2);
|
||||
|
||||
// Both are now in the database with assigned IDs:
|
||||
assert p1.getId() != null;
|
||||
assert p2.getId() != null;
|
||||
```
|
||||
|
||||
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
|
||||
`Database` reference in tests that already hold a `TestEntityBuilder`.
|
||||
|
||||
### Access the Underlying Database
|
||||
|
||||
The `database()` method returns the `Database` instance used internally by the builder.
|
||||
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
|
||||
still need to perform `find()`, `delete()`, or other database operations:
|
||||
|
||||
```java
|
||||
Product saved = builder.save(Product.class);
|
||||
|
||||
// Use builder.database() instead of injecting a separate Database bean:
|
||||
Product found = builder.database().find(Product.class, saved.getId());
|
||||
assert found != null;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using with Dependency Injection
|
||||
|
||||
Most applications using Ebean also use a DI framework. The recommended pattern is to
|
||||
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
|
||||
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
|
||||
|
||||
### Avaje Inject — `@TestScope @Factory`
|
||||
|
||||
Add a `@Bean` method to your test-scoped `@Factory` class:
|
||||
|
||||
```java
|
||||
import io.ebean.Database;
|
||||
import io.ebean.test.ContainerDatabase;
|
||||
import io.avaje.inject.Bean;
|
||||
import io.avaje.inject.Factory;
|
||||
import io.avaje.inject.test.TestScope;
|
||||
import io.ebean.test.TestEntityBuilder;
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes using `@InjectTest`:
|
||||
|
||||
```java
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject Database database;
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both patterns produce a single shared `TestEntityBuilder` instance, wired
|
||||
from the managed `Database` bean — no `@BeforeEach` required.
|
||||
|
||||
### Spring Boot — `@TestConfiguration`
|
||||
|
||||
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
PostgresContainer postgres() {
|
||||
return PostgresContainer.builder("17") // Postgres image version
|
||||
.dbName("my_app") // database to create inside the container
|
||||
.build()
|
||||
.start();
|
||||
}
|
||||
|
||||
// use @Primary if your main application context also wires a Database bean
|
||||
// or conditionally wire the main Database bean to exclude it from tests
|
||||
@Primary
|
||||
@Bean
|
||||
Database database(PostgresContainer container) {
|
||||
return container.ebean()
|
||||
.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then inject it directly into test classes:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired Database database;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void findByStatus() {
|
||||
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type-Specific Value Generation
|
||||
|
||||
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
|
||||
|
||||
| Type | Generated Value | Notes |
|
||||
|------|-----------------|-------|
|
||||
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
|
||||
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
|
||||
| `Integer` / `int` | Random in `[1, 1_000)` | |
|
||||
| `Long` / `long` | Random in `[1, 100_000)` | |
|
||||
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
|
||||
| `Double` / `double` | Random in `[1, 100)` | |
|
||||
| `Float` / `float` | Random in `[1, 100)` | |
|
||||
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
|
||||
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
|
||||
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
|
||||
| `LocalDate` | Today's date | Via `LocalDate.now()` |
|
||||
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
|
||||
| `Instant` | Current instant | Via `Instant.now()` |
|
||||
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
|
||||
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
|
||||
| `Enum` | First constant | Override in custom generator if needed |
|
||||
| Other types | `null` | Set these fields manually in tests |
|
||||
|
||||
### String Length Constraints
|
||||
|
||||
`TestEntityBuilder` respects column length constraints defined in the entity:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Column(length = 50)
|
||||
private String username;
|
||||
}
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getUsername().length() <= 50; // ✅ Constraint respected
|
||||
```
|
||||
|
||||
### BigDecimal Precision and Scale
|
||||
|
||||
For `BigDecimal` fields, the builder respects the database column precision and scale:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class LineItem {
|
||||
@Column(precision = 10, scale = 2) // max 99_999_999.99
|
||||
private BigDecimal amount;
|
||||
}
|
||||
|
||||
LineItem item = builder.build(LineItem.class);
|
||||
assert item.getAmount().scale() == 2;
|
||||
```
|
||||
|
||||
### Short Fields Used as Boolean Flags
|
||||
|
||||
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
|
||||
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
|
||||
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
|
||||
code checks `entity.getActive() == 1` specifically, override the field after building:
|
||||
|
||||
```java
|
||||
Organisation org = builder.build(Organisation.class)
|
||||
.setActive((short) 1); // explicit override — random short won't do
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Cascade-Persist Relationships: Recursively Built
|
||||
|
||||
Relationships marked with `cascade = PERSIST` are recursively populated:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Customer customer;
|
||||
}
|
||||
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Both order and customer are built:
|
||||
assert order != null;
|
||||
assert order.getCustomer() != null;
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
|
||||
// When saved, cascade handles both:
|
||||
Order saved = builder.save(Order.class);
|
||||
assert saved.getId() != null;
|
||||
assert saved.getCustomer().getId() != null; // parent also saved
|
||||
```
|
||||
|
||||
### Non-Cascade Relationships: Left Null
|
||||
|
||||
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
|
||||
Create and save the related entity first (the builder works well here), then assign it manually
|
||||
before saving the parent:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class BlogPost {
|
||||
@ManyToOne
|
||||
private Author author; // No cascade = left null by builder
|
||||
}
|
||||
|
||||
BlogPost post = builder.build(BlogPost.class);
|
||||
assert post.getAuthor() == null;
|
||||
|
||||
// Use the builder to create the related entity, then set it manually:
|
||||
Author author = builder.save(Author.class);
|
||||
post.setAuthor(author);
|
||||
database.save(post);
|
||||
```
|
||||
|
||||
### Collection Relationships: Left Empty
|
||||
|
||||
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
|
||||
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
|
||||
`.add()` or `.addAll()` directly is safe:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Author {
|
||||
@OneToMany(mappedBy = "author")
|
||||
private List<BlogPost> posts; // Left empty
|
||||
}
|
||||
|
||||
Author author = builder.build(Author.class);
|
||||
assert author.getPosts().isEmpty();
|
||||
|
||||
// Populate if needed for testing:
|
||||
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
|
||||
```
|
||||
|
||||
### Cycle Detection: Prevents Infinite Recursion
|
||||
|
||||
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Person {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Organization org;
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Organization {
|
||||
@ManyToOne(cascade = CascadeType.PERSIST)
|
||||
private Person founder;
|
||||
}
|
||||
|
||||
Person person = builder.build(Person.class);
|
||||
// One reference will be null to break the cycle:
|
||||
// either person.org or person.org.founder is null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Value Generators
|
||||
|
||||
### Why Customize?
|
||||
|
||||
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
|
||||
|
||||
- Email addresses with your company domain
|
||||
- Realistic phone numbers
|
||||
- Product SKUs following a pattern
|
||||
- Addresses in specific regions
|
||||
- Monetary amounts within realistic ranges
|
||||
|
||||
### Creating a Custom Generator
|
||||
|
||||
Subclass `RandomValueGenerator` and override individual `random*()` methods:
|
||||
|
||||
```java
|
||||
class CompanyTestDataGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
if (propName != null && propName.toLowerCase().contains("email")) {
|
||||
// Use company domain instead of generic @domain.com
|
||||
String localPart = UUID.randomUUID().toString().substring(0, 8);
|
||||
String email = localPart + "@mycompany.com";
|
||||
if (maxLength > 0 && email.length() > maxLength) {
|
||||
return email.substring(0, maxLength);
|
||||
}
|
||||
return email;
|
||||
}
|
||||
return super.randomString(propName, maxLength);
|
||||
}
|
||||
|
||||
// Override other methods as needed:
|
||||
@Override
|
||||
protected Object randomEnum(Class<?> type) {
|
||||
if (type == OrderStatus.class) {
|
||||
// Bias towards common statuses for realistic test data
|
||||
return ThreadLocalRandom.current().nextDouble() < 0.8
|
||||
? OrderStatus.PENDING
|
||||
: OrderStatus.COMPLETED;
|
||||
}
|
||||
return super.randomEnum(type);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using a Custom Generator
|
||||
|
||||
Pass the custom generator when building:
|
||||
|
||||
```java
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
|
||||
User user = builder.build(User.class);
|
||||
assert user.getEmail().endsWith("@mycompany.com");
|
||||
```
|
||||
|
||||
In a DI context, register this as the bean:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new CompanyTestDataGenerator())
|
||||
.build();
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Money Type
|
||||
|
||||
```java
|
||||
public class MoneyValueGenerator extends RandomValueGenerator {
|
||||
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Generate prices in a realistic range: $5.00 to $999.99
|
||||
BigDecimal price = BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
|
||||
);
|
||||
return price.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use for Integration Tests, Not Unit Tests
|
||||
|
||||
✅ **Good:** Integration test with database
|
||||
```java
|
||||
@Test
|
||||
void whenSaving_thenCanRetrieve() {
|
||||
Product product = builder.save(Product.class);
|
||||
Product found = database.find(Product.class, product.getId());
|
||||
assertThat(found).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
❌ **Poor:** Validation test requiring specific values
|
||||
```java
|
||||
@Test
|
||||
void whenNameIsBlank_thenThrowException() {
|
||||
Product product = builder.build(Product.class); // name is random!
|
||||
product.setName(""); // have to override anyway
|
||||
// ... test proceeds
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Override Values for Specific Test Scenarios
|
||||
|
||||
When test requirements demand specific field values, manually override after building:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenStockIsLow_thenShowWarning() {
|
||||
Product product = builder.build(Product.class);
|
||||
product.setQuantity(2); // Specific value for this test
|
||||
|
||||
boolean shouldWarn = product.shouldShowLowStockWarning();
|
||||
assertThat(shouldWarn).isTrue();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Fixture Factories for Common Patterns
|
||||
|
||||
For shared domain-specific setup, encapsulate build patterns in an instance helper class
|
||||
rather than a static factory. In a DI context, this class can be registered as a bean
|
||||
alongside `TestEntityBuilder`:
|
||||
|
||||
```java
|
||||
// Spring Boot
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
return new OrderTestFactory(builder, database);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderTestFactory {
|
||||
|
||||
private final TestEntityBuilder builder;
|
||||
private final Database database;
|
||||
|
||||
public OrderTestFactory(TestEntityBuilder builder, Database database) {
|
||||
this.builder = builder;
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public Order savePendingOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.PENDING);
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
public Order saveShippedOrder() {
|
||||
Order order = builder.build(Order.class);
|
||||
order.setStatus(OrderStatus.SHIPPED);
|
||||
order.setShippedAt(Instant.now());
|
||||
database.save(order);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in tests:
|
||||
@SpringBootTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Autowired OrderTestFactory orderFactory;
|
||||
|
||||
@Test
|
||||
void whenOrderPending_thenCanUpdate() {
|
||||
Order order = orderFactory.savePendingOrder();
|
||||
// ... test logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build Multiple Distinct Instances
|
||||
|
||||
Each call to `build()` or `save()` produces a new instance with fresh random values:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenFetchingMultipleOrders_thenAllUnique() {
|
||||
Order order1 = builder.save(Order.class);
|
||||
Order order2 = builder.save(Order.class);
|
||||
Order order3 = builder.save(Order.class);
|
||||
|
||||
assertThat(order1.getId()).isNotEqualTo(order2.getId());
|
||||
assertThat(order2.getId()).isNotEqualTo(order3.getId());
|
||||
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Integration Test with Spring Boot
|
||||
|
||||
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
|
||||
the repository under test:
|
||||
|
||||
```java
|
||||
@TestConfiguration
|
||||
class TestConfig {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootTest
|
||||
class OrderRepositoryTest {
|
||||
|
||||
@Autowired OrderRepository orderRepository;
|
||||
@Autowired TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
|
||||
assertThat(pending).hasSize(2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Integration Test with Avaje Inject
|
||||
|
||||
```java
|
||||
@TestScope
|
||||
@Factory
|
||||
class TestConfiguration {
|
||||
@Bean
|
||||
TestEntityBuilder testEntityBuilder(Database database) {
|
||||
return TestEntityBuilder.builder(database).build();
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTest
|
||||
class OrderControllerTest {
|
||||
|
||||
@Inject TestEntityBuilder builder;
|
||||
|
||||
@Test
|
||||
void whenFindingOrdersByStatus_thenReturnsMatching() {
|
||||
Order pending1 = builder.build(Order.class);
|
||||
pending1.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order pending2 = builder.build(Order.class);
|
||||
pending2.setStatus(OrderStatus.PENDING);
|
||||
|
||||
Order shipped = builder.build(Order.class);
|
||||
shipped.setStatus(OrderStatus.SHIPPED);
|
||||
|
||||
builder.saveAll(pending1, pending2, shipped);
|
||||
|
||||
// ... test assertions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Recursive Relationship Building
|
||||
|
||||
```java
|
||||
@Test
|
||||
void whenBuildingOrderWithCustomer_thenBothPopulated() {
|
||||
Order order = builder.build(Order.class);
|
||||
|
||||
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
|
||||
assertThat(order.getCustomer()).isNotNull();
|
||||
// Before persist, @Id values are typically unset
|
||||
// (0 for primitive IDs, null for boxed IDs).
|
||||
assertThat(order.getCustomer().getName()).isNotNull();
|
||||
|
||||
// Saving cascades to customer:
|
||||
Order saved = builder.save(Order.class);
|
||||
assertThat(saved.getId()).isNotNull();
|
||||
assertThat(saved.getCustomer().getId()).isNotNull();
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Custom Generator for Domain Values
|
||||
|
||||
```java
|
||||
// Custom generator for your domain
|
||||
class ECommerceTestDataGenerator extends RandomValueGenerator {
|
||||
@Override
|
||||
protected BigDecimal randomBigDecimal(int precision, int scale) {
|
||||
// Product prices typically range $10-$500
|
||||
return BigDecimal.valueOf(
|
||||
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
|
||||
).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingCustomGenerator() {
|
||||
TestEntityBuilder builder = TestEntityBuilder.builder(database)
|
||||
.valueGenerator(new ECommerceTestDataGenerator())
|
||||
.build();
|
||||
|
||||
Product product = builder.build(Product.class);
|
||||
assertThat(product.getPrice())
|
||||
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No BeanDescriptor found for [Class] — is it an @Entity?"
|
||||
|
||||
**Cause:** The class you're trying to build is not registered as an Ebean entity.
|
||||
|
||||
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "products")
|
||||
public class Product {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Fields are unset even though I expected them to be populated
|
||||
|
||||
**Cause:** `TestEntityBuilder` does **not** populate:
|
||||
- `@Id` fields (identity/primary key; left unset until persist)
|
||||
- `@Version` fields (optimistic locking; left unset until persist)
|
||||
- `@Transient` fields
|
||||
- `@OneToMany` collections
|
||||
- Non-cascade `@ManyToOne` relationships
|
||||
|
||||
**Solution:** Set only the fields your test scenario cares about, then persist.
|
||||
`@Id` and `@Version` are usually database-managed and should typically be left
|
||||
unset before save:
|
||||
```java
|
||||
Product product = builder.build(Product.class);
|
||||
product.setName("specific-name"); // test-specific override
|
||||
database.save(product); // database assigns @Id/@Version
|
||||
```
|
||||
|
||||
### Building recursive relationships causes StackOverflowError
|
||||
|
||||
**Cause:** Two or more entities mutually reference each other without cycle detection.
|
||||
|
||||
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
|
||||
```java
|
||||
Person person = builder.build(Person.class);
|
||||
person.getOrganization().setFounder(null); // Break cycle
|
||||
```
|
||||
|
||||
### Values generated are "too random" for my test
|
||||
|
||||
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
|
||||
|
||||
**Solution:** Create a custom generator that produces deterministic values:
|
||||
```java
|
||||
class DeterministicTestDataGenerator extends RandomValueGenerator {
|
||||
private int counter = 0;
|
||||
|
||||
@Override
|
||||
protected String randomString(String propName, int maxLength) {
|
||||
return "test_" + (counter++);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
`TestEntityBuilder` accelerates test development by:
|
||||
|
||||
1. **Reducing boilerplate** — No need to manually set every field
|
||||
2. **Improving readability** — Tests focus on what matters, not setup
|
||||
3. **Enabling variety** — Each build produces distinct random values
|
||||
4. **Respecting constraints** — Column lengths and decimal scales are enforced
|
||||
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
# Guide: Using `RawSql` with Ebean
|
||||
|
||||
## Purpose
|
||||
|
||||
`RawSql` lets you back an Ebean bean with a **hand-written SQL query** instead of
|
||||
Ebean generating the SQL from the entity mapping. Ebean still handles object
|
||||
mapping (result set columns → bean properties), lazy loading of associated beans,
|
||||
and - depending on how the `RawSql` is built - dynamic `WHERE`/`HAVING` predicates
|
||||
added through the normal query API.
|
||||
|
||||
Use this guide when you need to:
|
||||
|
||||
- run vendor-specific SQL, complex aggregation, or reporting queries that don't
|
||||
map cleanly to an ORM query
|
||||
- reuse a hand-tuned query but still want typed/dynamic predicates, paging, or
|
||||
`ORDER BY` added by the caller
|
||||
- back a query bean (`Q*`) or DTO-like bean with SQL containing a CTE, window
|
||||
function, or subquery in the `FROM` clause
|
||||
|
||||
Prefer ordinary query bean queries first - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md), Step 9,
|
||||
for the full decision order (query bean → `asDto()` → DTO query → raw SQL).
|
||||
This guide covers raw SQL once you've decided it's the right tool.
|
||||
|
||||
---
|
||||
|
||||
## The bean behind a `RawSql` query
|
||||
|
||||
A bean queried with `RawSql` is not necessarily backed by a physical table. Annotate
|
||||
it `@Entity @Sql` to tell Ebean it is mapped via `RawSql` rather than table DDL:
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Sql
|
||||
public class OrderAggregate {
|
||||
|
||||
@OneToOne
|
||||
Order order;
|
||||
|
||||
Double totalAmount;
|
||||
Long totalItems;
|
||||
|
||||
// getters/setters
|
||||
}
|
||||
```
|
||||
|
||||
`@Sql` beans still get a generated query bean (`QOrderAggregate`) if the
|
||||
querybean-generator annotation processor is configured - see
|
||||
[Using `RawSql` with query beans](#using-rawsql-with-query-beans) below.
|
||||
|
||||
You can also query an ordinary table-backed `@Entity` with `RawSql` - the column
|
||||
mapping just needs to line up with that entity's properties.
|
||||
|
||||
---
|
||||
|
||||
## Building a `RawSql` - three factory methods
|
||||
|
||||
`RawSqlBuilder` has three ways to construct a `RawSql`, depending on how much of
|
||||
the SQL Ebean needs to understand:
|
||||
|
||||
| Method | SELECT columns parsed? | Dynamic WHERE/HAVING/ORDER BY? | Use for |
|
||||
|--------|------------------------|------------------------|---------|
|
||||
| `RawSqlBuilder.parse(sql)` | Yes | Yes | Ordinary `SELECT ... FROM ... WHERE ...` statements |
|
||||
| `RawSqlBuilder.unparsed(sql)` | No | No | Fixed SQL that never needs additional predicates |
|
||||
| `RawSqlBuilder.withPlaceholders(sql)` | No (explicit `columnMapping()` required) | Yes, via `${where}` / `${andWhere}` / `${having}` / `${andHaving}` / `${orderBy}` / `${andOrderBy}` | CTEs, window functions, subqueries - SQL that keyword-based parsing can't handle |
|
||||
|
||||
### `parse(sql)` - the common case
|
||||
|
||||
`parse(sql)` scans the SQL text for the `select` / `from` / `where` / `group by`
|
||||
/ `having` / `order by` keywords to work out the SELECT column list (so it can
|
||||
validate your column mappings) and the injection points for dynamic `WHERE`/
|
||||
`HAVING` expressions.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse(
|
||||
"select c.id, c.name, c.status from customer c")
|
||||
.columnMapping("c.id", "id")
|
||||
.columnMapping("c.name", "name")
|
||||
.columnMapping("c.status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().eq("status", Customer.Status.ACTIVE)
|
||||
.orderBy("name")
|
||||
.findList();
|
||||
```
|
||||
|
||||
Because the SQL is parsed, mistakes in `columnMapping()` (unknown column, wrong
|
||||
order for `unparsed`-style mappings) are caught early. **This fails on SQL the
|
||||
keyword parser can't make sense of** - a `WITH` CTE, a window function, a
|
||||
subquery in `FROM`, etc. - because the keyword positions found don't correspond
|
||||
to the outer query's real structure. Use `withPlaceholders(sql)` for that SQL
|
||||
instead (see below).
|
||||
|
||||
### `unparsed(sql)` - fixed queries
|
||||
|
||||
`unparsed(sql)` skips all parsing. The SQL is used exactly as written, and **no
|
||||
further `WHERE`/`HAVING`/`ORDER BY` can be added** by the caller - useful for a
|
||||
completely fixed reporting query with no caller-supplied filtering.
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.unparsed(
|
||||
"select id, name, status from customer where status = 'ACTIVE'")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = DB.find(Customer.class)
|
||||
.setRawSql(rawSql)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Column mappings for `unparsed(sql)` must be supplied **in the same order** as
|
||||
the columns appear in the SQL, since there's no parsing to match them by name.
|
||||
|
||||
### `withPlaceholders(sql)` - complex SQL (CTEs, window functions, subqueries)
|
||||
|
||||
`withPlaceholders(sql)` avoids keyword scanning entirely. You mark exactly where
|
||||
a dynamic `WHERE`/`HAVING`/`ORDER BY` expression should be injected using
|
||||
placeholder tokens, and column mappings are always explicit (as with `unparsed`).
|
||||
|
||||
#### Placeholder reference
|
||||
|
||||
| Placeholder | Meaning | Use when |
|
||||
|-------------|---------|----------|
|
||||
| `${where}` | Insert a new `WHERE <expr>` clause here | No static `WHERE` clause exists yet at this point in the SQL |
|
||||
| `${andWhere}` | Insert `AND <expr>` here | A static `WHERE ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${having}` | Insert a new `HAVING <expr>` clause here | No static `HAVING` clause exists yet at this point in the SQL |
|
||||
| `${andHaving}` | Insert `AND <expr>` here | A static `HAVING ...` clause already exists in the SQL and you want to append to it |
|
||||
| `${orderBy}` | Insert a new `ORDER BY <expr>` clause here | No static `ORDER BY` clause exists yet at this point in the SQL, and callers may supply `.orderBy(...)` |
|
||||
| `${andOrderBy}` | Insert `, <expr>` here | A static `ORDER BY ...` clause already exists in the SQL and you want callers to be able to append extra sort columns to it |
|
||||
|
||||
Rules:
|
||||
|
||||
- At least one placeholder is required - `withPlaceholders(sql)` throws
|
||||
`IllegalArgumentException` if none of the six tokens are present.
|
||||
- Use only the placeholders you need. Omit `${where}`/`${andWhere}` entirely if
|
||||
the query never needs a dynamic `WHERE` (e.g. only a dynamic `HAVING` on an
|
||||
aggregate). Omit `${having}`/`${andHaving}` if there's no dynamic `HAVING`.
|
||||
Omit `${orderBy}`/`${andOrderBy}` if the ordering is always fixed.
|
||||
- Explicit `columnMapping()` is required for every returned column - there is no
|
||||
column-list parsing to infer names from.
|
||||
- **A caller-supplied `.orderBy(...)`/`.order(...)` is only applied if the SQL
|
||||
contains an `${orderBy}` or `${andOrderBy}` placeholder.** Without one of
|
||||
those placeholders there is no defined injection point for dynamic ordering,
|
||||
so any `.orderBy(...)` call on the query is safely ignored rather than risk
|
||||
producing invalid SQL - even if the template has a static trailing
|
||||
`ORDER BY ...` of its own. If you need callers to be able to influence
|
||||
ordering, add `${orderBy}` (no existing static order by) or `${andOrderBy}`
|
||||
(append after an existing static order by).
|
||||
- Any other static SQL that follows a `${where}`/`${having}` placeholder (e.g.
|
||||
a trailing `GROUP BY`) is preserved and correctly positioned **after**
|
||||
whatever dynamic expression gets injected at that placeholder.
|
||||
|
||||
#### Example - CTE with `${where}`
|
||||
|
||||
```java
|
||||
String sql = """
|
||||
with order_totals as (
|
||||
select o.id as order_id, sum(d.qty * d.unit_price) as total_amount
|
||||
from o_order o
|
||||
join o_order_detail d on d.order_id = o.id
|
||||
group by o.id
|
||||
)
|
||||
select order_id, total_amount
|
||||
from order_totals
|
||||
${where}
|
||||
order by order_id
|
||||
""";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
`total_amount` is a genuine column of the `order_totals` CTE here, so it's valid
|
||||
to filter on it in the outer `WHERE` - this only works because the aggregate is
|
||||
computed inside the CTE rather than as a same-level `SELECT` alias.
|
||||
|
||||
#### Example - static `WHERE` already present, append with `${andWhere}`
|
||||
|
||||
```java
|
||||
String sql = "... from order_totals where total_amount > 0 ${andWhere} order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > 0 and total_amount > ? order by order_id
|
||||
DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
#### Example - `${having}` only, filtering on an aggregate directly
|
||||
|
||||
No `WHERE` placeholder is needed if you only ever filter on the aggregate value:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.having().gt("totalAmount", 100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The dynamic `HAVING` clause is injected before the static trailing `ORDER BY`,
|
||||
even though `${having}` is the only placeholder present. Because there's no
|
||||
`${orderBy}`/`${andOrderBy}` placeholder here, a caller-supplied `.orderBy(...)`
|
||||
would be ignored - the ordering stays fixed as `order by order_id`.
|
||||
|
||||
#### Example - both `${where}` and `${having}`
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" ${where}" +
|
||||
" group by o.id" +
|
||||
" ${having}" +
|
||||
" order by order_id";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("order.id", 0)
|
||||
.having().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Both the dynamic `WHERE` and dynamic `HAVING` are injected at their respective
|
||||
placeholders, and the trailing `order by order_id` is preserved after the
|
||||
`HAVING` clause.
|
||||
|
||||
#### Example - `${orderBy}`, fully dynamic ordering
|
||||
|
||||
Use `${orderBy}` when there's no static default ordering and you want the
|
||||
caller's `.orderBy(...)` to control it entirely:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"with order_totals as (" +
|
||||
" select o.id as order_id, sum(d.qty * d.unit_price) as total_amount" +
|
||||
" from o_order o join o_order_detail d on d.order_id = o.id" +
|
||||
" group by o.id" +
|
||||
")" +
|
||||
" select order_id, total_amount from order_totals" +
|
||||
" ${where}" +
|
||||
" ${orderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... where total_amount > ? order by total_amount desc
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("totalAmount desc")
|
||||
.findList();
|
||||
```
|
||||
|
||||
If the caller doesn't call `.orderBy(...)`, nothing is injected at `${orderBy}`
|
||||
and no `ORDER BY` clause is emitted at all.
|
||||
|
||||
#### Example - `${andOrderBy}`, appending to a static default ordering
|
||||
|
||||
Use `${andOrderBy}` when there's a sensible static default ordering but you
|
||||
want callers to be able to add extra tie-breaker sort columns:
|
||||
|
||||
```java
|
||||
String sql =
|
||||
"... from order_totals" +
|
||||
" ${where}" +
|
||||
" order by total_amount desc ${andOrderBy}";
|
||||
|
||||
RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
.columnMapping("order_id", "order.id")
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
.create();
|
||||
|
||||
// executed SQL: ... order by total_amount desc , order_id
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql)
|
||||
.where().gt("totalAmount", 0)
|
||||
.orderBy("order.id")
|
||||
.findList();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using `fetchQuery()` to build out more of the graph
|
||||
|
||||
A `RawSql` query can be the **root query** and still use `fetchQuery(path)` the
|
||||
same way an ordinary ORM query does - Ebean runs the raw SQL for the root rows,
|
||||
then runs additional secondary ORM queries to populate the requested paths. This
|
||||
lets you hand-write only the part of the query that needs raw SQL (e.g. an
|
||||
aggregate/CTE) and let the ORM build out the rest of the object graph normally.
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = DB.find(OrderAggregate.class)
|
||||
.setRawSql(rawSql) // root query - runs the CTE/aggregate SQL
|
||||
.fetchQuery("order") // secondary query - loads the full Order
|
||||
.fetchQuery("order.details") // secondary query - loads Order.details
|
||||
.where().gt("totalAmount", 50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
This executes **three** queries: the raw SQL root query, then one secondary
|
||||
query per `fetchQuery(path)` call.
|
||||
|
||||
**Important**: if the raw SQL's column mapping only populates part of an
|
||||
association (e.g. only `order.id`, as in the examples above), that association
|
||||
is a *partial reference*. To load a nested to-many under it (e.g.
|
||||
`order.details`), you must add an explicit `fetchQuery(...)` (or `fetch(...)`)
|
||||
for the **intermediate path** (`order`) as well as the nested path
|
||||
(`order.details`) - `fetchQuery("order.details")` alone will leave `details` as
|
||||
a deferred/lazy collection, because Ebean doesn't otherwise have a fetch node
|
||||
for `order` to hang the secondary query off. If the raw SQL already selects the
|
||||
full set of columns for an association directly (no partial reference), this
|
||||
extra step isn't needed.
|
||||
|
||||
This is the same `fetchQuery()` mechanism used for ordinary query bean queries -
|
||||
see [Use `fetchQuery()` for to-many paths](writing-ebean-query-beans.md#step-7---use-fetchquery-for-to-many-paths-and-fetchgroup-for-reusable-query-shapes)
|
||||
for background on why to-many paths are loaded via secondary queries rather than
|
||||
a single joined query.
|
||||
|
||||
---
|
||||
|
||||
## Column mapping
|
||||
|
||||
Every `RawSqlBuilder` (except a bare `unparsed(sql)` with implicit positional
|
||||
mapping) uses `columnMapping(dbColumn, propertyName)` to map SQL result columns
|
||||
to bean properties:
|
||||
|
||||
```java
|
||||
.columnMapping("order_id", "order.id") // maps to the "order" association's "id" property
|
||||
.columnMapping("total_amount", "totalAmount")
|
||||
```
|
||||
|
||||
- Dotted property paths (e.g. `"order.id"`) map a column into a nested/associated
|
||||
bean property.
|
||||
- `columnMappingIgnore(dbColumn)` marks a selected column as intentionally unmapped
|
||||
(present in the SQL but not needed on the bean).
|
||||
- `tableAliasMapping(tableAlias, path)` bulk-renames every mapping using a given
|
||||
SQL table alias to be prefixed with a bean property path - handy when a `parse()`
|
||||
query selects many columns from a joined table (e.g. alias `c` → path `customer`)
|
||||
and you don't want to repeat the prefix in every `columnMapping()` call.
|
||||
|
||||
---
|
||||
|
||||
## Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()`-equivalent
|
||||
expressions (as bean properties) over hand-written SQL. Every generated query
|
||||
bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
This also works with `withPlaceholders(sql)` and an `@Sql` query bean:
|
||||
|
||||
```java
|
||||
List<OrderAggregate> list = new QOrderAggregate()
|
||||
.setRawSql(rawSql) // built with withPlaceholders() as shown above
|
||||
.totalAmount.gt(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
The typed property expression (`.totalAmount.gt(100)`) is translated to a bound
|
||||
predicate and injected at the `${where}`/`${having}` placeholder position, exactly
|
||||
as `.where().gt("totalAmount", 100)` would be on the plain `Query<T>` API.
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - reaching for raw SQL before trying a query bean
|
||||
|
||||
Complex-looking joins are often just ordinary association traversal in a query
|
||||
bean. Don't use raw SQL just because a query touches several tables - see
|
||||
[Write Ebean queries with query beans](writing-ebean-query-beans.md).
|
||||
|
||||
### Anti-pattern 2 - using `parse(sql)` on a CTE or window-function query
|
||||
|
||||
`parse(sql)` will throw a parsing exception (or silently mis-locate the WHERE
|
||||
injection point) on SQL it can't understand structurally. If your SQL starts
|
||||
with `WITH ...` or has a subquery in `FROM`, use `withPlaceholders(sql)` instead.
|
||||
|
||||
### Anti-pattern 3 - filtering on a same-level SELECT alias
|
||||
|
||||
You cannot add a dynamic `WHERE` predicate on a `SELECT`-clause alias in the
|
||||
same query level (e.g. `select sum(x) as total ... ${where}` - `total` isn't a
|
||||
real column yet at the `WHERE` stage of that query level). Either:
|
||||
|
||||
- move the aggregation into a CTE and filter on the CTE's output column in the
|
||||
outer query (`WHERE` case), or
|
||||
- use `${having}`/`${andHaving}` to filter on the aggregate at the `HAVING` stage
|
||||
of the same query level, where the aggregate expression is valid.
|
||||
|
||||
### Anti-pattern 4 - forgetting `columnMapping()` with `unparsed()`/`withPlaceholders()`
|
||||
|
||||
Both `unparsed(sql)` and `withPlaceholders(sql)` require **every** returned
|
||||
column to be explicitly mapped (or explicitly ignored via
|
||||
`columnMappingIgnore(...)`) - there's no column-list parsing to infer them.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `RuntimeException: Error parsing sql, can not find ... keyword` | `parse(sql)` used on SQL with a CTE, window function, or subquery in `FROM` | Use `RawSqlBuilder.withPlaceholders(sql)` instead |
|
||||
| `IllegalArgumentException: withPlaceholders() requires at least one of ${where}, ${andWhere}, ${having}, ${andHaving}, ${orderBy}, ${andOrderBy}...` | None of the six placeholder tokens were found in the SQL | Add the appropriate placeholder token at the injection point |
|
||||
| Dynamic `WHERE`/`HAVING` predicate silently has no effect, or query throws | Used `unparsed(sql)` and then tried to add a predicate | `unparsed(sql)` queries cannot be modified - switch to `parse(sql)` or `withPlaceholders(sql)` |
|
||||
| Generated SQL is invalid / clauses appear in the wrong order | Predicates added via `.where()`/`.having()` don't match the placeholders actually present in the SQL | Make sure `${where}`/`${having}` (or the `and` variants) exist at the point you expect predicates to be injected |
|
||||
| `.orderBy(...)`/`.order(...)` on the query silently has no effect | The SQL has no `${orderBy}`/`${andOrderBy}` placeholder | This is by design - without one of those placeholders there's no defined injection point, so the ordering is ignored rather than corrupting the SQL. Add `${orderBy}` or `${andOrderBy}` if you need caller-controlled ordering |
|
||||
| `Unknown column` / unmapped property error | Missing `columnMapping()` for a selected column | Add a `columnMapping(...)` or `columnMappingIgnore(...)` for every SQL column |
|
||||
| `fetchQuery("a.b")` collection stays deferred/lazy | `a` is a partial reference from the raw SQL column mapping (e.g. only `a.id` mapped), and there's no fetch node for `a` itself | Add `fetchQuery("a")` (or `fetch("a")`) alongside `fetchQuery("a.b")` |
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Write Ebean queries with query beans](writing-ebean-query-beans.md)
|
||||
- [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
@@ -1,598 +0,0 @@
|
||||
# Guide: Write Ebean Queries with Query Beans
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide gives step-by-step instructions for AI agents and developers to write
|
||||
application queries using Ebean query beans.
|
||||
|
||||
Use this guide when the project already has Ebean configured and you need to:
|
||||
|
||||
- add a repository/service query
|
||||
- replace string-based ORM queries with type-safe query beans
|
||||
- tune what data is fetched to avoid over-fetching or N+1 issues
|
||||
- return DTO projections for list screens or API responses
|
||||
|
||||
The default recommendation is:
|
||||
|
||||
1. Prefer query beans first
|
||||
2. Prefer entity queries for domain logic
|
||||
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
|
||||
4. Prefer DTO projection for summary/read-model use cases
|
||||
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The project already uses Ebean ORM
|
||||
- Query bean generation is configured (for Maven this usually means
|
||||
`querybean-generator` is registered as an annotation processor)
|
||||
- Entity beans already exist
|
||||
- A compile/build has run successfully since the last entity model change
|
||||
|
||||
If query beans are not yet configured, first follow:
|
||||
[`add-ebean-postgres-maven-pom.md`](add-ebean-postgres-maven-pom.md)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 - Verify the generated `Q*` query bean exists
|
||||
|
||||
For each entity bean, Ebean generates a query bean with the same name prefixed
|
||||
with `Q`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `Customer` -> `QCustomer`
|
||||
- `Order` -> `QOrder`
|
||||
- `Contact` -> `QContact`
|
||||
|
||||
Import the generated type from the query bean package:
|
||||
|
||||
```java
|
||||
import org.example.domain.query.QCustomer;
|
||||
```
|
||||
|
||||
If the `Q*` type does not exist or the IDE cannot resolve it:
|
||||
|
||||
1. Confirm the entity compiled successfully
|
||||
2. Run a normal project compile/build
|
||||
3. If the entity was renamed or moved, run a full rebuild rather than relying on
|
||||
incremental compilation
|
||||
|
||||
### Important caveat - entity rename
|
||||
|
||||
After refactoring an entity name, old generated query beans can remain on disk
|
||||
until the next full build. If both old and new `Q*` types appear to exist, do a
|
||||
clean rebuild before editing application queries.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 - Choose the terminal query method before writing predicates
|
||||
|
||||
Decide what the caller actually needs. This determines the terminal method and
|
||||
often the right query shape.
|
||||
|
||||
| Need | Preferred method | Notes |
|
||||
|------|------------------|-------|
|
||||
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
|
||||
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
|
||||
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
|
||||
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
|
||||
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
|
||||
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
|
||||
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
|
||||
|
||||
### Example - existence check
|
||||
|
||||
```java
|
||||
boolean alreadyUsed = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.exists();
|
||||
```
|
||||
|
||||
### Example - unique lookup
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.email.equalTo(email)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
Do **not** use `findOne()` for predicates that can match multiple rows.
|
||||
|
||||
### Example - stream and map to another type
|
||||
|
||||
Choose based on result size and how you consume it:
|
||||
|
||||
- **`findList().stream()`** — executes the query, materialises the rows,
|
||||
**releases the connection**, then streams over an in-memory list. No open
|
||||
database resources and no try-with-resources needed. Prefer this for small or
|
||||
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
|
||||
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
|
||||
connection (and an implicit transaction) open for the **whole lifetime of the
|
||||
stream pipeline**. It must be closed with try-with-resources. Prefer it when
|
||||
the result may be large, when you want constant memory, or when you want to
|
||||
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
|
||||
|
||||
```java
|
||||
// small, bounded result fully collected -> findList().stream()
|
||||
List<PendingPlan> pending = new QCaptureRequest()
|
||||
.collectedAt.isNull()
|
||||
.orderBy().requestedAt.asc()
|
||||
.findList()
|
||||
.stream()
|
||||
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
|
||||
.toList();
|
||||
|
||||
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
|
||||
try (Stream<Customer> stream = new QCustomer()
|
||||
.status.equalTo(Status.NEW)
|
||||
.findStream()) {
|
||||
stream
|
||||
.map(...)
|
||||
.forEach(...);
|
||||
}
|
||||
```
|
||||
|
||||
For processing large results one bean at a time, `findEach()` is often the
|
||||
simplest choice because it closes the underlying resources automatically.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 - Build predicates by traversing properties and associations
|
||||
|
||||
With query beans, write predicates directly against properties. When you
|
||||
traverse an association, Ebean adds the necessary joins automatically.
|
||||
|
||||
### Example - root property predicates
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - association traversal
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo("Auckland")
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - collection predicate
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.contacts.isEmpty()
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Optional predicates - prefer conditional helpers over `if` blocks
|
||||
|
||||
When a filter is driven by a nullable/optional parameter, use the built-in
|
||||
conditional helpers instead of wrapping predicates in `if` blocks. The query
|
||||
stays fluent and reads top-to-bottom, and no predicate is added when the value
|
||||
is absent.
|
||||
|
||||
| Helper | Adds predicate when | Resulting SQL |
|
||||
|--------|---------------------|---------------|
|
||||
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
|
||||
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
|
||||
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
|
||||
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
|
||||
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
|
||||
|
||||
```java
|
||||
// Instead of building the query with if blocks:
|
||||
QCustomer q = new QCustomer();
|
||||
if (name != null && !name.isBlank()) {
|
||||
q.name.eq(name.trim());
|
||||
}
|
||||
if (status != null) {
|
||||
q.status.eq(status);
|
||||
}
|
||||
List<Customer> customers = q.findList();
|
||||
|
||||
// Prefer the conditional helpers:
|
||||
List<Customer> customers = new QCustomer()
|
||||
.name.eqIfNotBlank(name)
|
||||
.status.eqIfPresent(status)
|
||||
.findList();
|
||||
```
|
||||
|
||||
Use `eqOrNull(v)` when a null column value should also match - for example an
|
||||
"any environment" row stored with `env_id is null` should surface under any env
|
||||
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
|
||||
|
||||
```java
|
||||
List<CaptureRequest> rows = new QCaptureRequest()
|
||||
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
When adding a new query:
|
||||
|
||||
1. Start from the root entity that the caller wants back
|
||||
2. Add predicates with query bean properties
|
||||
3. Traverse relationships instead of writing manual join SQL
|
||||
4. Keep property references type-safe; avoid string property names unless the API
|
||||
specifically requires them
|
||||
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
|
||||
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
|
||||
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
|
||||
null column"
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Add ordering, limits, and pagination deliberately
|
||||
|
||||
Do not leave list queries unordered unless the call site truly does not care.
|
||||
For UI lists, APIs, and background jobs, explicit ordering is usually better.
|
||||
|
||||
### Example - ordered list with limit
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.setMaxRows(50)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - offset/limit pagination
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Example - paged list with total count
|
||||
|
||||
```java
|
||||
PagedList<Customer> page = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().id.asc()
|
||||
.setFirstRow(offset)
|
||||
.setMaxRows(pageSize)
|
||||
.findPagedList();
|
||||
|
||||
page.loadRowCount();
|
||||
List<Customer> customers = page.getList();
|
||||
int totalRowCount = page.getTotalRowCount();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
- Use `findList()` when the caller only needs rows
|
||||
- Use `findPagedList()` when the caller also needs page metadata or total counts
|
||||
- Pair pagination with a stable `orderBy()` so page boundaries stay predictable
|
||||
|
||||
---
|
||||
|
||||
## Step 5 - Control fetched data with `select()` and `fetch()`
|
||||
|
||||
By default, entity queries can load more of the object graph than the caller
|
||||
actually needs. Use `select()` and `fetch()` to control the root and association
|
||||
properties that are loaded.
|
||||
|
||||
### Root properties with `select()`
|
||||
|
||||
Use `select()` to define which properties should be fetched on the root entity.
|
||||
|
||||
### Associated bean properties with `fetch()`
|
||||
|
||||
Use `fetch()` to define what should be fetched on associated paths.
|
||||
|
||||
### Example - partial entity query
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.name.istartsWith("rob")
|
||||
.findList();
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- `select(...)` tunes the root `Customer` properties
|
||||
- `contacts.fetch(...)` tunes the associated `Contact` properties
|
||||
- the query still returns `Customer` entity beans
|
||||
|
||||
### Agent rules for partial entity queries
|
||||
|
||||
1. Only use `select()`/`fetch()` when you know what the caller will read next
|
||||
2. Do not treat partially loaded entities like fully populated API DTOs
|
||||
3. If the caller only needs summary fields, prefer a DTO projection instead
|
||||
|
||||
---
|
||||
|
||||
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
|
||||
|
||||
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
|
||||
read-only graph.
|
||||
|
||||
This means:
|
||||
|
||||
- setters cannot mutate returned beans
|
||||
- associated collections are unmodifiable
|
||||
- lazy loading is disabled
|
||||
- accessing an unloaded property throws `LazyInitialisationException`
|
||||
- the query uses `PersistenceContextScope.QUERY`
|
||||
|
||||
### Example - read-only entity graph
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
private static final QContact CONT = QContact.alias();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.contacts.fetch(CONT.email)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.setUnmodifiable(true)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### When to prefer `setUnmodifiable(true)`
|
||||
|
||||
Use it when the result is meant to be read-only, such as:
|
||||
|
||||
- service/query methods returning entity graphs for display or serialization
|
||||
- query results you want the application to treat as immutable
|
||||
- cached query results or other shared read models backed by entity graphs
|
||||
- partial entity graphs where you want accidental lazy loading to fail fast
|
||||
|
||||
### When **not** to use it
|
||||
|
||||
Do **not** use `setUnmodifiable(true)` when the caller will:
|
||||
|
||||
- modify the beans and save them later
|
||||
- rely on lazy loading of associations or unloaded scalar properties
|
||||
- treat the result as a working persistence model rather than a read-only view
|
||||
|
||||
### Agent rule
|
||||
|
||||
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
|
||||
should be the default recommendation. If the caller needs a mutable model or a
|
||||
serialized summary shape, choose mutable entities or DTO projection instead.
|
||||
|
||||
If you need cached assoc-one references for unmodifiable graphs, see
|
||||
[Immutable bean cache for read-only references](immutable-bean-cache.md).
|
||||
|
||||
---
|
||||
|
||||
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
|
||||
|
||||
Ebean applies important SQL rules when translating ORM queries:
|
||||
|
||||
1. It does not generate SQL cartesian products
|
||||
2. It honors `maxRows` in SQL
|
||||
|
||||
This means to-many paths often need special handling.
|
||||
|
||||
### Use `fetchQuery()` when:
|
||||
|
||||
- the query includes a `OneToMany` or `ManyToMany` path
|
||||
- the query includes `setMaxRows(...)`
|
||||
- the query loads multiple to-many paths
|
||||
- you want the query shape to make the secondary-query behavior explicit
|
||||
|
||||
### Example - explicit secondary queries for to-many paths
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
List<Order> orders = new QOrder()
|
||||
.customer.fetch(CUST.name)
|
||||
.lines.fetchQuery()
|
||||
.shipments.fetchQuery()
|
||||
.status.equalTo(Order.Status.NEW)
|
||||
.setMaxRows(100)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Use `FetchGroup` when:
|
||||
|
||||
- the same fetch shape is reused in multiple places
|
||||
- you want to separate predicate logic from fetch-shape tuning
|
||||
- you want an immutable, static query-shape definition
|
||||
|
||||
### Example - reusable fetch group
|
||||
|
||||
```java
|
||||
private static final QCustomer CUST = QCustomer.alias();
|
||||
|
||||
private static final FetchGroup<Customer> CUSTOMER_SUMMARY =
|
||||
QCustomer.forFetchGroup()
|
||||
.select(CUST.name, CUST.status, CUST.whenCreated)
|
||||
.billingAddress.fetch()
|
||||
.buildFetchGroup();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.select(CUSTOMER_SUMMARY)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Agent rule
|
||||
|
||||
If the caller needs multiple to-many paths or a paged query, be suspicious of a
|
||||
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 - Use DTO projection when the caller does not need entity beans
|
||||
|
||||
For list screens, API summaries, exports, or read-model views, the caller often
|
||||
does **not** need managed entity beans. In those cases, project directly to a
|
||||
DTO using `asDto(...)`.
|
||||
|
||||
### Example - DTO projection with query beans
|
||||
|
||||
```java
|
||||
import static org.example.domain.query.QCustomer.Alias.id;
|
||||
import static org.example.domain.query.QCustomer.Alias.name;
|
||||
|
||||
public record CustomerSummary(long id, String name) {}
|
||||
|
||||
List<CustomerSummary> summaries = new QCustomer()
|
||||
.select(id, name)
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.orderBy().name.asc()
|
||||
.asDto(CustomerSummary.class)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Prefer DTO projection when:
|
||||
|
||||
- the caller will serialize the result directly
|
||||
- only a subset of fields is needed
|
||||
- the result is not going to be updated and saved back as an entity
|
||||
- the query contains formulas or aggregation intended for a read model
|
||||
|
||||
---
|
||||
|
||||
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
|
||||
|
||||
Prefer the following order:
|
||||
|
||||
1. Query bean query
|
||||
2. Query bean query + `asDto(...)`
|
||||
3. `database.findDto(...)` or DTO query
|
||||
4. Native SQL / `SqlQuery` / `RawSql`
|
||||
|
||||
### Typical reasons to use raw SQL
|
||||
|
||||
- vendor-specific SQL that query beans do not express well
|
||||
- advanced aggregation or database functions
|
||||
- hand-tuned reporting queries
|
||||
- stored procedures or raw JDBC workflows
|
||||
|
||||
Do **not** jump to raw SQL just because the query joins multiple tables. Query
|
||||
beans already handle ordinary relationship traversal well.
|
||||
|
||||
### Using `RawSql` with query beans
|
||||
|
||||
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
|
||||
generated query bean, giving type-safe `where()`/`having()` expressions over
|
||||
hand-written SQL. Every generated query bean exposes `setRawSql(...)`:
|
||||
|
||||
```java
|
||||
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
|
||||
.columnMapping("id", "id")
|
||||
.columnMapping("name", "name")
|
||||
.columnMapping("status", "status")
|
||||
.create();
|
||||
|
||||
List<Customer> customers = new QCustomer()
|
||||
.setRawSql(rawSql)
|
||||
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
|
||||
.findList();
|
||||
```
|
||||
|
||||
For the full guide to building `RawSql` - including `unparsed()`,
|
||||
`withPlaceholders()` for CTEs/window functions, the `${where}` / `${andWhere}`
|
||||
/ `${having}` / `${andHaving}` placeholder reference, and column mapping - see
|
||||
[Using `RawSql` with Ebean](using-rawsql-with-ebean.md).
|
||||
|
||||
---
|
||||
|
||||
## Common anti-patterns
|
||||
|
||||
### Anti-pattern 1 - Using raw SQL first
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = database.findNative(Customer.class,
|
||||
"select c.* from customer c join address a on a.id = c.billing_address_id where a.city = ?")
|
||||
.setParameter(1, city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
**Prefer:**
|
||||
|
||||
```java
|
||||
List<Customer> customers = new QCustomer()
|
||||
.billingAddress.city.equalTo(city)
|
||||
.findList();
|
||||
```
|
||||
|
||||
### Anti-pattern 2 - Using `findOne()` on a non-unique predicate
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```java
|
||||
Customer customer = new QCustomer()
|
||||
.status.equalTo(Customer.Status.ACTIVE)
|
||||
.findOne();
|
||||
```
|
||||
|
||||
**Why:** Many rows can match; this is not a unique lookup.
|
||||
|
||||
### Anti-pattern 3 - Returning partially loaded entities as API models
|
||||
|
||||
If the caller only needs summary fields, return a DTO instead of partially
|
||||
loaded entities that might later trigger more loading or confuse serializers.
|
||||
|
||||
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
|
||||
|
||||
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
|
||||
so accidental setter calls, collection mutation, and lazy loading fail fast.
|
||||
|
||||
### Anti-pattern 5 - Fetching every relationship "just in case"
|
||||
|
||||
Do not eagerly fetch large object graphs unless the immediate caller will use
|
||||
them. Query tuning is part of the job.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---------|--------------|-----|
|
||||
| `Cannot resolve symbol QCustomer` | Query bean generation not configured or build not run | Check the annotation processor and run a build |
|
||||
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
|
||||
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
|
||||
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
|
||||
| Setter calls or collection mutation fail on query results | `setUnmodifiable(true)` returned a read-only graph | Remove `setUnmodifiable(true)` or treat the result as read-only |
|
||||
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
|
||||
| Ebean executes secondary queries for a to-many path | ORM rules avoided cartesian product or honored `maxRows` | This is expected; use `fetchQuery()` explicitly when appropriate |
|
||||
|
||||
---
|
||||
|
||||
## Summary workflow for AI agents
|
||||
|
||||
When asked to add or modify an Ebean query:
|
||||
|
||||
1. Verify the relevant `Q*` type exists
|
||||
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
|
||||
3. Add predicates with query bean properties and association traversal
|
||||
4. Add explicit ordering and pagination if relevant
|
||||
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
|
||||
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
|
||||
7. Prefer DTO projection for read models and serialized responses
|
||||
8. Only use raw SQL if the ORM query is genuinely the wrong tool
|
||||
|
||||
---
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
|
||||
- [Entity Bean Creation](entity-bean-creation.md)
|
||||
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
|
||||
- [Using `RawSql` with Ebean](using-rawsql-with-ebean.md)
|
||||
- [Ebean query docs](https://ebean.io/docs/query/)
|
||||
@@ -1,333 +0,0 @@
|
||||
# Immutable Bean Cache — notes on multi-level / remote caching
|
||||
|
||||
These notes capture design thoughts for a possible future multi-level immutable bean cache,
|
||||
where immutable beans may be cached remotely (for example Redis or a Postgres cache table)
|
||||
in addition to an in-JVM cache.
|
||||
|
||||
## Current important constraint
|
||||
|
||||
`AssocOneHelp.read()` now uses `ImmutableBeanCache.getIfPresent(id)` as a direct-hit fast path.
|
||||
|
||||
That means:
|
||||
|
||||
- `getIfPresent(id)` is on the **row read hot path**
|
||||
- it must remain **cheap and local**
|
||||
- it should **not** perform network I/O
|
||||
- it should **not** deserialize remote payloads
|
||||
- it should **not** trigger loading or record misses
|
||||
|
||||
## Strong recommendation
|
||||
|
||||
For any multi-level cache design:
|
||||
|
||||
- **L1 cache** = in-JVM cache of already materialized immutable beans
|
||||
- **L2 cache** = remote/shared cache of serialized immutable snapshots
|
||||
- **Loader** = Ebean query using the configured fetch group
|
||||
|
||||
With that split:
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => batch through **L1 -> L2 -> loader**
|
||||
|
||||
This preserves the `AssocOneHelp` fast path.
|
||||
|
||||
---
|
||||
|
||||
## Snapshot mindset
|
||||
|
||||
Remote cache entries should be treated as **immutable snapshots**, not just arbitrary beans.
|
||||
|
||||
A cached value is specific to:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant (if multi-tenant)
|
||||
- fetch-group / cache identity
|
||||
- serializer/schema version
|
||||
|
||||
This matters because a `Customer` cached with:
|
||||
|
||||
- `select("name,version")`
|
||||
|
||||
is not equivalent to a `Customer` cached with:
|
||||
|
||||
- `select("name,version").fetch("billingAddress", "line1,city")`
|
||||
|
||||
## Key design recommendation
|
||||
|
||||
Remote keys should include at least:
|
||||
|
||||
- bean type
|
||||
- bean id
|
||||
- tenant id (if applicable)
|
||||
- cache/fetch-group identity
|
||||
- optionally serializer/schema version
|
||||
|
||||
Example shape:
|
||||
|
||||
- `immutable:Customer:basic:42`
|
||||
- `immutable:Customer:withAddresses:42`
|
||||
|
||||
---
|
||||
|
||||
## Recommended multi-level flow
|
||||
|
||||
### L1
|
||||
|
||||
Store actual read-only `EntityBean` instances.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- support `getIfPresent(id)`
|
||||
- avoid repeated deserialize cost
|
||||
- avoid network calls on row read path
|
||||
|
||||
### L2
|
||||
|
||||
Store serialized immutable snapshots.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- batch lookup only
|
||||
- support cross-JVM sharing
|
||||
- feed L1 with materialized immutable beans
|
||||
|
||||
### Loader
|
||||
|
||||
Use the existing query/fetch-group-based loader for misses.
|
||||
|
||||
### Suggested `getAll(ids)` flow
|
||||
|
||||
1. Check L1
|
||||
2. Batch remaining ids to L2
|
||||
3. Deserialize L2 hits into read-only beans
|
||||
4. Put those beans into L1
|
||||
5. Batch remaining misses to DB loader
|
||||
6. Freeze / ensure read-only beans
|
||||
7. Write through to L2
|
||||
8. Put into L1
|
||||
9. Negative-cache true misses if desired
|
||||
|
||||
---
|
||||
|
||||
## Invalidation is more important than serialization
|
||||
|
||||
Things to think about:
|
||||
|
||||
- update/delete invalidation across JVMs
|
||||
- local L1 invalidation when L2 entry is removed
|
||||
- ordering relative to DB commit
|
||||
- multiple cache instances for the same bean type but different fetch groups
|
||||
- tenant-scoped invalidation
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- keep current immutable-cache invalidation semantics
|
||||
- add a remote invalidation/event mechanism for L2-backed caches
|
||||
- each JVM should evict affected L1 entries when notified
|
||||
|
||||
Examples:
|
||||
|
||||
- Redis: pub/sub or streams
|
||||
- Postgres cache table: NOTIFY/listen, polling, or invalidation table/outbox pattern
|
||||
|
||||
---
|
||||
|
||||
## Serialization format considerations
|
||||
|
||||
## JSON
|
||||
|
||||
### Pros
|
||||
|
||||
- human readable / debuggable
|
||||
- easier rolling upgrades
|
||||
- field-name based, so generally more tolerant of schema evolution
|
||||
- good fit for Redis strings or Postgres JSONB
|
||||
- easier operational debugging
|
||||
|
||||
### Cons
|
||||
|
||||
- larger payloads
|
||||
- more CPU to serialize/deserialize
|
||||
- nested graphs / enums / dates / inheritance need disciplined handling
|
||||
|
||||
## Kryo / generic binary serialization
|
||||
|
||||
### Pros
|
||||
|
||||
- smaller payloads
|
||||
- often faster than JSON
|
||||
- can preserve object graphs efficiently
|
||||
|
||||
### Cons
|
||||
|
||||
- more fragile across versions and rolling deploys
|
||||
- class registration / compatibility pain
|
||||
- harder to inspect/debug
|
||||
- tighter coupling to JVM/class layout
|
||||
- riskier for long-lived shared cache entries
|
||||
|
||||
## Recommendation
|
||||
|
||||
For a first remote/shared implementation:
|
||||
|
||||
- prefer **JSON** or another self-describing structured format
|
||||
- if a binary format is later needed, prefer a stable schema-based format over generic object-graph serialization
|
||||
- **do not start with Kryo** unless short-lived entries and tight deployment coordination are acceptable
|
||||
|
||||
---
|
||||
|
||||
## What to serialize
|
||||
|
||||
Avoid thinking in terms of serializing arbitrary live entity bean graphs directly.
|
||||
|
||||
A cleaner model is:
|
||||
|
||||
- serialize a **snapshot representation**
|
||||
- deserialize into a fresh entity bean
|
||||
- mark loaded properties appropriately
|
||||
- freeze / ensure read-only state
|
||||
- store the resulting materialized bean in L1
|
||||
|
||||
This gives more control over:
|
||||
|
||||
- loaded-property semantics
|
||||
- read-only state
|
||||
- subtype handling
|
||||
- schema/version evolution
|
||||
|
||||
## Practical recommendation
|
||||
|
||||
Remote cache entries should represent exactly the configured fetch-group snapshot.
|
||||
|
||||
That means:
|
||||
|
||||
- cache what the fetch group loaded
|
||||
- include nested associations loaded by that fetch group
|
||||
- treat it as a self-contained immutable snapshot
|
||||
|
||||
This is simpler than trying to normalize the graph into many remote cache fragments and re-link it later.
|
||||
|
||||
---
|
||||
|
||||
## Redis vs Postgres cache table
|
||||
|
||||
## Redis
|
||||
|
||||
### Good for
|
||||
|
||||
- low latency
|
||||
- batch lookup via MGET / pipelining
|
||||
- TTL/eviction support
|
||||
- natural shared-cache use case
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- extra infrastructure
|
||||
- memory cost
|
||||
- invalidation/event coordination still required
|
||||
|
||||
## Postgres cache table (including unlogged-style approach)
|
||||
|
||||
### Good for
|
||||
|
||||
- simpler ops if Postgres is already present
|
||||
- easy batch lookup with `IN (...)`
|
||||
- fewer moving parts than introducing Redis
|
||||
|
||||
### Tradeoffs
|
||||
|
||||
- slower than Redis for hot shared-cache usage
|
||||
- adds pressure to Postgres
|
||||
- TTL/cleanup becomes application responsibility
|
||||
- still network/database I/O, so should remain off the `getIfPresent()` hot path
|
||||
|
||||
## Recommendation
|
||||
|
||||
- if the goal is a serious shared L2 cache, Redis is the more natural fit
|
||||
- if the goal is pragmatic shared caching with minimal extra infrastructure, Postgres can work but should still be treated as L2-only
|
||||
|
||||
---
|
||||
|
||||
## Versioning / evolution
|
||||
|
||||
Whatever serializer is used, include versioning information.
|
||||
|
||||
Useful dimensions:
|
||||
|
||||
- serializer/schema version
|
||||
- cache implementation version
|
||||
- fetch-group/cache identity version
|
||||
|
||||
This helps when:
|
||||
|
||||
- fields are added/removed
|
||||
- graph shape changes
|
||||
- fetch-group definitions evolve
|
||||
|
||||
---
|
||||
|
||||
## Compression
|
||||
|
||||
If remote snapshots become large:
|
||||
|
||||
- compress only above a size threshold
|
||||
- avoid compressing tiny payloads
|
||||
|
||||
This is especially relevant for JSON in Redis or Postgres L2.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
A multi-level cache should expose at least:
|
||||
|
||||
- L1 hit rate
|
||||
- L2 hit rate
|
||||
- DB loader rate
|
||||
- deserialize failures
|
||||
- invalidation counts
|
||||
- average payload size
|
||||
- cold-start amplification
|
||||
|
||||
Without this, it will be hard to judge whether the remote cache is helping.
|
||||
|
||||
---
|
||||
|
||||
## Overall recommended architecture
|
||||
|
||||
### Recommended model
|
||||
|
||||
- **L1**: actual read-only `EntityBean` instances
|
||||
- **L2**: serialized immutable snapshots
|
||||
- **Loader**: fetch-group-based DB query
|
||||
|
||||
### Method responsibilities
|
||||
|
||||
- `getIfPresent(id)` => **L1 only**
|
||||
- `getAll(ids)` => **L1 + L2 + DB loader** in batches
|
||||
|
||||
This aligns well with the current `AssocOneHelp` optimization and keeps the row-read path fast.
|
||||
|
||||
---
|
||||
|
||||
## Bottom line
|
||||
|
||||
If/when multi-level immutable caching is explored, the main points to preserve are:
|
||||
|
||||
1. keep `getIfPresent()` local-only
|
||||
2. do remote work only in batched `getAll()`
|
||||
3. key by type + id + tenant + fetch-group/cache identity
|
||||
4. treat remote values as immutable snapshots
|
||||
5. prefer JSON/self-describing format first
|
||||
6. be cautious with generic binary serializers like Kryo
|
||||
|
||||
---
|
||||
|
||||
## Possible follow-up
|
||||
|
||||
If this becomes active design work later, consider promoting these notes into one of:
|
||||
|
||||
- a dedicated design note under `docs/notes/`
|
||||
- a GitHub issue / discussion for design iteration
|
||||
- a lightweight ADR if this becomes a committed architectural direction
|
||||
+32
-56
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<artifactId>ebean-parent</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.2.0</version>
|
||||
<version>14.1.0</version>
|
||||
</parent>
|
||||
|
||||
<name>ebean api</name>
|
||||
@@ -26,16 +26,20 @@
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
Class retention Nonnull and Nullable annotations
|
||||
to assist with IDE auto-completion with Ebean API
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.jspecify</groupId>
|
||||
<artifactId>jspecify</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-lang</artifactId>
|
||||
<version>1.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-config</artifactId>
|
||||
<version>4.2</version>
|
||||
<version>3.12</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -70,13 +74,15 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson core used internally by Ebean -->
|
||||
<dependency>
|
||||
<groupId>io.avaje</groupId>
|
||||
<artifactId>avaje-json-core</artifactId>
|
||||
<version>${avaje-json-core.version}</version>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
|
||||
<!-- provided scope for JsonNode support -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
@@ -84,6 +90,23 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- JAVAX-DEPENDENCY-START ___
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
____ JAVAX-DEPENDENCY-END -->
|
||||
<!-- JAKARTA-DEPENDENCY-START -->
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
<version>6.0.0</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- JAKARTA-DEPENDENCY-END -->
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -103,53 +126,6 @@
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<release>11</release>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>compile-21</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<release>21</release>
|
||||
<compileSourceRoots>
|
||||
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
|
||||
</compileSourceRoots>
|
||||
<multiReleaseOutput>true</multiReleaseOutput>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<addMavenDescriptor>false</addMavenDescriptor>
|
||||
<manifestEntries>
|
||||
<Multi-Release>true</Multi-Release>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
<!-- <manifest>-->
|
||||
<!-- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>-->
|
||||
<!-- </manifest>-->
|
||||
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -19,7 +20,7 @@ import java.util.concurrent.TimeUnit;
|
||||
* This also propagates MDC context from the current thread to the
|
||||
* background task if defined.
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface BackgroundExecutor {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Unsupported access of a property on an entity bean.
|
||||
* <p>
|
||||
* Attempted a lazy load operation on a bean that has disabled lazy loading
|
||||
* or attempt to mutate an unmodifiable bean.
|
||||
*/
|
||||
public class BeanAccessException extends UnsupportedOperationException {
|
||||
private static final long serialVersionUID = 1;
|
||||
|
||||
/**
|
||||
* Create with no message.
|
||||
*/
|
||||
public BeanAccessException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create with message.
|
||||
*/
|
||||
public BeanAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.Optional;
|
||||
*
|
||||
* @see BeanRepository
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public abstract class BeanFinder<I,T> {
|
||||
|
||||
protected final Database database;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.ebean.bean.EntityBean;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -36,7 +36,7 @@ import java.util.Collection;
|
||||
* @param <I> The ID type
|
||||
* @param <T> The Bean type
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public abstract class BeanRepository<I, T> extends BeanFinder<I, T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,7 +89,12 @@ public interface BeanState {
|
||||
* <p>
|
||||
* If a setter is called on a readOnly bean it will throw an exception.
|
||||
*/
|
||||
boolean isUnmodifiable();
|
||||
boolean isReadOnly();
|
||||
|
||||
/**
|
||||
* Set the readOnly status for the bean.
|
||||
*/
|
||||
void setReadOnly(boolean readOnly);
|
||||
|
||||
/**
|
||||
* Advanced - Used to programmatically build a partially or fully loaded
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import io.ebean.annotation.TxIsolation;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
import io.ebean.plugin.Property;
|
||||
@@ -57,7 +57,7 @@ import java.util.concurrent.Callable;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public final class DB {
|
||||
|
||||
private static final DbContext context = DbContext.getInstance();
|
||||
@@ -605,7 +605,7 @@ public final class DB {
|
||||
* // find orders and their customers
|
||||
* List<Order> list = DB.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .orderBy("id")
|
||||
* .order("id")
|
||||
* .findList();
|
||||
*
|
||||
* // sort by customer name ascending, then by order shipDate
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
final class DPaging implements Paging {
|
||||
|
||||
static final Paging NONE = new DPaging(0, 0, null);
|
||||
|
||||
static Paging build(int pgIndex, int pgSize, OrderBy<?> orderBy) {
|
||||
return new DPaging(pgIndex, pgSize, orderBy);
|
||||
}
|
||||
|
||||
static Paging build(int pgIndex, int pgSize) {
|
||||
return new DPaging(pgIndex, pgSize, null);
|
||||
}
|
||||
|
||||
private final int pageNumber;
|
||||
private final int pageSize;
|
||||
private final OrderBy<?> orderBy;
|
||||
|
||||
DPaging(int pageNumber, int pageSize, OrderBy<?> orderBy) {
|
||||
this.pageNumber = pageNumber;
|
||||
this.pageSize = pageSize;
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pageIndex() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int pageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrderBy<?> orderBy() {
|
||||
return orderBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Paging withPage(int pageNumber) {
|
||||
return new DPaging(pageNumber, pageSize, orderBy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Paging withOrderBy(String orderByClause) {
|
||||
return new DPaging(pageNumber, pageSize, OrderBy.of(orderByClause));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import io.ebean.annotation.Platform;
|
||||
import io.ebean.annotation.TxIsolation;
|
||||
import io.ebean.cache.ServerCacheManager;
|
||||
@@ -23,18 +23,11 @@ import java.util.concurrent.Callable;
|
||||
/**
|
||||
* Provides the API for fetching and saving beans to a particular database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are typically constructed via {@link #builder()} and {@link DatabaseBuilder#build()}.
|
||||
* They can also be automatically constructed on demand using configuration information in
|
||||
* the application.properties file. The underlying implementation is provided by
|
||||
* {@link DatabaseFactory}.
|
||||
*
|
||||
* <h5>Registration with the DB singleton</h5>
|
||||
* <p>
|
||||
* When a Database instance is created it can be registered with the {@link DB}
|
||||
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
|
||||
* singleton is essentially a map of {@link Database}'s that have been registered
|
||||
* When a Database instance is created it can be registered with the DB
|
||||
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
|
||||
* singleton is essentially a map of Database's that have been registered
|
||||
* with it.
|
||||
* <p>
|
||||
* The Database can then be retrieved later via {@link DB#byName(String)}.
|
||||
@@ -42,10 +35,16 @@ import java.util.concurrent.Callable;
|
||||
* <h5>The 'default' Database</h5>
|
||||
* <p>
|
||||
* One Database can be designated as the 'default' or 'primary' Database
|
||||
* (see {@link DatabaseBuilder#defaultDatabase(boolean)}). Many methods on {@link DB}
|
||||
* (see {@link DatabaseConfig#setDefaultServer(boolean)}). Many methods on DB
|
||||
* such as {@link DB#find(Class)} etc are actually just a convenient way to
|
||||
* call methods on the 'default/primary' Database.
|
||||
*
|
||||
* <h5>Constructing a Database</h5>
|
||||
* <p>
|
||||
* Databases are constructed by the DatabaseFactory. They can be created
|
||||
* programmatically via {@link DatabaseFactory#create(DatabaseBuilder)} or they
|
||||
* can be automatically constructed on demand using configuration information in
|
||||
* the application.properties file.
|
||||
*
|
||||
* <h5>Example: Get a Database</h5>
|
||||
* <pre>{@code
|
||||
@@ -81,11 +80,10 @@ import java.util.concurrent.Callable;
|
||||
* method. Example: a single thread requires more than one transaction.
|
||||
*
|
||||
* @see DB
|
||||
* @see DatabaseBuilder
|
||||
* @see DatabaseFactory
|
||||
* @see DatabaseConfig
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface Database {
|
||||
|
||||
/**
|
||||
@@ -96,13 +94,11 @@ public interface Database {
|
||||
* // from application.properties / application.yaml
|
||||
*
|
||||
* Database db = Database.builder()
|
||||
* .name("db")
|
||||
* .loadFromProperties()
|
||||
* .build();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
static DatabaseBuilder builder() {
|
||||
return new DatabaseConfig();
|
||||
}
|
||||
@@ -141,7 +137,6 @@ public interface Database {
|
||||
/**
|
||||
* Return the associated read only DataSource for this Database instance (can be null).
|
||||
*/
|
||||
@Nullable
|
||||
DataSource readOnlyDataSource();
|
||||
|
||||
/**
|
||||
@@ -405,7 +400,7 @@ public interface Database {
|
||||
* // find orders and their customers
|
||||
* List<Order> list = database.find(Order.class)
|
||||
* .fetch("customer")
|
||||
* .orderBy("id")
|
||||
* .order("id")
|
||||
* .findList();
|
||||
*
|
||||
* // sort by customer name ascending, then by order shipDate
|
||||
@@ -777,6 +772,18 @@ public interface Database {
|
||||
*/
|
||||
<T> T reference(Class<T> beanType, Object id);
|
||||
|
||||
/**
|
||||
* Return the extended API for Database.
|
||||
* <p>
|
||||
* The extended API has the options for executing queries that take an explicit
|
||||
* transaction as an argument.
|
||||
* <p>
|
||||
* Typically, we only need to use the extended API when we do NOT want to use the
|
||||
* usual ThreadLocal based mechanism to obtain the current transaction but instead
|
||||
* supply the transaction explicitly.
|
||||
*/
|
||||
ExtendedServer extended();
|
||||
|
||||
/**
|
||||
* Either Insert or Update the bean depending on its state.
|
||||
* <p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import io.avaje.json.stream.JsonStream;
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import io.ebean.annotation.*;
|
||||
import io.ebean.cache.ServerCachePlugin;
|
||||
import io.ebean.config.*;
|
||||
@@ -61,10 +61,6 @@ public interface DatabaseBuilder {
|
||||
|
||||
/**
|
||||
* Build and return the Database instance.
|
||||
* <p>
|
||||
* When {@link #setRegister(boolean)} is set to true, and a database with the same
|
||||
* name is already registered, this may return the existing registered database
|
||||
* rather than creating a new one.
|
||||
*/
|
||||
Database build();
|
||||
|
||||
@@ -364,18 +360,18 @@ public interface DatabaseBuilder {
|
||||
DatabaseBuilder putServiceObject(Object configObject);
|
||||
|
||||
/**
|
||||
* Set the JsonStream to use.
|
||||
* Set the Jackson JsonFactory to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
default DatabaseBuilder jsonStream(JsonStream jsonStream) {
|
||||
return setJsonStream(jsonStream);
|
||||
default DatabaseBuilder jsonFactory(JsonFactory jsonFactory) {
|
||||
return setJsonFactory(jsonFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #jsonStream(JsonStream)}.
|
||||
* @deprecated migrate to {@link #jsonFactory(JsonFactory)}.
|
||||
*/
|
||||
DatabaseBuilder setJsonStream(JsonStream jsonStream);
|
||||
DatabaseBuilder setJsonFactory(JsonFactory jsonFactory);
|
||||
|
||||
/**
|
||||
* Set the JSON format to use for DateTime types.
|
||||
@@ -727,6 +723,19 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setReadAuditPrepare(ReadAuditPrepare readAuditPrepare);
|
||||
|
||||
/**
|
||||
* Set the configuration for profiling.
|
||||
*/
|
||||
default DatabaseBuilder profilingConfig(ProfilingConfig profilingConfig) {
|
||||
return setProfilingConfig(profilingConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #profilingConfig(ProfilingConfig)}.
|
||||
*/
|
||||
@Deprecated
|
||||
DatabaseBuilder setProfilingConfig(ProfilingConfig profilingConfig);
|
||||
|
||||
/**
|
||||
* Set the suffix appended to the base table to derive the view that contains the union
|
||||
* of the base table and the history table in order to support asOf queries.
|
||||
@@ -887,13 +896,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper);
|
||||
|
||||
/**
|
||||
* Enable tenant-partitioned caches. When enabled each tenant gets its own cache namespace,
|
||||
* improving cache-hit ratio by preventing cross-tenant key collisions.
|
||||
* Use {@link SpiCacheManager#clearTenant(Object)} when a tenant is deactivated.
|
||||
*/
|
||||
DatabaseBuilder tenantPartitionedCache(boolean tenantPartitionedCache);
|
||||
|
||||
/**
|
||||
* Set the L2 cache default max size.
|
||||
*/
|
||||
@@ -987,14 +989,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setNamingConvention(NamingConvention namingConvention);
|
||||
|
||||
/**
|
||||
* Set the AggregateFormulaContext which is used to determine if a database function
|
||||
* is an aggregate function (like sum, min, max, avg etc).
|
||||
* <p>
|
||||
* Use this to override the default known aggregation functions.
|
||||
*/
|
||||
DatabaseBuilder aggregateFormulaContext(AggregateFormulaContext aggregateFormulaContext);
|
||||
|
||||
/**
|
||||
* Set to true if all DB column and table names should use quoted identifiers.
|
||||
* <p>
|
||||
@@ -1068,11 +1062,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
DatabaseBuilder readOnlyDatabase(boolean readOnlyDatabase);
|
||||
|
||||
/**
|
||||
* Set to false such that the instance does not register a JVM shutdown hook.
|
||||
*/
|
||||
DatabaseBuilder shutdownHook(boolean shutdownHook);
|
||||
|
||||
/**
|
||||
* Set a DataSource.
|
||||
*/
|
||||
@@ -1979,8 +1968,8 @@ public interface DatabaseBuilder {
|
||||
DatabaseBuilder setLocalOnlyL2Cache(boolean localOnlyL2Cache);
|
||||
|
||||
/**
|
||||
* Controls if Ebean should ignore <code>@javax.validation.contstraints.NotNull</code> or
|
||||
* <code>@jakarta.validation.contstraints.NotNull</code>
|
||||
* Controls if Ebean should ignore <code>&x64;javax.validation.contstraints.NotNull</code> or
|
||||
* <code>&x64;jakarta.validation.contstraints.NotNull</code>
|
||||
* with respect to generating a <code>NOT NULL</code> column.
|
||||
* <p>
|
||||
* Normally when Ebean sees javax NotNull annotation it means that column is defined as NOT NULL.
|
||||
@@ -2030,11 +2019,6 @@ public interface DatabaseBuilder {
|
||||
@Deprecated
|
||||
DatabaseBuilder setQueryPlanTTLSeconds(int queryPlanTTLSeconds);
|
||||
|
||||
/**
|
||||
* Set the EXPLAIN (with options) to use for query plan capture.
|
||||
*/
|
||||
DatabaseBuilder queryPlanExplain(String queryPlanExplain);
|
||||
|
||||
/**
|
||||
* Create a new PlatformConfig based of the one held but with overridden properties by reading
|
||||
* properties with the given path and prefix.
|
||||
@@ -2232,7 +2216,7 @@ public interface DatabaseBuilder {
|
||||
*
|
||||
* @param includeLabelInSql When true include a SQL inline comment in generated SELECT queries.
|
||||
*/
|
||||
DatabaseBuilder includeLabelInSql(boolean includeLabelInSql);
|
||||
DatabaseConfig includeLabelInSql(boolean includeLabelInSql);
|
||||
|
||||
/**
|
||||
* Set the naming convention to apply to metrics names.
|
||||
@@ -2250,7 +2234,7 @@ public interface DatabaseBuilder {
|
||||
/**
|
||||
* Sets the length check mode.
|
||||
*/
|
||||
DatabaseBuilder lengthCheck(LengthCheck lengthCheck);
|
||||
DatabaseConfig lengthCheck(LengthCheck lengthCheck);
|
||||
|
||||
/**
|
||||
* Provides read access (getters) for the DatabaseBuilder configuration
|
||||
@@ -2265,11 +2249,11 @@ public interface DatabaseBuilder {
|
||||
boolean isAutoLoadModuleInfo();
|
||||
|
||||
/**
|
||||
* Return the JsonStream to use.
|
||||
* Return the Jackson JsonFactory to use.
|
||||
* <p>
|
||||
* If not set a default implementation will be used.
|
||||
*/
|
||||
JsonStream getJsonStream();
|
||||
JsonFactory getJsonFactory();
|
||||
|
||||
/**
|
||||
* Get the clock used for setting the timestamps (e.g. @UpdatedTimestamp) on objects.
|
||||
@@ -2488,6 +2472,11 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
TenantCatalogProvider getTenantCatalogProvider();
|
||||
|
||||
/**
|
||||
* Return the configuration for profiling.
|
||||
*/
|
||||
ProfilingConfig getProfilingConfig();
|
||||
|
||||
/**
|
||||
* Return the DB schema to use.
|
||||
*/
|
||||
@@ -2574,11 +2563,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean isAutoPersistUpdates();
|
||||
|
||||
/**
|
||||
* Return true if caches are partitioned by tenant.
|
||||
*/
|
||||
boolean isTenantPartitionedCache();
|
||||
|
||||
/**
|
||||
* Return the L2 cache default max size.
|
||||
*/
|
||||
@@ -2616,11 +2600,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
NamingConvention getNamingConvention();
|
||||
|
||||
/**
|
||||
* Return the AggregateFormulaContext.
|
||||
*/
|
||||
AggregateFormulaContext aggregateFormulaContext();
|
||||
|
||||
/**
|
||||
* Return true if all DB column and table names should use quoted identifiers.
|
||||
*/
|
||||
@@ -2659,11 +2638,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
boolean readOnlyDatabase();
|
||||
|
||||
/**
|
||||
* Return if a JVM shutdown hook should be registered.
|
||||
*/
|
||||
boolean shutdownHook();
|
||||
|
||||
/**
|
||||
* Return the DataSource.
|
||||
*/
|
||||
@@ -3070,11 +3044,6 @@ public interface DatabaseBuilder {
|
||||
*/
|
||||
int getQueryPlanTTLSeconds();
|
||||
|
||||
/**
|
||||
* Return the EXPLAIN (with options) to use for capturing query plans.
|
||||
*/
|
||||
String getQueryPlanExplain();
|
||||
|
||||
/**
|
||||
* Return mapping locations to search for xml mapping via class path search.
|
||||
*/
|
||||
|
||||
@@ -5,23 +5,23 @@ import io.ebean.service.SpiContainer;
|
||||
import io.ebean.service.SpiContainerFactory;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
|
||||
/**
|
||||
* Low-level factory for creating {@link Database} instances.
|
||||
* Creates Database instances.
|
||||
* <p>
|
||||
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
|
||||
* This factory remains for legacy creation entry points plus container lifecycle methods.
|
||||
* This uses either DatabaseConfig or properties in the application.properties file to
|
||||
* configure and create a Database instance.
|
||||
* <p>
|
||||
* The Database instance can either be registered with the {@link DB} singleton or
|
||||
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
|
||||
* If the Database is registered with the {@link DB} singleton you can retrieve it
|
||||
* The Database instance can either be registered with the DB singleton or
|
||||
* not. The DB singleton effectively holds a map of Database by a name.
|
||||
* If the Database is registered with the DB singleton you can retrieve it
|
||||
* later via {@link DB#byName(String)}.
|
||||
* <p>
|
||||
* One Database can be nominated as the 'default/primary' Database. Many
|
||||
* methods on the {@link DB} singleton such as {@link DB#find(Class)} are just a
|
||||
* methods on the DB singleton such as {@link DB#find(Class)} are just a
|
||||
* convenient way of using the 'default/primary' Database.
|
||||
*/
|
||||
public final class DatabaseFactory {
|
||||
@@ -38,8 +38,7 @@ public final class DatabaseFactory {
|
||||
* Initialise the container with clustering configuration.
|
||||
* <p>
|
||||
* Call this prior to creating any Database instances or alternatively set the
|
||||
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
|
||||
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
|
||||
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
|
||||
*/
|
||||
public static void initialiseContainer(ContainerConfig containerConfig) {
|
||||
lock.lock();
|
||||
@@ -51,11 +50,8 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using configuration loaded from properties for the given database name.
|
||||
*
|
||||
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
|
||||
* Create using properties to configure the database.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Database create(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
@@ -66,35 +62,34 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link DatabaseBuilder#build()}.
|
||||
* Create using the DatabaseConfig object to configure the database.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* DatabaseConfig config = new DatabaseConfig();
|
||||
* config.setName("db");
|
||||
* config.loadProperties();
|
||||
*
|
||||
* Database database = DatabaseFactory.create(config);
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public static Database create(DatabaseBuilder builder) {
|
||||
lock.lock();
|
||||
try {
|
||||
var config = builder.settings();
|
||||
var name = config.getName();
|
||||
if (name == null) {
|
||||
if (config.getName() == null) {
|
||||
throw new PersistenceException("The name is null (it is required)");
|
||||
}
|
||||
if (config.isRegister()) {
|
||||
// We're explicitly creating a database to be registered, so avoid
|
||||
// triggering DbContext static initialisation to auto-create a default one.
|
||||
DbPrimary.setSkip(true);
|
||||
Database existing = DbContext.getInstance().getRegistered(name);
|
||||
if (existing != null) {
|
||||
EbeanVersion.log.log(WARNING, "Using existing database with name:{0}", name);
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
Database server = createInternal(config);
|
||||
if (config.isRegister()) {
|
||||
if (config.isDefaultServer()) {
|
||||
if (defaultServerName != null && !defaultServerName.equals(name)) {
|
||||
throw new IllegalStateException("Registering [" + name + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
if (defaultServerName != null && !defaultServerName.equals(config.getName())) {
|
||||
throw new IllegalStateException("Registering [" + config.getName() + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
||||
}
|
||||
defaultServerName = name;
|
||||
defaultServerName = config.getName();
|
||||
}
|
||||
DbPrimary.setSkip(true);
|
||||
DbContext.getInstance().register(server, config.isDefaultServer());
|
||||
}
|
||||
return server;
|
||||
@@ -104,8 +99,7 @@ public final class DatabaseFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
|
||||
* context class loader.
|
||||
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
|
||||
*/
|
||||
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
|
||||
lock.lock();
|
||||
@@ -163,10 +157,10 @@ public final class DatabaseFactory {
|
||||
* Create the container instance using the configuration.
|
||||
*/
|
||||
private static SpiContainer createContainer(ContainerConfig containerConfig) {
|
||||
SpiContainerFactory factory = XBootstrapService.containerFactory();
|
||||
if (factory == null) {
|
||||
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
||||
Iterator<SpiContainerFactory> factories = ServiceLoader.load(SpiContainerFactory.class).iterator();
|
||||
if (factories.hasNext()) {
|
||||
return factories.next().create(containerConfig);
|
||||
}
|
||||
return factory.create(containerConfig);
|
||||
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
|
||||
import io.ebean.datasource.DataSourceConfigurationException;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
@@ -77,11 +75,6 @@ final class DbContext {
|
||||
return defaultDatabase;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Database getRegistered(String name) {
|
||||
return concMap.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the database by name.
|
||||
*/
|
||||
@@ -99,7 +92,6 @@ final class DbContext {
|
||||
/**
|
||||
* Read, create and put of Databases.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private Database getWithCreate(String name) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import io.ebean.docstore.DocQueryContext;
|
||||
import io.ebean.docstore.RawDoc;
|
||||
|
||||
@@ -14,7 +14,7 @@ import java.util.function.Predicate;
|
||||
/**
|
||||
* Document storage operations.
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface DocumentStore {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
@@ -42,7 +42,7 @@ import java.util.stream.Stream;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface DtoQuery<T> extends CancelableQuery {
|
||||
|
||||
/**
|
||||
@@ -140,17 +140,6 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
* Bind the named multi-value array parameter which we would use with Postgres ANY.
|
||||
* <p>
|
||||
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = "select id, name from o_customer where id = any(:idList)";
|
||||
*
|
||||
* var ids = List.of(1, 2, 3);
|
||||
*
|
||||
* List<CustomerDto> list2 = DB.findDto(CustomerDto.class, sql)
|
||||
* .setArrayParameter("idList", ids)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
DtoQuery<T> setArrayParameter(String name, Collection<?> values);
|
||||
|
||||
@@ -234,16 +223,5 @@ public interface DtoQuery<T> extends CancelableQuery {
|
||||
* source. We use {@code usingMaster()} to instead ensure that the query is executed
|
||||
* against the master data source.
|
||||
*/
|
||||
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);
|
||||
|
||||
DtoQuery<T> usingMaster();
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* @deprecated migrate to using {@link PagedList#emptyList()} only.
|
||||
* <p>
|
||||
* An empty PagedList.
|
||||
* <p>
|
||||
* For use in application code when we need to return a PagedList but don't want to
|
||||
@@ -19,8 +17,7 @@ import java.util.concurrent.Future;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public final class EmptyPagedList<T> implements PagedList<T> {
|
||||
public class EmptyPagedList<T> implements PagedList<T> {
|
||||
|
||||
@Override
|
||||
public void loadCount() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import io.ebean.search.*;
|
||||
|
||||
import jakarta.persistence.NonUniqueResultException;
|
||||
@@ -31,7 +31,7 @@ import java.util.function.Predicate;
|
||||
*
|
||||
* @see Query#where()
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface ExpressionList<T> {
|
||||
|
||||
/**
|
||||
@@ -53,6 +53,14 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
Query<T> orderById(boolean orderById);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy(String)}
|
||||
*/
|
||||
@Deprecated(since = "13.19", forRemoval = true)
|
||||
default ExpressionList<T> order(String orderByClause) {
|
||||
return orderBy(orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order by clause replacing the existing order by clause if there is
|
||||
* one.
|
||||
@@ -63,6 +71,14 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> orderBy(String orderBy);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy()}.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
default OrderBy<T> order() {
|
||||
return orderBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the OrderBy so that you can append an ascending or descending
|
||||
* property to the order by clause.
|
||||
@@ -206,20 +222,19 @@ public interface ExpressionList<T> {
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* @deprecated migrate to {@link #usingTransaction(Transaction)} then delete().
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* Execute as a delete query deleting the 'root level' beans that match the predicates
|
||||
* in the query.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
* </p>
|
||||
*
|
||||
* @return the number of rows that were permanently deleted.
|
||||
* @return the number of rows that were deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
@Deprecated(forRemoval = true, since = "13.1.0")
|
||||
int delete(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute as a update query.
|
||||
@@ -229,6 +244,17 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
int update();
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #usingTransaction(Transaction)} then update().
|
||||
* <p>
|
||||
* Execute as a update query with the given transaction.
|
||||
*
|
||||
* @return the number of rows that were updated.
|
||||
* @see UpdateQuery
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "13.1.0")
|
||||
int update(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the query returning true if a row is found.
|
||||
* <p>
|
||||
@@ -329,7 +355,7 @@ public interface ExpressionList<T> {
|
||||
* List<String> names =
|
||||
* DB.find(Customer.class)
|
||||
* .select("name")
|
||||
* .orderBy().asc("name")
|
||||
* .order().asc("name")
|
||||
* .findSingleAttributeList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -342,7 +368,7 @@ public interface ExpressionList<T> {
|
||||
* .setDistinct(true)
|
||||
* .select("name")
|
||||
* .where().eq("status", Customer.Status.NEW)
|
||||
* .orderBy().asc("name")
|
||||
* .order().asc("name")
|
||||
* .setMaxRows(100)
|
||||
* .findSingleAttributeList();
|
||||
*
|
||||
@@ -1089,14 +1115,6 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> like(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>likeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> likeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Like - property like value where the value contains the
|
||||
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
|
||||
@@ -1104,41 +1122,17 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> ilike(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive LIKE if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>ilikeIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> ilikeIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Starts With - property like value%.
|
||||
*/
|
||||
ExpressionList<T> startsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>startsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> startsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Starts With - property like value%. Typically uses a
|
||||
* lower() function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> istartsWith(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive STARTS WITH if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>istartsWithIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> istartsWithIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Ends With - property like %value.
|
||||
*/
|
||||
@@ -1155,28 +1149,12 @@ public interface ExpressionList<T> {
|
||||
*/
|
||||
ExpressionList<T> contains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>containsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> containsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* Case insensitive Contains - property like %value%. Typically uses a lower()
|
||||
* function to make the expression case insensitive.
|
||||
*/
|
||||
ExpressionList<T> icontains(String propertyName, String value);
|
||||
|
||||
/**
|
||||
* Is case insensitive CONTAINS if value is non-null and otherwise no expression is added to the query.
|
||||
* <p>
|
||||
* This is effectively a helper method that allows a query to be built in fluid style where some predicates are
|
||||
* effectively optional. We can use <code>icontainsIfPresent()</code> rather than having a separate if block.
|
||||
*/
|
||||
ExpressionList<T> icontainsIfPresent(String propertyName, @Nullable String value);
|
||||
|
||||
/**
|
||||
* In expression using pairs of value objects.
|
||||
*/
|
||||
@@ -1735,7 +1713,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .orderBy().asc("name")
|
||||
* .order().asc("name")
|
||||
* .findList();
|
||||
* }</pre>
|
||||
*/
|
||||
@@ -1756,7 +1734,7 @@ public interface ExpressionList<T> {
|
||||
* .or()
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .isNull("anniversary")
|
||||
* .orderBy().asc("name")
|
||||
* .order().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1776,7 +1754,7 @@ public interface ExpressionList<T> {
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .orderBy().asc("name")
|
||||
* .order().asc("name")
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
@@ -1810,7 +1788,7 @@ public interface ExpressionList<T> {
|
||||
* .gt("id", 1)
|
||||
* .eq("anniversary", onAfter)
|
||||
* .endNot()
|
||||
* .orderBy()
|
||||
* .order()
|
||||
* .asc("name")
|
||||
* .findList();
|
||||
*
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.ebean;
|
||||
|
||||
import java.time.Clock;
|
||||
|
||||
/**
|
||||
* The extended API for Database.
|
||||
*/
|
||||
public interface ExtendedServer {
|
||||
|
||||
/**
|
||||
* Deprecated but no yet determined suitable replacement (to support testing only change of clock).
|
||||
* <p>
|
||||
* Set the Clock to use for <code>@WhenCreated</code> and <code>@WhenModified</code>.
|
||||
* <p>
|
||||
* Note that we only expect to change the Clock for testing purposes.
|
||||
* </p>
|
||||
*/
|
||||
@Deprecated
|
||||
void setClock(Clock clock);
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.ebean.service.SpiFetchGroupQuery;
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ import io.ebean.service.SpiFetchGroupQuery;
|
||||
*
|
||||
* @param <T> The bean type the Fetch group can be applied to
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface FetchGroup<T> {
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ public interface FetchGroup<T> {
|
||||
* @return The FetchGroup with the given select clause
|
||||
*/
|
||||
static <T> FetchGroup<T> of(Class<T> cls, String select) {
|
||||
return XBootstrapService.fetchGroupOf(cls, select);
|
||||
return XServiceProvider.fetchGroupOf(cls, select);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,14 +108,14 @@ public interface FetchGroup<T> {
|
||||
* @return The FetchGroupBuilder with the given select clause which we will add fetch clauses to
|
||||
*/
|
||||
static <T> FetchGroupBuilder<T> of(Class<T> cls) {
|
||||
return XBootstrapService.fetchGroupOf(cls);
|
||||
return XServiceProvider.fetchGroupOf(cls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a query to be used by query beans for constructing FetchGroup.
|
||||
*/
|
||||
static <T> SpiFetchGroupQuery<T> queryFor(Class<T> beanType) {
|
||||
return XBootstrapService.fetchGroupQueryFor(beanType);
|
||||
return XServiceProvider.fetchGroupQueryFor(beanType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
|
||||
/**
|
||||
* Builds a FetchGroup by adding fetch clauses.
|
||||
@@ -23,7 +23,7 @@ import org.jspecify.annotations.NullMarked;
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface FetchGroupBuilder<T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -79,7 +79,7 @@ import java.util.Set;
|
||||
*
|
||||
* @param <T> the entity bean type
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface Filter<T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
import io.avaje.lang.Nullable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ import java.util.List;
|
||||
* public List<Customer> findNew() {
|
||||
* return query().where()
|
||||
* .eq("status", Customer.Status.NEW)
|
||||
* .orderBy("name")
|
||||
* .order("name")
|
||||
* .findList()
|
||||
* }
|
||||
* }
|
||||
@@ -60,7 +60,7 @@ import java.util.List;
|
||||
* @see BeanRepository
|
||||
* @see BeanFinder
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public class Finder<I, T> {
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* FutureMap represents the result of a background query execution that will
|
||||
* return a map of entities.
|
||||
* <p>
|
||||
* It extends the java.util.concurrent.Future with the ability to cancel the
|
||||
* query, check if it is finished and get the resulting list waiting for the
|
||||
* query to finish (ie. the standard features of java.util.concurrent.Future).
|
||||
* </p>
|
||||
* <p>
|
||||
* A simple example:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
*
|
||||
* // create a query to find all orders
|
||||
* Query<Long,Order> query = DB.find(Order.class)
|
||||
* .setMapKey("id");
|
||||
*
|
||||
* // execute the query in a background thread
|
||||
* // immediately returning the futureMap
|
||||
* FutureMap<Long,Order> futureMap = query.findFutureMap();
|
||||
*
|
||||
* // do something else ...
|
||||
*
|
||||
* if (!futureMap.isDone()){
|
||||
* // we can cancel the query execution. This will cancel
|
||||
* // the underlying query if that is supported by the JDBC
|
||||
* // driver and database
|
||||
* futureMap.cancel(true);
|
||||
* }
|
||||
*
|
||||
* if (!futureMap.isCancelled()){
|
||||
* // wait for the query to finish and return the map
|
||||
* Map<Long,Order> map = futureMap.get();
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public interface FutureMap<K, T> extends Future<Map<K, T>> {
|
||||
|
||||
/**
|
||||
* Return the query that is being executed by a background thread.
|
||||
*/
|
||||
Query<T> getQuery();
|
||||
|
||||
/**
|
||||
* Same as {@link #get()} but wraps InterruptedException and ExecutionException in the
|
||||
* unchecked PersistenceException.
|
||||
*
|
||||
* @return The query list result
|
||||
* @throws PersistenceException when a InterruptedException or ExecutionException occurs.
|
||||
*/
|
||||
Map<K, T> getUnchecked();
|
||||
|
||||
/**
|
||||
* Same as {@link #get(long, TimeUnit)} but wraps InterruptedException
|
||||
* and ExecutionException in the unchecked PersistenceException.
|
||||
*
|
||||
* @return The query list result
|
||||
* @throws TimeoutException if the wait timed out
|
||||
* @throws PersistenceException if a InterruptedException or ExecutionException occurs.
|
||||
*/
|
||||
Map<K, T> getUnchecked(long timeout, TimeUnit unit) throws TimeoutException;
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Query-scoped immutable bean cache.
|
||||
*
|
||||
* <p>Typical use is to attach an immutable cache to a query and let Ebean use it when
|
||||
* resolving assoc-one references.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
|
||||
* .select("name,version")
|
||||
* .fetch("billingAddress", "line1,city")
|
||||
* .fetch("shippingAddress", "line1,city")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
|
||||
* .loading(database, customerGroup)
|
||||
* .build();
|
||||
*
|
||||
* Order order = database.find(Order.class)
|
||||
* .setId(id)
|
||||
* .setUnmodifiable(true)
|
||||
* .using(customerCache)
|
||||
* .findOne();
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> The bean type.
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableBeanCache<T> {
|
||||
|
||||
/**
|
||||
* Return the bean type this cache provides values for.
|
||||
*/
|
||||
Class<T> type();
|
||||
|
||||
/**
|
||||
* Return immutable cached beans by id (loading and populating misses as needed).
|
||||
*/
|
||||
Map<Object, T> getAll(Set<Object> ids);
|
||||
|
||||
/**
|
||||
* Return a cached bean for the given id if it is already present.
|
||||
* <p>
|
||||
* This does not trigger loading or record a miss.
|
||||
*/
|
||||
default @Nullable T getIfPresent(Object id) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import io.ebean.service.SpiImmutableCacheFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
/**
|
||||
* Utility factory methods for {@link ImmutableBeanCache}.
|
||||
*
|
||||
* <p>Use {@link #builder(Class)} when you want explicit cache policy controls (for example
|
||||
* max size or TTL). Use {@link #loading(Class, Database, FetchGroup)} as a shorthand for
|
||||
* query-loader-backed memoization.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
@NullMarked
|
||||
public final class ImmutableBeanCaches {
|
||||
|
||||
private ImmutableBeanCaches() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a builder for immutable bean caches.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, FetchGroup.of(MyRef.class, "version"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableCacheBuilder<T> builder(Class<T> type) {
|
||||
SpiImmutableCacheFactory factory = XBootstrapService.immutableCacheFactory();
|
||||
if (factory != null) {
|
||||
return factory.builder(type);
|
||||
}
|
||||
return new LoadingBuilder<>(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a loader-backed immutable bean cache that memoizes both hits and misses.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(MyRef.class, ids ->
|
||||
* database.find(MyRef.class)
|
||||
* .setUnmodifiable(true)
|
||||
* .where().idIn(ids)
|
||||
* .findMap()
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @param type The bean type.
|
||||
* @param loader Batch loader for unresolved ids.
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
return builder(type).loader(loader).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a query-loader-backed immutable bean cache.
|
||||
*
|
||||
* <pre>{@code
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.loading(
|
||||
* MyRef.class,
|
||||
* database,
|
||||
* FetchGroup.of(MyRef.class, "version")
|
||||
* );
|
||||
* }</pre>
|
||||
*/
|
||||
public static <T> ImmutableBeanCache<T> loading(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
return builder(type).loading(db, fetchGroup).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a batch loader backed by an unmodifiable query using the given fetch group.
|
||||
*/
|
||||
public static <T> Function<Set<Object>, Map<Object, T>> queryLoader(Database db, Class<T> type, FetchGroup<T> fetchGroup) {
|
||||
return new QueryLoader<>(type, db, fetchGroup);
|
||||
}
|
||||
|
||||
private static final class QueryLoader<T> implements Function<Set<Object>, Map<Object, T>> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Database db;
|
||||
private final FetchGroup<T> fetchGroup;
|
||||
|
||||
QueryLoader(Class<T> type, Database db, FetchGroup<T> fetchGroup) {
|
||||
this.type = requireNonNull(type);
|
||||
this.db = requireNonNull(db);
|
||||
this.fetchGroup = requireNonNull(fetchGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> apply(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return db.find(type)
|
||||
.select(fetchGroup)
|
||||
.setUnmodifiable(true)
|
||||
.where().idIn(ids)
|
||||
.findMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingBuilder<T> implements ImmutableCacheBuilder<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private Function<Set<Object>, Map<Object, T>> loader;
|
||||
private int maxSize;
|
||||
private int maxIdleSeconds;
|
||||
private int maxSecondsToLive;
|
||||
|
||||
private LoadingBuilder(Class<T> type) {
|
||||
this.type = requireNonNull(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.loader = requireNonNull(loader);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup) {
|
||||
this.loader = new QueryLoader<>(type, db, fetchGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSize(int maxSize) {
|
||||
this.maxSize = maxSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds) {
|
||||
this.maxIdleSeconds = maxIdleSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive) {
|
||||
this.maxSecondsToLive = maxSecondsToLive;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImmutableBeanCache<T> build() {
|
||||
if (loader == null) {
|
||||
throw new IllegalStateException("No loader defined. Call loader(...) or loading(...) before build().");
|
||||
}
|
||||
if (maxSize > 0 || maxIdleSeconds > 0 || maxSecondsToLive > 0) {
|
||||
throw new IllegalStateException("Cache policy options require SpiImmutableCacheFactory (ebean-core).");
|
||||
}
|
||||
return new LoadingCache<>(type, loader);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LoadingCache<T> implements ImmutableBeanCache<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
private final Function<Set<Object>, Map<Object, T>> loader;
|
||||
private final ConcurrentHashMap<Object, T> cache = new ConcurrentHashMap<>();
|
||||
private final Set<Object> misses = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private LoadingCache(Class<T> type, Function<Set<Object>, Map<Object, T>> loader) {
|
||||
this.type = requireNonNull(type);
|
||||
this.loader = requireNonNull(loader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<T> type() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T getIfPresent(Object id) {
|
||||
return cache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, T> getAll(Set<Object> ids) {
|
||||
if (ids.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Set<Object> loadIds = null;
|
||||
for (Object id : ids) {
|
||||
if (!cache.containsKey(id) && !misses.contains(id)) {
|
||||
if (loadIds == null) {
|
||||
loadIds = new LinkedHashSet<>();
|
||||
}
|
||||
loadIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadIds != null && !loadIds.isEmpty()) {
|
||||
Map<Object, T> loaded = loader.apply(loadIds);
|
||||
if (loaded == null) {
|
||||
loaded = Collections.emptyMap();
|
||||
}
|
||||
for (Map.Entry<Object, T> entry : loaded.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
cache.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
for (Object id : loadIds) {
|
||||
if (!cache.containsKey(id)) {
|
||||
misses.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Object, T> result = new LinkedHashMap<>();
|
||||
for (Object id : ids) {
|
||||
T bean = cache.get(id);
|
||||
if (bean != null) {
|
||||
result.put(id, bean);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Builder for creating {@link ImmutableBeanCache} instances.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FetchGroup<MyRef> fetchGroup = FetchGroup.of(MyRef.class)
|
||||
* .select("version")
|
||||
* .fetch("names", "locale,text")
|
||||
* .build();
|
||||
*
|
||||
* ImmutableBeanCache<MyRef> cache = ImmutableBeanCaches.builder(MyRef.class)
|
||||
* .loading(database, fetchGroup)
|
||||
* .maxSize(10_000)
|
||||
* .maxIdleSeconds(300)
|
||||
* .maxSecondsToLive(1_800)
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* @see ImmutableBeanCaches#builder(Class)
|
||||
*/
|
||||
@NullMarked
|
||||
public interface ImmutableCacheBuilder<T> {
|
||||
|
||||
/**
|
||||
* Set the batch loader used for unresolved ids.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loader(Function<Set<Object>, Map<Object, T>> loader);
|
||||
|
||||
/**
|
||||
* Configure a query-based loader using the given database and fetch group.
|
||||
*/
|
||||
ImmutableCacheBuilder<T> loading(Database db, FetchGroup<T> fetchGroup);
|
||||
|
||||
/**
|
||||
* Configure max cache size (0 means unbounded).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSize(int maxSize);
|
||||
|
||||
/**
|
||||
* Configure max idle time in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxIdleSeconds(int maxIdleSeconds);
|
||||
|
||||
/**
|
||||
* Configure max time-to-live in seconds (0 means disabled).
|
||||
*/
|
||||
ImmutableCacheBuilder<T> maxSecondsToLive(int maxSecondsToLive);
|
||||
|
||||
/**
|
||||
* Build the immutable bean cache.
|
||||
*/
|
||||
ImmutableBeanCache<T> build();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Options to be used with insert such as ON CONFLICT DO UPDATE | NOTHING.
|
||||
|
||||
@@ -61,7 +61,7 @@ package io.ebean;
|
||||
* .eq("status", Customer.Status.ACTIVE)
|
||||
* .gt("id", 0)
|
||||
* .endAnd()
|
||||
* .orderBy().asc("name");
|
||||
* .order().asc("name");
|
||||
*
|
||||
* q.findList();
|
||||
* String s = q.getGeneratedSql();
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
/**
|
||||
* Thrown when trying to access a property that isn't loaded on an entity
|
||||
* that is unmodifiable or has disabled lazy loading.
|
||||
* <p>
|
||||
* On a normal mutable entity accessing the property would invoke lazy loading. On
|
||||
* a unmodifiable entity with lazy loading disabled, accessing an unloaded property
|
||||
* throws this LazyInitialisationException instead.
|
||||
*/
|
||||
public class LazyInitialisationException extends BeanAccessException {
|
||||
|
||||
/**
|
||||
* Create specifying the property that was being accessed.
|
||||
*/
|
||||
public LazyInitialisationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,4 @@ public interface ModifyAwareType {
|
||||
*/
|
||||
void setMarkedDirty(boolean markedDirty);
|
||||
|
||||
default Object freeze() {
|
||||
return this; // throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ import java.util.Objects;
|
||||
/**
|
||||
* Represents an Order By for a Query.
|
||||
* <p>
|
||||
* Is an ordered list of OrderBy.Property objects each specifying a property and
|
||||
* Is a ordered list of OrderBy.Property objects each specifying a property and
|
||||
* whether it is ascending or descending order.
|
||||
* </p>
|
||||
* <p>
|
||||
* Typically, you will not construct an OrderBy yourself but use one that exists
|
||||
* Typically you will not construct an OrderBy yourself but use one that exists
|
||||
* on the Query object.
|
||||
* </p>
|
||||
*/
|
||||
public class OrderBy<T> implements Serializable {
|
||||
|
||||
@@ -23,22 +25,8 @@ public class OrderBy<T> implements Serializable {
|
||||
private final List<Property> list;
|
||||
|
||||
/**
|
||||
* Create an OrderBy parsing the given order by clause.
|
||||
* <p>
|
||||
* The order by clause follows SQL order by clause with comma's between each
|
||||
* property and optionally "asc" or "desc" to represent ascending or
|
||||
* descending order respectively.
|
||||
*/
|
||||
public static <P> OrderBy<P> of(String orderByClause) {
|
||||
return new OrderBy<>(orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed from public API.
|
||||
* <p>
|
||||
* Create an empty OrderBy with no associated query.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy() {
|
||||
this.list = new ArrayList<>(3);
|
||||
}
|
||||
@@ -48,17 +36,20 @@ public class OrderBy<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link OrderBy#of(String)}.
|
||||
* Create an orderBy parsing the order by clause.
|
||||
* <p>
|
||||
* The order by clause follows SQL order by clause with comma's between each
|
||||
* property and optionally "asc" or "desc" to represent ascending or
|
||||
* descending order respectively.
|
||||
* </p>
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy(String orderByClause) {
|
||||
this(null, orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will be removed from public API.
|
||||
* Construct with a given query and order by clause.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy(Query<T> query, String orderByClause) {
|
||||
this.query = query;
|
||||
this.list = new ArrayList<>(3);
|
||||
@@ -119,11 +110,8 @@ public class OrderBy<T> implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This method will become internal only API.
|
||||
* <p>
|
||||
* Return a copy of this OrderBy with the path trimmed.
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public OrderBy<T> copyWithTrim(String path) {
|
||||
List<Property> newList = new ArrayList<>(list.size());
|
||||
for (Property aList : list) {
|
||||
@@ -198,15 +186,15 @@ public class OrderBy<T> implements Serializable {
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
var append = new StringAppend();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Property property = list.get(i);
|
||||
if (i > 0) {
|
||||
append.append(", ");
|
||||
sb.append(", ");
|
||||
}
|
||||
property.toStringFormat(append);
|
||||
sb.append(property.toStringFormat());
|
||||
}
|
||||
return append.toString();
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -243,55 +231,6 @@ public class OrderBy<T> implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the order by clause.
|
||||
*/
|
||||
public interface Append {
|
||||
|
||||
/**
|
||||
* Append a property expression.
|
||||
*/
|
||||
Append property(String property);
|
||||
|
||||
/**
|
||||
* Append a literal.
|
||||
*/
|
||||
Append append(String literal);
|
||||
|
||||
/**
|
||||
* Parse and append an expression.
|
||||
*/
|
||||
Append parse(String expression);
|
||||
}
|
||||
|
||||
private static final class StringAppend implements Append {
|
||||
|
||||
private final StringBuilder builder = new StringBuilder();
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append property(String property) {
|
||||
builder.append(property);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append append(String literal) {
|
||||
builder.append(literal);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Append parse(String raw) {
|
||||
builder.append(raw);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A property and its ascending descending order.
|
||||
*/
|
||||
@@ -370,25 +309,36 @@ public class OrderBy<T> implements Serializable {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return property;
|
||||
return toStringFormat();
|
||||
}
|
||||
|
||||
public void toStringFormat(Append append) {
|
||||
if (collation != null) {
|
||||
if (collation.contains("${}")) {
|
||||
// this is a complex collation, e.g. DB2 - we must replace the property
|
||||
append.parse(collation.replace("${}", property));
|
||||
public String toStringFormat() {
|
||||
if (nulls == null && collation == null) {
|
||||
if (ascending) {
|
||||
return property;
|
||||
} else {
|
||||
append.property(property).append(" collate ").append(collation);
|
||||
return property + " desc";
|
||||
}
|
||||
} else {
|
||||
append.property(property);
|
||||
}
|
||||
if (!ascending) {
|
||||
append.append(" desc");
|
||||
}
|
||||
if (nulls != null) {
|
||||
append.append(" ").append(nulls).append(" ").append(highLow);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collation != null) {
|
||||
if (collation.contains("${}")) {
|
||||
// this is a complex collation, e.g. DB2 - we must replace the property
|
||||
sb.append(collation.replace("${}", property));
|
||||
} else {
|
||||
sb.append(property);
|
||||
sb.append(" collate ").append(collation);
|
||||
}
|
||||
} else {
|
||||
sb.append(property);
|
||||
}
|
||||
if (!ascending) {
|
||||
sb.append(' ').append("desc");
|
||||
}
|
||||
if (nulls != null) {
|
||||
sb.append(' ').append(nulls).append(' ').append(highLow);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.concurrent.Future;
|
||||
*
|
||||
* PagedList<Order> pagedList = DB.find(Order.class)
|
||||
* .where().eq("status", Order.Status.NEW)
|
||||
* .orderBy().asc("id")
|
||||
* .order().asc("id")
|
||||
* .setFirstRow(0)
|
||||
* .setMaxRows(50)
|
||||
* .findPagedList();
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Used to specify Paging on a Query as an alternative to setting each of the
|
||||
* maxRows, firstRow and orderBy clause via:
|
||||
* {@link Query#setMaxRows(int)} + {@link Query#setFirstRow(int)} + {@link Query#setOrderBy(OrderBy)}.
|
||||
* <p>
|
||||
* Example use:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* var orderBy = OrderBy.of("lastName desc nulls first, firstName asc");
|
||||
* var paging = Paging.of(0, 100, orderBy);
|
||||
*
|
||||
* new QCustomer()
|
||||
* .name.isNotNull()
|
||||
* .setPaging(paging)
|
||||
* .findList();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
public interface Paging {
|
||||
|
||||
/**
|
||||
* Create a Paging with the given page index size and orderBy.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
* @param orderBy order by for the query result
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize, @Nullable OrderBy<?> orderBy) {
|
||||
return DPaging.build(pageIndex, pageSize, orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Paging with a raw order by clause.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
* @param orderByClause raw order by clause for ordering the query result
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize, @Nullable String orderByClause) {
|
||||
return of(pageIndex, pageSize, OrderBy.of(orderByClause));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Paging that will use the id property for ordering.
|
||||
*
|
||||
* @param pageIndex the page index starting from zero
|
||||
* @param pageSize the page size (effectively max rows)
|
||||
*/
|
||||
static Paging of(int pageIndex, int pageSize) {
|
||||
return DPaging.build(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Paging that will not apply any pagination to a query.
|
||||
*/
|
||||
static Paging ofNone() {
|
||||
return DPaging.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the page index.
|
||||
*/
|
||||
int pageIndex();
|
||||
|
||||
/**
|
||||
* Return the page size.
|
||||
*/
|
||||
int pageSize();
|
||||
|
||||
/**
|
||||
* Return the order by.
|
||||
*/
|
||||
OrderBy<?> orderBy();
|
||||
|
||||
/**
|
||||
* Return a Paging using the given page index.
|
||||
*/
|
||||
Paging withPage(int pageIndex);
|
||||
|
||||
/**
|
||||
* Return a Paging using the given order by clause.
|
||||
*/
|
||||
Paging withOrderBy(String orderByClause);
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import java.util.Objects;
|
||||
* .where()
|
||||
* .eq("store", "def")
|
||||
* .inPairs(pairs) // IN clause with 'pairs' of values
|
||||
* .orderBy("sku desc")
|
||||
* .order("sku desc")
|
||||
*
|
||||
* // query expressions cover the natural key properties
|
||||
* // so we can choose to hit the L2 bean cache if we want
|
||||
|
||||
@@ -13,21 +13,21 @@ public interface ProfileLocation {
|
||||
* Create and return a new ProfileLocation.
|
||||
*/
|
||||
static ProfileLocation create() {
|
||||
return XBootstrapService.profileLocationFactory().create();
|
||||
return XServiceProvider.profileLocationFactory().create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new ProfileLocation with line number.
|
||||
*/
|
||||
static ProfileLocation createWithLine() {
|
||||
return XBootstrapService.profileLocationFactory().createWithLine();
|
||||
return XServiceProvider.profileLocationFactory().createWithLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new ProfileLocation with a given lineNumber and label.
|
||||
*/
|
||||
static ProfileLocation create(String label) {
|
||||
return XBootstrapService.profileLocationFactory().create(label);
|
||||
return XServiceProvider.profileLocationFactory().create(label);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.NonNullApi;
|
||||
|
||||
/**
|
||||
* Object relational query for finding a List, Set, Map or single entity bean.
|
||||
@@ -14,7 +13,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* .where()
|
||||
* .like("customer.name","rob%")
|
||||
* .gt("orderDate",lastWeek)
|
||||
* .orderBy("customer.id, id desc")
|
||||
* .order("customer.id, id desc")
|
||||
* .setMaxRows(50)
|
||||
* .findList();
|
||||
*
|
||||
@@ -150,7 +149,7 @@ import org.jspecify.annotations.Nullable;
|
||||
*
|
||||
* @param <T> the type of Entity bean this query will fetch.
|
||||
*/
|
||||
@NullMarked
|
||||
@NonNullApi
|
||||
public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
|
||||
/**
|
||||
@@ -219,6 +218,29 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
*/
|
||||
boolean isCountDistinct();
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #usingTransaction(Transaction)} then delete().
|
||||
* <p>
|
||||
* Execute as a delete query returning the number of rows deleted using the given transaction.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
*
|
||||
* @return the number of beans/rows that were deleted.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "14.1.0")
|
||||
int delete(Transaction transaction);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #usingTransaction(Transaction)} then update().
|
||||
* <p>
|
||||
* Execute the UpdateQuery returning the number of rows updated using the given transaction.
|
||||
*
|
||||
* @return the number of beans/rows updated.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "14.1.0")
|
||||
int update(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute the UpdateQuery returning the number of rows updated.
|
||||
*
|
||||
@@ -311,7 +333,6 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
/**
|
||||
* Return the Id value.
|
||||
*/
|
||||
@Nullable
|
||||
Object getId();
|
||||
|
||||
/**
|
||||
@@ -419,6 +440,30 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
*/
|
||||
Query<T> having(Expression addExpressionToHaving);
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy()}.
|
||||
*/
|
||||
@Deprecated(since = "13.19", forRemoval = true)
|
||||
default Query<T> order(String orderByClause) {
|
||||
return orderBy(orderByClause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #orderBy()}.
|
||||
*/
|
||||
@Deprecated(since = "13.19", forRemoval = true)
|
||||
default OrderBy<T> order() {
|
||||
return orderBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated migrate to {@link #setOrderBy(OrderBy)}.
|
||||
*/
|
||||
@Deprecated(since = "13.19", forRemoval = true)
|
||||
default Query<T> setOrder(OrderBy<T> orderBy) {
|
||||
return setOrderBy(orderBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the OrderBy so that you can append an ascending or descending
|
||||
* property to the order by clause.
|
||||
@@ -448,13 +493,11 @@ public interface Query<T> extends CancelableQuery, QueryBuilder<Query<T>, T> {
|
||||
/**
|
||||
* Return the "for update" wait mode to use.
|
||||
*/
|
||||
@Nullable
|
||||
LockWait getForUpdateLockWait();
|
||||
|
||||
/**
|
||||
* Return the lock type (strength) to use with "for update".
|
||||
*/
|
||||
@Nullable
|
||||
LockType getForUpdateLockType();
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ebean;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import io.avaje.lang.Nullable;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
@@ -20,20 +20,13 @@ import java.util.stream.Stream;
|
||||
* @param <SELF> The type of the builder
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends QueryBuilderProjection<SELF, T> {
|
||||
public interface QueryBuilder<SELF, T> extends QueryBuilderProjection<SELF, T> {
|
||||
|
||||
/**
|
||||
* Set root table alias.
|
||||
*/
|
||||
SELF alias(String alias);
|
||||
|
||||
/**
|
||||
* Apply changes to the query using a function.
|
||||
*
|
||||
* @param apply Function that applies changes to the query.
|
||||
*/
|
||||
SELF also(Consumer<SELF> apply);
|
||||
|
||||
/**
|
||||
* Apply changes to the query conditional on the supplied predicate.
|
||||
* <p>
|
||||
@@ -44,16 +37,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF alsoIf(BooleanSupplier predicate, Consumer<SELF> apply);
|
||||
|
||||
/**
|
||||
* Apply changes to the query when the supplied value is non-null.
|
||||
* <p>
|
||||
* Typically, the changes are extra predicates etc.
|
||||
*
|
||||
* @param value The value which when non-null the changes are applied
|
||||
* @param apply The changes to apply to the query
|
||||
*/
|
||||
SELF alsoIfPresent(@Nullable Object value, Consumer<SELF> apply);
|
||||
|
||||
/**
|
||||
* Perform an 'As of' query using history tables to return the object graph
|
||||
* as of a time in the past.
|
||||
@@ -125,11 +108,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF usingTransaction(Transaction transaction);
|
||||
|
||||
/**
|
||||
* Execute this query using immutable bean cache values for matching bean types.
|
||||
*/
|
||||
SELF using(ImmutableBeanCache<?> beanCache);
|
||||
|
||||
/**
|
||||
* Execute the query using the given connection.
|
||||
*/
|
||||
@@ -150,17 +128,7 @@ 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.
|
||||
*/
|
||||
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);
|
||||
SELF usingMaster();
|
||||
|
||||
/**
|
||||
* Set the base table to use for this query.
|
||||
@@ -349,8 +317,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
SELF setMaxRows(int maxRows);
|
||||
|
||||
SELF setPaging(Paging paging);
|
||||
|
||||
/**
|
||||
* Set RawSql to use for this query.
|
||||
*/
|
||||
@@ -413,19 +379,9 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
SELF setUseDocStore(boolean useDocStore);
|
||||
|
||||
/**
|
||||
* When set to true when you want the returned beans to be unmodifiable read only.
|
||||
* <p>
|
||||
* This means that the returning graph can't be mutated via setters, all the collections
|
||||
* are unmodifiable collections, lazy loading is disabled and that the query uses
|
||||
* {@link PersistenceContextScope#QUERY}.
|
||||
* <p>
|
||||
* Attempting to mutate an unmodifiable bean will throw a <code>UnmodifiableEntityException</code>.
|
||||
* Attempting to load an unloaded property will throw a <code>LazyInitialisationException</code>
|
||||
*
|
||||
* @see LazyInitialisationException
|
||||
* @see UnmodifiableEntityException
|
||||
* When set to true when you want the returned beans to be read only.
|
||||
*/
|
||||
SELF setUnmodifiable(boolean unmodifiable);
|
||||
SELF setReadOnly(boolean readOnly);
|
||||
|
||||
/**
|
||||
* Set a timeout on this query.
|
||||
@@ -472,16 +428,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
|
||||
/**
|
||||
* Set the {@link CacheMode} to use the query for executing this query.
|
||||
* <p>
|
||||
* Since version 16.x using the query bean will set the query to use unmodifiable (see
|
||||
* {@link #setUnmodifiable(boolean)}) so the returned object graph is unmodifiable
|
||||
* and safe to cache by the application.
|
||||
* <p>
|
||||
* Attempting to mutate an unmodifiable bean will throw a <code>UnmodifiableEntityException</code>.
|
||||
* Attempting to load an unloaded property will throw a <code>LazyInitialisationException</code>
|
||||
*
|
||||
* @see LazyInitialisationException
|
||||
* @see UnmodifiableEntityException
|
||||
*/
|
||||
SELF setUseQueryCache(CacheMode cacheMode);
|
||||
|
||||
@@ -607,21 +553,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
int delete();
|
||||
|
||||
/**
|
||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
||||
* predicates in the query without soft delete.
|
||||
* <p>
|
||||
* This is the same as {@link #delete()} except that when the bean type uses soft delete
|
||||
* (e.g. {@code @SoftDelete}) the matching rows are permanently (hard) deleted rather than
|
||||
* being marked as deleted.
|
||||
* <p>
|
||||
* Note that if the query includes joins then the generated delete statement may not be
|
||||
* optimal depending on the database platform.
|
||||
*
|
||||
* @return the number of beans/rows that were permanently deleted.
|
||||
*/
|
||||
int deletePermanent();
|
||||
|
||||
/**
|
||||
* Execute the query returning true if a row is found.
|
||||
* <p>
|
||||
@@ -1012,16 +943,6 @@ public interface QueryBuilder<SELF extends QueryBuilder<SELF, T>, T> extends Que
|
||||
*/
|
||||
FutureList<T> findFutureList();
|
||||
|
||||
/**
|
||||
* Execute find map query in a background thread.
|
||||
* <p>
|
||||
* This query will execute in it's own PersistenceContext and using its own transaction.
|
||||
* What that means is that it will not share any bean instances with other queries.
|
||||
*
|
||||
* @return a Future object for the map result of the query
|
||||
*/
|
||||
<K> FutureMap<K,T> findFutureMap();
|
||||
|
||||
/**
|
||||
* Return a PagedList for this query using firstRow and maxRows.
|
||||
* <p>
|
||||
|
||||
@@ -6,7 +6,7 @@ package io.ebean;
|
||||
* @param <SELF> The builder type
|
||||
* @param <T> The entity bean type
|
||||
*/
|
||||
public interface QueryBuilderProjection<SELF extends QueryBuilderProjection<SELF, T>, T> {
|
||||
public interface QueryBuilderProjection<SELF, T> {
|
||||
|
||||
/**
|
||||
* Apply the path properties replacing the select and fetch clauses.
|
||||
@@ -40,13 +40,6 @@ public interface QueryBuilderProjection<SELF extends QueryBuilderProjection<SELF
|
||||
*/
|
||||
SELF select(String fetchProperties);
|
||||
|
||||
/**
|
||||
* Set DISTINCT ON clause. This is a Postgres only SQL feature.
|
||||
*
|
||||
* @param distinctOn The properties to include in the DISTINCT ON clause.
|
||||
*/
|
||||
SELF distinctOn(String distinctOn);
|
||||
|
||||
/**
|
||||
* Apply the fetchGroup which defines what part of the object graph to load.
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.Iterator;
|
||||
*
|
||||
* Query<Customer> query = database.find(Customer.class)
|
||||
* .where().gt("id", 0)
|
||||
* .orderBy("id")
|
||||
* .order("id")
|
||||
* .setMaxRows(2);
|
||||
*
|
||||
* QueryIterator<Customer> it = query.findIterate();
|
||||
|
||||
@@ -111,7 +111,7 @@ package io.ebean;
|
||||
* .fetch("order.customer", "name")
|
||||
* .where().gt("order.id", 0)
|
||||
* .having().gt("totalAmount", 20)
|
||||
* .orderBy().desc("totalAmount")
|
||||
* .order().desc("totalAmount")
|
||||
* .setMaxRows(10)
|
||||
* .findList();
|
||||
*
|
||||
|
||||
@@ -22,14 +22,14 @@ public interface RawSqlBuilder {
|
||||
* resultSet.
|
||||
*/
|
||||
static RawSql resultSet(ResultSet resultSet, String... propertyNames) {
|
||||
return XBootstrapService.rawSql().resultSet(resultSet, propertyNames);
|
||||
return XServiceProvider.rawSql().resultSet(resultSet, propertyNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a SqlRow based on the resultSet with dbTrueValue and binaryOptimizedUUID options.
|
||||
*/
|
||||
static SqlRow sqlRow(ResultSet resultSet, final String dbTrueValue, boolean binaryOptimizedUUID) throws SQLException {
|
||||
return XBootstrapService.rawSql().sqlRow(resultSet, dbTrueValue, binaryOptimizedUUID);
|
||||
return XServiceProvider.rawSql().sqlRow(resultSet, dbTrueValue, binaryOptimizedUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,50 +38,7 @@ public interface RawSqlBuilder {
|
||||
* this query.
|
||||
*/
|
||||
static RawSqlBuilder unparsed(String sql) {
|
||||
return XBootstrapService.rawSql().unparsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RawSqlBuilder for SQL containing {@code ${where}}, {@code ${having}} and/or
|
||||
* {@code ${orderBy}} placeholder(s). Unlike {@link #parse(String)} this does NOT attempt to parse
|
||||
* the SELECT columns, so it supports complex SQL such as CTEs, subqueries, and window functions.
|
||||
* <p>
|
||||
* Explicit column mappings must be provided (as with {@link #unparsed(String)}), but
|
||||
* WHERE, HAVING and ORDER BY expressions can be added dynamically via the query API - provided
|
||||
* the corresponding placeholder is present in the SQL. If a query calls {@code .orderBy(...)}
|
||||
* on a template with no {@code ${orderBy}}/{@code ${andOrderBy}} placeholder, that ordering is
|
||||
* ignored (there is no injection point for it) rather than producing invalid SQL.
|
||||
* </p>
|
||||
* <p>
|
||||
* Available placeholders:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@code ${where}} / {@code ${andWhere}} - inject "where <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${having}} / {@code ${andHaving}} - inject "having <expr>" / "and <expr>"</li>
|
||||
* <li>{@code ${orderBy}} / {@code ${andOrderBy}} - inject "order by <expr>" / ", <expr>"</li>
|
||||
* </ul>
|
||||
* <h3>Example:</h3>
|
||||
* <pre>{@code
|
||||
*
|
||||
* String sql = """
|
||||
* with agg as (
|
||||
* select company_id, sum(amount) as total
|
||||
* from orders
|
||||
* ${where}
|
||||
* group by company_id
|
||||
* )
|
||||
* select company_id, total from agg ${orderBy}
|
||||
* """;
|
||||
*
|
||||
* RawSql rawSql = RawSqlBuilder.withPlaceholders(sql)
|
||||
* .columnMapping("company_id", "companyId")
|
||||
* .columnMapping("total", "total")
|
||||
* .create();
|
||||
*
|
||||
* }</pre>
|
||||
*/
|
||||
static RawSqlBuilder withPlaceholders(String sql) {
|
||||
return XBootstrapService.rawSql().withPlaceholders(sql);
|
||||
return XServiceProvider.rawSql().unparsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +55,7 @@ public interface RawSqlBuilder {
|
||||
* </p>
|
||||
*/
|
||||
static RawSqlBuilder parse(String sql) {
|
||||
return XBootstrapService.rawSql().parsed(sql);
|
||||
return XServiceProvider.rawSql().parsed(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user